diff --git a/web/modals/concurrent-modification-modal.react.js b/web/modals/concurrent-modification-modal.react.js index 3b3349cdc..be5760cf3 100644 --- a/web/modals/concurrent-modification-modal.react.js +++ b/web/modals/concurrent-modification-modal.react.js @@ -1,33 +1,32 @@ // @flow import * as React from 'react'; +import Button from '../components/button.react'; import css from '../style.css'; import { useModalContext } from './modal-provider.react'; import Modal from './modal.react'; type Props = { +onRefresh: () => void, }; export default function ConcurrentModificationModal(props: Props): React.Node { const modalContext = useModalContext(); return (

It looks like somebody is attempting to modify that field at the same time as you! Please refresh the entry and try again.

- +
); } diff --git a/web/modals/threads/confirm-leave-thread-modal.react.js b/web/modals/threads/confirm-leave-thread-modal.react.js index d76fea143..2fa4b6619 100644 --- a/web/modals/threads/confirm-leave-thread-modal.react.js +++ b/web/modals/threads/confirm-leave-thread-modal.react.js @@ -1,32 +1,35 @@ // @flow import * as React from 'react'; import { type ThreadInfo } from 'lib/types/thread-types'; +import Button from '../../components/button.react'; import css from '../../style.css'; import Modal from '../modal.react'; type Props = { +threadInfo: ThreadInfo, +onClose: () => void, +onConfirm: () => void, }; function ConfirmLeaveThreadModal(props: Props): React.Node { return (

{'Are you sure you want to leave "'} {props.threadInfo.uiName} {'"?'}

