'use strict' import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import { HotKeys } from 'react-hotkeys' import { defineMessages, injectIntl } from 'react-intl' import { Switch, Redirect, withRouter } from 'react-router-dom' import debounce from 'lodash.debounce' import queryString from 'query-string' import moment from 'moment-mini' import { uploadCompose, resetCompose } from '../../actions/compose' import { expandHomeTimeline } from '../../actions/timelines' import { fetchGroups } from '../../actions/groups' import { initializeNotifications, expandNotifications, setFilter, } from '../../actions/notifications' import LoadingBar from '../../components/loading_bar' import { fetchFilters } from '../../actions/filters' import { clearHeight } from '../../actions/height_cache' import { openModal } from '../../actions/modal' import WrappedRoute from './util/wrapped_route' import ModalRoot from '../../components/modal/modal_root' import PopoverRoot from '../../components/popover/popover_root' import UploadArea from '../../components/upload_area' import ProfilePage from '../../pages/profile_page' import CommunityPage from '../../pages/community_page' import HashtagPage from '../../pages/hashtag_page' import ShortcutsPage from '../../pages/shortcuts_page' import GroupPage from '../../pages/group_page' import GroupsPage from '../../pages/groups_page' import SearchPage from '../../pages/search_page' import ErrorPage from '../../pages/error_page' import HomePage from '../../pages/home_page' import NotificationsPage from '../../pages/notifications_page' import ListPage from '../../pages/list_page' import ListsPage from '../../pages/lists_page' import BasicPage from '../../pages/basic_page' import ModalPage from '../../pages/modal_page' import SettingsPage from '../../pages/settings_page' import ProPage from '../../pages/pro_page' import ExplorePage from '../../pages/explore_page' import NewsPage from '../../pages/news_page' import AboutPage from '../../pages/about_page' import { About, AccountGallery, AccountTimeline, BlockedAccounts, BlockedDomains, BookmarkedStatuses, CommunityTimeline, Compose, DMCA, // Filters, Followers, Following, FollowRequests, GenericNotFound, GroupsCollection, GroupCollectionTimeline, GroupCreate, GroupAbout, GroupMembers, GroupRemovedAccounts, GroupTimeline, HashtagTimeline, HomeTimeline, Investors, LikedStatuses, ListCreate, ListsDirectory, ListEdit, ListTimeline, Mutes, News, Notifications, PrivacyPolicy, ProTimeline, Search, Shortcuts, StatusFeature, StatusLikes, StatusReposts, Suggestions, TermsOfSale, TermsOfService, } from './util/async_components' import { me, meUsername } from '../../initial_state' // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import '../../components/status' const messages = defineMessages({ beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Gab Social.' }, publish: { id: 'compose_form.publish', defaultMessage: 'Gab' }, }) const mapStateToProps = (state) => ({ isComposing: state.getIn(['compose', 'is_composing']), hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, }) const keyMap = { help: '?', new: 'n', search: 's', forceNew: 'option+n', reply: 'r', Favorite: 'f', boost: 'b', mention: 'm', open: ['enter', 'o'], openProfile: 'p', moveDown: ['down', 'j'], moveUp: ['up', 'k'], back: 'backspace', goToHome: 'g h', goToNotifications: 'g n', goToStart: 'g s', goToFavorites: 'g f', goToProfile: 'g u', goToBlocked: 'g b', goToMuted: 'g m', goToRequests: 'g r', } class SwitchingArea extends React.PureComponent { componentDidMount() { window.addEventListener('resize', this.handleResize, { passive: true }) } componentWillUnmount() { window.removeEventListener('resize', this.handleResize) } handleResize = debounce(() => { // The cached heights are no longer accurate, invalidate this.props.onLayoutChange() }, 500, { trailing: true, }) setRef = c => { this.node = c.getWrappedInstance() } render() { const { children } = this.props return ( { !!me && } { !me && } { /* */ } ) } } SwitchingArea.propTypes = { children: PropTypes.node, location: PropTypes.object, onLayoutChange: PropTypes.func.isRequired, } class UI extends React.PureComponent { static contextTypes = { router: PropTypes.object.isRequired, } state = { fetchedHome: false, fetchedNotifications: false, draggingOver: false, } handleBeforeUnload = (e) => { const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props if (isComposing && (hasComposingText || hasMediaAttachments)) { // Setting returnValue to any string causes confirmation dialog. // Many browsers no longer display this text to users, // but we set user-friendly message for other browsers, e.g. Edge. e.returnValue = intl.formatMessage(messages.beforeUnload) } } handleLayoutChange = () => { // The cached heights are no longer accurate, invalidate this.props.dispatch(clearHeight()) } handleDragEnter = (e) => { e.preventDefault() if (!this.dragTargets) { this.dragTargets = [] } if (this.dragTargets.indexOf(e.target) === -1) { this.dragTargets.push(e.target) } if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) { this.setState({ draggingOver: true, }) } } handleDragOver = (e) => { if (this.dataTransferIsText(e.dataTransfer)) return false e.preventDefault() e.stopPropagation() try { e.dataTransfer.dropEffect = 'copy' } catch (err) { // } return false } handleDrop = (e) => { if (!me) return if (this.dataTransferIsText(e.dataTransfer)) return e.preventDefault() this.setState({ draggingOver: false }) this.dragTargets = [] if (e.dataTransfer && e.dataTransfer.files.length >= 1) { this.props.dispatch(uploadCompose(e.dataTransfer.files)) } } handleDragLeave = (e) => { e.preventDefault() e.stopPropagation() this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el)) if (this.dragTargets.length > 0) return this.setState({ draggingOver: false }) } dataTransferIsText = (dataTransfer) => { return ( dataTransfer && Array.from(dataTransfer.types).includes('text/plain') && dataTransfer.items.length === 1 ) } closeUploadModal = () => { this.setState({ draggingOver: false }) } handleServiceWorkerPostMessage = ({ data }) => { if (data.type === 'navigate') { this.context.router.history.push(data.path) } else { console.warn('Unknown message type:', data.type) } } componentWillMount() { if (!me) return window.addEventListener('beforeunload', this.handleBeforeUnload, false) document.addEventListener('dragenter', this.handleDragEnter, false) document.addEventListener('dragover', this.handleDragOver, false) document.addEventListener('drop', this.handleDrop, false) document.addEventListener('dragleave', this.handleDragLeave, false) document.addEventListener('dragend', this.handleDragEnd, false) if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage) } if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { window.setTimeout(() => Notification.requestPermission(), 120 * 1000) } const pathname = this.context.router.route.location.pathname if (pathname === '/home') { this.setState({ fetchedHome: true }) this.props.dispatch(expandHomeTimeline()) } else if (pathname.startsWith('/notifications')) { try { const search = this.context.router.route.location.search const qp = queryString.parse(search) let view = `${qp.view}`.toLowerCase() if (pathname.startsWith('/notifications/follow_requests')) { view = 'follow_requests' } this.props.dispatch(setFilter('active', view)) } catch (error) { // } this.setState({ fetchedNotifications: true }) this.props.dispatch(expandNotifications()) } this.props.dispatch(initializeNotifications()) } componentDidMount() { // this.hotkeys.__mousetrap__.stopCallback = (e, element) => { // return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName) // } } componentWillUnmount() { window.removeEventListener('beforeunload', this.handleBeforeUnload) document.removeEventListener('dragenter', this.handleDragEnter) document.removeEventListener('dragover', this.handleDragOver) document.removeEventListener('drop', this.handleDrop) document.removeEventListener('dragleave', this.handleDragLeave) document.removeEventListener('dragend', this.handleDragEnd) } componentDidUpdate(prevProps) { if (this.props.location !== prevProps.location) { const pathname = this.props.location.pathname if (pathname === '/home' && !this.state.fetchedHome) { this.setState({ fetchedHome: true }) this.props.dispatch(expandHomeTimeline()) } else if (pathname.startsWith('/notifications') && !this.state.fetchedNotifications) { this.setState({ fetchedNotifications: true }) this.props.dispatch(expandNotifications()) } } } setRef = (c) => { this.node = c } handleHotkeyNew = (e) => { e.preventDefault() const element = this.node.querySelector('.compose-form__autosuggest-wrapper textarea') if (element) { element.focus() } } handleHotkeySearch = (e) => { e.preventDefault() const element = this.node.querySelector('.search__input') if (element) { element.focus() } } handleHotkeyForceNew = (e) => { this.handleHotkeyNew(e) this.props.dispatch(resetCompose()) } handleHotkeyBack = () => { if (window.history && window.history.length === 1) { this.context.router.history.push('/home') // homehack } else { this.context.router.history.goBack() } } setHotkeysRef = (c) => { this.hotkeys = c } handleHotkeyToggleHelp = () => { this.props.dispatch(openModal('HOTKEYS')) } handleHotkeyGoToHome = () => { this.context.router.history.push('/home') } handleHotkeyGoToNotifications = () => { this.context.router.history.push('/notifications') } handleHotkeyGoToStart = () => { this.context.router.history.push('/getting-started') } handleHotkeyGoToFavorites = () => { this.context.router.history.push(`/${meUsername}/favorites`) } handleHotkeyGoToProfile = () => { this.context.router.history.push(`/${meUsername}`) } handleHotkeyGoToBlocked = () => { this.context.router.history.push('/blocks') } handleHotkeyGoToMuted = () => { this.context.router.history.push('/mutes') } handleHotkeyGoToRequests = () => { this.context.router.history.push('/follow_requests') } handleOpenComposeModal = () => { this.props.dispatch(openModal('COMPOSE')) } render() { const { children, location } = this.props const { draggingOver } = this.state // : todo : // const handlers = me ? { // help: this.handleHotkeyToggleHelp, // new: this.handleHotkeyNew, // search: this.handleHotkeySearch, // forceNew: this.handleHotkeyForceNew, // back: this.handleHotkeyBack, // goToHome: this.handleHotkeyGoToHome, // goToNotifications: this.handleHotkeyGoToNotifications, // goToStart: this.handleHotkeyGoToStart, // goToFavorites: this.handleHotkeyGoToFavorites, // goToProfile: this.handleHotkeyGoToProfile, // goToBlocked: this.handleHotkeyGoToBlocked, // goToMuted: this.handleHotkeyGoToMuted, // goToRequests: this.handleHotkeyGoToRequests, // } : {} return (
{children}
) } } UI.propTypes = { dispatch: PropTypes.func.isRequired, children: PropTypes.node, isComposing: PropTypes.bool, hasComposingText: PropTypes.bool, hasMediaAttachments: PropTypes.bool, location: PropTypes.object, intl: PropTypes.object.isRequired, } export default injectIntl(withRouter(connect(mapStateToProps)(UI)))