Merge remote-tracking branch 'soapbox/main' into lexical

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2023-09-20 23:49:55 +02:00
1515 changed files with 6883 additions and 14598 deletions

View File

@ -0,0 +1,647 @@
export function AboutPage() {
return import('../../about');
}
export function EmojiPicker() {
return import('../../emoji/components/emoji-picker');
}
export function Notifications() {
return import('../../notifications');
}
export function HomeTimeline() {
return import('../../home-timeline');
}
export function PublicTimeline() {
return import('../../public-timeline');
}
export function RemoteTimeline() {
return import('../../remote-timeline');
}
export function CommunityTimeline() {
return import('../../community-timeline');
}
export function HashtagTimeline() {
return import('../../hashtag-timeline');
}
export function DirectTimeline() {
return import('../../direct-timeline');
}
export function Conversations() {
return import('../../conversations');
}
export function ListTimeline() {
return import('../../list-timeline');
}
export function Lists() {
return import('../../lists');
}
export function Bookmarks() {
return import('../../bookmarks');
}
export function Status() {
return import('../../status');
}
export function PinnedStatuses() {
return import('../../pinned-statuses');
}
export function AccountTimeline() {
return import('../../account-timeline');
}
export function AccountGallery() {
return import('../../account-gallery');
}
export function Followers() {
return import('../../followers');
}
export function Following() {
return import('../../following');
}
export function FollowRequests() {
return import('../../follow-requests');
}
export function GenericNotFound() {
return import('../../generic-not-found');
}
export function FavouritedStatuses() {
return import('../../favourited-statuses');
}
export function Blocks() {
return import('../../blocks');
}
export function DomainBlocks() {
return import('../../domain-blocks');
}
export function Mutes() {
return import('../../mutes');
}
export function MuteModal() {
return import('../components/modals/mute-modal');
}
export function Filters() {
return import('../../filters');
}
export function EditFilter() {
return import('../../filters/edit-filter');
}
export function ReportModal() {
return import('../components/modals/report-modal/report-modal');
}
export function AccountModerationModal() {
return import('../components/modals/account-moderation-modal/account-moderation-modal');
}
export function PolicyModal() {
return import('../components/modals/policy-modal');
}
export function MediaGallery() {
return import('../../../components/media-gallery');
}
export function Video() {
return import('../../video');
}
export function Audio() {
return import('../../audio');
}
export function MediaModal() {
return import('../components/modals/media-modal');
}
export function VideoModal() {
return import('../components/modals/video-modal');
}
export function BoostModal() {
return import('../components/modals/boost-modal');
}
export function ConfirmationModal() {
return import('../components/modals/confirmation-modal');
}
export function MissingDescriptionModal() {
return import('../components/modals/missing-description-modal');
}
export function ActionsModal() {
return import('../components/modals/actions-modal');
}
export function HotkeysModal() {
return import('../components/modals/hotkeys-modal');
}
export function ComposeModal() {
return import('../components/modals/compose-modal');
}
export function ReplyMentionsModal() {
return import('../components/modals/reply-mentions-modal');
}
export function UnauthorizedModal() {
return import('../components/modals/unauthorized-modal');
}
export function EditFederationModal() {
return import('../components/modals/edit-federation-modal');
}
export function EmbedModal() {
return import('../components/modals/embed-modal');
}
export function ComponentModal() {
return import('../components/modals/component-modal');
}
export function ReblogsModal() {
return import('../components/modals/reblogs-modal');
}
export function FavouritesModal() {
return import('../components/modals/favourites-modal');
}
export function DislikesModal() {
return import('../components/modals/dislikes-modal');
}
export function ReactionsModal() {
return import('../components/modals/reactions-modal');
}
export function MentionsModal() {
return import('../components/modals/mentions-modal');
}
export function LandingPageModal() {
return import('../components/modals/landing-page-modal');
}
export function BirthdaysModal() {
return import('../components/modals/birthdays-modal');
}
export function BirthdayPanel() {
return import('../../../components/birthday-panel');
}
export function ListEditor() {
return import('../../list-editor');
}
export function ListAdder() {
return import('../../list-adder');
}
export function Search() {
return import('../../search');
}
export function LoginPage() {
return import('../../auth-login/components/login-page');
}
export function ExternalLogin() {
return import('../../external-login');
}
export function LogoutPage() {
return import('../../auth-login/components/logout');
}
export function RegistrationPage() {
return import('../../auth-login/components/registration-page');
}
export function Settings() {
return import('../../settings');
}
export function EditProfile() {
return import('../../edit-profile');
}
export function EditEmail() {
return import('../../edit-email');
}
export function EmailConfirmation() {
return import('../../email-confirmation');
}
export function EditPassword() {
return import('../../edit-password');
}
export function DeleteAccount() {
return import('../../delete-account');
}
export function SoapboxConfig() {
return import('../../soapbox-config');
}
export function ExportData() {
return import('../../export-data');
}
export function ImportData() {
return import('../../import-data');
}
export function Backups() {
return import('../../backups');
}
export function PasswordReset() {
return import('../../auth-login/components/password-reset');
}
export function PasswordResetConfirm() {
return import('../../auth-login/components/password-reset-confirm');
}
export function MfaForm() {
return import('../../security/mfa-form');
}
export function ChatIndex() {
return import('../../chats');
}
export function ChatWidget() {
return import('../../chats/components/chat-widget/chat-widget');
}
export function ServerInfo() {
return import('../../server-info');
}
export function Dashboard() {
return import('../../admin');
}
export function ModerationLog() {
return import('../../admin/moderation-log');
}
export function ThemeEditor() {
return import('../../theme-editor');
}
export function UserPanel() {
return import('../components/user-panel');
}
export function PromoPanel() {
return import('../components/promo-panel');
}
export function SignUpPanel() {
return import('../components/panels/sign-up-panel');
}
export function CtaBanner() {
return import('../components/cta-banner');
}
export function FundingPanel() {
return import('../components/funding-panel');
}
export function TrendsPanel() {
return import('../components/trends-panel');
}
export function ProfileInfoPanel() {
return import('../components/profile-info-panel');
}
export function ProfileMediaPanel() {
return import('../components/profile-media-panel');
}
export function ProfileFieldsPanel() {
return import('../components/profile-fields-panel');
}
export function PinnedAccountsPanel() {
return import('../components/pinned-accounts-panel');
}
export function InstanceInfoPanel() {
return import('../components/instance-info-panel');
}
export function InstanceModerationPanel() {
return import('../components/instance-moderation-panel');
}
export function LatestAccountsPanel() {
return import('../../admin/components/latest-accounts-panel');
}
export function SidebarMenu() {
return import('../../../components/sidebar-menu');
}
export function ModalContainer() {
return import('../containers/modal-container');
}
export function ProfileHoverCard() {
return import('soapbox/components/profile-hover-card');
}
export function StatusHoverCard() {
return import('soapbox/components/status-hover-card');
}
export function CryptoDonate() {
return import('../../crypto-donate');
}
export function CryptoDonatePanel() {
return import('../../crypto-donate/components/crypto-donate-panel');
}
export function CryptoAddress() {
return import('../../crypto-donate/components/crypto-address');
}
export function CryptoDonateModal() {
return import('../components/modals/crypto-donate-modal');
}
export function ScheduledStatuses() {
return import('../../scheduled-statuses');
}
export function UserIndex() {
return import('../../admin/user-index');
}
export function FederationRestrictions() {
return import('../../federation-restrictions');
}
export function Aliases() {
return import('../../aliases');
}
export function Migration() {
return import('../../migration');
}
export function ScheduleForm() {
return import('../../compose/components/schedule-form');
}
export function WhoToFollowPanel() {
return import('../components/who-to-follow-panel');
}
export function FollowRecommendations() {
return import('../../follow-recommendations');
}
export function Directory() {
return import('../../directory');
}
export function RegisterInvite() {
return import('../../register-invite');
}
export function Share() {
return import('../../share');
}
export function NewStatus() {
return import('../../new-status');
}
export function IntentionalError() {
return import('../../intentional-error');
}
export function Developers() {
return import('../../developers');
}
export function CreateApp() {
return import('../../developers/apps/create');
}
export function SettingsStore() {
return import('../../developers/settings-store');
}
export function TestTimeline() {
return import('../../test-timeline');
}
export function ServiceWorkerInfo() {
return import('../../developers/service-worker-info');
}
export function DatePicker() {
return import('../../birthdays/date-picker');
}
export function OnboardingWizard() {
return import('../../onboarding/onboarding-wizard');
}
export function CompareHistoryModal() {
return import('../components/modals/compare-history-modal');
}
export function AuthTokenList() {
return import('../../auth-token-list');
}
export function FamiliarFollowersModal() {
return import('../components/modals/familiar-followers-modal');
}
export function AnnouncementsPanel() {
return import('../../../components/announcements/announcements-panel');
}
export function Quotes() {
return import('../../quotes');
}
export function ComposeEventModal() {
return import('../components/modals/compose-event-modal/compose-event-modal');
}
export function JoinEventModal() {
return import('../components/modals/join-event-modal');
}
export function EventHeader() {
return import('../../event/components/event-header');
}
export function EventInformation() {
return import('../../event/event-information');
}
export function EventDiscussion() {
return import('../../event/event-discussion');
}
export function EventMapModal() {
return import('../components/modals/event-map-modal');
}
export function EventParticipantsModal() {
return import('../components/modals/event-participants-modal');
}
export function Events() {
return import('../../events');
}
export function Groups() {
return import('../../groups');
}
export function GroupsDiscover() {
return import('../../groups/discover');
}
export function GroupsPopular() {
return import('../../groups/popular');
}
export function GroupsSuggested() {
return import('../../groups/suggested');
}
export function GroupsTag() {
return import('../../groups/tag');
}
export function GroupsTags() {
return import('../../groups/tags');
}
export function PendingGroupRequests() {
return import('../../groups/pending-requests');
}
export function GroupMembers() {
return import('../../group/group-members');
}
export function GroupTags() {
return import('../../group/group-tags');
}
export function GroupTagTimeline() {
return import('../../group/group-tag-timeline');
}
export function GroupTimeline() {
return import('../../group/group-timeline');
}
export function ManageGroup() {
return import('../../group/manage-group');
}
export function EditGroup() {
return import('../../group/edit-group');
}
export function GroupBlockedMembers() {
return import('../../group/group-blocked-members');
}
export function GroupMembershipRequests() {
return import('../../group/group-membership-requests');
}
export function GroupGallery() {
return import('../../group/group-gallery');
}
export function CreateGroupModal() {
return import('../components/modals/manage-group-modal/create-group-modal');
}
export function NewGroupPanel() {
return import('../components/panels/new-group-panel');
}
export function MyGroupsPanel() {
return import('../components/panels/my-groups-panel');
}
export function SuggestedGroupsPanel() {
return import('../components/panels/suggested-groups-panel');
}
export function GroupMediaPanel() {
return import('../components/group-media-panel');
}
export function NewEventPanel() {
return import('../components/panels/new-event-panel');
}
export function Announcements() {
return import('../../admin/announcements');
}
export function EditAnnouncementModal() {
return import('../components/modals/edit-announcement-modal');
}
export function FollowedTags() {
return import('../../followed-tags');
}
export function AccountNotePanel() {
return import('../components/panels/account-note-panel');
}
export function ComposeEditor() {
return import('../../compose/editor');
}

