<commit_before>/* @flow strict-local */
import React, { PureComponent } from 'react';
import { Platform, View, TextInput, findNodeHandle } from 'react-native';
import { connect } from 'react-redux';
import TextInputReset from 'react-native-text-input-reset';

import type {
  Auth,
  Context,
  Narrow,
  EditMessage,
  InputSelectionType,
  User,
  Dispatch,
  Dimensions,
  GlobalState,
} from '../types';
import {
  addToOutbox,
  cancelEditMessage,
  draftUpdate,
  fetchTopicsForActiveStream,
  sendTypingEvent,
} from '../actions';
import { updateMessage } from '../api';
import { FloatingActionButton, Input } from '../common';
import { showErrorAlert } from '../utils/info';
import { IconDone, IconSend } from '../common/Icons';
import { isStreamNarrow, isStreamOrTopicNarrow, topicNarrow } from '../utils/narrow';
import ComposeMenu from './ComposeMenu';
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
import NotSubscribed from '../message/NotSubscribed';
import AnnouncementOnly from '../message/AnnouncementOnly';

import {
  getAuth,
  getIsAdmin,
  getSession,
  getLastMessageTopic,
  getActiveUsersByEmail,
} from '../selectors';
import {
  getIsActiveStreamSubscribed,
  getIsActiveStreamAnnouncementOnly,
} from '../subscriptions/subscriptionSelectors';
import { getDraftForNarrow } from '../drafts/draftsSelectors';
import TopicAutocomplete from '../autocomplete/TopicAutocomplete';
import AutocompleteView from '../autocomplete/AutocompleteView';

type Props = {|
  auth: Auth,
  narrow: Narrow,
  usersByEmail: Map<string, User>,
  draft: string,
  lastMessageTopic: string,
  isAdmin: boolean,
  isAnnouncementOnly: boolean,
  isSubscribed: boolean,
  editMessage: EditMessage,
  safeAreaInsets: Dimensions,
  dispatch: Dispatch,
|};

type State = {|
  isMessageFocused: boolean,
  isTopicFocused: boolean,

  /** Almost the same as isMessageFocused || isTopicFocused ... except
   * debounced, to stay true while those flip from false/true to true/false
   * and back. */
  isFocused: boolean,

  isMenuExpanded: boolean,
  topic: string,
  message: string,
  height: number,
  selection: InputSelectionType,
|};

export const updateTextInput = (textInput: ?TextInput, text: string): void => {
  if (!textInput) {
    // Depending on the lifecycle events this function is called from,
    // this might not be set yet.
    return;
  }

  textInput.setNativeProps({ text });

  if (text.length === 0 && TextInputReset) {
    // React Native has problems with some custom keyboards when clearing
    // the input's contents.  Force reset to make sure it works.
    TextInputReset.resetKeyboardInput(findNodeHandle(textInput));
  }
};

class ComposeBox extends PureComponent<Props, State> {
  context: Context;

  messageInput: ?TextInput = null;
  topicInput: ?TextInput = null;

  static contextTypes = {
    styles: () => null,
  };

  state = {
    isMessageFocused: false,
    isTopicFocused: false,
    isFocused: false,
    isMenuExpanded: false,
    height: 20,
    topic: this.props.lastMessageTopic,
    message: this.props.draft,
    selection: { start: 0, end: 0 },
  };

  updateIsFocused = () => {
    this.setState(state => ({
      ...state,
      isFocused: state.isMessageFocused || state.isTopicFocused,
    }));
  };

  getCanSelectTopic = () => {
    const { editMessage, narrow } = this.props;
    if (editMessage) {
      return isStreamOrTopicNarrow(narrow);
    }
    if (!isStreamNarrow(narrow)) {
      return false;
    }
    return this.state.isFocused;
  };

  setMessageInputValue = (message: string) => {
    updateTextInput(this.messageInput, message);
    this.handleMessageChange(message);
  };

  setTopicInputValue = (topic: string) => {
    updateTextInput(this.topicInput, topic);
    this.handleTopicChange(topic);
  };

  handleComposeMenuToggle = () => {
    this.setState(({ isMenuExpanded }) => ({
      isMenuExpanded: !isMenuExpanded,
    }));
  };

  handleLayoutChange = event => {
    this.setState({
      height: event.nativeEvent.layout.height,
    });
  };

  handleTopicChange = (topic: string) => {
    this.setState({ topic, isMenuExpanded: false });
  };

  handleTopicAutocomplete = (topic: string) => {
    this.setTopicInputValue(topic);
  };

  handleMessageChange = (message: string) => {
    this.setState({ message, isMenuExpanded: false });
    const { dispatch, narrow } = this.props;
    dispatch(sendTypingEvent(narrow));
    dispatch(draftUpdate(narrow, message));
  };

  handleMessageAutocomplete = (message: string) => {
    this.setMessageInputValue(message);
  };

  handleMessageSelectionChange = (event: { nativeEvent: { selection: InputSelectionType } }) => {
    const { selection } = event.nativeEvent;
    this.setState({ selection });
  };

  handleMessageFocus = () => {
    this.setState((state, { lastMessageTopic }) => ({
      ...state,
      topic: state.topic || lastMessageTopic,
      isMessageFocused: true,
      isFocused: true,
      isMenuExpanded: false,
    }));
  };

  handleMessageBlur = () => {
    this.setState({
      isMessageFocused: false,
      isMenuExpanded: false,
    });
    setTimeout(this.updateIsFocused, 200); // give a chance to the topic input to get the focus
  };

  handleTopicFocus = () => {
    const { dispatch, narrow } = this.props;
    this.setState({
      isTopicFocused: true,
      isFocused: true,
      isMenuExpanded: false,
    });
    dispatch(fetchTopicsForActiveStream(narrow));
  };

  handleTopicBlur = () => {
    this.setState({
      isTopicFocused: false,
      isMenuExpanded: false,
    });
    setTimeout(this.updateIsFocused, 200); // give a chance to the message input to get the focus
  };

  handleInputTouchStart = () => {
    this.setState({ isMenuExpanded: false });
  };

  getDestinationNarrow = (): Narrow => {
    const { narrow } = this.props;
    const { topic } = this.state;
    return isStreamNarrow(narrow) ? topicNarrow(narrow[0].operand, topic || '(no topic)') : narrow;
  };

  handleSend = () => {
    const { dispatch } = this.props;
    const { message } = this.state;

    dispatch(addToOutbox(this.getDestinationNarrow(), message));

    this.setMessageInputValue('');
  };

  handleEdit = () => {
    const { auth, editMessage, dispatch } = this.props;
    const { message, topic } = this.state;
    const content = editMessage.content !== message ? message : undefined;
    const subject = topic !== editMessage.topic ? topic : undefined;
    if ((content !== undefined && content !== '') || (subject !== undefined && subject !== '')) {
      updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
        showErrorAlert(error.message, 'Failed to edit message');
      });
    }
    dispatch(cancelEditMessage());
  };

  componentWillReceiveProps(nextProps: Props) {
    if (nextProps.editMessage !== this.props.editMessage) {
      const topic =
        isStreamNarrow(nextProps.narrow) && nextProps.editMessage
          ? nextProps.editMessage.topic
          : '';
      const message = nextProps.editMessage ? nextProps.editMessage.content : '';
      this.setMessageInputValue(message);
      this.setTopicInputValue(topic);
      if (this.messageInput) {
        this.messageInput.focus();
      }
    }
  }

  inputMarginPadding = {
    paddingHorizontal: 8,
    paddingVertical: Platform.select({
      ios: 8,
      android: 2,
    }),
  };

  styles = {
    autocompleteWrapper: {
      position: 'absolute',
      bottom: 0,
      width: '100%',
    },
    composeBox: {
      flexDirection: 'row',
      alignItems: 'flex-end',
    },
    composeText: {
      flex: 1,
      paddingVertical: 8,
    },
    composeSendButton: {
      padding: 8,
    },
    topicInput: [
      {
        borderWidth: 0,
        borderRadius: 5,
        marginBottom: 8,
        ...this.inputMarginPadding,
      },
      this.context.styles.backgroundColor,
    ],
    composeTextInput: [
      {
        borderWidth: 0,
        borderRadius: 5,
        fontSize: 15,
        ...this.inputMarginPadding,
      },
      this.context.styles.backgroundColor,
    ],
  };

  render() {
    const { isTopicFocused, isMenuExpanded, height, message, topic, selection } = this.state;
    const {
      auth,
      narrow,
      usersByEmail,
      editMessage,
      safeAreaInsets,
      isAdmin,
      isAnnouncementOnly,
      isSubscribed,
    } = this.props;

    if (!isSubscribed) {
      return <NotSubscribed narrow={narrow} />;
    } else if (isAnnouncementOnly && !isAdmin) {
      return <AnnouncementOnly />;
    }

    const placeholder = getComposeInputPlaceholder(narrow, auth.email, usersByEmail);
    const style = {
      paddingBottom: safeAreaInsets.bottom,
      backgroundColor: 'rgba(127, 127, 127, 0.1)',
    };

    return (
      <View style={style}>
        <View
          style={[
            this.styles.autocompleteWrapper,
            { marginBottom: safeAreaInsets.bottom + height },
          ]}
        >
          <TopicAutocomplete
            isFocused={isTopicFocused}
            narrow={narrow}
            text={topic}
            onAutocomplete={this.handleTopicAutocomplete}
          />
          <AutocompleteView
            isFocused={this.state.isMessageFocused}
            selection={selection}
            text={message}
            onAutocomplete={this.handleMessageAutocomplete}
          />
        </View>
        <View style={this.styles.composeBox} onLayout={this.handleLayoutChange}>
          <ComposeMenu
            destinationNarrow={this.getDestinationNarrow()}
            expanded={isMenuExpanded}
            onExpandContract={this.handleComposeMenuToggle}
          />
          <View style={this.styles.composeText}>
            {this.getCanSelectTopic() && (
              <Input
                style={this.styles.topicInput}
                underlineColorAndroid="transparent"
                placeholder="Topic"
                defaultValue={topic}
                selectTextOnFocus
                textInputRef={component => {
                  this.topicInput = component;
                }}
                onChangeText={this.handleTopicChange}
                onFocus={this.handleTopicFocus}
                onBlur={this.handleTopicBlur}
                onTouchStart={this.handleInputTouchStart}
              />
            )}
            <Input
              multiline
              style={this.styles.composeTextInput}
              underlineColorAndroid="transparent"
              placeholder={placeholder}
              defaultValue={message}
              textInputRef={component => {
                this.messageInput = component;
              }}
              onBlur={this.handleMessageBlur}
              onChangeText={this.handleMessageChange}
              onFocus={this.handleMessageFocus}
              onSelectionChange={this.handleMessageSelectionChange}
              onTouchStart={this.handleInputTouchStart}
            />
          </View>
          <FloatingActionButton
            style={this.styles.composeSendButton}
            Icon={editMessage === null ? IconSend : IconDone}
            size={32}
            disabled={message.trim().length === 0}
            onPress={editMessage === null ? this.handleSend : this.handleEdit}
          />
        </View>
      </View>
    );
  }
}

