Merge branch 'cleanup' into 'develop'
TSX, functional, remove unused styles See merge request soapbox-pub/soapbox!2040
This commit is contained in:
commit
02509cb40d
|
@ -2,7 +2,9 @@ import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||||
|
import { AccountRecord } from 'soapbox/normalizers';
|
||||||
|
|
||||||
|
import { AuthUserRecord, ReducerRecord } from '../../reducers/auth';
|
||||||
import {
|
import {
|
||||||
fetchMe, patchMe,
|
fetchMe, patchMe,
|
||||||
} from '../me';
|
} from '../me';
|
||||||
|
@ -38,18 +40,18 @@ describe('fetchMe()', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState
|
const state = rootState
|
||||||
.set('auth', ImmutableMap({
|
.set('auth', ReducerRecord({
|
||||||
me: accountUrl,
|
me: accountUrl,
|
||||||
users: ImmutableMap({
|
users: ImmutableMap({
|
||||||
[accountUrl]: ImmutableMap({
|
[accountUrl]: AuthUserRecord({
|
||||||
'access_token': token,
|
'access_token': token,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
.set('accounts', ImmutableMap({
|
.set('accounts', ImmutableMap({
|
||||||
[accountUrl]: {
|
[accountUrl]: AccountRecord({
|
||||||
url: accountUrl,
|
url: accountUrl,
|
||||||
},
|
}),
|
||||||
}) as any);
|
}) as any);
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
||||||
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
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 nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
||||||
|
|
||||||
const fetchConfig = () =>
|
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 {
|
export {
|
||||||
ADMIN_CONFIG_FETCH_REQUEST,
|
ADMIN_CONFIG_FETCH_REQUEST,
|
||||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||||
|
@ -596,6 +650,13 @@ export {
|
||||||
ADMIN_USERS_UNSUGGEST_REQUEST,
|
ADMIN_USERS_UNSUGGEST_REQUEST,
|
||||||
ADMIN_USERS_UNSUGGEST_SUCCESS,
|
ADMIN_USERS_UNSUGGEST_SUCCESS,
|
||||||
ADMIN_USERS_UNSUGGEST_FAIL,
|
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,
|
fetchConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
updateSoapboxConfig,
|
updateSoapboxConfig,
|
||||||
|
@ -622,4 +683,7 @@ export {
|
||||||
setRole,
|
setRole,
|
||||||
suggestUsers,
|
suggestUsers,
|
||||||
unsuggestUsers,
|
unsuggestUsers,
|
||||||
|
setUserIndexQuery,
|
||||||
|
fetchUserIndex,
|
||||||
|
expandUserIndex,
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,6 @@ import api, { baseClient } from '../api';
|
||||||
import { importFetchedAccount } from './importer';
|
import { importFetchedAccount } from './importer';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
|
||||||
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
||||||
|
@ -94,11 +93,11 @@ const createAuthApp = () =>
|
||||||
|
|
||||||
const createAppToken = () =>
|
const createAppToken = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const app = getState().auth.get('app');
|
const app = getState().auth.app;
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id: app.get('client_id'),
|
client_id: app.client_id!,
|
||||||
client_secret: app.get('client_secret'),
|
client_secret: app.client_secret!,
|
||||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
grant_type: 'client_credentials',
|
grant_type: 'client_credentials',
|
||||||
scope: getScopes(getState()),
|
scope: getScopes(getState()),
|
||||||
|
@ -111,11 +110,11 @@ const createAppToken = () =>
|
||||||
|
|
||||||
const createUserToken = (username: string, password: string) =>
|
const createUserToken = (username: string, password: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const app = getState().auth.get('app');
|
const app = getState().auth.app;
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id: app.get('client_id'),
|
client_id: app.client_id!,
|
||||||
client_secret: app.get('client_secret'),
|
client_secret: app.client_secret!,
|
||||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
grant_type: 'password',
|
grant_type: 'password',
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -127,32 +126,12 @@ const createUserToken = (username: string, password: string) =>
|
||||||
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
.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) =>
|
export const otpVerify = (code: string, mfa_token: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const app = getState().auth.get('app');
|
const app = getState().auth.app;
|
||||||
return api(getState, 'app').post('/oauth/mfa/challenge', {
|
return api(getState, 'app').post('/oauth/mfa/challenge', {
|
||||||
client_id: app.get('client_id'),
|
client_id: app.client_id,
|
||||||
client_secret: app.get('client_secret'),
|
client_secret: app.client_secret,
|
||||||
mfa_token: mfa_token,
|
mfa_token: mfa_token,
|
||||||
code: code,
|
code: code,
|
||||||
challenge_type: 'totp',
|
challenge_type: 'totp',
|
||||||
|
@ -211,7 +190,7 @@ export const logIn = (username: string, password: string) =>
|
||||||
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
|
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
|
||||||
return dispatch(createUserToken(normalizeUsername(username), password));
|
return dispatch(createUserToken(normalizeUsername(username), password));
|
||||||
}).catch((error: AxiosError) => {
|
}).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.
|
// If MFA is required, throw the error and handle it in the component.
|
||||||
throw error;
|
throw error;
|
||||||
} else {
|
} else {
|
||||||
|
@ -233,9 +212,9 @@ export const logOut = () =>
|
||||||
if (!account) return dispatch(noOp);
|
if (!account) return dispatch(noOp);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id: state.auth.getIn(['app', 'client_id']),
|
client_id: state.auth.app.client_id!,
|
||||||
client_secret: state.auth.getIn(['app', 'client_secret']),
|
client_secret: state.auth.app.client_secret!,
|
||||||
token: state.auth.getIn(['users', account.url, 'access_token']),
|
token: state.auth.users.get(account.url)!.access_token,
|
||||||
};
|
};
|
||||||
|
|
||||||
return dispatch(revokeOAuthToken(params))
|
return dispatch(revokeOAuthToken(params))
|
||||||
|
@ -263,10 +242,10 @@ export const switchAccount = (accountId: string, background = false) =>
|
||||||
export const fetchOwnAccounts = () =>
|
export const fetchOwnAccounts = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
return state.auth.get('users').forEach((user: ImmutableMap<string, string>) => {
|
return state.auth.users.forEach((user) => {
|
||||||
const account = state.accounts.get(user.get('id'));
|
const account = state.accounts.get(user.id);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
dispatch(verifyCredentials(user.get('access_token')!, user.get('url')));
|
dispatch(verifyCredentials(user.access_token, user.url));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,12 +10,12 @@ import api from '../api';
|
||||||
|
|
||||||
const getMeUrl = (state: RootState) => {
|
const getMeUrl = (state: RootState) => {
|
||||||
const me = state.me;
|
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 */
|
/** Figure out the appropriate instance to fetch depending on the state */
|
||||||
export const getHost = (state: RootState) => {
|
export const getHost = (state: RootState) => {
|
||||||
const accountUrl = getMeUrl(state) || getAuthUserUrl(state);
|
const accountUrl = getMeUrl(state) || getAuthUserUrl(state) as string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new URL(accountUrl).host;
|
return new URL(accountUrl).host;
|
||||||
|
|
|
@ -30,8 +30,8 @@ const getMeUrl = (state: RootState) => {
|
||||||
|
|
||||||
const getMeToken = (state: RootState) => {
|
const getMeToken = (state: RootState) => {
|
||||||
// Fallback for upgrading IDs to URLs
|
// Fallback for upgrading IDs to URLs
|
||||||
const accountUrl = getMeUrl(state) || state.auth.get('me');
|
const accountUrl = getMeUrl(state) || state.auth.me;
|
||||||
return state.auth.getIn(['users', accountUrl, 'access_token']);
|
return state.auth.users.get(accountUrl!)?.access_token;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMe = () =>
|
const fetchMe = () =>
|
||||||
|
@ -46,7 +46,7 @@ const fetchMe = () =>
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fetchMeRequest());
|
dispatch(fetchMeRequest());
|
||||||
return dispatch(loadCredentials(token, accountUrl))
|
return dispatch(loadCredentials(token, accountUrl!))
|
||||||
.catch(error => dispatch(fetchMeFail(error)));
|
.catch(error => dispatch(fetchMeFail(error)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ const maybeParseJSON = (data: string) => {
|
||||||
|
|
||||||
const getAuthBaseURL = createSelector([
|
const getAuthBaseURL = createSelector([
|
||||||
(state: RootState, me: string | false | null) => state.accounts.getIn([me, 'url']),
|
(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) => {
|
], (accountUrl, authUserUrl) => {
|
||||||
const baseURL = parseBaseURL(accountUrl) || parseBaseURL(authUserUrl);
|
const baseURL = parseBaseURL(accountUrl) || parseBaseURL(authUserUrl);
|
||||||
return baseURL !== window.location.origin ? baseURL : '';
|
return baseURL !== window.location.origin ? baseURL : '';
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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
|
|
||||||
});
|
|
|
@ -46,7 +46,7 @@ interface IProfilePopper {
|
||||||
const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }): any =>
|
const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }): any =>
|
||||||
condition ? wrapper(children) : children;
|
condition ? wrapper(children) : children;
|
||||||
|
|
||||||
interface IAccount {
|
export interface IAccount {
|
||||||
account: AccountEntity,
|
account: AccountEntity,
|
||||||
action?: React.ReactElement,
|
action?: React.ReactElement,
|
||||||
actionAlignment?: 'center' | 'top',
|
actionAlignment?: 'center' | 'top',
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
@ -23,7 +23,6 @@ import StatusReplyMentions from './status-reply-mentions';
|
||||||
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
||||||
import { Card, HStack, Stack, Text } from './ui';
|
import { Card, HStack, Stack, Text } from './ui';
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
|
||||||
import type {
|
import type {
|
||||||
Account as AccountEntity,
|
Account as AccountEntity,
|
||||||
Status as StatusEntity,
|
Status as StatusEntity,
|
||||||
|
@ -45,7 +44,6 @@ export interface IStatus {
|
||||||
unread?: boolean,
|
unread?: boolean,
|
||||||
onMoveUp?: (statusId: string, featured?: boolean) => void,
|
onMoveUp?: (statusId: string, featured?: boolean) => void,
|
||||||
onMoveDown?: (statusId: string, featured?: boolean) => void,
|
onMoveDown?: (statusId: string, featured?: boolean) => void,
|
||||||
group?: ImmutableMap<string, any>,
|
|
||||||
focusable?: boolean,
|
focusable?: boolean,
|
||||||
featured?: boolean,
|
featured?: boolean,
|
||||||
hideActionBar?: boolean,
|
hideActionBar?: boolean,
|
||||||
|
|
|
@ -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));
|
|
|
@ -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;
|
|
@ -4,9 +4,8 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { closeReports } from 'soapbox/actions/admin';
|
import { closeReports } from 'soapbox/actions/admin';
|
||||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
||||||
import Avatar from 'soapbox/components/avatar';
|
|
||||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
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 DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
import { makeGetReport } from 'soapbox/selectors';
|
import { makeGetReport } from 'soapbox/selectors';
|
||||||
|
@ -86,7 +85,7 @@ const Report: React.FC<IReport> = ({ id }) => {
|
||||||
<HStack space={3} className='p-3' key={report.id}>
|
<HStack space={3} className='p-3' key={report.id}>
|
||||||
<HoverRefWrapper accountId={targetAccount.id} inline>
|
<HoverRefWrapper accountId={targetAccount.id} inline>
|
||||||
<Link to={`/@${acct}`} title={acct}>
|
<Link to={`/@${acct}`} title={acct}>
|
||||||
<Avatar account={targetAccount} size={32} />
|
<Avatar src={targetAccount.avatar} size={32} className='overflow-hidden' />
|
||||||
</Link>
|
</Link>
|
||||||
</HoverRefWrapper>
|
</HoverRefWrapper>
|
||||||
|
|
||||||
|
|
|
@ -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));
|
|
|
@ -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;
|
|
@ -7,8 +7,6 @@ import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack,
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
import { Token } from 'soapbox/reducers/security';
|
import { Token } from 'soapbox/reducers/security';
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' },
|
header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' },
|
||||||
revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' },
|
revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' },
|
||||||
|
@ -75,9 +73,9 @@ const AuthTokenList: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
|
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
|
||||||
const currentTokenId = useAppSelector(state => {
|
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(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Avatar from 'soapbox/components/avatar';
|
import AccountComponent from 'soapbox/components/account';
|
||||||
import DisplayName from 'soapbox/components/display-name';
|
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import { HStack } from 'soapbox/components/ui';
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
import { makeGetAccount } from 'soapbox/selectors';
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
|
||||||
|
@ -22,12 +21,6 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
|
||||||
|
|
||||||
const account = useAppSelector((state) => getAccount(state, accountId));
|
const account = useAppSelector((state) => getAccount(state, accountId));
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (accountId && !account) {
|
|
||||||
// fetchAccount(accountId);
|
|
||||||
// }
|
|
||||||
// }, [accountId]);
|
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
const birthday = account.birthday;
|
const birthday = account.birthday;
|
||||||
|
@ -36,15 +29,10 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
|
||||||
const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' });
|
const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account'>
|
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||||
<div className='account__wrapper'>
|
<div className='w-full'>
|
||||||
<Link className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
<AccountComponent account={account} withRelationship={false} />
|
||||||
<div className='account__display-name'>
|
|
||||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
|
||||||
<DisplayName account={account} />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
|
||||||
<div
|
<div
|
||||||
className='flex items-center gap-0.5'
|
className='flex items-center gap-0.5'
|
||||||
title={intl.formatMessage(messages.birthday, {
|
title={intl.formatMessage(messages.birthday, {
|
||||||
|
@ -54,8 +42,7 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
|
||||||
<Icon src={require('@tabler/icons/ballon.svg')} />
|
<Icon src={require('@tabler/icons/ballon.svg')} />
|
||||||
{formattedBirthday}
|
{formattedBirthday}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</HStack>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui';
|
import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui';
|
||||||
|
|
||||||
import ChatSearch from '../../chat-search/chat-search';
|
import ChatSearch from '../../chat-search/chat-search';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'chat.new_message.title', defaultMessage: 'New Message' },
|
||||||
|
});
|
||||||
|
|
||||||
interface IChatPageNew {
|
interface IChatPageNew {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** New message form to create a chat. */
|
/** New message form to create a chat. */
|
||||||
const ChatPageNew: React.FC<IChatPageNew> = () => {
|
const ChatPageNew: React.FC<IChatPageNew> = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -22,7 +28,7 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
|
||||||
onClick={() => history.push('/chats')}
|
onClick={() => history.push('/chats')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CardTitle title='New Message' />
|
<CardTitle title={intl.formatMessage(messages.title)} />
|
||||||
</HStack>
|
</HStack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { Icon, Input, Stack } from 'soapbox/components/ui';
|
import { Icon, Input, Stack } from 'soapbox/components/ui';
|
||||||
|
@ -17,11 +18,16 @@ import Blankslate from './blankslate';
|
||||||
import EmptyResultsBlankslate from './empty-results-blankslate';
|
import EmptyResultsBlankslate from './empty-results-blankslate';
|
||||||
import Results from './results';
|
import Results from './results';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
placeholder: { id: 'chat_search.placeholder', defaultMessage: 'Type a name' },
|
||||||
|
});
|
||||||
|
|
||||||
interface IChatSearch {
|
interface IChatSearch {
|
||||||
isMainPage?: boolean
|
isMainPage?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatSearch = (props: IChatSearch) => {
|
const ChatSearch = (props: IChatSearch) => {
|
||||||
|
const intl = useIntl();
|
||||||
const { isMainPage = false } = props;
|
const { isMainPage = false } = props;
|
||||||
|
|
||||||
const debounce = useDebounce;
|
const debounce = useDebounce;
|
||||||
|
@ -88,7 +94,7 @@ const ChatSearch = (props: IChatSearch) => {
|
||||||
data-testid='search'
|
data-testid='search'
|
||||||
type='text'
|
type='text'
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder='Type a name'
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
value={value || ''}
|
value={value || ''}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
outerClassName='mt-0'
|
outerClassName='mt-0'
|
||||||
|
|
|
@ -75,7 +75,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
||||||
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
|
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
|
||||||
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
|
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 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;
|
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt } = compose;
|
||||||
|
|
|
@ -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;
|
|
|
@ -37,7 +37,7 @@ const SettingsStore: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const settings = useSettings();
|
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 [rawJSON, setRawJSON] = useState<string>(JSON.stringify(settingsStore, null, 2));
|
||||||
const [jsonValid, setJsonValid] = useState(true);
|
const [jsonValid, setJsonValid] = useState(true);
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts';
|
import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts';
|
||||||
import Avatar from 'soapbox/components/avatar';
|
import Account from 'soapbox/components/account';
|
||||||
import DisplayName from 'soapbox/components/display-name';
|
import { Button, HStack } from 'soapbox/components/ui';
|
||||||
import IconButton from 'soapbox/components/icon-button';
|
|
||||||
import { Text } from 'soapbox/components/ui';
|
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
import { makeGetAccount } from 'soapbox/selectors';
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
|
||||||
|
@ -38,24 +35,28 @@ const AccountAuthorize: React.FC<IAccountAuthorize> = ({ id }) => {
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
const content = { __html: account.note_emojified };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-authorize__wrapper'>
|
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||||
<div className='account-authorize'>
|
<div className='w-full'>
|
||||||
<Link to={`/@${account.acct}`}>
|
<Account account={account} withRelationship={false} />
|
||||||
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
|
|
||||||
<DisplayName account={account} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Text className='account__header__content' dangerouslySetInnerHTML={content} />
|
|
||||||
</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>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ const NotificationFilterBar = () => {
|
||||||
name: 'pleroma:emoji_reaction',
|
name: 'pleroma:emoji_reaction',
|
||||||
});
|
});
|
||||||
items.push({
|
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),
|
title: intl.formatMessage(messages.boosts),
|
||||||
action: onClick('reblog'),
|
action: onClick('reblog'),
|
||||||
name: 'reblog',
|
name: 'reblog',
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { makeGetNotification } from 'soapbox/selectors';
|
||||||
import { NotificationType, validType } from 'soapbox/utils/notification';
|
import { NotificationType, validType } from 'soapbox/utils/notification';
|
||||||
|
|
||||||
import type { ScrollPosition } from 'soapbox/components/status';
|
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 notificationForScreenReader = (intl: IntlShape, message: string, timestamp: Date) => {
|
||||||
const output = [message];
|
const output = [message];
|
||||||
|
@ -27,7 +27,7 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp
|
||||||
return output.join(', ');
|
return output.join(', ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildLink = (account: Account): JSX.Element => (
|
const buildLink = (account: AccountEntity): JSX.Element => (
|
||||||
<bdi>
|
<bdi>
|
||||||
<Link
|
<Link
|
||||||
className='text-gray-800 dark:text-gray-200 font-bold hover:underline'
|
className='text-gray-800 dark:text-gray-200 font-bold hover:underline'
|
||||||
|
@ -127,7 +127,7 @@ const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
|
||||||
const buildMessage = (
|
const buildMessage = (
|
||||||
intl: IntlShape,
|
intl: IntlShape,
|
||||||
type: NotificationType,
|
type: NotificationType,
|
||||||
account: Account,
|
account: AccountEntity,
|
||||||
totalCount: number | null,
|
totalCount: number | null,
|
||||||
targetName: string,
|
targetName: string,
|
||||||
instanceTitle: string,
|
instanceTitle: string,
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { HStack } from 'soapbox/components/ui';
|
||||||
|
|
||||||
import PlaceholderAvatar from './placeholder-avatar';
|
import PlaceholderAvatar from './placeholder-avatar';
|
||||||
import PlaceholderDisplayName from './placeholder-display-name';
|
import PlaceholderDisplayName from './placeholder-display-name';
|
||||||
|
|
||||||
/** Fake account to display while data is loading. */
|
/** Fake account to display while data is loading. */
|
||||||
const PlaceholderAccount: React.FC = () => {
|
const PlaceholderAccount: React.FC = () => (
|
||||||
return (
|
<HStack space={3} alignItems='center'>
|
||||||
<div className='account'>
|
<div className='flex-shrink-0'>
|
||||||
<div className='account__wrapper'>
|
<PlaceholderAvatar size={42} />
|
||||||
<span className='account__display-name'>
|
|
||||||
<div className='account__avatar-wrapper'>
|
|
||||||
<PlaceholderAvatar size={36} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='min-w-0 flex-1'>
|
||||||
<PlaceholderDisplayName minLength={3} maxLength={25} />
|
<PlaceholderDisplayName minLength={3} maxLength={25} />
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</HStack>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export default PlaceholderAccount;
|
export default PlaceholderAccount;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import classNames from 'clsx';
|
import classNames from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import Account from 'soapbox/components/account';
|
||||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||||
import StatusContent from 'soapbox/components/status-content';
|
import StatusContent from 'soapbox/components/status-content';
|
||||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||||
import { HStack } from 'soapbox/components/ui';
|
import { HStack } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
|
||||||
import PollPreview from 'soapbox/features/ui/components/poll-preview';
|
import PollPreview from 'soapbox/features/ui/components/poll-preview';
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
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={classNames('status', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })} data-id={status.id}>
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<HStack justifyContent='between' alignItems='start'>
|
<HStack justifyContent='between' alignItems='start'>
|
||||||
<AccountContainer
|
<Account
|
||||||
key={account.id}
|
key={account.id}
|
||||||
id={account.id}
|
account={account}
|
||||||
timestamp={status.created_at}
|
timestamp={status.created_at}
|
||||||
futureTimestamp
|
futureTimestamp
|
||||||
|
hideActions
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import Account from 'soapbox/components/account';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import StatusContent from 'soapbox/components/status-content';
|
import StatusContent from 'soapbox/components/status-content';
|
||||||
import StatusMedia from 'soapbox/components/status-media';
|
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 SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
|
||||||
import TranslateButton from 'soapbox/components/translate-button';
|
import TranslateButton from 'soapbox/components/translate-button';
|
||||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
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 QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
|
||||||
import { getActualStatus } from 'soapbox/utils/status';
|
import { getActualStatus } from 'soapbox/utils/status';
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
<div className='border-box'>
|
<div className='border-box'>
|
||||||
<div ref={node} className='detailed-actualStatus' tabIndex={-1}>
|
<div ref={node} className='detailed-actualStatus' tabIndex={-1}>
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<AccountContainer
|
<Account
|
||||||
key={account.id}
|
key={account.id}
|
||||||
id={account.id}
|
account={account}
|
||||||
timestamp={actualStatus.created_at}
|
timestamp={actualStatus.created_at}
|
||||||
avatarSize={42}
|
avatarSize={42}
|
||||||
hideActions
|
hideActions
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import classNames from 'clsx';
|
import classNames from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import Account from 'soapbox/components/account';
|
||||||
import StatusContent from 'soapbox/components/status-content';
|
import StatusContent from 'soapbox/components/status-content';
|
||||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||||
import { Card, HStack } from 'soapbox/components/ui';
|
import { Card, HStack } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
|
||||||
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card';
|
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card';
|
||||||
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder-media-gallery';
|
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder-media-gallery';
|
||||||
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
|
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'>
|
<div className='mb-4'>
|
||||||
<HStack justifyContent='between' alignItems='start'>
|
<HStack justifyContent='between' alignItems='start'>
|
||||||
<AccountContainer
|
<Account
|
||||||
key={account.id}
|
key={account.id}
|
||||||
id={account.id}
|
account={account}
|
||||||
timestamp={status.created_at}
|
timestamp={status.created_at}
|
||||||
hideActions
|
hideActions
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -39,8 +39,8 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const authUsers = useAppSelector((state) => state.auth.get('users'));
|
const authUsers = useAppSelector((state) => state.auth.users);
|
||||||
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.get('id'))));
|
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.id)!));
|
||||||
|
|
||||||
const handleLogOut = () => {
|
const handleLogOut = () => {
|
||||||
dispatch(logOut());
|
dispatch(logOut());
|
||||||
|
|
|
@ -2,9 +2,8 @@ import React from 'react';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import Avatar from 'soapbox/components/avatar';
|
|
||||||
import StillImage from 'soapbox/components/still-image';
|
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 VerificationBadge from 'soapbox/components/verification-badge';
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
import { makeGetAccount } from 'soapbox/selectors';
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
@ -48,10 +47,7 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
||||||
title={acct}
|
title={acct}
|
||||||
className='-mt-12 block'
|
className='-mt-12 block'
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar src={account.avatar} className='h-20 w-20 bg-gray-50 ring-2 ring-white overflow-hidden' />
|
||||||
account={account}
|
|
||||||
className='h-20 w-20 bg-gray-50 ring-2 ring-white'
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{action && (
|
{action && (
|
||||||
|
|
|
@ -38,7 +38,9 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
|
||||||
title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}
|
title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}
|
||||||
action={
|
action={
|
||||||
<Link to='/suggestions'>
|
<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>
|
</Link>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -191,6 +191,7 @@
|
||||||
"chat.actions.send": "Send",
|
"chat.actions.send": "Send",
|
||||||
"chat.failed_to_send": "Message failed to send.",
|
"chat.failed_to_send": "Message failed to send.",
|
||||||
"chat.input.placeholder": "Type a message",
|
"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.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.play_sounds.label": "Play a sound when you receive a message",
|
||||||
"chat.page_settings.preferences": "Preferences",
|
"chat.page_settings.preferences": "Preferences",
|
||||||
|
@ -224,6 +225,7 @@
|
||||||
"chat_search.empty_results_blankslate.action": "Message someone",
|
"chat_search.empty_results_blankslate.action": "Message someone",
|
||||||
"chat_search.empty_results_blankslate.body": "Try searching for another name.",
|
"chat_search.empty_results_blankslate.body": "Try searching for another name.",
|
||||||
"chat_search.empty_results_blankslate.title": "No matches found",
|
"chat_search.empty_results_blankslate.title": "No matches found",
|
||||||
|
"chat_search.placeholder": "Type a name",
|
||||||
"chat_search.title": "Messages",
|
"chat_search.title": "Messages",
|
||||||
"chat_settings.auto_delete.14days": "14 days",
|
"chat_settings.auto_delete.14days": "14 days",
|
||||||
"chat_settings.auto_delete.2minutes": "2 minutes",
|
"chat_settings.auto_delete.2minutes": "2 minutes",
|
||||||
|
@ -468,8 +470,6 @@
|
||||||
"confirmations.scheduled_status_delete.heading": "Cancel scheduled post",
|
"confirmations.scheduled_status_delete.heading": "Cancel scheduled post",
|
||||||
"confirmations.scheduled_status_delete.message": "Are you sure you want to cancel this scheduled post?",
|
"confirmations.scheduled_status_delete.message": "Are you sure you want to cancel this scheduled post?",
|
||||||
"confirmations.unfollow.confirm": "Unfollow",
|
"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.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.explanation_box.title": "Sending cryptocurrency donations",
|
||||||
"crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}",
|
"crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}",
|
||||||
|
|
|
@ -21,12 +21,13 @@ const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Middleware to display Redux errors to the user. */
|
/** Middleware to display Redux errors to the user. */
|
||||||
export default function errorsMiddleware(): ThunkMiddleware {
|
const errorsMiddleware = (): ThunkMiddleware =>
|
||||||
return () => next => action => {
|
() => next => action => {
|
||||||
if (shouldShowError(action)) {
|
if (shouldShowError(action)) {
|
||||||
toast.showAlertForError(action.error);
|
toast.showAlertForError(action.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
export default errorsMiddleware;
|
||||||
|
|
|
@ -10,17 +10,18 @@ import {
|
||||||
} from 'soapbox/actions/auth';
|
} from 'soapbox/actions/auth';
|
||||||
import { ME_FETCH_SKIP } from 'soapbox/actions/me';
|
import { ME_FETCH_SKIP } from 'soapbox/actions/me';
|
||||||
import { MASTODON_PRELOAD_IMPORT } from 'soapbox/actions/preload';
|
import { MASTODON_PRELOAD_IMPORT } from 'soapbox/actions/preload';
|
||||||
|
import { AuthAppRecord, AuthTokenRecord, AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth';
|
||||||
|
|
||||||
import reducer from '../auth';
|
import reducer from '../auth';
|
||||||
|
|
||||||
describe('auth reducer', () => {
|
describe('auth reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableMap({
|
expect(reducer(undefined, {} as any).toJS()).toMatchObject({
|
||||||
app: ImmutableMap(),
|
app: {},
|
||||||
users: ImmutableMap(),
|
users: {},
|
||||||
tokens: ImmutableMap(),
|
tokens: {},
|
||||||
me: null,
|
me: null,
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('AUTH_APP_CREATED', () => {
|
describe('AUTH_APP_CREATED', () => {
|
||||||
|
@ -29,9 +30,9 @@ describe('auth reducer', () => {
|
||||||
const action = { type: AUTH_APP_CREATED, app: token };
|
const action = { type: AUTH_APP_CREATED, app: token };
|
||||||
|
|
||||||
const result = reducer(undefined, action);
|
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 action = { type: AUTH_LOGGED_IN, token };
|
||||||
|
|
||||||
const result = reducer(undefined, action);
|
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', () => {
|
it('should merge the token with existing state', () => {
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
tokens: { 'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' } },
|
tokens: ImmutableMap({ 'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = ImmutableMap({
|
||||||
'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' },
|
'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }),
|
||||||
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
|
'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const action = {
|
const action = {
|
||||||
|
@ -62,7 +63,7 @@ describe('auth reducer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = reducer(state, action);
|
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' }),
|
account: fromJS({ url: 'https://gleasonator.com/users/alex' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
users: {
|
users: ImmutableMap({
|
||||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
'https://gleasonator.com/users/alex': AuthUserRecord({ 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' },
|
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = ImmutableMap({
|
||||||
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = reducer(state, action);
|
const result = reducer(state, action);
|
||||||
expect(result.get('users')).toEqual(expected);
|
expect(result.users).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets `me` to the next available user', () => {
|
it('sets `me` to the next available user', () => {
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
me: 'https://gleasonator.com/users/alex',
|
me: 'https://gleasonator.com/users/alex',
|
||||||
users: {
|
users: ImmutableMap({
|
||||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
'https://gleasonator.com/users/alex': AuthUserRecord({ 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' },
|
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const action = {
|
const action = {
|
||||||
|
@ -103,7 +104,7 @@ describe('auth reducer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = reducer(state, action);
|
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' },
|
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = ImmutableMap({
|
||||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
'https://gleasonator.com/users/alex': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = reducer(undefined, action);
|
const result = reducer(undefined, action);
|
||||||
expect(result.get('users')).toEqual(expected);
|
expect(result.users).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the account in the token', () => {
|
it('should set the account in the token', () => {
|
||||||
|
@ -130,21 +131,21 @@ describe('auth reducer', () => {
|
||||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
tokens: { 'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' } },
|
tokens: ImmutableMap({ 'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = {
|
||||||
'ABCDEFG': {
|
'ABCDEFG': {
|
||||||
token_type: 'Bearer',
|
token_type: 'Bearer',
|
||||||
access_token: 'ABCDEFG',
|
access_token: 'ABCDEFG',
|
||||||
account: '1234',
|
account: '1234',
|
||||||
me: 'https://gleasonator.com/users/alex',
|
me: 'https://gleasonator.com/users/alex',
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
const result = reducer(state, action);
|
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', () => {
|
it('sets `me` to the account if unset', () => {
|
||||||
|
@ -155,7 +156,7 @@ describe('auth reducer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = reducer(undefined, action);
|
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', () => {
|
it('leaves `me` alone if already set', () => {
|
||||||
|
@ -165,10 +166,10 @@ describe('auth reducer', () => {
|
||||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
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);
|
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', () => {
|
it('deletes mismatched users', () => {
|
||||||
|
@ -178,21 +179,21 @@ describe('auth reducer', () => {
|
||||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
users: {
|
users: ImmutableMap({
|
||||||
'https://gleasonator.com/users/mk': { id: '4567', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/mk' },
|
'https://gleasonator.com/users/mk': AuthUserRecord({ 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/curtis': AuthUserRecord({ 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' },
|
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = ImmutableMap({
|
||||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
'https://gleasonator.com/users/alex': AuthUserRecord({ 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' },
|
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = reducer(state, action);
|
const result = reducer(state, action);
|
||||||
expect(result.get('users')).toEqual(expected);
|
expect(result.users).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('upgrades from an ID to a URL', () => {
|
it('upgrades from an ID to a URL', () => {
|
||||||
|
@ -202,18 +203,18 @@ describe('auth reducer', () => {
|
||||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
me: '1234',
|
me: '1234',
|
||||||
users: {
|
users: ImmutableMap({
|
||||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
'1234': AuthUserRecord({ id: '1234', access_token: 'ABCDEFG' }),
|
||||||
'5432': { id: '5432', access_token: 'HIJKLMN' },
|
'5432': AuthUserRecord({ id: '5432', access_token: 'HIJKLMN' }),
|
||||||
},
|
}),
|
||||||
tokens: {
|
tokens: ImmutableMap({
|
||||||
'ABCDEFG': { access_token: 'ABCDEFG', account: '1234' },
|
'ABCDEFG': AuthTokenRecord({ access_token: 'ABCDEFG', account: '1234' }),
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = {
|
||||||
me: 'https://gleasonator.com/users/alex',
|
me: 'https://gleasonator.com/users/alex',
|
||||||
users: {
|
users: {
|
||||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
||||||
|
@ -222,24 +223,24 @@ describe('auth reducer', () => {
|
||||||
tokens: {
|
tokens: {
|
||||||
'ABCDEFG': { access_token: 'ABCDEFG', account: '1234', me: 'https://gleasonator.com/users/alex' },
|
'ABCDEFG': { access_token: 'ABCDEFG', account: '1234', me: 'https://gleasonator.com/users/alex' },
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
const result = reducer(state, action);
|
const result = reducer(state, action);
|
||||||
expect(result).toEqual(expected);
|
expect(result.toJS()).toMatchObject(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('VERIFY_CREDENTIALS_FAIL', () => {
|
describe('VERIFY_CREDENTIALS_FAIL', () => {
|
||||||
it('should delete the failed token if it 403\'d', () => {
|
it('should delete the failed token if it 403\'d', () => {
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
tokens: {
|
tokens: ImmutableMap({
|
||||||
'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' },
|
'ABCDEFG': AuthTokenRecord({ token_type: 'Bearer', access_token: 'ABCDEFG' }),
|
||||||
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
|
'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }),
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = ImmutableMap({
|
||||||
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
|
'HIJKLMN': AuthTokenRecord({ token_type: 'Bearer', access_token: 'HIJKLMN' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const action = {
|
const action = {
|
||||||
|
@ -249,19 +250,19 @@ describe('auth reducer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = reducer(state, action);
|
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', () => {
|
it('should delete any users associated with the failed token', () => {
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
users: {
|
users: ImmutableMap({
|
||||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
'https://gleasonator.com/users/alex': AuthUserRecord({ 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' },
|
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = ImmutableMap({
|
||||||
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const action = {
|
const action = {
|
||||||
|
@ -271,16 +272,16 @@ describe('auth reducer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = reducer(state, action);
|
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', () => {
|
it('should reassign `me` to the next in line', () => {
|
||||||
const state = fromJS({
|
const state = ReducerRecord({
|
||||||
me: 'https://gleasonator.com/users/alex',
|
me: 'https://gleasonator.com/users/alex',
|
||||||
users: {
|
users: ImmutableMap({
|
||||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
'https://gleasonator.com/users/alex': AuthUserRecord({ 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' },
|
'https://gleasonator.com/users/benis': AuthUserRecord({ id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' }),
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const action = {
|
const action = {
|
||||||
|
@ -290,7 +291,7 @@ describe('auth reducer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = reducer(state, action);
|
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);
|
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', () => {
|
describe('ME_FETCH_SKIP', () => {
|
||||||
it('sets `me` to null', () => {
|
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 action = { type: ME_FETCH_SKIP };
|
||||||
const result = reducer(state, action);
|
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'),
|
data: require('soapbox/__fixtures__/mastodon_initial_state.json'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const expected = fromJS({
|
const expected = {
|
||||||
me: 'https://mastodon.social/@benis911',
|
me: 'https://mastodon.social/@benis911',
|
||||||
app: {},
|
app: {},
|
||||||
users: {
|
users: {
|
||||||
|
@ -341,10 +342,10 @@ describe('auth reducer', () => {
|
||||||
token_type: 'Bearer',
|
token_type: 'Bearer',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
const result = reducer(undefined, action);
|
const result = reducer(undefined, action);
|
||||||
expect(result).toEqual(expected);
|
expect(result.toJS()).toMatchObject(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,7 +23,7 @@ describe('modal reducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle MODAL_CLOSE', () => {
|
it('should handle MODAL_CLOSE', () => {
|
||||||
const state = ImmutableList([
|
const state = ImmutableList<any>([
|
||||||
ImmutableRecord({
|
ImmutableRecord({
|
||||||
modalType: 'type1',
|
modalType: 'type1',
|
||||||
modalProps: { props1: '1' },
|
modalProps: { props1: '1' },
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,16 +54,11 @@ export interface ReducerAdminReport extends AdminReportRecord {
|
||||||
statuses: ImmutableList<string | null>,
|
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
|
// 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 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 APIReport = { id: string, state: string, statuses: any[] };
|
||||||
type APIUser = { id: string, email: string, nickname: string, registration_reason: string };
|
type APIUser = { id: string, email: string, nickname: string, registration_reason: string };
|
||||||
|
|
||||||
|
|
|
@ -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 trim from 'lodash/trim';
|
||||||
|
|
||||||
import { MASTODON_PRELOAD_IMPORT } from 'soapbox/actions/preload';
|
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 KVStore from 'soapbox/storage/kv-store';
|
||||||
import { validId, isURL } from 'soapbox/utils/auth';
|
import { validId, isURL } from 'soapbox/utils/auth';
|
||||||
|
|
||||||
|
@ -17,17 +17,55 @@ import {
|
||||||
} from '../actions/auth';
|
} from '../actions/auth';
|
||||||
import { ME_FETCH_SKIP } from '../actions/me';
|
import { ME_FETCH_SKIP } from '../actions/me';
|
||||||
|
|
||||||
const defaultState = ImmutableMap({
|
import type { AxiosError } from 'axios';
|
||||||
app: ImmutableMap(),
|
import type { AnyAction } from 'redux';
|
||||||
users: ImmutableMap(),
|
import type { APIEntity, Account as AccountEntity } from 'soapbox/types/entities';
|
||||||
tokens: ImmutableMap(),
|
|
||||||
me: null,
|
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
|
// 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 STORAGE_KEY = buildKey([NAMESPACE, 'auth']);
|
||||||
const SESSION_KEY = buildKey([NAMESPACE, 'auth', 'me']);
|
const SESSION_KEY = buildKey([NAMESPACE, 'auth', 'me']);
|
||||||
|
@ -37,35 +75,48 @@ const getSessionUser = () => {
|
||||||
return validId(id) ? id : undefined;
|
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();
|
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
|
// Checks if the user has an ID and access token
|
||||||
const validUser = user => {
|
const validUser = (user?: AuthUser) => {
|
||||||
try {
|
try {
|
||||||
return validId(user.get('id')) && validId(user.get('access_token'));
|
return !!(user && validId(user.id) && validId(user.access_token));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Finds the first valid user in the state
|
// 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.
|
// For legacy purposes. IDs get upgraded to URLs further down.
|
||||||
const getUrlOrId = user => {
|
const getUrlOrId = (user?: AuthUser): string | null => {
|
||||||
try {
|
try {
|
||||||
const { id, url } = user.toJS();
|
const { id, url } = user!.toJS();
|
||||||
return url || id;
|
return (url || id) as string;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If `me` doesn't match an existing user, attempt to shift it.
|
// If `me` doesn't match an existing user, attempt to shift it.
|
||||||
const maybeShiftMe = state => {
|
const maybeShiftMe = (state: State) => {
|
||||||
const me = state.get('me');
|
const me = state.me!;
|
||||||
const user = state.getIn(['users', me]);
|
const user = state.users.get(me);
|
||||||
|
|
||||||
if (!validUser(user)) {
|
if (!validUser(user)) {
|
||||||
const nextUser = firstValidUser(state);
|
const nextUser = firstValidUser(state);
|
||||||
|
@ -76,29 +127,29 @@ const maybeShiftMe = state => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the user from the session or localStorage, whichever is valid first
|
// Set the user from the session or localStorage, whichever is valid first
|
||||||
const setSessionUser = state => state.update('me', null, me => {
|
const setSessionUser = (state: State) => state.update('me', me => {
|
||||||
const user = ImmutableList([
|
const user = ImmutableList<AuthUser>([
|
||||||
state.getIn(['users', sessionUser]),
|
state.users.get(sessionUser!)!,
|
||||||
state.getIn(['users', me]),
|
state.users.get(me!)!,
|
||||||
]).find(validUser);
|
]).find(validUser);
|
||||||
|
|
||||||
return getUrlOrId(user);
|
return getUrlOrId(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Upgrade the initial state
|
// Upgrade the initial state
|
||||||
const migrateLegacy = state => {
|
const migrateLegacy = (state: State) => {
|
||||||
if (localState) return state;
|
if (localState) return state;
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
const app = fromJS(JSON.parse(localStorage.getItem('soapbox:auth:app')));
|
const app = AuthAppRecord(JSON.parse(localStorage.getItem('soapbox:auth:app')!));
|
||||||
const user = fromJS(JSON.parse(localStorage.getItem('soapbox:auth:user')));
|
const user = fromJS(JSON.parse(localStorage.getItem('soapbox:auth:user')!)) as ImmutableMap<string, any>;
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
state.set('me', '_legacy'); // Placeholder account ID
|
state.set('me', '_legacy'); // Placeholder account ID
|
||||||
state.set('app', app);
|
state.set('app', app);
|
||||||
state.set('tokens', ImmutableMap({
|
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({
|
state.set('users', ImmutableMap({
|
||||||
'_legacy': ImmutableMap({
|
'_legacy': AuthUserRecord({
|
||||||
id: '_legacy',
|
id: '_legacy',
|
||||||
access_token: user.get('access_token'),
|
access_token: user.get('access_token'),
|
||||||
}),
|
}),
|
||||||
|
@ -106,26 +157,26 @@ const migrateLegacy = state => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isUpgradingUrlId = state => {
|
const isUpgradingUrlId = (state: State) => {
|
||||||
const me = state.get('me');
|
const me = state.me;
|
||||||
const user = state.getIn(['users', me]);
|
const user = state.users.get(me!);
|
||||||
return validId(me) && user && !isURL(me);
|
return validId(me) && user && !isURL(me);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Checks the state and makes it valid
|
// Checks the state and makes it valid
|
||||||
const sanitizeState = state => {
|
const sanitizeState = (state: State) => {
|
||||||
// Skip sanitation during ID to URL upgrade
|
// Skip sanitation during ID to URL upgrade
|
||||||
if (isUpgradingUrlId(state)) return state;
|
if (isUpgradingUrlId(state)) return state;
|
||||||
|
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
// Remove invalid users, ensure ID match
|
// Remove invalid users, ensure ID match
|
||||||
state.update('users', ImmutableMap(), users => (
|
state.update('users', users => (
|
||||||
users.filter((user, url) => (
|
users.filter((user, url) => (
|
||||||
validUser(user) && user.get('url') === url
|
validUser(user) && user.get('url') === url
|
||||||
))
|
))
|
||||||
));
|
));
|
||||||
// Remove mismatched tokens
|
// Remove mismatched tokens
|
||||||
state.update('tokens', ImmutableMap(), tokens => (
|
state.update('tokens', tokens => (
|
||||||
tokens.filter((token, id) => (
|
tokens.filter((token, id) => (
|
||||||
validId(id) && token.get('access_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 persistSession = (state: State) => {
|
||||||
const me = state.get('me');
|
const me = state.me;
|
||||||
if (me && typeof me === 'string') {
|
if (me && typeof me === 'string') {
|
||||||
sessionStorage.setItem(SESSION_KEY, me);
|
sessionStorage.setItem(SESSION_KEY, me);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const persistState = state => {
|
const persistState = (state: State) => {
|
||||||
persistAuth(state);
|
persistAuth(state);
|
||||||
persistSession(state);
|
persistSession(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialize = state => {
|
const initialize = (state: State) => {
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
maybeShiftMe(state);
|
maybeShiftMe(state);
|
||||||
setSessionUser(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) => {
|
const importToken = (state: State, token: APIEntity) => {
|
||||||
return state.setIn(['tokens', token.access_token], fromJS(token));
|
return state.setIn(['tokens', token.access_token], AuthTokenRecord(token));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Upgrade the `_legacy` placeholder ID with a real account
|
// Upgrade the `_legacy` placeholder ID with a real account
|
||||||
const upgradeLegacyId = (state, account) => {
|
const upgradeLegacyId = (state: State, account: APIEntity) => {
|
||||||
if (localState) return state;
|
if (localState) return state;
|
||||||
return state.withMutations(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']);
|
state.deleteIn(['users', '_legacy']);
|
||||||
});
|
});
|
||||||
// TODO: Delete `soapbox:auth:app` and `soapbox:auth:user` localStorage?
|
// 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
|
// Users are now stored by their ActivityPub ID instead of their
|
||||||
// primary key to support auth against multiple hosts.
|
// primary key to support auth against multiple hosts.
|
||||||
const upgradeNonUrlId = (state, account) => {
|
const upgradeNonUrlId = (state: State, account: APIEntity) => {
|
||||||
const me = state.get('me');
|
const me = state.me;
|
||||||
if (isURL(me)) return state;
|
if (isURL(me)) return state;
|
||||||
|
|
||||||
return state.withMutations(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]);
|
state.deleteIn(['users', account.id]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns a predicate function for filtering a mismatched user/token
|
// Returns a predicate function for filtering a mismatched user/token
|
||||||
const userMismatch = (token, account) => {
|
const userMismatch = (token: string, account: APIEntity) => {
|
||||||
return (user, url) => {
|
return (user: AuthUser, url: string) => {
|
||||||
const sameToken = user.get('access_token') === token;
|
const sameToken = user.get('access_token') === token;
|
||||||
const differentUrl = url !== account.url || user.get('url') !== account.url;
|
const differentUrl = url !== account.url || user.get('url') !== account.url;
|
||||||
const differentId = user.get('id') !== account.id;
|
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 => {
|
return state.withMutations(state => {
|
||||||
state.setIn(['users', account.url], ImmutableMap({
|
state.setIn(['users', account.url], AuthUserRecord({
|
||||||
id: account.id,
|
id: account.id,
|
||||||
access_token: token,
|
access_token: token,
|
||||||
url: account.url,
|
url: account.url,
|
||||||
}));
|
}));
|
||||||
state.setIn(['tokens', token, 'account'], account.id);
|
state.setIn(['tokens', token, 'account'], account.id);
|
||||||
state.setIn(['tokens', token, 'me'], account.url);
|
state.setIn(['tokens', token, 'me'], account.url);
|
||||||
state.update('users', ImmutableMap(), users => users.filterNot(userMismatch(token, account)));
|
state.update('users', users => users.filterNot(userMismatch(token, account)));
|
||||||
state.update('me', null, me => me || account.url);
|
state.update('me', me => me || account.url);
|
||||||
upgradeLegacyId(state, account);
|
upgradeLegacyId(state, account);
|
||||||
upgradeNonUrlId(state, account);
|
upgradeNonUrlId(state, account);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteToken = (state, token) => {
|
const deleteToken = (state: State, token: string) => {
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
state.update('tokens', ImmutableMap(), tokens => tokens.delete(token));
|
state.update('tokens', tokens => tokens.delete(token));
|
||||||
state.update('users', ImmutableMap(), users => users.filterNot(user => user.get('access_token') === token));
|
state.update('users', users => users.filterNot(user => user.get('access_token') === token));
|
||||||
maybeShiftMe(state);
|
maybeShiftMe(state);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUser = (state, account) => {
|
const deleteUser = (state: State, account: AccountEntity) => {
|
||||||
const accountUrl = account.get('url');
|
const accountUrl = account.get('url');
|
||||||
|
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
state.update('users', ImmutableMap(), users => users.delete(accountUrl));
|
state.update('users', users => users.delete(accountUrl));
|
||||||
state.update('tokens', ImmutableMap(), tokens => tokens.filterNot(token => token.get('me') === accountUrl));
|
state.update('tokens', tokens => tokens.filterNot(token => token.get('me') === accountUrl));
|
||||||
maybeShiftMe(state);
|
maybeShiftMe(state);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const importMastodonPreload = (state, data) => {
|
const importMastodonPreload = (state: State, data: ImmutableMap<string, any>) => {
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
const accountId = data.getIn(['meta', 'me']);
|
const accountId = data.getIn(['meta', 'me']) as string;
|
||||||
const accountUrl = data.getIn(['accounts', accountId, 'url']);
|
const accountUrl = data.getIn(['accounts', accountId, 'url']) as string;
|
||||||
const accessToken = data.getIn(['meta', 'access_token']);
|
const accessToken = data.getIn(['meta', 'access_token']) as string;
|
||||||
|
|
||||||
if (validId(accessToken) && validId(accountId) && isURL(accountUrl)) {
|
if (validId(accessToken) && validId(accountId) && isURL(accountUrl)) {
|
||||||
state.setIn(['tokens', accessToken], fromJS({
|
state.setIn(['tokens', accessToken], AuthTokenRecord({
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
account: accountId,
|
account: accountId,
|
||||||
me: accountUrl,
|
me: accountUrl,
|
||||||
|
@ -246,7 +297,7 @@ const importMastodonPreload = (state, data) => {
|
||||||
token_type: 'Bearer',
|
token_type: 'Bearer',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
state.setIn(['users', accountUrl], fromJS({
|
state.setIn(['users', accountUrl], AuthUserRecord({
|
||||||
id: accountId,
|
id: accountId,
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
url: accountUrl,
|
url: accountUrl,
|
||||||
|
@ -257,11 +308,11 @@ const importMastodonPreload = (state, data) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const persistAuthAccount = account => {
|
const persistAuthAccount = (account: APIEntity) => {
|
||||||
if (account && account.url) {
|
if (account && account.url) {
|
||||||
const key = `authAccount:${account.url}`;
|
const key = `authAccount:${account.url}`;
|
||||||
if (!account.pleroma) account.pleroma = {};
|
if (!account.pleroma) account.pleroma = {};
|
||||||
KVStore.getItem(key).then(oldAccount => {
|
KVStore.getItem(key).then((oldAccount: any) => {
|
||||||
const settings = oldAccount?.pleroma?.settings_store || {};
|
const settings = oldAccount?.pleroma?.settings_store || {};
|
||||||
if (!account.pleroma.settings_store) {
|
if (!account.pleroma.settings_store) {
|
||||||
account.pleroma.settings_store = settings;
|
account.pleroma.settings_store = settings;
|
||||||
|
@ -272,20 +323,20 @@ const persistAuthAccount = account => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteForbiddenToken = (state, error, token) => {
|
const deleteForbiddenToken = (state: State, error: AxiosError, token: string) => {
|
||||||
if ([401, 403].includes(error.response?.status)) {
|
if ([401, 403].includes(error.response?.status!)) {
|
||||||
return deleteToken(state, token);
|
return deleteToken(state, token);
|
||||||
} else {
|
} else {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state, action) => {
|
const reducer = (state: State, action: AnyAction) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case AUTH_APP_CREATED:
|
case AUTH_APP_CREATED:
|
||||||
return state.set('app', fromJS(action.app));
|
return state.set('app', AuthAppRecord(action.app));
|
||||||
case AUTH_APP_AUTHORIZED:
|
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:
|
case AUTH_LOGGED_IN:
|
||||||
return importToken(state, action.token);
|
return importToken(state, action.token);
|
||||||
case AUTH_LOGGED_OUT:
|
case AUTH_LOGGED_OUT:
|
||||||
|
@ -300,7 +351,7 @@ const reducer = (state, action) => {
|
||||||
case ME_FETCH_SKIP:
|
case ME_FETCH_SKIP:
|
||||||
return state.set('me', null);
|
return state.set('me', null);
|
||||||
case MASTODON_PRELOAD_IMPORT:
|
case MASTODON_PRELOAD_IMPORT:
|
||||||
return importMastodonPreload(state, fromJS(action.data));
|
return importMastodonPreload(state, fromJS(action.data) as ImmutableMap<string, any>);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -309,33 +360,33 @@ const reducer = (state, action) => {
|
||||||
const reload = () => location.replace('/');
|
const reload = () => location.replace('/');
|
||||||
|
|
||||||
// `me` is a user ID string
|
// `me` is a user ID string
|
||||||
const validMe = state => {
|
const validMe = (state: State) => {
|
||||||
const me = state.get('me');
|
const me = state.me;
|
||||||
return typeof me === 'string' && me !== '_legacy';
|
return typeof me === 'string' && me !== '_legacy';
|
||||||
};
|
};
|
||||||
|
|
||||||
// `me` has changed from one valid ID to another
|
// `me` has changed from one valid ID to another
|
||||||
const userSwitched = (oldState, state) => {
|
const userSwitched = (oldState: State, state: State) => {
|
||||||
const me = state.get('me');
|
const me = state.me;
|
||||||
const oldMe = oldState.get('me');
|
const oldMe = oldState.me;
|
||||||
|
|
||||||
const stillValid = validMe(oldState) && validMe(state);
|
const stillValid = validMe(oldState) && validMe(state);
|
||||||
const didChange = oldMe !== me;
|
const didChange = oldMe !== me;
|
||||||
const userUpgradedUrl = state.getIn(['users', me, 'id']) === oldMe;
|
const userUpgradedUrl = state.users.get(me!)?.id === oldMe;
|
||||||
|
|
||||||
return stillValid && didChange && !userUpgradedUrl;
|
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 loggedOutStandalone = action.type === AUTH_LOGGED_OUT && action.standalone;
|
||||||
const switched = userSwitched(oldState, state);
|
const switched = userSwitched(oldState, state);
|
||||||
|
|
||||||
if (switched || loggedOutStandalone) {
|
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);
|
const state = reducer(oldState, action);
|
||||||
|
|
||||||
if (!state.equals(oldState)) {
|
if (!state.equals(oldState)) {
|
|
@ -10,6 +10,7 @@ import accounts_counters from './accounts-counters';
|
||||||
import accounts_meta from './accounts-meta';
|
import accounts_meta from './accounts-meta';
|
||||||
import admin from './admin';
|
import admin from './admin';
|
||||||
import admin_log from './admin-log';
|
import admin_log from './admin-log';
|
||||||
|
import admin_user_index from './admin-user-index';
|
||||||
import aliases from './aliases';
|
import aliases from './aliases';
|
||||||
import announcements from './announcements';
|
import announcements from './announcements';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
|
@ -118,6 +119,7 @@ const reducers = {
|
||||||
history,
|
history,
|
||||||
announcements,
|
announcements,
|
||||||
compose_event,
|
compose_event,
|
||||||
|
admin_user_index,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build a default state from all reducers: it has the key and `undefined`
|
// Build a default state from all reducers: it has the key and `undefined`
|
||||||
|
|
|
@ -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 { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload';
|
||||||
import KVStore from 'soapbox/storage/kv-store';
|
import KVStore from 'soapbox/storage/kv-store';
|
||||||
|
@ -11,24 +11,24 @@ import {
|
||||||
SOAPBOX_CONFIG_REQUEST_FAIL,
|
SOAPBOX_CONFIG_REQUEST_FAIL,
|
||||||
} from '../actions/soapbox';
|
} from '../actions/soapbox';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap<string, any>();
|
||||||
|
|
||||||
const fallbackState = ImmutableMap({
|
const fallbackState = ImmutableMap<string, any>({
|
||||||
brandColor: '#0482d8', // Azure
|
brandColor: '#0482d8', // Azure
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateFromAdmin = (state, configs) => {
|
const updateFromAdmin = (state: ImmutableMap<string, any>, configs: ImmutableList<ImmutableMap<string, any>>) => {
|
||||||
try {
|
try {
|
||||||
return ConfigDB.find(configs, ':pleroma', ':frontend_configurations')
|
return ConfigDB.find(configs, ':pleroma', ':frontend_configurations')!
|
||||||
.get('value')
|
.get('value')
|
||||||
.find(value => value.getIn(['tuple', 0]) === ':soapbox_fe')
|
.find((value: ImmutableMap<string, any>) => value.getIn(['tuple', 0]) === ':soapbox_fe')
|
||||||
.getIn(['tuple', 1]);
|
.getIn(['tuple', 1]);
|
||||||
} catch {
|
} catch {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const preloadImport = (state, action) => {
|
const preloadImport = (state: ImmutableMap<string, any>, action: Record<string, any>) => {
|
||||||
const path = '/api/pleroma/frontend_configurations';
|
const path = '/api/pleroma/frontend_configurations';
|
||||||
const feData = action.data[path];
|
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) {
|
if (host) {
|
||||||
KVStore.setItem(`soapbox_config:${host}`, soapboxConfig.toJS()).catch(console.error);
|
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);
|
persistSoapboxConfig(soapboxConfig, host);
|
||||||
return soapboxConfig;
|
return soapboxConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function soapbox(state = initialState, action) {
|
export default function soapbox(state = initialState, action: Record<string, any>) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case PLEROMA_PRELOAD_IMPORT:
|
case PLEROMA_PRELOAD_IMPORT:
|
||||||
return preloadImport(state, action);
|
return preloadImport(state, action);
|
||||||
case SOAPBOX_CONFIG_REMEMBER_SUCCESS:
|
case SOAPBOX_CONFIG_REMEMBER_SUCCESS:
|
||||||
return fromJS(action.soapboxConfig);
|
return fromJS(action.soapboxConfig);
|
||||||
case SOAPBOX_CONFIG_REQUEST_SUCCESS:
|
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:
|
case SOAPBOX_CONFIG_REQUEST_FAIL:
|
||||||
return fallbackState.mergeDeep(state);
|
return fallbackState.mergeDeep(state);
|
||||||
case ADMIN_CONFIG_UPDATE_SUCCESS:
|
case ADMIN_CONFIG_UPDATE_SUCCESS:
|
||||||
return updateFromAdmin(state, fromJS(action.configs));
|
return updateFromAdmin(state, fromJS(action.configs) as ImmutableList<ImmutableMap<string, any>>);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
|
@ -269,16 +269,16 @@ export const makeGetReport = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAuthUserIds = createSelector([
|
const getAuthUserIds = createSelector([
|
||||||
(state: RootState) => state.auth.get('users', ImmutableMap()),
|
(state: RootState) => state.auth.users,
|
||||||
], authUsers => {
|
], authUsers => {
|
||||||
return authUsers.reduce((ids: ImmutableOrderedSet<string>, authUser: ImmutableMap<string, any>) => {
|
return authUsers.reduce((ids: ImmutableOrderedSet<string>, authUser) => {
|
||||||
try {
|
try {
|
||||||
const id = authUser.get('id');
|
const id = authUser.id;
|
||||||
return validId(id) ? ids.add(id) : ids;
|
return validId(id) ? ids.add(id) : ids;
|
||||||
} catch {
|
} catch {
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
}, ImmutableOrderedSet());
|
}, ImmutableOrderedSet<string>());
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeGetOtherAccounts = () => {
|
export const makeGetOtherAccounts = () => {
|
||||||
|
|
|
@ -4,7 +4,8 @@ import type { RootState } from 'soapbox/store';
|
||||||
|
|
||||||
export const validId = (id: any) => typeof id === 'string' && id !== 'null' && id !== 'undefined';
|
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 {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
return true;
|
return true;
|
||||||
|
@ -30,11 +31,11 @@ export const isLoggedIn = (getState: () => RootState) => {
|
||||||
return validId(getState().me);
|
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) => {
|
export const getUserToken = (state: RootState, accountId?: string | false | null) => {
|
||||||
const accountUrl = state.accounts.getIn([accountId, 'url']);
|
const accountUrl = state.accounts.getIn([accountId, 'url']) as string;
|
||||||
return state.auth.getIn(['users', accountUrl, 'access_token']) as string;
|
return state.auth.users.get(accountUrl)?.access_token as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAccessToken = (state: RootState) => {
|
export const getAccessToken = (state: RootState) => {
|
||||||
|
@ -43,24 +44,23 @@ export const getAccessToken = (state: RootState) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAuthUserId = (state: RootState) => {
|
export const getAuthUserId = (state: RootState) => {
|
||||||
const me = state.auth.get('me');
|
const me = state.auth.me;
|
||||||
|
|
||||||
return ImmutableList([
|
return ImmutableList([
|
||||||
state.auth.getIn(['users', me, 'id']),
|
state.auth.users.get(me!)?.id,
|
||||||
me,
|
me,
|
||||||
]).find(validId);
|
].filter(id => id)).find(validId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAuthUserUrl = (state: RootState) => {
|
export const getAuthUserUrl = (state: RootState) => {
|
||||||
const me = state.auth.get('me');
|
const me = state.auth.me;
|
||||||
|
|
||||||
return ImmutableList([
|
return ImmutableList([
|
||||||
state.auth.getIn(['users', me, 'url']),
|
state.auth.users.get(me!)?.url,
|
||||||
me,
|
me,
|
||||||
]).find(isURL);
|
].filter(url => url)).find(isURL);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get the VAPID public key. */
|
/** Get the VAPID public key. */
|
||||||
export const getVapidKey = (state: RootState) => {
|
export const getVapidKey = (state: RootState) =>
|
||||||
return state.auth.getIn(['app', 'vapid_key']) || state.instance.getIn(['pleroma', 'vapid_public_key']);
|
(state.auth.app.vapid_key || state.instance.pleroma.get('vapid_public_key')) as string;
|
||||||
};
|
|
||||||
|
|
|
@ -32,34 +32,3 @@
|
||||||
height: $size;
|
height: $size;
|
||||||
background-size: $size $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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,14 +10,6 @@
|
||||||
@media screen and (max-width: $no-gap-breakpoint) {
|
@media screen and (max-width: $no-gap-breakpoint) {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
.card__bar {
|
|
||||||
background-color: var(--brand-color--faint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,56 +20,6 @@
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
border-bottom: 1px solid var(--brand-color--med);
|
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 {
|
.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 {
|
.account__moved-note {
|
||||||
padding: 14px 10px;
|
padding: 14px 10px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
background: var(--brand-color--faint);
|
background: var(--brand-color--faint);
|
||||||
border-top: 1px solid var(--brand-color--med);
|
border-top: 1px solid var(--brand-color--med);
|
||||||
border-bottom: 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 {
|
.account__joined-at {
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
@import 'fonts';
|
@import 'fonts';
|
||||||
@import 'basics';
|
@import 'basics';
|
||||||
@import 'accounts';
|
@import 'accounts';
|
||||||
@import 'boost';
|
|
||||||
@import 'loading';
|
@import 'loading';
|
||||||
@import 'ui';
|
@import 'ui';
|
||||||
// @import 'introduction';
|
// @import 'introduction';
|
||||||
|
@ -22,7 +21,6 @@
|
||||||
@import 'rtl';
|
@import 'rtl';
|
||||||
@import 'accessibility';
|
@import 'accessibility';
|
||||||
@import 'dyslexic';
|
@import 'dyslexic';
|
||||||
@import 'chats';
|
|
||||||
@import 'navigation';
|
@import 'navigation';
|
||||||
@import 'placeholder';
|
@import 'placeholder';
|
||||||
@import 'autosuggest';
|
@import 'autosuggest';
|
||||||
|
@ -32,7 +30,6 @@
|
||||||
@import 'components/inputs';
|
@import 'components/inputs';
|
||||||
@import 'components/dropdown-menu';
|
@import 'components/dropdown-menu';
|
||||||
@import 'components/modal';
|
@import 'components/modal';
|
||||||
@import 'components/account-header';
|
|
||||||
@import 'components/compose-form';
|
@import 'components/compose-form';
|
||||||
@import 'components/emoji-reacts';
|
@import 'components/emoji-reacts';
|
||||||
@import 'components/status';
|
@import 'components/status';
|
||||||
|
@ -46,7 +43,6 @@
|
||||||
@import 'components/react-toggle';
|
@import 'components/react-toggle';
|
||||||
@import 'components/video-player';
|
@import 'components/video-player';
|
||||||
@import 'components/audio-player';
|
@import 'components/audio-player';
|
||||||
@import 'components/profile-hover-card';
|
|
||||||
@import 'components/filters';
|
@import 'components/filters';
|
||||||
@import 'components/backups';
|
@import 'components/backups';
|
||||||
@import 'components/crypto-donate';
|
@import 'components/crypto-donate';
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,16 +2,6 @@
|
||||||
&__accounts {
|
&__accounts {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.account__display-name {
|
|
||||||
&:hover strong {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__avatar {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.empty-column-indicator {
|
&.empty-column-indicator {
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
overflow-y: unset;
|
overflow-y: unset;
|
||||||
|
@ -21,16 +11,4 @@
|
||||||
|
|
||||||
.aliases-settings-panel {
|
.aliases-settings-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.item-list article {
|
|
||||||
border-bottom: 1px solid var(--primary-text-color--faint);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.slist--flex {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&--pending {
|
&--pending {
|
||||||
font-style: italic;
|
@apply text-gray-400 italic;
|
||||||
color: var(--primary-text-color--faint);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,14 +45,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&--transparent {
|
&--transparent {
|
||||||
background: transparent;
|
@apply bg-transparent;
|
||||||
color: var(--background-color);
|
color: var(--background-color);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
background: transparent;
|
@apply text-gray-900 bg-transparent;
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
|
@ -66,301 +65,13 @@
|
||||||
margin-right: 5px;
|
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 {
|
.column-header__setting-btn {
|
||||||
&--link {
|
&--link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
.fa {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--primary-text-color--faint);
|
@apply text-gray-400 underline;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -390,49 +101,3 @@
|
||||||
margin-bottom: 30px;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,12 +20,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__header:not(.react-datepicker__header--has-time-select) {
|
.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 {
|
.react-datepicker__header {
|
||||||
@apply bg-white dark:bg-gray-900 border-b-0 py-1 px-0;
|
@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,
|
.react-datepicker__current-month,
|
||||||
|
|
|
@ -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-name {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -31,13 +9,10 @@ a.account__display-name {
|
||||||
bdi {
|
bdi {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.display-name__html {
|
&__account {
|
||||||
|
position: relative;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-name__account {
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
.filter-settings-panel {
|
.filter-settings-panel {
|
||||||
.item-list article {
|
|
||||||
border-bottom: 1px solid var(--primary-text-color--faint);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fields-group .two-col {
|
.fields-group .two-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
|
@ -234,17 +234,6 @@
|
||||||
color: var(--primary-text-color--faint);
|
color: var(--primary-text-color--faint);
|
||||||
background-color: var(--background-color);
|
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);
|
border: 1px solid var(--background-color);
|
||||||
color: var(--primary-text-color--faint);
|
color: var(--primary-text-color--faint);
|
||||||
|
|
||||||
.status__display-name {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
padding-right: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu__separator {
|
.dropdown-menu__separator {
|
||||||
@apply block m-2 h-[1px] bg-gray-200 dark:bg-gray-600;
|
@apply block m-2 h-[1px] bg-gray-200 dark:bg-gray-600;
|
||||||
}
|
}
|
||||||
|
@ -327,10 +310,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
row-gap: 10px;
|
row-gap: 10px;
|
||||||
|
|
||||||
.unauthorized-modal-content__button {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__fields {
|
&__fields {
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
.display-name__account {
|
|
||||||
position: relative;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
|
@ -32,8 +32,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle-track {
|
.react-toggle-track {
|
||||||
@apply bg-gray-500 dark:bg-gray-700 w-[50px] p-0 rounded-full transition-colors;
|
@apply bg-gray-500 dark:bg-gray-700 h-[30px] w-[50px] p-0 rounded-full transition-colors;
|
||||||
height: var(--input-height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle--checked .react-toggle-track {
|
.react-toggle--checked .react-toggle-track {
|
||||||
|
|
|
@ -13,41 +13,6 @@
|
||||||
}
|
}
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
animation: fade 150ms linear;
|
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,
|
[column-type=filled] .status__wrapper,
|
||||||
|
@ -93,12 +58,6 @@
|
||||||
.focusable-within:focus-within {
|
.focusable-within:focus-within {
|
||||||
outline: 0; /* Required b/c HotKeys lib sets this outline */
|
outline: 0; /* Required b/c HotKeys lib sets this outline */
|
||||||
@apply ring-2 ring-primary-300;
|
@apply ring-2 ring-primary-300;
|
||||||
|
|
||||||
.status.status-direct {
|
|
||||||
&.muted {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card {
|
.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;
|
@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-video,
|
||||||
.status-card-audio {
|
.status-card-audio {
|
||||||
iframe {
|
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 {
|
.status-card__image {
|
||||||
flex: 0 0 40%;
|
flex: 0 0 40%;
|
||||||
background: var(--brand-color--med);
|
background: var(--brand-color--med);
|
||||||
|
@ -165,10 +103,6 @@ a.status-card {
|
||||||
|
|
||||||
.status-card.horizontal {
|
.status-card.horizontal {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.status-card__title {
|
|
||||||
white-space: inherit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card.compact {
|
.status-card.compact {
|
||||||
|
@ -194,27 +128,9 @@ a.status-card {
|
||||||
.status {
|
.status {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
|
||||||
&__avatar {
|
|
||||||
position: relative;
|
|
||||||
margin-right: 10px;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__profile {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__display-name {
|
|
||||||
.display-name__account {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,3 @@
|
||||||
font-family: 'OpenDyslexic' !important;
|
font-family: 'OpenDyslexic' !important;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dyslexic {
|
|
||||||
|
|
||||||
@media screen and (max-width: $nav-breakpoint-2) {
|
|
||||||
.column-header > button,
|
|
||||||
.column-header > .btn {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -22,14 +22,6 @@ select {
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
$no-columns-breakpoint: 600px;
|
|
||||||
|
|
||||||
.form-container {
|
|
||||||
max-width: 400px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple_form {
|
.simple_form {
|
||||||
.input {
|
.input {
|
||||||
&.hidden {
|
&.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 {
|
.required abbr {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: lighten($error-value-color, 12%);
|
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 {
|
.input.radio_buttons .radio label {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
@ -390,11 +315,6 @@ $no-columns-breakpoint: 600px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
&.actions--top {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// button,
|
// button,
|
||||||
|
@ -483,98 +403,6 @@ $no-columns-breakpoint: 600px;
|
||||||
// padding-top: 0.5rem;
|
// 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 {
|
h2 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: normal;
|
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 {
|
.input.with_label.toggle .label_input {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -692,48 +474,3 @@ $no-columns-breakpoint: 600px;
|
||||||
margin-left: 10px;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,16 +31,6 @@
|
||||||
100% { background-position-x: 0; }
|
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 {
|
.media-gallery.media-gallery--placeholder {
|
||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
|
|
|
@ -1,59 +1,16 @@
|
||||||
body.rtl {
|
body.rtl {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
|
|
||||||
.column-header > button {
|
.column-link__icon {
|
||||||
text-align: right;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-link__icon,
|
|
||||||
.column-header__icon {
|
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-left: 5px;
|
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 {
|
.status {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 68px;
|
padding-right: 68px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__avatar-overlay-overlay {
|
|
||||||
right: auto;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.privacy-dropdown__dropdown {
|
.privacy-dropdown__dropdown {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 40px;
|
margin-right: 40px;
|
||||||
|
@ -64,16 +21,6 @@ body.rtl {
|
||||||
margin-right: 0;
|
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 {
|
.simple_form .input.with_label.boolean label.checkbox {
|
||||||
padding-left: 25px;
|
padding-left: 25px;
|
||||||
padding-right: 0;
|
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 {
|
.simple_form .input.radio_buttons .radio > label input {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
|
@ -61,29 +61,11 @@ body,
|
||||||
// Colors
|
// Colors
|
||||||
--gray-900: #08051b;
|
--gray-900: #08051b;
|
||||||
// --gray-800: #1d1932;
|
// --gray-800: #1d1932;
|
||||||
--gray-700: #37344c;
|
|
||||||
--gray-500: #656175;
|
--gray-500: #656175;
|
||||||
--gray-400: #868393;
|
--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
|
// Forms
|
||||||
--input-height: 30px;
|
|
||||||
--input-border-color: #d1d5db;
|
--input-border-color: #d1d5db;
|
||||||
|
|
||||||
// Typography
|
|
||||||
--font-sans: 'Inter', arial, sans-serif;
|
|
||||||
--font-weight-heading: 700;
|
|
||||||
--font-weight-body: 400;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-mode-light {
|
.theme-mode-light {
|
||||||
|
@ -93,7 +75,6 @@ body,
|
||||||
var(--brand-color_s),
|
var(--brand-color_s),
|
||||||
calc(var(--brand-color_l) - 8%)
|
calc(var(--brand-color_l) - 8%)
|
||||||
);
|
);
|
||||||
--vignette-color: transparent;
|
|
||||||
|
|
||||||
// Meta-variables
|
// Meta-variables
|
||||||
--primary-text-color_h: 0;
|
--primary-text-color_h: 0;
|
||||||
|
@ -115,45 +96,4 @@ body,
|
||||||
var(--brand-color_s),
|
var(--brand-color_s),
|
||||||
calc(var(--brand-color_l) - 5%)
|
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%)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,87 +44,6 @@
|
||||||
&:active {
|
&:active {
|
||||||
outline: 0 !important;
|
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 {
|
.invisible {
|
||||||
|
@ -151,10 +70,6 @@
|
||||||
|
|
||||||
.ellipsis::after { content: "…"; }
|
.ellipsis::after { content: "…"; }
|
||||||
|
|
||||||
.no-reduce-motion .spoiler-input {
|
|
||||||
transition: height 0.4s ease, opacity 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-loader {
|
.image-loader {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -205,53 +120,17 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 0 calc(var(--thumb-navigation-height) + 86px);
|
padding: 0 0 calc(var(--thumb-navigation-height) + 86px);
|
||||||
|
|
||||||
.bg-shape-container {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
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 {
|
.slist__append {
|
||||||
&--flex {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__append {
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 30px 15px;
|
padding: 30px 15px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-text {
|
.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 {
|
.text-btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -316,170 +167,6 @@
|
||||||
cursor: pointer;
|
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 {
|
::-webkit-scrollbar-thumb {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// Commonly used web colors
|
||||||
|
|
||||||
$success-green: #79bd9a !default; // Padua
|
|
||||||
$error-red: #df405a !default; // Cerise
|
$error-red: #df405a !default; // Cerise
|
||||||
$warning-red: #ff5050 !default; // Sunset Orange
|
|
||||||
$gold-star: #ca8f04 !default; // Dark Goldenrod
|
|
||||||
|
|
||||||
// Variables for defaults in UI
|
// Variables for defaults in UI
|
||||||
$base-shadow-color: #000 !default;
|
$base-shadow-color: #000 !default;
|
||||||
$base-overlay-background: #000 !default;
|
$base-overlay-background: #000 !default;
|
||||||
$valid-value-color: $success-green !default;
|
|
||||||
$error-value-color: $error-red !default;
|
$error-value-color: $error-red !default;
|
||||||
|
|
||||||
// Language codes that uses CJK fonts
|
// Language codes that uses CJK fonts
|
||||||
|
@ -68,8 +22,5 @@ $no-gap-breakpoint: 415px;
|
||||||
// NOTE: Prefer CSS variables whenever possible.
|
// NOTE: Prefer CSS variables whenever possible.
|
||||||
// They're future-proof and more flexible.
|
// They're future-proof and more flexible.
|
||||||
:root {
|
:root {
|
||||||
--thumb-navigation-base-height: 60px;
|
--thumb-navigation-height: calc(60px + env(safe-area-inset-bottom));
|
||||||
--thumb-navigation-height: calc(
|
|
||||||
var(--thumb-navigation-base-height) + env(safe-area-inset-bottom)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,13 +123,12 @@
|
||||||
"es6-symbol": "^3.1.1",
|
"es6-symbol": "^3.1.1",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"exif-js": "^2.3.0",
|
"exif-js": "^2.3.0",
|
||||||
"feather-icons": "^4.28.0",
|
|
||||||
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
"html-webpack-harddisk-plugin": "^2.0.0",
|
"html-webpack-harddisk-plugin": "^2.0.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"http-link-header": "^1.0.2",
|
"http-link-header": "^1.0.2",
|
||||||
"immutable": "^4.0.0",
|
"immutable": "^4.2.1",
|
||||||
"imports-loader": "^4.0.0",
|
"imports-loader": "^4.0.0",
|
||||||
"intersection-observer": "^0.12.0",
|
"intersection-observer": "^0.12.0",
|
||||||
"intl": "^1.2.5",
|
"intl": "^1.2.5",
|
||||||
|
|
|
@ -59,14 +59,6 @@ const rules: RuleSetRule[] = [{
|
||||||
filename: 'packs/icons/[name]-[contenthash:8][ext]',
|
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$/,
|
test: /\.svg$/,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
include: [
|
include: [
|
||||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -4499,7 +4499,7 @@ core-js-pure@^3.23.3:
|
||||||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.27.1.tgz#ede4a6b8440585c7190062757069c01d37a19dca"
|
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.27.1.tgz#ede4a6b8440585c7190062757069c01d37a19dca"
|
||||||
integrity sha512-BS2NHgwwUppfeoqOXqi08mUqS5FiZpuRuJJpKsaME7kJz0xxuk0xkhDdfMIlP/zLa80krBqss1LtD7f889heAw==
|
integrity sha512-BS2NHgwwUppfeoqOXqi08mUqS5FiZpuRuJJpKsaME7kJz0xxuk0xkhDdfMIlP/zLa80krBqss1LtD7f889heAw==
|
||||||
|
|
||||||
core-js@^3.1.3, core-js@^3.15.2:
|
core-js@^3.15.2:
|
||||||
version "3.18.0"
|
version "3.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.0.tgz#9af3f4a6df9ba3428a3fb1b171f1503b3f40cc49"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.0.tgz#9af3f4a6df9ba3428a3fb1b171f1503b3f40cc49"
|
||||||
integrity sha512-WJeQqq6jOYgVgg4NrXKL0KLQhi0CT4ZOCvFL+3CQ5o7I6J8HkT5wd53EadMfqTDp1so/MT1J+w2ujhWcCJtN7w==
|
integrity sha512-WJeQqq6jOYgVgg4NrXKL0KLQhi0CT4ZOCvFL+3CQ5o7I6J8HkT5wd53EadMfqTDp1so/MT1J+w2ujhWcCJtN7w==
|
||||||
|
@ -5900,14 +5900,6 @@ fb-watchman@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bser "2.1.1"
|
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:
|
file-entry-cache@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
||||||
|
@ -6692,10 +6684,10 @@ immer@^9.0.7:
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20"
|
||||||
integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==
|
integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==
|
||||||
|
|
||||||
immutable@^4.0.0:
|
immutable@^4.2.1:
|
||||||
version "4.0.0"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.1.tgz#8a4025691018c560a40c67e43d698f816edc44d4"
|
||||||
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
|
integrity sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==
|
||||||
|
|
||||||
import-fresh@^3.0.0, import-fresh@^3.2.1:
|
import-fresh@^3.0.0, import-fresh@^3.2.1:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
|
|
Loading…
Reference in New Issue