762 lines
27 KiB
JavaScript
762 lines
27 KiB
JavaScript
'use strict';
|
|
|
|
import classNames from 'classnames';
|
|
import { debounce } from 'lodash';
|
|
import PropTypes from 'prop-types';
|
|
import React from 'react';
|
|
import { HotKeys } from 'react-hotkeys';
|
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
import { connect } from 'react-redux';
|
|
import { Switch, withRouter } from 'react-router-dom';
|
|
import { Redirect } from 'react-router-dom';
|
|
|
|
import { fetchChats } from 'soapbox/actions/chats';
|
|
import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis';
|
|
import { fetchMarker } from 'soapbox/actions/markers';
|
|
import { register as registerPushNotifications } from 'soapbox/actions/push_notifications';
|
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
|
import Icon from 'soapbox/components/icon';
|
|
import ThumbNavigation from 'soapbox/components/thumb_navigation';
|
|
import AdminPage from 'soapbox/pages/admin_page';
|
|
import DefaultPage from 'soapbox/pages/default_page';
|
|
// import GroupsPage from 'soapbox/pages/groups_page';
|
|
// import GroupPage from 'soapbox/pages/group_page';
|
|
import EmptyPage from 'soapbox/pages/default_page';
|
|
import HomePage from 'soapbox/pages/home_page';
|
|
import ProfilePage from 'soapbox/pages/profile_page';
|
|
import RemoteInstancePage from 'soapbox/pages/remote_instance_page';
|
|
import StatusPage from 'soapbox/pages/status_page';
|
|
import { isStaff, isAdmin } from 'soapbox/utils/accounts';
|
|
import { getAccessToken } from 'soapbox/utils/auth';
|
|
import { getVapidKey } from 'soapbox/utils/auth';
|
|
import { getFeatures } from 'soapbox/utils/features';
|
|
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
|
|
|
import { fetchFollowRequests } from '../../actions/accounts';
|
|
import { fetchReports, fetchUsers, fetchConfig } from '../../actions/admin';
|
|
import { uploadCompose, resetCompose } from '../../actions/compose';
|
|
import { fetchFilters } from '../../actions/filters';
|
|
import { clearHeight } from '../../actions/height_cache';
|
|
import { openModal } from '../../actions/modal';
|
|
import { expandNotifications } from '../../actions/notifications';
|
|
import { fetchScheduledStatuses } from '../../actions/scheduled_statuses';
|
|
import { connectUserStream } from '../../actions/streaming';
|
|
import { expandHomeTimeline } from '../../actions/timelines';
|
|
|
|
// import GroupSidebarPanel from '../groups/sidebar_panel';
|
|
import TabsBar from './components/tabs_bar';
|
|
import BundleContainer from './containers/bundle_container';
|
|
import {
|
|
Status,
|
|
CommunityTimeline,
|
|
PublicTimeline,
|
|
RemoteTimeline,
|
|
AccountTimeline,
|
|
AccountGallery,
|
|
HomeTimeline,
|
|
Followers,
|
|
Following,
|
|
DirectTimeline,
|
|
Conversations,
|
|
HashtagTimeline,
|
|
Notifications,
|
|
FollowRequests,
|
|
GenericNotFound,
|
|
FavouritedStatuses,
|
|
Blocks,
|
|
DomainBlocks,
|
|
Mutes,
|
|
Filters,
|
|
PinnedStatuses,
|
|
Search,
|
|
// Explore,
|
|
// Groups,
|
|
// GroupTimeline,
|
|
ListTimeline,
|
|
Lists,
|
|
Bookmarks,
|
|
// GroupMembers,
|
|
// GroupRemovedAccounts,
|
|
// GroupCreate,
|
|
// GroupEdit,
|
|
LoginPage,
|
|
ExternalLogin,
|
|
Preferences,
|
|
EditProfile,
|
|
SoapboxConfig,
|
|
ExportData,
|
|
ImportData,
|
|
Backups,
|
|
PasswordReset,
|
|
SecurityForm,
|
|
MfaForm,
|
|
ChatIndex,
|
|
ChatRoom,
|
|
ChatPanes,
|
|
ServerInfo,
|
|
Dashboard,
|
|
AwaitingApproval,
|
|
Reports,
|
|
ModerationLog,
|
|
CryptoDonate,
|
|
ScheduledStatuses,
|
|
UserIndex,
|
|
FederationRestrictions,
|
|
Aliases,
|
|
FollowRecommendations,
|
|
Directory,
|
|
SidebarMenu,
|
|
UploadArea,
|
|
NotificationsContainer,
|
|
ModalContainer,
|
|
ProfileHoverCard,
|
|
RegisterInvite,
|
|
Share,
|
|
NewStatus,
|
|
IntentionalError,
|
|
Developers,
|
|
CreateApp,
|
|
SettingsStore,
|
|
} from './util/async-components';
|
|
import { WrappedRoute } from './util/react_router_helpers';
|
|
|
|
|
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
|
// Without this it ends up in ~8 very commonly used bundles.
|
|
import '../../components/status';
|
|
|
|
const isMobile = width => width <= 1190;
|
|
|
|
const messages = defineMessages({
|
|
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave.' },
|
|
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
|
|
});
|
|
|
|
const mapStateToProps = state => {
|
|
const me = state.get('me');
|
|
const account = state.getIn(['accounts', me]);
|
|
const instance = state.get('instance');
|
|
const soapbox = getSoapboxConfig(state);
|
|
const vapidKey = getVapidKey(state);
|
|
|
|
return {
|
|
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
|
accessToken: getAccessToken(state),
|
|
streamingUrl: state.getIn(['instance', 'urls', 'streaming_api']),
|
|
me,
|
|
account,
|
|
features: getFeatures(instance),
|
|
soapbox,
|
|
vapidKey,
|
|
};
|
|
};
|
|
|
|
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',
|
|
};
|
|
|
|
class SwitchingColumnsArea extends React.PureComponent {
|
|
|
|
static propTypes = {
|
|
children: PropTypes.node,
|
|
location: PropTypes.object,
|
|
onLayoutChange: PropTypes.func.isRequired,
|
|
soapbox: ImmutablePropTypes.map.isRequired,
|
|
features: PropTypes.object.isRequired,
|
|
};
|
|
|
|
state = {
|
|
mobile: isMobile(window.innerWidth),
|
|
};
|
|
|
|
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();
|
|
|
|
this.setState({ mobile: isMobile(window.innerWidth) });
|
|
}, 500, {
|
|
trailing: true,
|
|
});
|
|
|
|
setRef = c => {
|
|
this.node = c.getWrappedInstance();
|
|
}
|
|
|
|
render() {
|
|
const { children, soapbox, features } = this.props;
|
|
const authenticatedProfile = soapbox.get('authenticatedProfile');
|
|
|
|
return (
|
|
<Switch>
|
|
<WrappedRoute path='/auth/sign_in' component={LoginPage} publicRoute exact />
|
|
<WrappedRoute path='/auth/reset_password' component={PasswordReset} publicRoute exact />
|
|
<WrappedRoute path='/auth/external' component={ExternalLogin} publicRoute exact />
|
|
<WrappedRoute path='/auth/edit' page={DefaultPage} component={SecurityForm} exact />
|
|
<WrappedRoute path='/auth/mfa' page={DefaultPage} component={MfaForm} exact />
|
|
|
|
<WrappedRoute path='/' exact page={HomePage} component={HomeTimeline} content={children} />
|
|
<WrappedRoute path='/timeline/local' exact page={HomePage} component={CommunityTimeline} content={children} publicRoute />
|
|
<WrappedRoute path='/timeline/fediverse' exact page={HomePage} component={PublicTimeline} content={children} publicRoute />
|
|
<WrappedRoute path='/timeline/:instance' exact page={RemoteInstancePage} component={RemoteTimeline} content={children} />
|
|
<WrappedRoute path='/conversations' page={DefaultPage} component={Conversations} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
|
<WrappedRoute path='/messages' page={DefaultPage} component={features.directTimeline ? DirectTimeline : Conversations} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
|
|
|
{/*
|
|
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'featured' }} />
|
|
<WrappedRoute path='/groups/create' page={GroupsPage} component={Groups} content={children} componentParams={{ showCreateForm: true, activeTab: 'featured' }} />
|
|
<WrappedRoute path='/groups/browse/member' page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'member' }} />
|
|
<WrappedRoute path='/groups/browse/admin' page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'admin' }} />
|
|
<WrappedRoute path='/groups/:id/members' page={GroupPage} component={GroupMembers} content={children} />
|
|
<WrappedRoute path='/groups/:id/removed_accounts' page={GroupPage} component={GroupRemovedAccounts} content={children} />
|
|
<WrappedRoute path='/groups/:id/edit' page={GroupPage} component={GroupEdit} content={children} />
|
|
<WrappedRoute path='/groups/:id' page={GroupPage} component={GroupTimeline} content={children} />
|
|
*/}
|
|
|
|
{/* Redirects from Mastodon, Pleroma FE, etc. to fix old bookmarks */}
|
|
<Redirect from='/web/:path1/:path2/:path3' to='/:path1/:path2/:path3' />
|
|
<Redirect from='/web/:path1/:path2' to='/:path1/:path2' />
|
|
<Redirect from='/web/:path' to='/:path' />
|
|
<Redirect from='/timelines/home' to='/' />
|
|
<Redirect from='/timelines/public/local' to='/timeline/local' />
|
|
<Redirect from='/timelines/public' to='/timeline/fediverse' />
|
|
<Redirect from='/timelines/direct' to='/messages' />
|
|
<Redirect from='/main/all' to='/timeline/fediverse' />
|
|
<Redirect from='/main/public' to='/timeline/local' />
|
|
<Redirect from='/main/friends' to='/' />
|
|
<Redirect from='/tag/:id' to='/tags/:id' />
|
|
<Redirect from='/user-settings' to='/settings/profile' />
|
|
<WrappedRoute path='/notice/:statusId' publicRoute exact page={DefaultPage} component={Status} content={children} />
|
|
<Redirect from='/users/:username/statuses/:statusId' to='/@:username/posts/:statusId' />
|
|
<Redirect from='/users/:username/chats' to='/chats' />
|
|
<Redirect from='/users/:username' to='/@:username' />
|
|
<Redirect from='/terms' to='/about' />
|
|
<Redirect from='/home' to='/' />
|
|
|
|
{/* Soapbox Legacy redirects */}
|
|
<Redirect from='/canary' to='/about/canary' />
|
|
<Redirect from='/canary.txt' to='/about/canary' />
|
|
|
|
<WrappedRoute path='/tags/:id' publicRoute page={DefaultPage} component={HashtagTimeline} content={children} />
|
|
|
|
<WrappedRoute path='/lists' page={DefaultPage} component={Lists} content={children} />
|
|
<WrappedRoute path='/list/:id' page={HomePage} component={ListTimeline} content={children} />
|
|
<WrappedRoute path='/bookmarks' page={DefaultPage} component={Bookmarks} content={children} />
|
|
|
|
<WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} />
|
|
|
|
<WrappedRoute path='/search' publicRoute page={DefaultPage} component={Search} content={children} />
|
|
<WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />
|
|
<WrappedRoute path='/directory' publicRoute page={DefaultPage} component={Directory} content={children} />
|
|
|
|
<WrappedRoute path='/chats' exact page={DefaultPage} component={ChatIndex} content={children} />
|
|
<WrappedRoute path='/chats/:chatId' page={DefaultPage} component={ChatRoom} content={children} />
|
|
|
|
<WrappedRoute path='/follow_requests' page={DefaultPage} component={FollowRequests} content={children} />
|
|
<WrappedRoute path='/blocks' page={DefaultPage} component={Blocks} content={children} />
|
|
<WrappedRoute path='/domain_blocks' page={DefaultPage} component={DomainBlocks} content={children} />
|
|
<WrappedRoute path='/mutes' page={DefaultPage} component={Mutes} content={children} />
|
|
<WrappedRoute path='/filters' page={DefaultPage} component={Filters} content={children} />
|
|
<WrappedRoute path='/@:username' publicRoute exact component={AccountTimeline} page={ProfilePage} content={children} />
|
|
<WrappedRoute path='/@:username/with_replies' publicRoute={!authenticatedProfile} component={AccountTimeline} page={ProfilePage} content={children} componentParams={{ withReplies: true }} />
|
|
<WrappedRoute path='/@:username/followers' publicRoute={!authenticatedProfile} component={Followers} page={ProfilePage} content={children} />
|
|
<WrappedRoute path='/@:username/following' publicRoute={!authenticatedProfile} component={Following} page={ProfilePage} content={children} />
|
|
<WrappedRoute path='/@:username/media' publicRoute={!authenticatedProfile} component={AccountGallery} page={ProfilePage} content={children} />
|
|
<WrappedRoute path='/@:username/tagged/:tag' exact component={AccountTimeline} page={ProfilePage} content={children} />
|
|
<WrappedRoute path='/@:username/favorites' component={FavouritedStatuses} page={ProfilePage} content={children} />
|
|
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
|
|
<WrappedRoute path='/@:username/posts/:statusId' publicRoute exact page={StatusPage} component={Status} content={children} />
|
|
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
|
|
|
|
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
|
|
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
|
<WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />
|
|
|
|
<Redirect from='/registration/:token' to='/invite/:token' />
|
|
<Redirect from='/registration' to='/' />
|
|
<WrappedRoute path='/invite/:token' component={RegisterInvite} content={children} publicRoute />
|
|
|
|
<Redirect exact from='/settings' to='/settings/preferences' />
|
|
<WrappedRoute path='/settings/preferences' page={DefaultPage} component={Preferences} content={children} />
|
|
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
|
|
<WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} />
|
|
<WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />
|
|
<WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />
|
|
<WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} />
|
|
<WrappedRoute path='/soapbox/config' adminOnly page={DefaultPage} component={SoapboxConfig} content={children} />
|
|
|
|
<Redirect from='/admin/dashboard' to='/admin' exact />
|
|
<WrappedRoute path='/admin' staffOnly page={AdminPage} component={Dashboard} content={children} exact />
|
|
<WrappedRoute path='/admin/approval' staffOnly page={AdminPage} component={AwaitingApproval} content={children} exact />
|
|
<WrappedRoute path='/admin/reports' staffOnly page={AdminPage} component={Reports} content={children} exact />
|
|
<WrappedRoute path='/admin/log' staffOnly page={AdminPage} component={ModerationLog} content={children} exact />
|
|
<WrappedRoute path='/admin/users' staffOnly page={AdminPage} component={UserIndex} content={children} exact />
|
|
<WrappedRoute path='/info' page={EmptyPage} component={ServerInfo} content={children} />
|
|
|
|
<WrappedRoute path='/developers/apps/create' developerOnly page={DefaultPage} component={CreateApp} content={children} />
|
|
<WrappedRoute path='/developers/settings_store' developerOnly page={DefaultPage} component={SettingsStore} content={children} />
|
|
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
|
<WrappedRoute path='/error' page={EmptyPage} component={IntentionalError} content={children} />
|
|
|
|
<WrappedRoute path='/donate/crypto' publicRoute page={DefaultPage} component={CryptoDonate} content={children} />
|
|
<WrappedRoute path='/federation_restrictions' publicRoute page={DefaultPage} component={FederationRestrictions} content={children} />
|
|
|
|
<WrappedRoute path='/share' page={DefaultPage} component={Share} content={children} exact />
|
|
|
|
<WrappedRoute page={EmptyPage} component={GenericNotFound} content={children} />
|
|
</Switch>
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
export default @connect(mapStateToProps)
|
|
@injectIntl
|
|
@withRouter
|
|
class UI extends React.PureComponent {
|
|
|
|
static contextTypes = {
|
|
router: PropTypes.object.isRequired,
|
|
};
|
|
|
|
static propTypes = {
|
|
dispatch: PropTypes.func.isRequired,
|
|
children: PropTypes.node,
|
|
location: PropTypes.object,
|
|
intl: PropTypes.object.isRequired,
|
|
dropdownMenuIsOpen: PropTypes.bool,
|
|
me: SoapboxPropTypes.me,
|
|
streamingUrl: PropTypes.string,
|
|
account: PropTypes.object,
|
|
features: PropTypes.object.isRequired,
|
|
soapbox: ImmutablePropTypes.map.isRequired,
|
|
vapidKey: PropTypes.string,
|
|
};
|
|
|
|
state = {
|
|
draggingOver: false,
|
|
mobile: isMobile(window.innerWidth),
|
|
};
|
|
|
|
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) {
|
|
// Do nothing
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
handleDrop = (e) => {
|
|
const { me } = this.props;
|
|
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);
|
|
}
|
|
}
|
|
|
|
connectStreaming = (prevProps) => {
|
|
const { dispatch } = this.props;
|
|
const keys = ['accessToken', 'streamingUrl'];
|
|
const credsSet = keys.every(p => this.props[p]);
|
|
if (!this.disconnect && credsSet) {
|
|
this.disconnect = dispatch(connectUserStream());
|
|
}
|
|
}
|
|
|
|
disconnectStreaming = () => {
|
|
if (this.disconnect) {
|
|
this.disconnect();
|
|
this.disconnect = null;
|
|
}
|
|
}
|
|
|
|
handleResize = debounce(() => {
|
|
this.setState({ mobile: isMobile(window.innerWidth) });
|
|
}, 500, {
|
|
trailing: true,
|
|
});
|
|
|
|
// Load initial data when a user is logged in
|
|
loadAccountData = () => {
|
|
const { account, features, dispatch } = this.props;
|
|
|
|
dispatch(expandHomeTimeline());
|
|
|
|
dispatch(expandNotifications())
|
|
.then(() => dispatch(fetchMarker(['notifications'])))
|
|
.catch(console.error);
|
|
|
|
if (features.chats) {
|
|
dispatch(fetchChats());
|
|
}
|
|
|
|
if (isStaff(account)) {
|
|
dispatch(fetchReports({ state: 'open' }));
|
|
dispatch(fetchUsers(['local', 'need_approval']));
|
|
}
|
|
|
|
if (isAdmin(account)) {
|
|
dispatch(fetchConfig());
|
|
}
|
|
|
|
setTimeout(() => dispatch(fetchFilters()), 500);
|
|
|
|
if (account.get('locked')) {
|
|
setTimeout(() => dispatch(fetchFollowRequests()), 700);
|
|
}
|
|
|
|
setTimeout(() => dispatch(fetchScheduledStatuses()), 900);
|
|
}
|
|
|
|
componentDidMount() {
|
|
const { account, vapidKey, dispatch } = this.props;
|
|
|
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
|
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);
|
|
}
|
|
|
|
if (account) {
|
|
this.loadAccountData();
|
|
}
|
|
|
|
dispatch(fetchCustomEmojis());
|
|
this.connectStreaming();
|
|
|
|
if (vapidKey) {
|
|
dispatch(registerPushNotifications());
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
this.connectStreaming();
|
|
|
|
const { dispatch, account, features, vapidKey } = this.props;
|
|
|
|
// The user has logged in
|
|
if (account && !prevProps.account) {
|
|
this.loadAccountData();
|
|
// The instance has loaded
|
|
} else if (account && features.chats && !prevProps.features.chats) {
|
|
dispatch(fetchChats());
|
|
}
|
|
|
|
if (vapidKey && !prevProps.vapidKey) {
|
|
dispatch(registerPushNotifications());
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener('resize', this.handleResize);
|
|
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);
|
|
this.disconnectStreaming();
|
|
}
|
|
|
|
setRef = c => {
|
|
this.node = c;
|
|
}
|
|
|
|
handleHotkeyNew = e => {
|
|
e.preventDefault();
|
|
if (!this.node) return;
|
|
|
|
const element = this.node.querySelector('.compose-form__autosuggest-wrapper textarea');
|
|
|
|
if (element) {
|
|
element.focus();
|
|
}
|
|
}
|
|
|
|
handleHotkeySearch = e => {
|
|
e.preventDefault();
|
|
if (!this.node) return;
|
|
|
|
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('/');
|
|
} else {
|
|
this.context.router.history.goBack();
|
|
}
|
|
}
|
|
|
|
setHotkeysRef = c => {
|
|
const { me } = this.props;
|
|
this.hotkeys = c;
|
|
|
|
if (!me || !this.hotkeys) return;
|
|
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
|
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
|
|
};
|
|
}
|
|
|
|
handleHotkeyToggleHelp = () => {
|
|
this.props.dispatch(openModal('HOTKEYS'));
|
|
}
|
|
|
|
handleHotkeyGoToHome = () => {
|
|
this.context.router.history.push('/');
|
|
}
|
|
|
|
handleHotkeyGoToNotifications = () => {
|
|
this.context.router.history.push('/notifications');
|
|
}
|
|
|
|
handleHotkeyGoToFavourites = () => {
|
|
const { account } = this.props;
|
|
if (!account) return;
|
|
|
|
this.context.router.history.push(`/@${account.get('username')}/favorites`);
|
|
}
|
|
|
|
handleHotkeyGoToPinned = () => {
|
|
const { account } = this.props;
|
|
if (!account) return;
|
|
|
|
this.context.router.history.push(`/@${account.get('username')}/pins`);
|
|
}
|
|
|
|
handleHotkeyGoToProfile = () => {
|
|
const { account } = this.props;
|
|
if (!account) return;
|
|
|
|
this.context.router.history.push(`/@${account.get('username')}`);
|
|
}
|
|
|
|
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'));
|
|
}
|
|
|
|
shouldHideFAB = () => {
|
|
const path = this.context.router.history.location.pathname;
|
|
return path.match(/^\/posts\/|^\/search|^\/getting-started|^\/chats/);
|
|
}
|
|
|
|
isChatRoomLocation = () => {
|
|
const path = this.context.router.history.location.pathname;
|
|
return path.match(/^\/chats\/(.*)/);
|
|
}
|
|
|
|
render() {
|
|
const { streamingUrl, features, soapbox } = this.props;
|
|
const { draggingOver, mobile } = this.state;
|
|
const { intl, children, location, dropdownMenuIsOpen, me } = this.props;
|
|
|
|
// Wait for login to succeed or fail
|
|
if (me === null) return null;
|
|
// If login didn't fail, wait for streaming to become available
|
|
if (me !== false && !streamingUrl) return null;
|
|
|
|
const handlers = me ? {
|
|
help: this.handleHotkeyToggleHelp,
|
|
new: this.handleHotkeyNew,
|
|
search: this.handleHotkeySearch,
|
|
forceNew: this.handleHotkeyForceNew,
|
|
back: this.handleHotkeyBack,
|
|
goToHome: this.handleHotkeyGoToHome,
|
|
goToNotifications: this.handleHotkeyGoToNotifications,
|
|
goToFavourites: this.handleHotkeyGoToFavourites,
|
|
goToPinned: this.handleHotkeyGoToPinned,
|
|
goToProfile: this.handleHotkeyGoToProfile,
|
|
goToBlocked: this.handleHotkeyGoToBlocked,
|
|
goToMuted: this.handleHotkeyGoToMuted,
|
|
goToRequests: this.handleHotkeyGoToRequests,
|
|
} : {};
|
|
|
|
const fabElem = (
|
|
<button
|
|
key='floating-action-button'
|
|
onClick={this.handleOpenComposeModal}
|
|
className='floating-action-button'
|
|
aria-label={intl.formatMessage(messages.publish)}
|
|
>
|
|
<Icon src={require('icons/pen-plus.svg')} />
|
|
</button>
|
|
);
|
|
|
|
const floatingActionButton = this.shouldHideFAB() ? null : fabElem;
|
|
|
|
const classnames = classNames('ui', {
|
|
'ui--chatroom': this.isChatRoomLocation(),
|
|
});
|
|
|
|
const style = {
|
|
pointerEvents: dropdownMenuIsOpen ? 'none' : null,
|
|
};
|
|
|
|
return (
|
|
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
|
<div className={classnames} ref={this.setRef} style={style}>
|
|
<TabsBar />
|
|
|
|
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange} soapbox={soapbox} features={features}>
|
|
{children}
|
|
</SwitchingColumnsArea>
|
|
|
|
{me && floatingActionButton}
|
|
|
|
<BundleContainer fetchComponent={NotificationsContainer}>
|
|
{Component => <Component />}
|
|
</BundleContainer>
|
|
|
|
<BundleContainer fetchComponent={ModalContainer}>
|
|
{Component => <Component />}
|
|
</BundleContainer>
|
|
|
|
<BundleContainer fetchComponent={UploadArea}>
|
|
{Component => <Component active={draggingOver} onClose={this.closeUploadModal} />}
|
|
</BundleContainer>
|
|
|
|
{me && (
|
|
<BundleContainer fetchComponent={SidebarMenu}>
|
|
{Component => <Component />}
|
|
</BundleContainer>
|
|
)}
|
|
{me && features.chats && !mobile && (
|
|
<BundleContainer fetchComponent={ChatPanes}>
|
|
{Component => <Component />}
|
|
</BundleContainer>
|
|
)}
|
|
<ThumbNavigation />
|
|
|
|
<BundleContainer fetchComponent={ProfileHoverCard}>
|
|
{Component => <Component />}
|
|
</BundleContainer>
|
|
</div>
|
|
</HotKeys>
|
|
);
|
|
}
|
|
|
|
}
|