Merge remote-tracking branch 'origin/develop' into thread-cta

This commit is contained in:
Alex Gleason 2022-05-12 14:41:16 -05:00
commit c7375a2e01
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
147 changed files with 2304 additions and 2684 deletions

View File

@ -21,6 +21,7 @@ module.exports = {
plugins: [
'react',
'jsdoc',
'jsx-a11y',
'import',
'promise',
@ -65,11 +66,14 @@ module.exports = {
],
'comma-style': ['warn', 'last'],
'space-before-function-paren': ['error', 'never'],
'space-infix-ops': 'error',
'space-in-parens': ['error', 'never'],
'keyword-spacing': 'error',
'consistent-return': 'error',
'dot-notation': 'error',
eqeqeq: 'error',
indent: ['error', 2, {
SwitchCase: 1, // https://stackoverflow.com/a/53055584/8811886
ignoredNodes: ['TemplateLiteral'],
}],
'jsx-quotes': ['error', 'prefer-single'],
@ -267,5 +271,23 @@ module.exports = {
},
parser: '@typescript-eslint/parser',
},
{
// Only enforce JSDoc comments on UI components for now.
// https://www.npmjs.com/package/eslint-plugin-jsdoc
files: ['app/soapbox/components/ui/**/*'],
rules: {
'jsdoc/require-jsdoc': ['error', {
publicOnly: true,
require: {
ArrowFunctionExpression: true,
ClassDeclaration: true,
ClassExpression: true,
FunctionDeclaration: true,
FunctionExpression: true,
MethodDefinition: true,
},
}],
},
},
],
};

View File

@ -1,8 +1,6 @@
# Custom icons
- dashboard-filled.svg - Modified from Tabler icons, MIT
- fediverse.svg - Modified from Wikipedia, CC0
- home-squared.svg - Modified from Tabler icons, MIT
- pen-plus.svg - Modified from Tabler icons, MIT
- verified.svg - Created by Alex Gleason. CC0

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 14v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h6M19 9a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 267 B

View File

@ -1 +0,0 @@
<svg fill="white" stroke="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="M8.21 4.175V5.86h1.685a.842.842 0 0 1 0 1.684H8.21v1.684a.842.842 0 1 1-1.685 0V7.544H4.842a.842.842 0 1 1 0-1.684h1.684V4.175a.842.842 0 1 1 1.685 0Zm12.87 3.523a.814.814 0 0 1 0 1.18l-1.43 1.6-3.2-3.2 1.515-1.517a.814.814 0 0 1 1.179 0l1.937 1.937ZM6.573 18.364a5 5 0 0 1 1.392-2.686l7.559-7.559 3.116 3.2-7.47 7.544a5 5 0 0 1-2.704 1.409l-2.29.395.397-2.303Z"/></svg>

Before

Width:  |  Height:  |  Size: 487 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler" width="24" height="24" stroke-width="2" stroke="currentColor" fill="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M11.969 3.955A9 9 0 0 0 6.4 20H17.6a9 9 0 0 0-5.631-16.045zM15.529 8.5a1 1 0 0 1 .678 1.707l-1.51 1.51c.189.391.303.823.303 1.283 0 1.645-1.355 3-3 3s-3-1.355-3-3 1.355-3 3-3c.46 0 .892.114 1.283.303l1.51-1.51a1 1 0 0 1 .736-.293zM12 12c-.564 0-1 .436-1 1s.436 1 1 1 1-.436 1-1-.436-1-1-1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 506 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M22 3H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1ZM1 15h22M1 21h22" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 264 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler" viewBox="0 0 24 24" width="24" height="24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="m 12,3 -9,7 v 11 c 1.9540513,0 3.8465823,0 6,0 v -6 c 0,-1.104569 0.8954305,-2 2,-2 h 2 c 1.104569,0 2,0.895431 2,2 v 6 c 2,0 4,0 6,0 V 10 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 370 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0zm-4 7a7 7 0 0 0-7 7h14a7 7 0 0 0-7-7z"/></svg>

Before

Width:  |  Height:  |  Size: 247 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#ffffff"/></svg>

After

Width:  |  Height:  |  Size: 812 B

View File

