<commit_before>/* @flow */
import React, { PureComponent } from 'react';
import { StyleSheet, View, TextInput } from 'react-native';

import type { Auth, Narrow, EditMessage, InputSelectionType, User, Actions } from '../types';
import patchMessage from '../api/updateMessage';
import { FloatingActionButton, Input, MultilineInput } from '../common';
import { showErrorAlert } from '../common/errorAlert';
import { IconDone, IconSend } from '../common/Icons';
import { isStreamNarrow, topicNarrow } from '../utils/narrow';
import ComposeMenuContainer from './ComposeMenuContainer';
import AutoCompleteView from '../autocomplete/AutoCompleteView';
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
import { registerUserInputActivity } from '../utils/activity';
import { replaceEmoticonsWithEmoji } from '../emoji/emoticons';

const MIN_HEIGHT = 46;
const MAX_HEIGHT = 100;

const componentStyles = StyleSheet.create({
  bottom: {
    flexDirection: 'column',
    justifyContent: 'flex-end',
  },
  composeText: {
    flex: 1,
    flexDirection: 'column',
  },
  topic: {
    height: 30,
    backgroundColor: 'rgba(127, 127, 127, 0.25)',
  },
  button: {
    margin: 5,
  },
});

type Props = {
  auth: Auth,
  narrow: Narrow,
  users: User[],
  composeTools: boolean,
  editMessage: EditMessage,
  actions: Actions,
  messageInputRef: (component: Object) => void,
  onSend: () => void,
};

export default class ComposeBox extends PureComponent {
  topicInput = null;
  messageInput = null;

  messageInput: TextInput;
  topicInput: TextInput;
  props: Props;

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

  state: {
    optionSelected: number,
    topic: string,
    message: string,
    height: number,
    selection: InputSelectionType,
  };

  state = {
    optionSelected: 0,
    height: 28,
    topic: '',
    message: '',
    selection: { start: 0, end: 0 },
  };

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

  handleMessageChange = (message: string) => {
    this.setState({ message });
    const { auth } = this.props;
    registerUserInputActivity(auth);
  };

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

  handleHeightChange = (height: number) => {
    this.setState({ height });
  };

  clearMessageInput = () => {
    if (this.topicInput) {
      this.topicInput.clear();
    }
    if (this.messageInput) {
      this.messageInput.clear();
    }
    this.handleMessageChange('');
  };

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

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

    actions.addToOutbox(destinationNarrow, messageToSend);

    this.clearMessageInput();
    onSend();
  };

  handleEdit = () => {
    const { auth, editMessage, actions } = this.props;
    const { message } = this.state;
    if (editMessage.content !== message) {
      patchMessage(auth, replaceEmoticonsWithEmoji(message), editMessage.id).catch(error =>
        showErrorAlert(error.message, 'Failed to edit message'),
      );
    }
    actions.cancelEditMessage();
  };

  handleAutoComplete = (input: string) => {
    this.setState({ message: input });
  };

  handleChangeText = (input: string) => {
    this.setState({ message: input });
  };

  componentWillReceiveProps(nextProps: Props) {
    if (nextProps.editMessage !== this.props.editMessage) {
      this.setState({
        message: nextProps.editMessage ? nextProps.editMessage.content : '',
      });
      this.messageInput.focus();
    }
  }

  render() {
    const { styles } = this.context;
    const { height, message, selection, topic } = this.state;
    const { auth, composeTools, narrow, users, editMessage, messageInputRef } = this.props;

    const canSelectTopic = composeTools && isStreamNarrow(narrow);
    const messageHeight = Math.min(Math.max(MIN_HEIGHT, height + 10), MAX_HEIGHT);
    const totalHeight = canSelectTopic ? messageHeight + 30 : messageHeight;
    const placeholder = getComposeInputPlaceholder(narrow, auth.email, users);

    return (
      <View>
        <AutoCompleteView
          text={message}
          onAutocomplete={this.handleAutoComplete}
          selection={selection}
        />
        <View style={[styles.composeBox, { height: totalHeight }]}>
          <View style={componentStyles.bottom}>
            <ComposeMenuContainer />
          </View>
          <View style={[componentStyles.composeText]}>
            {canSelectTopic &&
              <Input
                style={[styles.composeTextInput, componentStyles.topic]}
                underlineColorAndroid="transparent"
                placeholder="Topic"
                textInputRef={component => {
                  this.topicInput = component;
                }}
                onChangeText={this.handleTopicChange}
                value={topic}
              />}
            <MultilineInput
              style={[styles.composeTextInput, { height: totalHeight }]}
              placeholder={placeholder}
              textInputRef={component => {
                this.messageInput = component;
                if (component) messageInputRef(component);
              }}
              onChange={this.handleMessageChange}
              onHeightChange={this.handleHeightChange}
              onSelectionChange={this.handleMessageSelectionChange}
              value={message}
            />
          </View>
          <View style={componentStyles.bottom}>
            <FloatingActionButton
              style={componentStyles.button}
              Icon={editMessage === null ? IconSend : IconDone}
              size={32}
              disabled={message.length === 0}
              onPress={editMessage === null ? this.handleSend : this.handleEdit}
            />
          </View>
        </View>
      </View>
    );
  }
}
<commit_msg>Rework on compose box height. (#1105)<commit_after>/* @flow */
import React, { PureComponent } from 'react';
import { StyleSheet, View, TextInput } from 'react-native';

import type { Auth, Narrow, EditMessage, InputSelectionType, User, Actions } from '../types';
import patchMessage from '../api/updateMessage';
import { FloatingActionButton, Input, MultilineInput } from '../common';
import { showErrorAlert } from '../common/errorAlert';
import { IconDone, IconSend } from '../common/Icons';
import { isStreamNarrow, topicNarrow } from '../utils/narrow';
import ComposeMenuContainer from './ComposeMenuContainer';
import AutoCompleteView from '../autocomplete/AutoCompleteView';
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
import { registerUserInputActivity } from '../utils/activity';
import { replaceEmoticonsWithEmoji } from '../emoji/emoticons';

const MIN_HEIGHT = 46;
const MAX_HEIGHT = 100;

const componentStyles = StyleSheet.create({
  bottom: {
    flexDirection: 'column',
    justifyContent: 'flex-end',
  },
  composeText: {
    flex: 1,
    flexDirection: 'column',
  },
  topic: {
    height: 30,
    backgroundColor: 'rgba(127, 127, 127, 0.25)',
  },
  button: {
    margin: 5,
  },
});

type Props = {
  auth: Auth,
  narrow: Narrow,
  users: User[],
  composeTools: boolean,
  editMessage: EditMessage,
  actions: Actions,
  messageInputRef: (component: Object) => void,
  onSend: () => void,
};

export default class ComposeBox extends PureComponent {
  topicInput = null;
  messageInput = null;

  messageInput: TextInput;
  topicInput: TextInput;
  props: Props;

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

  state: {
    optionSelected: number,
    topic: string,
    message: string,
    height: number,
    selection: InputSelectionType,
  };

  state = {
    optionSelected: 0,
    height: 46,
    topic: '',
    message: '',
    selection: { start: 0, end: 0 },
  };

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

  handleMessageChange = (message: string) => {
    this.setState({ message });
    const { auth } = this.props;
    registerUserInputActivity(auth);
  };

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

  handleHeightChange = (height: number) => {
    this.setState({ height });
  };

  clearMessageInput = () => {
    if (this.topicInput) {
      this.topicInput.clear();
    }
    if (this.messageInput) {
      this.messageInput.clear();
    }
    this.handleMessageChange('');
  };

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

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

    actions.addToOutbox(destinationNarrow, messageToSend);

    this.clearMessageInput();
    onSend();
  };

  handleEdit = () => {
    const { auth, editMessage, actions } = this.props;
    const { message } = this.state;
    if (editMessage.content !== message) {
      patchMessage(auth, replaceEmoticonsWithEmoji(message), editMessage.id).catch(error =>
        showErrorAlert(error.message, 'Failed to edit message'),
      );
    }
    actions.cancelEditMessage();
  };

  handleAutoComplete = (input: string) => {
    this.setState({ message: input });
  };

  handleChangeText = (input: string) => {
    this.setState({ message: input });
  };

  componentWillReceiveProps(nextProps: Props) {
    if (nextProps.editMessage !== this.props.editMessage) {
      this.setState({
        message: nextProps.editMessage ? nextProps.editMessage.content : '',
      });
      this.messageInput.focus();
    }
  }

  render() {
    const { styles } = this.context;
    const { height, message, selection, topic } = this.state;
    const { auth, composeTools, narrow, users, editMessage, messageInputRef } = this.props;

    const canSelectTopic = composeTools && isStreamNarrow(narrow);
    const messageHeight = Math.min(Math.max(MIN_HEIGHT, height), MAX_HEIGHT);
    const totalHeight = canSelectTopic ? messageHeight + 30 : messageHeight;
    const placeholder = getComposeInputPlaceholder(narrow, auth.email, users);

    return (
      <View>
        <AutoCompleteView
          text={message}
          onAutocomplete={this.handleAutoComplete}
          selection={selection}
        />
        <View style={[styles.composeBox, { height: totalHeight }]}>
          <View style={componentStyles.bottom}>
            <ComposeMenuContainer />
          </View>
          <View style={[componentStyles.composeText]}>
            {canSelectTopic &&
              <Input
                style={[styles.composeTextInput, componentStyles.topic]}
                underlineColorAndroid="transparent"
                placeholder="Topic"
                textInputRef={component => {
                  this.topicInput = component;
                }}
                onChangeText={this.handleTopicChange}
                value={topic}
              />}
            <MultilineInput
              style={[styles.composeTextInput, { height: messageHeight }]}
              placeholder={placeholder}
              textInputRef={component => {
                this.messageInput = component;
                if (component) messageInputRef(component);
              }}
              onChange={this.handleMessageChange}
              onHeightChange={this.handleHeightChange}
              onSelectionChange={this.handleMessageSelectionChange}
              value={message}
            />
          </View>
          <View style={componentStyles.bottom}>
            <FloatingActionButton
              style={componentStyles.button}
              Icon={editMessage === null ? IconSend : IconDone}
              size={32}
              disabled={message.length === 0}
              onPress={editMessage === null ? this.handleSend : this.handleEdit}
            />
          </View>
        </View>
      </View>
    );
  }
}
<|endoftext|>


