Merge remote-tracking branch 'origin/develop' into renovate/eslint-8.x

renovate/lock-file-maintenance
Alex Gleason 2023-01-05 11:48:29 -06:00
commit bce33db154
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
78 zmienionych plików z 778 dodań i 2790 usunięć

Wyświetl plik

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ServiceWorker: don't serve favicon, robots.txt, and others from ServiceWorker.
- Datepicker: correctly default to the current year.
- Scheduled posts: fix page crashing on deleting a scheduled post.
- Events: don't crash when searching for a location.
## [3.0.0] - 2022-12-25

Wyświetl plik

@ -2,7 +2,9 @@ import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { AccountRecord } from 'soapbox/normalizers';
import { AuthUserRecord, ReducerRecord } from '../../reducers/auth';
import {
fetchMe, patchMe,
} from '../me';
@ -38,18 +40,18 @@ describe('fetchMe()', () => {
beforeEach(() => {
const state = rootState
.set('auth', ImmutableMap({
.set('auth', ReducerRecord({
me: accountUrl,
users: ImmutableMap({
[accountUrl]: ImmutableMap({
[accountUrl]: AuthUserRecord({
'access_token': token,
}),
}),
}))
.set('accounts', ImmutableMap({
[accountUrl]: {
[accountUrl]: AccountRecord({
url: accountUrl,
},
}),
}) as any);
store = mockStore(state);
});
@ -112,4 +114,4 @@ describe('patchMe()', () => {
expect(actions).toEqual(expectedActions);
});
});
});
});

Wyświetl plik

@ -77,6 +77,16 @@ const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
const fetchConfig = () =>
@ -544,6 +554,50 @@ const unsuggestUsers = (accountIds: string[]) =>
});
};
const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query });
const fetchUserIndex = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const { filters, page, query, pageSize, isLoading } = getState().admin_user_index;
if (isLoading) return;
dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST });
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize))
.then((data: any) => {
if (data.error) {
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
} else {
const { users, count, next } = (data);
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next });
}
}).catch(() => {
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
});
};
const expandUserIndex = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const { filters, page, query, pageSize, isLoading, next, loaded } = getState().admin_user_index;
if (!loaded || isLoading) return;
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next))
.then((data: any) => {
if (data.error) {
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
} else {
const { users, count, next } = (data);
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next });
}
}).catch(() => {
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
});
};
export {
ADMIN_CONFIG_FETCH_REQUEST,
ADMIN_CONFIG_FETCH_SUCCESS,
@ -596,6 +650,13 @@ export {
ADMIN_USERS_UNSUGGEST_REQUEST,
ADMIN_USERS_UNSUGGEST_SUCCESS,
ADMIN_USERS_UNSUGGEST_FAIL,
ADMIN_USER_INDEX_EXPAND_FAIL,
ADMIN_USER_INDEX_EXPAND_REQUEST,
ADMIN_USER_INDEX_EXPAND_SUCCESS,
ADMIN_USER_INDEX_FETCH_FAIL,
ADMIN_USER_INDEX_FETCH_REQUEST,
ADMIN_USER_INDEX_FETCH_SUCCESS,
ADMIN_USER_INDEX_QUERY_SET,
fetchConfig,
updateConfig,
updateSoapboxConfig,
@ -622,4 +683,7 @@ export {
setRole,
suggestUsers,
unsuggestUsers,
setUserIndexQuery,
fetchUserIndex,
expandUserIndex,
};

Wyświetl plik

@ -29,7 +29,6 @@ import api, { baseClient } from '../api';
import { importFetchedAccount } from './importer';
import type { AxiosError } from 'axios';
import type { Map as ImmutableMap } from 'immutable';
import type { AppDispatch, RootState } from 'soapbox/store';
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
@ -94,11 +93,11 @@ const createAuthApp = () =>
const createAppToken = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const app = getState().auth.get('app');
const app = getState().auth.app;
const params = {
client_id: app.get('client_id'),
client_secret: app.get('client_secret'),
client_id: app.client_id!,
client_secret: app.client_secret!,
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
grant_type: 'client_credentials',
scope: getScopes(getState()),
@ -111,11 +110,11 @@ const createAppToken = () =>
const createUserToken = (username: string, password: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const app = getState().auth.get('app');
const app = getState().auth.app;
const params = {
client_id: app.get('client_id'),
client_secret: app.get('client_secret'),
client_id: app.client_id!,
client_secret: app.client_secret!,
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
grant_type: 'password',
username: username,
@ -127,32 +126,12 @@ const createUserToken = (username: string, password: string) =>
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
};
export const refreshUserToken = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const refreshToken = getState().auth.getIn(['user', 'refresh_token']);
const app = getState().auth.get('app');
if (!refreshToken) return dispatch(noOp);
const params = {
client_id: app.get('client_id'),
client_secret: app.get('client_secret'),
refresh_token: refreshToken,
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
grant_type: 'refresh_token',
scope: getScopes(getState()),
};
return dispatch(obtainOAuthToken(params))
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
};
export const otpVerify = (code: string, mfa_token: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const app = getState().auth.get('app');
const app = getState().auth.app;
return api(getState, 'app').post('/oauth/mfa/challenge', {
client_id: app.get('client_id'),
client_secret: app.get('client_secret'),
client_id: app.client_id,
client_secret: app.client_secret,
mfa_token: mfa_token,
code: code,
challenge_type: 'totp',
@ -211,7 +190,7 @@ export const logIn = (username: string, password: string) =>
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
return dispatch(createUserToken(normalizeUsername(username), password));
}).catch((error: AxiosError) => {
if ((error.response?.data as any).error === 'mfa_required') {
if ((error.response?.data as any)?.error === 'mfa_required') {
// If MFA is required, throw the error and handle it in the component.
throw error;
} else {
@ -233,9 +212,9 @@ export const logOut = () =>
if (!account) return dispatch(noOp);
const params = {
client_id: state.auth.getIn(['app', 'client_id']),
client_secret: state.auth.getIn(['app', 'client_secret']),
token: state.auth.getIn(['users', account.url, 'access_token']),
client_id: state.auth.app.client_id!,
client_secret: state.auth.app.client_secret!,
token: state.auth.users.get(account.url)!.access_token,
};
return dispatch(revokeOAuthToken(params))
@ -263,10 +242,10 @@ export const switchAccount = (accountId: string, background = false) =>
export const fetchOwnAccounts = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
return state.auth.get('users').forEach((user: ImmutableMap<string, string>) => {
const account = state.accounts.get(user.get('id'));
return state.auth.users.forEach((user) => {
const account = state.accounts.get(user.id);
if (!account) {
dispatch(verifyCredentials(user.get('access_token')!, user.get('url')));
dispatch(verifyCredentials(user.access_token, user.url));
}
});
};

Wyświetl plik

@ -10,12 +10,12 @@ import api from '../api';
const getMeUrl = (state: RootState) => {
const me = state.me;
return state.accounts.getIn([me, 'url']);
return state.accounts.get(me)?.url;
};
/** Figure out the appropriate instance to fetch depending on the state */
export const getHost = (state: RootState) => {
const accountUrl = getMeUrl(state) || getAuthUserUrl(state);
const accountUrl = getMeUrl(state) || getAuthUserUrl(state) as string;
try {
return new URL(accountUrl).host;

Wyświetl plik

@ -30,8 +30,8 @@ const getMeUrl = (state: RootState) => {
const getMeToken = (state: RootState) => {
// Fallback for upgrading IDs to URLs
const accountUrl = getMeUrl(state) || state.auth.get('me');
return state.auth.getIn(['users', accountUrl, 'access_token']);
const accountUrl = getMeUrl(state) || state.auth.me;
return state.auth.users.get(accountUrl!)?.access_token;
};
const fetchMe = () =>
@ -46,7 +46,7 @@ const fetchMe = () =>
}
dispatch(fetchMeRequest());
return dispatch(loadCredentials(token, accountUrl))
return dispatch(loadCredentials(token, accountUrl!))
.catch(error => dispatch(fetchMeFail(error)));
};

Wyświetl plik

@ -43,7 +43,7 @@ const maybeParseJSON = (data: string) => {
const getAuthBaseURL = createSelector([
(state: RootState, me: string | false | null) => state.accounts.getIn([me, 'url']),
(state: RootState, _me: string | false | null) => state.auth.get('me'),
(state: RootState, _me: string | false | null) => state.auth.me,
], (accountUrl, authUserUrl) => {
const baseURL = parseBaseURL(accountUrl) || parseBaseURL(authUserUrl);
return baseURL !== window.location.origin ? baseURL : '';

Wyświetl plik

@ -1,31 +0,0 @@
import React from 'react';
import { normalizeAccount } from 'soapbox/normalizers';
import { render, screen } from '../../jest/test-helpers';
import AvatarOverlay from '../avatar-overlay';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
describe('<AvatarOverlay', () => {
const account = normalizeAccount({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
}) as ReducerAccount;
const friend = normalizeAccount({
username: 'eve',
acct: 'eve@blackhat.lair',
display_name: 'Evelyn',
avatar: '/animated/eve.gif',
avatar_static: '/static/eve.jpg',
}) as ReducerAccount;
it('renders a overlay avatar', () => {
render(<AvatarOverlay account={account} friend={friend} />);
expect(screen.queryAllByRole('img')).toHaveLength(2);
});
});

Wyświetl plik

@ -1,38 +0,0 @@
import React from 'react';
import { normalizeAccount } from 'soapbox/normalizers';
import { render, screen } from '../../jest/test-helpers';
import Avatar from '../avatar';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
describe('<Avatar />', () => {
const account = normalizeAccount({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
}) as ReducerAccount;
const size = 100;
// describe('Autoplay', () => {
// it('renders an animated avatar', () => {
// render(<Avatar account={account} animate size={size} />);
// expect(screen.getByRole('img').getAttribute('src')).toBe(account.get('avatar'));
// });
// });
describe('Still', () => {
it('renders a still avatar', () => {
render(<Avatar account={account} size={size} />);
expect(screen.getByRole('img').getAttribute('src')).toBe(account.get('avatar'));
});
});
// TODO add autoplay test if possible
});

Wyświetl plik

@ -46,7 +46,7 @@ interface IProfilePopper {
const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }): any =>
condition ? wrapper(children) : children;
interface IAccount {
export interface IAccount {
account: AccountEntity,
action?: React.ReactElement,
actionAlignment?: 'center' | 'top',

Wyświetl plik

@ -32,7 +32,7 @@ const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {
<Icon src={ADDRESS_ICONS[location.type] || mapPinIcon} />
<Stack>
<Text>{location.description}</Text>
<Text size='xs' theme='muted'>{[location.street, location.locality, location.country].filter(val => val.trim()).join(' · ')}</Text>
<Text size='xs' theme='muted'>{[location.street, location.locality, location.country].filter(val => val?.trim()).join(' · ')}</Text>
</Stack>
</HStack>
);

Wyświetl plik

@ -1,19 +0,0 @@
import React from 'react';
import StillImage from 'soapbox/components/still-image';
import type { Account as AccountEntity } from 'soapbox/types/entities';
interface IAvatarOverlay {
account: AccountEntity,
friend: AccountEntity,
}
const AvatarOverlay: React.FC<IAvatarOverlay> = ({ account, friend }) => (
<div className='account__avatar-overlay'>
<StillImage src={account.avatar} className='account__avatar-overlay-base' />
<StillImage src={friend.avatar} className='account__avatar-overlay-overlay' />
</div>
);
export default AvatarOverlay;

Wyświetl plik

@ -1,38 +0,0 @@
import classNames from 'clsx';
import React from 'react';
import StillImage from 'soapbox/components/still-image';
import type { Account } from 'soapbox/types/entities';
interface IAvatar {
account?: Account | null,
size?: number,
className?: string,
}
/**
* Legacy avatar component.
* @see soapbox/components/ui/avatar/avatar.tsx
* @deprecated
*/
const Avatar: React.FC<IAvatar> = ({ account, size, className }) => {
if (!account) return null;
// : TODO : remove inline and change all avatars to be sized using css
const style: React.CSSProperties = !size ? {} : {
width: `${size}px`,
height: `${size}px`,
};
return (
<StillImage
className={classNames('rounded-full overflow-hidden', className)}
style={style}
src={account.avatar}
alt=''
/>
);
};
export default Avatar;

Wyświetl plik

@ -1,188 +0,0 @@
import classNames from 'clsx';
import PropTypes from 'prop-types';
import React from 'react';
import spring from 'react-motion/lib/spring';
import Icon from 'soapbox/components/icon';
import emojify from 'soapbox/features/emoji/emoji';
import Motion from '../features/ui/util/optional-motion';
export default class IconButton extends React.PureComponent {
static propTypes = {
className: PropTypes.string,
iconClassName: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string,
src: PropTypes.string,
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyPress: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
activeStyle: PropTypes.object,
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
text: PropTypes.string,
emoji: PropTypes.string,
type: PropTypes.string,
};
static defaultProps = {
size: 18,
active: false,
disabled: false,
animate: false,
overlay: false,
tabIndex: '0',
onKeyUp: () => {},
onKeyDown: () => {},
onClick: () => {},
onMouseEnter: () => {},
onMouseLeave: () => {},
type: 'button',
};
handleClick = (e) => {
e.preventDefault();
if (!this.props.disabled) {
this.props.onClick(e);
}
}
handleMouseDown = (e) => {
if (!this.props.disabled && this.props.onMouseDown) {
this.props.onMouseDown(e);
}
}
handleKeyDown = (e) => {
if (!this.props.disabled && this.props.onKeyDown) {
this.props.onKeyDown(e);
}
}
handleKeyUp = (e) => {
if (!this.props.disabled && this.props.onKeyUp) {
this.props.onKeyUp(e);
}
}
handleKeyPress = (e) => {
if (this.props.onKeyPress && !this.props.disabled) {
this.props.onKeyPress(e);
}
}
render() {
const style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
...(this.props.active ? this.props.activeStyle : {}),
};
const {
active,
animate,
className,
iconClassName,
disabled,
expanded,
icon,
src,
inverted,
overlay,
pressed,
tabIndex,
title,
text,
emoji,
type,
} = this.props;
const classes = classNames(className, 'icon-button', {
active,
disabled,
inverted,
overlayed: overlay,
});
if (!animate) {
// Perf optimization: avoid unnecessary <Motion> components unless
// we actually need to animate.
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onKeyPress={this.handleKeyPress}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
tabIndex={tabIndex}
disabled={disabled}
type={type}
>
<div style={src ? {} : style}>
{emoji
? <div className='icon-button__emoji' dangerouslySetInnerHTML={{ __html: emojify(emoji) }} aria-hidden='true' />
: <Icon className={iconClassName} id={icon} src={src} fixedWidth aria-hidden='true' />}
</div>
{text && <span className='icon-button__text'>{text}</span>}
</button>
);
}
return (
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) => (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onKeyPress={this.handleKeyPress}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
tabIndex={tabIndex}
disabled={disabled}
type={type}
>
<div style={src ? {} : style}>
{emoji
? <div className='icon-button__emoji' style={{ transform: `rotate(${rotate}deg)` }} dangerouslySetInnerHTML={{ __html: emojify(emoji) }} aria-hidden='true' />
: <Icon className={iconClassName} id={icon} src={src} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />}
</div>
{text && <span className='icon-button__text'>{text}</span>}
</button>
)}
</Motion>
);
}
}

Wyświetl plik

@ -0,0 +1,100 @@
import classNames from 'clsx';
import React from 'react';
import Icon from 'soapbox/components/icon';
interface IIconButton extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'disabled' | 'onClick' | 'onKeyDown' | 'onKeyPress' | 'onKeyUp' | 'onMouseDown' | 'onMouseEnter' | 'onMouseLeave' | 'tabIndex' | 'title'> {
active?: boolean
expanded?: boolean
iconClassName?: string
pressed?: boolean
size?: number
src: string
text?: React.ReactNode
}
const IconButton: React.FC<IIconButton> = ({
active,
className,
disabled,
expanded,
iconClassName,
onClick,
onKeyDown,
onKeyUp,
onKeyPress,
onMouseDown,
onMouseEnter,
onMouseLeave,
pressed,
size = 18,
src,
tabIndex = 0,
text,
title,
}) => {
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
if (!disabled && onClick) {
onClick(e);
}
};
const handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
if (!disabled && onMouseDown) {
onMouseDown(e);
}
};
const handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
if (!disabled && onKeyDown) {
onKeyDown(e);
}
};
const handleKeyUp: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
if (!disabled && onKeyUp) {
onKeyUp(e);
}
};
const handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
if (onKeyPress && !disabled) {
onKeyPress(e);
}
};
const classes = classNames(className, 'icon-button', {
active,
disabled,
});
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={handleClick}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onKeyPress={handleKeyPress}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
tabIndex={tabIndex}
disabled={disabled}
type='button'
>
<div>
<Icon className={iconClassName} src={src} fixedWidth aria-hidden='true' />
</div>
{text && <span className='icon-button__text'>{text}</span>}
</button>
);
};
export default IconButton;

Wyświetl plik

@ -23,7 +23,6 @@ import StatusReplyMentions from './status-reply-mentions';
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
import { Card, HStack, Stack, Text } from './ui';
import type { Map as ImmutableMap } from 'immutable';
import type {
Account as AccountEntity,
Status as StatusEntity,
@ -45,7 +44,6 @@ export interface IStatus {
unread?: boolean,
onMoveUp?: (statusId: string, featured?: boolean) => void,
onMoveDown?: (statusId: string, featured?: boolean) => void,
group?: ImmutableMap<string, any>,
focusable?: boolean,
featured?: boolean,
hideActionBar?: boolean,

Wyświetl plik

@ -1,77 +0,0 @@
import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
} from '../actions/accounts';
import { openModal } from '../actions/modals';
import { initMuteModal } from '../actions/mutes';
import { getSettings } from '../actions/settings';
import Account from '../components/account';
import { makeGetAccount } from '../selectors';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow(account) {
dispatch((_, getState) => {
const unfollowModal = getSettings(getState()).get('unfollowModal');
if (account.relationship?.following || account.relationship?.requested) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/minus.svg'),
heading: <FormattedMessage id='confirmations.unfollow.heading' defaultMessage='Unfollow {name}' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
}));
} else {
dispatch(unfollowAccount(account.get('id')));
}
} else {
dispatch(followAccount(account.get('id')));
}
});
},
onBlock(account) {
if (account.relationship?.blocking) {
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(blockAccount(account.get('id')));
}
},
onMute(account) {
if (account.relationship?.muting) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
}
},
onMuteNotifications(account, notifications) {
dispatch(muteAccount(account.get('id'), notifications));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));