- +
); } export default ConfirmLeaveThreadModal; diff --git a/web/modals/threads/new-thread-modal.react.js b/web/modals/threads/new-thread-modal.react.js index aad066243..30f9c6af8 100644 --- a/web/modals/threads/new-thread-modal.react.js +++ b/web/modals/threads/new-thread-modal.react.js @@ -1,319 +1,321 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { newThreadActionTypes, newThread } from 'lib/actions/thread-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { generateRandomColor, threadTypeDescriptions, } from 'lib/shared/thread-utils'; import type { CalendarQuery } from 'lib/types/entry-types'; import { type ThreadInfo, threadTypes, assertThreadType, type ThreadType, type ClientNewThreadRequest, type NewThreadResult, } from 'lib/types/thread-types'; import { type DispatchActionPromise, useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import { firstLine } from 'lib/utils/string-utils'; +import Button from '../../components/button.react'; import { useSelector } from '../../redux/redux-utils'; import { nonThreadCalendarQuery } from '../../selectors/nav-selectors'; import css from '../../style.css'; import Modal from '../modal.react'; import ColorPicker from './color-picker.react'; type BaseProps = { +onClose: () => void, +parentThreadID?: ?string, }; type Props = { ...BaseProps, +inputDisabled: boolean, +calendarQuery: () => CalendarQuery, +parentThreadInfo: ?ThreadInfo, +dispatchActionPromise: DispatchActionPromise, +newThread: (request: ClientNewThreadRequest) => Promise, }; type State = { +threadType: ?ThreadType, +name: string, +description: string, +color: string, +errorMessage: string, }; const { COMMUNITY_OPEN_SUBTHREAD, COMMUNITY_SECRET_SUBTHREAD } = threadTypes; class NewThreadModal extends React.PureComponent { nameInput: ?HTMLInputElement; openPrivacyInput: ?HTMLInputElement; threadPasswordInput: ?HTMLInputElement; constructor(props: Props) { super(props); this.state = { threadType: props.parentThreadID ? undefined : COMMUNITY_SECRET_SUBTHREAD, name: '', description: '', color: props.parentThreadInfo ? props.parentThreadInfo.color : generateRandomColor(), errorMessage: '', }; } componentDidMount() { invariant(this.nameInput, 'nameInput ref unset'); this.nameInput.focus(); } render() { let threadTypeSection = null; if (this.props.parentThreadID) { threadTypeSection = (
Thread type
); } return (
Thread name
Description
Color
); } else if (this.state.currentTabType === 'privacy') { mainContent = (
Thread type
); } else if (this.state.currentTabType === 'delete') { mainContent = ( <>

Your thread will be permanently deleted. There is no way to reverse this.

Please enter your account password to confirm your identity

Account password
); } let buttons = null; if (this.state.currentTabType === 'delete') { buttons = ( - + > + Delete + ); } else { buttons = ( - + > + Save + ); } const tabs = [ , ]; // This UI needs to be updated to handle sidebars but we haven't gotten // there yet. We'll probably end up ripping it out anyways, so for now we // are just hiding the privacy tab for any thread that was created as a // sidebar const canSeePrivacyTab = this.possiblyChangedValue('parentThreadID') && threadInfo.sourceMessageID && (threadInfo.type === threadTypes.COMMUNITY_OPEN_SUBTHREAD || threadInfo.type === threadTypes.COMMUNITY_SECRET_SUBTHREAD); if (canSeePrivacyTab) { tabs.push( , ); } const canDeleteThread = this.hasPermissionForTab(threadInfo, 'delete'); if (canDeleteThread) { tabs.push( , ); } return (
    {tabs}
{mainContent}
{buttons}
{this.state.errorMessage}
); } setTab = (tabType: TabType) => { this.setState({ currentTabType: tabType }); }; nameInputRef = (nameInput: ?HTMLInputElement) => { this.nameInput = nameInput; }; newThreadPasswordInputRef = (newThreadPasswordInput: ?HTMLInputElement) => { this.newThreadPasswordInput = newThreadPasswordInput; }; accountPasswordInputRef = (accountPasswordInput: ?HTMLInputElement) => { this.accountPasswordInput = accountPasswordInput; }; onChangeName = (event: SyntheticEvent) => { const target = event.currentTarget; const newValue = target.value !== this.props.threadInfo.name ? target.value : undefined; this.setState((prevState: State) => ({ ...prevState, queuedChanges: { ...prevState.queuedChanges, name: firstLine(newValue), }, })); }; onChangeDescription = (event: SyntheticEvent) => { const target = event.currentTarget; const newValue = target.value !== this.props.threadInfo.description ? target.value : undefined; this.setState((prevState: State) => ({ ...prevState, queuedChanges: { ...prevState.queuedChanges, description: newValue, }, })); }; onChangeColor = (color: string) => { const newValue = color !== this.props.threadInfo.color ? color : undefined; this.setState((prevState: State) => ({ ...prevState, queuedChanges: { ...prevState.queuedChanges, color: newValue, }, })); }; onChangeThreadType = (event: SyntheticEvent) => { const uiValue = assertThreadType(parseInt(event.currentTarget.value, 10)); const newValue = uiValue !== this.props.threadInfo.type ? uiValue : undefined; this.setState((prevState: State) => ({ ...prevState, queuedChanges: { ...prevState.queuedChanges, type: newValue, }, })); }; onChangeAccountPassword = (event: SyntheticEvent) => { const target = event.currentTarget; this.setState({ accountPassword: target.value }); }; - onSubmit = (event: SyntheticEvent) => { + onSubmit = (event: SyntheticEvent) => { event.preventDefault(); this.props.dispatchActionPromise( changeThreadSettingsActionTypes, this.changeThreadSettingsAction(), ); }; async changeThreadSettingsAction() { try { const response = await this.props.changeThreadSettings({ threadID: this.props.threadInfo.id, changes: this.state.queuedChanges, }); this.props.onClose(); return response; } catch (e) { this.setState( prevState => ({ ...prevState, queuedChanges: Object.freeze({}), accountPassword: '', errorMessage: 'unknown error', currentTabType: 'general', }), () => { invariant(this.nameInput, 'nameInput ref unset'); this.nameInput.focus(); }, ); throw e; } } - onDelete = (event: SyntheticEvent) => { + onDelete = (event: SyntheticEvent) => { event.preventDefault(); this.props.dispatchActionPromise( deleteThreadActionTypes, this.deleteThreadAction(), ); }; async deleteThreadAction() { try { const response = await this.props.deleteThread( this.props.threadInfo.id, this.state.accountPassword, ); this.props.onClose(); return response; } catch (e) { const errorMessage = e.message === 'invalid_credentials' ? 'wrong password' : 'unknown error'; this.setState( { accountPassword: '', errorMessage: errorMessage, }, () => { invariant( this.accountPasswordInput, 'accountPasswordInput ref unset', ); this.accountPasswordInput.focus(); }, ); throw e; } } } const deleteThreadLoadingStatusSelector = createLoadingStatusSelector( deleteThreadActionTypes, ); const changeThreadSettingsLoadingStatusSelector = createLoadingStatusSelector( changeThreadSettingsActionTypes, ); const ConnectedThreadSettingsModal: React.ComponentType = React.memo( function ConnectedThreadSettingsModal(props) { const changeInProgress = useSelector( state => deleteThreadLoadingStatusSelector(state) === 'loading' || changeThreadSettingsLoadingStatusSelector(state) === 'loading', ); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const userInfos = useSelector(state => state.userStore.userInfos); const callDeleteThread = useServerCall(deleteThread); const callChangeThreadSettings = useServerCall(changeThreadSettings); const dispatchActionPromise = useDispatchActionPromise(); const threadInfo: ?ThreadInfo = useSelector( state => threadInfoSelector(state)[props.threadID], ); const modalContext = useModalContext(); if (!threadInfo) { return (

You no longer have permission to view this thread

); } return ( ); }, ); export default ConnectedThreadSettingsModal; diff --git a/web/splash/splash.react.js b/web/splash/splash.react.js index 548ba6c2d..98de90329 100644 --- a/web/splash/splash.react.js +++ b/web/splash/splash.react.js @@ -1,282 +1,283 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { requestAccessActionTypes, requestAccess, } from 'lib/actions/user-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { validEmailRegex } from 'lib/shared/account-utils'; import type { AccessRequest } from 'lib/types/account-types'; import { type DeviceType, assertDeviceType } from 'lib/types/device-types'; import { type LoadingStatus } from 'lib/types/loading-types'; import { type DispatchActionPromise, useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; +import Button from '../components/button.react'; import LoadingIndicator from '../loading-indicator.react'; import LogInModal from '../modals/account/log-in-modal.react'; import { useModalContext } from '../modals/modal-provider.react'; import { useSelector } from '../redux/redux-utils'; import css from './splash.css'; const defaultRequestAccessScrollHeight = 390; type Props = { +loadingStatus: LoadingStatus, +dispatchActionPromise: DispatchActionPromise, +requestAccess: (accessRequest: AccessRequest) => Promise, +setModal: (modal: React.Node) => void, +modal: ?React.Node, }; type State = { +platform: DeviceType, +email: string, +error: ?string, +success: ?string, }; class Splash extends React.PureComponent { emailInput: ?HTMLInputElement; bottomContainer: ?HTMLDivElement; state: State = { platform: 'ios', email: '', error: null, success: null, }; componentDidMount() { if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; } } render() { let androidWarning = null; if (this.state.platform === 'android') { androidWarning = (

Make sure this is the email you use to log in to the Google Play Store!

); } let error = null; if (this.state.error) { error =

{this.state.error}

; } let success = null; if (this.state.success) { success = (

{this.state.success}

); } let submitButtonContent = 'Submit'; if (this.props.loadingStatus === 'loading') { submitButtonContent = ( ); } return (

Comm is a chat app with an integrated calendar.

We make it incredibly easy to plan events with your friends.

We're currently alpha testing the first version of our app.

If you'd like to try it out, please let us know!

- +
{androidWarning}
{error} {success}
{this.props.modal} ); } bottomContainerRef = (bottomContainer: ?HTMLDivElement) => { this.bottomContainer = bottomContainer; }; emailInputRef = (emailInput: ?HTMLInputElement) => { this.emailInput = emailInput; }; onChangeEmail = (event: SyntheticEvent) => { this.setState({ email: event.currentTarget.value }); }; onChangePlatform = (event: SyntheticEvent) => { this.setState({ platform: assertDeviceType(event.currentTarget.value) }); }; onClickLogIn = (event: SyntheticEvent) => { event.preventDefault(); this.props.setModal(); }; onClickRequestAccess = (event: SyntheticEvent) => { event.preventDefault(); const { bottomContainer } = this; invariant(bottomContainer, 'bottomContainer should exist'); const formHeight = 180; const contentHeight = 790; const guaranteesSpace = contentHeight - window.innerHeight + formHeight; if (bottomContainer.scrollTop < guaranteesSpace) { bottomContainer.scrollTop = Math.max( defaultRequestAccessScrollHeight, guaranteesSpace, ); } if (this.emailInput) { this.emailInput.focus(); } }; - onSubmitRequestAccess = (event: SyntheticEvent) => { + onSubmitRequestAccess = (event: SyntheticEvent) => { event.preventDefault(); if (this.state.email.search(validEmailRegex) === -1) { this.setState({ success: null, error: 'Please enter a valid email!' }); invariant(this.emailInput, 'should be set'); this.emailInput.focus(); return; } this.props.dispatchActionPromise( requestAccessActionTypes, this.requestAccessAction(), ); }; async requestAccessAction() { try { await this.props.requestAccess({ email: this.state.email, platform: this.state.platform, }); this.setState({ success: "Thanks for your interest! We'll let you know as soon as " + "we're able to extend an invite.", error: null, }); } catch (e) { this.setState({ success: null, error: 'Unknown error...' }); throw e; } } } const loadingStatusSelector = createLoadingStatusSelector( requestAccessActionTypes, ); const ConnectedSplash: React.ComponentType<{}> = React.memo<{}>( function ConnectedSplash(): React.Node { const loadingStatus = useSelector(loadingStatusSelector); const callRequestAccess = useServerCall(requestAccess); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); return ( ); }, ); export default ConnectedSplash;