<commit_before>/* File: helpers.js
 *
 * Copyright (c) 2014
 * Centre National d’Enseignement à Distance (Cned), Boulevard Nicephore Niepce, 86360 CHASSENEUIL-DU-POITOU, France
 * (direction-innovation@cned.fr)
 *
 * GNU Affero General Public License (AGPL) version 3.0 or later version
 *
 * This file is part of a program which is free software: you can
 * redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 */

'use strict';

var config = require('../../env/config.json');

var nodemailer = require('nodemailer');

var smtpTransport = nodemailer.createTransport('SMTP', {
	host: process.env.EMAIL_HOST || config.EMAIL_HOST, // hostname
	secureConnection: true, // use SSL
	port: 465, // port for secure SMTP
	auth: {
		user: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
		pass: process.env.EMAIL_HOST_PWD || config.EMAIL_HOST_PWD
	}
});

exports.sendMail = function(req, res) {
	var nodemailer = require('nodemailer');
	var sentMailInfos = req.body;
	// create reusable transport method (opens pool of SMTP connections)
	var smtpTransport = nodemailer.createTransport('SMTP', {
		host: process.env.EMAIL_HOST || config.EMAIL_HOST, // hostname
		secureConnection: true, // use SSL
		port: 465, // port for secure SMTP
		auth: {
			user: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
			pass: process.env.EMAIL_HOST_PWD || config.EMAIL_HOST_PWD
		}
	});

	// setup e-mail data with unicode symbols
	if (sentMailInfos.doc.indexOf('idProfil') != -1) {
		var mailOptions = {
			from: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
			to: sentMailInfos.to,
			subject: sentMailInfos.fullName + ' a partagé le lien de son profil avec vous',
			text: sentMailInfos.prenom + ' ' + sentMailInfos.content,
			html: sentMailInfos.prenom + ' ' + sentMailInfos.encoded
		};
	} else {
		var mailOptions = {
			from: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
			to: sentMailInfos.to,
			subject: sentMailInfos.fullName + ' a partagé ' + sentMailInfos.doc + ' avec vous',
			text: sentMailInfos.prenom + ' ' + sentMailInfos.content,
			html: sentMailInfos.prenom + ' ' + sentMailInfos.encoded
		};
	}

	// send mail with defined transport object
	smtpTransport.sendMail(mailOptions, function(error, response) {
		if (error) {
			throw error;
		} else {
			res.send(response);
		}

		// if you don't want to use this transport object anymore, uncomment following line
		//smtpTransport.close(); // shut down the connection pool, no more messages
	});
};
exports.passwordRestoreEmail = function(emailTo, subject, content) {
	var mailOptions = {
		from: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
		to: emailTo,
		subject: subject,
		text: '',
		html: content
	};
	smtpTransport.sendMail(mailOptions, function(error) {
		if (error) {
			return false;
		} else {
			return true;
		}
	});
};<commit_msg>Commit de test<commit_after>/* File: helpers.js
 *
 * Copyright (c) 2014
 * Centre National d’Enseignement à Distance (Cned), Boulevard Nicephore Niepce, 86360 CHASSENEUIL-DU-POITOU, France
 * (direction-innovation@cned.fr)
 *
 * GNU Affero General Public License (AGPL) version 3.0 or later version
 *
 * This file is part of a program which is free software: you can
 * redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 */

'use strict';

var config = require('../../env/config.json');

var nodemailer = require('nodemailer');

var smtpTransport = nodemailer.createTransport('SMTP', {
	host: process.env.EMAIL_HOST || config.EMAIL_HOST, // hostname
	secureConnection: true, // use SSL
	port: 465, // port for secure SMTP
	auth: {
		user: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
		pass: process.env.EMAIL_HOST_PWD || config.EMAIL_HOST_PWD
	}
});

exports.sendMail = function(req, res) {
	var nodemailer = require('nodemailer');
	var sentMailInfos = req.body;
	// create reusable transport method (opens pool of SMTP connections)
	var smtpTransport = nodemailer.createTransport('SMTP', {
		host: process.env.EMAIL_HOST || config.EMAIL_HOST, // hostname
		secureConnection: true, // use SSL
		port: 465, // port for secure SMTP
		auth: {
			user: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
			pass: process.env.EMAIL_HOST_PWD || config.EMAIL_HOST_PWD
		}
	});

	// setup e-mail data with unicode symbols
	if (sentMailInfos.doc.indexOf('idProfil') != -1) {
		var mailOptions = {
			from: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
			to: sentMailInfos.to,
			subject: sentMailInfos.fullName + ' a partagé le lien de son profil avec vous',
			text: sentMailInfos.prenom + ' ' + sentMailInfos.content,
			html: sentMailInfos.prenom + ' ' + sentMailInfos.encoded
		};
	} else {
		var mailOptions = {
			from: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
			to: sentMailInfos.to,
			subject: sentMailInfos.fullName + ' a partagé ' + sentMailInfos.doc + ' avec vous',
			text: sentMailInfos.prenom + ' ' + sentMailInfos.content,
			html: sentMailInfos.prenom + ' ' + sentMailInfos.encoded
		};
	}

	// send mail with defined transport object
	smtpTransport.sendMail(mailOptions, function(error, response) {
		if (error) {
			throw error;
		} else {
			res.send(response);
		}

		// if you don't want to use this transport object anymore, uncomment following line
		//smtpTransport.close(); // shut down the connection pool, no more messages
	});
};
exports.passwordRestoreEmail = function(emailTo, subject, content) {
	var mailOptions = {
		from: process.env.EMAIL_HOST_UID || config.EMAIL_HOST_UID,
		to: emailTo,
		subject: subject,
		text: '',
		html: content
	};
	smtpTransport.sendMail(mailOptions, function(error) {
		if (error) {
			return false;
		} else {
			return true;
		}
	});
};<|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, 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.handleMessageChange(nextProps.editMessage ? nextProps.editMessage.content : '');
      this.handleTopicChange(topic);
      if (this.messageInput) {
        this.messageInput.focus();
      }
    } else if (!isEqual(nextProps.narrow, this.props.narrow)) {
      this.tryUpdateDraft();

      if (!nextProps.draft) {
        this.clearMessageInput();
      }

      this.handleMessageChange(nextProps.draft);
    }
  }

  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>compose: Ensure topic gets set after message input focused.<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 { topic } = this.state;
    const { lastMessageTopic } = this.props;
    this.setState({
      isMessageFocused: true,
      isMenuExpanded: false,
    });
    setTimeout(() => {
      this.handleTopicChange(topic || lastMessageTopic);
    }, 200); // wait, to hope the component is shown
  };

  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.handleMessageChange(nextProps.editMessage ? nextProps.editMessage.content : '');
      this.handleTopicChange(topic);
      if (this.messageInput) {
        this.messageInput.focus();
      }
    } else if (!isEqual(nextProps.narrow, this.props.narrow)) {
      this.tryUpdateDraft();

      if (!nextProps.draft) {
        this.clearMessageInput();
      }

      this.handleMessageChange(nextProps.draft);
    }
  }

  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|>


