export default `import React from "react";
import useForm from "react-hook-form";

export default function YourForm() {
  const { register, handleSubmit, reset } = useForm();
  const onSubmit = (data, e) => {
    e.target.reset(); // standard reset after form submit
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="text" name="First name" ref={register({ required: true })} />
      <input type="text" name="Last name" ref={register} />

      <input type="submit" />
      <input style={{ display: "block", marginTop: 20 }} type="reset" />
      <input style={{ display: "block", marginTop: 20 }} type="button" onClick={reset} />
    </form>
  );
}
`


? field.options.findIndex(({ ref }): boolean => value === ref.value)

if ((!isRadio && field) || (isRadio && existRadioOptionIndex > -1)) return;

if (!field) fields[name] = { options: [], required, validate, ref: { type: 'radio', name } };

if (!type || (!isRadio && field) || (isRadio && existRadioOptionIndex > -1)) return;

---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


English | 简体中文

> React hook form validation without the hassle

[![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=React+Hook-Form&url=https://github.com/bluebill1049/react-hook-form)
[![CircleCI](https://circleci.com/gh/bluebill1049/react-hook-form.svg?style=svg)](https://circleci.com/gh/bluebill1049/react-hook-form)
[![Coverage Status](https://coveralls.io/repos/github/bluebill1049/react-hook-form/badge.svg?branch=master)](https://coveralls.io/github/bluebill1049/react-hook-form?branch=master)
[![npm downloads](https://img.shields.io/npm/dm/react-hook-form.svg?style=flat-square)](https://www.npmjs.com/package/react-hook-form)
[![npm](https://img.shields.io/npm/dt/react-hook-form.svg?style=flat-square)](https://www.npmjs.com/package/react-hook-form)
[![npm](https://badgen.net/bundlephobia/minzip/react-hook-form)](https://badgen.net/bundlephobia/minzip/react-hook-form)

- Super easy to integrate and create forms
- Built with performance and developer experience in mind
- Follows HTML standard for validation
- Tiny size without any dependency
- Build forms quickly with the [form builder](https://react-hook-form.now.sh/builder)

## Install

    $ npm install react-hook-form

## [Docs](https://react-hook-form.now.sh/api)

- [Motivation](https://medium.com/@bruce1049/form-validation-with-hook-in-3kb-c5414edf7d64)
- [Get started](https://react-hook-form.now.sh/api)
- [API](https://react-hook-form.now.sh/api)
- [Examples](https://github.com/bluebill1049/react-hook-form/tree/master/examples)
- [Demo](https://react-hook-form.now.sh)
- [Form Builder](https://react-hook-form.now.sh/builder)

## Quickstart

```jsx
import React from 'react'
import useForm from 'react-hook-form'

function App() {
  const { register, handleSubmit, errors } = useForm() // initialise the hook
  const onSubmit = (data) => { console.log(data) } // callback when validation pass

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstname" ref={register} /> {/* register an input */}

      <input name="lastname" ref={register({ required: true })} /> {/* apply required validation */}
      {errors.lastname && 'Last name is required.'} {/* error message */}

      <input name="age" ref={register({ pattern: /\d+/ })} /> {/* apply pattern validation */}
      {errors.age && 'Please enter number for age.'} {/* error message */}

      <input type="submit" />
    </form>
  )
}
```

## Contributors
Thanks goes to these wonderful people:


keep keep keep replace replace replace replace replace replace replace replace keep keep keep keep keep

function register(data: Ref | RegisterInput, rules?: RegisterInput): RegisterFunction | undefined {

if (!data || typeof window === 'undefined') return;

import { useRef, useState, useEffect } from 'react';
import getFieldsValues from './logic/getFieldsValues';
import getFieldValue from './logic/getFieldValue';
import validateField from './logic/validateField';
import findRemovedFieldAndRemoveListener from './logic/findRemovedFieldAndRemoveListener';
import attachEventListeners from './logic/attachEventListeners';
import validateWithSchema from './logic/validateWithSchema';
import combineFieldValues from './logic/combineFieldValues';
import shouldUpdateWithError from './logic/shouldUpdateWithError';
import warnMissingRef from './utils/warnMissingRef';
import modeChecker from './utils/validationModeChecker';
import onDomRemove from './utils/onDomRemove';
import isRadioInput from './utils/isRadioInput';
import isEmptyObject from './utils/isEmptyObject';
import filterUndefinedErrors from './utils/filterUndefinedErrors';
import {
  Props,
  Field,
  ErrorMessages,
  Ref,
  SubmitPromiseResult,
  FieldsObject,
  VoidFunction,
  UseFormFunctions,
  RegisterFunction,
  FieldValue,
  RegisterInput,
} from './types';
import isCheckBoxInput from './utils/isCheckBoxInput';

export default function useForm(
  { mode, validationSchema, defaultValues }: Props = {
    mode: 'onSubmit',
    defaultValues: {},
  },
): UseFormFunctions {
  const fieldsRef = useRef<FieldsObject>({});
  const errorsRef = useRef<ErrorMessages>({});
  const submitCountRef = useRef<number>(0);
  const touchedFieldsRef = useRef<string[]>([]);
  const watchFieldsRef = useRef<{ [key: string]: boolean }>({});
  const isWatchAllRef = useRef<boolean>(false);
  const isSubmittingRef = useRef<boolean>(false);
  const isSubmittedRef = useRef<boolean>(false);
  const isDirtyRef = useRef<boolean>(false);
  const reRenderForm = useState({})[1];
  const validateAndStateUpdateRef = useRef<Function>();

  validateAndStateUpdateRef.current = validateAndStateUpdateRef.current
    ? validateAndStateUpdateRef.current
    : async ({ target: { name }, type }: Ref): Promise<void> => {
        const fields = fieldsRef.current;
        const errors = errorsRef.current;
        const ref = fields[name];
        const isBlurType = type === 'blur';
        const { isOnChange, isOnSubmit, isOnBlur } = modeChecker(mode);
        const onSubmitModeNotSubmitted = !isSubmittedRef.current && (isOnSubmit || !mode);
        const isWatchAll = isWatchAllRef.current;
        const shouldUpdateWatchMode = isWatchAll || watchFieldsRef.current[name];
        const shouldUpdateValidateMode = isOnChange || (isOnBlur && isBlurType);
        let shouldUpdateState = isWatchAll;

        if (!isDirtyRef.current) {
          isDirtyRef.current = true;
          shouldUpdateState = true;
        }

        if (!touchedFieldsRef.current.includes(name)) {
          touchedFieldsRef.current.push(name);
          shouldUpdateState = true;
        }

        if (onSubmitModeNotSubmitted && shouldUpdateWatchMode) return reRenderForm({});

        if (validationSchema) {
          const result = getFieldsValues(fields);
          const schemaValidateErrors = (await validateWithSchema(validationSchema, result)) || {};
          const error = schemaValidateErrors[name];
          const shouldUpdate = ((!error && errors[name]) || error) && shouldUpdateValidateMode;

          if (shouldUpdate || shouldUpdateWatchMode) {
            const errorsCopy = { ...errors, ...{ [name]: error } };
            if (!error) delete errorsCopy[name];

            errorsRef.current = errorsCopy;
            return reRenderForm({});
          }
        } else {
          const error = await validateField(ref, fields);
          const shouldUpdate = shouldUpdateWithError({
            errors,
            error,
            onSubmitModeNotSubmitted,
            isOnBlur,
            isBlurType,
            name,
          });

          if (shouldUpdate || shouldUpdateValidateMode || shouldUpdateWatchMode) {
            const errorsCopy = { ...filterUndefinedErrors(errors), ...error };
            if (!error[name]) delete errorsCopy[name];

            errorsRef.current = errorsCopy;
            return reRenderForm({});
          }
        }

        if (shouldUpdateState) reRenderForm({});
      };

  const removeEventListener: Function = findRemovedFieldAndRemoveListener.bind(
    null,
    fieldsRef.current,
    validateAndStateUpdateRef.current,
  );

  const setValue = (name: string, value: string | number | boolean): void => {
    const field = fieldsRef.current[name];
    if (!field) return;
    const elm = field.ref;
    if (elm.type) elm[isCheckBoxInput(elm.type) ? 'checked' : 'value'] = value;
  };

  function registerIntoFieldsRef(elementRef, data = { required: false, validate: undefined }): void {
    if (elementRef && !elementRef.name) return warnMissingRef(elementRef);

    const { name, type, value } = elementRef;
    const { required, validate } = data;
    const inputData = {
      ...data,
      ref: elementRef,
    };
    const fields = fieldsRef.current;
    const isRadio = isRadioInput(type);
    const field = fields[name];
    const existRadioOptionIndex =
      isRadio && field && Array.isArray(field.options)
        ? field.options.findIndex(({ ref }): boolean => value === ref.value)
        : -1;

    if ((!isRadio && field) || (isRadio && existRadioOptionIndex > -1)) return;

    if (isRadio) {
      if (!field) fields[name] = { options: [], required, validate, ref: { type: 'radio', name } };
      if (validate) fields[name].validate = validate;

      (fields[name].options || []).push({
        ...inputData,
        mutationWatcher: onDomRemove(elementRef, (): Function => removeEventListener(inputData, true)),
      });
    } else {
      fields[name] = {
        ...inputData,
        mutationWatcher: onDomRemove(elementRef, (): Function => removeEventListener(inputData, true)),
      };
    }

    if (defaultValues && defaultValues[name]) {
      setValue(name, defaultValues[name]);
    }

    attachEventListeners({
      field: isRadio ? (fields[name].options || [])[(fields[name].options || []).length - 1] : fields[name],
      isRadio,
      validateAndStateUpdate: validateAndStateUpdateRef.current,
    });
  }

  function watch(
    filedNames?: string | string[] | undefined,
    defaultValue?: string | string[] | undefined,
  ): FieldValue | FieldValue[] | undefined {
    const watchFields = watchFieldsRef.current;

    if (typeof filedNames === 'string') {
      watchFields[filedNames] = true;
    } else if (Array.isArray(filedNames)) {
      filedNames.forEach(
        (name): void => {
          watchFields[name] = true;
        },
      );
    } else {
      isWatchAllRef.current = true;
      watchFieldsRef.current = {};
    }

    const values = getFieldsValues(fieldsRef.current, filedNames);
    const result = values === undefined || isEmptyObject(values) ? undefined : values;
    return result === undefined ? defaultValue : result;
  }

  function register(data: Ref | RegisterInput): RegisterFunction | undefined {
    if (!data) return;
    if (data.type) {
      if (!data.name) {
        warnMissingRef(data);
        return;
      }
      registerIntoFieldsRef(data);
    }

    return (ref): void => ref && registerIntoFieldsRef(ref, data);
  }

  const handleSubmit = (callback: (Object, e) => void): Function => async (e): Promise<void> => {
    if (e) {
      e.preventDefault();
      e.persist();
    }
    let fieldErrors;
    let fieldValues;
    const fields = fieldsRef.current;
    const currentFieldValues = Object.values(fields);
    isSubmittingRef.current = true;
    reRenderForm({});

    if (validationSchema) {
      fieldValues = getFieldsValues(fields);
      fieldErrors = await validateWithSchema(validationSchema, fieldValues);
    } else {
      const { errors, values }: SubmitPromiseResult = await currentFieldValues.reduce(
        async (previous: Promise<SubmitPromiseResult>, field: Field): Promise<SubmitPromiseResult> => {
          const resolvedPrevious = await previous;
          const {
            ref,
            ref: { name },
          } = field;

          if (!fields[name]) return Promise.resolve(resolvedPrevious);

          const fieldError = await validateField(field, fields);

          if (fieldError[name]) {
            resolvedPrevious.errors = { ...(resolvedPrevious.errors || {}), ...fieldError };
            return Promise.resolve(resolvedPrevious);
          }

          resolvedPrevious.values[name] = getFieldValue(fields, ref);
          return Promise.resolve(resolvedPrevious);
        },
        Promise.resolve({
          errors: {},
          values: {},
        }),
      );

      fieldErrors = {
        ...errors,
        ...filterUndefinedErrors(errorsRef.current),
      };
      fieldValues = values;
    }

    isSubmittedRef.current = true;
    submitCountRef.current += 1;
    isSubmittingRef.current = false;

    if (isEmptyObject(fieldErrors)) {
      callback(combineFieldValues(fieldValues), e);
    } else {
      errorsRef.current = fieldErrors;
      reRenderForm({});
    }
  };

  const unSubscribe = (): void => {
    fieldsRef.current &&
      Object.values(fieldsRef.current).forEach(
        (field: Field): void => {
          const { ref, options } = field;
          isRadioInput(ref.type) && Array.isArray(options)
            ? options.forEach((fieldRef): void => removeEventListener(fieldRef, true))
            : removeEventListener(field, true);
        },
      );
    fieldsRef.current = {};
    watchFieldsRef.current = {};
    errorsRef.current = {};
    isWatchAllRef.current = false;
    isSubmittedRef.current = false;
    isDirtyRef.current = false;
    touchedFieldsRef.current = [];
  };

  const reset = (): void => {
    Object.values(fieldsRef.current)[0]
      .ref.closest('form')
      .reset();
    unSubscribe();
    reRenderForm({});
  };

  const setError = (name: string, type: string, message?: string, ref?: Ref): void => {
    errorsRef.current[name] = {
      type,
      message,
      ref,
      isManual: true,
    };
    reRenderForm({});
  };

  useEffect((): VoidFunction => unSubscribe, [mode]);

  return {
    register,
    handleSubmit,
    watch,
    unSubscribe,
    reset,
    setError,
    setValue,
    errors: errorsRef.current,
    formState: {
      dirty: isDirtyRef.current,
      isSubmitted: isSubmittedRef.current,
      submitCount: submitCountRef.current,
      touched: touchedFieldsRef.current,
      isSubmitting: isSubmittingRef.current,
    },
  };
}


export default (options: HTMLOptionElement[]): string[] =>
  options.filter(({ selected }): boolean => selected).map(({ value }): string => value);


import React from 'react';
import styled from 'styled-components';
import copyClipBoard from './utils/copyClipBoard';
import generateCode from './logic/generateCode';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { xonokai } from 'react-syntax-highlighter/dist/styles/prism';
import colors from './styles/colors';
import track from './utils/track';

const CopyButton = styled.button`
  background: ${colors.lightBlue};
  border: none;
  color: white;
  border-radius: 4px;
  font-size: 16px;
  padding: 5px 10px;
  position: absolute;
  right: 0;
  z-index: 1;
  top: 10px;
  box-shadow: 0 0 10px #000;
  display: none;

  @media (min-width: 768px) {
    display: inline-block;
  }

  &:hover {
    opacity: 0.8;
  }

  &:active {
    transform: translateY(2px);
  }
`;

export const LinkToSandBox = styled.a`
  background: ${colors.lightPink};
  border: none;
  color: white;
  border-radius: 4px;
  font-size: 16px;
  padding: 5px 10px;
  position: absolute;
  top: 10px;
  right: 0;
  z-index: 1;
  box-shadow: 0 0 10px #000;
  text-decoration: none;

  @media (min-width: 768px) {
    display: inline-block;
    right: 170px;
  }

  &:hover {
    opacity: 0.8;
  }

  &:active {
    transform: translateY(2px);
  }
`;

const SyntaxHighlighterWrapper = styled.div`
  pre {
    padding-top: 50px !important;

    @media (min-width: 1024px) {
      padding-top: 20px !important;
    }
  }
`;

export default function SyntaxHighlighterWithCopy({ rawData, data, url, tabIndex, withOutCopy }: any) {
  return (
    <div
      style={{
        position: 'relative',
      }}
    >
      {!withOutCopy && (
        <CopyButton
          tabIndex={tabIndex}
          onClick={() => {
            track({
              label: 'copy',
              category: 'CodeExample',
              action: 'copy code',
            });
            rawData || copyClipBoard(generateCode(data));
            alert('Code copied into your clipboard.');
          }}
          aria-label="Copy code into your clipboard"
        >
          Copy to clipboard
        </CopyButton>
      )}

      {url && (
        <LinkToSandBox
          onClick={() => {
            track({
              label: 'CodeSandbox',
              category: 'CodeExample',
              action: `Go to codeSandBox ${url}`,
            });
          }}
          tabIndex={tabIndex}
          href={url}
          target="_blank"
        >
          CodeSandbox
        </LinkToSandBox>
      )}
      <SyntaxHighlighterWrapper>
        <SyntaxHighlighter
          customStyle={{
            border: 'none',
          }}
          style={xonokai}
          language={'jsx'}
        >
          {rawData || generateCode(data)}
        </SyntaxHighlighter>
      </SyntaxHighlighterWrapper>
    </div>
  );
}


validateAndStateUpdate: Function | undefined;

ref.addEventListener(isCheckBoxInput(ref.type) || isRadio ? 'change' : 'input', validateAndStateUpdate);

import * as React from 'react';
import useForm from './';
import { act } from 'react-dom/test-utils';
import attachEventListeners from './logic/attachEventListeners';
import getFieldsValues from './logic/getFieldsValues';
import findRemovedFieldAndRemoveListener from './logic/findRemovedFieldAndRemoveListener';
import validateWithSchema from './logic/validateWithSchema';
import validateField from './logic/validateField';
import onDomRemove from './utils/onDomRemove';
import { mount } from 'enzyme';

jest.mock('./utils/onDomRemove');
jest.mock('./logic/findRemovedFieldAndRemoveListener');
jest.mock('./logic/validateField');
jest.mock('./logic/attachEventListeners');
jest.mock('./logic/getFieldsValues');
jest.mock('./logic/validateWithSchema');

const TestHook = ({ callback }) => {
  const { errors } = callback();
  return errors ?
errors
 : null;
};

const testComponent = callback => {
  mount(<TestHook callback={callback} />);
};

let hookForm;
let hookFormWithValidationSchema;

describe('useForm', () => {
  beforeEach(() => {
    testComponent(() => {
      hookForm = useForm();
      return hookForm;
    });

    testComponent(() => {
      hookFormWithValidationSchema = useForm({
        mode: 'onSubmit',
        validationSchema: {},
      });
      return hookFormWithValidationSchema;
    });

    jest.resetAllMocks();
  });

  describe('register', () => {
    it('should return undefined when ref is undefined', () => {
      expect(hookForm.register(undefined)).toBeUndefined();
    });

    it('should return undefined when ref name is missing', () => {
      expect(hookForm.register({ type: 'input' })).toBeUndefined();
    });

    it('should register field and call attachEventListeners method', () => {
      hookForm.register({ type: 'input', name: 'test' });
      expect(attachEventListeners).toBeCalledWith({
        field: {
          mutationWatcher: undefined,
          ref: {
            name: 'test',
            type: 'input',
          },
          required: false,
          validate: undefined,
        },
        isRadio: false,
        validateAndStateUpdate: expect.any(Function),
      });
      expect(onDomRemove).toBeCalled();
    });

    it('should register field for radio type and call attachEventListeners method', () => {
      hookForm.register({ type: 'radio', name: 'test' });
      expect(attachEventListeners).toBeCalledWith({
        field: {
          mutationWatcher: undefined,
          ref: {
            name: 'test',
            type: 'radio',
          },
          required: false,
          validate: undefined,
        },
        isRadio: true,
        validateAndStateUpdate: expect.any(Function),
      });
      expect(onDomRemove).toBeCalled();
    });
  });

  describe('watch', () => {
    it('should watch individual input', () => {
      expect(hookForm.watch('test')).toBeUndefined();
      // @ts-ignore
      getFieldsValues.mockImplementation((_, name) => {
        if (name === 'test') {
          return 'data';
        }
      });
      expect(hookForm.watch('test')).toBe('data');
    });

    it('should watch array of inputs', () => {
      expect(hookForm.watch(['test', 'test1'])).toBeUndefined();
      // @ts-ignore
      getFieldsValues.mockImplementation((_, name) => {
        if (name && name.includes('test1') && name.includes('test')) {
          return ['data1', 'data2'];
        }
      });
      expect(hookForm.watch(['test', 'test1'])).toEqual(['data1', 'data2']);
    });

    it('should watch every fields', () => {
      expect(hookForm.watch()).toBeUndefined();
      // @ts-ignore
      getFieldsValues.mockImplementation((_, name) => {
        if (name === undefined) {
          return ['data1', 'data2'];
        }
      });
      expect(hookForm.watch()).toEqual(['data1', 'data2']);
    });
  });

  describe('unSubscribe', () => {
    it('should remove all reference when mode change', () => {
      hookForm.register({ type: 'input', name: 'test' });
      hookForm.register({
        type: 'radio',
        name: 'test1',
        options: [{ type: 'radio', name: 'test3' }, { type: 'radio', name: 'test4' }],
      });
      expect(attachEventListeners).toBeCalledWith({
        field: {
          mutationWatcher: undefined,
          ref: {
            name: 'test',
            type: 'input',
          },
          required: false,
          validate: undefined,
        },
        isRadio: false,
        validateAndStateUpdate: expect.any(Function),
      });
      hookForm.register({ type: 'input', name: 'test' });
      act(() => {
        hookForm.unSubscribe();
      });
      expect(findRemovedFieldAndRemoveListener).toBeCalled();
      hookForm.register({ type: 'input', name: 'test' });
      expect(attachEventListeners).toBeCalledTimes(3);
    });
  });

  describe('handleSubmit', () => {
    it('should invoke the callback when validation pass', async () => {
      const callback = jest.fn();
      const preventDefault = jest.fn();
      const persist = jest.fn();
      await hookForm.handleSubmit(callback)({
        preventDefault,
        persist,
      });
      expect(callback).toBeCalled();
    });

    it('should not invoke callback when there are errors', async () => {
      hookForm.register({ value: '', type: 'input', name: 'test' }, { required: true });
      const callback = jest.fn();
      // @ts-ignore
      validateField.mockImplementation(async () => {
        return { test: { type: 'test' } };
      });
      const test = hookForm.handleSubmit(callback);
      await test({
        preventDefault: () => {},
        persist: () => {},
      });
      expect(callback).not.toBeCalled();
    });
  });

  describe('handleSubmit with validationSchema', () => {
    it('should invoke callback when error not found', async () => {
      hookFormWithValidationSchema.register({ value: '', type: 'input', name: 'test' }, { required: true });
      const callback = jest.fn();
      // @ts-ignore
      getFieldsValues.mockImplementation(async () => {
        return { test: 'test' };
      });
      // @ts-ignore
      validateWithSchema.mockImplementation(async () => {
        return {};
      });
      await hookFormWithValidationSchema.handleSubmit(callback)({
        preventDefault: () => {},
        persist: () => {},
      });
      expect(callback).toBeCalled();
    });
  });
});


export default `import React from "react";
import useForm from "react-hook-form";

const Example = () => {
  const { handleSubmit, register, errors } = useForm();
  const onSubmit = values => {
    console.log(values);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="email"
        ref={register({
          required: 'Required',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}$/i,
            message: "invalid email address"
          }
        })}
      />
      {errors.email && errors.email.message}

      <input
        name="username"
        ref={register({
          validate: value => value === "admin" || "Nice try!"
        })}
      />
      {errors.username && errors.username.message}

      <button type="submit">Submit</button>
    </form>
  );
};
`;


my-app

>0.2%
not dead
not ie <= 11
not op_mini all


import React from "react";
import ReactDOM from "react-dom";
import useForm from "react-hook-form";

import "./styles.css";

function App() {
  const { register, handleSubmit } = useForm({
    defaultValues: {
      firstName: "bill",
      lastName: "luo",
      email: "test@test.com",
      isDeveloper: true
    }
  });
  const onSubmit = data => {
    alert(JSON.stringify(data));
  };

  return (

      <form onSubmit={handleSubmit(onSubmit)}>

          First Name
          <input name="firstName" placeholder="bill" ref={register} />

          Last Name
          <input name="lastName" placeholder="luo" ref={register} />

          Email
          <input
            name="email"
            placeholder="bluebill1049@hotmail.com"
            type="email"
            ref={register}
          />

          Is developer?
          <input name="isDeveloper" type="checkbox" ref={register} />

        <button type="submit">Submit</button>
      </form>

  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


this._department = new ReactiveVar(null);

this._widgetOpened = new ReactiveVar(false);

this._showAgentEmail = new ReactiveVar(true);

this.stream = new Meteor.Streamer('livechat-room');

if (this._room.get() && Meteor.userId()) {

keep keep keep keep replace keep keep keep keep keep

/**
 * Sets an user as (non)operator
 * @param {string} _id - User's _id
 * @param {boolean} operator - Flag to set as operator or not
 */
RocketChat.models.Users.setOperator = function(_id, operator) {
	const update = {
		$set: {
			operator
		}
	};

	return this.update(_id, update);
};

/**
 * Gets all online agents
 * @return
 */
RocketChat.models.Users.findOnlineAgents = function() {
	const query = {
		status: {
			$exists: true,
			$ne: 'offline'
		},
		statusLivechat: 'available',
		roles: 'livechat-agent'
	};

	return this.find(query);
};

/**
 * Gets all agents
 * @return
 */
RocketChat.models.Users.findAgents = function() {
	const query = {
		roles: 'livechat-agent'
	};

	return this.find(query);
};

/**
 * Find online users from a list
 * @param {array} userList - array of usernames
 * @return
 */
RocketChat.models.Users.findOnlineUserFromList = function(userList) {
	const query = {
		status: {
			$exists: true,
			$ne: 'offline'
		},
		statusLivechat: 'available',
		roles: 'livechat-agent',
		username: {
			$in: [].concat(userList)
		}
	};

	return this.find(query);
};

/**
 * Get next user agent in order
 * @return {object} User from db
 */
RocketChat.models.Users.getNextAgent = function() {
	const query = {
		status: {
			$exists: true,
			$ne: 'offline'
		},
		statusLivechat: 'available',
		roles: 'livechat-agent'
	};

	const collectionObj = this.model.rawCollection();
	const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj);

	const sort = {
		livechatCount: 1,
		username: 1
	};

	const update = {
		$inc: {
			livechatCount: 1
		}
	};

	const user = findAndModify(query, sort, update);
	if (user && user.value) {
		return {
			agentId: user.value._id,
			username: user.value.username
		};
	} else {
		return null;
	}
};

/**
 * Gets visitor by token
 * @param {string} token - Visitor token
 */
RocketChat.models.Users.getVisitorByToken = function(token, options) {
	const query = {
		'profile.guest': true,
		'profile.token': token
	};

	return this.findOne(query, options);
};

/**
 * Gets visitor by token
 * @param {string} token - Visitor token
 */
RocketChat.models.Users.findVisitorByToken = function(token) {
	const query = {
		'profile.guest': true,
		'profile.token': token
	};

	return this.find(query);
};

/**
 * Change user's livechat status
 * @param {string} token - Visitor token
 */
RocketChat.models.Users.setLivechatStatus = function(userId, status) {
	const query = {
		'_id': userId
	};

	const update = {
		$set: {
			'statusLivechat': status
		}
	};

	return this.update(query, update);
};

/**
 * change all livechat agents livechat status to "not-available"
 */
RocketChat.models.Users.closeOffice = function() {
	self = this;
	self.findAgents().forEach(function(agent) {
		self.setLivechatStatus(agent._id, 'not-available');
	});
};

/**
 * change all livechat agents livechat status to "available"
 */
RocketChat.models.Users.openOffice = function() {
	self = this;
	self.findAgents().forEach(function(agent) {
		self.setLivechatStatus(agent._id, 'available');
	});
};

RocketChat.models.Users.updateLivechatDataByToken = function(token, key, value, overwrite = true) {
	const query = {
		'profile.token': token
	};

	if (!overwrite) {
		const user = this.findOne(query, { fields: { livechatData: 1 } });
		if (user.livechatData && typeof user.livechatData[key] !== 'undefined') {
			return true;
		}
	}

	const update = {
		$set: {
			[`livechatData.${ key }`]: value
		}
	};

	return this.update(query, update);
};

/**
 * Find a visitor by their phone number
 * @return {object} User from db
 */
RocketChat.models.Users.findOneVisitorByPhone = function(phone) {
	const query = {
		'phone.phoneNumber': phone
	};

	return this.findOne(query);
};

/**
 * Get the next visitor name
 * @return {string} The next visitor name
 */
RocketChat.models.Users.getNextVisitorUsername = function() {
	const settingsRaw = RocketChat.models.Settings.model.rawCollection();
	const findAndModify = Meteor.wrapAsync(settingsRaw.findAndModify, settingsRaw);

	const query = {
		_id: 'Livechat_guest_count'
	};

	const update = {
		$inc: {
			value: 1
		}
	};

	const livechatCount = findAndModify(query, null, update);

	return `guest-${ livechatCount.value.value + 1 }`;
};

RocketChat.models.Users.saveGuestById = function(_id, data) {
	const setData = {};
	const unsetData = {};

	if (data.name) {
		if (!_.isEmpty(s.trim(data.name))) {
			setData.name = s.trim(data.name);
		} else {
			unsetData.name = 1;
		}
	}

	if (data.email) {
		if (!_.isEmpty(s.trim(data.email))) {
			setData.visitorEmails = [
				{ address: s.trim(data.email) }
			];
		} else {
			unsetData.visitorEmails = 1;
		}
	}

	if (data.phone) {
		if (!_.isEmpty(s.trim(data.phone))) {
			setData.phone = [
				{ phoneNumber: s.trim(data.phone) }
			];
		} else {
			unsetData.phone = 1;
		}
	}

	const update = {};

	if (!_.isEmpty(setData)) {
		update.$set = setData;
	}

	if (!_.isEmpty(unsetData)) {
		update.$unset = unsetData;
	}

	if (_.isEmpty(update)) {
		return true;
	}

	return this.update({ _id }, update);
};

RocketChat.models.Users.findOneGuestByEmailAddress = function(emailAddress) {
	const query = {
		'visitorEmails.address': new RegExp(`^${ s.escapeRegExp(emailAddress) }$`, 'i')
	};

	return this.findOne(query);
};

RocketChat.models.Users.getAgentInfo = function(agentId) {
	const query = {
		_id: agentId
	};

	const options = {
		fields: {
			name: 1,
			username: 1,
			emails: 1,
			customFields: 1
		}
	};

	return this.findOne(query, options);
};


Package.describe({
  name: "mizzao:timesync",
  summary: "NTP-style time synchronization between server and client",
  version: "0.3.4",
  git: "https://github.com/mizzao/meteor-timesync.git"
});

Package.onUse(function (api) {
  api.use([
    'check',
    'tracker',
    'http'
  ], 'client');

  api.use('webapp', 'server');

  // Our files
  api.addFiles('timesync-server.js', 'server');
  api.addFiles('timesync-client.js', 'client');

  api.export('TimeSync', 'client');
  api.export('SyncInternals', 'client', {testOnly: true} );
});

Package.onTest(function (api) {
  api.use([
    'tinytest',
    'test-helpers'
  ]);

  api.use(["tracker", "underscore"], 'client');

  api.use("mizzao:timesync");

  api.addFiles('tests/client.js', 'client');
});


<template name="createCombinedFlex">

{{_ "Channels"}}

{{_ "Create_new" }}

				{{_ "Name"}}
				<input type="text" id="channel-name" class="required" dir="auto" placeholder="{{_ 'Enter_name_here'}}">

				{{_ "Private"}}

					<input type="checkbox" id="channel-type" {{privateSwitchDisabled}} {{privateSwitchChecked}}>

				{{_ "Read_only_channel"}}

					<input type="checkbox" id="channel-ro">

				{{_ "Select_users"}}
				{{> inputAutocomplete settings=autocompleteSettings id="channel-members" class="search" placeholder=(_ "Search_by_username") autocomplete="off"}}

					{{#each selectedUsers}}

{{.}}

					{{/each}}

			{{#if error.fields}}

					{{_ "Oops!"}}
					{{#each error.fields}}

{{_ "The_field_is_required" .}}

					{{/each}}

			{{/if}}
			{{#if error.invalid}}

					{{_ "Oops!"}}
					{{{_ "Invalid_room_name" roomName}}}

			{{/if}}
			{{#if error.duplicate}}

					{{_ "Oops!"}}
					{{{_ "Duplicate_channel_name" roomName}}}

			{{/if}}
			{{#if error.archivedduplicate}}

					{{_ "Oops!"}}
					{{{_ "Duplicate_archived_channel_name" roomName}}}

			{{/if}}

				<button class="button primary save-channel">{{_ "Create" }}</button>
				<button class="button cancel-channel">{{_ "Cancel" }}</button>

</template>


import toastr from 'toastr';
Meteor.startup(function() {
	RocketChat.MessageAction.addButton({
		id: 'star-message',
		icon: 'star',
		label: 'Star_Message',
		context: ['starred', 'message', 'message-mobile'],
		action() {
			const message = this._arguments[1];
			message.starred = Meteor.userId();
			Meteor.call('starMessage', message, function(error) {
				if (error) {
					return handleError(error);
				}
			});
		},
		condition(message) {
			if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null && RocketChat.settings.get('Message_AllowStarring')) {
				return false;
			}

			return !_.findWhere(message.starred, {_id: Meteor.userId()});
		},
		order: 10,
		group: 'menu'
	});

	RocketChat.MessageAction.addButton({
		id: 'unstar-message',
		icon: 'star',
		label: 'Unstar_Message',
		context: ['starred', 'message', 'message-mobile'],
		action() {
			const message = this._arguments[1];
			message.starred = false;
			Meteor.call('starMessage', message, function(error) {
				if (error) {
					handleError(error);
				}
			});
		},
		condition(message) {
			if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null && RocketChat.settings.get('Message_AllowStarring')) {
				return false;
			}

			return Boolean(_.findWhere(message.starred, {_id: Meteor.userId()}));
		},
		order: 10,
		group: 'menu'
	});

	RocketChat.MessageAction.addButton({
		id: 'jump-to-star-message',
		icon: 'jump',
		label: 'Jump_to_message',
		context: ['starred'],
		action() {
			const message = this._arguments[1];
			if (window.matchMedia('(max-width: 500px)').matches) {
				Template.instance().tabBar.close();
			}
			RoomHistoryManager.getSurroundingMessages(message, 50);
		},
		condition(message) {
			if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null) {
				return false;
			}
			return true;
		},
		order: 100,
		group: 'menu'
	});

	RocketChat.MessageAction.addButton({
		id: 'permalink-star',
		icon: 'permalink',
		label: 'Permalink',
		classes: 'clipboard',
		context: ['starred'],
		action(event) {
			const message = this._arguments[1];
			$(event.currentTarget).attr('data-clipboard-text', RocketChat.MessageAction.getPermaLink(message._id));
			toastr.success(TAPi18n.__('Copied'));
		},
		condition(message) {
			if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null) {
				return false;
			}
			return true;
		},
		order: 101,
		group: 'menu'
	});
});


}
get agent() {
return this._agent.get();
}
get showAgentEmail() {
return this._showAgentEmail.get();
}
set online(value) {
this._online.set(value);
}
set title(value) {

keep keep keep keep replace replace replace keep keep keep keep keep


RocketChat.validateCustomFields = function(fields) {
	// Special Case:
	// If an admin didn't set any custom fields there's nothing to validate against so consider any customFields valid
	if (s.trim(RocketChat.settings.get('Accounts_CustomFields')) === '') {
		return;
	}

	let customFieldsMeta;
	try {
		customFieldsMeta = JSON.parse(RocketChat.settings.get('Accounts_CustomFields'));
	} catch (e) {
		throw new Meteor.Error('error-invalid-customfield-json', 'Invalid JSON for Custom Fields');
	}

	const customFields = {};

	Object.keys(customFieldsMeta).forEach((fieldName) => {
		const field = customFieldsMeta[fieldName];

		customFields[fieldName] = fields[fieldName];
		const fieldValue = s.trim(fields[fieldName]);

		if (field.required && fieldValue === '') {
			throw new Meteor.Error('error-user-registration-custom-field', `Field ${ fieldName } is required`, {method: 'registerUser'});
		}

		if (field.type === 'select' && field.options.indexOf(fields[fieldName]) === -1) {
			throw new Meteor.Error('error-user-registration-custom-field', `Value for field ${ fieldName } is invalid`, {method: 'registerUser'});
		}

		if (field.maxLength && fieldValue.length > field.maxLength) {
			throw new Meteor.Error('error-user-registration-custom-field', `Max length of field ${ fieldName } ${ field.maxLength }`, {method: 'registerUser'});
		}

		if (field.minLength && fieldValue.length < field.minLength) {
			throw new Meteor.Error('error-user-registration-custom-field', `Min length of field ${ fieldName } ${ field.minLength }`, {method: 'registerUser'});
		}
	});
};


export NODE_ENV="production"
export LIVECHAT_DIR="../../../public/livechat"
export BUILD_DIR="../build"
export BUNDLE_DIR="../build/bundle/programs/web.browser"

cd packages/rocketchat-livechat/app
meteor npm install --production
meteor build --headless --directory $BUILD_DIR

rm -rf $LIVECHAT_DIR
mkdir -p $LIVECHAT_DIR
cp $BUNDLE_DIR/*.css $LIVECHAT_DIR/livechat.css
cp $BUNDLE_DIR/*.js $LIVECHAT_DIR/livechat.js

rm -rf ../public
mkdir -p ../public
cp $BUNDLE_DIR/head.html ../public/head.html

rm -rf $BUILD_DIR


Package.describe({
	name: 'rocketchat:push-notifications',
	version: '0.0.1',
	summary: 'Push Notifications Settings',
	git: ''
});

Package.onUse(function(api) {
	api.use([
		'ecmascript',
		'underscore',
		'rocketchat:lib'
	]);

	api.use('templating', 'client');

	api.addFiles([
		'client/stylesheets/pushNotifications.css',
		'client/views/pushNotificationsFlexTab.html',
		'client/views/pushNotificationsFlexTab.js',
		'client/tabBar.js'
	], 'client');

	api.addFiles([
		'server/methods/saveNotificationSettings.js',
		'server/models/Subscriptions.js'
	], 'server');
});


this.Livechat = new (class Livechat {
	constructor() {
		this._online = new ReactiveVar(null);

		this._customColor = new ReactiveVar();
		this._onlineColor = new ReactiveVar('#C1272D');
		this._offlineColor = new ReactiveVar('#666666');

		this._customFontColor = new ReactiveVar();
		this._onlineFontColor = new ReactiveVar('#FFFFFF');
		this._offlineFontColor = new ReactiveVar('#FFFFFF');

		this._title = new ReactiveVar('Rocket.Chat');
		this._registrationForm = new ReactiveVar(true);
		this._showSwitchDepartmentForm = new ReactiveVar(false);
		this._allowSwitchingDepartments = new ReactiveVar(false);
		this._offlineMessage = new ReactiveVar('');
		this._offlineUnavailableMessage = new ReactiveVar('');
		this._displayOfflineForm = new ReactiveVar(true);
		this._offlineSuccessMessage = new ReactiveVar(TAPi18n.__('Thanks_We_ll_get_back_to_you_soon'));
		this._videoCall = new ReactiveVar(false);
		this._transcriptMessage = new ReactiveVar('');
		this._connecting = new ReactiveVar(false);
		this._room = new ReactiveVar(null);
		this._department = new ReactiveVar(null);
		this._widgetOpened = new ReactiveVar(false);
		this._ready = new ReactiveVar(false);
		this._agent = new ReactiveVar();
		this._showAgentEmail = new ReactiveVar(true);

		this.stream = new Meteor.Streamer('livechat-room');

		Tracker.autorun(() => {
			if (this._room.get() && Meteor.userId()) {
				RoomHistoryManager.getMoreIfIsEmpty(this._room.get());
				visitor.subscribeToRoom(this._room.get());
				visitor.setRoom(this._room.get());

				Meteor.call('livechat:getAgentData', this._room.get(), (error, result) => {
					if (!error) {
						this._agent.set(result);
					}
				});
				this.stream.on(this._room.get(), (eventData) => {
					if (!eventData || !eventData.type) {
						return;
					}

					if (eventData.type === 'agentData') {
						this._agent.set(eventData.data);
					}
				});
			}
		});
	}

	get online() {
		return this._online.get();
	}
	get color() {
		if (!this._online.get()) {
			return this._offlineColor.get();
		}
		return this._customColor.get() || this._onlineColor.get();
	}
	get fontColor() {
		if (!this._online.get()) {
			return this._offlineFontColor.get();
		}
		return this._customFontColor.get() || this._onlineFontColor.get();
	}
	get title() {
		return this._title.get();
	}
	get registrationForm() {
		return this._registrationForm.get();
	}
	get showSwitchDepartmentForm() {
		return this._showSwitchDepartmentForm.get();
	}
	get allowSwitchingDepartments() {
		return this._allowSwitchingDepartments.get();
	}
	get offlineMessage() {
		return this._offlineMessage.get();
	}
	get offlineUnavailableMessage() {
		return this._offlineUnavailableMessage.get();
	}
	get displayOfflineForm() {
		return this._displayOfflineForm.get();
	}
	get offlineSuccessMessage() {
		return this._offlineSuccessMessage.get();
	}
	get videoCall() {
		return this._videoCall.get();
	}
	get transcriptMessage() {
		return this._transcriptMessage.get();
	}
	get department() {
		return this._department.get();
	}
	get connecting() {
		return this._connecting.get();
	}
	get agent() {
		return this._agent.get();
	}
	get showAgentEmail() {
		return this._showAgentEmail.get();
	}

	set online(value) {
		this._online.set(value);
	}
	set title(value) {
		this._title.set(value);
	}
	set registrationForm(value) {
		this._registrationForm.set(value);
	}
	set showSwitchDepartmentForm(value) {
		this._showSwitchDepartmentForm.set(value);
	}
	set allowSwitchingDepartments(value) {
		this._allowSwitchingDepartments.set(value);
	}
	set offlineMessage(value) {
		this._offlineMessage.set(value);
	}
	set offlineUnavailableMessage(value) {
		this._offlineUnavailableMessage.set(value);
	}
	set displayOfflineForm(value) {
		this._displayOfflineForm.set(value);
	}
	set offlineSuccessMessage(value) {
		this._offlineSuccessMessage.set(value);
	}
	set customColor(value) {
		this._customColor.set(value);
	}
	set onlineColor(value) {
		this._onlineColor.set(value);
	}
	set offlineColor(value) {
		this._offlineColor.set(value);
	}
	set customFontColor(value) {
		this._customFontColor.set(value);
	}
	set onlineFontColor(value) {
		this._onlineFontColor.set(value);
	}
	set offlineFontColor(value) {
		this._offlineFontColor.set(value);
	}
	set videoCall(value) {
		this._videoCall.set(value);
	}
	set transcriptMessage(value) {
		this._transcriptMessage.set(value);
	}
	set connecting(value) {
		this._connecting.set(value);
	}
	set room(roomId) {
		this._room.set(roomId);
	}
	set department(departmentId) {
		const dept = Department.findOne({ _id: departmentId }) || Department.findOne({ name: departmentId });

		if (dept) {
			this._department.set(dept._id);
		}
	}
	set agent(agentData) {
		this._agent.set(agentData);
	}
	set showAgentEmail(value) {
		this._showAgentEmail.set(value);
	}

	ready() {
		this._ready.set(true);
	}

	isReady() {
		return this._ready.get();
	}

	setWidgetOpened() {
		return this._widgetOpened.set(true);
	}

	setWidgetClosed() {
		return this._widgetOpened.set(false);
	}

	isWidgetOpened() {
		return this._widgetOpened.get();
	}
})();


this.LivechatCustomField = new Mongo.Collection('rocketchat_livechat_custom_field');


<template name="emojiEdit">
	{{#unless hasPermission 'manage-emoji'}}

{{_ "You_are_not_authorized_to_view_this_page"}}

	{{else}}

			<form class="edit-form" autocomplete="off">
				{{#if emoji}}

{{emoji.name}}

				{{else}}

{{_ "Custom_Emoji_Add"}}

				{{/if}}

					{{_ "Name"}}
					<input type="text" id="name" autocomplete="off" value="{{emoji.name}}" class="content-background-color">

					{{_ "Aliases"}}
					<input type="text" id="aliases" autocomplete="off" value="{{emoji.aliases}}"  class="content-background-color">

					{{_ "Image"}}
					<input id="image" type="file" />

					<button class='button button-block cancel' type="button">{{_ "Cancel"}}</button>
					<button class='button button-block primary save'>{{_ "Save"}}</button>

			</form>

	{{/unless}}
</template>


this.LivechatTrigger = new Mongo.Collection('rocketchat_livechat_trigger');


# Meteor packages used by this project, one per line.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.

rocketchat:cors
accounts-facebook@1.2.1
accounts-github@1.3.0
accounts-google@1.2.0
accounts-meteor-developer@1.3.0
accounts-password@1.4.0
accounts-twitter@1.3.0
blaze-html-templates
check@1.2.5
ddp-rate-limiter@1.0.7
ecmascript@0.8.3
ejson@1.0.14
email@1.2.3
fastclick@1.0.13
http@1.2.12
jquery@1.11.10
less@2.7.9
logging@1.1.17
meteor-base@1.1.0
mobile-experience@1.0.5
mongo@1.2.2
random@1.0.10
rate-limit@1.0.8
reactive-dict@1.1.9
reactive-var@1.0.11
reload@1.1.11
service-configuration@1.0.11
session@1.1.7
shell-server@0.2.4
spacebars
standard-minifier-js@2.1.2
tracker@1.1.3

rocketchat:2fa
rocketchat:action-links
rocketchat:analytics
rocketchat:api
rocketchat:assets
rocketchat:authorization
rocketchat:autolinker
rocketchat:autotranslate
rocketchat:bot-helpers
rocketchat:cas
rocketchat:channel-settings
rocketchat:channel-settings-mail-messages
rocketchat:colors
rocketchat:crowd
rocketchat:custom-oauth
rocketchat:custom-sounds
rocketchat:dolphin
rocketchat:emoji
rocketchat:emoji-custom
rocketchat:emoji-emojione
rocketchat:error-handler
rocketchat:favico
rocketchat:file
rocketchat:file-upload
rocketchat:github-enterprise
rocketchat:gitlab
rocketchat:google-vision
rocketchat:highlight-words
rocketchat:iframe-login
rocketchat:importer
rocketchat:importer-csv
rocketchat:importer-hipchat
rocketchat:importer-hipchat-enterprise
rocketchat:importer-slack
rocketchat:integrations
rocketchat:internal-hubot
rocketchat:irc
rocketchat:issuelinks
rocketchat:katex
rocketchat:ldap
rocketchat:lib
rocketchat:livechat
rocketchat:logger
rocketchat:login-token
rocketchat:mailer
rocketchat:mapview
rocketchat:markdown
rocketchat:mentions
rocketchat:mentions-flextab
rocketchat:message-attachments
rocketchat:message-mark-as-unread
rocketchat:message-pin
rocketchat:message-star
rocketchat:migrations
rocketchat:oauth2-server-config
rocketchat:oembed
rocketchat:otr
rocketchat:push-notifications
rocketchat:reactions
rocketchat:sandstorm
rocketchat:slackbridge
rocketchat:slashcommands-archive
rocketchat:slashcommands-asciiarts
rocketchat:slashcommands-create
rocketchat:slashcommands-help
rocketchat:slashcommands-invite
rocketchat:slashcommands-invite-all
rocketchat:slashcommands-join
rocketchat:slashcommands-kick
rocketchat:slashcommands-leave
rocketchat:slashcommands-me
rocketchat:slashcommands-msg
rocketchat:slashcommands-mute
rocketchat:slashcommands-open
rocketchat:slashcommands-topic
rocketchat:slashcommands-unarchive
rocketchat:slider
rocketchat:smarsh-connector
rocketchat:spotify
rocketchat:statistics
rocketchat:streamer
rocketchat:theme
rocketchat:tooltip
rocketchat:tutum
rocketchat:ui
rocketchat:ui-account
rocketchat:ui-admin
rocketchat:ui-flextab
rocketchat:ui-login
rocketchat:ui-master
rocketchat:ui-message
rocketchat:ui-sidenav
rocketchat:ui-vrecord
rocketchat:version
rocketchat:videobridge
rocketchat:webrtc
rocketchat:wordpress
rocketchat:message-snippet
#rocketchat:google-natural-language
rocketchat:drupal
rocketchat:monitoring
#rocketchat:chatops

konecty:change-case
konecty:delayed-task
konecty:mongo-counter
konecty:multiple-instances-status
konecty:nrr
konecty:user-presence

dispatch:run-as-user
francocatena:status
jalik:ufs
jalik:ufs-gridfs
jparker:gravatar
kadira:blaze-layout
kadira:flow-router
#kenton:accounts-sandstorm
mizzao:autocomplete
mizzao:timesync
mrt:reactive-store
mystor:device-detection
nimble:restivus
nooitaf:colors
ostrio:cookies
pauli:accounts-linkedin
percolate:synced-cron
raix:handlebar-helpers
raix:push
raix:ui-dropped-event
smoral:sweetalert
steffo:meteor-accounts-saml
tmeasday:crypto-md5
todda00:friendly-slugs
underscorestring:underscore.string
yasaricli:slugify
yasinuslu:blaze-meta
deepwell:bootstrap-datepicker2
rocketchat:postcss
dynamic-import@0.1.3
keepnox:perfect-scrollbar


/* eslint-env mocha */
/* globals expect */
/* eslint no-unused-vars: 0 */

import {getCredentials, api, login, request, credentials, apiEmail, apiUsername, targetUser, log} from '../../data/api-data.js';
import {adminEmail, password} from '../../data/user.js';
import {imgURL} from '../../data/interactions.js';
import {customFieldText, clearCustomFields, setCustomFields} from '../../data/custom-fields.js';

describe('[Users]', function() {
	this.retries(0);

	before(done => getCredentials(done));

	describe('[/users.create]', () => {
		before(done => clearCustomFields(done));
		after(done => clearCustomFields(done));

		it('should create a new user', (done) => {
			request.post(api('users.create'))
				.set(credentials)
				.send({
					email: apiEmail,
					name: apiUsername,
					username: apiUsername,
					password,
					active: true,
					roles: ['user'],
					joinDefaultChannels: true,
					verified: true
				})
				.expect('Content-Type', 'application/json')
				.expect(200)
				.expect((res) => {
					expect(res.body).to.have.property('success', true);
					expect(res.body).to.have.deep.property('user.username', apiUsername);
					expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
					expect(res.body).to.have.deep.property('user.active', true);
					expect(res.body).to.have.deep.property('user.name', apiUsername);

					expect(res.body).to.not.have.deep.property('user.customFields');

					targetUser._id = res.body.user._id;
				})
				.end(done);
		});

		it('should create a new user with custom fields', (done) => {
			setCustomFields({customFieldText}, (error) => {
				if (error) {
					return done(error);
				}

				const username = `customField_${ apiUsername }`;
				const email = `customField_${ apiEmail }`;
				const customFields = { customFieldText: 'success' };

				request.post(api('users.create'))
					.set(credentials)
					.send({
						email,
						name: username,
						username,
						password,
						active: true,
						roles: ['user'],
						joinDefaultChannels: true,
						verified: true,
						customFields
					})
					.expect('Content-Type', 'application/json')
					.expect(200)
					.expect((res) => {
						expect(res.body).to.have.property('success', true);
						expect(res.body).to.have.deep.property('user.username', username);
						expect(res.body).to.have.deep.property('user.emails[0].address', email);
						expect(res.body).to.have.deep.property('user.active', true);
						expect(res.body).to.have.deep.property('user.name', username);
						expect(res.body).to.have.deep.property('user.customFields.customFieldText', 'success');
					})
					.end(done);
			});
		});

		function failUserWithCustomField(field) {
			it(`should not create a user if a custom field ${ field.reason }`, (done) => {
				setCustomFields({ customFieldText }, (error) => {
					if (error) {
						return done(error);
					}

					const customFields = {};
					customFields[field.name] = field.value;

					request.post(api('users.create'))
						.set(credentials)
						.send({
							email: `customField_fail_${ apiEmail }`,
							name: `customField_fail_${ apiUsername }`,
							username: `customField_fail_${ apiUsername }`,
							password,
							active: true,
							roles: ['user'],
							joinDefaultChannels: true,
							verified: true,
							customFields
						})
						.expect('Content-Type', 'application/json')
						.expect(400)
						.expect((res) => {
							expect(res.body).to.have.property('success', false);
							expect(res.body).to.have.property('errorType', 'error-user-registration-custom-field');
						})
						.end(done);
				});
			});
		}

		[
			{name: 'customFieldText', value: '', reason: 'is required and missing'},
			{name: 'customFieldText', value: '0', reason: 'length is less than minLength'},
			{name: 'customFieldText', value: '0123456789-0', reason: 'length is more than maxLength'}
		].forEach((field) => { failUserWithCustomField(field); });
	});

	describe('[/users.info]', () => {
		it('should query information about a user by userId', (done) => {
			request.get(api('users.info'))
				.set(credentials)
				.query({
					userId: targetUser._id
				})
				.expect('Content-Type', 'application/json')
				.expect(200)
				.expect((res) => {
					expect(res.body).to.have.property('success', true);
					expect(res.body).to.have.deep.property('user.username', apiUsername);
					expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
					expect(res.body).to.have.deep.property('user.active', true);
					expect(res.body).to.have.deep.property('user.name', apiUsername);
				})
				.end(done);
		});
	});

	describe('[/users.getPresence]', () => {
		it('should query a user\'s presence by userId', (done) => {
			request.get(api('users.getPresence'))
				.set(credentials)
				.query({
					userId: targetUser._id
				})
				.expect('Content-Type', 'application/json')
				.expect(200)
				.expect((res) => {
					expect(res.body).to.have.property('success', true);
					expect(res.body).to.have.deep.property('presence', 'offline');
				})
				.end(done);
		});
	});

	describe('[/users.list]', () => {
		it('should query all users in the system', (done) => {
			request.get(api('users.list'))
				.set(credentials)
				.expect('Content-Type', 'application/json')
				.expect(200)
				.expect((res) => {
					expect(res.body).to.have.property('success', true);
					expect(res.body).to.have.property('count');
					expect(res.body).to.have.property('total');
				})
				.end(done);
		});

		it.skip('should query all users in the system by name', (done) => {
			//filtering user list
			request.get(api('users.list'))
				.set(credentials)
				.query({
					name: { '$regex': 'g' }
				})
				.field('username', 1)
				.sort('createdAt', -1)
				.expect(log)
				.expect('Content-Type', 'application/json')
				.expect(200)
				.expect((res) => {
					expect(res.body).to.have.property('success', true);
					expect(res.body).to.have.property('count');
					expect(res.body).to.have.property('total');
				})
				.end(done);
		});
	});

	describe('[/users.setAvatar]', () => {
		it.skip('should set the avatar of the auth user', (done) => {
			request.post(api('users.setAvatar'))
				.set(credentials)
				.attach('avatarUrl', imgURL)
				.expect('Content-Type', 'application/json')
				.expect(200)
				.expect((res) => {
					expect(res.body).to.have.property('success', true);
				})
				.end(done);
		});
	});

	describe('[/users.update]', () => {
		it('should update a user\'s info by userId', (done) => {
			request.post(api('users.update'))
				.set(credentials)
				.send({
					userId: targetUser._id,
					data :{
						email: apiEmail,
						name: `edited${ apiUsername }`,
						username: `edited${ apiUsername }`,
						password,
						active: true,
						roles: ['user']
					}
				})
				.expect('Content-Type', 'application/json')
				.expect(200)
				.expect((res) => {
					expect(res.body).to.have.property('success', true);
					expect(res.body).to.have.deep.property('user.username', `edited${ apiUsername }`);
					expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail);
					expect(res.body).to.have.deep.property('user.active', true);
					expect(res.body).to.have.deep.property('user.name', `edited${ apiUsername }`);
				})
				.end(done);
		});
	});

	describe('[/users.createToken]', () => {
		let user;
		beforeEach((done) => {
			const username = `user.test.${ Date.now() }`;
			const email = `${ username }@rocket.chat`;
			request.post(api('users.create'))
				.set(credentials)
				.send({ email, name: username, username, password })
				.end((err, res) => {
					user = res.body.user;
					done();
				});
		});

		let userCredentials;
		beforeEach((done) => {
			request.post(api('login'))
				.send({
					user: user.username,
					password
				})
				.expect('Content-Type', 'application/json')
				.expect(200)
				.expect((res) => {
					userCredentials = {};
					userCredentials['X-Auth-Token'] = res.body.data.authToken;
					userCredentials['X-User-Id'] = res.body.data.userId;
				})
				.end(done);
		});
		afterEach(done => {
			request.post(api('users.delete')).set(credentials).send({
				userId: user._id
			}).end(done);
			user = undefined;
		});

		describe('logged as admin:', () => {
			it('should return the user id and a new token', (done) => {
				request.post(api('users.createToken'))
					.set(credentials)
					.send({
						username: user.username
					})
					.expect('Content-Type', 'application/json')
					.expect(200)
					.expect((res) => {
						expect(res.body).to.have.property('success', true);
						expect(res.body).to.have.deep.property('data.userId', user._id);
						expect(res.body).to.have.deep.property('data.authToken');
					})
					.end(done);
			});
		});

		describe('logged as itself:', () => {
			it('should return the user id and a new token', (done) => {
				request.post(api('users.createToken'))
					.set(userCredentials)
					.send({
						username: user.username
					})
					.expect('Content-Type', 'application/json')
					.expect(200)
					.expect((res) => {
						expect(res.body).to.have.property('success', true);
						expect(res.body).to.have.deep.property('data.userId', user._id);
						expect(res.body).to.have.deep.property('data.authToken');
					})
					.end(done);
			});
		});

		describe('As an user not allowed:', () => {
			it('should return 401 unauthorized', (done) => {
				request.post(api('users.createToken'))
					.set(userCredentials)
					.send({
						username: 'rocket.cat'
					})
					.expect('Content-Type', 'application/json')
					.expect(400)
					.expect((res) => {
						expect(res.body).to.have.property('errorType');
						expect(res.body).to.have.property('error');
					})
					.end(done);
			});
		});

		describe('Not logged in:', () => {
			it('should return 401 unauthorized', (done) => {
				request.post(api('users.createToken'))
					.send({
						username: user.username
					})
					.expect('Content-Type', 'application/json')
					.expect(401)
					.expect((res) => {
						expect(res.body).to.have.property('message');
					})
					.end(done);
			});
		});

		describe('Testing if the returned token is valid:', (done) => {
			it('should return 200', (done) => {
				return request.post(api('users.createToken'))
					.set(credentials)
					.send({ username: user.username })
					.expect('Content-Type', 'application/json')
					.end((err, res) => {
						return err ? done () : request.get(api('me'))
							.set({ 'X-Auth-Token': `${ res.body.data.authToken }`, 'X-User-Id': res.body.data.userId })
							.expect(200)
							.expect((res) => {
								expect(res.body).to.have.property('success', true);
							})
							.end(done);
					});
			});
		});
	});
});


if (Livechat.showAgentEmail && agent.emails && agent.emails[0] && agent.emails[0].address) {

if (agent.customFields && agent.customFields.phone) {

keep keep keep keep keep replace keep keep keep keep

if (agent.emails && agent.emails[0] && agent.emails[0].address) {

/* globals HTTP */
import UAParser from 'ua-parser-js';

RocketChat.Livechat = {
	historyMonitorType: 'url',

	logger: new Logger('Livechat', {
		sections: {
			webhook: 'Webhook'
		}
	}),

	getNextAgent(department) {
		if (department) {
			return RocketChat.models.LivechatDepartmentAgents.getNextAgentForDepartment(department);
		} else {
			return RocketChat.models.Users.getNextAgent();
		}
	},
	getAgents(department) {
		if (department) {
			return RocketChat.models.LivechatDepartmentAgents.findByDepartmentId(department);
		} else {
			return RocketChat.models.Users.findAgents();
		}
	},
	getOnlineAgents(department) {
		if (department) {
			return RocketChat.models.LivechatDepartmentAgents.getOnlineForDepartment(department);
		} else {
			return RocketChat.models.Users.findOnlineAgents();
		}
	},
	getRoom(guest, message, roomInfo) {
		let room = RocketChat.models.Rooms.findOneById(message.rid);
		let newRoom = false;

		if (room && !room.open) {
			message.rid = Random.id();
			room = null;
		}

		if (room == null) {
			// if no department selected verify if there is at least one active and pick the first
			if (!guest.department) {
				const departments = RocketChat.models.LivechatDepartment.findEnabledWithAgents();
				if (departments.count() > 0) {
					departments.forEach((dept) => {
						if (!guest.department && dept.showOnRegistration) {
							guest.department = dept._id;
						}
					});
				}
			}

			// delegate room creation to QueueMethods
			const routingMethod = RocketChat.settings.get('Livechat_Routing_Method');
			room = RocketChat.QueueMethods[routingMethod](guest, message, roomInfo);

			newRoom = true;
		} else {
			room = Meteor.call('canAccessRoom', message.rid, guest._id);
		}
		if (!room) {
			throw new Meteor.Error('cannot-acess-room');
		}

		return { room, newRoom };
	},
	sendMessage({ guest, message, roomInfo }) {
		const { room, newRoom } = this.getRoom(guest, message, roomInfo);
		if (guest.name) {
			message.alias = guest.name;
		}

		// return messages;
		return _.extend(RocketChat.sendMessage(guest, message, room), { newRoom, showConnecting: this.showConnecting() });
	},
	registerGuest({ token, name, email, department, phone, loginToken, username } = {}) {
		check(token, String);

		let userId;
		const updateUser = {
			$set: {
				profile: {
					guest: true,
					token
				}
			}
		};

		const user = RocketChat.models.Users.getVisitorByToken(token, { fields: { _id: 1 } });

		if (user) {
			userId = user._id;
			if (loginToken) {
				if (!updateUser.$addToSet) {
					updateUser.$addToSet = {};
				}
				updateUser.$addToSet['services.resume.loginTokens'] = loginToken;
			}
		} else {
			if (!username) {
				username = RocketChat.models.Users.getNextVisitorUsername();
			}

			let existingUser = null;

			if (s.trim(email) !== '' && (existingUser = RocketChat.models.Users.findOneGuestByEmailAddress(email))) {
				if (loginToken) {
					if (!updateUser.$addToSet) {
						updateUser.$addToSet = {};
					}
					updateUser.$addToSet['services.resume.loginTokens'] = loginToken;
				}

				userId = existingUser._id;
			} else {

				const userData = {
					username,
					globalRoles: ['livechat-guest'],
					department,
					type: 'visitor',
					joinDefaultChannels: false
				};

				if (this.connection) {
					userData.userAgent = this.connection.httpHeaders['user-agent'];
					userData.ip = this.connection.httpHeaders['x-real-ip'] || this.connection.httpHeaders['x-forwarded-for'] || this.connection.clientAddress;
					userData.host = this.connection.httpHeaders.host;
				}

				userId = Accounts.insertUserDoc({}, userData);

				if (loginToken) {
					updateUser.$set.services = {
						resume: {
							loginTokens: [ loginToken ]
						}
					};
				}
			}
		}

		if (phone) {
			updateUser.$set.phone = [
				{ phoneNumber: phone.number }
			];
		}

		if (email && email.trim() !== '') {
			updateUser.$set.visitorEmails = [
				{ address: email }
			];
		}

		if (name) {
			RocketChat._setRealName(userId, name);
		}

		Meteor.users.update(userId, updateUser);

		return userId;
	},
	setDepartmentForGuest({ token, department } = {}) {
		check(token, String);

		const updateUser = {
			$set: {
				department
			}
		};

		const user = RocketChat.models.Users.getVisitorByToken(token, { fields: { _id: 1 } });
		if (user) {
			return Meteor.users.update(user._id, updateUser);
		}
		return false;
	},
	saveGuest({ _id, name, email, phone }) {
		const updateData = {};

		if (name) {
			updateData.name = name;
		}
		if (email) {
			updateData.email = email;
		}
		if (phone) {
			updateData.phone = phone;
		}
		const ret = RocketChat.models.Users.saveGuestById(_id, updateData);

		Meteor.defer(() => {
			RocketChat.callbacks.run('livechat.saveGuest', updateData);
		});

		return ret;
	},

	closeRoom({ user, room, comment }) {
		const now = new Date();
		RocketChat.models.Rooms.closeByRoomId(room._id, {
			user: {
				_id: user._id,
				username: user.username
			},
			closedAt: now,
			chatDuration: (now.getTime() - room.ts) / 1000
		});

		const message = {
			t: 'livechat-close',
			msg: comment,
			groupable: false
		};

		RocketChat.sendMessage(user, message, room);

		RocketChat.models.Subscriptions.hideByRoomIdAndUserId(room._id, user._id);
		RocketChat.models.Messages.createCommandWithRoomIdAndUser('promptTranscript', room._id, user);

		Meteor.defer(() => {
			RocketChat.callbacks.run('livechat.closeRoom', room);
		});

		return true;
	},

	getInitSettings() {
		const settings = {};

		RocketChat.models.Settings.findNotHiddenPublic([
			'Livechat_title',
			'Livechat_title_color',
			'Livechat_enabled',
			'Livechat_show_agent_email',
			'Livechat_registration_form',
			'Livechat_allow_switching_departments',
			'Livechat_offline_title',
			'Livechat_offline_title_color',
			'Livechat_offline_message',
			'Livechat_offline_success_message',
			'Livechat_offline_form_unavailable',
			'Livechat_display_offline_form',
			'Livechat_videocall_enabled',
			'Jitsi_Enabled',
			'Language',
			'Livechat_enable_transcript',
			'Livechat_transcript_message'
		]).forEach((setting) => {
			settings[setting._id] = setting.value;
		});

		return settings;
	},

	saveRoomInfo(roomData, guestData) {
		if ((roomData.topic != null || roomData.tags != null) && !RocketChat.models.Rooms.setTopicAndTagsById(roomData._id, roomData.topic, roomData.tags)) {
			return false;
		}

		Meteor.defer(() => {
			RocketChat.callbacks.run('livechat.saveRoom', roomData);
		});

		if (!_.isEmpty(guestData.name)) {
			return RocketChat.models.Rooms.setLabelByRoomId(roomData._id, guestData.name) && RocketChat.models.Subscriptions.updateNameByRoomId(roomData._id, guestData.name);
		}
	},

	closeOpenChats(userId, comment) {
		const user = RocketChat.models.Users.findOneById(userId);
		RocketChat.models.Rooms.findOpenByAgent(userId).forEach((room) => {
			this.closeRoom({ user, room, comment});
		});
	},

	forwardOpenChats(userId) {
		RocketChat.models.Rooms.findOpenByAgent(userId).forEach((room) => {
			const guest = RocketChat.models.Users.findOneById(room.v._id);
			this.transfer(room, guest, { departmentId: guest.department });
		});
	},

	savePageHistory(token, pageInfo) {
		if (pageInfo.change === RocketChat.Livechat.historyMonitorType) {
			return RocketChat.models.LivechatPageVisited.saveByToken(token, pageInfo);
		}

		return;
	},

	transfer(room, guest, transferData) {
		let agent;

		if (transferData.userId) {
			const user = RocketChat.models.Users.findOneById(transferData.userId);
			agent = {
				agentId: user._id,
				username: user.username
			};
		} else {
			agent = RocketChat.Livechat.getNextAgent(transferData.departmentId);
		}

		const servedBy = room.servedBy;

		if (agent && agent.agentId !== servedBy._id) {
			room.usernames = _.without(room.usernames, servedBy.username).concat(agent.username);

			RocketChat.models.Rooms.changeAgentByRoomId(room._id, agent);

			const subscriptionData = {
				rid: room._id,
				name: guest.name || guest.username,
				alert: true,
				open: true,
				unread: 1,
				userMentions: 1,
				groupMentions: 0,
				code: room.code,
				u: {
					_id: agent.agentId,
					username: agent.username
				},
				t: 'l',
				desktopNotifications: 'all',
				mobilePushNotifications: 'all',
				emailNotifications: 'all'
			};
			RocketChat.models.Subscriptions.removeByRoomIdAndUserId(room._id, servedBy._id);

			RocketChat.models.Subscriptions.insert(subscriptionData);

			RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(room._id, { _id: servedBy._id, username: servedBy.username });
			RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(room._id, { _id: agent.agentId, username: agent.username });

			RocketChat.Livechat.stream.emit(room._id, {
				type: 'agentData',
				data: RocketChat.models.Users.getAgentInfo(agent.agentId)
			});

			return true;
		}

		return false;
	},

	sendRequest(postData, callback, trying = 1) {
		try {
			const options = {
				headers: {
					'X-RocketChat-Livechat-Token': RocketChat.settings.get('Livechat_secret_token')
				},
				data: postData
			};
			return HTTP.post(RocketChat.settings.get('Livechat_webhookUrl'), options);
		} catch (e) {
			RocketChat.Livechat.logger.webhook.error(`Response error on ${ trying } try ->`, e);
			// try 10 times after 10 seconds each
			if (trying < 10) {
				RocketChat.Livechat.logger.webhook.warn('Will try again in 10 seconds ...');
				trying++;
				setTimeout(Meteor.bindEnvironment(() => {
					RocketChat.Livechat.sendRequest(postData, callback, trying);
				}), 10000);
			}
		}
	},

	getLivechatRoomGuestInfo(room) {
		const visitor = RocketChat.models.Users.findOneById(room.v._id);
		const agent = RocketChat.models.Users.findOneById(room.servedBy._id);

		const ua = new UAParser();
		ua.setUA(visitor.userAgent);

		const postData = {
			_id: room._id,
			label: room.label,
			topic: room.topic,
			code: room.code,
			createdAt: room.ts,
			lastMessageAt: room.lm,
			tags: room.tags,
			customFields: room.livechatData,
			visitor: {
				_id: visitor._id,
				name: visitor.name,
				username: visitor.username,
				email: null,
				phone: null,
				department: visitor.department,
				ip: visitor.ip,
				os: ua.getOS().name && (`${ ua.getOS().name } ${ ua.getOS().version }`),
				browser: ua.getBrowser().name && (`${ ua.getBrowser().name } ${ ua.getBrowser().version }`),
				customFields: visitor.livechatData
			},
			agent: {
				_id: agent._id,
				username: agent.username,
				name: agent.name,
				email: null
			}
		};

		if (room.crmData) {
			postData.crmData = room.crmData;
		}

		if (visitor.visitorEmails && visitor.visitorEmails.length > 0) {
			postData.visitor.email = visitor.visitorEmails[0].address;
		}
		if (visitor.phone && visitor.phone.length > 0) {
			postData.visitor.phone = visitor.phone[0].phoneNumber;
		}

		if (agent.emails && agent.emails.length > 0) {
			postData.agent.email = agent.emails[0].address;
		}

		return postData;
	},

	addAgent(username) {
		check(username, String);

		const user = RocketChat.models.Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } });

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:addAgent' });
		}

		if (RocketChat.authz.addUserRoles(user._id, 'livechat-agent')) {
			RocketChat.models.Users.setOperator(user._id, true);
			RocketChat.models.Users.setLivechatStatus(user._id, 'available');
			return user;
		}

		return false;
	},

	addManager(username) {
		check(username, String);

		const user = RocketChat.models.Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } });

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:addManager' });
		}

		if (RocketChat.authz.addUserRoles(user._id, 'livechat-manager')) {
			return user;
		}

		return false;
	},

	removeAgent(username) {
		check(username, String);

		const user = RocketChat.models.Users.findOneByUsername(username, { fields: { _id: 1 } });

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:removeAgent' });
		}

		if (RocketChat.authz.removeUserFromRoles(user._id, 'livechat-agent')) {
			RocketChat.models.Users.setOperator(user._id, false);
			RocketChat.models.Users.setLivechatStatus(user._id, 'not-available');
			return true;
		}

		return false;
	},

	removeManager(username) {
		check(username, String);

		const user = RocketChat.models.Users.findOneByUsername(username, { fields: { _id: 1 } });

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:removeManager' });
		}

		return RocketChat.authz.removeUserFromRoles(user._id, 'livechat-manager');
	},

	saveDepartment(_id, departmentData, departmentAgents) {
		check(_id, Match.Maybe(String));

		check(departmentData, {
			enabled: Boolean,
			name: String,
			description: Match.Optional(String),
			showOnRegistration: Boolean
		});

		check(departmentAgents, [
			Match.ObjectIncluding({
				agentId: String,
				username: String
			})
		]);

		if (_id) {
			const department = RocketChat.models.LivechatDepartment.findOneById(_id);
			if (!department) {
				throw new Meteor.Error('error-department-not-found', 'Department not found', { method: 'livechat:saveDepartment' });
			}
		}

		return RocketChat.models.LivechatDepartment.createOrUpdateDepartment(_id, departmentData, departmentAgents);
	},

	removeDepartment(_id) {
		check(_id, String);

		const department = RocketChat.models.LivechatDepartment.findOneById(_id, { fields: { _id: 1 } });

		if (!department) {
			throw new Meteor.Error('department-not-found', 'Department not found', { method: 'livechat:removeDepartment' });
		}

		return RocketChat.models.LivechatDepartment.removeById(_id);
	},

	showConnecting() {
		if (RocketChat.settings.get('Livechat_Routing_Method') === 'Guest_Pool') {
			return RocketChat.settings.get('Livechat_open_inquiery_show_connecting');
		} else {
			return false;
		}
	}
};

RocketChat.Livechat.stream = new Meteor.Streamer('livechat-room');
RocketChat.Livechat.stream.allowRead('logged');

RocketChat.settings.get('Livechat_history_monitor_type', (key, value) => {
	RocketChat.Livechat.historyMonitorType = value;
});


//! moment.js locale configuration
//! locale : Arabic (Morocco) [ar-ma]
//! author : ElFadili Yassine : https://github.com/ElFadiliY
//! author : Abdel Said : https://github.com/abdelsaid

;(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined'
       && typeof require === 'function' ? factory(require('../moment')) :
   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
   factory(global.moment)
}(this, function (moment) { 'use strict';

    var ar_ma = moment.defineLocale('ar-ma', {
        months : 'يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر'.split('_'),
        monthsShort : 'يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر'.split('_'),
        weekdays : 'الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'),
        weekdaysShort : 'احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت'.split('_'),
        weekdaysMin : 'ح_ن_ث_ر_خ_ج_س'.split('_'),
        weekdaysParseExact : true,
        longDateFormat : {
            LT : 'HH:mm',
            LTS : 'HH:mm:ss',
            L : 'DD/MM/YYYY',
            LL : 'D MMMM YYYY',
            LLL : 'D MMMM YYYY HH:mm',
            LLLL : 'dddd D MMMM YYYY HH:mm'
        },
        calendar : {
            sameDay: '[اليوم على الساعة] LT',
            nextDay: '[غدا على الساعة] LT',
            nextWeek: 'dddd [على الساعة] LT',
            lastDay: '[أمس على الساعة] LT',
            lastWeek: 'dddd [على الساعة] LT',
            sameElse: 'L'
        },
        relativeTime : {
            future : 'في %s',
            past : 'منذ %s',
            s : 'ثوان',
            m : 'دقيقة',
            mm : '%d دقائق',
            h : 'ساعة',
            hh : '%d ساعات',
            d : 'يوم',
            dd : '%d أيام',
            M : 'شهر',
            MM : '%d أشهر',
            y : 'سنة',
            yy : '%d سنوات'
        },
        week : {
            dow : 6, // Saturday is the first day of the week.
            doy : 12  // The week that contains Jan 1st is the first week of the year.
        }
    });

    return ar_ma;

}));


/* globals Importer */
// "ENUM" of the import step, the value is the translation string
Importer.ProgressStep = Object.freeze({
	NEW: 'importer_new',
	PREPARING_STARTED: 'importer_preparing_started',
	PREPARING_USERS: 'importer_preparing_users',
	PREPARING_CHANNELS: 'importer_preparing_channels',
	PREPARING_MESSAGES: 'importer_preparing_messages',
	USER_SELECTION: 'importer_user_selection',
	IMPORTING_STARTED: 'importer_importing_started',
	IMPORTING_USERS: 'importer_importing_users',
	IMPORTING_CHANNELS: 'importer_importing_channels',
	IMPORTING_MESSAGES: 'importer_importing_messages',
	FINISHING: 'importer_finishing',
	DONE: 'importer_done',
	ERROR: 'importer_import_failed',
	CANCELLED: 'importer_import_cancelled'
});


RocketChat.settings.add('IssueLinks_Enabled', false, {
	type: 'boolean',
	i18nLabel: 'Enabled',
	i18nDescription: 'IssueLinks_Incompatible',
	group: 'Message',
	section: 'Issue_Links',
	public: true
});

RocketChat.settings.add('IssueLinks_Template', '', {
	type: 'string',
	i18nLabel: 'IssueLinks_LinkTemplate',
	i18nDescription: 'IssueLinks_LinkTemplate_Description',
	group: 'Message',
	section: 'Issue_Links',
	public: true
});


keep keep keep replace keep keep keep keep

/**
 * Sets an user as (non)operator
 * @param {string} _id - User's _id
 * @param {boolean} operator - Flag to set as operator or not
 */
RocketChat.models.Users.setOperator = function(_id, operator) {
	const update = {
		$set: {
			operator
		}
	};

	return this.update(_id, update);
};

/**
 * Gets all online agents
 * @return
 */
RocketChat.models.Users.findOnlineAgents = function() {
	const query = {
		status: {
			$exists: true,
			$ne: 'offline'
		},
		statusLivechat: 'available',
		roles: 'livechat-agent'
	};

	return this.find(query);
};

/**
 * Gets all agents
 * @return
 */
RocketChat.models.Users.findAgents = function() {
	const query = {
		roles: 'livechat-agent'
	};

	return this.find(query);
};

/**
 * Find online users from a list
 * @param {array} userList - array of usernames
 * @return
 */
RocketChat.models.Users.findOnlineUserFromList = function(userList) {
	const query = {
		status: {
			$exists: true,
			$ne: 'offline'
		},
		statusLivechat: 'available',
		roles: 'livechat-agent',
		username: {
			$in: [].concat(userList)
		}
	};

	return this.find(query);
};

/**
 * Get next user agent in order
 * @return {object} User from db
 */
RocketChat.models.Users.getNextAgent = function() {
	const query = {
		status: {
			$exists: true,
			$ne: 'offline'
		},
		statusLivechat: 'available',
		roles: 'livechat-agent'
	};

	const collectionObj = this.model.rawCollection();
	const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj);

	const sort = {
		livechatCount: 1,
		username: 1
	};

	const update = {
		$inc: {
			livechatCount: 1
		}
	};

	const user = findAndModify(query, sort, update);
	if (user && user.value) {
		return {
			agentId: user.value._id,
			username: user.value.username
		};
	} else {
		return null;
	}
};

/**
 * Gets visitor by token
 * @param {string} token - Visitor token
 */
RocketChat.models.Users.getVisitorByToken = function(token, options) {
	const query = {
		'profile.guest': true,
		'profile.token': token
	};

	return this.findOne(query, options);
};

/**
 * Gets visitor by token
 * @param {string} token - Visitor token
 */
RocketChat.models.Users.findVisitorByToken = function(token) {
	const query = {
		'profile.guest': true,
		'profile.token': token
	};

	return this.find(query);
};

/**
 * Change user's livechat status
 * @param {string} token - Visitor token
 */
RocketChat.models.Users.setLivechatStatus = function(userId, status) {
	const query = {
		'_id': userId
	};

	const update = {
		$set: {
			'statusLivechat': status
		}
	};

	return this.update(query, update);
};

/**
 * change all livechat agents livechat status to "not-available"
 */
RocketChat.models.Users.closeOffice = function() {
	self = this;
	self.findAgents().forEach(function(agent) {
		self.setLivechatStatus(agent._id, 'not-available');
	});
};

/**
 * change all livechat agents livechat status to "available"
 */
RocketChat.models.Users.openOffice = function() {
	self = this;
	self.findAgents().forEach(function(agent) {
		self.setLivechatStatus(agent._id, 'available');
	});
};

RocketChat.models.Users.updateLivechatDataByToken = function(token, key, value, overwrite = true) {
	const query = {
		'profile.token': token
	};

	if (!overwrite) {
		const user = this.findOne(query, { fields: { livechatData: 1 } });
		if (user.livechatData && typeof user.livechatData[key] !== 'undefined') {
			return true;
		}
	}

	const update = {
		$set: {
			[`livechatData.${ key }`]: value
		}
	};

	return this.update(query, update);
};

/**
 * Find a visitor by their phone number
 * @return {object} User from db
 */
RocketChat.models.Users.findOneVisitorByPhone = function(phone) {
	const query = {
		'phone.phoneNumber': phone
	};

	return this.findOne(query);
};

/**
 * Get the next visitor name
 * @return {string} The next visitor name
 */
RocketChat.models.Users.getNextVisitorUsername = function() {
	const settingsRaw = RocketChat.models.Settings.model.rawCollection();
	const findAndModify = Meteor.wrapAsync(settingsRaw.findAndModify, settingsRaw);

	const query = {
		_id: 'Livechat_guest_count'
	};

	const update = {
		$inc: {
			value: 1
		}
	};

	const livechatCount = findAndModify(query, null, update);

	return `guest-${ livechatCount.value.value + 1 }`;
};

RocketChat.models.Users.saveGuestById = function(_id, data) {
	const setData = {};
	const unsetData = {};

	if (data.name) {
		if (!_.isEmpty(s.trim(data.name))) {
			setData.name = s.trim(data.name);
		} else {
			unsetData.name = 1;
		}
	}

	if (data.email) {
		if (!_.isEmpty(s.trim(data.email))) {
			setData.visitorEmails = [
				{ address: s.trim(data.email) }
			];
		} else {
			unsetData.visitorEmails = 1;
		}
	}

	if (data.phone) {
		if (!_.isEmpty(s.trim(data.phone))) {
			setData.phone = [
				{ phoneNumber: s.trim(data.phone) }
			];
		} else {
			unsetData.phone = 1;
		}
	}

	const update = {};

	if (!_.isEmpty(setData)) {
		update.$set = setData;
	}

	if (!_.isEmpty(unsetData)) {
		update.$unset = unsetData;
	}

	if (_.isEmpty(update)) {
		return true;
	}

	return this.update({ _id }, update);
};

RocketChat.models.Users.findOneGuestByEmailAddress = function(emailAddress) {
	const query = {
		'visitorEmails.address': new RegExp(`^${ s.escapeRegExp(emailAddress) }$`, 'i')
	};

	return this.findOne(query);
};

RocketChat.models.Users.getAgentInfo = function(agentId) {
	const query = {
		_id: agentId
	};

	const options = {
		fields: {
			name: 1,
			username: 1,
			emails: 1,
			customFields: 1
		}
	};

	return this.findOne(query, options);
};


// TODO: remove global
this.getAvatarUrlFromUsername = function(username) {
	const key = `avatar_random_${ username }`;
	const random = typeof Session !== 'undefined' ? Session.keys[key] : 0;
	if (username == null) {
		return;
	}
	const cdnPrefix = (RocketChat.settings.get('CDN_PREFIX') || '').trim().replace(/\/$/, '');
	const pathPrefix = (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '').trim().replace(/\/$/, '');
	let path = pathPrefix;
	if (cdnPrefix) {
		path = cdnPrefix + pathPrefix;
	} else if (Meteor.isCordova) {
		path = Meteor.absoluteUrl().replace(/\/$/, '');
	}
	return `${ path }/avatar/${ encodeURIComponent(username) }?_dc=${ random }`;
};


<template name="adminEmojiEdit">

			{{> emojiEdit .}}

</template>


if (visitor.phone && visitor.phone.length > 0) {

if (agent.emails && agent.emails.length > 0) {


RocketChat.authz.addUserRoles = function(userId, roleNames, scope) {
	if (!userId || !roleNames) {
		return false;
	}

	const user = RocketChat.models.Users.db.findOneById(userId);
	if (!user) {
		throw new Meteor.Error('error-invalid-user', 'Invalid user', {
			function: 'RocketChat.authz.addUserRoles'
		});
	}

	roleNames = [].concat(roleNames);
	const existingRoleNames = _.pluck(RocketChat.authz.getRoles(), '_id');
	const invalidRoleNames = _.difference(roleNames, existingRoleNames);

	if (!_.isEmpty(invalidRoleNames)) {
		for (const role of invalidRoleNames) {
			RocketChat.models.Roles.createOrUpdate(role);
		}
	}

	RocketChat.models.Roles.addUserRoles(userId, roleNames, scope);

	return true;
};


/* globals Google */

function getIdentity(accessToken) {
	try {
		return HTTP.get(
			'https://www.googleapis.com/oauth2/v1/userinfo',
			{params: {access_token: accessToken}}).data;
	} catch (err) {
		throw _.extend(new Error(`Failed to fetch identity from Google. ${ err.message }`), {response: err.response});
	}
}

function getScopes(accessToken) {
	try {
		return HTTP.get(
			'https://www.googleapis.com/oauth2/v1/tokeninfo',
			{params: {access_token: accessToken}}).data.scope.split(' ');
	} catch (err) {
		throw _.extend(new Error(`Failed to fetch tokeninfo from Google. ${ err.message }`), {response: err.response});
	}
}

RocketChat.registerAccessTokenService('google', function(options) {
	check(options, Match.ObjectIncluding({
		accessToken: String,
		idToken: String,
		expiresIn: Match.Integer,
		scope: Match.Maybe(String),
		identity: Match.Maybe(Object)
	}));

	const identity = options.identity || getIdentity(options.accessToken);

	const serviceData = {
		accessToken: options.accessToken,
		idToken: options.idToken,
		expiresAt: (+new Date) + (1000 * parseInt(options.expiresIn, 10)),
		scope: options.scopes || getScopes(options.accessToken)
	};

	const fields = _.pick(identity, Google.whitelistedFields);
	_.extend(serviceData, fields);

	// only set the token in serviceData if it's there. this ensures
	// that we don't lose old ones (since we only get this on the first
	// log in attempt)
	if (options.refreshToken) {
		serviceData.refreshToken = options.refreshToken;
	}

	return {
		serviceData,
		options: {
			profile: {
				name: identity.name
			}
		}
	};
});


RocketChat.applyCustomTranslations = function applyCustomTranslations() {
	let CustomTranslations = RocketChat.settings.get('Custom_Translations');
	if (typeof CustomTranslations === 'string' && CustomTranslations.trim() !== '') {
		try {
			CustomTranslations = JSON.parse(CustomTranslations);

			for (const lang in CustomTranslations) {
				if (CustomTranslations.hasOwnProperty(lang)) {
					const translations = CustomTranslations[lang];
					TAPi18next.addResourceBundle(lang, 'project', translations);
				}
			}
			TAPi18n._language_changed_tracker.changed();
		} catch (e) {
			console.error('Invalid setting Custom_Translations', e);
		}
	}
};

Meteor.startup(function() {
	Meteor.autorun(function() {
		// Re apply translations if tap language was changed
		Session.get(TAPi18n._loaded_lang_session_key);
		RocketChat.applyCustomTranslations();
	});
});


const room = RocketChat.models.Rooms.findOpenByVisitorToken(visitorToken, {

keep keep keep keep replace keep keep keep keep


Czy jesteś pewny że chcesz zakończyć rozmowę?

Czy jesteś pewny że chcesz zakończyć ten czat i zmienić departament?

Jak bardzo byłeś/aś zadowolony/a z tej rozmowy?

Proszę poświęć chwilę, aby odpowiedzieć na krótką ankietę na temat tej rozmowy

Oceń każde pytanie zgodnie z zadowolenia, 1 co oznacza, że są całkowicie niedosyt i 5 co oznacza, że są w pełni zadowoleni.

Dziękujemy! Odezwiemy się do Ciebie wkrótce.

Jesteśmy offline. Przepraszamy za niedogodności.


class EmojiCustom extends RocketChat.models._Base {
	constructor() {
		super();
		this._initModel('custom_emoji');
	}
}

RocketChat.models.EmojiCustom = new EmojiCustom();


<template name="options">

			{{#if showDepartments}}

<button class="switch-department"> {{_ "Switch_department"}}</button>

			{{/if}}

<button class="end-chat"> {{_ "End_chat"}}</button>

</template>


Template.createCombinedFlex.helpers({
	selectedUsers() {
		return Template.instance().selectedUsers.get();
	},

	name() {
		return Template.instance().selectedUserNames[this.valueOf()];
	},

	error() {
		return Template.instance().error.get();
	},

	roomName() {
		return Template.instance().roomName.get();
	},

	autocompleteSettings() {
		return {
			limit: 10,
			// inputDelay: 300
			rules: [
				{
					// @TODO maybe change this 'collection' and/or template
					collection: 'UserAndRoom',
					subscription: 'userAutocomplete',
					field: 'username',
					template: Template.userSearch,
					noMatchTemplate: Template.userSearchEmpty,
					matchAll: true,
					filter: {
						exceptions: [Meteor.user().username].concat(Template.instance().selectedUsers.get())
					},
					selector(match) {
						return { term: match };
					},
					sort: 'username'
				}
			]
		};
	},
	privateSwitchDisabled() {
		return RocketChat.authz.hasAllPermission(['create-c', 'create-p']) ? '' : 'disabled';
	},
	privateSwitchChecked() {
		return RocketChat.authz.hasAllPermission('create-c') ? '' : 'checked';
	}
});

Template.createCombinedFlex.events({
	'autocompleteselect #channel-members'(event, instance, doc) {
		instance.selectedUsers.set(instance.selectedUsers.get().concat(doc.username));

		instance.selectedUserNames[doc.username] = doc.name;

		event.currentTarget.value = '';
		return event.currentTarget.focus();
	},

	'click .remove-room-member'() {
		const self = this;

		let users = Template.instance().selectedUsers.get();
		users = _.reject(Template.instance().selectedUsers.get(), _id => _id === self.valueOf());

		Template.instance().selectedUsers.set(users);

		return $('#channel-members').focus();
	},

	'click header'(e, instance) {
		return SideNav.closeFlex(() => instance.clearForm());
	},

	'click .cancel-channel'(e, instance) {
		return SideNav.closeFlex(() => instance.clearForm());
	},

	'mouseenter header'() {
		return SideNav.overArrow();
	},

	'mouseleave header'() {
		return SideNav.leaveArrow();
	},

	'keydown input[type="text"]'() {
		return Template.instance().error.set([]);
	},

	'keyup #channel-name'(e, instance) {
		if (e.keyCode === 13) {
			return instance.$('#channel-members').focus();
		}
	},

	'keydown #channel-members'(e, instance) {
		if (($(e.currentTarget).val() === '') && (e.keyCode === 13)) {
			return instance.$('.save-channel').click();
		}
	},

	'click .save-channel'(e, instance) {
		const err = SideNav.validate();
		const name = instance.find('#channel-name').value.toLowerCase().trim().replace(/</g, '<').replace(/>/g, '>');
		const privateGroup = instance.find('#channel-type').checked;
		const readOnly = instance.find('#channel-ro').checked;
		const createRoute = privateGroup ? 'createPrivateGroup' : 'createChannel';
		const successRoute = privateGroup ? 'group' : 'channel';
		instance.roomName.set(name);
		if (!err) {
			return Meteor.call(createRoute, name, instance.selectedUsers.get(), readOnly, function(err, result) {
				if (err) {
					if (err.error === 'error-invalid-room-name') {
						instance.error.set({ invalid: true });
						return;
					}
					if (err.error === 'error-duplicate-channel-name') {
						instance.error.set({ duplicate: true });
						return;
					}
					if (err.error === 'error-archived-duplicate-name') {
						instance.error.set({ archivedduplicate: true });
						return;
					} else {
						return handleError(err);
					}
				}

				SideNav.closeFlex(() => instance.clearForm());

				if (!privateGroup) {
					RocketChat.callbacks.run('aftercreateCombined', { _id: result.rid, name: result.name });
				}

				return FlowRouter.go(successRoute, { name: result.name }, FlowRouter.current().queryParams);
			});
		} else {
			return instance.error.set({ fields: err });
		}
	}
});

Template.createCombinedFlex.onCreated(function() {
	const instance = this;
	instance.selectedUsers = new ReactiveVar([]);
	instance.selectedUserNames = {};
	instance.error = new ReactiveVar([]);
	instance.roomName = new ReactiveVar('');

	return instance.clearForm = function() {
		instance.error.set([]);
		instance.roomName.set('');
		instance.selectedUsers.set([]);
		instance.find('#channel-name').value = '';
		instance.find('#channel-type').checked = false;
		return instance.find('#channel-members').value = '';
	};
});


/* globals RocketChat */
RocketChat.getFullUserData = function({userId, filter, limit}) {
	let fields = {
		name: 1,
		username: 1,
		status: 1,
		utcOffset: 1,
		type: 1,
		active: 1
	};

	if (RocketChat.authz.hasPermission(userId, 'view-full-other-user-info')) {
		fields = _.extend(fields, {
			emails: 1,
			phone: 1,
			statusConnection: 1,
			createdAt: 1,
			lastLogin: 1,
			services: 1,
			requirePasswordChange: 1,
			requirePasswordChangeReason: 1,
			roles: 1,
			customFields: 1
		});
	} else if (limit !== 0) {
		limit = 1;
	}

	filter = s.trim(filter);

	if (!filter && limit === 1) {
		return undefined;
	}

	const options = {
		fields,
		limit,
		sort: { username: 1 }
	};

	if (filter) {
		if (limit === 1) {
			return RocketChat.models.Users.findByUsername(filter, options);
		} else {
			const filterReg = new RegExp(s.escapeRegExp(filter), 'i');
			return RocketChat.models.Users.findByUsernameNameOrEmailAddress(filterReg, options);
		}
	}

	return RocketChat.models.Users.find({}, options);
};


const query = {
_id: agentId
};
const options = {
fields: {
name: 1,
username: 1,

/* globals Livechat, LivechatVideoCall, MsgTyping */

Template.messages.helpers({
	messages() {
		return ChatMessage.find({
			rid: visitor.getRoom(),
			t: {
				'$ne': 't'
			}
		}, {
			sort: {
				ts: 1
			}
		});
	},
	showOptions() {
		if (Template.instance().showOptions.get()) {
			return 'show';
		} else {
			return '';
		}
	},
	optionsLink() {
		if (Template.instance().showOptions.get()) {
			return t('Close_menu');
		} else {
			return t('Options');
		}
	},
	videoCallEnabled() {
		return Livechat.videoCall;
	},
	showConnecting() {
		return Livechat.connecting;
	},
	usersTyping() {
		const users = MsgTyping.get(visitor.getRoom());
		if (users.length === 0) {
			return;
		}
		if (users.length === 1) {
			return {
				multi: false,
				selfTyping: MsgTyping.selfTyping.get(),
				users: users[0]
			};
		}
		// usernames = _.map messages, (message) -> return message.u.username
		let last = users.pop();
		if (users.length > 4) {
			last = t('others');
		}
		// else
		let usernames = users.join(', ');
		usernames = [usernames, last];
		return {
			multi: true,
			selfTyping: MsgTyping.selfTyping.get(),
			users: usernames.join(` ${ t('and') } `)
		};
	},
	agentData() {
		const agent = Livechat.agent;
		if (!agent) {
			return null;
		}

		const agentData = {
			avatar: getAvatarUrlFromUsername(agent.username)
		};

		if (agent.name) {
			agentData.name = agent.name;
		}

		if (Livechat.showAgentEmail && agent.emails && agent.emails[0] && agent.emails[0].address) {
			agentData.email = agent.emails[0].address;
		}

		if (agent.customFields && agent.customFields.phone) {
			agentData.phone = agent.customFields.phone;
		}

		return agentData;
	}
});

Template.messages.events({
	'keyup .input-message'(event, instance) {
		instance.chatMessages.keyup(visitor.getRoom(), event, instance);
		instance.updateMessageInputHeight(event.currentTarget);
	},
	'keydown .input-message'(event, instance) {
		return instance.chatMessages.keydown(visitor.getRoom(), event, instance);
	},
	'click .send-button'(event, instance) {
		const input = instance.find('.input-message');
		const sent = instance.chatMessages.send(visitor.getRoom(), input);
		input.focus();
		instance.updateMessageInputHeight(input);

		return sent;
	},
	'click .new-message'(event, instance) {
		instance.atBottom = true;
		return instance.find('.input-message').focus();
	},
	'click .error'(event) {
		return $(event.currentTarget).removeClass('show');
	},
	'click .toggle-options'(event, instance) {
		instance.showOptions.set(!instance.showOptions.get());
	},
	'click .video-button'(event) {
		event.preventDefault();

		if (!Meteor.userId()) {
			Meteor.call('livechat:registerGuest', { token: visitor.getToken() }, (error, result) => {
				if (error) {
					return console.log(error.reason);
				}

				Meteor.loginWithToken(result.token, (error) => {
					if (error) {
						return console.log(error.reason);
					}

					LivechatVideoCall.request();
				});
			});
		} else {
			LivechatVideoCall.request();
		}
	}
});

Template.messages.onCreated(function() {
	this.atBottom = true;

	this.showOptions = new ReactiveVar(false);

	this.updateMessageInputHeight = function(input) {
		// Inital height is 28. If the scrollHeight is greater than that( we have more text than area ),
		// increase the size of the textarea. The max-height is set at 200
		// even if the scrollHeight become bigger than that it should never exceed that.
		// Account for no text in the textarea when increasing the height.
		// If there is no text, reset the height.
		const inputScrollHeight = $(input).prop('scrollHeight');
		if (inputScrollHeight > 28) {
			return $(input).height($(input).val() === '' ? '15px' : (inputScrollHeight >= 200 ? inputScrollHeight - 50 : inputScrollHeight - 20));
		}
	};

	$(document).click((/*event*/) => {
		if (!this.showOptions.get()) {
			return;
		}
		const target = $(event.target);
		if (!target.closest('.options-menu').length && !target.is('.options-menu') && !target.closest('.toggle-options').length && !target.is('.toggle-options')) {
			this.showOptions.set(false);
		}
	});
});

Template.messages.onRendered(function() {
	this.chatMessages = new ChatMessages();
	this.chatMessages.init(this.firstNode);
});

Template.messages.onRendered(function() {
	const messages = this.find('.messages');
	const newMessage = this.find('.new-message');
	const template = this;

	if (messages) {
		const onscroll = _.throttle(function() {
			template.atBottom = messages.scrollTop >= messages.scrollHeight - messages.clientHeight;
		}, 200);
		Meteor.setInterval(function() {
			if (template.atBottom) {
				messages.scrollTop = messages.scrollHeight - messages.clientHeight;
				newMessage.className = 'new-message not';
			}
		}, 100);
		messages.addEventListener('touchstart', function() {
			template.atBottom = false;
		});
		messages.addEventListener('touchend', function() {
			onscroll();
		});
		messages.addEventListener('scroll', function() {
			template.atBottom = false;
			onscroll();
		});
		messages.addEventListener('mousewheel', function() {
			template.atBottom = false;
			onscroll();
		});
		messages.addEventListener('wheel', function() {
			template.atBottom = false;
			onscroll();
		});
	}
});


Template.livechatFlex.helpers({
	menuItem(name, icon, section) {
		return {
			name: t(name),
			icon,
			pathSection: section,
			darken: true
		};
	},
	embeddedVersion() {
		return RocketChat.Layout.isEmbedded();
	}
});

Template.livechatFlex.events({
	'click [data-action="close"]'() {
		SideNav.closeFlex();
	}
});