@ -67,7 +67,7 @@ export function createFilter(intl, phrase, expires_at, context, whole_word, irre
export function deleteFilter(intl, id) {
return (dispatch, getState) => {
dispatch({ type: FILTERS_DELETE_REQUEST });
return api(getState).delete('/api/v1/filters/'+id).then(response => {
return api(getState).delete('/api/v1/filters/' + id).then(response => {
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
dispatch(snackbar.success(intl.formatMessage(messages.removed)));
}).catch(error => {

View File

@ -46,7 +46,7 @@ export const fetchInstance = createAsyncThunk<void, void, { state: RootState }>(
dispatch(fetchNodeinfo());
}
return instance;
} catch(e) {
} catch (e) {
return rejectWithValue(e);
}
},

View File

@ -51,7 +51,7 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a
},
onReceive(data) {
switch(data.event) {
switch (data.event) {
case 'update':
dispatch(processTimelineUpdate(timelineId, JSON.parse(data.payload), accept));
break;

View File

@ -108,7 +108,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
return;
}
switch(e.key) {
switch (e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();

View File

@ -97,7 +97,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
return;
}
switch(e.key) {
switch (e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();

View File

@ -95,25 +95,25 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
const index = items.indexOf(document.activeElement as any);
let element = null;
switch(e.key) {
switch (e.key) {
case 'ArrowDown':
element = items[index+1] || items[0];
element = items[index + 1] || items[0];
break;
case 'ArrowUp':
element = items[index-1] || items[items.length-1];
element = items[index - 1] || items[items.length - 1];
break;
case 'Tab':
if (e.shiftKey) {
element = items[index-1] || items[items.length-1];
element = items[index - 1] || items[items.length - 1];
} else {
element = items[index+1] || items[0];
element = items[index + 1] || items[0];
}
break;
case 'Home':
element = items[0];
break;
case 'End':
element = items[items.length-1];
element = items[items.length - 1];
break;
case 'Escape':
this.props.onClose();
@ -312,7 +312,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
}
handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (e) => {
switch(e.key) {
switch (e.key) {
case ' ':
case 'Enter':
this.handleMouseDown(e);
@ -321,7 +321,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
}
handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (e) => {
switch(e.key) {
switch (e.key) {
case ' ':
case 'Enter':
this.handleClick(e);

View File

@ -56,12 +56,12 @@ class FilterBar extends React.PureComponent {
const index = items.indexOf(document.activeElement);
let element = null;
switch(e.key) {
switch (e.key) {
case 'ArrowRight':
element = items[index+1] || items[0];
element = items[index + 1] || items[0];
break;
case 'ArrowLeft':
element = items[index-1] || items[items.length-1];
element = items[index - 1] || items[items.length - 1];
break;
}

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { Helmet as ReactHelmet } from'react-helmet';
import { Helmet as ReactHelmet } from 'react-helmet';
import { useAppSelector, useSettings } from 'soapbox/hooks';
import FaviconService from 'soapbox/utils/favicon_service';

View File

@ -337,7 +337,7 @@ class MediaGallery extends React.PureComponent {
const aspectRatio = media.getIn([0, 'meta', 'original', 'aspect']);
const getHeight = () => {
if (!aspectRatio) return width*9/16;
if (!aspectRatio) return width * 9 / 16;
if (isPanoramic(aspectRatio)) return Math.floor(width / maximumAspectRatio);
if (isPortrait(aspectRatio)) return Math.floor(width / minimumAspectRatio);
return Math.floor(width / aspectRatio);

View File

@ -208,7 +208,12 @@ class ModalRoot extends React.PureComponent {
})}
style={{ opacity: revealed ? 1 : 0 }}
>
<div role='presentation' id='modal-overlay' className='fixed inset-0 bg-gray-600 bg-opacity-90' onClick={this.handleOnClose} />
<div
role='presentation'
id='modal-overlay'
className='fixed inset-0 bg-gray-600 bg-opacity-90'
onClick={this.handleOnClose}
/>
<div
role='dialog'

View File

@ -35,8 +35,7 @@ const SidebarNavigation = () => {
to: '/follow_requests',
text: <FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' />,
icon: require('@tabler/icons/icons/user-plus.svg'),
// TODO: let menu items have a counter
// count: followRequestsCount,
count: followRequestsCount,
});
}
@ -138,7 +137,7 @@ const SidebarNavigation = () => {
<div className='flex flex-col space-y-2'>
<SidebarNavigationLink
to='/'
icon={require('icons/feed.svg')}
icon={require('@tabler/icons/icons/home.svg')}
text={<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />}
/>
@ -152,13 +151,13 @@ const SidebarNavigation = () => {
<>
<SidebarNavigationLink
to={`/@${account.acct}`}
icon={require('icons/user.svg')}
icon={require('@tabler/icons/icons/user.svg')}
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
/>
<SidebarNavigationLink
to='/notifications'
icon={require('icons/alert.svg')}
icon={require('@tabler/icons/icons/bell.svg')}
count={notificationCount}
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
/>

View File

@ -9,9 +9,10 @@ import { fetchOwnAccounts } from 'soapbox/actions/auth';
import { getSettings } from 'soapbox/actions/settings';
import { closeSidebar } from 'soapbox/actions/sidebar';
import Account from 'soapbox/components/account';
import SiteLogo from 'soapbox/components/site-logo';
import { Stack } from 'soapbox/components/ui';
import ProfileStats from 'soapbox/features/ui/components/profile_stats';
import { useAppSelector, useSoapboxConfig, useFeatures } from 'soapbox/hooks';
import { useAppSelector, useFeatures } from 'soapbox/hooks';
import { makeGetAccount, makeGetOtherAccounts } from 'soapbox/selectors';
import { getBaseURL } from 'soapbox/utils/accounts';
@ -66,7 +67,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const intl = useIntl();
const dispatch = useDispatch();
const { logo } = useSoapboxConfig();
const features = useFeatures();
const getAccount = makeGetAccount();
const instance = useAppSelector((state) => state.instance);
@ -141,15 +141,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<Stack space={4}>
<HStack alignItems='center' justifyContent='between'>
<Link to='/' onClick={onClose}>
{logo ? (
<img alt='Logo' src={logo} className='h-5 w-auto cursor-pointer' />
): (
<Icon
alt='Logo'
src={require('@tabler/icons/icons/home.svg')}
className='h-6 w-6 text-gray-400 hover:text-gray-600 dark:text-gray-200 cursor-pointer'
/>
)}
<SiteLogo alt='Logo' className='h-5 w-auto cursor-pointer' />
</Link>
<IconButton

View File

@ -0,0 +1,35 @@
import React from 'react';
import { useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks';
/** Display the most appropriate site logo based on the theme and configuration. */
const SiteLogo: React.FC<React.ComponentProps<'img'>> = (props) => {
const { logo, logoDarkMode } = useSoapboxConfig();
const settings = useSettings();
const systemTheme = useSystemTheme();
const userTheme = settings.get('themeMode');
const darkMode = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark');
/** Soapbox logo. */
const soapboxLogo = darkMode
? require('images/soapbox-logo-white.svg')
: require('images/soapbox-logo.svg');
// Use the right logo if provided, then use fallbacks.
const getSrc = () => {
// In demo mode, use the Soapbox logo.
if (settings.get('demo')) return soapboxLogo;
return (darkMode && logoDarkMode)
? logoDarkMode
: logo || logoDarkMode || soapboxLogo;
};
return (
// eslint-disable-next-line jsx-a11y/alt-text
<img src={getSrc()} {...props} />
);
};
export default SiteLogo;

View File

@ -673,7 +673,7 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
count={emojiReactCount}
/>
</EmojiButtonWrapper>
): (
) : (
<StatusActionButton
title={intl.formatMessage(messages.favourite)}
icon={require('@tabler/icons/icons/heart.svg')}

View File

@ -43,7 +43,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
return (
<div className='thumb-navigation'>
<ThumbNavigationLink
src={require('icons/feed.svg')}
src={require('@tabler/icons/icons/home.svg')}
text={<FormattedMessage id='navigation.home' defaultMessage='Home' />}
to='/'
exact

View File

@ -10,6 +10,7 @@ type IButtonStyles = {
size: ButtonSizes
}
/** Provides class names for the <Button> component. */
const useButtonStyles = ({
theme,
block,

View File

@ -13,7 +13,7 @@ interface ISvgIcon {
}
/** Renders an inline SVG with an empty frame loading state */
const SvgIcon: React.FC<ISvgIcon> = ({ src, alt, size = 24, className }): JSX.Element => {
const SvgIcon: React.FC<ISvgIcon> = ({ src, alt, size = 24, className, ...filteredProps }): JSX.Element => {
const loader = (
<svg
className={className}
@ -33,6 +33,7 @@ const SvgIcon: React.FC<ISvgIcon> = ({ src, alt, size = 24, className }): JSX.El
height={size}
loader={loader}
data-testid='svg-icon'
{...filteredProps}
>
{/* If the fetch fails, fall back to displaying the loader */}
{loader}

View File

@ -4,6 +4,7 @@ interface IProgressBar {
progress: number,
}
/** A horizontal meter filled to the given percentage. */
const ProgressBar: React.FC<IProgressBar> = ({ progress }) => (
<div className='h-2.5 w-full rounded-full bg-gray-100 dark:bg-slate-900/50 overflow-hidden'>
<div className='h-full bg-accent-500' style={{ width: `${Math.floor(progress * 100)}%` }} />

View File

@ -20,7 +20,7 @@ import PublicLayout from 'soapbox/features/public_layout';
import NotificationsContainer from 'soapbox/features/ui/containers/notifications_container';
import WaitlistPage from 'soapbox/features/verification/waitlist_page';
import { createGlobals } from 'soapbox/globals';
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures, useSoapboxConfig, useSettings } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures, useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks';
import MESSAGES from 'soapbox/locales/messages';
import { generateThemeCss } from 'soapbox/utils/theme';
@ -82,18 +82,13 @@ const SoapboxMount = () => {
const [localeLoading, setLocaleLoading] = useState(true);
const [isLoaded, setIsLoaded] = useState(false);
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)');
const [isSystemDarkMode, setSystemDarkMode] = useState(colorSchemeQueryList.matches);
const systemTheme = useSystemTheme();
const userTheme = settings.get('themeMode');
const darkMode = userTheme === 'dark' || (userTheme === 'system' && isSystemDarkMode);
const darkMode = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark');
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
const themeCss = generateThemeCss(soapboxConfig);
const handleSystemModeChange = (event: MediaQueryListEvent) => {
setSystemDarkMode(event.matches);
};
// Load the user's locale
useEffect(() => {
MESSAGES[locale]().then(messages => {
@ -111,12 +106,6 @@ const SoapboxMount = () => {
});
}, []);
useEffect(() => {
colorSchemeQueryList.addEventListener('change', handleSystemModeChange);
return () => colorSchemeQueryList.removeEventListener('change', handleSystemModeChange);
}, []);
// @ts-ignore: I don't actually know what these should be, lol
const shouldUpdateScroll = (prevRouterProps, { location }) => {
return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey);

View File

@ -85,7 +85,7 @@ class Audio extends React.PureComponent {
_setDimensions() {
const width = this.player.offsetWidth;
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16 / 9));
if (this.props.cacheWidth) {
this.props.cacheWidth(width);
@ -412,7 +412,7 @@ class Audio extends React.PureComponent {
}
handleKeyDown = e => {
switch(e.key) {
switch (e.key) {
case 'k':
e.preventDefault();
e.stopPropagation();

View File

@ -40,7 +40,7 @@ export default class Visualizer {
getTickPoints(count) {
const coords = [];
for(let i = 0; i < count; i++) {
for (let i = 0; i < count; i++) {
const rad = Math.PI * 2 * i / count;
coords.push({ x: Math.cos(rad), y: -Math.sin(rad) });
}

View File

@ -2,10 +2,10 @@ import React from 'react';
import { Link, Redirect, Route, Switch } from 'react-router-dom';
import LandingGradient from 'soapbox/components/landing-gradient';
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
import SiteLogo from 'soapbox/components/site-logo';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import { useAppSelector } from 'soapbox/hooks';
import { Card, CardBody } from '../../components/ui';
import LoginPage from '../auth_login/components/login_page';
@ -18,7 +18,6 @@ import Verification from '../verification';
import EmailPassthru from '../verification/email_passthru';
const AuthLayout = () => {
const { logo } = useSoapboxConfig();
const siteTitle = useAppSelector(state => state.instance.title);
return (
@ -29,15 +28,7 @@ const AuthLayout = () => {
<div className='w-full sm:max-w-lg md:max-w-2xl space-y-8'>
<header className='flex justify-center relative'>
<Link to='/' className='cursor-pointer'>
{logo ? (
<img src={logo} alt={siteTitle} className='h-7' />
) : (
<SvgIcon
className='w-7 h-7 dark:text-white'
alt={siteTitle}
src={require('@tabler/icons/icons/home.svg')}
/>
)}
<SiteLogo alt={siteTitle} className='h-7' />
</Link>
</header>

View File

@ -30,7 +30,7 @@ const CaptchaField: React.FC<ICaptchaField> = ({
onFetch = noOp,
onFetchFail = noOp,
onClick = noOp,
refreshInterval = 5*60*1000, // 5 minutes, Pleroma default
refreshInterval = 5 * 60 * 1000, // 5 minutes, Pleroma default
idempotencyKey,
}) => {
const dispatch = useAppDispatch();
@ -71,7 +71,7 @@ const CaptchaField: React.FC<ICaptchaField> = ({
};
}, [idempotencyKey]);
switch(captcha.get('type')) {
switch (captcha.get('type')) {
case 'native':
return (
<div>

View File

@ -132,7 +132,7 @@ class ChatBox extends ImmutablePureComponent {
onUploadProgress = (e) => {
const { loaded, total } = e;
this.setState({ uploadProgress: loaded/total });
this.setState({ uploadProgress: loaded / total });
}
handleFiles = (files) => {
@ -193,7 +193,7 @@ class ChatBox extends ImmutablePureComponent {
<div className='chat-box' onMouseOver={this.handleHover}>
<ChatMessageList chatMessageIds={chatMessageIds} chatId={chatId} />
{this.renderAttachment()}
<UploadProgress active={isUploading} progress={uploadProgress*100} />
<UploadProgress active={isUploading} progress={uploadProgress * 100} />
<div className='chat-box__actions simple_form'>
<div className='chat-box__send'>
{this.renderActionButton()}

View File

@ -314,11 +314,11 @@ class ChatMessageList extends ImmutablePureComponent {
return (
<div className='chat-messages' ref={this.setRef}>
{chatMessages.reduce((acc, curr, idx) => {
const lastMessage = chatMessages.get(idx-1);
const lastMessage = chatMessages.get(idx - 1);
if (lastMessage) {
const key = `${curr.get('id')}_divider`;
switch(timeChange(lastMessage, curr)) {
switch (timeChange(lastMessage, curr)) {
case 'today':
acc.push(this.renderDivider(key, intl.formatMessage(messages.today)));
break;

View File

@ -55,7 +55,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
});
let element = null;
switch(e.key) {
switch (e.key) {
case 'Escape':
this.props.onClose();
break;
@ -211,7 +211,7 @@ class PrivacyDropdown extends React.PureComponent {
}
handleKeyDown = e => {
switch(e.key) {
switch (e.key) {
case 'Escape':
this.handleClose();
break;
@ -225,7 +225,7 @@ class PrivacyDropdown extends React.PureComponent {
}
handleButtonKeyDown = (e) => {
switch(e.key) {
switch (e.key) {
case ' ':
case 'Enter':
this.handleMouseDown();

View File

@ -1,129 +0,0 @@
'use strict';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import IconButton from 'soapbox/components/icon_button';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { DatePicker } from 'soapbox/features/ui/util/async-components';
import { setSchedule, removeSchedule } from '../../../actions/compose';
const messages = defineMessages({
schedule: { id: 'schedule.post_time', defaultMessage: 'Post Date/Time' },
remove: { id: 'schedule.remove', defaultMessage: 'Remove schedule' },
});
const mapStateToProps = state => ({
active: state.getIn(['compose', 'schedule']) ? true : false,
scheduledAt: state.getIn(['compose', 'schedule']),
});
const mapDispatchToProps = dispatch => ({
onSchedule(date) {
dispatch(setSchedule(date));
},
onRemoveSchedule(date) {
dispatch(removeSchedule());
},
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class ScheduleForm extends React.Component {
static propTypes = {
scheduledAt: PropTypes.instanceOf(Date),
intl: PropTypes.object.isRequired,
onSchedule: PropTypes.func.isRequired,
onRemoveSchedule: PropTypes.func.isRequired,
dispatch: PropTypes.func,
active: PropTypes.bool,
};
state = {
initialized: false,
}
setSchedule = date => {
this.props.onSchedule(date);
}
setRef = c => {
this.datePicker = c;
}
openDatePicker = () => {
if (!this.datePicker) return;
this.datePicker.setOpen(true);
}
isCurrentOrFutureDate(date) {
return date && new Date().setHours(0, 0, 0, 0) <= new Date(date).setHours(0, 0, 0, 0);
}
isFiveMinutesFromNow(time) {
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); // now, plus five minutes (Pleroma won't schedule posts )
const selectedDate = new Date(time);
return fiveMinutesFromNow.getTime() < selectedDate.getTime();
}
handleRemove = e => {
this.props.onRemoveSchedule();
e.preventDefault();
}
initialize = () => {
const { initialized } = this.state;
if (!initialized && this.datePicker) {
this.openDatePicker();
this.setState({ initialized: true });
}
}
componentDidUpdate() {
this.initialize();
}
render() {
if (!this.props.active) {
return null;
}
const { intl, scheduledAt } = this.props;
return (
<div className={classNames('datepicker', { 'datepicker--error': !this.isFiveMinutesFromNow(scheduledAt) })}>
<div className='datepicker__hint'>
<FormattedMessage id='datepicker.hint' defaultMessage='Scheduled to post at…' />
</div>
<div className='datepicker__input'>
<BundleContainer fetchComponent={DatePicker}>
{Component => (<Component
selected={scheduledAt}
showTimeSelect
dateFormat='MMMM d, yyyy h:mm aa'
timeIntervals={15}
wrapperClassName='react-datepicker-wrapper'
onChange={this.setSchedule}
placeholderText={this.props.intl.formatMessage(messages.schedule)}
filterDate={this.isCurrentOrFutureDate}
filterTime={this.isFiveMinutesFromNow}
ref={this.setRef}
/>)}
</BundleContainer>
<div className='datepicker__cancel'>
<IconButton title={intl.formatMessage(messages.remove)} src={require('@tabler/icons/icons/x.svg')} onClick={this.handleRemove} />
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,84 @@
'use strict';
import classNames from 'classnames';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { setSchedule, removeSchedule } from 'soapbox/actions/compose';
import IconButton from 'soapbox/components/icon_button';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { DatePicker } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const isCurrentOrFutureDate = (date: Date) => {
return date && new Date().setHours(0, 0, 0, 0) <= new Date(date).setHours(0, 0, 0, 0);
};
const isFiveMinutesFromNow = (time: Date) => {
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); // now, plus five minutes (Pleroma won't schedule posts )
const selectedDate = new Date(time);
return fiveMinutesFromNow.getTime() < selectedDate.getTime();
};
const messages = defineMessages({
schedule: { id: 'schedule.post_time', defaultMessage: 'Post Date/Time' },
remove: { id: 'schedule.remove', defaultMessage: 'Remove schedule' },
});
const ScheduleForm: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const scheduledAt = useAppSelector((state) => state.compose.get('schedule'));
const active = !!scheduledAt;
const onSchedule = (date: Date) => {
dispatch(setSchedule(date));
};
const handleRemove = (e: React.MouseEvent<HTMLButtonElement>) => {
dispatch(removeSchedule());
e.preventDefault();
};
if (!active) {
return null;
}
return (
<Stack className='mb-2' space={1}>
<Text theme='muted'>
<FormattedMessage id='datepicker.hint' defaultMessage='Scheduled to post at…' />
</Text>
<HStack space={2} alignItems='center'>
<BundleContainer fetchComponent={DatePicker}>
{Component => (<Component
selected={scheduledAt}
showTimeSelect
dateFormat='MMMM d, yyyy h:mm aa'
timeIntervals={15}
wrapperClassName='react-datepicker-wrapper'
onChange={onSchedule}
placeholderText={intl.formatMessage(messages.schedule)}
filterDate={isCurrentOrFutureDate}
filterTime={isFiveMinutesFromNow}
className={classNames({
'has-error': !isFiveMinutesFromNow(scheduledAt),
})}
/>)}
</BundleContainer>
<IconButton
iconClassName='w-4 h-4'
className='bg-transparent text-gray-400 hover:text-gray-600'
src={require('@tabler/icons/icons/x.svg')}
onClick={handleRemove}
title={intl.formatMessage(messages.remove)}
/>
</HStack>
</Stack>
);
};
export default ScheduleForm;

View File

@ -158,7 +158,7 @@ class Upload extends ImmutablePureComponent {
className={classNames('compose-form__upload-thumbnail', `${mediaType}`)}
style={{
transform: `scale(${scale})`,
backgroundImage: mediaType === 'image' ? `url(${media.get('preview_url')})`: null,
backgroundImage: mediaType === 'image' ? `url(${media.get('preview_url')})` : null,
backgroundPosition: `${x}% ${y}%` }}
>
<div className={classNames('compose-form__upload__actions', { active })}>

View File

@ -177,8 +177,8 @@ export const urlRegex = (function() {
'(?:' +
'#{validGeneralUrlPathChars}*' +
'(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
'#{validUrlPathEndingChars}'+
')|(?:@#{validGeneralUrlPathChars}+\/)'+
'#{validUrlPathEndingChars}' +
')|(?:@#{validGeneralUrlPathChars}+\/)' +
')', 'i');
regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;

View File

@ -48,7 +48,7 @@ const Developers = () => {
</Link>
<Link to='/developers/timeline' className='bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center justify-center space-y-2 hover:-translate-y-1 transition-transform'>
<SvgIcon src={require('icons/feed.svg')} className='dark:text-gray-100' />
<SvgIcon src={require('@tabler/icons/icons/home.svg')} className='dark:text-gray-100' />
<Text>
<FormattedMessage id='developers.navigation.test_timeline_label' defaultMessage='Test timeline' />

View File

@ -409,7 +409,7 @@ const EditProfile: React.FC = () => {
hint={<FormattedMessage id='edit_profile.hints.hide_network' defaultMessage='Who you follow and who follows you will not be shown on your profile' />}
>
<Toggle
checked={account ? hidesNetwork(account): false}
checked={account ? hidesNetwork(account) : false}
onChange={handleHideNetworkChange}
/>
</ListItem>

View File

@ -1,4 +1,3 @@
import classNames from 'classnames';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
@ -6,6 +5,7 @@ import { Link, Redirect } from 'react-router-dom';
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
import { fetchInstance } from 'soapbox/actions/instance';
import SiteLogo from 'soapbox/components/site-logo';
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
import { openModal } from '../../../actions/modals';
@ -31,7 +31,6 @@ const Header = () => {
const soapboxConfig = useSoapboxConfig();
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
const { logo, logoDarkMode } = soapboxConfig;
const features = useFeatures();
const instance = useAppSelector((state) => state.instance);
const isOpen = features.accountCreation && instance.registrations;
@ -80,11 +79,7 @@ const Header = () => {
<Sonar />
</div>
<Link to='/' className='z-10'>
<img alt='Logo' src={logo} className={classNames('h-6 w-auto cursor-pointer', { 'dark:hidden': logoDarkMode })} />
{logoDarkMode && (
<img alt='Logo' src={logoDarkMode} className='h-6 w-auto cursor-pointer hidden dark:block' />
)}
<SiteLogo alt='Logo' className='h-6 w-auto cursor-pointer' />
<span className='hidden'>{intl.formatMessage(messages.home)}</span>
</Link>
</div>

View File

@ -1,25 +0,0 @@
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
const mapStateToProps = (state, props) => ({
instance: state.get('instance'),
soapbox: getSoapboxConfig(state),
});
class SiteBanner extends ImmutablePureComponent {
render() {
const { instance, soapbox } = this.props;
const logos = {
imgLogo: (<img alt={instance.get('title')} src={soapbox.get('banner')} />),
textLogo: (<h1>{instance.get('title')}</h1>),
};
return soapbox.getIn(['banner']) ? logos.imgLogo : logos.textLogo;
}
}
export default connect(mapStateToProps)(SiteBanner);

View File

@ -1,25 +0,0 @@
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
const mapStateToProps = (state, props) => ({
instance: state.get('instance'),
soapbox: getSoapboxConfig(state),
});
class SiteLogo extends ImmutablePureComponent {
render() {
const { instance, soapbox } = this.props;
const logos = {
imgLogo: (<img alt={instance.get('title')} src={soapbox.get('logo')} />),
textLogo: (<h1>{instance.get('title')}</h1>),
};
return soapbox.getIn(['logo']) ? logos.imgLogo : logos.textLogo;
}
}
export default connect(mapStateToProps)(SiteLogo);

View File

@ -1,69 +0,0 @@
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { Switch, Route, Redirect } from 'react-router-dom';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import LandingGradient from 'soapbox/components/landing-gradient';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import {
NotificationsContainer,
ModalContainer,
} from 'soapbox/features/ui/util/async-components';
import { isStandalone } from 'soapbox/utils/state';
import AboutPage from '../about';
import LandingPage from '../landing_page';
import MobilePage from '../mobile';
import Footer from './components/footer';
import Header from './components/header';
const mapStateToProps = (state, props) => ({
soapbox: getSoapboxConfig(state),
standalone: isStandalone(state),
});
class PublicLayout extends ImmutablePureComponent {
render() {
const { standalone } = this.props;
if (standalone) {
return <Redirect to='/login/external' />;
}
return (
<div className='h-full'>
<LandingGradient />
<div className='flex flex-col h-screen'>
<div className='flex-shrink-0'>
<Header />
<div className='relative'>
<Switch>
<Route exact path='/' component={LandingPage} />
<Route exact path='/about/:slug?' component={AboutPage} />
<Route exact path='/mobile/:slug?' component={MobilePage} />
</Switch>
</div>
</div>
<Footer />
<BundleContainer fetchComponent={NotificationsContainer}>
{(Component) => <Component />}
</BundleContainer>
<BundleContainer fetchComponent={ModalContainer}>
{(Component) => <Component />}
</BundleContainer>
</div>
</div>
);
}
}
export default connect(mapStateToProps)(PublicLayout);

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import LandingGradient from 'soapbox/components/landing-gradient';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import {
NotificationsContainer,
ModalContainer,
} from 'soapbox/features/ui/util/async-components';
import { useAppSelector } from 'soapbox/hooks';
import { isStandalone } from 'soapbox/utils/state';
import AboutPage from '../about';
import LandingPage from '../landing_page';
import MobilePage from '../mobile';
import Footer from './components/footer';
import Header from './components/header';
const PublicLayout = () => {
const standalone = useAppSelector((state) => isStandalone(state));
if (standalone) {
return <Redirect to='/login/external' />;
}
return (
<div className='h-full'>
<LandingGradient />
<div className='flex flex-col h-screen'>
<div className='flex-shrink-0'>
<Header />
<div className='relative'>
<Switch>
<Route exact path='/' component={LandingPage} />
<Route exact path='/about/:slug?' component={AboutPage} />
<Route exact path='/mobile/:slug?' component={MobilePage} />
</Switch>
</div>
</div>
<Footer />
<BundleContainer fetchComponent={NotificationsContainer}>
{(Component) => <Component />}
</BundleContainer>
<BundleContainer fetchComponent={ModalContainer}>
{(Component) => <Component />}
</BundleContainer>
</div>
</div>
);
};
export default PublicLayout;

View File

@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
import { defaultSettings } from 'soapbox/actions/settings';
import BackgroundShapes from 'soapbox/features/ui/components/background_shapes';
import { useSystemTheme } from 'soapbox/hooks';
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
import { generateThemeCss } from 'soapbox/utils/theme';
@ -17,7 +18,10 @@ const SitePreview: React.FC<ISitePreview> = ({ soapbox }) => {
const soapboxConfig = useMemo(() => normalizeSoapboxConfig(soapbox), [soapbox]);
const settings = defaultSettings.mergeDeep(soapboxConfig.defaultSettings);
const dark = settings.get('themeMode') === 'dark';
const userTheme = settings.get('themeMode');
const systemTheme = useSystemTheme();
const dark = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark');
const bodyClass = classNames(
'site-preview',

View File

@ -11,7 +11,7 @@ const ComposeButton = () => {
return (
<div className='mt-4'>
<Button icon={require('icons/compose.svg')} block size='lg' onClick={onOpenCompose}>
<Button icon={require('icons/pen-plus.svg')} block size='lg' onClick={onOpenCompose}>
<span><FormattedMessage id='navigation.compose' defaultMessage='Compose' /></span>
</Button>
</div>

View File

@ -22,7 +22,7 @@ const moneyFormat = (amount: number): string => (
currency: 'usd',
notation: 'compact',
})
.format(amount/100)
.format(amount / 100)
);
const FundingPanel: React.FC = () => {
@ -59,7 +59,7 @@ const FundingPanel: React.FC = () => {
<div className='funding-panel__ratio'>
<Text>{ratioText}</Text>
</div>
<ProgressBar progress={amount/goal} />
<ProgressBar progress={amount / goal} />
<div className='funding-panel__description'>
<Text>{goalText}</Text>
</div>

View File

@ -57,7 +57,7 @@ class MediaModal extends ImmutablePureComponent {
}
handleKeyDown = (e) => {
switch(e.key) {
switch (e.key) {
case 'ArrowLeft':
this.handlePrevClick();
e.preventDefault();

View File

@ -2,6 +2,7 @@ import classNames from 'classnames';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import SiteLogo from 'soapbox/components/site-logo';
import { Button } from 'soapbox/components/ui';
import { Modal } from 'soapbox/components/ui';
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
@ -23,7 +24,6 @@ const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
const soapboxConfig = useSoapboxConfig();
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
const { logo } = soapboxConfig;
const instance = useAppSelector((state) => state.instance);
const features = useFeatures();
@ -32,7 +32,7 @@ const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
return (
<Modal
title={<img alt='Logo' src={logo} className='h-4 w-auto' />}
title={<SiteLogo alt='Logo' className='h-4 w-auto' />}
onClose={() => onClose('LANDING_PAGE')}
>
<div className='mt-4 divide-y divide-solid divide-gray-200 dark:divide-slate-700'>

View File

@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { blockAccount } from 'soapbox/actions/accounts';
import { submitReport, cancelReport, submitReportSuccess, submitReportFail } from 'soapbox/actions/reports';
import { submitReport, submitReportSuccess, submitReportFail } from 'soapbox/actions/reports';
import { expandAccountTimeline } from 'soapbox/actions/timelines';
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
import StatusContent from 'soapbox/components/status_content';
@ -87,6 +87,7 @@ const ReportModal = ({ onClose }: IReportModal) => {
const ruleIds = useAppSelector((state) => state.reports.getIn(['new', 'rule_ids']) as ImmutableSet<string>);
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
const isReportingAccount = useMemo(() => selectedStatusIds.size === 0, []);
const shouldRequireRule = rules.length > 0;
const [currentStep, setCurrentStep] = useState<Steps>(Steps.ONE);
@ -101,11 +102,6 @@ const ReportModal = ({ onClose }: IReportModal) => {
}
};
const handleClose = () => {
dispatch(cancelReport());
onClose();
};
const handleNextStep = () => {
switch (currentStep) {
case Steps.ONE:
@ -152,8 +148,8 @@ const ReportModal = ({ onClose }: IReportModal) => {
return false;
}
return isSubmitting || (shouldRequireRule && ruleIds.isEmpty()) || selectedStatusIds.size === 0;
}, [currentStep, isSubmitting, shouldRequireRule, ruleIds, selectedStatusIds.size]);
return isSubmitting || (shouldRequireRule && ruleIds.isEmpty()) || (!isReportingAccount && selectedStatusIds.size === 0);
}, [currentStep, isSubmitting, shouldRequireRule, ruleIds, selectedStatusIds.size, isReportingAccount]);
const calculateProgress = useCallback(() => {
switch (currentStep) {
@ -183,7 +179,7 @@ const ReportModal = ({ onClose }: IReportModal) => {
return (
<Modal
title={<FormattedMessage id='report.target' defaultMessage='Reporting {target}' values={{ target: <strong>@{account.acct}</strong> }} />}
onClose={handleClose}
onClose={onClose}
cancelAction={currentStep === Steps.THREE ? undefined : onClose}
confirmationAction={handleNextStep}
confirmationText={confirmationText}
@ -193,7 +189,7 @@ const ReportModal = ({ onClose }: IReportModal) => {
<Stack space={4}>
<ProgressBar progress={calculateProgress()} />
{currentStep !== Steps.THREE && renderSelectedStatuses()}
{(currentStep !== Steps.THREE && !isReportingAccount) && renderSelectedStatuses()}
<StepToRender account={account} />
</Stack>

View File

@ -1,5 +1,5 @@
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
@ -36,6 +36,9 @@ const ReasonStep = (_props: IReasonStep) => {
const ruleIds = useAppSelector((state) => state.reports.getIn(['new', 'rule_ids']) as ImmutableSet<string>);
const shouldRequireRule = rules.length > 0;
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
const isReportingAccount = useMemo(() => selectedStatusIds.size === 0, []);
const handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
dispatch(changeReportComment(event.target.value));
};
@ -58,6 +61,16 @@ const ReasonStep = (_props: IReasonStep) => {
}
};
const filterRuleType = (rule: any) => {
const ruleTypeToFilter = isReportingAccount ? 'account' : 'content';
if (rule.rule_type) {
return rule.rule_type === ruleTypeToFilter;
}
return true;
};
useEffect(() => {
dispatch(fetchRules());
}, []);
@ -87,7 +100,7 @@ const ReasonStep = (_props: IReasonStep) => {
onScroll={handleRulesScrolling}
ref={rulesListRef}
>
{rules.map((rule, idx) => {
{rules.filter(filterRuleType).map((rule, idx) => {
const isSelected = ruleIds.includes(String(rule.id));
return (

View File

@ -4,9 +4,10 @@ import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { Avatar, Button, Icon } from 'soapbox/components/ui';
import SiteLogo from 'soapbox/components/site-logo';
import { Avatar, Button } from 'soapbox/components/ui';
import Search from 'soapbox/features/compose/components/search';
import { useOwnAccount, useSoapboxConfig, useSettings } from 'soapbox/hooks';
import { useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
import { openSidebar } from '../../../actions/sidebar';
@ -17,14 +18,9 @@ const Navbar = () => {
const node = React.useRef(null);
const account = useOwnAccount();
const settings = useSettings();
const soapboxConfig = useSoapboxConfig();
const singleUserMode = soapboxConfig.get('singleUserMode');
// In demo mode, use the Soapbox logo
const logo = settings.get('demo') ? require('images/soapbox-logo.svg') : soapboxConfig.logo;
const logoDarkMode = soapboxConfig.logoDarkMode;
const onOpenSidebar = () => dispatch(openSidebar());
return (
@ -46,21 +42,10 @@ const Navbar = () => {
'justify-start': !account,
})}
>
{logo ? (
<Link key='logo' to='/' data-preview-title-id='column.home' className='flex-shrink-0 flex items-center'>
<img alt='Logo' src={logo} className={classNames('h-5 lg:h-6 w-auto cursor-pointer', { 'dark:hidden': logoDarkMode })} />
{logoDarkMode && (
<img alt='Logo' src={logoDarkMode} className='h-5 lg:h-6 w-auto cursor-pointer hidden dark:block' />
)}
<SiteLogo alt='Logo' className='h-5 lg:h-6 w-auto cursor-pointer' />
<span className='hidden'><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></span>
</Link>
) : (
<Link key='logo' to='/' data-preview-title-id='column.home' className='flex-shrink-0 flex items-center'>
<Icon alt='Logo' src={require('@tabler/icons/icons/home.svg')} className='h-5 lg:h-6 w-auto text-primary-700' />
<span className='hidden'><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></span>
</Link>
)}
{account && (
<div className='flex-1 hidden lg:flex justify-center px-2 lg:ml-6 lg:justify-start items-center'>

View File

@ -20,7 +20,7 @@ const UploadArea: React.FC<IUploadArea> = ({ active, onClose }) => {
const keyCode = e.keyCode;
if (active) {
switch(keyCode) {
switch (keyCode) {
case 27:
e.preventDefault();
e.stopPropagation();

View File

@ -1,5 +1,7 @@
import { connect } from 'react-redux';
import { cancelReport } from 'soapbox/actions/reports';
import { cancelReplyCompose } from '../../../actions/compose';
import { closeModal } from '../../../actions/modals';
import ModalRoot from '../components/modal_root';
@ -18,8 +20,15 @@ const mapStateToProps = state => {
const mapDispatchToProps = (dispatch) => ({
onClose(type) {
if (type === 'COMPOSE') {
switch (type) {
case 'COMPOSE':
dispatch(cancelReplyCompose());
break;
case 'REPORT':
dispatch(cancelReport());
break;
default:
break;
}
dispatch(closeModal(type));

View File

@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import React from 'react';
import { useIntl } from 'react-intl';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import LandingGradient from 'soapbox/components/landing-gradient';
import SiteLogo from 'soapbox/components/site-logo';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
@ -16,8 +16,6 @@ const WaitlistPage = ({ account }) => {
const dispatch = useDispatch();
const intl = useIntl();
const logo = useSelector((state) => getSoapboxConfig(state).get('logo'));
const onClickLogOut = (event) => {
event.preventDefault();
dispatch(logOut(intl));
@ -31,7 +29,7 @@ const WaitlistPage = ({ account }) => {
<header className='relative flex justify-between h-16'>
<div className='flex-1 flex items-stretch justify-center relative'>
<Link to='/' className='cursor-pointer flex-shrink-0 flex items-center'>
<img alt='Logo' src={logo} className='h-7' />
<SiteLogo alt='Logo' className='h-7' />
</Link>
<div className='absolute inset-y-0 right-0 flex items-center pr-2 space-x-3'>

View File

@ -290,7 +290,7 @@ class Video extends React.PureComponent {
handleKeyDown = e => {
const frameTime = 1 / 25;
switch(e.key) {
switch (e.key) {
case 'k':
e.preventDefault();
e.stopPropagation();
@ -505,7 +505,7 @@ class Video extends React.PureComponent {
if (inline && containerWidth) {
width = containerWidth;
const minSize = containerWidth / (16/9);
const minSize = containerWidth / (16 / 9);
if (isPanoramic(aspectRatio)) {
height = Math.max(Math.floor(containerWidth / maximumAspectRatio), minSize);

View File

@ -6,3 +6,4 @@ export { useOnScreen } from './useOnScreen';
export { useOwnAccount } from './useOwnAccount';
export { useSettings } from './useSettings';
export { useSoapboxConfig } from './useSoapboxConfig';
export { useSystemTheme } from './useSystemTheme';

View File

@ -0,0 +1,21 @@
import { useState, useEffect } from 'react';
type SystemTheme = 'light' | 'dark';
/** Get the system color scheme of the system. */
export const useSystemTheme = (): SystemTheme => {
const query = window.matchMedia('(prefers-color-scheme: dark)');
const [dark, setDark] = useState(query.matches);
const handleChange = (event: MediaQueryListEvent) => {
setDark(event.matches);
};
useEffect(() => {
query.addEventListener('change', handleChange);
return () => query.removeEventListener('change', handleChange);
}, []);
return dark ? 'dark' : 'light';
};

View File

@ -93,7 +93,7 @@ const addTags = (
);
tags.forEach(tag => {
switch(tag) {
switch (tag) {
case 'verified':
state.setIn([id, 'verified'], true);
break;
@ -118,7 +118,7 @@ const removeTags = (
);
tags.forEach(tag => {
switch(tag) {
switch (tag) {
case 'verified':
state.setIn([id, 'verified'], false);
break;
@ -243,7 +243,7 @@ const setSuggested = (state: State, accountIds: Array<string>, isSuggested: bool
};
export default function accounts(state: State = initialState, action: AnyAction): State {
switch(action.type) {
switch (action.type) {
case ACCOUNT_IMPORT:
return fixAccount(state, action.account);
case ACCOUNTS_IMPORT:

View File

@ -36,7 +36,7 @@ const updateFollowCounters = (state, counterUpdates) => {
const initialState = ImmutableMap();
export default function accountsCounters(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case ACCOUNT_IMPORT:
return normalizeAccount(state, action.account);
case ACCOUNTS_IMPORT:

View File

@ -20,7 +20,7 @@ const importAccount = (state, account) => {
};
export default function accounts_meta(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case ME_FETCH_SUCCESS:
case ME_PATCH_SUCCESS:
return importAccount(state, fromJS(action.me));

View File

@ -137,7 +137,7 @@ function handleReportDiffs(state: State, reports: APIReport[]) {
// hence the need for a new function.
return state.withMutations(state => {
reports.forEach(report => {
switch(report.state) {
switch (report.state) {
case 'open':
state.update('openReports', orderedSet => orderedSet.add(report.id));
break;
@ -159,7 +159,7 @@ const importConfigs = (state: State, configs: any): State => {
};
export default function admin(state: State = ReducerRecord(), action: AnyAction): State {
switch(action.type) {
switch (action.type) {
case ADMIN_CONFIG_FETCH_SUCCESS:
case ADMIN_CONFIG_UPDATE_SUCCESS:
return importConfigs(state, action.configs);

View File

@ -36,7 +36,7 @@ const importItems = (state, items, total) => {
};
export default function admin_log(state = ReducerRecord(), action) {
switch(action.type) {
switch (action.type) {
case ADMIN_LOG_FETCH_SUCCESS:
return importItems(state, action.items, action.total);
default:

View File

@ -40,7 +40,7 @@ const deleteAlert = (state: State, alert: PlainAlert): State => {
};
export default function alerts(state: State = ImmutableList<Alert>(), action: AnyAction): State {
switch(action.type) {
switch (action.type) {
case ALERT_SHOW:
return importAlert(state, action);
case ALERT_DISMISS:

View File

@ -20,7 +20,7 @@ const initialState = ImmutableMap({
});
export default function aliasesReducer(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case ALIASES_FETCH_SUCCESS:
return state
.setIn(['aliases', 'items'], action.value);

View File

@ -282,7 +282,7 @@ const deleteForbiddenToken = (state, error, token) => {
};
const reducer = (state, action) => {
switch(action.type) {
switch (action.type) {
case AUTH_APP_CREATED:
return state.set('app', fromJS(action.app));
case AUTH_APP_AUTHORIZED:

View File

@ -18,7 +18,7 @@ const importBackups = (state, backups) => {
};
export default function backups(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case BACKUPS_FETCH_SUCCESS:
case BACKUPS_CREATE_SUCCESS:
return importBackups(state, fromJS(action.backups));

View File

@ -51,7 +51,7 @@ const replaceMessage = (state: State, chatId: string, oldId: string, newId: stri
};
export default function chatMessageLists(state = initialState, action: AnyAction) {
switch(action.type) {
switch (action.type) {
case CHAT_MESSAGE_SEND_REQUEST:
return updateList(state, action.chatId, [action.uuid]);
case CHATS_FETCH_SUCCESS:

View File

@ -37,7 +37,7 @@ const importLastMessages = (state: State, chats: APIEntities) =>
const initialState: State = ImmutableMap();
export default function chatMessages(state = initialState, action: AnyAction) {
switch(action.type) {
switch (action.type) {
case CHAT_MESSAGE_SEND_REQUEST:
return importMessage(state, fromJS({
id: action.uuid, // Make fake message to get overriden later

View File

@ -55,7 +55,7 @@ const importChats = (state: State, chats: APIEntities, next?: string) =>
});
export default function chats(state: State = ReducerRecord(), action: AnyAction): State {
switch(action.type) {
switch (action.type) {
case CHATS_FETCH_REQUEST:
case CHATS_EXPAND_REQUEST:
return state.set('isLoading', true);

View File

@ -283,7 +283,7 @@ const updateSetting = (state, path, value) => {
};
export default function compose(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case COMPOSE_MOUNT:
return state.set('mounted', state.get('mounted') + 1);
case COMPOSE_UNMOUNT:

View File

@ -141,7 +141,7 @@ const deletePendingStatus = (state, { in_reply_to_id }, idempotencyKey) => {
};
export default function replies(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_MUTE_SUCCESS:
return filterContexts(state, action.relationship, action.statuses);

View File

@ -13,7 +13,7 @@ const initialState = ImmutableMap({
});
export default function domainLists(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case DOMAIN_BLOCKS_FETCH_SUCCESS:
return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
case DOMAIN_BLOCKS_EXPAND_SUCCESS:

View File

@ -16,7 +16,7 @@ const importFilters = (_state: State, filters: unknown): State => {
};
export default function filters(state: State = ImmutableList<Filter>(), action: AnyAction): State {
switch(action.type) {
switch (action.type) {
case FILTERS_FETCH_SUCCESS:
return importFilters(state, action.filters);
default:

View File

@ -22,7 +22,7 @@ const initialState = ImmutableMap({
});
export default function groupEditorReducer(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case GROUP_EDITOR_RESET:
return initialState;
case GROUP_EDITOR_SETUP:

View File

@ -13,7 +13,7 @@ const normalizeList = (state, type, id, groups) => {
};
export default function groupLists(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case GROUPS_FETCH_SUCCESS:
return normalizeList(state, action.tab, action.id, action.groups);
default:

View File

@ -15,7 +15,7 @@ const normalizeRelationships = (state, relationships) => {
const initialState = ImmutableMap();
export default function group_relationships(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case GROUP_JOIN_SUCCESS:
case GROUP_LEAVE_SUCCESS:
return normalizeRelationship(state, action.relationship);

View File

@ -20,7 +20,7 @@ const normalizeGroups = (state, groups) => {
};
export default function groups(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case GROUP_FETCH_SUCCESS:
case GROUP_UPDATE_SUCCESS:
return normalizeGroup(state, action.group);

View File

@ -16,7 +16,7 @@ type State = ImmutableMap<string, ReturnType<typeof HistoryRecord>>;
const initialState: State = ImmutableMap();
export default function history(state: State = initialState, action: AnyAction) {
switch(action.type) {
switch (action.type) {
case HISTORY_FETCH_REQUEST:
return state.update(action.statusId, HistoryRecord(), history => history!.withMutations(map => {
map.set('loading', true);

View File

@ -9,7 +9,7 @@ import {
const initialState = ImmutableMap();
export default function identityProofsReducer(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST:
return state.set('isLoading', true);
case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL:

View File

@ -152,7 +152,7 @@ const logOut = (state: any = StateRecord()): ReturnType<typeof appReducer> => {
};
const rootReducer: typeof appReducer = (state, action) => {
switch(action.type) {
switch (action.type) {
case AUTH_LOGGED_OUT:
return appReducer(logOut(state), action);
default:

View File

@ -111,7 +111,7 @@ const handleInstanceFetchFail = (state: typeof initialState, error: Record<strin
};
export default function instance(state = initialState, action: AnyAction) {
switch(action.type) {
switch (action.type) {
case PLEROMA_PRELOAD_IMPORT:
return preloadImport(state, action, '/api/v1/instance');
case rememberInstance.fulfilled.type:

View File

@ -26,7 +26,7 @@ const ReducerRecord = ImmutableRecord({
type State = ReturnType<typeof ReducerRecord>;
export default function listAdderReducer(state: State = ReducerRecord(), action: AnyAction) {
switch(action.type) {
switch (action.type) {
case LIST_ADDER_RESET:
return ReducerRecord();
case LIST_ADDER_SETUP:

View File

@ -46,7 +46,7 @@ const ReducerRecord = ImmutableRecord({
type State = ReturnType<typeof ReducerRecord>;
export default function listEditorReducer(state: State = ReducerRecord(), action: AnyAction) {
switch(action.type) {
switch (action.type) {
case LIST_EDITOR_RESET:
return ReducerRecord();
case LIST_EDITOR_SETUP:

View File

@ -30,7 +30,7 @@ const importLists = (state: State, lists: APIEntities) => {
};
export default function lists(state: State = initialState, action: AnyAction) {
switch(action.type) {
switch (action.type) {
case LIST_FETCH_SUCCESS:
case LIST_CREATE_SUCCESS:
case LIST_UPDATE_SUCCESS:

View File

@ -25,7 +25,7 @@ const handleForbidden = (state: Me, error: AxiosError) => {
};
export default function me(state: Me = initialState, action: AnyAction): Me {
switch(action.type) {
switch (action.type) {
case ME_FETCH_SUCCESS:
case ME_PATCH_SUCCESS:
return action.me.id;

View File

@ -11,7 +11,7 @@ const ReducerRecord = ImmutableRecord({
});
export default function meta(state = ReducerRecord(), action: AnyAction) {
switch(action.type) {
switch (action.type) {
case fetchInstance.rejected.type:
if (action.payload.response?.status === 404) {
return state.set('instance_fetch_failed', true);

View File

@ -5,7 +5,7 @@ import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modals';
const initialState = ImmutableList();
export default function modal(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case MODAL_OPEN:
return state.push({ modalType: action.modalType, modalProps: action.modalProps });
case MODAL_CLOSE:

View File

@ -183,7 +183,7 @@ const importMarker = (state, marker) => {
};
export default function notifications(state = ReducerRecord(), action) {
switch(action.type) {
switch (action.type) {
case NOTIFICATIONS_EXPAND_REQUEST:
return state.set('isLoading', true);
case NOTIFICATIONS_EXPAND_FAIL:

View File

@ -11,7 +11,7 @@ const initialState: OnboardingState = {
};
export default function onboarding(state: OnboardingState = initialState, action: OnboardingActions): OnboardingState {
switch(action.type) {
switch (action.type) {
case ONBOARDING_START:
return { ...state, needsOnboarding: true };
case ONBOARDING_END:

View File

@ -39,7 +39,7 @@ const normalizePatronAccount = (state: State, account: Record<string, any>) => {
};
export default function patron(state = ReducerRecord(), action: AnyAction) {
switch(action.type) {
switch (action.type) {
case PATRON_INSTANCE_FETCH_SUCCESS:
return state.set('instance', PatronInstanceRecord(ImmutableMap(fromJS(action.instance))));
case PATRON_ACCOUNT_FETCH_SUCCESS:

View File

@ -14,7 +14,7 @@ const deleteStatus = (state, idempotencyKey) => state.delete(idempotencyKey);
const initialState = ImmutableMap();
export default function pending_statuses(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case STATUS_CREATE_REQUEST:
return importStatus(state, fromJS(action.params), action.idempotencyKey);
case STATUS_CREATE_SUCCESS:

View File

@ -30,7 +30,7 @@ const importPolls = (state: State, polls: Array<APIEntity>) => {
const initialState: State = ImmutableMap();
export default function polls(state: State = initialState, action: AnyAction): State {
switch(action.type) {
switch (action.type) {
case POLLS_IMPORT:
return importPolls(state, action.polls);
default:

View File

@ -9,7 +9,7 @@ import {
const initialState = ImmutableMap();
export default function profileHoverCard(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case PROFILE_HOVER_CARD_OPEN:
return ImmutableMap({
ref: action.ref,

View File

@ -17,7 +17,7 @@ const initialState = ImmutableMap({
});
export default function push_subscriptions(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case SET_SUBSCRIPTION:
return state
.set('subscription', new ImmutableMap({

View File

@ -64,7 +64,7 @@ const importPleromaAccounts = (state, accounts) => {
};
const followStateToRelationship = followState => {
switch(followState) {
switch (followState) {
case 'follow_pending':
return { following: false, requested: true };
case 'follow_accept':
@ -84,7 +84,7 @@ const updateFollowRelationship = (state, id, followState) => {
const initialState = ImmutableMap();
export default function relationships(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case ACCOUNT_IMPORT:
return importPleromaAccount(state, action.account);
case ACCOUNTS_IMPORT:

View File

@ -26,7 +26,7 @@ const initialState = ImmutableMap({
});
export default function reports(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case REPORT_INIT:
return state.withMutations(map => {
map.setIn(['new', 'isSubmitting'], false);

View File

@ -22,7 +22,7 @@ const deleteStatus = (state, id) => state.delete(id);
const initialState = ImmutableMap();
export default function scheduled_statuses(state = initialState, action) {
switch(action.type) {
switch (action.type) {
case STATUS_IMPORT:
case STATUS_CREATE_SUCCESS:
return importStatus(state, action.status);

Some files were not shown because too many files have changed in this diff Show More