export default connect((state: GlobalState, props) => ({
  auth: getAuth(state),
  usersByEmail: getActiveUsersByEmail(state),
  safeAreaInsets: getSession(state).safeAreaInsets,
  isAdmin: getIsAdmin(state),
  isAnnouncementOnly: getIsActiveStreamAnnouncementOnly(state, props.narrow),
  isSubscribed: getIsActiveStreamSubscribed(state, props.narrow),
  editMessage: getSession(state).editMessage,
  draft: getDraftForNarrow(props.narrow)(state),
  lastMessageTopic: getLastMessageTopic(props.narrow)(state),
}))(ComposeBox);
<commit_msg>autocomplete: Fix autocomplete popup not responding to touch.<commit_after>/* @flow strict-local */
import React, { PureComponent } from 'react';
import { Platform, View, TextInput, findNodeHandle } from 'react-native';
import { connect } from 'react-redux';
import TextInputReset from 'react-native-text-input-reset';

import type {
  Auth,
  Context,
  Narrow,
  EditMessage,
  InputSelectionType,
  User,
  Dispatch,
  Dimensions,
  GlobalState,
} from '../types';
import {
  addToOutbox,
  cancelEditMessage,
  draftUpdate,
  fetchTopicsForActiveStream,
  sendTypingEvent,
} from '../actions';
import { updateMessage } from '../api';
import { FloatingActionButton, Input } from '../common';
import { showErrorAlert } from '../utils/info';
import { IconDone, IconSend } from '../common/Icons';
import { isStreamNarrow, isStreamOrTopicNarrow, topicNarrow } from '../utils/narrow';
import ComposeMenu from './ComposeMenu';
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
import NotSubscribed from '../message/NotSubscribed';
import AnnouncementOnly from '../message/AnnouncementOnly';

import {
  getAuth,
  getIsAdmin,
  getSession,
  getLastMessageTopic,
  getActiveUsersByEmail,
} from '../selectors';
import {
  getIsActiveStreamSubscribed,
  getIsActiveStreamAnnouncementOnly,
} from '../subscriptions/subscriptionSelectors';
import { getDraftForNarrow } from '../drafts/draftsSelectors';
import TopicAutocomplete from '../autocomplete/TopicAutocomplete';
import AutocompleteView from '../autocomplete/AutocompleteView';

type Props = {|
  auth: Auth,
  narrow: Narrow,
  usersByEmail: Map<string, User>,
  draft: string,
  lastMessageTopic: string,
  isAdmin: boolean,
  isAnnouncementOnly: boolean,
  isSubscribed: boolean,
  editMessage: EditMessage,
  safeAreaInsets: Dimensions,
  dispatch: Dispatch,
|};

type State = {|
  isMessageFocused: boolean,
  isTopicFocused: boolean,

  /** Almost the same as isMessageFocused || isTopicFocused ... except
   * debounced, to stay true while those flip from false/true to true/false
   * and back. */
  isFocused: boolean,

  isMenuExpanded: boolean,
  topic: string,
  message: string,
  height: number,
  selection: InputSelectionType,
|};

export const updateTextInput = (textInput: ?TextInput, text: string): void => {
  if (!textInput) {
    // Depending on the lifecycle events this function is called from,
    // this might not be set yet.
    return;
  }

  textInput.setNativeProps({ text });

  if (text.length === 0 && TextInputReset) {
    // React Native has problems with some custom keyboards when clearing
    // the input's contents.  Force reset to make sure it works.
    TextInputReset.resetKeyboardInput(findNodeHandle(textInput));
  }
};

class ComposeBox extends PureComponent<Props, State> {
  context: Context;

  messageInput: ?TextInput = null;
  topicInput: ?TextInput = null;

  static contextTypes = {
    styles: () => null,
  };

  state = {
    isMessageFocused: false,
    isTopicFocused: false,
    isFocused: false,
    isMenuExpanded: false,
    height: 20,
    topic: this.props.lastMessageTopic,
    message: this.props.draft,
    selection: { start: 0, end: 0 },
  };

  updateIsFocused = () => {
    this.setState(state => ({
      ...state,
      isFocused: state.isMessageFocused || state.isTopicFocused,
    }));
  };

  getCanSelectTopic = () => {
    const { editMessage, narrow } = this.props;
    if (editMessage) {
      return isStreamOrTopicNarrow(narrow);
    }
    if (!isStreamNarrow(narrow)) {
      return false;
    }
    return this.state.isFocused;
  };

  setMessageInputValue = (message: string) => {
    updateTextInput(this.messageInput, message);
    this.handleMessageChange(message);
  };

  setTopicInputValue = (topic: string) => {
    updateTextInput(this.topicInput, topic);
    this.handleTopicChange(topic);
  };

  handleComposeMenuToggle = () => {
    this.setState(({ isMenuExpanded }) => ({
      isMenuExpanded: !isMenuExpanded,
    }));
  };

  handleLayoutChange = event => {
    this.setState({
      height: event.nativeEvent.layout.height,
    });
  };

  handleTopicChange = (topic: string) => {
    this.setState({ topic, isMenuExpanded: false });
  };

  handleTopicAutocomplete = (topic: string) => {
    this.setTopicInputValue(topic);
  };

  handleMessageChange = (message: string) => {
    this.setState({ message, isMenuExpanded: false });
    const { dispatch, narrow } = this.props;
    dispatch(sendTypingEvent(narrow));
    dispatch(draftUpdate(narrow, message));
  };

  handleMessageAutocomplete = (message: string) => {
    this.setMessageInputValue(message);
  };

  handleMessageSelectionChange = (event: { nativeEvent: { selection: InputSelectionType } }) => {
    const { selection } = event.nativeEvent;
    this.setState({ selection });
  };

  handleMessageFocus = () => {
    this.setState((state, { lastMessageTopic }) => ({
      ...state,
      topic: state.topic || lastMessageTopic,
      isMessageFocused: true,
      isFocused: true,
      isMenuExpanded: false,
    }));
  };

  handleMessageBlur = () => {
    this.setState({
      isMessageFocused: false,
      isMenuExpanded: false,
    });
    setTimeout(this.updateIsFocused, 200); // give a chance to the topic input to get the focus
  };

  handleTopicFocus = () => {
    const { dispatch, narrow } = this.props;
    this.setState({
      isTopicFocused: true,
      isFocused: true,
      isMenuExpanded: false,
    });
    dispatch(fetchTopicsForActiveStream(narrow));
  };

  handleTopicBlur = () => {
    this.setState({
      isTopicFocused: false,
      isMenuExpanded: false,
    });
    setTimeout(this.updateIsFocused, 200); // give a chance to the message input to get the focus
  };

  handleInputTouchStart = () => {
    this.setState({ isMenuExpanded: false });
  };

  getDestinationNarrow = (): Narrow => {
    const { narrow } = this.props;
    const { topic } = this.state;
    return isStreamNarrow(narrow) ? topicNarrow(narrow[0].operand, topic || '(no topic)') : narrow;
  };

  handleSend = () => {
    const { dispatch } = this.props;
    const { message } = this.state;

    dispatch(addToOutbox(this.getDestinationNarrow(), message));

    this.setMessageInputValue('');
  };