<commit_before>module.exports = {
  forCommunities: function (opts) {
    return Community.query(qb => {
      if (opts.communities) {
        qb.whereIn('community.id', opts.communities)
      }

      if (opts.autocomplete) {
        qb.whereRaw('community.name ilike ?', opts.autocomplete + '%')
      }

      if (opts.term) {
        Search.addTermToQueryBuilder(opts.term, qb, {
          columns: ['community.name']
        })
      }

      // this counts total rows matching the criteria, disregarding limit,
      // which is useful for pagination
      qb.select(bookshelf.knex.raw('community.*, count(*) over () as total'))

      qb.limit(opts.limit)
      qb.offset(opts.offset)
      qb.groupBy('community.id')
      qb.orderBy('community.name', 'asc')
    })
  },

  forPosts: function (opts) {
    return Post.query(function (qb) {
      qb.limit(opts.limit || 20)
      qb.offset(opts.offset)
      qb.where({'post.active': true})

      // this counts total rows matching the criteria, disregarding limit,
      // which is useful for pagination
      qb.select(bookshelf.knex.raw('post.*, count(*) over () as total'))

      if (opts.users) {
        qb.whereIn('post.user_id', opts.users)
      }

      if (opts.excludeUsers) {
        qb.whereNotIn('post.user_id', opts.excludeUsers)
      }

      if (opts.tag) {
        qb.join('posts_tags', 'posts_tags.post_id', '=', 'post.id')
        qb.whereIn('posts_tags.tag_id', [opts.tag])
      }

      if (opts.term) {
        Search.addTermToQueryBuilder(opts.term, qb, {
          columns: ['post.name', 'post.description']
        })
      }

      if (opts.follower) {
        qb.join('follower', 'follower.post_id', '=', 'post.id')
        qb.where('follower.user_id', opts.follower)
        qb.whereRaw('(post.user_id != ? or post.user_id is null)', opts.follower)
      }

      if (!opts.type || opts.type === 'all') {
        qb.where(function () {
          this.where('post.type', '!=', 'welcome')
          .orWhere('post.type', null)
        })
      } else if (opts.type !== 'all+welcome') {
        qb.where({type: opts.type})
      }

      if (opts.type === 'event' && opts.filter === 'future') {
        qb.whereRaw('(post.start_time > now())')
      }

      if (opts.type === 'project' && opts.filter === 'mine') {
        qb.leftJoin('follower', 'post.id', 'follower.post_id')
        qb.where(function () {
          this.where('post.user_id', opts.currentUserId)
          .orWhere('follower.user_id', opts.currentUserId)
        })
      }

      if (opts.start_time && opts.end_time) {
        qb.whereRaw('((post.created_at between ? and ?) or (post.updated_at between ? and ?))',
          [opts.start_time, opts.end_time, opts.start_time, opts.end_time])
      }

      if (opts.visibility) {
        qb.whereIn('visibility', opts.visibility)
      }

      if (opts.sort === 'suggested') {
        qb.join('user_post_relevance', 'user_post_relevance.post_id', '=', 'post.id')
        qb.where('user_post_relevance.user_id', opts.forUser)
        qb.orderBy('similarity', 'desc')
        qb.groupBy('similarity')
      } else if (opts.sort === 'fulfilled_at') {
        qb.orderByRaw('post.fulfilled_at desc, post.updated_at desc')
      } else if (Array.isArray(opts.sort)) {
        qb.orderBy(opts.sort[0], opts.sort[1])
      } else if (opts.sort) {
        qb.orderBy(opts.sort, 'desc')
      }

      if (opts.omit) {
        qb.whereNotIn('post.id', opts.omit)
      }

      if (opts.communities) {
        qb.join('post_community', 'post_community.post_id', '=', 'post.id')
        qb.whereIn('post_community.community_id', opts.communities)
        qb.groupBy(['post.id', 'post_community.post_id'])
      }

      if (!opts.includeChildren) {
        qb.where('parent_post_id', null)
      }
    })
  },

  forUsers: function (opts) {
    return User.query(function (qb) {
      qb.limit(opts.limit || 1000)
      qb.offset(opts.offset || 0)
      qb.where('users.active', '=', true)

      // this is not necessarily what any consumer desires, but
      // some ordering must be specified for pagination
      qb.orderBy('name', 'asc')

      // this counts total rows matching the criteria, disregarding limit,
      // which is useful for pagination
      qb.select(bookshelf.knex.raw('users.*, count(users.*) over () as total'))

      if (opts.communities) {
        qb.join('users_community', 'users_community.user_id', '=', 'users.id')
        qb.whereIn('users_community.community_id', opts.communities)
        qb.where('users_community.active', true)
      }

      if (opts.autocomplete) {
        qb.whereRaw('users.name ilike ?', opts.autocomplete + '%')
      }

      if (opts.term) {
        Search.addTermToQueryBuilder(opts.term, qb, {
          columns: ['users.name', 'users.bio']
        })
      }

      // prevent duplicates due to the joins
      qb.groupBy('users.id')

      if (opts.start_time && opts.end_time) {
        qb.whereRaw('users.created_at between ? and ?', [opts.start_time, opts.end_time])
      }

      if (opts.exclude) {
        qb.whereNotIn('id', opts.exclude)
      }
    })
  },

  forTags: function (opts) {
    return Tag.query(q => {
      if (opts.communities) {
        q.join('communities_tags', 'communities_tags.tag_id', '=', 'tags.id')
        q.whereIn('communities_tags.community_id', opts.communities)
      }
      if (opts.autocomplete) {
        q.whereRaw('tags.name ilike ?', opts.autocomplete + '%')
      }

      q.groupBy('tags.id')
      q.limit(opts.limit)
    })
  },

  addTermToQueryBuilder: function (term, qb, opts) {
    var query = _.chain(term.split(/\s*\s/)) // split on whitespace
    .map(word => word.replace(/[,;|:&()!\\]+/, ''))
    .reject(_.isEmpty)
    .map(word => word + ':*') // add prefix matching
    .reduce((result, word) => {
      // build the tsquery string using logical AND operands
      result += ' & ' + word
      return result
    }).value()

    var statement = format('(%s)',
      opts.columns
      .map(col => format("(to_tsvector('english', %s) @@ to_tsquery(?))", col))
      .join(' or '))

    var values = _.times(opts.columns.length, () => query)

    qb.where(function () {
      this.whereRaw(statement, values)
    })
  }
}
<commit_msg>include tags in user searches<commit_after>module.exports = {
  forCommunities: function (opts) {
    return Community.query(qb => {
      if (opts.communities) {
        qb.whereIn('community.id', opts.communities)
      }

      if (opts.autocomplete) {
        qb.whereRaw('community.name ilike ?', opts.autocomplete + '%')
      }

      if (opts.term) {
        Search.addTermToQueryBuilder(opts.term, qb, {
          columns: ['community.name']
        })
      }

      // this counts total rows matching the criteria, disregarding limit,
      // which is useful for pagination
      qb.select(bookshelf.knex.raw('community.*, count(*) over () as total'))

      qb.limit(opts.limit)
      qb.offset(opts.offset)
      qb.groupBy('community.id')
      qb.orderBy('community.name', 'asc')
    })
  },

  forPosts: function (opts) {
    return Post.query(function (qb) {
      qb.limit(opts.limit || 20)
      qb.offset(opts.offset)
      qb.where({'post.active': true})

      // this counts total rows matching the criteria, disregarding limit,
      // which is useful for pagination
      qb.select(bookshelf.knex.raw('post.*, count(*) over () as total'))

      if (opts.users) {
        qb.whereIn('post.user_id', opts.users)
      }

      if (opts.excludeUsers) {
        qb.whereNotIn('post.user_id', opts.excludeUsers)
      }

      if (opts.tag) {
        qb.join('posts_tags', 'posts_tags.post_id', '=', 'post.id')
        qb.whereIn('posts_tags.tag_id', [opts.tag])
      }

      if (opts.term) {
        Search.addTermToQueryBuilder(opts.term, qb, {
          columns: ['post.name', 'post.description']
        })
      }

      if (opts.follower) {
        qb.join('follower', 'follower.post_id', '=', 'post.id')
        qb.where('follower.user_id', opts.follower)
        qb.whereRaw('(post.user_id != ? or post.user_id is null)', opts.follower)
      }

      if (!opts.type || opts.type === 'all') {
        qb.where(function () {
          this.where('post.type', '!=', 'welcome')
          .orWhere('post.type', null)
        })
      } else if (opts.type !== 'all+welcome') {
        qb.where({type: opts.type})
      }

      if (opts.type === 'event' && opts.filter === 'future') {
        qb.whereRaw('(post.start_time > now())')
      }

      if (opts.type === 'project' && opts.filter === 'mine') {
        qb.leftJoin('follower', 'post.id', 'follower.post_id')
        qb.where(function () {
          this.where('post.user_id', opts.currentUserId)
          .orWhere('follower.user_id', opts.currentUserId)
        })
      }

      if (opts.start_time && opts.end_time) {
        qb.whereRaw('((post.created_at between ? and ?) or (post.updated_at between ? and ?))',
          [opts.start_time, opts.end_time, opts.start_time, opts.end_time])
      }

      if (opts.visibility) {
        qb.whereIn('visibility', opts.visibility)
      }

      if (opts.sort === 'suggested') {
        qb.join('user_post_relevance', 'user_post_relevance.post_id', '=', 'post.id')
        qb.where('user_post_relevance.user_id', opts.forUser)
        qb.orderBy('similarity', 'desc')
        qb.groupBy('similarity')
      } else if (opts.sort === 'fulfilled_at') {
        qb.orderByRaw('post.fulfilled_at desc, post.updated_at desc')
      } else if (Array.isArray(opts.sort)) {
        qb.orderBy(opts.sort[0], opts.sort[1])
      } else if (opts.sort) {
        qb.orderBy(opts.sort, 'desc')
      }

      if (opts.omit) {
        qb.whereNotIn('post.id', opts.omit)
      }

      if (opts.communities) {
        qb.join('post_community', 'post_community.post_id', '=', 'post.id')
        qb.whereIn('post_community.community_id', opts.communities)
        qb.groupBy(['post.id', 'post_community.post_id'])
      }

      if (!opts.includeChildren) {
        qb.where('parent_post_id', null)
      }
    })
  },

  forUsers: function (opts) {
    return User.query(function (qb) {
      qb.limit(opts.limit || 1000)
      qb.offset(opts.offset || 0)
      qb.where('users.active', '=', true)

      // this is not necessarily what any consumer desires, but
      // some ordering must be specified for pagination
      qb.orderBy('name', 'asc')

      // this counts total rows matching the criteria, disregarding limit,
      // which is useful for pagination
      qb.select(bookshelf.knex.raw('users.*, count(users.*) over () as total'))

      if (opts.communities) {
        qb.join('users_community', 'users_community.user_id', 'users.id')
        qb.whereIn('users_community.community_id', opts.communities)
        qb.where('users_community.active', true)
      }

      if (opts.autocomplete) {
        qb.whereRaw('users.name ilike ?', opts.autocomplete + '%')
      }

      if (opts.term) {
        qb.leftJoin('tags_users', 'tags_users.user_id', 'users.id')
        qb.leftJoin('tags', 'tags.id', 'tags_users.tag_id')
        Search.addTermToQueryBuilder(opts.term, qb, {
          columns: ['users.name', 'users.bio', 'tags.name']
        })
      }

      // prevent duplicates due to the joins
      qb.groupBy('users.id')

      if (opts.start_time && opts.end_time) {
        qb.whereRaw('users.created_at between ? and ?', [opts.start_time, opts.end_time])
      }

      if (opts.exclude) {
        qb.whereNotIn('id', opts.exclude)
      }
    })
  },

  forTags: function (opts) {
    return Tag.query(q => {
      if (opts.communities) {
        q.join('communities_tags', 'communities_tags.tag_id', '=', 'tags.id')
        q.whereIn('communities_tags.community_id', opts.communities)
      }
      if (opts.autocomplete) {
        q.whereRaw('tags.name ilike ?', opts.autocomplete + '%')
      }

      q.groupBy('tags.id')
      q.limit(opts.limit)
    })
  },

  addTermToQueryBuilder: function (term, qb, opts) {
    var query = _.chain(term.split(/\s*\s/)) // split on whitespace
    .map(word => word.replace(/[,;|:&()!\\]+/, ''))
    .reject(_.isEmpty)
    .map(word => word + ':*') // add prefix matching
    .reduce((result, word) => {
      // build the tsquery string using logical AND operands
      result += ' & ' + word
      return result
    }).value()

    var statement = format('(%s)',
      opts.columns
      .map(col => format("(to_tsvector('english', %s) @@ to_tsquery(?))", col))
      .join(' or '))

    var values = _.times(opts.columns.length, () => query)

    qb.where(function () {
      this.whereRaw(statement, values)
    })
  }
}
<|endoftext|>