Wyświetl plik

@ -0,0 +1,21 @@
import React, { useCallback } from 'react';
import { useAppSelector } from 'soapbox/hooks';
import Account, { IAccount } from '../components/account';
import { makeGetAccount } from '../selectors';
interface IAccountContainer extends Omit<IAccount, 'account'> {
id: string
}
const AccountContainer: React.FC<IAccountContainer> = ({ id, ...props }) => {
const getAccount = useCallback(makeGetAccount(), []);
const account = useAppSelector(state => getAccount(state, id));
return (
<Account account={account!} {...props} />
);
};
export default AccountContainer;

Wyświetl plik

@ -4,9 +4,8 @@ import { Link } from 'react-router-dom';
import { closeReports } from 'soapbox/actions/admin';
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
import Avatar from 'soapbox/components/avatar';
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
import { Accordion, Button, Stack, HStack, Text } from 'soapbox/components/ui';
import { Accordion, Avatar, Button, Stack, HStack, Text } from 'soapbox/components/ui';
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetReport } from 'soapbox/selectors';
@ -86,7 +85,7 @@ const Report: React.FC<IReport> = ({ id }) => {
<HStack space={3} className='p-3' key={report.id}>
<HoverRefWrapper accountId={targetAccount.id} inline>
<Link to={`/@${acct}`} title={acct}>
<Avatar account={targetAccount} size={32} />
<Avatar src={targetAccount.avatar} size={32} className='overflow-hidden' />
</Link>
</HoverRefWrapper>

Wyświetl plik

@ -1,132 +0,0 @@
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, is } from 'immutable';
import debounce from 'lodash/debounce';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
import { fetchUsers } from 'soapbox/actions/admin';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Column } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import { SimpleForm, TextInput } from 'soapbox/features/forms';
const messages = defineMessages({
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
empty: { id: 'admin.user_index.empty', defaultMessage: 'No users found.' },
searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
});
class UserIndex extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
};
state = {
isLoading: true,
filters: ImmutableSet(['local', 'active']),
accountIds: ImmutableOrderedSet(),
total: Infinity,
pageSize: 50,
page: 0,
query: '',
nextLink: undefined,
}
clearState = callback => {
this.setState({
isLoading: true,
accountIds: ImmutableOrderedSet(),
page: 0,
}, callback);
}
fetchNextPage = () => {
const { filters, page, query, pageSize, nextLink } = this.state;
const nextPage = page + 1;
this.props.dispatch(fetchUsers(filters, nextPage, query, pageSize, nextLink))
.then(({ users, count, next }) => {
const newIds = users.map(user => user.id);
this.setState({
isLoading: false,
accountIds: this.state.accountIds.union(newIds),
total: count,
page: nextPage,
nextLink: next,
});
})
.catch(() => { });
}
componentDidMount() {
this.fetchNextPage();
}
refresh = () => {
this.clearState(() => {
this.fetchNextPage();
});
}
componentDidUpdate(prevProps, prevState) {
const { filters, query } = this.state;
const filtersChanged = !is(filters, prevState.filters);
const queryChanged = query !== prevState.query;
if (filtersChanged || queryChanged) {
this.refresh();
}
}
handleLoadMore = debounce(() => {
this.fetchNextPage();
}, 2000, { leading: true });
updateQuery = debounce(query => {
this.setState({ query });
}, 900)
handleQueryChange = e => {
this.updateQuery(e.target.value);
};
render() {
const { intl } = this.props;
const { accountIds, isLoading } = this.state;
const hasMore = accountIds.count() < this.state.total && this.state.nextLink !== false;
const showLoading = isLoading && accountIds.isEmpty();
return (
<Column label={intl.formatMessage(messages.heading)}>
<SimpleForm style={{ paddingBottom: 0 }}>
<TextInput
onChange={this.handleQueryChange}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
/>
</SimpleForm>
<ScrollableList
scrollKey='user-index'
hasMore={hasMore}
isLoading={isLoading}
showLoading={showLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={intl.formatMessage(messages.empty)}
className='mt-4'
itemClassName='pb-4'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withDate />,
)}
</ScrollableList>
</Column>
);
}
}
export default injectIntl(connect()(UserIndex));

Wyświetl plik

@ -0,0 +1,71 @@
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { expandUserIndex, fetchUserIndex, setUserIndexQuery } from 'soapbox/actions/admin';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Column } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import { SimpleForm, TextInput } from 'soapbox/features/forms';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
empty: { id: 'admin.user_index.empty', defaultMessage: 'No users found.' },
searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
});
const UserIndex: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { isLoading, items, total, query, next } = useAppSelector((state) => state.admin_user_index);
const handleLoadMore = () => {
dispatch(expandUserIndex());
};
const updateQuery = useCallback(debounce(() => {
dispatch(fetchUserIndex());
}, 900, { leading: true }), []);
const handleQueryChange: React.ChangeEventHandler<HTMLInputElement> = e => {
dispatch(setUserIndexQuery(e.target.value));
};
useEffect(() => {
updateQuery();
}, [query]);
const hasMore = items.count() < total && next !== null;
const showLoading = isLoading && items.isEmpty();
return (
<Column label={intl.formatMessage(messages.heading)}>
<SimpleForm style={{ paddingBottom: 0 }}>
<TextInput
value={query}
onChange={handleQueryChange}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
/>
</SimpleForm>
<ScrollableList
scrollKey='user-index'
hasMore={hasMore}
isLoading={isLoading}
showLoading={showLoading}
onLoadMore={handleLoadMore}
emptyMessage={intl.formatMessage(messages.empty)}
className='mt-4'
itemClassName='pb-4'
>
{items.map(id =>
<AccountContainer key={id} id={id} withDate />,
)}
</ScrollableList>
</Column>
);
};
export default UserIndex;

Wyświetl plik