  handleEdit = () => {
    const { auth, editMessage, dispatch } = this.props;
    const { message, topic } = this.state;
    const content = editMessage.content !== message ? message : undefined;
    const subject = topic !== editMessage.topic ? topic : undefined;
    if ((content !== undefined && content !== '') || (subject !== undefined && subject !== '')) {
      updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
        showErrorAlert(error.message, 'Failed to edit message');
      });
    }
    dispatch(cancelEditMessage());
  };

  componentWillReceiveProps(nextProps: Props) {
    if (nextProps.editMessage !== this.props.editMessage) {
      const topic =
        isStreamNarrow(nextProps.narrow) && nextProps.editMessage
          ? nextProps.editMessage.topic
          : '';
      const message = nextProps.editMessage ? nextProps.editMessage.content : '';
      this.setMessageInputValue(message);
      this.setTopicInputValue(topic);
      if (this.messageInput) {
        this.messageInput.focus();
      }
    }
  }

  inputMarginPadding = {
    paddingHorizontal: 8,
    paddingVertical: Platform.select({
      ios: 8,
      android: 2,
    }),
  };

  styles = {
    autocompleteWrapper: {
      position: 'absolute',
      bottom: 0,
      width: '100%',
    },
    composeBox: {
      flexDirection: 'row',
      alignItems: 'flex-end',
    },
    composeText: {
      flex: 1,
      paddingVertical: 8,
    },
    composeSendButton: {
      padding: 8,
    },
    topicInput: [
      {
        borderWidth: 0,
        borderRadius: 5,
        marginBottom: 8,
        ...this.inputMarginPadding,
      },
      this.context.styles.backgroundColor,
    ],
    composeTextInput: [
      {
        borderWidth: 0,
        borderRadius: 5,
        fontSize: 15,
        ...this.inputMarginPadding,
      },
      this.context.styles.backgroundColor,
    ],
  };

  render() {
    const { isTopicFocused, isMenuExpanded, height, message, topic, selection } = this.state;
    const {
      auth,
      narrow,
      usersByEmail,
      editMessage,
      safeAreaInsets,
      isAdmin,
      isAnnouncementOnly,
      isSubscribed,
    } = this.props;

    if (!isSubscribed) {
      return <NotSubscribed narrow={narrow} />;
    } else if (isAnnouncementOnly && !isAdmin) {
      return <AnnouncementOnly />;
    }

    const placeholder = getComposeInputPlaceholder(narrow, auth.email, usersByEmail);
    const style = {
      paddingBottom: safeAreaInsets.bottom,
      backgroundColor: 'rgba(127, 127, 127, 0.1)',
    };

    return (
      <View>
        <View style={[this.styles.autocompleteWrapper, { marginBottom: height }]}>
          <TopicAutocomplete
            isFocused={isTopicFocused}
            narrow={narrow}
            text={topic}
            onAutocomplete={this.handleTopicAutocomplete}
          />
          <AutocompleteView
            isFocused={this.state.isMessageFocused}
            selection={selection}
            text={message}
            onAutocomplete={this.handleMessageAutocomplete}
          />
        </View>
        <View style={[this.styles.composeBox, style]} onLayout={this.handleLayoutChange}>
          <ComposeMenu
            destinationNarrow={this.getDestinationNarrow()}
            expanded={isMenuExpanded}
            onExpandContract={this.handleComposeMenuToggle}
          />
          <View style={this.styles.composeText}>
            {this.getCanSelectTopic() && (
              <Input
                style={this.styles.topicInput}
                underlineColorAndroid="transparent"
                placeholder="Topic"
                defaultValue={topic}
                selectTextOnFocus
                textInputRef={component => {
                  this.topicInput = component;
                }}
                onChangeText={this.handleTopicChange}
                onFocus={this.handleTopicFocus}
                onBlur={this.handleTopicBlur}
                onTouchStart={this.handleInputTouchStart}
              />
            )}
            <Input
              multiline
              style={this.styles.composeTextInput}
              underlineColorAndroid="transparent"
              placeholder={placeholder}
              defaultValue={message}
              textInputRef={component => {
                this.messageInput = component;
              }}
              onBlur={this.handleMessageBlur}
              onChangeText={this.handleMessageChange}
              onFocus={this.handleMessageFocus}
              onSelectionChange={this.handleMessageSelectionChange}
              onTouchStart={this.handleInputTouchStart}
            />
          </View>
          <FloatingActionButton
            style={this.styles.composeSendButton}
            Icon={editMessage === null ? IconSend : IconDone}
            size={32}
            disabled={message.trim().length === 0}
            onPress={editMessage === null ? this.handleSend : this.handleEdit}
          />
        </View>
      </View>
    );
  }
}

export default connect((state: GlobalState, props) => ({
  auth: getAuth(state),
  usersByEmail: getActiveUsersByEmail(state),
  safeAreaInsets: getSession(state).safeAreaInsets,
  isAdmin: getIsAdmin(state),
  isAnnouncementOnly: getIsActiveStreamAnnouncementOnly(state, props.narrow),
  isSubscribed: getIsActiveStreamSubscribed(state, props.narrow),
  editMessage: getSession(state).editMessage,
  draft: getDraftForNarrow(props.narrow)(state),
  lastMessageTopic: getLastMessageTopic(props.narrow)(state),
}))(ComposeBox);
<|endoftext|>


<commit_before>/**
 * Controller for editing publishing time of an assignment.
 */
Ext.define('devilry_subjectadmin.controller.assignment.EditPublishingTime', {
    extend: 'Ext.app.Controller',
    mixins: {
        'handleProxyError': 'devilry_subjectadmin.utils.DjangoRestframeworkProxyErrorMixin'
    },

    views: [
        'assignment.EditPublishingTimePanel',
        'assignment.EditPublishingTimeWidget'
    ],

    requires: [
        'Ext.util.KeyNav',
        'devilry_extjsextras.DatetimeHelpers'
    ],

    models: ['Assignment'],
    controllers: [
        'assignment.Overview'
    ],

    refs: [{
        ref: 'cardContainer',
        selector: 'editpublishingtime-widget'
    }, {
        ref: 'readOnlyView',
        selector: 'editpublishingtime-widget containerwithedittitle#readPublishingTime'
    }, {
        ref: 'readOnlyViewBody',
        selector: 'editpublishingtime-widget markupmoreinfobox'
    }, {

        ref: 'editPublishingTime',
        selector: 'editpublishingtime-widget editpublishingtimepanel'
    }, {
        ref: 'publishingTimeField',
        selector: 'editpublishingtime-widget editpublishingtimepanel devilry_extjsextras-datetimefield'
    }, {
        ref: 'formPanel',
        selector: 'editpublishingtime-widget editpublishingtimepanel form'
    }, {
        ref: 'alertMessageList',
        selector: 'editpublishingtime-widget editpublishingtimepanel alertmessagelist'
    }],

    init: function() {
        // This is called when the application is initialized, not when the window is opened
        this.application.addListener({
            scope: this,
            assignmentSuccessfullyLoaded: this._onLoadAssignment
        });
        this.control({
            'editpublishingtime-widget #readPublishingTime': {
                edit: this._onEdit
            },
            'editpublishingtime-widget editpublishingtimepanel form devilry_extjsextras-datetimefield': {
                allRendered: this._onRenderForm
            },
            'editpublishingtime-widget editpublishingtimepanel #okbutton': {
                click: this._onSave
            },
            'editpublishingtime-widget editpublishingtimepanel #cancelbutton': {
                click: this._onCancel
            },
        });
    },

    _onLoadAssignment: function(assignmentRecord) {
        this.assignmentRecord = assignmentRecord;
        this.getReadOnlyView().enable();
        this._updatePublishingTimeWidget();
    },

    //////////////////////////////////
    //
    // View pubtime
    //
    //////////////////////////////////

    _showReadView: function() {
        this.getCardContainer().getLayout().setActiveItem('readPublishingTime');
    },

    _updatePublishingTimeWidget: function() {
        var offset_from_now = this.assignmentRecord.formatPublishOffsetFromNow();
        var is_published = this.assignmentRecord.get('is_published');
        this.getReadOnlyView().updateTitle(gettext('Publishing time'));
        this.getReadOnlyViewBody().update({
            publishing_time: this.assignmentRecord.formatPublishingTime(),
            offset_from_now: offset_from_now,
            is_published: is_published
        });
    },

    _onEdit: function() {
        this._showEditView();
    },

    //////////////////////////////////
    //
    // Edit pubtime
    //
    //////////////////////////////////

    _showEditView: function() {
        this.getCardContainer().getLayout().setActiveItem('editPublishingTime');
        this.getPublishingTimeField().setValue(this.assignmentRecord.get('publishing_time'));
        Ext.defer(function() {
            this.getFormPanel().down('devilry_extjsextras-datetimefield').focus();
        }, 200, this);
    },

    _onRenderForm: function() {
        this.getFormPanel().keyNav = Ext.create('Ext.util.KeyNav', this.getFormPanel().el, {
            enter: this._onSave,
            scope: this
        });
    },

    _onSave: function() {
        var form = this.getFormPanel().getForm();
        var assignmentRecord = this.assignmentRecord;
        form.updateRecord(assignmentRecord);
        this._getMaskElement().mask(gettext('Saving ...'));

        this.getAssignmentModel().proxy.addListener({
            scope: this,
            exception: this._onProxyError
        });
        assignmentRecord.save({
            scope: this,
            callback: function(r, operation) {
                this.getAssignmentModel().proxy.removeListener({
                    scope: this,
                    exception: this._onProxyError
                });
                if(operation.success) {
                    this._onSaveSuccess();
                }
            }
        });
    },

    _onCancel: function() {
        this._showReadView();
    },

    _getMaskElement: function() {
        return this.getFormPanel().getEl();
    },

    _onSaveSuccess: function() {
        this._getMaskElement().unmask();
        this._showReadView();
        this._updatePublishingTimeWidget();
    },

    _onProxyError: function(proxy, response, operation) {
        this._getMaskElement().unmask();
        this.getAlertMessageList().removeAll();
        this.handleProxyError(this.getAlertMessageList(), this.getFormPanel(),
            response, operation);
    }
});
<commit_msg>devilry_subjectadmin...EditPublishingTime: Trailing comma.<commit_after>/**
 * Controller for editing publishing time of an assignment.
 */
Ext.define('devilry_subjectadmin.controller.assignment.EditPublishingTime', {
    extend: 'Ext.app.Controller',
    mixins: {
        'handleProxyError': 'devilry_subjectadmin.utils.DjangoRestframeworkProxyErrorMixin'
    },

    views: [
        'assignment.EditPublishingTimePanel',
        'assignment.EditPublishingTimeWidget'
    ],

    requires: [
        'Ext.util.KeyNav',
        'devilry_extjsextras.DatetimeHelpers'
    ],

    models: ['Assignment'],
    controllers: [
        'assignment.Overview'
    ],

    refs: [{
        ref: 'cardContainer',
        selector: 'editpublishingtime-widget'
    }, {
        ref: 'readOnlyView',
        selector: 'editpublishingtime-widget containerwithedittitle#readPublishingTime'
    }, {
        ref: 'readOnlyViewBody',
        selector: 'editpublishingtime-widget markupmoreinfobox'
    }, {

        ref: 'editPublishingTime',
        selector: 'editpublishingtime-widget editpublishingtimepanel'
    }, {
        ref: 'publishingTimeField',
        selector: 'editpublishingtime-widget editpublishingtimepanel devilry_extjsextras-datetimefield'
    }, {
        ref: 'formPanel',
        selector: 'editpublishingtime-widget editpublishingtimepanel form'
    }, {
        ref: 'alertMessageList',
        selector: 'editpublishingtime-widget editpublishingtimepanel alertmessagelist'
    }],

    init: function() {
        // This is called when the application is initialized, not when the window is opened
        this.application.addListener({
            scope: this,
            assignmentSuccessfullyLoaded: this._onLoadAssignment
        });
        this.control({
            'editpublishingtime-widget #readPublishingTime': {
                edit: this._onEdit
            },
            'editpublishingtime-widget editpublishingtimepanel form devilry_extjsextras-datetimefield': {
                allRendered: this._onRenderForm
            },
            'editpublishingtime-widget editpublishingtimepanel #okbutton': {
                click: this._onSave
            },
            'editpublishingtime-widget editpublishingtimepanel #cancelbutton': {
                click: this._onCancel
            }
        });
    },

    _onLoadAssignment: function(assignmentRecord) {
        this.assignmentRecord = assignmentRecord;
        this.getReadOnlyView().enable();
        this._updatePublishingTimeWidget();
    },

    //////////////////////////////////
    //
    // View pubtime
    //
    //////////////////////////////////

    _showReadView: function() {
        this.getCardContainer().getLayout().setActiveItem('readPublishingTime');
    },

    _updatePublishingTimeWidget: function() {
        var offset_from_now = this.assignmentRecord.formatPublishOffsetFromNow();
        var is_published = this.assignmentRecord.get('is_published');
        this.getReadOnlyView().updateTitle(gettext('Publishing time'));
        this.getReadOnlyViewBody().update({
            publishing_time: this.assignmentRecord.formatPublishingTime(),
            offset_from_now: offset_from_now,
            is_published: is_published
        });
    },

    _onEdit: function() {
        this._showEditView();
    },

    //////////////////////////////////
    //
    // Edit pubtime
    //
    //////////////////////////////////

    _showEditView: function() {
        this.getCardContainer().getLayout().setActiveItem('editPublishingTime');
        this.getPublishingTimeField().setValue(this.assignmentRecord.get('publishing_time'));
        Ext.defer(function() {
            this.getFormPanel().down('devilry_extjsextras-datetimefield').focus();
        }, 200, this);
    },

    _onRenderForm: function() {
        this.getFormPanel().keyNav = Ext.create('Ext.util.KeyNav', this.getFormPanel().el, {
            enter: this._onSave,
            scope: this
        });
    },

    _onSave: function() {
        var form = this.getFormPanel().getForm();
        var assignmentRecord = this.assignmentRecord;
        form.updateRecord(assignmentRecord);
        this._getMaskElement().mask(gettext('Saving ...'));

        this.getAssignmentModel().proxy.addListener({
            scope: this,
            exception: this._onProxyError
        });
        assignmentRecord.save({
            scope: this,
            callback: function(r, operation) {
                this.getAssignmentModel().proxy.removeListener({
                    scope: this,
                    exception: this._onProxyError
                });
                if(operation.success) {
                    this._onSaveSuccess();
                }
            }
        });
    },

    _onCancel: function() {
        this._showReadView();
    },

    _getMaskElement: function() {
        return this.getFormPanel().getEl();
    },

    _onSaveSuccess: function() {
        this._getMaskElement().unmask();
        this._showReadView();
        this._updatePublishingTimeWidget();
    },

    _onProxyError: function(proxy, response, operation) {
        this._getMaskElement().unmask();
        this.getAlertMessageList().removeAll();
        this.handleProxyError(this.getAlertMessageList(), this.getFormPanel(),
            response, operation);
    }
});
<|endoftext|>


<commit_before>var common = require('./common');
var _ = common._;
var sinon = common.sinon;
var should = common.should;
var utils = require('../lib/utils');

var DataStore = require('../lib/ds/DataStore');
var RequestExecutor = require('../lib/ds/RequestExecutor');
var MemoryStore = require('../lib/cache/MemoryStore');
var InstanceResource = require('../lib/resource/InstanceResource');

function random() {
  return Math.floor(Math.random() * Date.now());
}

describe('data store module', function () {
  describe('data store class', function () {

    describe('constructor', function () {
      describe('if request executor not provided in config', function () {
        var ds = new DataStore({apiKey: {id: 1, secret: 2}});
        it('should create instance of default RequestExecutor', function () {
          ds.requestExecutor.should.be.an.instanceof(RequestExecutor);
        });
      });

      describe('if request executor was provided in config', function () {
        var reqExec = new RequestExecutor({apiKey: {id: 1, secret: 2}});
        var ds = new DataStore({requestExecutor: reqExec});
        it('should reuse provided request executor instance', function () {
          ds.requestExecutor.should.be.equal(reqExec);
        });
      });

      describe('if cache options was not provided', function () {
        var ds = new DataStore({apiKey: {id: 1, secret: 2}});
        it('should not create caches', function () {
          should.not.exist(ds.caches);
        });
      });

      describe('if config not provided', function () {
        function createDataStoreWithoutConfig() {
          return new DataStore();
        }

        it('should throw "config argument is required"', function () {
          createDataStoreWithoutConfig.should
            .throw(/config argument is required/i);
        });
      });

      describe('for all provided regions', function () {
        var CACHE_REGIONS = ['applications', 'directories', 'accounts', 'groups',
          'groupMemberships', 'tenants', 'accountStoreMappings'];
        var cacheOptions = {};
        _.each(CACHE_REGIONS, function (region) {
          cacheOptions[region] = {};
        });
        var ds = new DataStore({cacheOptions: cacheOptions,
          apiKey: {id: 1, secret: 2}});
        _.each(CACHE_REGIONS, function (region) {
          it('should create cache instance for ' + region, function () {
            should.exist(ds.cacheManager.getCache(region));
          });
        });

      });
    });

    describe('storing recursive response object', function () {
      var data = {
        'href': 'http://example.com/accounts/FOO',
        'name': 'Foo',
        'groups': {
          'href': 'http://example.com/accounts/FOO/groups',
          'items': [
            {
              'href': 'http://example.com/groups/G1',
              'name': 'Foo Group 1'
            },
            {
              'href': 'http://example.com/groups/G2',
              'name': 'Foo Group 2'
            }
          ]
        },
        'directory': {
          'href': 'http://example.com/directories/BAR',
          'name': 'Directory'
        }
      };
      var ds = new DataStore({
        cacheOptions: {
          store: MemoryStore,
          accounts: {},
          groups: {},
          directories: {}
        },
        apiKey: {id: 1, secret: 2}
      });

      var cbSpy = sinon.spy();
      var sandbox, reqExecSpy,
        accountsCachePutSpy,
        groupsCachePutSpy,
        directoryCachePutSpy;

      before(function () {
        sandbox = sinon.sandbox.create();
        accountsCachePutSpy = sandbox.spy(ds.cacheManager.getCache('accounts'), 'put');
        groupsCachePutSpy = sandbox.spy(ds.cacheManager.getCache('groups'), 'put');
        directoryCachePutSpy = sandbox.spy(ds.cacheManager.getCache('directories'), 'put');
        reqExecSpy = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
          cb(null, data);
        });

        // act
        ds.getResource(data.href, cbSpy);
      });
      after(function () {
        sandbox.restore();
      });

      it('should store root entity', function () {
        /* jshint -W030 */
        accountsCachePutSpy.should.have.been.calledOnce;
        accountsCachePutSpy.should.have.been.calledWith(data.href, {
          'href': 'http://example.com/accounts/FOO',
          'name': 'Foo',
          'groups': {
            'href': 'http://example.com/accounts/FOO/groups',
            'items': [
              {'href': 'http://example.com/groups/G1'},
              {'href': 'http://example.com/groups/G2'}
            ]
          },
          'directory': {
            'href': 'http://example.com/directories/BAR'
          }}, true);
      });
      it('should store all nested entities', function () {
        _.each(data.groups.items, function (group) {
          groupsCachePutSpy.should.have.been
            .calledWith(group.href, group, true, utils.noop);
        });

        directoryCachePutSpy.should.have.been
          .calledWith(data.directory.href, data.directory, true, utils.noop);
      });
    });

    describe('get resource', function () {
      var region = 'tenants';
      var ds = new DataStore({
        cacheOptions: {
          store: MemoryStore,
          tenants: {
            store: MemoryStore,
            ttl: 60,
            tti: 60
          },
          directories: {
            ttl: 120,
            tti: 120
          }
        },
        apiKey: {id: 1, secret: 2}
      });

      describe('required params', function () {
        it('should throw error if href is not defined', function () {
          ds.getResource.should.throw();
        });

        it('should throw error if callback is not defined', function () {
          ds.getResource.should.throw();
        });
      });

      describe('if href already cached', function () {
        var href = '/tenants/1' + random();
        var data = {data: random()};
        var cbSpy = sinon.spy();
        var sandbox, cacheGetSpy, reqExecSpy;
        before(function (done) {
          sandbox = sinon.sandbox.create();
          reqExecSpy = sandbox.spy(ds.requestExecutor, 'execute');
          var cache = ds.cacheManager.getCache(region);
          cacheGetSpy = sandbox.spy(cache, 'get');
          cache.put(href, data, true, done);
        });
        after(function () {
          sandbox.restore();
        });

        it('should return entry from cache', function () {
          ds.getResource(href, cbSpy);

          /* jshint -W030 */
          cacheGetSpy.should.have.been.calledOnce;
          cacheGetSpy.should.have.been.calledWith(href);

          reqExecSpy.should.not.been.called;

          cbSpy.should.have.been.calledOnce;
          cbSpy.should.have.been.calledWith(null);
        });
      });

      // todo: tbd - ignore cache if query param provided
      /*      describe('if query provided', function(){
       it('should ignore cache');
       });*/

      describe('if href not found in cache', function () {
        var href = '/tenants/2' + random();
        var data = {
          href: href,
          data: random()};
        var cbSpy = sinon.spy();
        var sandbox, cacheGetSpy, cachePutSpy, reqExecStub;
        before(function () {
          sandbox = sinon.sandbox.create();
          var cache = ds.cacheManager.getCache(region);
          cacheGetSpy = sandbox.spy(cache, 'get');
          cachePutSpy = sandbox.spy(cache, 'put');
          reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
            cb(null, data);
          });

          //act
          ds.getResource(href, {}, InstanceResource, cbSpy);
        });
        after(function () {
          sandbox.restore();
        });
        it('request executor should be called once', function () {
          /* jshint -W030 */
          cacheGetSpy.should.have.been.calledOnce;
          cacheGetSpy.should.have.been.calledWith(href);

          reqExecStub.should.have.been.called;

          cbSpy.should.have.been.calledOnce;
        });
        it('and result should be stored in cache', function () {
          /* jshint -W030 */
          cachePutSpy.should.have.been.calledOnce;
          cachePutSpy.should.have.been.calledWith(href, data);
        });
      });

      describe('if href region not cached', function () {
        var href = '/directory/2' + random();
        var data = {'data': random()};
        var cbSpy = sinon.spy();
        var sandbox, reqExecStub;
        before(function () {
          sandbox = sinon.sandbox.create();
          reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
            cb(null, data);
          });

          //act
          ds.getResource(href, {}, InstanceResource, cbSpy);
        });
        after(function () {
          sandbox.restore();
        });
        it('request executor should be called once', function () {
          /* jshint -W030 */
          reqExecStub.should.have.been.called;

          cbSpy.should.have.been.calledOnce;
        });
      });

      describe('request error handling', function () {
        var ds = new DataStore({
          cacheOptions: {
            tenants: {
              store: MemoryStore,
              ttl: 60,
              tti: 60
            }
          },
          apiKey: {id: 1, secret: 2}
        });

        var href = '/tenants/3' + random();
        var cbSpy = sinon.spy();
        var error = new Error();
        var sandbox, reqExecStub;
        before(function () {
          sandbox = sinon.sandbox.create();
          reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
            cb(error);
          });

        });
        after(function () {
          sandbox.restore();
        });
        it('get resource should return error', function () {
          //act
          ds.getResource(href, cbSpy);
          cbSpy.should.have.been.calledWith(error);
        });
        it('create resource should return error', function () {
          //act
          ds.createResource(href, cbSpy);
          cbSpy.should.have.been.calledWith(error);
        });
        it('save resource should return error', function () {
          //act
          ds.saveResource(href, cbSpy);
          cbSpy.should.have.been.calledWith(error);
        });
        it('delete resource should return error', function () {
          //act
          ds.deleteResource({href: href}, cbSpy);
          cbSpy.should.have.been.calledWith(error);
        });
      });

      describe('request undefined state handling', function () {
        var ds = new DataStore({
          cacheOptions: {
            tenants: {
              store: MemoryStore,
              ttl: 60,
              tti: 60
            }
          },
          apiKey: {id: 1, secret: 2}
        });

        var href = '/tenants/3' + random();
        var cbSpy = sinon.spy();
        var sandbox, reqExecStub;
        before(function () {
          sandbox = sinon.sandbox.create();
          reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
            cb(null, null);
          });

          //act
          ds.getResource(href, cbSpy);
        });
        after(function () {
          sandbox.restore();
        });
        it('should not hang if response undefined', function () {
          /* jshint -W030 */
          cbSpy.should.have.been.calledOnce;
          cbSpy.should.have.been.calledWith(null);
        });
      });
    });

    describe('create resource', function () {
      var region = 'tenants';
      var ds = new DataStore({
        cacheOptions: {
          tenants: {
            store: MemoryStore,
            ttl: 60,
            tti: 60
          }
        },
        apiKey: {id: 1, secret: 2}
      });

      var href = '/tenants/3' + random();
      var query = {q: 'all'};
      var data = {'data': random()};
      var response = {
        href: href,
        data: random()
      };
      var cbSpy = sinon.spy();
      var sandbox, cachePutSpy, reqExecStub, requestSpy;
      before(function () {
        sandbox = sinon.sandbox.create();
        var cache = ds.cacheManager.getCache(region);
        cachePutSpy = sandbox.spy(cache, 'put');
        reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
          requestSpy = req;
          cb(null, response);
        });

        //act
        ds.createResource(href, query, data, InstanceResource, cbSpy);
      });
      after(function () {
        sandbox.restore();
      });

      describe('if provided incorrect resource constructor', function () {
        function callToCreateResourceWithInvalidResouceCtor() {
          ds.createResource(href, query, data, utils.noop, utils.noop);
        }

        it('should throw error', function () {
          callToCreateResourceWithInvalidResouceCtor
            .should.throw(/constructor function.*InstanceResource/i);
        });
      });

      describe('params translation to request object', function () {
        it('should translate href to req.uri', function () {
          requestSpy.uri.should.be.equal(href);
        });
        it('should translate query to req.query', function () {
          requestSpy.query.should.be.equal(query);
        });
        it('should translate body to req.body', function () {
          requestSpy.body.should.be.equal(data);
        });
      });

      describe('after resource creation', function () {
        it('resource should be stored in cache by href in response', function () {
          /* jshint -W030 */
          cachePutSpy.should.have.been.calledOnce;
        });
      });
    });

    describe('save resource', function () {
      var region = 'tenants';
      var ds = new DataStore({
        cacheOptions: {
          tenants: {
            store: MemoryStore,
            ttl: 60,
            tti: 60
          }
        },
        apiKey: {id: 1, secret: 2}
      });

      var href = '/tenants/2' + random();
      var data = {'data': random()};
      var response = {'data': data, href: href};
      var cbSpy = sinon.spy();
      var sandbox, cachePutSpy, reqExecStub, requestSpy;
      before(function () {
        sandbox = sinon.sandbox.create();
        var cache = ds.cacheManager.getCache(region);
        cachePutSpy = sandbox.spy(cache, 'put');
        reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
          requestSpy = req;
          cb(null, response);
        });

        //act
        ds.saveResource(response, cbSpy);
      });
      after(function () {
        sandbox.restore();
      });

      describe('transfer params in request', function () {
        it('should translate href to req.uri', function () {
          requestSpy.uri.should.be.equal(href);
        });
        it('req.method === POST', function () {
          requestSpy.method.should.be.equal('POST');
        });
        it('should translate body to req.body', function () {
          requestSpy.body.should.be.equal(response);
        });
      });

      describe('after resource update', function () {
        it('should be stored in cache', function () {
          /* jshint -W030 */
          cachePutSpy.should.have.been.calledOnce;
          cachePutSpy.should.have.been.calledWith(response.href, response, false, utils.noop);
        });
      });
    });

    describe('delete resource', function () {
      var region = 'tenants';
      var ds = new DataStore({
        cacheOptions: {
          tenants: {
            store: MemoryStore,
            ttl: 60,
            tti: 60
          }
        },
        apiKey: {id: 1, secret: 2}
      });

      var href = '/tenants/2' + random();
      var data = {'data': random()};
      var response = {'data': data, href: href};
      var cbSpy = sinon.spy();
      var sandbox, cacheDeleteSpy, reqExecStub, requestSpy;
      before(function () {
        sandbox = sinon.sandbox.create();
        var cache = ds.cacheManager.getCache(region);
        cacheDeleteSpy = sandbox.spy(cache, 'delete');
        reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
          requestSpy = req;
          cb(null, response);
        });

        //act
        ds.deleteResource(response, cbSpy);
      });
      after(function () {
        sandbox.restore();
      });

      it('should remove entry from cache', function () {
        /* jshint -W030 */
        cacheDeleteSpy.should.have.been.calledOnce;
        cacheDeleteSpy.should.have.been.calledWith(href, utils.noop);
      });

      it('href -> req.uri', function () {
        requestSpy.uri.should.be.equal(href);
      });

      it('req.method === DELETE', function () {
        requestSpy.method.should.be.equal('DELETE');
      });
    });
  });
});<commit_msg>fixed DataStore tests<commit_after>var common = require('./common');
var _ = common._;
var sinon = common.sinon;
var should = common.should;
var utils = require('../lib/utils');

var DataStore = require('../lib/ds/DataStore');
var RequestExecutor = require('../lib/ds/RequestExecutor');
var MemoryStore = require('../lib/cache/MemoryStore');
var InstanceResource = require('../lib/resource/InstanceResource');

function random() {
  return Math.floor(Math.random() * Date.now());
}

describe('data store module', function () {
  describe('data store class', function () {

    describe('constructor', function () {
      describe('if request executor not provided in config', function () {
        var ds = new DataStore({apiKey: {id: 1, secret: 2}});
        it('should create instance of default RequestExecutor', function () {
          ds.requestExecutor.should.be.an.instanceof(RequestExecutor);
        });
      });

      describe('if request executor was provided in config', function () {
        var reqExec = new RequestExecutor({apiKey: {id: 1, secret: 2}});
        var ds = new DataStore({requestExecutor: reqExec});
        it('should reuse provided request executor instance', function () {
          ds.requestExecutor.should.be.equal(reqExec);
        });
      });

      describe('if cache options was not provided', function () {
        var ds = new DataStore({apiKey: {id: 1, secret: 2}});
        it('should not create caches', function () {
          should.not.exist(ds.caches);
        });
      });

      describe('if config not provided', function () {
        function createDataStoreWithoutConfig() {
          return new DataStore();
        }

        it('should throw "config argument is required"', function () {
          createDataStoreWithoutConfig.should
            .throw(/config argument is required/i);
        });
      });

      describe('for all provided regions', function () {
        var CACHE_REGIONS = ['applications', 'directories', 'accounts', 'groups',
          'groupMemberships', 'tenants', 'accountStoreMappings'];
        var cacheOptions = {};
        _.each(CACHE_REGIONS, function (region) {
          cacheOptions[region] = {};
        });
        var ds = new DataStore({cacheOptions: cacheOptions,
          apiKey: {id: 1, secret: 2}});
        _.each(CACHE_REGIONS, function (region) {
          it('should create cache instance for ' + region, function () {
            should.exist(ds.cacheHandler.cacheManager.getCache(region));
          });
        });

      });
    });

    describe('storing recursive response object', function () {
      var data = {
        'href': 'http://example.com/accounts/FOO',
        'name': 'Foo',
        'groups': {
          'href': 'http://example.com/accounts/FOO/groups',
          'items': [
            {
              'href': 'http://example.com/groups/G1',
              'name': 'Foo Group 1'
            },
            {
              'href': 'http://example.com/groups/G2',
              'name': 'Foo Group 2'
            }
          ]
        },
        'directory': {
          'href': 'http://example.com/directories/BAR',
          'name': 'Directory'
        }
      };
      var ds = new DataStore({
        cacheOptions: {
          store: MemoryStore,
          accounts: {},
          groups: {},
          directories: {}
        },
        apiKey: {id: 1, secret: 2}
      });

      var cbSpy = sinon.spy();
      var sandbox, reqExecSpy,
        accountsCachePutSpy,
        groupsCachePutSpy,
        directoryCachePutSpy;

      before(function () {
        sandbox = sinon.sandbox.create();
        accountsCachePutSpy = sandbox.spy(ds.cacheHandler.cacheManager.getCache('accounts'), 'put');
        groupsCachePutSpy = sandbox.spy(ds.cacheHandler.cacheManager.getCache('groups'), 'put');
        directoryCachePutSpy = sandbox.spy(ds.cacheHandler.cacheManager.getCache('directories'), 'put');
        reqExecSpy = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
          cb(null, data);
        });

        // act
        ds.getResource(data.href, cbSpy);
      });
      after(function () {
        sandbox.restore();
      });

      it('should store root entity', function () {
        /* jshint -W030 */
        accountsCachePutSpy.should.have.been.calledOnce;
        accountsCachePutSpy.should.have.been.calledWith(data.href, {
          'href': 'http://example.com/accounts/FOO',
          'name': 'Foo',
          'groups': {
            'href': 'http://example.com/accounts/FOO/groups',
            'items': [
              {'href': 'http://example.com/groups/G1'},
              {'href': 'http://example.com/groups/G2'}
            ]
          },
          'directory': {
            'href': 'http://example.com/directories/BAR'
          }}, true);
      });
      it('should store all nested entities', function () {
        _.each(data.groups.items, function (group) {
          groupsCachePutSpy.should.have.been
            .calledWith(group.href, group, true, utils.noop);
        });

        directoryCachePutSpy.should.have.been
          .calledWith(data.directory.href, data.directory, true, utils.noop);
      });
    });

    describe('get resource', function () {
      var region = 'tenants';
      var ds = new DataStore({
        cacheOptions: {
          store: MemoryStore,
          tenants: {
            store: MemoryStore,
            ttl: 60,
            tti: 60
          },
          directories: {
            ttl: 120,
            tti: 120
          }
        },
        apiKey: {id: 1, secret: 2}
      });

      describe('required params', function () {
        it('should throw error if href is not defined', function () {
          ds.getResource.should.throw();
        });

        it('should throw error if callback is not defined', function () {
          ds.getResource.should.throw();
        });
      });

      describe('if href already cached', function () {
        var href = '/tenants/1' + random();
        var data = {data: random()};
        var cbSpy = sinon.spy();
        var sandbox, cacheGetSpy, reqExecSpy;
        before(function (done) {
          sandbox = sinon.sandbox.create();
          reqExecSpy = sandbox.spy(ds.requestExecutor, 'execute');
          var cache = ds.cacheHandler.cacheManager.getCache(region);
          cacheGetSpy = sandbox.spy(cache, 'get');
          cache.put(href, data, true, done);
        });
        after(function () {
          sandbox.restore();
        });

        it('should return entry from cache', function () {
          ds.getResource(href, cbSpy);

          /* jshint -W030 */
          cacheGetSpy.should.have.been.calledOnce;
          cacheGetSpy.should.have.been.calledWith(href);

          reqExecSpy.should.not.been.called;

          cbSpy.should.have.been.calledOnce;
          cbSpy.should.have.been.calledWith(null);
        });
      });

      // todo: tbd - ignore cache if query param provided
      /*      describe('if query provided', function(){
       it('should ignore cache');
       });*/

      describe('if href not found in cache', function () {
        var href = '/tenants/2' + random();
        var data = {
          href: href,
          data: random()};
        var cbSpy = sinon.spy();
        var sandbox, cacheGetSpy, cachePutSpy, reqExecStub;
        before(function () {
          sandbox = sinon.sandbox.create();
          var cache = ds.cacheHandler.cacheManager.getCache(region);
          cacheGetSpy = sandbox.spy(cache, 'get');
          cachePutSpy = sandbox.spy(cache, 'put');
          reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
            cb(null, data);
          });

          //act
          ds.getResource(href, {}, InstanceResource, cbSpy);
        });
        after(function () {
          sandbox.restore();
        });
        it('request executor should be called once', function () {
          /* jshint -W030 */
          cacheGetSpy.should.have.been.calledOnce;
          cacheGetSpy.should.have.been.calledWith(href);

          reqExecStub.should.have.been.called;

          cbSpy.should.have.been.calledOnce;
        });
        it('and result should be stored in cache', function () {
          /* jshint -W030 */
          cachePutSpy.should.have.been.calledOnce;
          cachePutSpy.should.have.been.calledWith(href, data);
        });
      });

      describe('if href region not cached', function () {
        var href = '/directory/2' + random();
        var data = {'data': random()};
        var cbSpy = sinon.spy();
        var sandbox, reqExecStub;
        before(function () {
          sandbox = sinon.sandbox.create();
          reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
            cb(null, data);
          });

          //act
          ds.getResource(href, {}, InstanceResource, cbSpy);
        });
        after(function () {
          sandbox.restore();
        });
        it('request executor should be called once', function () {
          /* jshint -W030 */
          reqExecStub.should.have.been.called;

          cbSpy.should.have.been.calledOnce;
        });
      });

      describe('request error handling', function () {
        var ds = new DataStore({
          cacheOptions: {
            tenants: {
              store: MemoryStore,
              ttl: 60,
              tti: 60
            }
          },
          apiKey: {id: 1, secret: 2}
        });

        var href = '/tenants/3' + random();
        var cbSpy = sinon.spy();
        var error = new Error();
        var sandbox, reqExecStub;
        before(function () {
          sandbox = sinon.sandbox.create();
          reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
            cb(error);
          });

        });
        after(function () {
          sandbox.restore();
        });
        it('get resource should return error', function () {
          //act
          ds.getResource(href, cbSpy);
          cbSpy.should.have.been.calledWith(error);
        });
        it('create resource should return error', function () {
          //act
          ds.createResource(href, cbSpy);
          cbSpy.should.have.been.calledWith(error);
        });
        it('save resource should return error', function () {
          //act
          ds.saveResource(href, cbSpy);
          cbSpy.should.have.been.calledWith(error);
        });
        it('delete resource should return error', function () {
          //act
          ds.deleteResource({href: href}, cbSpy);
          cbSpy.should.have.been.calledWith(error);
        });
      });

      describe('request undefined state handling', function () {
        var ds = new DataStore({
          cacheOptions: {
            tenants: {
              store: MemoryStore,
              ttl: 60,
              tti: 60
            }
          },
          apiKey: {id: 1, secret: 2}
        });

        var href = '/tenants/3' + random();
        var cbSpy = sinon.spy();
        var sandbox, reqExecStub;
        before(function () {
          sandbox = sinon.sandbox.create();
          reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
            cb(null, null);
          });

          //act
          ds.getResource(href, cbSpy);
        });
        after(function () {
          sandbox.restore();
        });
        it('should not hang if response undefined', function () {
          /* jshint -W030 */
          cbSpy.should.have.been.calledOnce;
          cbSpy.should.have.been.calledWith(null);
        });
      });
    });

    describe('create resource', function () {
      var region = 'tenants';
      var ds = new DataStore({
        cacheOptions: {
          tenants: {
            store: MemoryStore,
            ttl: 60,
            tti: 60
          }
        },
        apiKey: {id: 1, secret: 2}
      });

      var href = '/tenants/3' + random();
      var query = {q: 'all'};
      var data = {'data': random()};
      var response = {
        href: href,
        data: random()
      };
      var cbSpy = sinon.spy();
      var sandbox, cachePutSpy, reqExecStub, requestSpy;
      before(function () {
        sandbox = sinon.sandbox.create();
        var cache = ds.cacheHandler.cacheManager.getCache(region);
        cachePutSpy = sandbox.spy(cache, 'put');
        reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
          requestSpy = req;
          cb(null, response);
        });

        //act
        ds.createResource(href, query, data, InstanceResource, cbSpy);
      });
      after(function () {
        sandbox.restore();
      });

      describe('if provided incorrect resource constructor', function () {
        function callToCreateResourceWithInvalidResouceCtor() {
          ds.createResource(href, query, data, utils.noop, utils.noop);
        }

        it('should throw error', function () {
          callToCreateResourceWithInvalidResouceCtor
            .should.throw(/constructor function.*InstanceResource/i);
        });
      });

      describe('params translation to request object', function () {
        it('should translate href to req.uri', function () {
          requestSpy.uri.should.be.equal(href);
        });
        it('should translate query to req.query', function () {
          requestSpy.query.should.be.equal(query);
        });
        it('should translate body to req.body', function () {
          requestSpy.body.should.be.equal(data);
        });
      });

      describe('after resource creation', function () {
        it('resource should be stored in cache by href in response', function () {
          /* jshint -W030 */
          cachePutSpy.should.have.been.calledOnce;
        });
      });
    });

    describe('save resource', function () {
      var region = 'tenants';
      var ds = new DataStore({
        cacheOptions: {
          tenants: {
            store: MemoryStore,
            ttl: 60,
            tti: 60
          }
        },
        apiKey: {id: 1, secret: 2}
      });

      var href = '/tenants/2' + random();
      var data = {'data': random()};
      var response = {'data': data, href: href};
      var cbSpy = sinon.spy();
      var sandbox, cachePutSpy, reqExecStub, requestSpy;
      before(function () {
        sandbox = sinon.sandbox.create();
        var cache = ds.cacheHandler.cacheManager.getCache(region);
        cachePutSpy = sandbox.spy(cache, 'put');
        reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
          requestSpy = req;
          cb(null, response);
        });

        //act
        ds.saveResource(response, cbSpy);
      });
      after(function () {
        sandbox.restore();
      });

      describe('transfer params in request', function () {
        it('should translate href to req.uri', function () {
          requestSpy.uri.should.be.equal(href);
        });
        it('req.method === POST', function () {
          requestSpy.method.should.be.equal('POST');
        });
        it('should translate body to req.body', function () {
          requestSpy.body.should.be.equal(response);
        });
      });

      describe('after resource update', function () {
        it('should be stored in cache', function () {
          /* jshint -W030 */
          cachePutSpy.should.have.been.calledOnce;
          cachePutSpy.should.have.been.calledWith(response.href, response, false, utils.noop);
        });
      });
    });

    describe('delete resource', function () {
      var region = 'tenants';
      var ds = new DataStore({
        cacheOptions: {
          tenants: {
            store: MemoryStore,
            ttl: 60,
            tti: 60
          }
        },
        apiKey: {id: 1, secret: 2}
      });

      var href = '/tenants/2' + random();
      var data = {'data': random()};
      var response = {'data': data, href: href};
      var cbSpy = sinon.spy();
      var sandbox, cacheDeleteSpy, reqExecStub, requestSpy;
      before(function () {
        sandbox = sinon.sandbox.create();
        var cache = ds.cacheHandler.cacheManager.getCache(region);
        cacheDeleteSpy = sandbox.spy(cache, 'delete');
        reqExecStub = sandbox.stub(ds.requestExecutor, 'execute', function (req, cb) {
          requestSpy = req;
          cb(null, response);
        });

        //act
        ds.deleteResource(response, cbSpy);
      });
      after(function () {
        sandbox.restore();
      });

      it('should remove entry from cache', function () {
        /* jshint -W030 */
        cacheDeleteSpy.should.have.been.calledOnce;
        cacheDeleteSpy.should.have.been.calledWith(href, utils.noop);
      });

      it('href -> req.uri', function () {
        requestSpy.uri.should.be.equal(href);
      });

      it('req.method === DELETE', function () {
        requestSpy.method.should.be.equal('DELETE');
      });
    });
  });
});<|endoftext|>


<commit_before>/* @flow */
import React, { PureComponent } from 'react';
import { View, TextInput, findNodeHandle } from 'react-native';
import { connect } from 'react-redux';
import TextInputReset from 'react-native-text-input-reset';
import isEqual from 'lodash.isequal';

import type {
  Auth,
  Context,
  Narrow,
  EditMessage,
  InputSelectionType,
  User,
  Dispatch,
  Dimensions,
  GlobalState,
} from '../types';
import {
  addToOutbox,
  cancelEditMessage,
  draftAdd,
  draftRemove,
  fetchTopicsForActiveStream,
  sendTypingEvent,
} from '../actions';
import { updateMessage } from '../api';
import { FloatingActionButton, Input, MultilineInput } from '../common';
import { showErrorAlert } from '../utils/info';
import { IconDone, IconSend } from '../common/Icons';
import { isStreamNarrow, topicNarrow } from '../utils/narrow';
import ComposeMenu from './ComposeMenu';
import AutocompleteViewWrapper from '../autocomplete/AutocompleteViewWrapper';
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
import NotSubscribed from '../message/NotSubscribed';

import {
  getAuth,
  getSession,
  canSendToActiveNarrow,
  getLastMessageTopic,
  getActiveUsersAndBots,
  getShowMessagePlaceholders,
} from '../selectors';
import { getIsActiveStreamSubscribed } from '../subscriptions/subscriptionSelectors';
import { getDraftForActiveNarrow } from '../drafts/draftsSelectors';

type Props = {
  auth: Auth,
  canSend: boolean,
  narrow: Narrow,
  usersAndBots: User[],
  draft: string,
  lastMessageTopic: string,
  isSubscribed: boolean,
  editMessage: EditMessage,
  safeAreaInsets: Dimensions,
  dispatch: Dispatch,
  messageInputRef: (component: any) => void,
};

type State = {
  isMessageFocused: boolean,
  isTopicFocused: boolean,
  isMenuExpanded: boolean,
  topic: string,
  message: string,
  height: number,
  selection: InputSelectionType,
};

class ComposeBox extends PureComponent<Props, State> {
  context: Context;
  props: Props;
  state: State;

  messageInput: TextInput = null;
  topicInput: TextInput = null;

  static contextTypes = {
    styles: () => null,
  };

  state = {
    isMessageFocused: false,
    isTopicFocused: false,
    isMenuExpanded: false,
    height: 20,
    topic: '',
    message: this.props.draft,
    selection: { start: 0, end: 0 },
  };

  getCanSelectTopic = () => {
    const { isMessageFocused, isTopicFocused } = this.state;
    const { narrow } = this.props;
    if (!isStreamNarrow(narrow)) {
      return false;
    }
    return isMessageFocused || isTopicFocused;
  };

  handleComposeMenuToggle = () => {
    this.setState(({ isMenuExpanded }) => ({
      isMenuExpanded: !isMenuExpanded,
    }));
  };

  handleLayoutChange = (event: Object) => {
    this.setState({
      height: event.nativeEvent.layout.height,
    });
  };

  handleTopicChange = (topic: string) => {
    this.setState({ topic, isMenuExpanded: false });
  };

  handleMessageChange = (message: string) => {
    this.setState({ message, isMenuExpanded: false });
    const { dispatch, narrow } = this.props;
    dispatch(sendTypingEvent(narrow));
  };

  handleMessageSelectionChange = (event: Object) => {
    const { selection } = event.nativeEvent;
    this.setState({ selection });
  };

  handleMessageFocus = () => {
    const { lastMessageTopic } = this.props;
    this.setState(({ topic }) => ({
      isMessageFocused: true,
      isMenuExpanded: false,
      topic: topic || lastMessageTopic,
    }));
  };

  handleMessageBlur = () => {
    setTimeout(() => {
      this.setState({
        isMessageFocused: false,
        isMenuExpanded: false,
      });
    }, 200); // give a chance to the topic input to get the focus
  };

  handleTopicFocus = () => {
    const { dispatch, narrow } = this.props;
    this.setState({
      isTopicFocused: true,
      isMenuExpanded: false,
    });
    dispatch(fetchTopicsForActiveStream(narrow));
  };

  handleTopicBlur = () => {
    setTimeout(() => {
      this.setState({
        isTopicFocused: false,
        isMenuExpanded: false,
      });
    }, 200); // give a chance to the mesage input to get the focus
  };

  handleInputTouchStart = () => {
    this.setState({ isMenuExpanded: false });
  };

  clearMessageInput = () => {
    if (this.messageInput) {
      this.messageInput.clear();
      if (TextInputReset) {
        TextInputReset.resetKeyboardInput(findNodeHandle(this.messageInput));
      }
    }

    this.handleMessageChange('');
  };

  handleSend = () => {
    const { dispatch, narrow } = this.props;
    const { topic, message } = this.state;

    const destinationNarrow = isStreamNarrow(narrow)
      ? topicNarrow(narrow[0].operand, topic || '(no topic)')
      : narrow;

    dispatch(addToOutbox(destinationNarrow, message));
    dispatch(draftRemove(narrow));

    this.clearMessageInput();
  };

  handleEdit = () => {
    const { auth, editMessage, dispatch } = this.props;
    const { message, topic } = this.state;
    const content = editMessage.content !== message ? message : undefined;
    const subject = topic !== editMessage.topic ? topic : undefined;
    if (content || subject) {
      updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
        showErrorAlert(error.message, 'Failed to edit message');
      });
    }
    dispatch(cancelEditMessage());
  };

  tryUpdateDraft = () => {
    const { dispatch, draft, narrow } = this.props;
    const { message } = this.state;

    if (draft.trim() === message.trim()) {
      return;
    }

    if (message.trim().length === 0) {
      dispatch(draftRemove(narrow));
    } else {
      dispatch(draftAdd(narrow, message));
    }
  };

  componentWillUnmount() {
    this.tryUpdateDraft();
  }

  componentWillReceiveProps(nextProps: Props) {
    if (nextProps.editMessage !== this.props.editMessage) {
      const topic =
        isStreamNarrow(nextProps.narrow) && nextProps.editMessage
          ? nextProps.editMessage.topic
          : '';
      this.setState({
        message: nextProps.editMessage ? nextProps.editMessage.content : '',
        topic,
      });
      if (this.messageInput) {
        this.messageInput.focus();
      }
    } else if (!isEqual(nextProps.narrow, this.props.narrow)) {
      this.tryUpdateDraft();

      if (nextProps.draft) {
        this.setState({ message: nextProps.draft });
      } else {
        this.clearMessageInput();
      }
    }
  }

  render() {
    const { styles } = this.context;
    const { isTopicFocused, isMenuExpanded, height, message, topic, selection } = this.state;
    const {
      auth,
      canSend,
      narrow,
      usersAndBots,
      editMessage,
      safeAreaInsets,
      messageInputRef,
      isSubscribed,
    } = this.props;

    if (!canSend) {
      return null;
    }

    if (!isSubscribed) {
      return <NotSubscribed narrow={narrow} />;
    }

    const placeholder = getComposeInputPlaceholder(narrow, auth.email, usersAndBots);

    return (
      <View style={{ marginBottom: safeAreaInsets.bottom }}>
        <AutocompleteViewWrapper
          composeText={message}
          isTopicFocused={isTopicFocused}
          marginBottom={height}
          messageSelection={selection}
          narrow={narrow}
          topicText={topic}
          onMessageAutocomplete={this.handleMessageChange}
          onTopicAutocomplete={this.handleTopicChange}
        />
        <View style={styles.composeBox} onLayout={this.handleLayoutChange}>
          <View style={styles.alignBottom}>
            <ComposeMenu
              narrow={narrow}
              expanded={isMenuExpanded}
              onExpandContract={this.handleComposeMenuToggle}
            />
          </View>
          <View style={styles.composeText}>
            {this.getCanSelectTopic() && (
              <Input
                style={styles.topicInput}
                underlineColorAndroid="transparent"
                placeholder="Topic"
                selectTextOnFocus
                textInputRef={component => {
                  this.topicInput = component;
                }}
                onChangeText={this.handleTopicChange}
                onFocus={this.handleTopicFocus}
                onBlur={this.handleTopicBlur}
                onTouchStart={this.handleInputTouchStart}
                value={topic}
              />
            )}
            <MultilineInput
              style={styles.composeTextInput}
              placeholder={placeholder}
              textInputRef={component => {
                if (component) {
                  this.messageInput = component;
                  messageInputRef(component);
                }
              }}
              value={message}
              onBlur={this.handleMessageBlur}
              onChange={this.handleMessageChange}
              onFocus={this.handleMessageFocus}
              onSelectionChange={this.handleMessageSelectionChange}
              onTouchStart={this.handleInputTouchStart}
            />
          </View>
          <View style={styles.alignBottom}>
            <FloatingActionButton
              style={styles.composeSendButton}
              Icon={editMessage === null ? IconSend : IconDone}
              size={32}
              disabled={message.trim().length === 0}
              onPress={editMessage === null ? this.handleSend : this.handleEdit}
            />
          </View>
        </View>
      </View>
    );
  }
}

export default connect((state: GlobalState, props) => ({
  auth: getAuth(state),
  usersAndBots: getActiveUsersAndBots(state),
  safeAreaInsets: getSession(state).safeAreaInsets,
  isSubscribed: getIsActiveStreamSubscribed(props.narrow)(state),
  canSend: canSendToActiveNarrow(props.narrow) && !getShowMessagePlaceholders(props.narrow)(state),
  editMessage: getSession(state).editMessage,
  draft: getDraftForActiveNarrow(props.narrow)(state),
  lastMessageTopic: getLastMessageTopic(props.narrow)(state),
}))(ComposeBox);
<commit_msg>message edit: Show topic input even in topic narrow.<commit_after>/* @flow */
import React, { PureComponent } from 'react';
import { View, TextInput, findNodeHandle } from 'react-native';
import { connect } from 'react-redux';
import TextInputReset from 'react-native-text-input-reset';
import isEqual from 'lodash.isequal';

import type {
  Auth,
  Context,
  Narrow,
  EditMessage,
  InputSelectionType,
  User,
  Dispatch,
  Dimensions,
  GlobalState,
} from '../types';
import {
  addToOutbox,
  cancelEditMessage,
  draftAdd,
  draftRemove,
  fetchTopicsForActiveStream,
  sendTypingEvent,
} from '../actions';
import { updateMessage } from '../api';
import { FloatingActionButton, Input, MultilineInput } from '../common';
import { showErrorAlert } from '../utils/info';
import { IconDone, IconSend } from '../common/Icons';
import { isStreamNarrow, isStreamOrTopicNarrow, topicNarrow } from '../utils/narrow';
import ComposeMenu from './ComposeMenu';
import AutocompleteViewWrapper from '../autocomplete/AutocompleteViewWrapper';
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
import NotSubscribed from '../message/NotSubscribed';

import {
  getAuth,
  getSession,
  canSendToActiveNarrow,
  getLastMessageTopic,
  getActiveUsersAndBots,
  getShowMessagePlaceholders,
} from '../selectors';
import { getIsActiveStreamSubscribed } from '../subscriptions/subscriptionSelectors';
import { getDraftForActiveNarrow } from '../drafts/draftsSelectors';

type Props = {
  auth: Auth,
  canSend: boolean,
  narrow: Narrow,
  usersAndBots: User[],
  draft: string,
  lastMessageTopic: string,
  isSubscribed: boolean,
  editMessage: EditMessage,
  safeAreaInsets: Dimensions,
  dispatch: Dispatch,
  messageInputRef: (component: any) => void,
};

type State = {
  isMessageFocused: boolean,
  isTopicFocused: boolean,
  isMenuExpanded: boolean,
  topic: string,
  message: string,
  height: number,
  selection: InputSelectionType,
};

class ComposeBox extends PureComponent<Props, State> {
  context: Context;
  props: Props;
  state: State;

  messageInput: TextInput = null;
  topicInput: TextInput = null;

  static contextTypes = {
    styles: () => null,
  };

  state = {
    isMessageFocused: false,
    isTopicFocused: false,
    isMenuExpanded: false,
    height: 20,
    topic: '',
    message: this.props.draft,
    selection: { start: 0, end: 0 },
  };

  getCanSelectTopic = () => {
    const { isMessageFocused, isTopicFocused } = this.state;
    const { editMessage, narrow } = this.props;
    if (editMessage) {
      return isStreamOrTopicNarrow(narrow);
    }
    if (!isStreamNarrow(narrow)) {
      return false;
    }
    return isMessageFocused || isTopicFocused;
  };

  handleComposeMenuToggle = () => {
    this.setState(({ isMenuExpanded }) => ({
      isMenuExpanded: !isMenuExpanded,
    }));
  };

  handleLayoutChange = (event: Object) => {
    this.setState({
      height: event.nativeEvent.layout.height,
    });
  };

  handleTopicChange = (topic: string) => {
    this.setState({ topic, isMenuExpanded: false });
  };

  handleMessageChange = (message: string) => {
    this.setState({ message, isMenuExpanded: false });
    const { dispatch, narrow } = this.props;
    dispatch(sendTypingEvent(narrow));
  };

  handleMessageSelectionChange = (event: Object) => {
    const { selection } = event.nativeEvent;
    this.setState({ selection });
  };

  handleMessageFocus = () => {
    const { lastMessageTopic } = this.props;
    this.setState(({ topic }) => ({
      isMessageFocused: true,
      isMenuExpanded: false,
      topic: topic || lastMessageTopic,
    }));
  };

  handleMessageBlur = () => {
    setTimeout(() => {
      this.setState({
        isMessageFocused: false,
        isMenuExpanded: false,
      });
    }, 200); // give a chance to the topic input to get the focus
  };

  handleTopicFocus = () => {
    const { dispatch, narrow } = this.props;
    this.setState({
      isTopicFocused: true,
      isMenuExpanded: false,
    });
    dispatch(fetchTopicsForActiveStream(narrow));
  };

  handleTopicBlur = () => {
    setTimeout(() => {
      this.setState({
        isTopicFocused: false,
        isMenuExpanded: false,
      });
    }, 200); // give a chance to the mesage input to get the focus
  };

  handleInputTouchStart = () => {
    this.setState({ isMenuExpanded: false });
  };

  clearMessageInput = () => {
    if (this.messageInput) {
      this.messageInput.clear();
      if (TextInputReset) {
        TextInputReset.resetKeyboardInput(findNodeHandle(this.messageInput));
      }
    }

    this.handleMessageChange('');
  };

  handleSend = () => {
    const { dispatch, narrow } = this.props;
    const { topic, message } = this.state;

    const destinationNarrow = isStreamNarrow(narrow)
      ? topicNarrow(narrow[0].operand, topic || '(no topic)')
      : narrow;

    dispatch(addToOutbox(destinationNarrow, message));
    dispatch(draftRemove(narrow));

    this.clearMessageInput();
  };

  handleEdit = () => {
    const { auth, editMessage, dispatch } = this.props;
    const { message, topic } = this.state;
    const content = editMessage.content !== message ? message : undefined;
    const subject = topic !== editMessage.topic ? topic : undefined;
    if (content || subject) {
      updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
        showErrorAlert(error.message, 'Failed to edit message');
      });
    }
    dispatch(cancelEditMessage());
  };

  tryUpdateDraft = () => {
    const { dispatch, draft, narrow } = this.props;
    const { message } = this.state;

    if (draft.trim() === message.trim()) {
      return;
    }

    if (message.trim().length === 0) {
      dispatch(draftRemove(narrow));
    } else {
      dispatch(draftAdd(narrow, message));
    }
  };

  componentWillUnmount() {
    this.tryUpdateDraft();
  }

  componentWillReceiveProps(nextProps: Props) {
    if (nextProps.editMessage !== this.props.editMessage) {
      const topic =
        isStreamNarrow(nextProps.narrow) && nextProps.editMessage
          ? nextProps.editMessage.topic
          : '';
      this.setState({
        message: nextProps.editMessage ? nextProps.editMessage.content : '',
        topic,
      });
      if (this.messageInput) {
        this.messageInput.focus();
      }
    } else if (!isEqual(nextProps.narrow, this.props.narrow)) {
      this.tryUpdateDraft();

      if (nextProps.draft) {
        this.setState({ message: nextProps.draft });
      } else {
        this.clearMessageInput();
      }
    }
  }

  render() {
    const { styles } = this.context;
    const { isTopicFocused, isMenuExpanded, height, message, topic, selection } = this.state;
    const {
      auth,
      canSend,
      narrow,
      usersAndBots,
      editMessage,
      safeAreaInsets,
      messageInputRef,
      isSubscribed,
    } = this.props;

    if (!canSend) {
      return null;
    }

    if (!isSubscribed) {
      return <NotSubscribed narrow={narrow} />;
    }

    const placeholder = getComposeInputPlaceholder(narrow, auth.email, usersAndBots);

    return (
      <View style={{ marginBottom: safeAreaInsets.bottom }}>
        <AutocompleteViewWrapper
          composeText={message}
          isTopicFocused={isTopicFocused}
          marginBottom={height}
          messageSelection={selection}
          narrow={narrow}
          topicText={topic}
          onMessageAutocomplete={this.handleMessageChange}
          onTopicAutocomplete={this.handleTopicChange}
        />
        <View style={styles.composeBox} onLayout={this.handleLayoutChange}>
          <View style={styles.alignBottom}>
            <ComposeMenu
              narrow={narrow}
              expanded={isMenuExpanded}
              onExpandContract={this.handleComposeMenuToggle}
            />
          </View>
          <View style={styles.composeText}>
            {this.getCanSelectTopic() && (
              <Input
                style={styles.topicInput}
                underlineColorAndroid="transparent"
                placeholder="Topic"
                selectTextOnFocus
                textInputRef={component => {
                  this.topicInput = component;
                }}
                onChangeText={this.handleTopicChange}
                onFocus={this.handleTopicFocus}
                onBlur={this.handleTopicBlur}
                onTouchStart={this.handleInputTouchStart}
                value={topic}
              />
            )}
            <MultilineInput
              style={styles.composeTextInput}
              placeholder={placeholder}
              textInputRef={component => {
                if (component) {
                  this.messageInput = component;
                  messageInputRef(component);
                }
              }}
              value={message}
              onBlur={this.handleMessageBlur}
              onChange={this.handleMessageChange}
              onFocus={this.handleMessageFocus}
              onSelectionChange={this.handleMessageSelectionChange}
              onTouchStart={this.handleInputTouchStart}
            />
          </View>
          <View style={styles.alignBottom}>
            <FloatingActionButton
              style={styles.composeSendButton}
              Icon={editMessage === null ? IconSend : IconDone}
              size={32}
              disabled={message.trim().length === 0}
              onPress={editMessage === null ? this.handleSend : this.handleEdit}
            />
          </View>
        </View>
      </View>
    );
  }
}

export default connect((state: GlobalState, props) => ({
  auth: getAuth(state),
  usersAndBots: getActiveUsersAndBots(state),
  safeAreaInsets: getSession(state).safeAreaInsets,
  isSubscribed: getIsActiveStreamSubscribed(props.narrow)(state),
  canSend: canSendToActiveNarrow(props.narrow) && !getShowMessagePlaceholders(props.narrow)(state),
  editMessage: getSession(state).editMessage,
  draft: getDraftForActiveNarrow(props.narrow)(state),
  lastMessageTopic: getLastMessageTopic(props.narrow)(state),
}))(ComposeBox);
<|endoftext|>