<commit_before>/**
 * SidePane functional tests
 */
define(["intern!object",
	"intern/chai!assert",
	"require"
], function (registerSuite, assert, require) {
	registerSuite({
		name: "SidePane",
		"init": function () {
			var remote = this.remote;
			return loadTestPage(remote, "./SidePane.html").wait(50);
		},
		"test initial state": function () {
			var remote = this.remote;
			var element = remote.elementById("sp");
			var test = element.getAttribute("class").then(function (classString) {
				checkCssClasses(classString, "d-side-pane", "-d-side-pane-push", "-d-side-pane-start",
					"-d-side-pane-hidden", "-d-side-pane-animate");
			});
			test = test.then(isVisible(element, false));

			return test;
		// Interactions tests are broken on Mac and iOS simulators. See #25
		}/*,
		"test opening": function () {
			return this.remote.elementById("showButton").click().end()
				.wait(800)
				.then(isVisible(this.remote.elementById("sp"), true));
		},
		"test closing": function () {
			return this.remote.elementById("hideButton").click().end()
				.wait(800)
				.then(isVisible(this.remote.elementById("sp"), false));
		},
		"test swipe closing": function () {
			return this.remote.elementById("showButton").click().end().wait(800)
				.elementById("page").moveTo(30, 300).buttonDown().moveTo(10, 300)
				.wait(800)
				.then(isVisible(this.remote.elementById("sp"), false));
		}*/
	});

	function checkCssClasses(classString, args) {
		args = Array.prototype.slice.call(arguments);
		args.shift();
		var nodeClasses = classString.split(" ");
		var cls;
		while (args.length > 0) {
			cls = args.shift();
			assert.isTrue(nodeClasses.indexOf(cls) !== -1,
					"SidePane should contains the class " + cls);
		}
	}

	function isVisible(element, v) {
		var errMsg = "SidePane should be" + (v ? " ":" not ") + "visible";
		element.getComputedCss("display").then(function (value) {
			assert.isTrue(value === (v ? "block" : "none"), errMsg);
		});
		element.getComputedCss("visibility").then(function (value) {
			assert.isTrue(value === (v ? "visible" : "hidden"), errMsg);
		});
		element.getAttribute("class").then(function (classString) {
			checkCssClasses(classString, v ? "-d-side-pane-visible" : "-d-side-pane-hidden");
		});
	}

	function loadTestPage(remote, url) {
		return remote
			.get(require.toUrl(url))
			.waitForCondition("'ready' in window &&  ready", 10000, 1000)
			.then(function () {
				return remote.end();
			});
	}
});
<commit_msg>SidePane func. tests filter for Mac/iOS, refs #25<commit_after>/**
 * SidePane functional tests
 */
define(["intern!object",
	"intern/chai!assert",
	"require"
], function (registerSuite, assert, require) {
	registerSuite({
		name: "SidePane",
		"init": function () {
			var remote = this.remote;
			return loadTestPage(remote, "./SidePane.html").wait(50);
		},
		"test initial state": function () {
			var remote = this.remote;
			var element = remote.elementById("sp");
			var test = element.getAttribute("class").then(function (classString) {
				checkCssClasses(classString, "d-side-pane", "-d-side-pane-push", "-d-side-pane-start",
					"-d-side-pane-hidden", "-d-side-pane-animate");
			});
			test = test.then(isVisible(element, false));

			return test;
		// Interactions tests are broken on Mac and iOS simulators. See #25
		},
		"test opening": function () {
			if (/safari|iPhone|iPad/.test(this.remote.environmentType.browserName)
				|| this.remote.environmentType.safari) {
				console.log("Skipping test '" + this.parent.name + ": " + this.name + "' on this platform");
				return;
			}
			return this.remote.elementById("showButton").click().end()
				.wait(800)
				.then(isVisible(this.remote.elementById("sp"), true));
		},
		"test closing": function () {
			if (/safari|iPhone|iPad/.test(this.remote.environmentType.browserName)
				|| this.remote.environmentType.safari) {
				console.log("Skipping test '" + this.parent.name + ": " + this.name + "' on this platform");
				return;
			}
			return this.remote.elementById("hideButton").click().end()
				.wait(800)
				.then(isVisible(this.remote.elementById("sp"), false));
		},
		"test swipe closing": function () {
			if (/safari|iPhone|iPad/.test(this.remote.environmentType.browserName)
				|| this.remote.environmentType.safari) {
				console.log("Skipping test '" + this.parent.name + ": " + this.name + "' on this platform");
				return;
			}
			return this.remote.elementById("showButton").click().end().wait(800)
				.elementById("page").moveTo(30, 300).buttonDown().moveTo(10, 300)
				.wait(800)
				.then(isVisible(this.remote.elementById("sp"), false));
		}
	});

	function checkCssClasses(classString, args) {
		args = Array.prototype.slice.call(arguments);
		args.shift();
		var nodeClasses = classString.split(" ");
		var cls;
		while (args.length > 0) {
			cls = args.shift();
			assert.isTrue(nodeClasses.indexOf(cls) !== -1,
					"SidePane should contains the class " + cls);
		}
	}

	function isVisible(element, v) {
		var errMsg = "SidePane should be" + (v ? " ":" not ") + "visible";
		element.getComputedCss("display").then(function (value) {
			assert.isTrue(value === (v ? "block" : "none"), errMsg);
		});
		element.getComputedCss("visibility").then(function (value) {
			assert.isTrue(value === (v ? "visible" : "hidden"), errMsg);
		});
		element.getAttribute("class").then(function (classString) {
			checkCssClasses(classString, v ? "-d-side-pane-visible" : "-d-side-pane-hidden");
		});
	}

	function loadTestPage(remote, url) {
		return remote
			.get(require.toUrl(url))
			.waitForCondition("'ready' in window &&  ready", 10000, 1000)
			.then(function () {
				return remote.end();
			});
	}
});
<|endoftext|>


<commit_before>/* File: images.js
 *
 * Copyright (c) 2014
 * Centre National d’Enseignement à Distance (Cned), Boulevard Nicephore Niepce, 86360 CHASSENEUIL-DU-POITOU, France
 * (direction-innovation@cned.fr)
 *
 * GNU Affero General Public License (AGPL) version 3.0 or later version
 *
 * This file is part of a program which is free software: you can
 * redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 */

'use strict';

// Require helpers
var helper = require('../helpers/helpers');

//
// var docStructureDao = require('../dao/docStructure');

var numberCalls = 0;
var sourcesUpload = [];
var counter = 0;

/**
 * Crop Image
 */
exports.cropImage = function(req, res) {

	var extension = helper.getFileExtension(req.body.DataCrop.srcImg);
	var source = req.body.DataCrop.srcImg;
	var targetImage = './files/decoup.thumb_' + Math.random() + extension;

	/* Crop image with ImageMagick */
	var exec = require('child_process').exec;
	exec('convert ' + source + ' +repage -density 450 -quality 100 -crop ' + req.body.DataCrop.w + 'x' + req.body.DataCrop.h + '+' + req.body.DataCrop.x + '+' + req.body.DataCrop.y + ' ' + targetImage, function(err, stdout, stderr) {
		if (err) {
			throw err;
		} else {
			return res.jsonp({
				source: targetImage,
				order: req.body.DataCrop.order
			});
		}
	});
};

function imageToBase64(url) {
	var fs = require('fs');
	var bitmap = fs.readFileSync(url);
	return new Buffer(bitmap).toString('base64');
}

/* Based on node-teseract module*/
exports.oceriser = function(req, res) {

	var exec = require('child_process').exec;
	var fs = require('fs');
	var crypto = require('crypto');
	var date = new Date().getTime();

	//Replace encodedImg value by base64image
	var base64Str = req.body.encodedImg.replace('data:image/png;base64,', '');

	//image with unique hashed name
	var createdImg = 'img_' + crypto.createHash('md5').update(base64Str + date).digest('hex');
	var fullImgPath = './files/' + createdImg + '.png';

	//create PNG image from base64 string
	fs.writeFile(fullImgPath, new Buffer(base64Str, 'base64'), function() {});

	//Output a JPEG image
	var output = './files/out_' + crypto.createHash('md5').update(base64Str + date).digest('hex') + '.jpg';

	//convert created PNG image to high quality JPEG image
	exec('convert ' + fullImgPath + ' -geometry 4000x5000 -density 300x300 -quality 80 -units PixelsPerInch -depth 8 -background white -type truecolor -define jpeg:extent=1000kb ' + output, function(err) {

		fs.exists(output, function(exists) {
			if (exists) {
				return 'File is there';
			} else {
				return 'File is not there';
			}
			return 'error';
		});

		if (err) {
			throw err;
		}

		//Run tesseract-ocr
		exec('tesseract ' + output + ' ' + output + ' -l fra', function(errTess) {
			if (errTess) {
				throw errTess;
			}
			fs.readFile(output + '.txt', function(err, data) {
				if (err) throw err;

				var text = data.toString('utf8');
				res.jsonp(text);
				//remove text file
				fs.unlink(output + '.txt', function(err) {
					if (err) throw err;
					//remove JPEG image
					fs.unlink(output, function(err) {
						if (err) {
							throw err;
						}
						//remove PNG image
						fs.unlink(fullImgPath, function(err) {
							if (err) {
								throw err;
							}

						});
					});
				});
			});
		});
	});

};

/* Upload Files */
exports.uploadFiles = function(req, res) {

	var fs = require('fs');
	var filesToUpload = [];
	sourcesUpload = [];
	counter = 0;

	if (!req.files.uploadedFile.length) {
		filesToUpload.push(req.files.uploadedFile);
		numberCalls = 1;
	} else {
		for (var i = 0; i < req.files.uploadedFile.length; i++) {
			filesToUpload.push(req.files.uploadedFile[i]);
		}
		numberCalls = filesToUpload.length;
	}

	// parcourir la liste des fichiers a uploader
	for (var k = 0; k < filesToUpload.length; k++) {

		var currentFile = filesToUpload[k];
		// Detect file type
		var extension = helper.getFileExtension(filesToUpload[k].originalFilename);

		var newPath = './files/file-' + Math.random() + extension;

		// Ouvrir et ecrire les fichier uploadés de meniere synchronous
		var fileReaded = fs.readFileSync(filesToUpload[k].path);
		var fileWrited = fs.writeFileSync(newPath, fileReaded);
		if (extension === '.pdf') {
			// (if PDF convert to JPEGs)
			/*return number pages of PDF*/
			exports.getNumberPagesPDF(newPath, extension, res);
			// exports.convertsPdfToPng(newPath, res);
		} else if (extension === '.jpg' || extension === '.jpeg') {
			// (if PDF convert to JPEGs)
			exports.convertsJpegToPng(newPath, res);
		} else {
			sourcesUpload.push({
				path: newPath,
				extension: extension
			});
			counter += 1;
			if (numberCalls === counter) {
				return res.jsonp(sourcesUpload);
			}
		}
	}
};

/* Get number pages of PDF */
exports.getNumberPagesPDF = function(filePath, extension, res) {
	var exec = require('child_process').exec;
	exec('gs  -c "(' + filePath + ') (r) file runpdfbegin pdfpagecount = quit"', function(error, stdout) {
		if (error) {
			return error;
		}
		var numbPages = stdout.split('\n')[3];
		sourcesUpload.push({
			path: filePath,
			extension: extension,
			numberPages: numbPages
		});
		res.jsonp(sourcesUpload);
	});
};

/* Convert PDF to JPEG */
exports.convertsPdfToPng = function(req, res) {

	var exec = require('child_process').exec;
	var imageFileName = req.body.pdfData.source.substr(0, req.body.pdfData.source.lastIndexOf('.')) + Math.random();
	var imageConverted = imageFileName + '[' + req.body.pdfData.page + ']' + '.png';

	// Render image with imagemagick
	exec('convert -geometry 892x1263 -density 450 -quality 100 ' + req.body.pdfData.source + '[' + req.body.pdfData.page + '] -background white -alpha remove -alpha off  ' + imageConverted, function(error) {
		if (error !== null) {
			throw error;
		} else {

			return res.jsonp({
				path: imageToBase64(imageConverted),
				extension: '.png'
			});

			// Get converted files by Command
			/*exec('ls files | grep  ' + imageFileName.substr(8, imageFileName.length), function(errorls, stdoutls) {

				if (errorls !== null) {
					console.log(errorls);
					return 'error';
				}

				var files = stdoutls.replace(/\n/g, ' ').split(' ');
				for (var i = 0; i < files.length; i++) {
					if (files[i] !== '') {
						sourcesUpload.push('./files/' + files[i]);
					}
				}
				counter += 1;
				if (numberCalls === counter) {
					return res.jsonp(sourcesUpload);
				}

			});*/
		}
	});
};

exports.convertsJpegToPng = function(source, res) {

	var exec = require('child_process').exec;
	var imageFileName = source.substr(0, source.lastIndexOf('.')) + Math.random();

	// Render image with imagemagick
	exec('convert ' + source + ' ' + imageFileName + '.png', function(error) {
		if (error !== null) {
			throw error;
		} else {
			// console.log('[Done] Conversion from PDF to JPEG image' + imageFileName + '.jpg');
			//
			sourcesUpload.push({
				path: imageFileName + '.png',
				extension: '.png'
			});
			counter += 1;
			if (numberCalls === counter) {
				return res.jsonp(sourcesUpload);
			}
		}
	});
};

/*Text to speech*/
exports.textToSpeech = function(req, res) {
	var exec = require('child_process').exec;

	var fileName = './files/audio/mp3/audio_' + Math.random() + '.mp3';

	var tmpStr = req.body.text;

	// text to speech using espeak API
	exec('espeak -v french -s 110 "' + tmpStr + '" && espeak -v french -s 110 "' + tmpStr + '" --stdout | lame - ' + fileName, function(error) {
		if (error !== null) {
			throw error;
		} else {
			res.jsonp(fileName);
		}

	});
};

/*Festival Demo Text to speech*/
exports.festivalTextToSpeech = function(req, res) {
	var exec = require('child_process').exec;

	var tmpStr = req.body.texte;

	exec('echo \'' + tmpStr + '\'  | festival --tts ', function(error) {
		if (error !== null) {
			throw error;
		} else {
			res.jsonp('[Done] festival');
		}

	});
};

/*Espeak Demo Text to speech*/
exports.espeakTextToSpeech = function(req, res) {
	var exec = require('child_process').exec;

	var tmpStr = req.body.text;

	// text to speech using espeak API
	exec('espeak -v french \'' + tmpStr + '\' ', function(error) {
		if (error !== null) {
			throw error;
		} else {
			res.jsonp(200);
			//console.log('[Done] espeak');
		}

	});
};
var http = require('http');

exports.sendPdf = function(req, responce) {
	var donneRecu = req.body;

	var url = donneRecu['lien']; // jshint ignore:line
	http.get(url, function(res) {
		var chunks = [];
		res.on('data', function(chunk) {

			chunks.push(chunk);
		});
		res.on('end', function() {
			var jsfile = new Buffer.concat(chunks).toString('base64');
			// var jsfile = Buffer.concat(chunks);

			responce.header('Access-Control-Allow-Origin', '*');
			responce.header('Access-Control-Allow-Headers', 'X-Requested-With');
			responce.header('content-type', 'application/pdf');
			responce.send(200,jsfile);
		});
	}).on('error', function() {
		responce.jsonp(404, null);
	});
};
var https = require('https');
exports.sendPdfHTTPS = function(req, responce) {
	var donneRecu = req.body;
	var url = donneRecu['lien']; // jshint ignore:line
	https.get(url, function(res) {
		var chunks = [];
		res.on('data', function(chunk) {

			chunks.push(chunk);
		});
		res.on('end', function() {
			var jsfile = new Buffer.concat(chunks).toString('base64');
			// var jsfile = Buffer.concat(chunks);

			responce.header('Access-Control-Allow-Origin', '*');
			responce.header('Access-Control-Allow-Headers', 'X-Requested-With');
			responce.header('content-type', 'application/pdf');
			responce.send(jsfile);
		});
	}).on('error', function() {
		responce.jsonp(404, null);
	});
};<commit_msg>Modifier textToSpeech pour retourner mp3 format base64.<commit_after>/* File: images.js
 *
 * Copyright (c) 2014
 * Centre National d’Enseignement à Distance (Cned), Boulevard Nicephore Niepce, 86360 CHASSENEUIL-DU-POITOU, France
 * (direction-innovation@cned.fr)
 *
 * GNU Affero General Public License (AGPL) version 3.0 or later version
 *
 * This file is part of a program which is free software: you can
 * redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 */

'use strict';

// Require helpers
var helper = require('../helpers/helpers');

//
// var docStructureDao = require('../dao/docStructure');

var numberCalls = 0;
var sourcesUpload = [];
var counter = 0;

/**
 * Crop Image
 */
exports.cropImage = function(req, res) {

	var extension = helper.getFileExtension(req.body.DataCrop.srcImg);
	var source = req.body.DataCrop.srcImg;
	var targetImage = './files/decoup.thumb_' + Math.random() + extension;

	/* Crop image with ImageMagick */
	var exec = require('child_process').exec;
	exec('convert ' + source + ' +repage -density 450 -quality 100 -crop ' + req.body.DataCrop.w + 'x' + req.body.DataCrop.h + '+' + req.body.DataCrop.x + '+' + req.body.DataCrop.y + ' ' + targetImage, function(err, stdout, stderr) {
		if (err) {
			throw err;
		} else {
			return res.jsonp({
				source: targetImage,
				order: req.body.DataCrop.order
			});
		}
	});
};

function imageToBase64(url) {
	var fs = require('fs');
	var bitmap = fs.readFileSync(url);
	return new Buffer(bitmap).toString('base64');
}

/* Based on node-teseract module*/
exports.oceriser = function(req, res) {

	var exec = require('child_process').exec;
	var fs = require('fs');
	var crypto = require('crypto');
	var date = new Date().getTime();

	//Replace encodedImg value by base64image
	var base64Str = req.body.encodedImg.replace('data:image/png;base64,', '');

	//image with unique hashed name
	var createdImg = 'img_' + crypto.createHash('md5').update(base64Str + date).digest('hex');
	var fullImgPath = './files/' + createdImg + '.png';

	//create PNG image from base64 string
	fs.writeFile(fullImgPath, new Buffer(base64Str, 'base64'), function() {});

	//Output a JPEG image
	var output = './files/out_' + crypto.createHash('md5').update(base64Str + date).digest('hex') + '.jpg';

	//convert created PNG image to high quality JPEG image
	exec('convert ' + fullImgPath + ' -geometry 4000x5000 -density 300x300 -quality 80 -units PixelsPerInch -depth 8 -background white -type truecolor -define jpeg:extent=1000kb ' + output, function(err) {

		fs.exists(output, function(exists) {
			if (exists) {
				return 'File is there';
			} else {
				return 'File is not there';
			}
			return 'error';
		});

		if (err) {
			throw err;
		}

		//Run tesseract-ocr
		exec('tesseract ' + output + ' ' + output + ' -l fra', function(errTess) {
			if (errTess) {
				throw errTess;
			}
			fs.readFile(output + '.txt', function(err, data) {
				if (err) throw err;

				var text = data.toString('utf8');
				res.jsonp(text);
				//remove text file
				fs.unlink(output + '.txt', function(err) {
					if (err) throw err;
					//remove JPEG image
					fs.unlink(output, function(err) {
						if (err) {
							throw err;
						}
						//remove PNG image
						fs.unlink(fullImgPath, function(err) {
							if (err) {
								throw err;
							}

						});
					});
				});
			});
		});
	});

};

/* Upload Files */
exports.uploadFiles = function(req, res) {

	var fs = require('fs');
	var filesToUpload = [];
	sourcesUpload = [];
	counter = 0;

	if (!req.files.uploadedFile.length) {
		filesToUpload.push(req.files.uploadedFile);
		numberCalls = 1;
	} else {
		for (var i = 0; i < req.files.uploadedFile.length; i++) {
			filesToUpload.push(req.files.uploadedFile[i]);
		}
		numberCalls = filesToUpload.length;
	}

	// parcourir la liste des fichiers a uploader
	for (var k = 0; k < filesToUpload.length; k++) {

		var currentFile = filesToUpload[k];
		// Detect file type
		var extension = helper.getFileExtension(filesToUpload[k].originalFilename);

		var newPath = './files/file-' + Math.random() + extension;

		// Ouvrir et ecrire les fichier uploadés de meniere synchronous
		var fileReaded = fs.readFileSync(filesToUpload[k].path);
		var fileWrited = fs.writeFileSync(newPath, fileReaded);
		if (extension === '.pdf') {
			// (if PDF convert to JPEGs)
			/*return number pages of PDF*/
			exports.getNumberPagesPDF(newPath, extension, res);
			// exports.convertsPdfToPng(newPath, res);
		} else if (extension === '.jpg' || extension === '.jpeg') {
			// (if PDF convert to JPEGs)
			exports.convertsJpegToPng(newPath, res);
		} else {
			sourcesUpload.push({
				path: newPath,
				extension: extension
			});
			counter += 1;
			if (numberCalls === counter) {
				return res.jsonp(sourcesUpload);
			}
		}
	}
};

/* Get number pages of PDF */
exports.getNumberPagesPDF = function(filePath, extension, res) {
	var exec = require('child_process').exec;
	exec('gs  -c "(' + filePath + ') (r) file runpdfbegin pdfpagecount = quit"', function(error, stdout) {
		if (error) {
			return error;
		}
		var numbPages = stdout.split('\n')[3];
		sourcesUpload.push({
			path: filePath,
			extension: extension,
			numberPages: numbPages
		});
		res.jsonp(sourcesUpload);
	});
};

/* Convert PDF to JPEG */
exports.convertsPdfToPng = function(req, res) {

	var exec = require('child_process').exec;
	var imageFileName = req.body.pdfData.source.substr(0, req.body.pdfData.source.lastIndexOf('.')) + Math.random();
	var imageConverted = imageFileName + '[' + req.body.pdfData.page + ']' + '.png';

	// Render image with imagemagick
	exec('convert -geometry 892x1263 -density 450 -quality 100 ' + req.body.pdfData.source + '[' + req.body.pdfData.page + '] -background white -alpha remove -alpha off  ' + imageConverted, function(error) {
		if (error !== null) {
			throw error;
		} else {

			return res.jsonp({
				path: imageToBase64(imageConverted),
				extension: '.png'
			});

			// Get converted files by Command
			/*exec('ls files | grep  ' + imageFileName.substr(8, imageFileName.length), function(errorls, stdoutls) {

				if (errorls !== null) {
					console.log(errorls);
					return 'error';
				}

				var files = stdoutls.replace(/\n/g, ' ').split(' ');
				for (var i = 0; i < files.length; i++) {
					if (files[i] !== '') {
						sourcesUpload.push('./files/' + files[i]);
					}
				}
				counter += 1;
				if (numberCalls === counter) {
					return res.jsonp(sourcesUpload);
				}

			});*/
		}
	});
};

exports.convertsJpegToPng = function(source, res) {

	var exec = require('child_process').exec;
	var imageFileName = source.substr(0, source.lastIndexOf('.')) + Math.random();

	// Render image with imagemagick
	exec('convert ' + source + ' ' + imageFileName + '.png', function(error) {
		if (error !== null) {
			throw error;
		} else {
			// console.log('[Done] Conversion from PDF to JPEG image' + imageFileName + '.jpg');
			//
			sourcesUpload.push({
				path: imageFileName + '.png',
				extension: '.png'
			});
			counter += 1;
			if (numberCalls === counter) {
				return res.jsonp(sourcesUpload);
			}
		}
	});
};

/*Text to speech*/
exports.textToSpeech = function(req, res) {
	var exec = require('child_process').exec;

	var fileName = './files/audio/mp3/audio_' + Math.random() + '.mp3';

	var tmpStr = req.body.text;

	// text to speech using espeak API
	exec('espeak -v french -s 110 "' + tmpStr + '" && espeak -v french -s 110 "' + tmpStr + '" --stdout | lame - ' + fileName, function(error) {
		if (error !== null) {
			throw error;
		} else {
			res.jsonp(imageToBase64(fileName));
		}

	});
};

/*Festival Demo Text to speech*/
exports.festivalTextToSpeech = function(req, res) {
	var exec = require('child_process').exec;

	var tmpStr = req.body.texte;

	exec('echo \'' + tmpStr + '\'  | festival --tts ', function(error) {
		if (error !== null) {
			throw error;
		} else {
			res.jsonp('[Done] festival');
		}

	});
};

/*Espeak Demo Text to speech*/
exports.espeakTextToSpeech = function(req, res) {
	var exec = require('child_process').exec;

	var tmpStr = req.body.text;

	// text to speech using espeak API
	exec('espeak -v french \'' + tmpStr + '\' ', function(error) {
		if (error !== null) {
			throw error;
		} else {
			res.jsonp(200);
			//console.log('[Done] espeak');
		}

	});
};
var http = require('http');

exports.sendPdf = function(req, responce) {
	var donneRecu = req.body;

	var url = donneRecu['lien']; // jshint ignore:line
	http.get(url, function(res) {
		var chunks = [];
		res.on('data', function(chunk) {

			chunks.push(chunk);
		});
		res.on('end', function() {
			var jsfile = new Buffer.concat(chunks).toString('base64');
			// var jsfile = Buffer.concat(chunks);

			responce.header('Access-Control-Allow-Origin', '*');
			responce.header('Access-Control-Allow-Headers', 'X-Requested-With');
			responce.header('content-type', 'application/pdf');
			responce.send(200, jsfile);
		});
	}).on('error', function() {
		responce.jsonp(404, null);
	});
};
var https = require('https');
exports.sendPdfHTTPS = function(req, responce) {
	var donneRecu = req.body;
	var url = donneRecu['lien']; // jshint ignore:line
	https.get(url, function(res) {
		var chunks = [];
		res.on('data', function(chunk) {

			chunks.push(chunk);
		});
		res.on('end', function() {
			var jsfile = new Buffer.concat(chunks).toString('base64');
			// var jsfile = Buffer.concat(chunks);

			responce.header('Access-Control-Allow-Origin', '*');
			responce.header('Access-Control-Allow-Headers', 'X-Requested-With');
			responce.header('content-type', 'application/pdf');
			responce.send(jsfile);
		});
	}).on('error', function() {
		responce.jsonp(404, null);
	});
};<|endoftext|>


<commit_before>import './loadenv';
import config from './config';

import Queue from 'bull';
import { getStatsDClient } from './utils/statsd';

const rssQueue = new Queue('rss', config.cache.uri);
const ogQueue = new Queue('og', config.cache.uri);
const podcastQueue = new Queue('podcast', config.cache.uri);

function makeMetricKey(queue, event) {
    return ['winds', 'bull', queue.name, event].join(':');
}

async function trackQueueSize(statsd, queue) {
    let queueStatus = await queue.getJobCounts();
    statsd.gauge(makeMetricKey(queue, 'waiting'), queueStatus.waiting);
    statsd.gauge(makeMetricKey(queue, 'active'), queueStatus.active);
}

function AddQueueTracking(queue){
    var statsd = getStatsDClient();

    queue.on('error', function(error) {
        statsd.increment(makeMetricKey(queue, 'error'));
    })

    queue.on('active', function(job, jobPromise){
        statsd.increment(makeMetricKey(queue, 'active'));
    })

    queue.on('completed', function(job, result){
        statsd.timing(makeMetricKey(queue, 'elapsed'), new Date() - job.timestamp);
        statsd.increment(makeMetricKey(queue, 'completed'));
    })

    queue.on('failed', function(job, err){
        statsd.increment(makeMetricKey(queue, 'failed'));
    })

    queue.on('paused', function(){
        statsd.increment(makeMetricKey(queue, 'paused'));
    })

    queue.on('resumed', function(job){
        statsd.increment(makeMetricKey(queue, 'resumed'));
    })

    setInterval(trackQueueSize, 30000, statsd, queue);
}

AddQueueTracking(rssQueue);
AddQueueTracking(ogQueue);
AddQueueTracking(podcastQueue);

exports.RssQueueAdd = rssQueue.add.bind(rssQueue);
exports.OgQueueAdd = ogQueue.add.bind(ogQueue);
exports.PodcastQueueAdd = podcastQueue.add.bind(podcastQueue);

exports.ProcessRssQueue = function() {
    getStatsDClient().increment(makeMetricKey(rssQueue, 'started'));
    return rssQueue.process(...arguments);
}

exports.ProcessOgQueue = function() {
    getStatsDClient().increment(makeMetricKey(ogQueue, 'started'));
    ogQueue.process(...arguments);
}

exports.ProcessPodcastQueue = function() {
    getStatsDClient().increment(makeMetricKey(podcastQueue, 'started'));
    podcastQueue.process(...arguments);
}
<commit_msg>join with dots<commit_after>import './loadenv';
import config from './config';

import Queue from 'bull';
import { getStatsDClient } from './utils/statsd';

const rssQueue = new Queue('rss', config.cache.uri);
const ogQueue = new Queue('og', config.cache.uri);
const podcastQueue = new Queue('podcast', config.cache.uri);

function makeMetricKey(queue, event) {
    return ['winds', 'bull', queue.name, event].join('.');
}

async function trackQueueSize(statsd, queue) {
    let queueStatus = await queue.getJobCounts();
    statsd.gauge(makeMetricKey(queue, 'waiting'), queueStatus.waiting);
    statsd.gauge(makeMetricKey(queue, 'active'), queueStatus.active);
}

function AddQueueTracking(queue){
    var statsd = getStatsDClient();

    queue.on('error', function(error) {
        statsd.increment(makeMetricKey(queue, 'error'));
    })

    queue.on('active', function(job, jobPromise){
        statsd.increment(makeMetricKey(queue, 'active'));
    })

    queue.on('completed', function(job, result){
        statsd.timing(makeMetricKey(queue, 'elapsed'), new Date() - job.timestamp);
        statsd.increment(makeMetricKey(queue, 'completed'));
    })

    queue.on('failed', function(job, err){
        statsd.increment(makeMetricKey(queue, 'failed'));
    })

    queue.on('paused', function(){
        statsd.increment(makeMetricKey(queue, 'paused'));
    })

    queue.on('resumed', function(job){
        statsd.increment(makeMetricKey(queue, 'resumed'));
    })

    setInterval(trackQueueSize, 30000, statsd, queue);
}

AddQueueTracking(rssQueue);
AddQueueTracking(ogQueue);
AddQueueTracking(podcastQueue);

exports.RssQueueAdd = rssQueue.add.bind(rssQueue);
exports.OgQueueAdd = ogQueue.add.bind(ogQueue);
exports.PodcastQueueAdd = podcastQueue.add.bind(podcastQueue);

exports.ProcessRssQueue = function() {
    getStatsDClient().increment(makeMetricKey(rssQueue, 'started'));
    return rssQueue.process(...arguments);
}

exports.ProcessOgQueue = function() {
    getStatsDClient().increment(makeMetricKey(ogQueue, 'started'));
    ogQueue.process(...arguments);
}

exports.ProcessPodcastQueue = function() {
    getStatsDClient().increment(makeMetricKey(podcastQueue, 'started'));
    podcastQueue.process(...arguments);
}
<|endoftext|>


<commit_before>import "../loadenv"

import stream from "getstream"
import moment from "moment"
import normalize from "normalize-url"

import RSS from "../models/rss"
import Article from "../models/article"

import "../utils/db"
import config from "../config"
import logger from "../utils/logger"
import util from "util"

import sendRssFeedToCollections from "../utils/events/sendRssFeedToCollections"
import { ParseFeed } from "../parsers"

import async_tasks from "../async_tasks"
import { getStatsDClient } from '../utils/statsd';

const streamClient = stream.connect(config.stream.apiKey, config.stream.apiSecret)

// connect the handler to the queue
logger.info("Starting the RSS worker")

// TODO: move this to a separate main.js
async_tasks.ProcessRssQueue(30, handleRSS)

const statsd = getStatsDClient()

// the top level handleRSS just intercepts error handling before it goes to Bull
async function handleRSS(job) {
    let promise = _handleRSS(job)
    promise.catch(err => {
        logger.warn(`rss job ${job} broke with err ${err}`)
        console.log(err)

    })
    return promise
}

// Handle Podcast scrapes the podcast and updates the episodes
async function _handleRSS(job) {
    logger.info(`Processing ${job.data.url}`)

    // verify we have the rss object
    let rssID = job.data.rss
    let rss = await RSS.findOne({ _id: rssID })
    if (!rss) {
        logger.warn(`RSS with ID ${rssID} does not exist`)
        return
    }

    // mark as done, will be schedule again in 15 min from now
    // we do this early so a temporary failure doesnt leave things in a broken state
    await markDone(rssID)
    logger.info(`Marked ${rssID} as done`)

    // parse the articles
    let rssContent
    try {
        rssContent = await util.promisify(ParseFeed)(job.data.url)
    } catch (e) {
        logger.info(`rss scraping broke for url ${job.data.url}`)
        return
    }

	// update the articles
    logger.info(`Updating ${rssContent.articles.length} articles for feed ${rssID}`)

	if (rssContent.articles.length < 1) {
		return
	}

	statsd.timing("winds.handle_rss.articles.parsed", rssContent.articles.length)

    let allArticles = await upsertManyArticles(rssID, rssContent.articles)

    // updatedArticles will contain `null` for all articles that didn't get updated, that we already have in the system.
    let updatedArticles = allArticles.filter(updatedArticle => {
        return updatedArticle
    })

	statsd.increment("winds.handle_rss.articles.upserted", updatedArticles.length)

	await Promise.all(updatedArticles.map(article => {
		async_tasks.OgQueueAdd(
			{
				type: 'rss',
				url: article.url,
			},
			{
				removeOnComplete: true,
				removeOnFail: true,
			},
		)
	}))

    let rssFeed = streamClient.feed("rss", rssID)
    logger.info(`Syncing ${updatedArticles.length} articles to Stream`)
    if (updatedArticles.length > 0) {
        let chunkSize = 100
        for (let i = 0, j = updatedArticles.length; i < j; i += chunkSize) {
            let chunk = updatedArticles.slice(i, i + chunkSize)
            let streamArticles = chunk.map(article => {
                return {
                    actor: article.rss,
                    foreign_id: `articles:${article._id}`,
                    object: article._id,
                    time: article.publicationDate,
                    verb: "rss_article",
                }
            })
            await rssFeed.addActivities(streamArticles)
        }
		await sendRssFeedToCollections(rss)
    }
    logger.info(`Completed scraping for ${job.data.url}`)
}

async function upsertManyArticles(rssID, articles){
	let articlesData = articles.map(article => {
		const clone = Object.assign({}, article)
		clone.url = normalize(article.url)
		if (!clone.images || Object.keys(clone.images).length > 0) {
			delete(clone.images)
		}
		return clone
	})

	let existingArticles = await Article.find({$or: articlesData}, { "url": 1 })
	let existingArticleUrls = existingArticles.map(a => {return a.url})

	statsd.increment("winds.handle_rss.articles.already_in_mongo", existingArticleUrls.length)

	let articlesToUpsert = articlesData.filter(article => {
		return existingArticleUrls.indexOf(article.url) === -1
	})

	logger.info(`Feed ${rssID}: got ${articles.length} articles of which ${articlesToUpsert.length} need a sync`)

	return Promise.all(articlesToUpsert.map(article => {
		return upsertArticle(rssID, article)
	}))
}

// updateArticle updates the article in mongodb if it changed and create a new one if it did not exist
async function upsertArticle(rssID, post) {
	let update = {
		commentUrl: post.commentUrl,
		content: post.content,
		description: post.description,
		publicationDate: post.publicationDate,
		rss: rssID,
		title: post.title,
		url: post.url,
    	enclosures: post.enclosures,
	};

	try {
		return await Article.findOneAndUpdate(
			{
				$and: [
					{
						rss: rssID,
						url: post.url,
					},
					{
						$or: Object.keys(update).map(k => {
							return {
								[k]: {
									$ne: update[k],
								},
							};
						}),
					},
				],
			},
			update,
			{
				new: true,
				upsert: true,
			},
		)
	} catch(err) {
		if (err.code === 11000){
			statsd.increment("winds.handle_rss.articles.ignored")
			return null
		} else {
			throw error;
		}
	}
}

// markDone sets lastScraped to now and isParsing to false
async function markDone(rssID) {
    /*
	Set the last scraped for the given rssID
	*/
    let now = moment().toISOString()
    let updated = await RSS.update(
        { _id: rssID },
        {
            lastScraped: now,
            isParsing: false,
        },
    )
    return updated
}
<commit_msg>put back count and fix bad check for images<commit_after>import "../loadenv"

import stream from "getstream"
import moment from "moment"
import normalize from "normalize-url"

import RSS from "../models/rss"
import Article from "../models/article"

import "../utils/db"
import config from "../config"
import logger from "../utils/logger"
import util from "util"

import sendRssFeedToCollections from "../utils/events/sendRssFeedToCollections"
import { ParseFeed } from "../parsers"

import async_tasks from "../async_tasks"
import { getStatsDClient } from '../utils/statsd';

const streamClient = stream.connect(config.stream.apiKey, config.stream.apiSecret)

// connect the handler to the queue
logger.info("Starting the RSS worker")

// TODO: move this to a separate main.js
async_tasks.ProcessRssQueue(30, handleRSS)

const statsd = getStatsDClient()

// the top level handleRSS just intercepts error handling before it goes to Bull
async function handleRSS(job) {
    let promise = _handleRSS(job)
    promise.catch(err => {
        logger.warn(`rss job ${job} broke with err ${err}`)
        console.log(err)

    })
    return promise
}

// Handle Podcast scrapes the podcast and updates the episodes
async function _handleRSS(job) {
    logger.info(`Processing ${job.data.url}`)

    // verify we have the rss object
    let rssID = job.data.rss
    let rss = await RSS.findOne({ _id: rssID })
    if (!rss) {
        logger.warn(`RSS with ID ${rssID} does not exist`)
        return
    }

    // mark as done, will be schedule again in 15 min from now
    // we do this early so a temporary failure doesnt leave things in a broken state
    await markDone(rssID)
    logger.info(`Marked ${rssID} as done`)

    // parse the articles
    let rssContent
    try {
        rssContent = await util.promisify(ParseFeed)(job.data.url)
    } catch (e) {
        logger.info(`rss scraping broke for url ${job.data.url}`)
        return
    }

	// update the articles
    logger.info(`Updating ${rssContent.articles.length} articles for feed ${rssID}`)

	if (rssContent.articles.length < 1) {
		return
	}

	statsd.increment("winds.handle_rss.articles.parsed", rssContent.articles.length)
	statsd.timing("winds.handle_rss.articles.parsed", rssContent.articles.length)

    let allArticles = await upsertManyArticles(rssID, rssContent.articles)

    // updatedArticles will contain `null` for all articles that didn't get updated, that we already have in the system.
    let updatedArticles = allArticles.filter(updatedArticle => {
        return updatedArticle
    })

	statsd.increment("winds.handle_rss.articles.upserted", updatedArticles.length)

	await Promise.all(updatedArticles.map(article => {
		async_tasks.OgQueueAdd(
			{
				type: 'rss',
				url: article.url,
			},
			{
				removeOnComplete: true,
				removeOnFail: true,
			},
		)
	}))

    let rssFeed = streamClient.feed("rss", rssID)
    logger.info(`Syncing ${updatedArticles.length} articles to Stream`)
    if (updatedArticles.length > 0) {
        let chunkSize = 100
        for (let i = 0, j = updatedArticles.length; i < j; i += chunkSize) {
            let chunk = updatedArticles.slice(i, i + chunkSize)
            let streamArticles = chunk.map(article => {
                return {
                    actor: article.rss,
                    foreign_id: `articles:${article._id}`,
                    object: article._id,
                    time: article.publicationDate,
                    verb: "rss_article",
                }
            })
            await rssFeed.addActivities(streamArticles)
        }
		await sendRssFeedToCollections(rss)
    }
    logger.info(`Completed scraping for ${job.data.url}`)
}

async function upsertManyArticles(rssID, articles){
	let articlesData = articles.map(article => {
		const clone = Object.assign({}, article)
		clone.url = normalize(article.url)
		if (!clone.images || Object.keys(clone.images).length < 1) {
			delete(clone.images)
		}
		return clone
	})

	let existingArticles = await Article.find({$or: articlesData}, { "url": 1 })
	let existingArticleUrls = existingArticles.map(a => {return a.url})

	statsd.increment("winds.handle_rss.articles.already_in_mongo", existingArticleUrls.length)

	let articlesToUpsert = articlesData.filter(article => {
		return existingArticleUrls.indexOf(article.url) === -1
	})

	logger.info(`Feed ${rssID}: got ${articles.length} articles of which ${articlesToUpsert.length} need a sync`)

	return Promise.all(articlesToUpsert.map(article => {
		return upsertArticle(rssID, article)
	}))
}

// updateArticle updates the article in mongodb if it changed and create a new one if it did not exist
async function upsertArticle(rssID, post) {
	let update = {
		commentUrl: post.commentUrl,
		content: post.content,
		description: post.description,
		publicationDate: post.publicationDate,
		rss: rssID,
		title: post.title,
		url: post.url,
    	enclosures: post.enclosures,
	};

	try {
		return await Article.findOneAndUpdate(
			{
				$and: [
					{
						rss: rssID,
						url: post.url,
					},
					{
						$or: Object.keys(update).map(k => {
							return {
								[k]: {
									$ne: update[k],
								},
							};
						}),
					},
				],
			},
			update,
			{
				new: true,
				upsert: true,
			},
		)
	} catch(err) {
		if (err.code === 11000){
			statsd.increment("winds.handle_rss.articles.ignored")
			return null
		} else {
			throw error;
		}
	}
}

// markDone sets lastScraped to now and isParsing to false
async function markDone(rssID) {
    /*
	Set the last scraped for the given rssID
	*/
    let now = moment().toISOString()
    let updated = await RSS.update(
        { _id: rssID },
        {
            lastScraped: now,
            isParsing: false,
        },
    )
    return updated
}
<|endoftext|>