@ -7,8 +7,6 @@ import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack,
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { Token } from 'soapbox/reducers/security';
import type { Map as ImmutableMap } from 'immutable';
const messages = defineMessages({
header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' },
revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' },
@ -75,9 +73,9 @@ const AuthTokenList: React.FC = () => {
const intl = useIntl();
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
const currentTokenId = useAppSelector(state => {
const currentToken = state.auth.get('tokens').valueSeq().find((token: ImmutableMap<string, any>) => token.get('me') === state.auth.get('me'));
const currentToken = state.auth.tokens.valueSeq().find((token) => token.me === state.auth.me);
return currentToken?.get('id');
return currentToken?.id;
});
useEffect(() => {

Wyświetl plik

@ -1,10 +1,9 @@
import React, { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display-name';
import AccountComponent from 'soapbox/components/account';
import Icon from 'soapbox/components/icon';
import { HStack } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
@ -22,12 +21,6 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
const account = useAppSelector((state) => getAccount(state, accountId));
// useEffect(() => {
// if (accountId && !account) {
// fetchAccount(accountId);
// }
// }, [accountId]);
if (!account) return null;
const birthday = account.birthday;
@ -36,26 +29,20 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' });
return (
<div className='account'>
<div className='account__wrapper'>
<Link className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</div>
</Link>
<div
className='flex items-center gap-0.5'
title={intl.formatMessage(messages.birthday, {
date: formattedBirthday,
})}
>
<Icon src={require('@tabler/icons/ballon.svg')} />
{formattedBirthday}
</div>
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
<div className='w-full'>
<AccountComponent account={account} withRelationship={false} />
</div>
</div>
<div
className='flex items-center gap-0.5'
title={intl.formatMessage(messages.birthday, {
date: formattedBirthday,
})}
>
<Icon src={require('@tabler/icons/ballon.svg')} />
{formattedBirthday}
</div>
</HStack>
);
};

Wyświetl plik

@ -1,15 +1,21 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui';
import ChatSearch from '../../chat-search/chat-search';
const messages = defineMessages({
title: { id: 'chat.new_message.title', defaultMessage: 'New Message' },
});
interface IChatPageNew {
}
/** New message form to create a chat. */
const ChatPageNew: React.FC<IChatPageNew> = () => {
const intl = useIntl();
const history = useHistory();
return (
@ -22,7 +28,7 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
onClick={() => history.push('/chats')}
/>
<CardTitle title='New Message' />
<CardTitle title={intl.formatMessage(messages.title)} />
</HStack>
</Stack>
@ -31,4 +37,4 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
);
};
export default ChatPageNew;
export default ChatPageNew;

Wyświetl plik

@ -1,6 +1,7 @@
import { useMutation } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { Icon, Input, Stack } from 'soapbox/components/ui';
@ -17,11 +18,16 @@ import Blankslate from './blankslate';
import EmptyResultsBlankslate from './empty-results-blankslate';
import Results from './results';
const messages = defineMessages({
placeholder: { id: 'chat_search.placeholder', defaultMessage: 'Type a name' },
});
interface IChatSearch {
isMainPage?: boolean
}
const ChatSearch = (props: IChatSearch) => {
const intl = useIntl();
const { isMainPage = false } = props;
const debounce = useDebounce;
@ -88,7 +94,7 @@ const ChatSearch = (props: IChatSearch) => {
data-testid='search'
type='text'
autoFocus
placeholder='Type a name'
placeholder={intl.formatMessage(messages.placeholder)}
value={value || ''}
onChange={(event) => setValue(event.target.value)}
outerClassName='mt-0'
@ -112,4 +118,4 @@ const ChatSearch = (props: IChatSearch) => {
);
};
export default ChatSearch;
export default ChatSearch;

Wyświetl plik

@ -75,7 +75,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
const scheduledStatusCount = useAppSelector((state) => state.get('scheduled_statuses').size);
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const features = useFeatures();
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt } = compose;

Wyświetl plik

@ -1,36 +0,0 @@
import React from 'react';
interface ITextIconButton {
label: string,
title: string,
active: boolean,
onClick: () => void,
ariaControls: string,
unavailable: boolean,
}
const TextIconButton: React.FC<ITextIconButton> = ({
label,
title,
active,
ariaControls,
unavailable,
onClick,
}) => {
const handleClick: React.MouseEventHandler = (e) => {
e.preventDefault();
onClick();
};
if (unavailable) {
return null;
}
return (
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={handleClick} aria-controls={ariaControls}>
{label}
</button>
);
};
export default TextIconButton;

Wyświetl plik

@ -37,7 +37,7 @@ const SettingsStore: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const settings = useSettings();
const settingsStore = useAppSelector(state => state.get('settings'));
const settingsStore = useAppSelector(state => state.settings);
const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(settingsStore, null, 2));
const [jsonValid, setJsonValid] = useState(true);

Wyświetl plik

@ -1,13 +1,10 @@
import React, { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display-name';
import IconButton from 'soapbox/components/icon-button';
import { Text } from 'soapbox/components/ui';
import Account from 'soapbox/components/account';
import { Button, HStack } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
@ -38,24 +35,28 @@ const AccountAuthorize: React.FC<IAccountAuthorize> = ({ id }) => {
if (!account) return null;
const content = { __html: account.note_emojified };
return (
<div className='account-authorize__wrapper'>
<div className='account-authorize'>
<Link to={`/@${account.acct}`}>
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
<DisplayName account={account} />
</Link>
<Text className='account__header__content' dangerouslySetInnerHTML={content} />
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
<div className='w-full'>
<Account account={account} withRelationship={false} />
</div>
<div className='account--panel'>
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} src={require('@tabler/icons/check.svg')} onClick={onAuthorize} /></div>
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} src={require('@tabler/icons/x.svg')} onClick={onReject} /></div>
</div>
</div>
<HStack space={2}>
<Button
theme='secondary'
size='sm'
text={intl.formatMessage(messages.authorize)}
icon={require('@tabler/icons/check.svg')}
onClick={onAuthorize}
/>
<Button
theme='danger'
size='sm'
text={intl.formatMessage(messages.reject)}
icon={require('@tabler/icons/x.svg')}
onClick={onReject}
/>
</HStack>
</HStack>
);
};

Wyświetl plik

@ -64,7 +64,7 @@ const NotificationFilterBar = () => {
name: 'pleroma:emoji_reaction',
});
items.push({
text: <Icon src={require('feather-icons/dist/icons/repeat.svg')} />,
text: <Icon src={require('@tabler/icons/repeat.svg')} />,
title: intl.formatMessage(messages.boosts),
action: onClick('reblog'),
name: 'reblog',

Wyświetl plik

@ -17,7 +17,7 @@ import { makeGetNotification } from 'soapbox/selectors';
import { NotificationType, validType } from 'soapbox/utils/notification';
import type { ScrollPosition } from 'soapbox/components/status';
import type { Account, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities';
import type { Account as AccountEntity, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities';
const notificationForScreenReader = (intl: IntlShape, message: string, timestamp: Date) => {
const output = [message];
@ -27,7 +27,7 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp
return output.join(', ');
};
const buildLink = (account: Account): JSX.Element => (
const buildLink = (account: AccountEntity): JSX.Element => (
<bdi>
<Link
className='text-gray-800 dark:text-gray-200 font-bold hover:underline'
@ -127,7 +127,7 @@ const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
const buildMessage = (
intl: IntlShape,
type: NotificationType,
account: Account,
account: AccountEntity,
totalCount: number | null,
targetName: string,
instanceTitle: string,

Wyświetl plik

@ -1,22 +1,21 @@
import React from 'react';
import { HStack } from 'soapbox/components/ui';
import PlaceholderAvatar from './placeholder-avatar';
import PlaceholderDisplayName from './placeholder-display-name';
/** Fake account to display while data is loading. */
const PlaceholderAccount: React.FC = () => {
return (
<div className='account'>
<div className='account__wrapper'>
<span className='account__display-name'>
<div className='account__avatar-wrapper'>
<PlaceholderAvatar size={36} />
</div>
<PlaceholderDisplayName minLength={3} maxLength={25} />
</span>
</div>
const PlaceholderAccount: React.FC = () => (
<HStack space={3} alignItems='center'>
<div className='flex-shrink-0'>
<PlaceholderAvatar size={42} />
</div>
);
};
<div className='min-w-0 flex-1'>
<PlaceholderDisplayName minLength={3} maxLength={25} />
</div>
</HStack>
);
export default PlaceholderAccount;

Wyświetl plik

@ -1,11 +1,11 @@
import classNames from 'clsx';
import React from 'react';
import Account from 'soapbox/components/account';
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
import StatusContent from 'soapbox/components/status-content';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import { HStack } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import PollPreview from 'soapbox/features/ui/components/poll-preview';
import { useAppSelector } from 'soapbox/hooks';
@ -36,11 +36,12 @@ const ScheduledStatus: React.FC<IScheduledStatus> = ({ statusId, ...other }) =>
<div className={classNames('status', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })} data-id={status.id}>
<div className='mb-4'>
<HStack justifyContent='between' alignItems='start'>
<AccountContainer
<Account
key={account.id}
id={account.id}
account={account}
timestamp={status.created_at}
futureTimestamp
hideActions
/>
</HStack>
</div>

Wyświetl plik

@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import Account from 'soapbox/components/account';
import Icon from 'soapbox/components/icon';
import StatusContent from 'soapbox/components/status-content';
import StatusMedia from 'soapbox/components/status-media';
@ -8,7 +9,6 @@ import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
import TranslateButton from 'soapbox/components/translate-button';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
import { getActualStatus } from 'soapbox/utils/status';
@ -84,9 +84,9 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
<div className='border-box'>
<div ref={node} className='detailed-actualStatus' tabIndex={-1}>
<div className='mb-4'>
<AccountContainer
<Account
key={account.id}
id={account.id}
account={account}
timestamp={actualStatus.created_at}
avatarSize={42}
hideActions

Wyświetl plik

@ -1,10 +1,10 @@
import classNames from 'clsx';
import React from 'react';
import Account from 'soapbox/components/account';
import StatusContent from 'soapbox/components/status-content';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import { Card, HStack } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card';
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder-media-gallery';
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
@ -65,9 +65,9 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
>
<div className='mb-4'>
<HStack justifyContent='between' alignItems='start'>
<AccountContainer
<Account
key={account.id}
id={account.id}
account={account}
timestamp={status.created_at}
hideActions
/>

Wyświetl plik

@ -39,8 +39,8 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
const features = useFeatures();
const intl = useIntl();
const authUsers = useAppSelector((state) => state.auth.get('users'));
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.get('id'))));
const authUsers = useAppSelector((state) => state.auth.users);
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.id)!));
const handleLogOut = () => {
dispatch(logOut());

Wyświetl plik

@ -2,9 +2,8 @@ import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import Avatar from 'soapbox/components/avatar';
import StillImage from 'soapbox/components/still-image';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification-badge';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
@ -48,10 +47,7 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
title={acct}
className='-mt-12 block'
>
<Avatar
account={account}
className='h-20 w-20 bg-gray-50 ring-2 ring-white'
/>
<Avatar src={account.avatar} className='h-20 w-20 bg-gray-50 ring-2 ring-white overflow-hidden' />
</Link>
{action && (

Wyświetl plik

@ -38,7 +38,9 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}
action={
<Link to='/suggestions'>
<Text tag='span' theme='primary' size='sm' className='hover:underline'>View all</Text>
<Text tag='span' theme='primary' size='sm' className='hover:underline'>
<FormattedMessage id='feed_suggestions.view_all' defaultMessage='View all' />
</Text>
</Link>
}
>

Wyświetl plik

@ -191,6 +191,7 @@
"chat.actions.send": "Send",
"chat.failed_to_send": "Message failed to send.",
"chat.input.placeholder": "Type a message",
"chat.new_message.title": "New Message",
"chat.page_settings.accepting_messages.label": "Allow users to start a new chat with you",
"chat.page_settings.play_sounds.label": "Play a sound when you receive a message",
"chat.page_settings.preferences": "Preferences",
@ -224,6 +225,7 @@
"chat_search.empty_results_blankslate.action": "Message someone",
"chat_search.empty_results_blankslate.body": "Try searching for another name.",
"chat_search.empty_results_blankslate.title": "No matches found",
"chat_search.placeholder": "Type a name",
"chat_search.title": "Messages",
"chat_settings.auto_delete.14days": "14 days",
"chat_settings.auto_delete.2minutes": "2 minutes",
@ -468,8 +470,6 @@
"confirmations.scheduled_status_delete.heading": "Cancel scheduled post",
"confirmations.scheduled_status_delete.message": "Are you sure you want to cancel this scheduled post?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.heading": "Unfollow {name}",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"crypto_donate.explanation_box.message": "{siteTitle} accepts cryptocurrency donations. You may send a donation to any of the addresses below. Thank you for your support!",
"crypto_donate.explanation_box.title": "Sending cryptocurrency donations",
"crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}",

Wyświetl plik

@ -21,12 +21,13 @@ const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => {
};
/** Middleware to display Redux errors to the user. */
export default function errorsMiddleware(): ThunkMiddleware {
return () => next => action => {
const errorsMiddleware = (): ThunkMiddleware =>
() => next => action => {
if (shouldShowError(action)) {
toast.showAlertForError(action.error);
}
return next(action);
};
}
export default errorsMiddleware;

Wyświetl plik

@ -112,7 +112,7 @@ const fixAkkoma = (instance: ImmutableMap<string, any>) => {
}
};
/** Set Takahe version to a Pleroma-like string */
/** Set Takahē version to a Pleroma-like string */
const fixTakahe = (instance: ImmutableMap<string, any>) => {
const version: string = instance.get('version', '');

Wyświetl plik

@ -10,17 +10,18 @@ import {
} from 'soapbox/actions/auth';
import { ME_FETCH_SKIP } from 'soapbox/actions/me';
import { MASTODON_PRELOAD_IMPORT } from 'soapbox/actions/preload';
import { AuthAppRecord, AuthTokenRecord, AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth';
import reducer from '../auth';
describe('auth reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
app: ImmutableMap(),
users: ImmutableMap(),
tokens: ImmutableMap(),
expect(reducer(undefined, {} as any).toJS()).toMatchObject({
app: {},
users: {},
tokens: {},
me: null,
}));
});
});
describe('AUTH_APP_CREATED', () => {
@ -29,9 +30,9 @@ describe('auth reducer', () => {
const action = { type: AUTH_APP_CREATED, app: token };
const result = reducer(undefined, action);
const expected = fromJS(token);
const expected = AuthAppRecord(token);
expect(result.get('app')).toEqual(expected);
expect(result.app).toEqual(expected);
});
});
@ -41,19 +42,19 @@ describe('auth reducer', () => {
const action = { type: AUTH_LOGGED_IN, token };
const result = reducer(undefined, action);
const expected = fromJS({ 'ABCDEFG': token });
const expected = ImmutableMap({ 'ABCDEFG': AuthTokenRecord(token) });
expect(result.get('tokens')).toEqual(expected);
expect(result.tokens).toEqual(expected);
});
it('should merge the token with existing state', () => {
const state = fromJS({
tokens: { 'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' } },
const state = ReducerRecord({
tokens: ImmutableMap({ 'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }) }),
});
const expected = fromJS({
'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' },
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
const expected = ImmutableMap({
'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }),
'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }),
});
const action = {
@ -62,7 +63,7 @@ describe('auth reducer', () => {
};
const result = reducer(state, action);
expect(result.get('tokens')).toEqual(expected);
expect(result.tokens).toEqual(expected);
});
});
@ -73,28 +74,28 @@ describe('auth reducer', () => {
account: fromJS({ url: 'https://gleasonator.com/users/alex' }),
};
const state = fromJS({
users: {
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
},
const state = ReducerRecord({
users: ImmutableMap({
'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }),
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
}),
});
const expected = fromJS({
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
const expected = ImmutableMap({
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
});
const result = reducer(state, action);
expect(result.get('users')).toEqual(expected);
expect(result.users).toEqual(expected);
});
it('sets `me` to the next available user', () => {
const state = fromJS({
const state = ReducerRecord({
me: 'https://gleasonator.com/users/alex',
users: {
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
},
users: ImmutableMap({
'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }),
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
}),
});
const action = {
@ -103,7 +104,7 @@ describe('auth reducer', () => {
};
const result = reducer(state, action);
expect(result.get('me')).toEqual('https://gleasonator.com/users/benis');
expect(result.me).toEqual('https://gleasonator.com/users/benis');
});
});
@ -115,12 +116,12 @@ describe('auth reducer', () => {
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
};
const expected = fromJS({
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
const expected = ImmutableMap({
'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }),
});
const result = reducer(undefined, action);
expect(result.get('users')).toEqual(expected);
expect(result.users).toEqual(expected);
});
it('should set the account in the token', () => {
@ -130,21 +131,21 @@ describe('auth reducer', () => {
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
};
const state = fromJS({
tokens: { 'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' } },
const state = ReducerRecord({
tokens: ImmutableMap({ 'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }) }),
});
const expected = fromJS({
const expected = {
'ABCDEFG': {
token_type: 'Bearer',
access_token: 'ABCDEFG',
account: '1234',
me: 'https://gleasonator.com/users/alex',
},
});
};
const result = reducer(state, action);
expect(result.get('tokens')).toEqual(expected);
expect(result.tokens.toJS()).toMatchObject(expected);
});
it('sets `me` to the account if unset', () => {
@ -155,7 +156,7 @@ describe('auth reducer', () => {
};
const result = reducer(undefined, action);
expect(result.get('me')).toEqual('https://gleasonator.com/users/alex');
expect(result.me).toEqual('https://gleasonator.com/users/alex');
});
it('leaves `me` alone if already set', () => {
@ -165,10 +166,10 @@ describe('auth reducer', () => {
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
};
const state = fromJS({ me: 'https://gleasonator.com/users/benis' });
const state = ReducerRecord({ me: 'https://gleasonator.com/users/benis' });
const result = reducer(state, action);
expect(result.get('me')).toEqual('https://gleasonator.com/users/benis');
expect(result.me).toEqual('https://gleasonator.com/users/benis');
});
it('deletes mismatched users', () => {
@ -178,21 +179,21 @@ describe('auth reducer', () => {
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
};
const state = fromJS({
users: {
'https://gleasonator.com/users/mk': { id: '4567', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/mk' },
'https://gleasonator.com/users/curtis': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/curtis' },
'https://gleasonator.com/users/benis': { id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
},
const state = ReducerRecord({
users: ImmutableMap({
'https://gleasonator.com/users/mk': AuthUserRecord({ id: '4567', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/mk' }),
'https://gleasonator.com/users/curtis': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/curtis' }),
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
}),
});
const expected = fromJS({
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
'https://gleasonator.com/users/benis': { id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
const expected = ImmutableMap({
'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }),
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
});
const result = reducer(state, action);
expect(result.get('users')).toEqual(expected);
expect(result.users).toEqual(expected);
});
it('upgrades from an ID to a URL', () => {
@ -202,18 +203,18 @@ describe('auth reducer', () => {
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
};
const state = fromJS({
const state = ReducerRecord({
me: '1234',
users: {
'1234': { id: '1234', access_token: 'ABCDEFG' },
'5432': { id: '5432', access_token: 'HIJKLMN' },
},
tokens: {
'ABCDEFG': { access_token: 'ABCDEFG', account: '1234' },
},
users: ImmutableMap({
'1234': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG' }),
'5432': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN' }),
}),
tokens: ImmutableMap({
'ABCDEFG': AuthTokenRecord({ access_token: 'ABCDEFG', account: '1234' }),
}),
});
const expected = fromJS({
const expected = {
me: 'https://gleasonator.com/users/alex',
users: {
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
@ -222,24 +223,24 @@ describe('auth reducer', () => {
tokens: {
'ABCDEFG': { access_token: 'ABCDEFG', account: '1234', me: 'https://gleasonator.com/users/alex' },
},
});
};
const result = reducer(state, action);
expect(result).toEqual(expected);
expect(result.toJS()).toMatchObject(expected);
});
});
describe('VERIFY_CREDENTIALS_FAIL', () => {
it('should delete the failed token if it 403\'d', () => {
const state = fromJS({
tokens: {
'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' },
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
},
const state = ReducerRecord({
tokens: ImmutableMap({
'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }),
'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }),
}),
});
const expected = fromJS({
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
const expected = ImmutableMap({
'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }),
});
const action = {
@ -249,19 +250,19 @@ describe('auth reducer', () => {
};
const result = reducer(state, action);
expect(result.get('tokens')).toEqual(expected);
expect(result.tokens).toEqual(expected);
});
it('should delete any users associated with the failed token', () => {
const state = fromJS({
users: {
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
},
const state = ReducerRecord({
users: ImmutableMap({
'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }),
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
}),
});
const expected = fromJS({
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
const expected = ImmutableMap({
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
});
const action = {
@ -271,16 +272,16 @@ describe('auth reducer', () => {
};
const result = reducer(state, action);
expect(result.get('users')).toEqual(expected);
expect(result.users).toEqual(expected);
});
it('should reassign `me` to the next in line', () => {
const state = fromJS({
const state = ReducerRecord({
me: 'https://gleasonator.com/users/alex',
users: {
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
},
users: ImmutableMap({
'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }),
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
}),
});
const action = {
@ -290,7 +291,7 @@ describe('auth reducer', () => {
};
const result = reducer(state, action);
expect(result.get('me')).toEqual('https://gleasonator.com/users/benis');
expect(result.me).toEqual('https://gleasonator.com/users/benis');
});
});
@ -302,16 +303,16 @@ describe('auth reducer', () => {
};
const result = reducer(undefined, action);
expect(result.get('me')).toEqual('https://gleasonator.com/users/benis');
expect(result.me).toEqual('https://gleasonator.com/users/benis');
});
});
describe('ME_FETCH_SKIP', () => {
it('sets `me` to null', () => {
const state = fromJS({ me: 'https://gleasonator.com/users/alex' });
const state = ReducerRecord({ me: 'https://gleasonator.com/users/alex' });
const action = { type: ME_FETCH_SKIP };
const result = reducer(state, action);
expect(result.get('me')).toEqual(null);
expect(result.me).toEqual(null);
});
});
@ -322,7 +323,7 @@ describe('auth reducer', () => {
data: require('soapbox/__fixtures__/mastodon_initial_state.json'),
};
const expected = fromJS({
const expected = {
me: 'https://mastodon.social/@benis911',
app: {},
users: {
@ -341,10 +342,10 @@ describe('auth reducer', () => {
token_type: 'Bearer',
},
},
});
};
const result = reducer(undefined, action);
expect(result).toEqual(expected);
expect(result.toJS()).toMatchObject(expected);
});
});
});

Wyświetl plik

@ -23,7 +23,7 @@ describe('modal reducer', () => {
});
it('should handle MODAL_CLOSE', () => {
const state = ImmutableList([
const state = ImmutableList<any>([
ImmutableRecord({
modalType: 'type1',
modalProps: { props1: '1' },

Wyświetl plik

@ -0,0 +1,68 @@
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
import {
ADMIN_USER_INDEX_EXPAND_FAIL,
ADMIN_USER_INDEX_EXPAND_REQUEST,
ADMIN_USER_INDEX_EXPAND_SUCCESS,
ADMIN_USER_INDEX_FETCH_FAIL,
ADMIN_USER_INDEX_FETCH_REQUEST,
ADMIN_USER_INDEX_FETCH_SUCCESS,
ADMIN_USER_INDEX_QUERY_SET,
} from 'soapbox/actions/admin';
import type { AnyAction } from 'redux';
import type { APIEntity } from 'soapbox/types/entities';
const ReducerRecord = ImmutableRecord({
isLoading: false,
loaded: false,
items: ImmutableOrderedSet<string>(),
filters: ImmutableSet(['local', 'active']),
total: Infinity,
pageSize: 50,
page: -1,
query: '',
next: null as string | null,
});
type State = ReturnType<typeof ReducerRecord>;
export default function admin_user_index(state: State = ReducerRecord(), action: AnyAction): State {
switch (action.type) {
case ADMIN_USER_INDEX_QUERY_SET:
return state.set('query', action.query);
case ADMIN_USER_INDEX_FETCH_REQUEST:
return state
.set('isLoading', true)
.set('loaded', true)
.set('items', ImmutableOrderedSet())
.set('total', action.count)
.set('page', 0)
.set('next', null);
case ADMIN_USER_INDEX_FETCH_SUCCESS:
return state
.set('isLoading', false)
.set('loaded', true)
.set('items', ImmutableOrderedSet(action.users.map((user: APIEntity) => user.id)))
.set('total', action.count)
.set('page', 1)
.set('next', action.next);
case ADMIN_USER_INDEX_FETCH_FAIL:
case ADMIN_USER_INDEX_EXPAND_FAIL:
return state
.set('isLoading', false);
case ADMIN_USER_INDEX_EXPAND_REQUEST:
return state
.set('isLoading', true);
case ADMIN_USER_INDEX_EXPAND_SUCCESS:
return state
.set('isLoading', false)
.set('loaded', true)
.set('items', state.items.union(action.users.map((user: APIEntity) => user.id)))
.set('total', action.count)
.set('page', 1)
.set('next', action.next);
default:
return state;
}
}

Wyświetl plik

@ -54,16 +54,11 @@ export interface ReducerAdminReport extends AdminReportRecord {
statuses: ImmutableList<string | null>,
}
// Umm... based?
// https://itnext.io/typescript-extract-unpack-a-type-from-a-generic-baca7af14e51
type InnerRecord<R> = R extends ImmutableRecord<infer TProps> ? TProps : never;
type InnerState = InnerRecord<State>;
// Lol https://javascript.plainenglish.io/typescript-essentials-conditionally-filter-types-488705bfbf56
type FilterConditionally<Source, Condition> = Pick<Source, {[K in keyof Source]: Source[K] extends Condition ? K : never}[keyof Source]>;
type SetKeys = keyof FilterConditionally<InnerState, ImmutableOrderedSet<string>>;
type SetKeys = keyof FilterConditionally<State, ImmutableOrderedSet<string>>;
type APIReport = { id: string, state: string, statuses: any[] };
type APIUser = { id: string, email: string, nickname: string, registration_reason: string };

Wyświetl plik

@ -1,8 +1,8 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import trim from 'lodash/trim';
import { MASTODON_PRELOAD_IMPORT } from 'soapbox/actions/preload';
import { FE_SUBDIRECTORY } from 'soapbox/build-config';
import BuildConfig from 'soapbox/build-config';
import KVStore from 'soapbox/storage/kv-store';
import { validId, isURL } from 'soapbox/utils/auth';
@ -17,17 +17,55 @@ import {
} from '../actions/auth';
import { ME_FETCH_SKIP } from '../actions/me';
const defaultState = ImmutableMap({
app: ImmutableMap(),
users: ImmutableMap(),
tokens: ImmutableMap(),
me: null,
import type { AxiosError } from 'axios';
import type { AnyAction } from 'redux';
import type { APIEntity, Account as AccountEntity } from 'soapbox/types/entities';
export const AuthAppRecord = ImmutableRecord({
access_token: null as string | null,
client_id: null as string | null,
client_secret: null as string | null,
id: null as string | null,
name: null as string | null,
redirect_uri: null as string | null,
token_type: null as string | null,
vapid_key: null as string | null,
website: null as string | null,
});
const buildKey = parts => parts.join(':');
export const AuthTokenRecord = ImmutableRecord({
access_token: '',
account: null as string | null,
created_at: 0,
expires_in: null as number | null,
id: null as number | null,
me: null as string | null,
refresh_token: null as string | null,
scope: '',
token_type: '',
});
export const AuthUserRecord = ImmutableRecord({
access_token: '',
id: '',
url: '',
});
export const ReducerRecord = ImmutableRecord({
app: AuthAppRecord(),
tokens: ImmutableMap<string, AuthToken>(),
users: ImmutableMap<string, AuthUser>(),
me: null as string | null,
});
type AuthToken = ReturnType<typeof AuthTokenRecord>;
type AuthUser = ReturnType<typeof AuthUserRecord>;
type State = ReturnType<typeof ReducerRecord>;
const buildKey = (parts: string[]) => parts.join(':');
// For subdirectory support
const NAMESPACE = trim(FE_SUBDIRECTORY, '/') ? `soapbox@${FE_SUBDIRECTORY}` : 'soapbox';
const NAMESPACE = trim(BuildConfig.FE_SUBDIRECTORY, '/') ? `soapbox@${BuildConfig.FE_SUBDIRECTORY}` : 'soapbox';
const STORAGE_KEY = buildKey([NAMESPACE, 'auth']);
const SESSION_KEY = buildKey([NAMESPACE, 'auth', 'me']);
@ -37,35 +75,48 @@ const getSessionUser = () => {
return validId(id) ? id : undefined;
};
const getLocalState = () => {
const state = JSON.parse(localStorage.getItem(STORAGE_KEY)!);
if (!state) return undefined;
return ReducerRecord({
app: AuthAppRecord(state.app),
tokens: ImmutableMap(Object.entries(state.tokens).map(([key, value]) => [key, AuthTokenRecord(value as any)])),
users: ImmutableMap(Object.entries(state.users).map(([key, value]) => [key, AuthUserRecord(value as any)])),
me: state.me,
});
};
const sessionUser = getSessionUser();
export const localState = fromJS(JSON.parse(localStorage.getItem(STORAGE_KEY)));
export const localState = getLocalState(); fromJS(JSON.parse(localStorage.getItem(STORAGE_KEY)!));
// Checks if the user has an ID and access token
const validUser = user => {
const validUser = (user?: AuthUser) => {
try {
return validId(user.get('id')) && validId(user.get('access_token'));
return !!(user && validId(user.id) && validId(user.access_token));
} catch (e) {
return false;
}
};
// Finds the first valid user in the state
const firstValidUser = state => state.get('users', ImmutableMap()).find(validUser);
const firstValidUser = (state: State) => state.users.find(validUser);
// For legacy purposes. IDs get upgraded to URLs further down.
const getUrlOrId = user => {
const getUrlOrId = (user?: AuthUser): string | null => {
try {
const { id, url } = user.toJS();
return url || id;
const { id, url } = user!.toJS();
return (url || id) as string;
} catch {
return null;
}
};
// If `me` doesn't match an existing user, attempt to shift it.
const maybeShiftMe = state => {
const me = state.get('me');
const user = state.getIn(['users', me]);
const maybeShiftMe = (state: State) => {
const me = state.me!;
const user = state.users.get(me);
if (!validUser(user)) {
const nextUser = firstValidUser(state);
@ -76,29 +127,29 @@ const maybeShiftMe = state => {
};
// Set the user from the session or localStorage, whichever is valid first
const setSessionUser = state => state.update('me', null, me => {
const user = ImmutableList([
state.getIn(['users', sessionUser]),
state.getIn(['users', me]),
const setSessionUser = (state: State) => state.update('me', me => {
const user = ImmutableList<AuthUser>([
state.users.get(sessionUser!)!,
state.users.get(me!)!,
]).find(validUser);
return getUrlOrId(user);
});
// Upgrade the initial state
const migrateLegacy = state => {
const migrateLegacy = (state: State) => {
if (localState) return state;
return state.withMutations(state => {
const app = fromJS(JSON.parse(localStorage.getItem('soapbox:auth:app')));
const user = fromJS(JSON.parse(localStorage.getItem('soapbox:auth:user')));
const app = AuthAppRecord(JSON.parse(localStorage.getItem('soapbox:auth:app')!));
const user = fromJS(JSON.parse(localStorage.getItem('soapbox:auth:user')!)) as ImmutableMap<string, any>;
if (!user) return;
state.set('me', '_legacy'); // Placeholder account ID
state.set('app', app);
state.set('tokens', ImmutableMap({
[user.get('access_token')]: user.set('account', '_legacy'),
[user.get('access_token')]: AuthTokenRecord(user.set('account', '_legacy')),
}));
state.set('users', ImmutableMap({
'_legacy': ImmutableMap({
'_legacy': AuthUserRecord({
id: '_legacy',
access_token: user.get('access_token'),
}),
@ -106,26 +157,26 @@ const migrateLegacy = state => {
});
};
const isUpgradingUrlId = state => {
const me = state.get('me');
const user = state.getIn(['users', me]);
const isUpgradingUrlId = (state: State) => {
const me = state.me;
const user = state.users.get(me!);
return validId(me) && user && !isURL(me);
};
// Checks the state and makes it valid
const sanitizeState = state => {
const sanitizeState = (state: State) => {
// Skip sanitation during ID to URL upgrade
if (isUpgradingUrlId(state)) return state;
return state.withMutations(state => {
// Remove invalid users, ensure ID match
state.update('users', ImmutableMap(), users => (
state.update('users', users => (
users.filter((user, url) => (
validUser(user) && user.get('url') === url
))
));
// Remove mismatched tokens
state.update('tokens', ImmutableMap(), tokens => (
state.update('tokens', tokens => (
tokens.filter((token, id) => (
validId(id) && token.get('access_token') === id
))
@ -133,21 +184,21 @@ const sanitizeState = state => {
});
};
const persistAuth = state => localStorage.setItem(STORAGE_KEY, JSON.stringify(state.toJS()));
const persistAuth = (state: State) => localStorage.setItem(STORAGE_KEY, JSON.stringify(state.toJS()));
const persistSession = state => {
const me = state.get('me');
const persistSession = (state: State) => {
const me = state.me;
if (me && typeof me === 'string') {
sessionStorage.setItem(SESSION_KEY, me);
}
};
const persistState = state => {
const persistState = (state: State) => {
persistAuth(state);
persistSession(state);
};
const initialize = state => {
const initialize = (state: State) => {
return state.withMutations(state => {
maybeShiftMe(state);
setSessionUser(state);
@ -157,17 +208,17 @@ const initialize = state => {
});
};
const initialState = initialize(defaultState.merge(localState));
const initialState = initialize(ReducerRecord().merge(localState as any));
const importToken = (state, token) => {
return state.setIn(['tokens', token.access_token], fromJS(token));
const importToken = (state: State, token: APIEntity) => {
return state.setIn(['tokens', token.access_token], AuthTokenRecord(token));
};
// Upgrade the `_legacy` placeholder ID with a real account
const upgradeLegacyId = (state, account) => {
const upgradeLegacyId = (state: State, account: APIEntity) => {
if (localState) return state;
return state.withMutations(state => {
state.update('me', null, me => me === '_legacy' ? account.url : me);
state.update('me', me => me === '_legacy' ? account.url : me);
state.deleteIn(['users', '_legacy']);
});
// TODO: Delete `soapbox:auth:app` and `soapbox:auth:user` localStorage?
@ -176,19 +227,19 @@ const upgradeLegacyId = (state, account) => {
// Users are now stored by their ActivityPub ID instead of their
// primary key to support auth against multiple hosts.
const upgradeNonUrlId = (state, account) => {
const me = state.get('me');
const upgradeNonUrlId = (state: State, account: APIEntity) => {
const me = state.me;
if (isURL(me)) return state;
return state.withMutations(state => {
state.update('me', null, me => me === account.id ? account.url : me);
state.update('me', me => me === account.id ? account.url : me);
state.deleteIn(['users', account.id]);
});
};
// Returns a predicate function for filtering a mismatched user/token
const userMismatch = (token, account) => {
return (user, url) => {
const userMismatch = (token: string, account: APIEntity) => {
return (user: AuthUser, url: string) => {
const sameToken = user.get('access_token') === token;
const differentUrl = url !== account.url || user.get('url') !== account.url;
const differentId = user.get('id') !== account.id;
@ -197,48 +248,48 @@ const userMismatch = (token, account) => {
};
};
const importCredentials = (state, token, account) => {
const importCredentials = (state: State, token: string, account: APIEntity) => {
return state.withMutations(state => {
state.setIn(['users', account.url], ImmutableMap({
state.setIn(['users', account.url], AuthUserRecord({
id: account.id,
access_token: token,
url: account.url,
}));
state.setIn(['tokens', token, 'account'], account.id);
state.setIn(['tokens', token, 'me'], account.url);
state.update('users', ImmutableMap(), users => users.filterNot(userMismatch(token, account)));
state.update('me', null, me => me || account.url);
state.update('users', users => users.filterNot(userMismatch(token, account)));
state.update('me', me => me || account.url);
upgradeLegacyId(state, account);
upgradeNonUrlId(state, account);
});
};
const deleteToken = (state, token) => {
const deleteToken = (state: State, token: string) => {
return state.withMutations(state => {
state.update('tokens', ImmutableMap(), tokens => tokens.delete(token));
state.update('users', ImmutableMap(), users => users.filterNot(user => user.get('access_token') === token));
state.update('tokens', tokens => tokens.delete(token));
state.update('users', users => users.filterNot(user => user.get('access_token') === token));
maybeShiftMe(state);
});
};
const deleteUser = (state, account) => {
const deleteUser = (state: State, account: AccountEntity) => {
const accountUrl = account.get('url');
return state.withMutations(state => {
state.update('users', ImmutableMap(), users => users.delete(accountUrl));
state.update('tokens', ImmutableMap(), tokens => tokens.filterNot(token => token.get('me') === accountUrl));
state.update('users', users => users.delete(accountUrl));
state.update('tokens', tokens => tokens.filterNot(token => token.get('me') === accountUrl));
maybeShiftMe(state);
});
};
const importMastodonPreload = (state, data) => {
const importMastodonPreload = (state: State, data: ImmutableMap<string, any>) => {
return state.withMutations(state => {
const accountId = data.getIn(['meta', 'me']);
const accountUrl = data.getIn(['accounts', accountId, 'url']);
const accessToken = data.getIn(['meta', 'access_token']);
const accountId = data.getIn(['meta', 'me']) as string;
const accountUrl = data.getIn(['accounts', accountId, 'url']) as string;
const accessToken = data.getIn(['meta', 'access_token']) as string;
if (validId(accessToken) && validId(accountId) && isURL(accountUrl)) {
state.setIn(['tokens', accessToken], fromJS({
state.setIn(['tokens', accessToken], AuthTokenRecord({
access_token: accessToken,
account: accountId,
me: accountUrl,
@ -246,7 +297,7 @@ const importMastodonPreload = (state, data) => {
token_type: 'Bearer',
}));
state.setIn(['users', accountUrl], fromJS({
state.setIn(['users', accountUrl], AuthUserRecord({
id: accountId,
access_token: accessToken,
url: accountUrl,
@ -257,11 +308,11 @@ const importMastodonPreload = (state, data) => {
});
};
const persistAuthAccount = account => {
const persistAuthAccount = (account: APIEntity) => {
if (account && account.url) {
const key = `authAccount:${account.url}`;
if (!account.pleroma) account.pleroma = {};
KVStore.getItem(key).then(oldAccount => {
KVStore.getItem(key).then((oldAccount: any) => {
const settings = oldAccount?.pleroma?.settings_store || {};
if (!account.pleroma.settings_store) {
account.pleroma.settings_store = settings;
@ -272,20 +323,20 @@ const persistAuthAccount = account => {
}
};
const deleteForbiddenToken = (state, error, token) => {
if ([401, 403].includes(error.response?.status)) {
const deleteForbiddenToken = (state: State, error: AxiosError, token: string) => {
if ([401, 403].includes(error.response?.status!)) {
return deleteToken(state, token);
} else {
return state;
}
};
const reducer = (state, action) => {
const reducer = (state: State, action: AnyAction) => {
switch (action.type) {
case AUTH_APP_CREATED:
return state.set('app', fromJS(action.app));
return state.set('app', AuthAppRecord(action.app));
case AUTH_APP_AUTHORIZED:
return state.update('app', ImmutableMap(), app => app.merge(fromJS(action.token)));
return state.update('app', app => app.merge(action.token));
case AUTH_LOGGED_IN:
return importToken(state, action.token);
case AUTH_LOGGED_OUT:
@ -300,7 +351,7 @@ const reducer = (state, action) => {
case ME_FETCH_SKIP:
return state.set('me', null);
case MASTODON_PRELOAD_IMPORT:
return importMastodonPreload(state, fromJS(action.data));
return importMastodonPreload(state, fromJS(action.data) as ImmutableMap<string, any>);
default:
return state;
}
@ -309,33 +360,33 @@ const reducer = (state, action) => {
const reload = () => location.replace('/');
// `me` is a user ID string
const validMe = state => {
const me = state.get('me');
const validMe = (state: State) => {
const me = state.me;
return typeof me === 'string' && me !== '_legacy';
};
// `me` has changed from one valid ID to another
const userSwitched = (oldState, state) => {
const me = state.get('me');
const oldMe = oldState.get('me');
const userSwitched = (oldState: State, state: State) => {
const me = state.me;
const oldMe = oldState.me;
const stillValid = validMe(oldState) && validMe(state);
const didChange = oldMe !== me;
const userUpgradedUrl = state.getIn(['users', me, 'id']) === oldMe;
const userUpgradedUrl = state.users.get(me!)?.id === oldMe;
return stillValid && didChange && !userUpgradedUrl;
};
const maybeReload = (oldState, state, action) => {
const maybeReload = (oldState: State, state: State, action: AnyAction) => {
const loggedOutStandalone = action.type === AUTH_LOGGED_OUT && action.standalone;
const switched = userSwitched(oldState, state);
if (switched || loggedOutStandalone) {
reload(state);
reload();
}
};
export default function auth(oldState = initialState, action) {
export default function auth(oldState: State = initialState, action: AnyAction) {
const state = reducer(oldState, action);
if (!state.equals(oldState)) {

Wyświetl plik

@ -10,6 +10,7 @@ import accounts_counters from './accounts-counters';
import accounts_meta from './accounts-meta';
import admin from './admin';
import admin_log from './admin-log';
import admin_user_index from './admin-user-index';
import aliases from './aliases';
import announcements from './announcements';
import auth from './auth';
@ -118,6 +119,7 @@ const reducers = {
history,
announcements,
compose_event,
admin_user_index,
};
// Build a default state from all reducers: it has the key and `undefined`

Wyświetl plik

@ -1,4 +1,4 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload';
import KVStore from 'soapbox/storage/kv-store';
@ -11,24 +11,24 @@ import {
SOAPBOX_CONFIG_REQUEST_FAIL,
} from '../actions/soapbox';
const initialState = ImmutableMap();
const initialState = ImmutableMap<string, any>();
const fallbackState = ImmutableMap({
const fallbackState = ImmutableMap<string, any>({
brandColor: '#0482d8', // Azure
});
const updateFromAdmin = (state, configs) => {
const updateFromAdmin = (state: ImmutableMap<string, any>, configs: ImmutableList<ImmutableMap<string, any>>) => {
try {
return ConfigDB.find(configs, ':pleroma', ':frontend_configurations')
return ConfigDB.find(configs, ':pleroma', ':frontend_configurations')!
.get('value')
.find(value => value.getIn(['tuple', 0]) === ':soapbox_fe')
.find((value: ImmutableMap<string, any>) => value.getIn(['tuple', 0]) === ':soapbox_fe')
.getIn(['tuple', 1]);
} catch {
return state;
}
};
const preloadImport = (state, action) => {
const preloadImport = (state: ImmutableMap<string, any>, action: Record<string, any>) => {
const path = '/api/pleroma/frontend_configurations';
const feData = action.data[path];
@ -40,29 +40,29 @@ const preloadImport = (state, action) => {
}
};
const persistSoapboxConfig = (soapboxConfig, host) => {
const persistSoapboxConfig = (soapboxConfig: ImmutableMap<string, any>, host: string) => {
if (host) {
KVStore.setItem(`soapbox_config:${host}`, soapboxConfig.toJS()).catch(console.error);
}
};
const importSoapboxConfig = (state, soapboxConfig, host) => {
const importSoapboxConfig = (state: ImmutableMap<string, any>, soapboxConfig: ImmutableMap<string, any>, host: string) => {
persistSoapboxConfig(soapboxConfig, host);
return soapboxConfig;
};
export default function soapbox(state = initialState, action) {
export default function soapbox(state = initialState, action: Record<string, any>) {
switch (action.type) {
case PLEROMA_PRELOAD_IMPORT:
return preloadImport(state, action);
case SOAPBOX_CONFIG_REMEMBER_SUCCESS:
return fromJS(action.soapboxConfig);
case SOAPBOX_CONFIG_REQUEST_SUCCESS:
return importSoapboxConfig(state, fromJS(action.soapboxConfig), action.host);
return importSoapboxConfig(state, fromJS(action.soapboxConfig) as ImmutableMap<string, any>, action.host);
case SOAPBOX_CONFIG_REQUEST_FAIL:
return fallbackState.mergeDeep(state);
case ADMIN_CONFIG_UPDATE_SUCCESS:
return updateFromAdmin(state, fromJS(action.configs));
return updateFromAdmin(state, fromJS(action.configs) as ImmutableList<ImmutableMap<string, any>>);
default:
return state;
}

Wyświetl plik

@ -269,16 +269,16 @@ export const makeGetReport = () => {
};
const getAuthUserIds = createSelector([
(state: RootState) => state.auth.get('users', ImmutableMap()),
(state: RootState) => state.auth.users,
], authUsers => {
return authUsers.reduce((ids: ImmutableOrderedSet<string>, authUser: ImmutableMap<string, any>) => {
return authUsers.reduce((ids: ImmutableOrderedSet<string>, authUser) => {
try {
const id = authUser.get('id');
const id = authUser.id;
return validId(id) ? ids.add(id) : ids;
} catch {
return ids;
}
}, ImmutableOrderedSet());
}, ImmutableOrderedSet<string>());
});
export const makeGetOtherAccounts = () => {

Wyświetl plik

@ -4,7 +4,8 @@ import type { RootState } from 'soapbox/store';
export const validId = (id: any) => typeof id === 'string' && id !== 'null' && id !== 'undefined';
export const isURL = (url: string) => {
export const isURL = (url?: string | null) => {
if (typeof url !== 'string') return false;
try {
new URL(url);
return true;
@ -30,11 +31,11 @@ export const isLoggedIn = (getState: () => RootState) => {
return validId(getState().me);
};
export const getAppToken = (state: RootState) => state.auth.getIn(['app', 'access_token']) as string;
export const getAppToken = (state: RootState) => state.auth.app.access_token as string;
export const getUserToken = (state: RootState, accountId?: string | false | null) => {
const accountUrl = state.accounts.getIn([accountId, 'url']);
return state.auth.getIn(['users', accountUrl, 'access_token']) as string;
const accountUrl = state.accounts.getIn([accountId, 'url']) as string;
return state.auth.users.get(accountUrl)?.access_token as string;
};
export const getAccessToken = (state: RootState) => {
@ -43,24 +44,23 @@ export const getAccessToken = (state: RootState) => {
};
export const getAuthUserId = (state: RootState) => {
const me = state.auth.get('me');
const me = state.auth.me;
return ImmutableList([
state.auth.getIn(['users', me, 'id']),
state.auth.users.get(me!)?.id,
me,
]).find(validId);
].filter(id => id)).find(validId);
};
export const getAuthUserUrl = (state: RootState) => {
const me = state.auth.get('me');
const me = state.auth.me;
return ImmutableList([
state.auth.getIn(['users', me, 'url']),
state.auth.users.get(me!)?.url,
me,
]).find(isURL);
].filter(url => url)).find(isURL);
};
/** Get the VAPID public key. */
export const getVapidKey = (state: RootState) => {
return state.auth.getIn(['app', 'vapid_key']) || state.instance.getIn(['pleroma', 'vapid_public_key']);
};
export const getVapidKey = (state: RootState) =>
(state.auth.app.vapid_key || state.instance.pleroma.get('vapid_public_key')) as string;

Wyświetl plik

@ -123,6 +123,7 @@ const getInstanceFeatures = (instance: Instance) => {
accountLookup: any([
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
v.software === PLEROMA && gte(v.version, '2.4.50'),
v.software === TAKAHE && gte(v.version, '0.6.1'),
v.software === TRUTHSOCIAL,
]),
@ -386,6 +387,7 @@ const getInstanceFeatures = (instance: Instance) => {
/** Whether the accounts who favourited or emoji-reacted to a status can be viewed through the API. */
exposableReactions: any([
v.software === MASTODON,
v.software === TAKAHE && gte(v.version, '0.6.1'),
v.software === TRUTHSOCIAL,
features.includes('exposable_reactions'),
]),
@ -394,7 +396,10 @@ const getInstanceFeatures = (instance: Instance) => {
* Can see accounts' followers you know
* @see GET /api/v1/accounts/familiar_followers
*/
familiarFollowers: v.software === MASTODON && gte(v.version, '3.5.0'),
familiarFollowers: any([
v.software === MASTODON && gte(v.version, '3.5.0'),
v.software === TAKAHE,
]),
/** Whether the instance federates. */
federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
@ -524,6 +529,7 @@ const getInstanceFeatures = (instance: Instance) => {
notificationsIncludeTypes: any([
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
v.software === PLEROMA && gte(v.version, '2.4.50'),
v.software === TAKAHE && gte(v.version, '0.6.2'),
]),
/**

Wyświetl plik

@ -32,34 +32,3 @@
height: $size;
background-size: $size $size;
}
@mixin search-input {
@include font-size(16);
@include line-height(19);
outline: 0;
box-sizing: border-box;
width: 100%;
box-shadow: none;
font-family: inherit;
background: var(--background-color);
color: var(--highlight-text-color);
margin: 0;
border-radius: 999px;
border: 0;
padding-left: 15px;
// Chrome does not like these concatinated together
&::placeholder { color: var(--primary-text-color--faint); }
&:-ms-input-placeholder { color: var(--primary-text-color--faint); }
&::-ms-input-placeholder { color: var(--primary-text-color--faint); }
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
}

Wyświetl plik

@ -10,14 +10,6 @@
@media screen and (max-width: $no-gap-breakpoint) {
box-shadow: none;
}
&:hover,
&:active,
&:focus {
.card__bar {
background-color: var(--brand-color--faint);
}
}
}
}
@ -28,56 +20,6 @@
&:not(:last-of-type) {
border-bottom: 1px solid var(--brand-color--med);
}
.account__display-name {
flex: 1 1 auto;
display: block;
color: var(--primary-text-color--faint);
overflow: hidden;
text-decoration: none;
font-size: 14px;
.display-name__name {
display: flex;
}
}
}
.account__wrapper {
display: flex;
align-items: center;
}
.account__avatar-wrapper {
float: left;
margin-right: 12px;
}
a .account__avatar {
cursor: pointer;
}
.account__avatar-overlay {
@include avatar-size(48px);
&-base {
@include avatar-radius;
@include avatar-size(36px);
}
&-overlay {
@include avatar-radius;
@include avatar-size(24px);
position: absolute;
bottom: 0;
right: 0;
z-index: 1;
}
}
.account-authorize__avatar {
float: left;
margin-right: 10px;
}
.account-gallery__container {
@ -118,47 +60,12 @@ a .account__avatar {
}
}
.account--panel {
background: var(--brand-color--faint);
border-top: 1px solid var(--brand-color--med);
border-bottom: 1px solid var(--brand-color--med);
display: flex;
flex-direction: row;
padding: 10px 0;
&__button .svg-icon {
height: 20px;
width: 20px;
}
}
.account__moved-note {
padding: 14px 10px;
padding-bottom: 16px;
background: var(--brand-color--faint);
border-top: 1px solid var(--brand-color--med);
border-bottom: 1px solid var(--brand-color--med);
&__message {
position: relative;
margin-left: 58px;
color: var(--primary-text-color);
padding: 8px 0;
padding-top: 0;
padding-bottom: 4px;
font-size: 14px;
> span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__icon-wrapper {
left: -26px;
position: absolute;
}
}
.account__joined-at {

Wyświetl plik

@ -14,7 +14,6 @@
@import 'fonts';
@import 'basics';
@import 'accounts';
@import 'boost';
@import 'loading';
@import 'ui';
// @import 'introduction';
@ -22,7 +21,6 @@
@import 'rtl';
@import 'accessibility';
@import 'dyslexic';
@import 'chats';
@import 'navigation';
@import 'placeholder';
@import 'autosuggest';
@ -32,7 +30,6 @@
@import 'components/inputs';
@import 'components/dropdown-menu';
@import 'components/modal';
@import 'components/account-header';
@import 'components/compose-form';
@import 'components/emoji-reacts';
@import 'components/status';
@ -46,7 +43,6 @@
@import 'components/react-toggle';
@import 'components/video-player';
@import 'components/audio-player';
@import 'components/profile-hover-card';
@import 'components/filters';
@import 'components/backups';
@import 'components/crypto-donate';

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,318 +0,0 @@
.pane {
@apply flex flex-col shadow-md rounded-t-md fixed bottom-0 right-5 w-96 h-[350px] z-[1000];
max-height: calc(100vh - 70px);
transition: 0.05s;
&--main {
height: calc(100vh - 70px);
.pane__header .pane__title {
font-size: 16px;
}
}
.search--account {
border-top: 1px solid hsla(var(--primary-text-color_hsl), 0.2);
padding: 5px;
}
&__header {
@apply flex items-center py-0 px-2.5 h-14 box-border rounded-t-md font-bold bg-primary-600 text-white;
.account__avatar {
margin-right: 7px;
}
.pane__title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
height: 100%;
background: transparent;
border: 0;
padding: 0;
color: #fff;
font-weight: bold;
text-align: left;
font-size: 14px;
}
.icon-button {
opacity: 0.7;
color: #fff;
&:hover {
opacity: 1;
}
> div {
margin-right: -6px;
}
}
.pane__close {
margin-left: auto;
.svg-icon {
width: 18px;
height: 18px;
transform: translateY(2px);
}
}
}
&__content {
@apply flex flex-1 flex-col overflow-hidden bg-white dark:bg-gray-900;
> div {
@apply max-h-full;
}
.chat-box {
@apply flex flex-1 flex-col overflow-hidden;
}
.chat-list {
@apply overflow-y-auto max-h-full;
}
}
.audio-toggle .react-toggle-thumb {
@apply w-3.5 h-3.5 border border-solid border-primary-400;
}
.audio-toggle .react-toggle {
@apply top-1;
}
.audio-toggle .react-toggle-track {
@apply h-4 w-8 bg-accent-500 dark:bg-accent-500;
}
.audio-toggle .react-toggle-track-check {
left: 4px;
bottom: 0;
}
.react-toggle--checked .react-toggle-thumb {
left: 19px;
}
.audio-toggle .react-toggle-track-x {
right: 5px;
bottom: 0;
}
.fa {
font-size: 14px;
}
.chat-message--me .chat-message__bubble {
@apply dark:bg-primary-900;
}
}
.chat-messages {
overflow-y: scroll;
flex: 1;
}
.chat-message {
margin: 14px 10px;
display: flex;
&__bubble {
@apply px-2.5 py-1 rounded-lg bg-primary-50 dark:bg-gray-700;
max-width: 70%;
text-overflow: ellipsis;
overflow-wrap: break-word;
white-space: break-spaces;
position: relative;
a {
color: var(--brand-color--hicontrast);
}
&:hover,
&:focus,
&:active {
.chat-message__menu {
opacity: 1;
pointer-events: all;
}
}
}
&--me .chat-message__bubble {
@apply bg-primary-200 dark:bg-primary-800;
margin-left: auto;
}
&--pending .chat-message__bubble {
opacity: 0.5;
}
&__menu {
position: absolute;
top: -8px;
right: -8px;
height: 20px;
padding: 1px;
opacity: 0;
pointer-events: none;
transition: 0.2s;
button {
@apply p-1 bg-gray-100 dark:bg-gray-800;
svg {
@apply h-4 w-4;
}
}
}
}
.chat-list {
flex: 1;
&__content {
height: 100%;
}
.empty-column-indicator {
height: 100%;
box-sizing: border-box;
background: transparent;
align-items: start;
}
.account {
border-bottom: none;
}
.account__display-name {
position: relative;
.display-name {
display: flex;
.hover-ref-wrapper {
display: flex;
}
bdi {
overflow: hidden;
text-overflow: ellipsis;
}
.display-name__account {
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
display: none;
}
}
}
}
.chat-box {
&__attachment {
display: flex;
align-items: center;
font-size: 13px;
padding: 0 10px;
height: 25px;
.chat-box__remove-attachment {
margin-left: auto;
.icon-button > div {
display: flex;
align-items: center;
}
}
}
&__actions {
background: var(--foreground-color);
margin-top: auto;
padding: 6px;
position: relative;
textarea {
@apply pr-6;
}
}
&__send {
.icon-button,
button {
@apply absolute right-2.5 w-auto h-auto border-0 p-0 m-0 bg-transparent text-black dark:text-white;
top: 50%;
transform: translateY(-50%);
svg {
@apply w-[18px] h-[18px];
}
}
}
}
@media (max-width: 630px) {
.chat-panes {
display: none;
}
}
.chatroom__header {
display: flex;
margin-left: auto;
padding-right: 15px;
overflow: hidden;
text-decoration: none;
.account__avatar {
margin-right: 7px;
}
.chatroom__title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
height: 100%;
background: transparent;
border: 0;
padding: 0;
color: var(--primary-text-color);
font-weight: bold;
text-align: left;
font-size: 14px;
}
}
.chat-message .media-gallery {
height: 100% !important;
margin: 4px 0 8px;
.media-gallery__item:not(.media-gallery__item--image) {
max-width: 100%;
width: 120px !important;
height: 100% !important;
}
&__preview {
background-color: transparent;
}
}
.chat-messages__divider {
@apply pt-3.5 pb-0.5 text-center uppercase text-black dark:text-white;
font-size: 13px;
opacity: 0.8;
}
.floating-link {
@apply w-full h-full inset-0 absolute z-10;
}

Wyświetl plik

@ -1,24 +0,0 @@
.account__header__content {
font-size: 14px;
font-weight: 400;
overflow: hidden;
word-break: normal;
word-wrap: break-word;
p {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
a {
color: inherit;
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}

Wyświetl plik

@ -2,16 +2,6 @@
&__accounts {
overflow-y: auto;
.account__display-name {
&:hover strong {
text-decoration: none;
}
}
.account__avatar {
cursor: default;
}
&.empty-column-indicator {
min-height: unset;
overflow-y: unset;
@ -21,16 +11,4 @@
.aliases-settings-panel {
flex: 1;
.item-list article {
border-bottom: 1px solid var(--primary-text-color--faint);
&:last-child {
border-bottom: 0;
}
}
.slist--flex {
height: 100%;
}
}

Wyświetl plik

@ -7,7 +7,6 @@
}
&--pending {
font-style: italic;
color: var(--primary-text-color--faint);
@apply text-gray-400 italic;
}
}

Wyświetl plik

@ -45,14 +45,13 @@
}
&--transparent {
background: transparent;
@apply bg-transparent;
color: var(--background-color);
&:hover,
&:focus,
&:active {
background: transparent;
color: var(--primary-text-color);
@apply text-gray-900 bg-transparent;
}
&.active {
@ -66,301 +65,13 @@
margin-right: 5px;
}
.column-link__badge {
display: inline-block;
border-radius: 4px;
font-size: 12px;
line-height: 19px;
font-weight: 500;
background: var(--brand-color--med);
padding: 4px 8px;
margin: -6px 10px;
}
.column-subheading {
background: var(--brand-color--med);
color: var(--primary-text-color--faint);
padding: 8px 20px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
cursor: default;
}
.column-header__wrapper {
position: relative;
flex: 0 0 auto;
overflow: hidden;
&.active {
&::before {
display: block;
content: "";
position: absolute;
top: 35px;
left: 0;
right: 0;
margin: 0 auto;
width: 60%;
pointer-events: none;
height: 28px;
z-index: 1;
background: radial-gradient(ellipse, hsla(var(--brand-color_hsl), 0.23) 0%, hsla(var(--brand-color_hsl), 0) 60%);
}
}
}
.column-header {
display: flex;
font-size: 16px;
flex: 0 0 auto;
cursor: pointer;
position: relative;
z-index: 2;
outline: 0;
overflow-x: auto;
& > button,
& > .btn {
margin: 0;
border: 0;
padding: 15px;
color: inherit;
background: transparent;
font: inherit;
text-align: left;
text-decoration: none;
white-space: nowrap;
position: relative;
display: flex;
align-items: center;
justify-content: center;
transition: 0.2s;
&--sub {
font-size: 14px;
padding: 6px 10px;
}
&.grouped {
margin: 6px;
color: var(--primary-text-color--faint);
}
&.active {
color: var(--primary-text-color);
&::before {
height: 100%;
opacity: 1;
}
}
&::before {
content: '';
display: block;
position: absolute;
width: 100%;
background-color: var(--accent-color--faint);
border-radius: 10px;
transition: 0.2s;
opacity: 0;
z-index: -1;
}
@media screen and (max-width: $nav-breakpoint-2) {
padding: 8px;
font-size: 14px;
&.grouped {
margin: 6px 2px 6px 6px;
}
&.active {
border-radius: 5px;
}
}
}
&:hover .btn.grouped {
&::before {
height: 70% !important;
opacity: 0 !important;
}
&:hover::before {
height: 100% !important;
opacity: 1 !important;
}
&:hover {
color: var(--primary-text-color);
}
}
&.active {
box-shadow: 0 1px 0 hsla(var(--highlight-text-color_hsl), 0.3);
.column-header__icon {
color: var(--highlight-text-color);
text-shadow: 0 0 10px hsla(var(--highlight-text-color_hsl), 0.4);
}
}
&:focus,
&:active {
outline: 0;
}
}
.column-header__buttons {
height: 48px;
display: flex;
margin-left: auto;
}
.column-header__links .text-btn {
margin-right: 10px;
}
.column-header__button {
cursor: pointer;
border: 0;
padding: 0 15px;
font-size: 16px;
color: var(--primary-text-color--faint);
background: transparent;
&:hover,
&:focus {
color: hsla(var(--primary-text-color_hsl), 0.8);
}
&.active {
color: var(--primary-text-color);
background: var(--accent-color--med);
&:hover {
color: var(--primary-text-color);
background: var(--accent-color--med);
}
}
}
.column-header__collapsible {
max-height: 70vh;
overflow: hidden;
overflow-y: auto;
color: var(--primary-text-color--faint);
transition: max-height 150ms ease-in-out, opacity 300ms linear;
opacity: 1;
&.collapsed {
max-height: 0;
opacity: 0.5;
}
&.animating {
overflow-y: hidden;
}
hr {
height: 0;
background: transparent;
border: 0;
border-top: 1px solid var(--brand-color--med);
margin: 10px 0;
}
}
.column-header__collapsible-inner {
background: var(--background-color);
padding: 15px;
}
.column-header__setting-btn {
&--link {
text-decoration: none;
.fa {
margin-left: 10px;
}
}
&:hover {
color: var(--primary-text-color--faint);
text-decoration: underline;
}
}
.column-header__icon {
display: inline-block;
margin-right: 5px;
font-size: 20px;
}
.column-settings {
width: 100%;
display: flex;
flex-direction: column;
&__header {
border-bottom: 1px solid hsla(var(--primary-text-color_hsl), 0.2);
padding: 10px 20px;
display: flex;
align-items: center;
}
&__title {
font-weight: bold;
color: var(--primary-text-color--faint);
}
&__content {
padding: 10px 20px;
overflow-y: auto;
}
&__description {
font-size: 14px;
margin: 5px 0 15px;
color: var(--primary-text-color--faint);
}
&__close {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
.svg-icon {
width: 20px;
height: 20px;
margin-right: -10px;
}
}
}
.column-settings__section {
color: var(--primary-text-color--faint);
cursor: default;
display: block;
font-weight: 500;
margin-bottom: 10px;
}
.column-settings__row {
.text-btn {
margin-bottom: 15px;
&.column-header__setting-btn {
display: flex;
align-items: center;
.svg-icon {
margin-right: 10px;
}
}
@apply text-gray-400 underline;
}
}
@ -390,49 +101,3 @@
margin-bottom: 30px;
}
}
.column__switch .audio-toggle {
@apply absolute top-3 right-[14px] rtl:left-[14px] rtl:right-auto z-10;
.react-toggle-track-check {
@apply left-1.5;
}
.react-toggle-track-x {
@apply right-2;
}
}
.column--better {
.column__top {
display: flex;
align-items: center;
border-bottom: 1px solid hsla(var(--primary-text-color_hsl), 0.2);
}
.column-header {
margin-right: auto;
}
.column__menu {
display: flex;
align-items: center;
justify-content: center;
&,
> div,
button {
height: 100%;
}
button {
padding: 0 15px;
> div {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}

Wyświetl plik

@ -20,12 +20,11 @@
}
.react-datepicker__header:not(.react-datepicker__header--has-time-select) {
border-top-right-radius: var(--border-radius-lg);
@apply rounded-tr-lg;
}
.react-datepicker__header {
@apply bg-white dark:bg-gray-900 border-b-0 py-1 px-0;
// border-top-left-radius: var(--border-radius-lg);
}
.react-datepicker__current-month,

Wyświetl plik

@ -1,25 +1,3 @@
.account__display-name {
text-decoration: none;
}
.account__display-name {
strong {
@apply text-gray-800 dark:text-gray-200;
}
}
a.account__display-name {
&:hover strong {
text-decoration: underline;
}
}
.account__display-name strong {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.display-name {
display: block;
max-width: 100%;
@ -31,13 +9,10 @@ a.account__display-name {
bdi {
min-width: 0;
}
}
.display-name__html {
font-weight: 600;
padding-right: 4px;
}
.display-name__account {
font-size: 14px;
&__account {
position: relative;
font-weight: 600;
font-size: 14px;
}
}

Wyświetl plik

@ -1,12 +1,4 @@
.filter-settings-panel {
.item-list article {
border-bottom: 1px solid var(--primary-text-color--faint);
&:last-child {
border-bottom: 0;
}
}
.fields-group .two-col {
display: flex;
align-items: flex-start;

Wyświetl plik

@ -234,17 +234,6 @@
color: var(--primary-text-color--faint);
background-color: var(--background-color);
}
&.onboarding-modal__done,
&.onboarding-modal__next {
color: var(--primary-text-color);
&:hover,
&:focus,
&:active {
color: var(--primary-text-color);
}
}
}
}
@ -256,12 +245,6 @@
border: 1px solid var(--background-color);
color: var(--primary-text-color--faint);
.status__display-name {
display: block;
max-width: 100%;
padding-right: 25px;
}
.dropdown-menu__separator {
@apply block m-2 h-[1px] bg-gray-200 dark:bg-gray-600;
}
@ -327,10 +310,6 @@
display: flex;
flex-direction: column;
row-gap: 10px;
.unauthorized-modal-content__button {
margin: 0 auto;
}
}
&__fields {

Wyświetl plik

@ -1,4 +0,0 @@
.display-name__account {
position: relative;
font-weight: 600;
}

Wyświetl plik

@ -32,8 +32,7 @@
}
.react-toggle-track {
@apply bg-gray-500 dark:bg-gray-700 w-[50px] p-0 rounded-full transition-colors;
height: var(--input-height);
@apply bg-gray-500 dark:bg-gray-700 h-[30px] w-[50px] p-0 rounded-full transition-colors;
}
.react-toggle--checked .react-toggle-track {

Wyświetl plik

@ -13,41 +13,6 @@
}
opacity: 1;
animation: fade 150ms linear;
&.light {
.display-name {
strong {
color: var(--primary-text-color);
}
span {
color: var(--primary-text-color--faint);
}
}
.status__content {
color: var(--primary-text-color);
a {
color: var(--highlight-text-color);
}
}
}
&__meta {
font-size: 14px;
color: var(--primary-text-color--faint);
a {
color: var(--brand-color);
font-weight: bold;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
[column-type=filled] .status__wrapper,
@ -93,12 +58,6 @@
.focusable-within:focus-within {
outline: 0; /* Required b/c HotKeys lib sets this outline */
@apply ring-2 ring-primary-300;
.status.status-direct {
&.muted {
background: transparent;
}
}
}
.status-card {
@ -109,10 +68,6 @@ a.status-card {
@apply cursor-pointer hover:bg-gray-100 dark:hover:bg-primary-800/30 hover:no-underline;
}
.status-card-photo {
@apply cursor-zoom-in block no-underline w-full h-auto;
}
.status-card-video,
.status-card-audio {
iframe {
@ -121,23 +76,6 @@ a.status-card {
}
}
.status-card__host {
@apply text-primary-600 dark:text-accent-blue;
display: flex;
margin-top: 10px;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
align-items: center;
.svg-icon {
height: 15px;
width: 15px;
margin-right: 3px;
}
}
.status-card__image {
flex: 0 0 40%;
background: var(--brand-color--med);
@ -165,10 +103,6 @@ a.status-card {
.status-card.horizontal {
display: block;
.status-card__title {
white-space: inherit;
}
}
.status-card.compact {
@ -194,27 +128,9 @@ a.status-card {
.status {
padding: 8px 10px;
&__avatar {
position: relative;
margin-right: 10px;
top: 0;
left: 0;
}
&__profile {
display: flex;
align-items: center;
}
&__content {
padding-top: 10px;
}
&__display-name {
.display-name__account {
display: block;
}
}
}
}

Wyświetl plik

@ -2,13 +2,3 @@
font-family: 'OpenDyslexic' !important;
margin-bottom: 8px;
}
body.dyslexic {
@media screen and (max-width: $nav-breakpoint-2) {
.column-header > button,
.column-header > .btn {
font-size: 11px;
}
}
}

Wyświetl plik

@ -22,14 +22,6 @@ select {
margin-left: -1px;
}
$no-columns-breakpoint: 600px;
.form-container {
max-width: 400px;
padding: 20px;
margin: 0 auto;
}
.simple_form {
.input {
&.hidden {
@ -194,31 +186,6 @@ $no-columns-breakpoint: 600px;
}
}
.input.font_icon_picker {
width: 52px;
}
.input.with_block_label {
max-width: none;
& > label {
font-family: inherit;
font-size: 16px;
color: var(--primary-text-color);
display: block;
font-weight: 500;
padding-top: 5px;
}
.hint {
margin-bottom: 15px;
}
ul {
columns: 2;
}
}
.required abbr {
text-decoration: none;
color: lighten($error-value-color, 12%);
@ -236,48 +203,6 @@ $no-columns-breakpoint: 600px;
}
}
.fields-row {
display: flex;
margin: 0 -10px;
padding-top: 5px;
margin-bottom: 25px;
.input {
max-width: none;
}
&__column {
box-sizing: border-box;
padding: 0 10px;
flex: 1 1 auto;
min-height: 1px;
&-6 {
max-width: 50%;
}
}
.fields-group:last-child,
.fields-row__column.fields-group {
margin-bottom: 0;
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;
margin-bottom: 0;
&__column {
max-width: none;
}
.fields-group:last-child,
.fields-row__column.fields-group,
.fields-row__column {
margin-bottom: 25px;
}
}
}
.input.radio_buttons .radio label {
margin-bottom: 5px;
font-family: inherit;
@ -390,11 +315,6 @@ $no-columns-breakpoint: 600px;
margin-top: 20px;
display: flex;
justify-content: flex-end;
&.actions--top {
margin-top: 0;
margin-bottom: 30px;
}
}
// button,
@ -483,98 +403,6 @@ $no-columns-breakpoint: 600px;
// padding-top: 0.5rem;
// }
.select-wrapper {
display: flex;
align-items: center;
&::after {
display: flex;
align-items: center;
font-family: 'Font Awesome 5 Free';
content: "";
position: absolute;
right: 12px;
height: calc(100% - 8px);
padding-left: 12px;
pointer-events: none;
margin-top: 8px;
font-weight: 900;
}
}
.label_input {
&__color {
display: inline-flex;
font-size: 14px;
.color-swatch {
width: 32px;
height: 16px;
margin-left: 12px;
}
}
&__wrapper {
position: relative;
}
&__append {
position: absolute;
right: 3px;
top: 1px;
padding: 10px;
padding-bottom: 9px;
font-size: 16px;
color: var(--primary-text-color);
font-family: inherit;
pointer-events: none;
cursor: default;
max-width: 140px;
white-space: nowrap;
overflow: hidden;
&::after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 1px;
width: 5px;
background-image: linear-gradient(to right, hsla(var(--background-color_hsl), 0), var(--background-color));
}
}
}
&__overlay-area {
position: relative;
&__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: var(--background-color);
backdrop-filter: blur(2px);
border-radius: 4px;
&__content {
text-align: center;
&.rich-formatting {
&,
p {
color: var(--primary-text-color);
}
}
}
}
}
h2 {
font-size: 20px;
line-height: normal;
@ -637,52 +465,6 @@ $no-columns-breakpoint: 600px;
}
}
.file-picker img {
max-width: 100px;
max-height: 100px;
}
.code-editor {
textarea {
font-family: monospace;
white-space: pre;
}
&--invalid textarea {
border-color: $error-red !important;
color: $error-red;
}
.input {
margin-bottom: 0;
}
.hint {
margin-top: 10px;
}
}
.delete-account {
margin-top: 50px;
display: flex;
justify-content: center;
}
.input .row > .svg-icon.delete-field {
height: 20px;
width: 20px;
position: absolute;
right: 13px;
cursor: pointer;
color: $error-red;
transform: translateY(-11px);
}
.input .row > .input.with_label + .svg-icon.delete-field {
right: 5px;
transform: translateY(7px);
}
.input.with_label.toggle .label_input {
display: flex;
font-size: 14px;
@ -692,48 +474,3 @@ $no-columns-breakpoint: 600px;
margin-left: 10px;
}
}
.actions.add-row {
margin: 10px 0 0;
.button {
display: flex;
align-items: center;
border: 0;
background: transparent;
&:hover {
color: var(--primary-text-color);
}
.svg-icon {
height: 20px;
width: 20px;
}
}
}
.copyable-input {
display: flex;
align-items: center;
justify-content: center;
height: 38px;
input {
flex: 1;
font-size: 14px !important;
border-radius: 4px 0 0 4px !important;
height: 100%;
}
button {
width: auto;
font-size: 14px;
margin: 0;
border-radius: 0 4px 4px 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}

Wyświetl plik

@ -31,16 +31,6 @@
100% { background-position-x: 0; }
}
.chat-list-item--placeholder .chat__last-message {
letter-spacing: -1px;
color: var(--brand-color) !important;
opacity: 0.1;
}
.slist {
position: relative;
}
.media-gallery.media-gallery--placeholder {
background: none;

Wyświetl plik

@ -1,59 +1,16 @@
body.rtl {
direction: rtl;
.column-header > button {
text-align: right;
padding-left: 0;
padding-right: 15px;
}
.column-link__icon,
.column-header__icon {
.column-link__icon {
margin-right: 0;
margin-left: 5px;
}
.search__icon .fa {
right: auto;
left: 10px;
}
.column-header__buttons {
left: 0;
right: auto;
margin-left: 0;
margin-right: -15px;
}
.column-inline-form .icon-button {
margin-left: 0;
margin-right: 5px;
}
.column-header__links .text-btn {
margin-left: 10px;
margin-right: 0;
}
.account__avatar-wrapper {
float: right;
}
.setting-toggle__label {
margin-left: 0;
margin-right: 8px;
}
.status {
padding-left: 10px;
padding-right: 68px;
}
.account__avatar-overlay-overlay {
right: auto;
left: 0;
}
.privacy-dropdown__dropdown {
margin-left: 0;
margin-right: 40px;
@ -64,16 +21,6 @@ body.rtl {
margin-right: 0;
}
.fa-ul {
margin-left: 0;
margin-left: 2.14285714em;
}
.fa-li {
left: auto;
right: -2.14285714em;
}
.simple_form .input.with_label.boolean label.checkbox {
padding-left: 25px;
padding-right: 0;
@ -121,29 +68,6 @@ body.rtl {
}
}
.public-layout {
.header {
.nav-button {
margin-left: 8px;
margin-right: 0;
}
}
}
.card__bar .display-name {
margin-left: 0;
margin-right: 15px;
text-align: right;
}
.fa-chevron-left::before {
content: "\F054";
}
.fa-chevron-right::before {
content: "\F053";
}
.simple_form .input.radio_buttons .radio > label input {
left: auto;
right: 0;

Wyświetl plik

@ -61,29 +61,11 @@ body,
// Colors
--gray-900: #08051b;
// --gray-800: #1d1932;
--gray-700: #37344c;
--gray-500: #656175;
--gray-400: #868393;
--gray-300: #c9c8cc;
--gray-50: #f9f8fc;
--white: #fff;
--dark-blue: #1d1953;
--electric-blue: #5448ee;
--electric-blue-contrast: #e8e7fd;
// Sizes
--border-radius-base: 4px;
--border-radius-lg: 8px;
--border-radius-xl: 12px;
// Forms
--input-height: 30px;
--input-border-color: #d1d5db;
// Typography
--font-sans: 'Inter', arial, sans-serif;
--font-weight-heading: 700;
--font-weight-body: 400;
}
.theme-mode-light {
@ -93,7 +75,6 @@ body,
var(--brand-color_s),
calc(var(--brand-color_l) - 8%)
);
--vignette-color: transparent;
// Meta-variables
--primary-text-color_h: 0;
@ -115,45 +96,4 @@ body,
var(--brand-color_s),
calc(var(--brand-color_l) - 5%)
);
--warning-color--hicontrast: hsl(
var(--warning-color_h),
var(--warning-color_s),
calc(var(--warning-color_l) - 12%)
);
}
.theme-mode-dark {
// Primary variables
--highlight-text-color: hsl(
var(--brand-color_h),
var(--brand-color_s),
calc(var(--brand-color_l) + 8%)
);
--vignette-color: #000;
// Meta-variables
--primary-text-color_h: 0;
--primary-text-color_s: 0%;
--primary-text-color_l: 100%;
--background-color_h: 0;
--background-color_s: 0%;
--background-color_l: 20%;
--foreground-color_h: 0;
--foreground-color_s: 0%;
--foreground-color_l: 13%;
--warning-color_h: 0;
--warning-color_s: 100%;
--warning-color_l: 66%;
// Modifiers
--brand-color--hicontrast: hsl(
var(--brand-color_h),
var(--brand-color_s),
calc(var(--brand-color_l) + 12%)
);
--warning-color--hicontrast: hsl(
var(--warning-color_h),
var(--warning-color_s),
calc(var(--warning-color_l) + 12%)
);
}

Wyświetl plik

@ -44,87 +44,6 @@
&:active {
outline: 0 !important;
}
&.inverted {
color: var(--primary-text-color--faint);
opacity: 1;
&:hover,
&:active,
&:focus {
color: var(--primary-text-color);
}
&.disabled {
color: var(--primary-text-color--faint);
}
&.active {
color: var(--highlight-text-color);
&.disabled {
color: var(--highlight-text-color);
}
}
}
&.overlayed {
box-sizing: content-box;
background: var(--foreground-color);
color: var(--primary-text-color--faint);
border-radius: 6px;
padding: 2px;
opacity: 1;
> div {
display: flex;
align-items: center;
justify-content: center;
}
&:hover {
background: var(--background-color);
}
}
}
.text-icon-button {
color: var(--primary-text-color--faint);
border: 0;
background: transparent;
cursor: pointer;
font-weight: 600;
font-size: 11px;
padding: 0 3px;
line-height: 27px;
outline: 0;
transition: color 100ms ease-in;
&:hover,
&:active,
&:focus {
color: var(--primary-text-color);
transition: color 200ms ease-out;
}
&.disabled {
color: var(--primary-text-color--faint);
cursor: default;
}
&.active {
color: var(--highlight-text-color);
}
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
}
.invisible {
@ -151,10 +70,6 @@
.ellipsis::after { content: ""; }
.no-reduce-motion .spoiler-input {
transition: height 0.4s ease, opacity 0.4s ease;
}
.image-loader {
position: relative;
width: 100%;
@ -205,53 +120,17 @@
width: 100%;
padding: 0 0 calc(var(--thumb-navigation-height) + 86px);
.bg-shape-container {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.page {
display: flex;
flex-direction: column;
width: 100%;
&__top {
@include standard-panel-shadow;
display: flex;
width: 100%;
height: auto;
z-index: 1000;
background: var(--foreground-color);
@media (min-width: 896px) {
top: -290px;
position: sticky;
}
}
&__columns {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
}
}
.slist {
&--flex {
display: flex;
flex-direction: column;
}
&__append {
flex: 1 1 auto;
position: relative;
padding: 30px 15px;
}
.slist__append {
flex: 1 1 auto;
position: relative;
padding: 30px 15px;
}
.setting-text {
@ -277,34 +156,6 @@
}
}
.morefollows-indicator {
text-align: center;
font-size: 16px;
font-weight: 500;
color: var(--primary-text-color);
background: var(--brand-color--med);
cursor: default;
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: center;
padding: 20px;
& > div {
width: 100%;
background: transparent;
padding-top: 0;
}
&__label {
strong {
display: block;
margin-bottom: 10px;
color: var(--primary-text-color);
}
}
}
.text-btn {
display: inline-block;
padding: 0;
@ -316,170 +167,6 @@
cursor: pointer;
}
.account--panel__button {
flex: 1 1 auto;
text-align: center;
}
.emoji-button {
display: block;
font-size: 24px;
line-height: 24px;
margin-left: 2px;
width: 24px;
outline: 0;
cursor: pointer;
&:active,
&:focus {
outline: 0 !important;
}
img {
filter: grayscale(100%);
opacity: 0.8;
display: block;
margin: 0;
width: 22px;
height: 22px;
margin-top: 2px;
}
&:hover,
&:active,
&:focus {
img {
opacity: 1;
filter: none;
}
}
}
.attachment-list {
display: flex;
font-size: 14px;
border: 1px solid var(--brand-color--med);
border-radius: 4px;
margin-top: 14px;
overflow: hidden;
&__icon {
flex: 0 0 auto;
color: var(--primary-text-color);
padding: 8px 18px;
cursor: default;
border-right: 1px solid var(--brand-color--med);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 26px;
.fa {
display: block;
}
}
&__list {
list-style: none;
padding: 4px 0;
padding-left: 8px;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
white-space: nowrap;
li {
display: block;
padding: 4px 0;
overflow: hidden;
text-overflow: ellipsis;
}
a {
text-decoration: none;
color: var(--primary-text-color);
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
}
&.compact {
border: 0;
margin-top: 4px;
.attachment-list__list {
padding: 0;
display: block;
}
.fa {
color: var(--primary-text-color);
}
}
}
.filter-bar {
cursor: default;
display: flex;
flex-shrink: 0;
button {
border: 0;
margin: 0;
}
button,
a {
@apply block flex-1 text-gray-500 py-4 text-sm font-semibold text-center relative no-underline;
&:active,
&:focus,
&:hover,
&.active {
@apply text-gray-900;
}
.svg-icon {
width: 18px;
height: 18px;
margin: 0 auto;
}
}
}
.filter-bar {
@apply relative;
&__active {
@apply absolute h-[3px] bottom-0 bg-primary-600;
}
&__bottom {
@apply absolute h-[3px] w-full bottom-0 bg-primary-200;
}
}
.no-reduce-motion .filter-bar__active {
transition: all 0.3s;
}
.reaction__filter-bar {
overflow-x: auto;
overflow-y: hidden;
button,
a {
flex: unset;
padding: 15px 24px;
min-width: max-content;
}
}
::-webkit-scrollbar-thumb {
border-radius: 0;
}

Wyświetl plik

@ -1,56 +1,10 @@
// Truth Colors
$color-1: #c62828;
$color-1-dark: #8e0000;
$color-1-light: #ff5f52;
$color-2: #e53935;
$color-2-dark: #ab000d;
$color-2-light: #ff6f60;
$color-3: #1a237e;
$color-3-dark: #000051;
$color-3-light: #534bae;
$color-4: #3949ab;
$color-4-dark: #00227b;
$color-4-light: #6f74dd;
$color-5: #37474f;
$color-5-dark: #102027;
$color-5-light: #62727b;
$color-6: #f5f5f5;
$color-6-dark: #c2c2c2;
$color-6-light: #fff;
$color-7: #00e676;
$color-7-dark: #00b248;
$color-7-light: #66ffa6;
$color-8: #ffea00;
$color-8-dark: #c7b800;
$color-8-light: #ffff56;
$color-9: #ffab00;
$color-9-dark: #c67c00;
$color-9-light: #ffdd4b;
// BREAKPOINT SETS
// navigation breakpoints - by default show all elements and link names along with icons
// turns navigation links into icon-only buttons
$nav-breakpoint-1: 850px;
// search field hidden and replaced with search icon link
$nav-breakpoint-2: 650px;
// "Post" button hidden and replaced with floating button on bottom corner
$nav-breakpoint-3: 450px;
// Site Logo hidden - bare minimum navigation for smallest width devices
$nav-breakpoint-4: 375px;
// Commonly used web colors
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
// Variables for defaults in UI
$base-shadow-color: #000 !default;
$base-overlay-background: #000 !default;
$valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Language codes that uses CJK fonts
@ -68,8 +22,5 @@ $no-gap-breakpoint: 415px;
// NOTE: Prefer CSS variables whenever possible.
// They're future-proof and more flexible.
:root {
--thumb-navigation-base-height: 60px;
--thumb-navigation-height: calc(
var(--thumb-navigation-base-height) + env(safe-area-inset-bottom)
);
--thumb-navigation-height: calc(60px + env(safe-area-inset-bottom));
}

Wyświetl plik

@ -41,7 +41,7 @@ module.exports = {
'transformIgnorePatterns': [
// FIXME: react-sticky-box doesn't provide a CJS build, so transform it for now
// https://github.com/codecks-io/react-sticky-box/issues/79
`/node_modules/(?!(react-sticky-box|.+\\.(${ASSET_EXTS})$))`,
`/node_modules/(?!(react-sticky-box|blurhash|.+\\.(${ASSET_EXTS})$))`,
// Ignore node_modules, except static assets
// `/node_modules/(?!.+\\.(${ASSET_EXTS})$)`,
],

Wyświetl plik

@ -54,7 +54,7 @@
"@gamestdio/websocket": "^0.3.2",
"@jest/globals": "^29.0.0",
"@lcdp/offline-plugin": "^5.1.0",
"@metamask/providers": "^9.0.0",
"@metamask/providers": "^10.0.0",
"@popperjs/core": "^2.11.5",
"@reach/combobox": "^0.18.0",
"@reach/menu-button": "^0.18.0",
@ -105,7 +105,7 @@
"babel-plugin-react-intl": "^7.5.20",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-plugin-transform-require-context": "^0.1.1",
"blurhash": "^1.1.5",
"blurhash": "^2.0.0",
"bootstrap-icons": "^1.5.0",
"bowser": "^2.11.0",
"browserslist": "^4.16.6",
@ -117,19 +117,18 @@
"css-loader": "^6.7.1",
"cssnano": "^5.1.10",
"detect-passive-events": "^2.0.0",
"dotenv": "^8.0.0",
"dotenv": "^16.0.0",
"emoji-datasource": "5.0.1",
"emoji-mart": "npm:emoji-mart-lazyload",
"es6-symbol": "^3.1.1",
"escape-html": "^1.0.3",
"exif-js": "^2.3.0",
"feather-icons": "^4.28.0",
"fork-ts-checker-webpack-plugin": "^7.2.11",
"history": "^4.10.1",
"html-webpack-harddisk-plugin": "^2.0.0",
"html-webpack-plugin": "^5.5.0",
"http-link-header": "^1.0.2",
"immutable": "^4.0.0",
"immutable": "^4.2.1",
"imports-loader": "^4.0.0",
"intersection-observer": "^0.12.0",
"intl": "^1.2.5",
@ -239,7 +238,7 @@
"react-refresh": "^0.14.0",
"stylelint": "^13.7.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.18.0",
"stylelint-scss": "^4.0.0",
"tailwindcss": "^3.2.1",
"ts-jest": "^29.0.0",
"webpack-dev-server": "^4.9.1",

Wyświetl plik

@ -59,14 +59,6 @@ const rules: RuleSetRule[] = [{
filename: 'packs/icons/[name]-[contenthash:8][ext]',
},
}, {
test: /\.svg$/,
type: 'asset/resource',
include: resolve('node_modules', 'feather-icons'),
generator: {
filename: 'packs/icons/[name]-[contenthash:8][ext]',
},
}, {
test: /\.svg$/,
type: 'asset/resource',
include: [

Wyświetl plik

@ -1906,10 +1906,10 @@
once "^1.4.0"
readable-stream "^2.3.3"
"@metamask/providers@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-9.0.0.tgz#644684f9eceb952138e80afb9103c7e39d8350fe"
integrity sha512-9qUkaFafZUROK0CAUBqjsut+7mqKOXFhBCpAhAPVRBqj5TfUTdPI4t8S7GYzPVaDbC7M6kH/YLNCgcfaFWAS+w==
"@metamask/providers@^10.0.0":
version "10.2.1"
resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-10.2.1.tgz#61304940adeccc7421dcda30ffd1d834273cc77b"
integrity sha512-p2TXw2a1Nb8czntDGfeIYQnk4LLVbd5vlcb3GY//lylYlKdSqp+uUTegCvxiFblRDOT68jsY8Ib1VEEzVUOolA==
dependencies:
"@metamask/object-multiplex" "^1.1.0"
"@metamask/safe-event-emitter" "^2.0.0"
@ -1920,7 +1920,7 @@
fast-deep-equal "^2.0.1"
is-stream "^2.0.0"
json-rpc-engine "^6.1.0"
json-rpc-middleware-stream "^3.0.0"
json-rpc-middleware-stream "^4.2.1"
pump "^3.0.0"
webextension-polyfill-ts "^0.25.0"
@ -3907,10 +3907,10 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
blurhash@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.5.tgz#3034104cd5dce5a3e5caa871ae2f0f1f2d0ab566"
integrity sha512-a+LO3A2DfxTaTztsmkbLYmUzUeApi0LZuKalwbNmqAHR6HhJGMt1qSV/R3wc+w4DL28holjqO3Bg74aUGavGjg==
blurhash@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.4.tgz#60642a823b50acaaf3732ddb6c7dfd721bdfef2a"
integrity sha512-r/As72u2FbucLoK5NTegM/GucxJc3d8GvHc4ngo13IO/nt2HU4gONxNLq1XPN6EM/V8Y9URIa7PcSz2RZu553A==
body-parser@1.20.0:
version "1.20.0"
@ -4493,7 +4493,7 @@ core-js-pure@^3.23.3:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.27.1.tgz#ede4a6b8440585c7190062757069c01d37a19dca"
integrity sha512-BS2NHgwwUppfeoqOXqi08mUqS5FiZpuRuJJpKsaME7kJz0xxuk0xkhDdfMIlP/zLa80krBqss1LtD7f889heAw==
core-js@^3.1.3, core-js@^3.15.2:
core-js@^3.15.2:
version "3.18.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.0.tgz#9af3f4a6df9ba3428a3fb1b171f1503b3f40cc49"
integrity sha512-WJeQqq6jOYgVgg4NrXKL0KLQhi0CT4ZOCvFL+3CQ5o7I6J8HkT5wd53EadMfqTDp1so/MT1J+w2ujhWcCJtN7w==
@ -5088,10 +5088,10 @@ dot-case@^3.0.4:
no-case "^3.0.4"
tslib "^2.0.3"
dotenv@^8.0.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
dotenv@^16.0.0:
version "16.0.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
duplexer@^0.1.2:
version "0.1.2"
@ -5894,14 +5894,6 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
feather-icons@^4.28.0:
version "4.28.0"
resolved "https://registry.yarnpkg.com/feather-icons/-/feather-icons-4.28.0.tgz#e1892a401fe12c4559291770ff6e68b0168e760f"
integrity sha512-gRdqKESXRBUZn6Nl0VBq2wPHKRJgZz7yblrrc2lYsS6odkNFDnA4bqvrlEVRUPjE1tFax+0TdbJKZ31ziJuzjg==
dependencies:
classnames "^2.2.5"
core-js "^3.1.3"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -6686,10 +6678,10 @@ immer@^9.0.7:
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20"
integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
immutable@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.1.tgz#8a4025691018c560a40c67e43d698f816edc44d4"
integrity sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==
import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0"
@ -7717,10 +7709,10 @@ json-rpc-engine@^6.1.0:
"@metamask/safe-event-emitter" "^2.0.0"
eth-rpc-errors "^4.0.2"
json-rpc-middleware-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-3.0.0.tgz#8540331d884f36b9e0ad31054cc68ac6b5a89b52"
integrity sha512-JmZmlehE0xF3swwORpLHny/GvW3MZxCsb2uFNBrn8TOqMqivzCfz232NSDLLOtIQlrPlgyEjiYpyzyOPFOzClw==
json-rpc-middleware-stream@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-4.2.1.tgz#e5cb8795ebfd7503c6ceaa43daaf065687cc2f22"
integrity sha512-6QKayke/8lg0nTjOpRCq4JCgRx7bVybldmloIfY21HSDV0GUevcV9i8DJNvuKTJx4KR9EDIf6HTStAnEovGUvA==
dependencies:
"@metamask/safe-event-emitter" "^2.0.0"
readable-stream "^2.3.3"
@ -9383,6 +9375,14 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.6:
version "6.0.11"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc"
integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-svgo@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d"
@ -11013,15 +11013,15 @@ stylelint-config-standard@^22.0.0:
dependencies:
stylelint-config-recommended "^5.0.0"
stylelint-scss@^3.18.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.21.0.tgz#9f50898691b16b1c1ca3945837381d98c5b22331"
integrity sha512-CMI2wSHL+XVlNExpauy/+DbUcB/oUZLARDtMIXkpV/5yd8nthzylYd1cdHeDMJVBXeYHldsnebUX6MoV5zPW4A==
stylelint-scss@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-4.3.0.tgz#638800faf823db11fff60d537c81051fe74c90fa"
integrity sha512-GvSaKCA3tipzZHoz+nNO7S02ZqOsdBzMiCx9poSmLlb3tdJlGddEX/8QzCOD8O7GQan9bjsvLMsO5xiw6IhhIQ==
dependencies:
lodash "^4.17.15"
lodash "^4.17.21"
postcss-media-query-parser "^0.2.3"
postcss-resolve-nested-selector "^0.1.1"
postcss-selector-parser "^6.0.2"
postcss-selector-parser "^6.0.6"
postcss-value-parser "^4.1.0"
stylelint@^13.7.2: