Merge remote-tracking branch 'soapbox/main' into lexical
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
647
src/features/ui/util/async-components.ts
Normal file
647
src/features/ui/util/async-components.ts
Normal 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');
|
||||
}
|
||||
36
src/features/ui/util/fullscreen.ts
Normal file
36
src/features/ui/util/fullscreen.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
176
src/features/ui/util/global-hotkeys.tsx
Normal file
176
src/features/ui/util/global-hotkeys.tsx
Normal 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;
|
||||
16
src/features/ui/util/optional-motion.tsx
Normal file
16
src/features/ui/util/optional-motion.tsx
Normal 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;
|
||||
50
src/features/ui/util/pending-status-builder.ts
Normal file
50
src/features/ui/util/pending-status-builder.ts
Normal 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));
|
||||
};
|
||||
120
src/features/ui/util/react-router-helpers.tsx
Normal file
120
src/features/ui/util/react-router-helpers.tsx
Normal 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,
|
||||
};
|
||||
31
src/features/ui/util/reduced-motion.tsx
Normal file
31
src/features/ui/util/reduced-motion.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user