View File

@ -0,0 +1,36 @@
// APIs for normalizing fullscreen operations. Note that Edge uses
// the WebKit-prefixed APIs currently (as of Edge 16).
export const isFullscreen = (): boolean => {
return Boolean(
document.fullscreenElement ||
// @ts-ignore
document.webkitFullscreenElement ||
// @ts-ignore
document.mozFullScreenElement,
);
};
export const exitFullscreen = (): void => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if ('webkitExitFullscreen' in document) {
// @ts-ignore
document.webkitExitFullscreen();
} else if ('mozCancelFullScreen' in document) {
// @ts-ignore
document.mozCancelFullScreen();
}
};
export const requestFullscreen = (el: Element): void => {
if (el.requestFullscreen) {
el.requestFullscreen();
} else if ('webkitRequestFullscreen' in el) {
// @ts-ignore
el.webkitRequestFullscreen();
} else if ('mozRequestFullScreen' in el) {
// @ts-ignore
el.mozRequestFullScreen();
}
};

View File

@ -0,0 +1,176 @@
import React, { useRef } from 'react';
import { useHistory } from 'react-router-dom';
import { resetCompose } from 'soapbox/actions/compose';
import { openModal } from 'soapbox/actions/modals';
import { FOCUS_EDITOR_COMMAND } from 'soapbox/features/compose/editor/plugins/focus-plugin';
import { useAppSelector, useAppDispatch, useOwnAccount, useSettings } from 'soapbox/hooks';
import { HotKeys } from '../components/hotkeys';
import type { LexicalEditor } from 'lexical';
const keyMap = {
help: '?',
new: 'n',
search: ['s', '/'],
forceNew: 'option+n',
reply: 'r',
favourite: 'f',
react: 'e',
boost: 'b',
mention: 'm',
open: ['enter', 'o'],
openProfile: 'p',
moveDown: ['down', 'j'],
moveUp: ['up', 'k'],
back: 'backspace',
goToHome: 'g h',
goToNotifications: 'g n',
goToFavourites: 'g f',
goToPinned: 'g p',
goToProfile: 'g u',
goToBlocked: 'g b',
goToMuted: 'g m',
goToRequests: 'g r',
toggleHidden: 'x',
toggleSensitive: 'h',
openMedia: 'a',
};
interface IGlobalHotkeys {
children: React.ReactNode
node: React.MutableRefObject<HTMLDivElement | null>
}
const GlobalHotkeys: React.FC<IGlobalHotkeys> = ({ children, node }) => {
const hotkeys = useRef<HTMLDivElement | null>(null);
const history = useHistory();
const dispatch = useAppDispatch();
const me = useAppSelector(state => state.me);
const { account } = useOwnAccount();
const wysiwygEditor = useSettings().get('wysiwyg');
const handleHotkeyNew = (e?: KeyboardEvent) => {
e?.preventDefault();
let element;
if (wysiwygEditor) {
element = node.current?.querySelector('div[data-lexical-editor="true"]') as HTMLTextAreaElement;
} else {
element = node.current?.querySelector('textarea#compose-textarea') as HTMLTextAreaElement;
}
if (element) {
if (wysiwygEditor) {
((element as any).__lexicalEditor as LexicalEditor).dispatchCommand(FOCUS_EDITOR_COMMAND, undefined);
} else {
element.focus();
}
} else {
dispatch(openModal('COMPOSE'));
}
};
const handleHotkeySearch = (e?: KeyboardEvent) => {
e?.preventDefault();
if (!node.current) return;
const element = node.current.querySelector('input#search') as HTMLInputElement;
if (element) {
element.focus();
}
};
const handleHotkeyForceNew = (e?: KeyboardEvent) => {
handleHotkeyNew(e);
dispatch(resetCompose());
};
const handleHotkeyBack = () => {
if (window.history && window.history.length === 1) {
history.push('/');
} else {
history.goBack();
}
};
const setHotkeysRef: React.LegacyRef<typeof HotKeys> = (c: any) => {
hotkeys.current = c;
if (!me || !hotkeys.current) return;
// @ts-ignore
hotkeys.current.__mousetrap__.stopCallback = (_e, element) => {
return ['TEXTAREA', 'SELECT', 'INPUT', 'EM-EMOJI-PICKER'].includes(element.tagName) || !!element.closest('[contenteditable]');
};
};
const handleHotkeyToggleHelp = () => {
dispatch(openModal('HOTKEYS'));
};
const handleHotkeyGoToHome = () => {
history.push('/');
};
const handleHotkeyGoToNotifications = () => {
history.push('/notifications');
};
const handleHotkeyGoToFavourites = () => {
if (!account) return;
history.push(`/@${account.username}/favorites`);
};
const handleHotkeyGoToPinned = () => {
if (!account) return;
history.push(`/@${account.username}/pins`);
};
const handleHotkeyGoToProfile = () => {
if (!account) return;
history.push(`/@${account.username}`);
};
const handleHotkeyGoToBlocked = () => {
history.push('/blocks');
};
const handleHotkeyGoToMuted = () => {
history.push('/mutes');
};
const handleHotkeyGoToRequests = () => {
history.push('/follow_requests');
};
type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void };
const handlers: HotkeyHandlers = {
help: handleHotkeyToggleHelp,
new: handleHotkeyNew,
search: handleHotkeySearch,
forceNew: handleHotkeyForceNew,
back: handleHotkeyBack,
goToHome: handleHotkeyGoToHome,
goToNotifications: handleHotkeyGoToNotifications,
goToFavourites: handleHotkeyGoToFavourites,
goToPinned: handleHotkeyGoToPinned,
goToProfile: handleHotkeyGoToProfile,
goToBlocked: handleHotkeyGoToBlocked,
goToMuted: handleHotkeyGoToMuted,
goToRequests: handleHotkeyGoToRequests,
};
return (
<HotKeys keyMap={keyMap} handlers={me ? handlers : undefined} ref={setHotkeysRef} attach={window} focused>
{children}
</HotKeys>
);
};
export default GlobalHotkeys;

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Motion, MotionProps } from 'react-motion';
import { useSettings } from 'soapbox/hooks';
import ReducedMotion from './reduced-motion';
const OptionalMotion = (props: MotionProps) => {
const reduceMotion = useSettings().get('reduceMotion');
return (
reduceMotion ? <ReducedMotion {...props} /> : <Motion {...props} />
);
};
export default OptionalMotion;

View File

@ -0,0 +1,50 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { normalizeStatus } from 'soapbox/normalizers/status';
import { calculateStatus } from 'soapbox/reducers/statuses';
import { makeGetAccount } from 'soapbox/selectors';
import type { PendingStatus } from 'soapbox/reducers/pending-statuses';
import type { RootState } from 'soapbox/store';
const getAccount = makeGetAccount();
const buildMentions = (pendingStatus: PendingStatus) => {
if (pendingStatus.in_reply_to_id) {
return ImmutableList(pendingStatus.to || []).map(acct => ImmutableMap({ acct }));
} else {
return ImmutableList();
}
};
const buildPoll = (pendingStatus: PendingStatus) => {
if (pendingStatus.hasIn(['poll', 'options'])) {
return pendingStatus.poll!.update('options', (options: ImmutableMap<string, any>) => {
return options.map((title: string) => ImmutableMap({ title }));
});
} else {
return null;
}
};
export const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotencyKey: string) => {
const me = state.me as string;
const account = getAccount(state, me);
const inReplyToId = pendingStatus.in_reply_to_id;
const status = ImmutableMap({
account,
content: pendingStatus.status.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
id: `末pending-${idempotencyKey}`,
in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null),
in_reply_to_id: inReplyToId,
media_attachments: (pendingStatus.media_ids || ImmutableList()).map((id: string) => ImmutableMap({ id })),
mentions: buildMentions(pendingStatus),
poll: buildPoll(pendingStatus),
quote: pendingStatus.quote_id,
sensitive: pendingStatus.sensitive,
visibility: pendingStatus.visibility,
});
return calculateStatus(normalizeStatus(status));
};

View File

@ -0,0 +1,120 @@
import React from 'react';
import { Redirect, Route, useHistory, RouteProps, RouteComponentProps, match as MatchType } from 'react-router-dom';
import { Layout } from 'soapbox/components/ui';
import { useOwnAccount, useSettings } from 'soapbox/hooks';
import BundleColumnError from '../components/bundle-column-error';
import ColumnForbidden from '../components/column-forbidden';
import ColumnLoading from '../components/column-loading';
import ColumnsArea from '../components/columns-area';
import BundleContainer from '../containers/bundle-container';
type PageProps = {
params?: MatchType['params']
layout?: any
children: React.ReactNode
};
interface IWrappedRoute extends RouteProps {
component: (...args: any[]) => any
page?: React.ComponentType<PageProps>
content?: React.ReactNode
componentParams?: Record<string, any>
layout?: any
publicRoute?: boolean
staffOnly?: boolean
adminOnly?: boolean
developerOnly?: boolean
}
const WrappedRoute: React.FC<IWrappedRoute> = ({
component,
page: Page,
content,
componentParams = {},
layout,
publicRoute = false,
staffOnly = false,
adminOnly = false,
developerOnly = false,
...rest
}) => {
const history = useHistory();
const { account } = useOwnAccount();
const settings = useSettings();
const renderComponent = ({ match }: RouteComponentProps) => {
if (Page) {
return (
<BundleContainer fetchComponent={component} loading={renderLoading} error={renderError}>
{Component =>
(
<Page params={match.params} layout={layout} {...componentParams}>
<Component params={match.params} {...componentParams}>
{content}
</Component>
</Page>
)
}
</BundleContainer>
);
}
return (
<BundleContainer fetchComponent={component} loading={renderLoading} error={renderError}>
{Component =>
(
<ColumnsArea layout={layout}>
<Component params={match.params} {...componentParams}>
{content}
</Component>
</ColumnsArea>
)
}
</BundleContainer>
);
};
const renderWithLayout = (children: JSX.Element) => (
<>
<Layout.Main>
{children}
</Layout.Main>
<Layout.Aside />
</>
);
const renderLoading = () => renderWithLayout(<ColumnLoading />);
const renderForbidden = () => renderWithLayout(<ColumnForbidden />);
const renderError = (props: any) => renderWithLayout(<BundleColumnError {...props} />);
const loginRedirect = () => {
const actualUrl = encodeURIComponent(`${history.location.pathname}${history.location.search}`);
localStorage.setItem('soapbox:redirect_uri', actualUrl);
return <Redirect to='/login' />;
};
const authorized = [
account || publicRoute,
developerOnly ? settings.get('isDeveloper') : true,
staffOnly ? account && account.staff : true,
adminOnly ? account && account.admin : true,
].every(c => c);
if (!authorized) {
if (!account) {
return loginRedirect();
} else {
return renderForbidden();
}
}
return <Route {...rest} render={renderComponent} />;
};
export {
WrappedRoute,
};

View File

@ -0,0 +1,31 @@
// Like react-motion's Motion, but reduces all animations to cross-fades
// for the benefit of users with motion sickness.
import React from 'react';
import { Motion, MotionProps } from 'react-motion';
const stylesToKeep = ['opacity', 'backgroundOpacity'];
const extractValue = (value: any) => {
// This is either an object with a "val" property or it's a number
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
};
const ReducedMotion: React.FC<MotionProps> = ({ style = {}, defaultStyle = {}, children }) => {
Object.keys(style).forEach(key => {
if (stylesToKeep.includes(key)) {
return;
}
// If it's setting an x or height or scale or some other value, we need
// to preserve the end-state value without actually animating it
style[key] = defaultStyle[key] = extractValue(style[key]);
});
return (
<Motion style={style} defaultStyle={defaultStyle}>
{children}
</Motion>
);
};
export default ReducedMotion;