import React, { useState, useRef, useEffect } from 'react';
import { Animate } from 'react-simple-animate';
import useForm from 'react-hook-form';
import styled from 'styled-components';
import colors from './styles/colors';
import SortableContainer from './SortableContainer';
import { SubHeading, HeadingWithTopMargin, Error, Title } from './styles/typography';
import { setHomePage } from './ButtonGroup';
import SyntaxHighlighterWithCopy from './SyntaxHighlighterWithCopy';

const errorStyle = { border: `1px solid ${colors.secondary}`, background: colors.errorPink };

const Root = styled.main`
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  background: #0e101c;
  z-index: 4;
  color: white;
  box-sizing: border-box;
  -webkit-overflow-scrolling: touch;

  & pre,
  & code {
    font-size: 14px;
    text-align: left;
    color: white;
    overflow-x: auto;
    overflow-y: hidden;
    line-height: 1.3;
    white-space: pre-wrap;
    word-wrap: break-word;
    background: none !important;
  }
`;

const Wrapper = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  grid-column-gap: 60px;
  overflow: hidden;
  margin-bottom: 100px;
  padding: 0 20px 100px 20px;

  & > div:first-child {
    margin-top: 50px;
    order: 3;
  }

  & > form:nth-child(2) {
    order: 1;
  }

  & > div:nth-child(3) {
    order: 2;
  }

  @media (min-width: 768px) {
    & > div:first-child {
      margin-top: 0;
      order: 1;
    }

    & > form:nth-child(2) {
      order: 2;
    }

    & > div:nth-child(3) {
      order: 3;
    }
  }
`;

const SubmitButton = styled.input`
  margin-top: 30px;
  height: 55px;
  text-transform: uppercase;
  letter-spacing: 0.5rem;
  background: ${props => props.background || colors.lightPink};
  color: ${props => props.color || 'white'};
  border: none;
`;

const Form = styled.form`
  & fieldset {
    border-radius: 4px;
    border: 1px solid #6a6b7f;
    display: flex;
    padding: 10px 20px;
  }

  & fieldset > input {
    display: block;
    box-sizing: border-box;
    width: 100%;
    border-radius: 4px;
    border: 1px solid white;
    padding: 10px 15px;
    margin-bottom: 10px;
    font-size: 14px;

    &:hover {
      border: 1px solid ${colors.lightPink};
    }
  }

  & label {
    line-height: 2;
    text-align: left;
    display: block;
    margin-bottom: 13px;
    margin-top: 20px;
  }
`;

const CloseButton = styled.button`
  font-size: 30px;
  top: 0;
  right: 0;
  padding: 20px;
  position: absolute;
  cursor: pointer;
  border: none;
  font-weight: 200;
  z-index: 5;
  border-radius: 10px;
  color: white;
  display: none;
  background: rgba(14, 16, 28, 0.5294117647058824);

  @media (min-width: 768px) {
    font-size: 35px;
    display: block;
    padding: 20px;
    top: 15px;
    right: 20px;
  }
`;

function Builder({
  formData,
  updateFormData,
  showBuilder,
  toggleBuilder,
  editFormData,
  setFormData,
  builderButton,
  HomeRef,
  isMobile,
}) {
  const { register, handleSubmit, errors = {}, watch } = useForm();
  const [editIndex, setEditIndex] = useState(-1);
  const copyFormData = useRef([]);
  const closeButton = useRef(null);
  const [showValidation, toggleValidation] = useState(false);
  const onSubmit = (data, event) => {
    if (editIndex >= 0) {
      formData[editIndex] = data;
      updateFormData([...formData]);
      setFormData({});
      setEditIndex(-1);
    } else {
      // @ts-ignore
      updateFormData([...formData, ...[data]]);
    }
    event.target.reset();
  };
  const type = watch('type');
  const tabIndex = showBuilder ? 0 : -1;
  copyFormData.current = formData;

  function validate(value) {
    // @ts-ignore
    return !Object.values(copyFormData.current).find(data => data.name === value) || editIndex !== -1;
  }

  useEffect(() => {
    if (showBuilder && closeButton.current) {
      // @ts-ignore
      closeButton.current.focus();
    }
  }, [showBuilder]);

  return (
    <Animate
      play={showBuilder}
      easeType="ease-in"
      duration={isMobile ? 0.3 : 0.5}
      start={{
        transform: 'translateY(100vh)',
      }}
      end={{
        transform: 'translateY(0)',
      }}
      render={({ style }) => {
        return (
          <Root style={style}>

              <CloseButton
                aria-label="close builder"
                ref={closeButton}
                tabIndex={tabIndex}
                onClick={() => {
                  toggleBuilder(false);
                  builderButton.current.focus();
                  setHomePage();
                }}
              >
                ✕
              </CloseButton>
              <HeadingWithTopMargin>Builder</HeadingWithTopMargin>
              <SubHeading>Build your own form with code and example.</SubHeading>

              <Wrapper>

                  <Title>Form</Title>

                  <SortableContainer
                    {...{ updateFormData, tabIndex, formData, editIndex, setEditIndex, setFormData, editFormData }}
                  />

                  {formData.length === 0 && (

                      You can start adding fields with Fields Creator ▸

                  )}

                <Form onSubmit={handleSubmit(onSubmit)}>
                  <Title>Input Creator</Title>

                  Name:
                  <input
                    autoComplete="off"
                    defaultValue={editFormData.name}
                    aria-label="name"
                    name="name"
                    tabIndex={tabIndex}
                    style={{
                      ...(errors['name'] ? errorStyle : null),
                    }}
                    ref={register({
                      required: true,
                      validate,
                    })}
                  />
                  <Animate
                    play={!!errors['name']}
                    duration={0.6}
                    start={{
                      maxHeight: 0,
                    }}
                    end={{ maxHeight: 20 }}
                  >
                    {errors.name && errors.name['type'] === 'required' && <Error>This is required.</Error>}
                    {errors.name && errors.name['type'] === 'validate' && <Error>Name required to be unique.</Error>}
                  </Animate>

                  Type:
                  <select aria-label="Select type" name="type" ref={register} value={editFormData.type} tabIndex={tabIndex}>
                    <option value="text">Text</option>
                    <option value="select">Select</option>
                    <option value="checkbox">Checkbox</option>
                    <option value="radio">Radio</option>
                    <option value="number">Number</option>
                    <option value="textarea">Textarea</option>
                    <option value="email">Email</option>
                    <option value="range">Range</option>
                    <option value="search">Search</option>
                    <option value="tel">Tel</option>
                    <option value="url">url</option>
                    <option value="time">Time</option>
                    <option value="datetime">datetime</option>
                    <option value="datetime-local">datetime-local</option>
                    <option value="week">week</option>
                    <option value="month">month</option>
                    <option value="validate" disabled>
                      validate
                    </option>
                  </select>

                  {(type === 'select' ||
                    type === 'radio' ||
                    editFormData.type === 'select' ||
                    editFormData.type === 'radio') && (
                    <>
                      Options:
                      <input
                        tabIndex={tabIndex}
                        key={editFormData.name}
                        defaultValue={editFormData.options}
                        type="text"
                        name="options"
                        placeholder="Enter options separate by ;"
                        ref={register}
                      />
                    </>
                  )}

                    <input
                      type="checkbox"
                      tabIndex={tabIndex}
                      defaultChecked={editFormData.checkbox}
                      onClick={() => toggleValidation(!showValidation)}
                    />
                      Toggle Validation Panel

                  <Animate
                    play={showValidation}
                    start={{
                      maxHeight: 0,
                      overflow: 'hidden',
                    }}
                    end={{
                      maxHeight: 800,
                      overflow: 'hidden',
                    }}
                  >
                    Validation
                    <fieldset>

                        <input tabIndex={showValidation ? 0 : -1} defaultChecked={editFormData.required} type="checkbox" name="required" ref={register} />{' '}
                        Required

                      Max
                      <input
                        tabIndex={showValidation ? 0 : -1}
                        defaultValue={editFormData.max}
                        aria-label="max"
                        autoComplete="false"
                        name="max"
                        type="number"
                        ref={register}
                      />
                      Min
                      <input
                        defaultValue={editFormData.min}
                        autoComplete="false"
                        aria-label="min"
                        name="min"
                        tabIndex={showValidation ? 0 : -1}
                        type="number"
                        ref={register}
                      />
                      MaxLength
                      <input
                        defaultValue={editFormData.maxLength}
                        autoComplete="false"
                        aria-label="max length"
                        name="maxLength"
                        type="number"
                        tabIndex={showValidation ? 0 : -1}
                        ref={register}
                      />
                      MinLength
                      <input
                        tabIndex={showValidation ? 0 : -1}
                        defaultValue={editFormData.minLength}
                        aria-label="min length"
                        autoComplete="false"
                        name="minLength"
                        type="number"
                        ref={register}
                      />
                      Pattern
                      <input
                        tabIndex={showValidation ? 0 : -1}
                        autoComplete="false"
                        defaultValue={editFormData.pattern}
                        style={{
                          marginBottom: '20px',
                        }}
                        aria-label="pattern"
                        name="pattern"
                        type="text"
                        ref={register}
                      />
                    </fieldset>
                  </Animate>

                  <SubmitButton tabIndex={tabIndex} type="submit" value={editIndex >= 0 ? 'Update' : 'Create'} />

                  <Title
                    style={{
                      fontSize: 14,
                      maxWidth: '80%',
                      margin: '20px auto 0',
                    }}
                  >
                    or
                  </Title>

                  <Animate
                    play={formData.length > 0}
                    start={{
                      opacity: 0,
                      pointerEvents: 'none',
                    }}
                    end={{
                      opacity: 1,
                      pointerEvents: 'auto',
                    }}
                    render={({ style }) => (
                      <SubmitButton
                        tabIndex={tabIndex}
                        style={style}
                        type="button"
                        color="white"
                        onClick={() => {
                          toggleBuilder(false);
                          builderButton.current.focus();
                          document.body.style.overflow = 'auto';
                          HomeRef.current.scrollIntoView({ behavior: 'smooth' });
                          setHomePage();
                        }}
                        background="black"
                        value="Generate Form"
                      />
                    )}
                  />
                </Form>

                  <Title>Code</Title>

                  <SyntaxHighlighterWithCopy tabIndex={tabIndex} data={formData} />

              </Wrapper>

          </Root>
        );
      }}
    />
  );
}

export default React.memo(Builder);


function registerIntoFieldsRef(elementRef, data = { required: false, validate: undefined }): void {

if (elementRef && !elementRef.name) return warnMissingRef(elementRef);

const { name, type, value } = elementRef;

keep keep keep keep add keep keep keep keep keep

const setValue = (name: string, value: string | number | boolean): void => {

if (elm.type) elm[isCheckBoxInput(elm.type) ? 'checked' : 'value'] = value;

import getFieldValue from './getFieldValue';

jest.mock('./getRadioValue', () => ({
  default: () => ({
    value: 2
  }),
}));
jest.mock('./getMultipleSelectValue', () => ({
  default: () => 3,
}));

describe('getFieldValue', () => {
  it('should return correct value when type is radio', () => {
    expect(
      getFieldValue(
        {
          test: {
            ref: 'test',
          },
        },
        {
          type: 'radio',
          name: 'test',
        },
      ),
    ).toBe(2);
  });

  it('should return teh correct select value when type is select-multiple', () => {
    expect(
      getFieldValue(
        {
          test: {
            ref: {
              name: 'bill',
              value: 'test',
            },
          },
        },
        {
          type: 'select-multiple',
          name: 'bill',
        },
      ),
    ).toBe(3);
  });

  it('should return checked status if type is checked', () => {
    expect(
      getFieldValue(
        {
          test: {
            ref: {
              name: 'bill',
              value: 'test',
            },
          },
        },
        {
          type: 'checkbox',
          checked: true,
        },
      ),
    ).toBeTruthy();
  });

  it('should return it value for other types', () => {
    expect(
      getFieldValue(
        {
          test: {
            ref: {
              name: 'bill',
              value: 'test',
            },
          },
        },
        {
          type: 'text',
          value: 'value',
        },
      ),
    ).toBe('value');
  });
});


import isEmptyObject from '../utils/isEmptyObject';

export default function shouldUpdateWithError({
  errors,
  name,
  error,
  isOnBlur,
  isBlurType,
  onSubmitModeNotSubmitted,
}): boolean {
  if (
    onSubmitModeNotSubmitted ||
    (isOnBlur && !isBlurType) ||
    (errors[name] && errors[name].isManual && errors[name].type) ||
    (isEmptyObject(error) && isEmptyObject(errors))
  ) {
    return false;
  }

  if ((isEmptyObject(errors) && !isEmptyObject(error)) || (isEmptyObject(error) && errors[name])) {
    return true;
  }

  if (!errors[name]) {
    return true;
  } else if (errors[name].type !== error[name].type || errors[name].message !== error[name].message) {
    return true;
  }

  return false;
}


keep keep keep replace replace replace keep keep keep keep

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,
  );

  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 (defaultValues && defaultValues[name]) {
      elementRef[isCheckBoxInput(type) ? 'checked' : 'value'] = defaultValues[name];
    }

    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)),
      };
    }

    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,
    errors: errorsRef.current,
    formState: {
      dirty: isDirtyRef.current,
      isSubmitted: isSubmittedRef.current,
      submitCount: submitCountRef.current,
      touched: touchedFieldsRef.current,
      isSubmitting: isSubmittingRef.current,
    },
  };
}


import { VALIDATION_MODE } from '../constants';

export default (
  mode,
): {
  isOnSubmit: boolean;
  isOnBlur: boolean;
  isOnChange: boolean;
} => ({
  isOnSubmit: mode === VALIDATION_MODE.onSubmit,
  isOnBlur: mode === VALIDATION_MODE.onBlur,
  isOnChange: mode === VALIDATION_MODE.onchange,
});


import getRadioValue from './getRadioValue';
import isRadioInput from '../utils/isRadioInput';
import { DATE_INPUTS, STRING_INPUTS } from '../constants';
import { Field, ErrorMessages } from '../types';
import getValueAndMessage from './getValueAndMessage';
import isCheckBoxInput from '../utils/isCheckBoxInput';
import isString from '../utils/isString';
import isEmptyObject from '../utils/isEmptyObject';

type ValidatePromiseResult =
  | {}
  | void
  | {
      type: string;
      message: string | number | boolean | Date;
    };

export default async (
  {
    ref,
    ref: { type, value, name, checked },
    options,
    required,
    maxLength,
    minLength,
    min,
    max,
    pattern,
    validate,
  }: Field,
  fields: { [key: string]: Field },
): Promise<ErrorMessages> => {
  const error = {};
  const isRadio = isRadioInput(type);
  const isCheckBox = isCheckBoxInput(type);
  const isSelectOrInput = !isCheckBox && !isRadio;

  if (
    required &&
    ((isCheckBox && !checked) ||
      (isSelectOrInput && value === '') ||
      (isRadio && !getRadioValue(fields[name].options).isValid))
  ) {
    error[name] = {
      type: 'required',
      message: isString(required) ? required : '',
      ref: isRadio ? (fields[name].options || [{ ref: '' }])[0].ref : ref,
    };
    return error;
  }

  if ((min || max) && !STRING_INPUTS.includes(type)) {
    let exceedMax;
    let exceedMin;
    const valueNumber = parseFloat(value);

    const { value: maxValue, message: maxMessage } = getValueAndMessage(max);
    const { value: minValue, message: minMessage } = getValueAndMessage(min);

    if (type === 'number') {
      exceedMax = maxValue && valueNumber > maxValue;
      exceedMin = minValue && valueNumber < minValue;
    } else if (DATE_INPUTS.includes(type)) {
      if (typeof maxValue === 'string') exceedMax = maxValue && new Date(value) > new Date(maxValue);
      if (typeof minValue === 'string') exceedMin = minValue && new Date(value) < new Date(minValue);
    }

    if (exceedMax || exceedMin) {
      error[name] = {
        ...error[name],
        type: exceedMax ? 'max' : 'min',
        message: exceedMax ? maxMessage : minMessage,
        ref,
      };
      return error;
    }
  }

  if ((maxLength || minLength) && STRING_INPUTS.includes(type)) {
    const { value: maxLengthValue, message: maxLengthMessage } = getValueAndMessage(maxLength);
    const { value: minLengthValue, message: minLengthMessage } = getValueAndMessage(minLength);
    const exceedMax = maxLength && value.toString().length > maxLengthValue;
    const exceedMin = minLength && value.toString().length < minLengthValue;

    if (exceedMax || exceedMin) {
      error[name] = {
        ...error[name],
        type: exceedMax ? 'maxLength' : 'minLength',
        message: exceedMax ? maxLengthMessage : minLengthMessage,
        ref,
      };
      return error;
    }
  }

  if (pattern) {
    const { value: patternValue, message: patternMessage } = getValueAndMessage(pattern);

    if (patternValue instanceof RegExp && !patternValue.test(value)) {
      error[name] = {
        ...error[name],
        type: 'pattern',
        message: patternMessage,
        ref,
      };
      return error;
    }
  }

  if (validate) {
    const fieldValue = isRadio ? getRadioValue(options).value : value;
    const validateRef = isRadio && options ? options[0].ref : ref;

    if (typeof validate === 'function') {
      const result = await validate(fieldValue);
      if (typeof result !== 'boolean' || !result) {
        error[name] = {
          ...error[name],
          type: 'validate',
          message: isString(result) ? result : '',
          ref: validateRef,
        };
        return error;
      }
    } else if (typeof validate === 'object') {
      const validationResult = await new Promise(
        (resolve): ValidatePromiseResult => {
          const values = Object.entries(validate);
          values.reduce(async (previous, [key, validate], index): Promise<ValidatePromiseResult> => {
            const lastChild = values.length - 1 === index;

            if (typeof validate === 'function') {
              const result = await validate(fieldValue);

              if (typeof result !== 'boolean' || !result) {
                const data = {
                  type: key,
                  message: isString(result) ? result : '',
                  ref: validateRef,
                };
                return lastChild ? resolve(data) : data;
              }
            }

            return lastChild ? resolve(previous) : previous;
          }, {});
        },
      );

      if (validationResult && !isEmptyObject(validationResult)) {
        error[name] = {
          ...error[name],
          ref: validateRef,
          ...validationResult,
        };
        return error;
      }
    }
  }

  return error;
};


mutationWatcher: onDomRemove(elementRef, (): Function => removeEventListener(inputData, true)),

field: isRadio ? (fields[name].options || [])[(fields[name].options || []).length - 1] : fields[name],

keep keep keep add keep keep keep keep keep

if (defaultValues && defaultValues[name]) {


import React from 'react';
import styled from 'styled-components';
import formik from './assets/formik.png';
import hookFrom from './assets/hookForm.png';
import reduxForm from './assets/reduxForm.png';
import { H1, Title } from './styles/typography';
import Link from './styles/link';
import { SimpleImg } from 'react-simple-img';
import colors from './styles/colors';

const Wrapper = styled.section`
  text-align: center;

  & > p {
    max-width: 1033px;
    display: block;
    margin: 0 auto;
  }

  & > img {
    max-width: 100%;
    border-radius: 4px;

    @media (min-width: 768px) {
      border-radius: 10px;
    }
  }

  & > h2 {
    margin-left: auto;
    margin-right: auto;
    max-width: 600px;
  }
`;

const ImgSection = styled.div`
  display: flex;
  flex-direction: column;

  @media (min-width: 768px) {
    flex-direction: row;
  }

  & > img {
    align-self: center;
    border-radius: 4px;
    max-width: 100%;
    margin: 20px 0;

    @media (min-width: 768px) {
      max-width: 80%;
      margin: 0;
      margin-bottom: 40px;
    }
  }

  & ul {
    max-width: 100%;
    list-style: none;
    min-width: 250px;
    padding-left: 0;
    margin: 0;

    & > li {
      text-align: left;
      padding: 2px 0;
    }
  }
`;

export default function CodePerfCompareSection() {
  return (
    <Wrapper>

Performance Comparison

        The following results are captured under 6x CPU slow down on App start with Chrome devtool's performance tab,
        and code is from section above 'Library Code Comparison'.

      <Title
        style={{
          marginTop: 50,
        }}
      >
        React Hook Form
      </Title>
      <ImgSection>

No. of mount: 1

No. of committing change: 1

            Total time:{' '}

              1800ms

        <SimpleImg src={hookFrom} placeholder={false} alt="React Hook Form performance" />
      </ImgSection>
      <Title>Formik</Title>
      <ImgSection>

No. of mount: 6

No. of committing change: 1

            Total time:{' '}

              2070ms

        <SimpleImg src={formik} placeholder={false} alt="Formik performance" />
      </ImgSection>
      <Title>Redux Form</Title>
      <ImgSection>

No. of mount: 17

No. of committing change: 2

No. of update: 2

            Total time:{' '}

              2380ms

        <SimpleImg src={reduxForm} placeholder={false} alt="Redux Form performance" />
      </ImgSection>

        ⚠ Wants to see more intense performance test?{' '}
        <Link href="https://github.com/bluebill1049/react-hook-form-performance-compare">
          Check out the result for 1000 fields within a form here
        </Link>
        .

    </Wrapper>
  );
}


import filterUndefinedErrors from './filterUndefinedErrors';

describe('filterUndefinedErrors', () => {
  it('should filter out undefined errors fields', () => {
    const errors = {
      test: { type: 'test' },
      bill: { type: undefined },
    };
    // @ts-ignore
    expect(filterUndefinedErrors(errors)).toEqual({ test: { type: 'test' } });
  });
});


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


export default `import React from "react";
import { Formik, Form, Field } from "formik";

function validateEmail(value) {
  let error;

  if (!value) {
    error = "Required";
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}$/i.test(value)) {
    error = "Invalid email address";
  }

  return error;
}

function validateUsername(value) {
  let error;

  if (value === "admin") {
    error = "Nice try!";
  }

  return error;
}

const Example = () => {
  const onSubmit = values => {
    console.log(values);
  };

  return (
    <Formik
      initialValues={{
        username: "",
        email: ""
      }}
      onSubmit={onSubmit}
    >
      {({ errors, touched }) => (
        <Form>
          <Field name="email" validate={validateEmail} />
          {errors.email && touched.email && errors.email}

          <Field name="username" validate={validateUsername} />
          {errors.username && touched.username && errors.username}

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


export default `
import React from 'react'
import ReactDOM from 'react-dom'
import useForm from 'react-hook-form'
import * as yup from 'yup'

const SignupSchema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required(),
});

export default function YourForm() {
  const { register, handleSubmit, errors } = useForm({
    validationSchema: SignupSchema
  });
  const onSubmit = data => { console.log(data); };
  console.log(errors);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="text" name="name" ref={register} />
      <input type="number" name="age" ref={register} />
      <input type="submit" />
    </form>
  )
}
`


filedNames?: string | string[] | undefined,

defaultValue?: string | string[] | undefined,

) => FieldValue | FieldValue[] | undefined;

setError: (name: string, type: string, message?: string, ref?: Ref) => void;

setValue: (name: string, value: string | number | boolean) => void;


import React, { useState, useRef, Suspense, useEffect } from 'react';
import useForm from 'react-hook-form';
import { Animate } from 'react-simple-animate';
import ButtonGroup from './ButtonGroup';
import styled from 'styled-components';
import FORM_DATA from './constants/formData';
import colors from './styles/colors';
import Form from './Form';
import Header from './Header';
import CodeCompareSection from './CodeCompareSection';
import CodePerfCompareSection from './CodePerfCompareSection';

const Setting = React.lazy(() => import('./Setting'));
const Builder = React.lazy(() => import('./Builder'));
const Api = React.lazy(() => import('./Api'));

const Root = styled.div`
  overflow: hidden;
  color: white;
  padding: 0 20px 50px;
  position: relative;

  @media (min-width: 1024px) {
    padding: 0 50px;
  }

  & form > select,
  & form > input {
    display: block;
    box-sizing: border-box;
    width: 100%;
    border-radius: 4px;
    padding: 10px 15px;
    margin-bottom: 10px;
    font-size: 16px;
  }

  & form > select {
    width: 100%;
  }

  & form > select:not([multiple]) {
    height: 37px;
  }

  & form {
    flex: 1;
  }

  & form > input.form-error {
    border: 1px solid #bf1650;
  }
`;

const Footer = styled.footer`
  padding: 40px 0;
  font-size: 12px;
  font-weight: 200;
  margin-bottom: 60px;

  @media (min-width: 768px) {
    font-size: 16px;
  }

  & > a {
    color: white;
    text-decoration: none;
    transition: 0.3s all;

    &:hover {
      color: ${colors.lightPink};
    }
  }
`;

const isMobile = window.matchMedia('(max-width: 768px)').matches;

function App() {
  const [submitData, updateSubmitData] = useState({});
  const settingButton = useRef(null);
  const builderButton = useRef(null);
  const apiButton = useRef(null);
  const [editFormData, setFormData] = useState({});
  const HomeRef = useRef(null);
  const [showSetting, toggleSetting] = useState(false);
  const [showApi, toggleApi] = useState(false);
  const [showBuilder, toggleBuilder] = useState(false);
  const [formData, updateFormData] = useState(FORM_DATA);
  const [setting, setConfig] = useState<{
    mode: 'onSubmit' | 'onBlur' | 'onChange';
    showError: boolean;
    showWatch: boolean;
    showTouch: boolean;
    showSubmit: boolean;
  }>({
    mode: 'onChange',
    showError: true,
    showWatch: true,
    showTouch: true,
    showSubmit: true,
  });
  const {
    register,
    errors,
    handleSubmit,
    watch,
    formState: { touched },
  } = useForm({
    mode: setting.mode,
  });
  const tabIndex = showApi || showBuilder ? -1 : 0;

  const onSubmit = data => {
    updateSubmitData(data);
  };

  useEffect(() => {
    window.addEventListener('popstate', function() {
      const pathName = window.location.pathname;
      if (pathName === '/api') {
        toggleApi(true);
        toggleBuilder(false);
        document.body.style.overflow = 'hidden';
      } else if (pathName === '/builder') {
        toggleApi(false);
        toggleBuilder(true);
        document.body.style.overflow = 'hidden';
      } else {
        toggleApi(false);
        toggleBuilder(false);
        document.body.style.overflow = 'auto';
      }
    });
  }, []);

  useEffect(
    () => {
      const pathName = window.location.pathname;
      if (pathName === '/api') {
        toggleApi(true);
        toggleBuilder(false);
        document.body.style.overflow = 'hidden';
      } else if (pathName === '/builder') {
        toggleApi(false);
        toggleBuilder(true);
        document.body.style.overflow = 'hidden';
      } else {
        toggleApi(false);
        toggleBuilder(false);
        document.body.style.overflow = 'auto';
      }
    },
    [window.location.pathname],
  );

  const Buttons = (
    <ButtonGroup
      tabIndex={tabIndex}
      builderButton={builderButton}
      toggleBuilder={toggleBuilder}
      toggleSetting={toggleSetting}
      showSetting={showSetting}
      settingButton={settingButton}
      showBuilder={showBuilder}
      apiButton={apiButton}
      showApi={showApi}
      toggleApi={toggleApi}
    />
  );

  return (
    <Root>
      {!isMobile && Buttons}

      <Suspense fallback={}>
        <Setting
          settingButton={settingButton}
          toggleSetting={toggleSetting}
          setting={setting}
          showSetting={showSetting}
          setConfig={setConfig}
        />
      </Suspense>

      <Suspense fallback={}>
        <Builder
          showBuilder={showBuilder}
          toggleBuilder={toggleBuilder}
          editFormData={editFormData}
          setFormData={setFormData}
          formData={formData}
          updateFormData={updateFormData}
          builderButton={builderButton}
          isMobile={isMobile}
          HomeRef={HomeRef}
        />
      </Suspense>

      {isMobile && Buttons}

      <Suspense fallback={}>
        <Api
          showApi={showApi}
          toggleApi={toggleApi}
          editFormData={editFormData}
          setFormData={setFormData}
          formData={formData}
          updateFormData={updateFormData}
          builderButton={builderButton}
          isMobile={isMobile}
          apiButton={apiButton}
        />
      </Suspense>

       {
          if (showSetting || showBuilder) {
            toggleSetting(false);
            toggleBuilder(false);
          }
        }}
        style={{
          perspective: '2000px',
        }}
      >
        <Animate
          play={showBuilder || showSetting || showApi}
          start={{ minHeight: '100vh', filter: 'blur(0)', transform: 'scale(1)' }}
          end={{ minHeight: '100vh', filter: 'blur(3px)', transform: 'scale(0.9) rotateX(5deg)' }}
        >

          <CodeCompareSection />

          <CodePerfCompareSection />

            <Form
              {...{
                tabIndex,
                showSetting,
                toggleApi,
                toggleSetting,
                handleSubmit,
                onSubmit,
                submitData,
                register,
                errors,
                watch,
                touched,
                formData,
                toggleBuilder,
                setting,
              }}
            />

            Build with ♡ by{' '}

              @Bill Luo
            {' '}
            with{' '}

              React Hook Form
            {' '}
            +{' '}

              React Simple Animate
            {' '}
            +{' '}

              React Simple Img

        </Animate>

    </Root>
  );
}

export default React.memo(App);


import styled from 'styled-components';
import colors from './colors';

export const SubHeading = styled.p`
  font-size: 13px;
  padding: 0 20px;
  margin-top: 0;
  text-align: center;
  color: ${colors.lightPink};

  @media (min-width: 768px) {
    font-size: 16px;
    margin-bottom: 30px;
  }
`;

export const Heading = styled.h1`
  font-weight: 100;
  text-align: center;
  margin-bottom: 10px;

  @media (min-width: 768px) {
    font-size: 55px !important;
    margin-bottom: 10px;
  }
`;

export const H1 = styled.h1`
  font-size: 24px;
  display: inline-block;
  margin: 40px auto 30px;
  font-weight: 300;
  padding-bottom: 5px;
  border-bottom: 2px solid ${colors.secondary};

  @media (min-width: 768px) {
    font-size: 36px;
  }
`;

export const H5 = styled.h5`
  font-size: 18px;
  text-align: center;
  font-weight: 500;
  border-bottom: 1px solid ${colors.lightPink};
  margin: 0;
  padding-bottom: 8px;

  & > code {
    font-size: 18px !important;
  }
`;

export const HeadingWithTopMargin = styled(Heading)`
  @media (min-width: 768px) {
    margin-top: 30px;
  }
`;

export const Title = styled.h2`
  text-align: center;
  font-weight: 200;
  margin-top: 10px;
  font-size: 1.1rem;
  overflow: hidden;
  margin-bottom: 30px;
  position: relative;

  @media (min-width: 768px) {
    margin-top: 20px;
    font-size: 1.5rem;
  }

  &:before,
  &:after {
    background-color: #4f6294;
    content: '';
    display: inline-block;
    height: 1px;
    position: relative;
    vertical-align: middle;
    width: 50%;
  }

  &:before {
    right: 0.5em;
    margin-left: -50%;
  }

  &:after {
    left: 0.5em;
    margin-right: -50%;
  }
`;

export const Error = styled.p`
  font-size: 12px;
  color: ${colors.lightPink};
`;


export const DATE_INPUTS = ['date', 'time', 'month', 'datetime', 'datetime-local', 'week'];

export const STRING_INPUTS = ['text', 'email', 'password', 'search', 'tel', 'url', 'textarea'];

export const VALIDATION_MODE = {
  onBlur: 'onBlur',
  onchange: 'onChange',
  onSubmit: 'onSubmit',
};


return this.callUser({ uid: data.callee, rid: roomId, callId: data.callId });

keep keep keep keep keep replace keep keep keep

return this.callUser({ uid: data.calleeId, rid: roomId, callId: data.callId });

import { MongoInternals } from 'meteor/mongo';
import type {
	IDirectVideoConference,
	ILivechatVideoConference,
	IRoom,
	IUser,
	VideoConferenceInstructions,
	DirectCallInstructions,
	ConferenceInstructions,
	LivechatInstructions,
	AtLeast,
	IGroupVideoConference,
	IVideoConferenceUser,
	IMessage,
	IStats,
	VideoConference,
	VideoConferenceCapabilities,
	VideoConferenceCreateData,
	Optional,
} from '@rocket.chat/core-typings';
import {
	VideoConferenceStatus,
	isDirectVideoConference,
	isGroupVideoConference,
	isLivechatVideoConference,
} from '@rocket.chat/core-typings';
import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit';
import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers';
import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import { Users, VideoConference as VideoConferenceModel, Rooms, Messages, Subscriptions } from '@rocket.chat/models';
import type { IVideoConfService, VideoConferenceJoinOptions } from '@rocket.chat/core-services';
import { api, ServiceClassInternal } from '@rocket.chat/core-services';

import { Apps } from '../../../app/apps/server';
import { sendMessage } from '../../../app/lib/server/functions/sendMessage';
import { settings } from '../../../app/settings/server';
import { videoConfProviders } from '../../lib/videoConfProviders';
import { videoConfTypes } from '../../lib/videoConfTypes';
import { updateCounter } from '../../../app/statistics/server/functions/updateStatsCounter';
import { readSecondaryPreferred } from '../../database/readSecondaryPreferred';
import { availabilityErrors } from '../../../lib/videoConference/constants';
import { callbacks } from '../../../lib/callbacks';
import { Notifications } from '../../../app/notifications/server';
import { canAccessRoomIdAsync } from '../../../app/authorization/server/functions/canAccessRoom';

const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo;

export class VideoConfService extends ServiceClassInternal implements IVideoConfService {
	protected name = 'video-conference';

	// VideoConference.create: Start a video conference using the type and provider specified as arguments
	public async create(
		{ type, rid, createdBy, providerName, ...data }: VideoConferenceCreateData,
		useAppUser = true,
	): Promise<VideoConferenceInstructions> {
		const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't' | 'uids' | 'name' | 'fname'>>(rid, {
			projection: { t: 1, uids: 1, name: 1, fname: 1 },
		});

		if (!room) {
			throw new Error('invalid-room');
		}

		const user = await Users.findOneById<IUser>(createdBy, {});
		if (!user) {
			throw new Error('failed-to-load-own-data');
		}

		if (type === 'direct') {
			if (room.t !== 'd' || !room.uids || room.uids.length > 2) {
				throw new Error('type-and-room-not-compatible');
			}

			return this.startDirect(providerName, user, room, data);
		}

		if (type === 'livechat') {
			return this.startLivechat(providerName, user, rid);
		}

		const title = (data as Partial<IGroupVideoConference>).title || room.fname || room.name || '';
		return this.startGroup(providerName, user, room._id, title, data, useAppUser);
	}

	// VideoConference.start: Detect the desired type and provider then start a video conference using them
	public async start(
		caller: IUser['_id'],
		rid: string,
		{ title, allowRinging }: { title?: string; allowRinging?: boolean },
	): Promise<VideoConferenceInstructions> {
		const providerName = await this.getValidatedProvider();
		const initialData = await this.getTypeForNewVideoConference(rid, Boolean(allowRinging));

		const data = {
			...initialData,
			createdBy: caller,
			rid,
			providerName,
		} as VideoConferenceCreateData;

		if (data.type === 'videoconference') {
			data.title = title;
		}

		return this.create(data, false);
	}

	public async join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise<string> {
		const call = await VideoConferenceModel.findOneById(callId);
		if (!call || call.endedAt) {
			throw new Error('invalid-call');
		}

		let user: Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'> | null = null;

		if (uid) {
			user = await Users.findOneById<Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'>>(uid, {
				projection: { name: 1, username: 1, avatarETag: 1 },
			});
			if (!user) {
				throw new Error('failed-to-load-own-data');
			}
		}

		if (call.providerName === 'jitsi') {
			updateCounter({ settingsId: 'Jitsi_Click_To_Join_Count' });
		}

		return this.joinCall(call, user || undefined, options);
	}

	public async getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise<IBlock[]> {
		const call = await VideoConferenceModel.findOneById(callId);
		if (!call) {
			throw new Error('invalid-call');
		}

		if (!videoConfProviders.isProviderAvailable(call.providerName)) {
			throw new Error('video-conf-provider-unavailable');
		}

		let user: Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> | null = null;

		if (uid) {
			user = await Users.findOneById<Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'>>(uid, {
				projection: { name: 1, username: 1, avatarETag: 1 },
			});
			if (!user) {
				throw new Error('failed-to-load-own-data');
			}
		}

		const blocks = await (await this.getProviderManager()).getVideoConferenceInfo(call.providerName, call, user || undefined).catch((e) => {
			throw new Error(e);
		});

		if (blocks?.length) {
			return blocks;
		}

		return [
			{
				blockId: 'videoconf-info',
				type: 'section',
				text: {
					type: 'mrkdwn',
					text: `**${TAPi18n.__('Video_Conference_Url')}**: ${call.url}`,
				},
			} as IBlock,
		];
	}

	public async cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise<void> {
		const call = await VideoConferenceModel.findOneById(callId);
		if (!call || !isDirectVideoConference(call)) {
			throw new Error('invalid-call');
		}

		if (call.status !== VideoConferenceStatus.CALLING || call.endedBy || call.endedAt) {
			throw new Error('invalid-call-status');
		}

		const user = await Users.findOneById(uid);
		if (!user) {
			throw new Error('failed-to-load-own-data');
		}

		await VideoConferenceModel.setDataById(callId, {
			ringing: false,
			status: VideoConferenceStatus.DECLINED,
			endedAt: new Date(),
			endedBy: {
				_id: user._id,
				name: user.name as string,
				username: user.username as string,
			},
		});

		await this.runVideoConferenceChangedEvent(callId);
		this.notifyVideoConfUpdate(call.rid, call._id);
	}

	public async get(callId: VideoConference['_id']): Promise<Omit<VideoConference, 'providerData'> | null> {
		return VideoConferenceModel.findOneById<Omit<VideoConference, 'providerData'>>(callId, { projection: { providerData: 0 } });
	}

	public async getUnfiltered(callId: VideoConference['_id']): Promise<VideoConference | null> {
		return VideoConferenceModel.findOneById(callId);
	}

	public async list(
		roomId: IRoom['_id'],
		pagination: { offset?: number; count?: number } = {},
	): Promise<PaginatedResult<{ data: VideoConference[] }>> {
		const { cursor, totalCount } = VideoConferenceModel.findPaginatedByRoomId(roomId, pagination);

		const [data, total] = await Promise.all([cursor.toArray(), totalCount]);

		return {
			data,
			offset: pagination.offset || 0,
			count: data.length,
			total,
		};
	}

	public async setProviderData(callId: VideoConference['_id'], data: VideoConference['providerData'] | undefined): Promise<void> {
		VideoConferenceModel.setProviderDataById(callId, data);
	}

	public async setEndedBy(callId: VideoConference['_id'], endedBy: IUser['_id']): Promise<void> {
		const user = await Users.findOneById<Required<Pick<IUser, '_id' | 'username' | 'name'>>>(endedBy, {
			projection: { username: 1, name: 1 },
		});
		if (!user) {
			throw new Error('Invalid User');
		}

		VideoConferenceModel.setEndedById(callId, {
			_id: user._id,
			username: user.username,
			name: user.name,
		});
	}

	public async setEndedAt(callId: VideoConference['_id'], endedAt: Date): Promise<void> {
		VideoConferenceModel.setEndedById(callId, undefined, endedAt);
	}

	public async setStatus(callId: VideoConference['_id'], status: VideoConference['status']): Promise<void> {
		switch (status) {
			case VideoConferenceStatus.ENDED:
				return this.endCall(callId);
			case VideoConferenceStatus.EXPIRED:
				return this.expireCall(callId);
		}

		VideoConferenceModel.setStatusById(callId, status);
	}

	public async addUser(callId: VideoConference['_id'], userId?: IUser['_id'], ts?: Date): Promise<void> {
		const call = await this.get(callId);
		if (!call) {
			throw new Error('Invalid video conference');
		}

		if (!userId) {
			if (call.type === 'videoconference') {
				return this.addAnonymousUser(call as Omit<IGroupVideoConference, 'providerData'>);
			}

			throw new Error('Invalid User');
		}

		const user = await Users.findOneById<Required<Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'>>>(userId, {
			projection: { username: 1, name: 1, avatarETag: 1 },
		});
		if (!user) {
			throw new Error('Invalid User');
		}

		this.addUserToCall(call, {
			_id: user._id,
			username: user.username,
			name: user.name,
			avatarETag: user.avatarETag,
			ts: ts || new Date(),
		});
	}

	public async listProviders(): Promise<{ key: string; label: string }[]> {
		return videoConfProviders.getProviderList();
	}

	public async listProviderCapabilities(providerName: string): Promise<VideoConferenceCapabilities> {
		return videoConfProviders.getProviderCapabilities(providerName) || {};
	}

	public async listCapabilities(): Promise<{ providerName: string; capabilities: VideoConferenceCapabilities }> {
		const providerName = await this.getValidatedProvider();

		return {
			providerName,
			capabilities: videoConfProviders.getProviderCapabilities(providerName) || {},
		};
	}

	public async declineLivechatCall(callId: VideoConference['_id']): Promise<boolean> {
		const call = await this.getUnfiltered(callId);

		if (!isLivechatVideoConference(call)) {
			return false;
		}

		if (call.messages.started) {
			const name =
				(settings.get<boolean>('UI_Use_Real_Name') ? call.createdBy.name : call.createdBy.username) || call.createdBy.username || '';
			const text = TAPi18n.__('video_livechat_missed', { username: name });
			await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text)]);
		}

		await VideoConferenceModel.setDataById(call._id, {
			status: VideoConferenceStatus.DECLINED,
			endedAt: new Date(),
		});

		return true;
	}

	public async diagnoseProvider(uid: string, rid: string, providerName?: string): Promise<string | undefined> {
		try {
			if (providerName) {
				await this.validateProvider(providerName);
			} else {
				await this.getValidatedProvider();
			}
		} catch (error: unknown) {
			if (error instanceof Error) {
				this.createEphemeralMessage(uid, rid, error.message);
				return error.message;
			}
		}
	}

	public async getStatistics(): Promise<IStats['videoConf']> {
		const options = {
			readPreference: readSecondaryPreferred(db),
		};

		return {
			videoConference: {
				started: await VideoConferenceModel.countByTypeAndStatus('videoconference', VideoConferenceStatus.STARTED, options),
				ended: await VideoConferenceModel.countByTypeAndStatus('videoconference', VideoConferenceStatus.ENDED, options),
			},
			direct: {
				calling: await VideoConferenceModel.countByTypeAndStatus('direct', VideoConferenceStatus.CALLING, options),
				started: await VideoConferenceModel.countByTypeAndStatus('direct', VideoConferenceStatus.STARTED, options),
				ended: await VideoConferenceModel.countByTypeAndStatus('direct', VideoConferenceStatus.ENDED, options),
			},
			livechat: {
				started: await VideoConferenceModel.countByTypeAndStatus('livechat', VideoConferenceStatus.STARTED, options),
				ended: await VideoConferenceModel.countByTypeAndStatus('livechat', VideoConferenceStatus.ENDED, options),
			},
			settings: {
				provider: settings.get<string>('VideoConf_Default_Provider'),
				dms: settings.get<boolean>('VideoConf_Enable_DMs'),
				channels: settings.get<boolean>('VideoConf_Enable_Channels'),
				groups: settings.get<boolean>('VideoConf_Enable_Groups'),
				teams: settings.get<boolean>('VideoConf_Enable_Teams'),
			},
		};
	}

	public async validateAction(
		action: string,
		caller: IUser['_id'],
		{ callId, uid, rid }: { callId: VideoConference['_id']; uid: IUser['_id']; rid: IRoom['_id'] },
	): Promise<boolean> {
		if (!callId || !uid || !rid) {
			return false;
		}

		if (!(await canAccessRoomIdAsync(rid, caller)) || (caller !== uid && !(await canAccessRoomIdAsync(rid, uid)))) {
			return false;
		}

		const call = await VideoConferenceModel.findOneById<Pick<VideoConference, '_id' | 'status' | 'endedAt' | 'createdBy'>>(callId, {
			projection: { status: 1, endedAt: 1, createdBy: 1 },
		});

		if (!call) {
			return false;
		}

		if (action === 'end') {
			return true;
		}

		if (call.endedAt || call.status > VideoConferenceStatus.STARTED) {
			// If the caller is still calling about a call that has already ended, notify it
			if (action === 'call' && caller === call.createdBy._id) {
				Notifications.notifyUser(call.createdBy._id, 'video-conference.end', { rid, uid, callId });
			}

			return false;
		}

		return true;
	}

	private notifyVideoConfUpdate(rid: IRoom['_id'], callId: VideoConference['_id']): void {
		Notifications.notifyRoom(rid, callId);
	}

	private async endCall(callId: VideoConference['_id']): Promise<void> {
		const call = await this.getUnfiltered(callId);
		if (!call) {
			return;
		}

		await VideoConferenceModel.setDataById(call._id, { endedAt: new Date(), status: VideoConferenceStatus.ENDED });
		await this.runVideoConferenceChangedEvent(call._id);
		this.notifyVideoConfUpdate(call.rid, call._id);

		if (call.type === 'direct') {
			return this.endDirectCall(call);
		}
	}

	private async expireCall(callId: VideoConference['_id']): Promise<void> {
		const call = await VideoConferenceModel.findOneById<Pick<VideoConference, '_id' | 'messages'>>(callId, { projection: { messages: 1 } });
		if (!call) {
			return;
		}

		await VideoConferenceModel.setDataById(call._id, { endedAt: new Date(), status: VideoConferenceStatus.EXPIRED });
	}

	private async endDirectCall(call: IDirectVideoConference): Promise<void> {
		const params = { rid: call.rid, uid: call.createdBy._id, callId: call._id };

		// Notify the caller that the call was ended by the server
		Notifications.notifyUser(call.createdBy._id, 'video-conference.end', params);

		// If the callee hasn't joined the call yet, notify them that it has already ended
		const subscriptions = await Subscriptions.findByRoomIdAndNotUserId(call.rid, call.createdBy._id, {
			projection: { 'u._id': 1, '_id': 0 },
		}).toArray();

		for (const subscription of subscriptions) {
			if (call.users.find(({ _id }) => _id === subscription.u._id)) {
				continue;
			}

			Notifications.notifyUser(subscription.u._id, 'video-conference.end', params);
		}
	}

	private async getTypeForNewVideoConference(
		rid: IRoom['_id'],
		allowRinging: boolean,
	): Promise<AtLeast<VideoConferenceCreateData, 'type'>> {
		const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't'>>(rid, {
			projection: { t: 1 },
		});

		if (!room) {
			throw new Error('invalid-room');
		}

		return videoConfTypes.getTypeForRoom(room, allowRinging);
	}

	private async createMessage(call: VideoConference, createdBy?: IUser, customBlocks?: IMessage['blocks']): Promise<IMessage['_id']> {
		const record = {
			t: 'videoconf',
			msg: '',
			groupable: false,
			blocks: customBlocks || [this.buildVideoConfBlock(call._id)],
		};

		const room = await Rooms.findOneById(call.rid);
		const appId = videoConfProviders.getProviderAppId(call.providerName);
		const user = createdBy || (appId && (await Users.findOneByAppId(appId))) || (await Users.findOneById('rocket.cat'));

		const message = sendMessage(user, record, room, false);
		return message._id;
	}

	private async validateProvider(providerName: string): Promise<void> {
		const manager = await this.getProviderManager();
		const configured = await manager.isFullyConfigured(providerName).catch(() => false);
		if (!configured) {
			throw new Error(availabilityErrors.NOT_CONFIGURED);
		}
	}

	private async getValidatedProvider(): Promise<string> {
		if (!videoConfProviders.hasAnyProvider()) {
			throw new Error(availabilityErrors.NO_APP);
		}

		const providerName = videoConfProviders.getActiveProvider();
		if (!providerName) {
			throw new Error(availabilityErrors.NOT_ACTIVE);
		}

		await this.validateProvider(providerName);

		return providerName;
	}

	private async createEphemeralMessage(uid: string, rid: string, i18nKey: string): Promise<void> {
		const user = await Users.findOneById<Pick<IUser, 'language' | 'roles'>>(uid, { projection: { language: 1, roles: 1 } });
		const language = user?.language || settings.get<string>('Language') || 'en';
		const key = user?.roles.includes('admin') ? `admin-${i18nKey}` : i18nKey;
		const msg = TAPi18n.__(key, {
			lng: language,
		});

		api.broadcast('notify.ephemeralMessage', uid, rid, {
			msg,
		});
	}

	private async createLivechatMessage(call: ILivechatVideoConference, user: IUser, url: string): Promise<IMessage['_id']> {
		const username = (settings.get<boolean>('UI_Use_Real_Name') ? user.name : user.username) || user.username || '';
		const text = TAPi18n.__('video_livechat_started', {
			username,
		});

		return this.createMessage(call, user, [
			this.buildMessageBlock(text),
			{
				type: 'actions',
				appId: 'videoconf-core',
				blockId: call._id,
				elements: [
					{
						appId: 'videoconf-core',
						blockId: call._id,
						actionId: 'joinLivechat',
						type: 'button',
						text: {
							type: 'plain_text',
							text: TAPi18n.__('Join_call'),
							emoji: true,
						},
						url,
					},
				],
			},
		]);
	}

	private buildVideoConfBlock(callId: string): MessageSurfaceLayout[number] {
		return {
			type: 'video_conf',
			blockId: callId,
			callId,
			appId: 'videoconf-core',
		};
	}

	private buildMessageBlock(text: string): MessageSurfaceLayout[number] {
		return {
			type: 'section',
			appId: 'videoconf-core',
			text: {
				type: 'mrkdwn',
				text: `${text}`,
			},
		};
	}

	private async startDirect(
		providerName: string,
		user: IUser,
		{ _id: rid, uids }: AtLeast<IRoom, '_id' | 'uids'>,
		extraData?: Partial<IDirectVideoConference>,
	): Promise<DirectCallInstructions> {
		const callee = uids?.filter((uid) => uid !== user._id).pop();
		if (!callee) {
			// Are you trying to call yourself?
			throw new Error('invalid-call-target');
		}

		const callId = await VideoConferenceModel.createDirect({
			...extraData,
			rid,
			createdBy: {
				_id: user._id,
				name: user.name as string,
				username: user.username as string,
			},
			providerName,
		});
		const call = (await this.getUnfiltered(callId)) as IDirectVideoConference | null;
		if (!call) {
			throw new Error('failed-to-create-direct-call');
		}

		await this.runNewVideoConferenceEvent(callId);

		const url = await this.generateNewUrl(call);
		VideoConferenceModel.setUrlById(callId, url);

		const messageId = await this.createMessage(call, user);
		call.messages.started = messageId;
		VideoConferenceModel.setMessageById(callId, 'started', messageId);

		// After 40 seconds if the status is still "calling", we cancel the call automatically.
		setTimeout(async () => {
			try {
				const call = await VideoConferenceModel.findOneById<Pick<VideoConference, '_id' | 'status'>>(callId, { projection: { status: 1 } });

				if (call?.status !== VideoConferenceStatus.CALLING) {
					return;
				}

				await this.cancel(user._id, callId);
			} catch {
				// Ignore errors on this timeout
			}
		}, 40000);

		return {
			type: 'direct',
			callId,
			callee,
		};
	}

	private async notifyUsersOfRoom(rid: IRoom['_id'], uid: IUser['_id'], eventName: string, ...args: any[]): Promise<void> {
		const subscriptions = Subscriptions.findByRoomIdAndNotUserId(rid, uid, {
			projection: { 'u._id': 1, '_id': 0 },
		});

		await subscriptions.forEach((subscription) => Notifications.notifyUser(subscription.u._id, eventName, ...args));
	}

	private async startGroup(
		providerName: string,
		user: IUser,
		rid: IRoom['_id'],
		title: string,
		extraData?: Partial<IGroupVideoConference>,
		useAppUser = true,
	): Promise<ConferenceInstructions> {
		const callId = await VideoConferenceModel.createGroup({
			...extraData,
			rid,
			title,
			createdBy: {
				_id: user._id,
				name: user.name as string,
				username: user.username as string,
			},
			providerName,
		});
		const call = (await this.getUnfiltered(callId)) as IGroupVideoConference | null;
		if (!call) {
			throw new Error('failed-to-create-group-call');
		}

		await this.runNewVideoConferenceEvent(callId);

		const url = await this.generateNewUrl(call);
		VideoConferenceModel.setUrlById(callId, url);

		call.url = url;

		const messageId = await this.createMessage(call, useAppUser ? undefined : user);
		call.messages.started = messageId;
		VideoConferenceModel.setMessageById(callId, 'started', messageId);

		if (call.ringing) {
			await this.notifyUsersOfRoom(rid, user._id, 'video-conference.ring', { callId, rid, title, uid: call.createdBy, providerName });
		}

		return {
			type: 'videoconference',
			callId,
			rid,
		};
	}

	private async startLivechat(providerName: string, user: IUser, rid: IRoom['_id']): Promise<LivechatInstructions> {
		const callId = await VideoConferenceModel.createLivechat({
			rid,
			createdBy: {
				_id: user._id,
				name: user.name as string,
				username: user.username as string,
			},
			providerName,
		});

		const call = (await this.getUnfiltered(callId)) as ILivechatVideoConference | null;
		if (!call) {
			throw new Error('failed-to-create-livechat-call');
		}

		await this.runNewVideoConferenceEvent(callId);

		const joinUrl = await this.getUrl(call);
		const messageId = await this.createLivechatMessage(call, user, joinUrl);
		call.messages.started = messageId;
		await VideoConferenceModel.setMessageById(callId, 'started', messageId);

		return {
			type: 'livechat',
			callId,
		};
	}

	private async joinCall(
		call: VideoConference,
		user: AtLeast<IUser, '_id' | 'username' | 'name' | 'avatarETag'> | undefined,
		options: VideoConferenceJoinOptions,
	): Promise<string> {
		await callbacks.runAsync('onJoinVideoConference', call._id, user?._id);

		await this.runOnUserJoinEvent(call._id, user as IVideoConferenceUser);

		return this.getUrl(call, user, options);
	}

	private async getProviderManager(): Promise<AppVideoConfProviderManager> {
		if (!Apps?.isLoaded()) {
			throw new Error('apps-engine-not-loaded');
		}

		const manager = Apps.getManager()?.getVideoConfProviderManager();
		if (!manager) {
			throw new Error(availabilityErrors.NO_APP);
		}

		return manager;
	}

	private async getRoomName(rid: string): Promise<string> {
		const room = await Rooms.findOneById<Pick<IRoom, '_id' | 'name' | 'fname'>>(rid, { projection: { name: 1, fname: 1 } });

		return room?.fname || room?.name || rid;
	}

	private async generateNewUrl(call: VideoConference): Promise<string> {
		if (!videoConfProviders.isProviderAvailable(call.providerName)) {
			throw new Error('video-conf-provider-unavailable');
		}

		const title = isGroupVideoConference(call) ? call.title || (await this.getRoomName(call.rid)) : '';

		return (await this.getProviderManager())
			.generateUrl(call.providerName, {
				_id: call._id,
				type: call.type,
				rid: call.rid,
				createdBy: call.createdBy as Required<VideoConference['createdBy']>,
				title,
				providerData: call.providerData,
			})
			.catch((e) => {
				throw new Error(e);
			});
	}

	private async getCallTitleForUser(call: VideoConference, userId?: IUser['_id']): Promise<string> {
		if (call.type === 'videoconference' && call.title) {
			return call.title;
		}

		if (userId) {
			const subscription = await Subscriptions.findOneByRoomIdAndUserId(call.rid, userId, { projection: { fname: 1, name: 1 } });
			if (subscription) {
				return subscription.fname || subscription.name;
			}
		}

		const room = await Rooms.findOneById(call.rid);
		return room?.fname || room?.name || 'Rocket.Chat';
	}

	private async getCallTitle(call: VideoConference): Promise<string> {
		if (call.type === 'videoconference') {
			if (call.title) {
				return call.title;
			}
		}

		const room = await Rooms.findOneById(call.rid);
		if (room) {
			if (room.t === 'd') {
				if (room.usernames?.length) {
					return room.usernames.join(', ');
				}
			} else if (room.fname) {
				return room.fname;
			} else if (room.name) {
				return room.name;
			}
		}

		return 'Rocket.Chat';
	}

	private async getUrl(
		call: VideoConference,
		user?: AtLeast<IUser, '_id' | 'username' | 'name'>,
		options: VideoConferenceJoinOptions = {},
	): Promise<string> {
		if (!videoConfProviders.isProviderAvailable(call.providerName)) {
			throw new Error('video-conf-provider-unavailable');
		}

		if (!call.url) {
			call.url = await this.generateNewUrl(call);
			VideoConferenceModel.setUrlById(call._id, call.url);
		}

		const callData = {
			_id: call._id,
			type: call.type,
			rid: call.rid,
			url: call.url,
			createdBy: call.createdBy as Required<VideoConference['createdBy']>,
			providerData: {
				...(call.providerData || {}),
				...{ customCallTitle: await this.getCallTitleForUser(call, user?._id) },
			},
			title: await this.getCallTitle(call),
		};

		const userData = user && {
			_id: user._id,
			username: user.username as string,
			name: user.name as string,
		};

		return (await this.getProviderManager()).customizeUrl(call.providerName, callData, userData, options).catch((e) => {
			throw new Error(e);
		});
	}

	private async runNewVideoConferenceEvent(callId: VideoConference['_id']): Promise<void> {
		const call = await VideoConferenceModel.findOneById(callId);

		if (!call) {
			throw new Error('video-conf-data-not-found');
		}

		if (!videoConfProviders.isProviderAvailable(call.providerName)) {
			throw new Error('video-conf-provider-unavailable');
		}

		(await this.getProviderManager()).onNewVideoConference(call.providerName, call).catch((e) => {
			throw new Error(e);
		});
	}

	private async runVideoConferenceChangedEvent(callId: VideoConference['_id']): Promise<void> {
		const call = await VideoConferenceModel.findOneById(callId);

		if (!call) {
			throw new Error('video-conf-data-not-found');
		}

		if (!videoConfProviders.isProviderAvailable(call.providerName)) {
			throw new Error('video-conf-provider-unavailable');
		}

		(await this.getProviderManager()).onVideoConferenceChanged(call.providerName, call).catch((e) => {
			throw new Error(e);
		});
	}

	private async runOnUserJoinEvent(callId: VideoConference['_id'], user?: IVideoConferenceUser): Promise<void> {
		const call = await VideoConferenceModel.findOneById(callId);

		if (!call) {
			throw new Error('video-conf-data-not-found');
		}

		if (!videoConfProviders.isProviderAvailable(call.providerName)) {
			throw new Error('video-conf-provider-unavailable');
		}

		(await this.getProviderManager()).onUserJoin(call.providerName, call, user).catch((e) => {
			throw new Error(e);
		});
	}

	private async addUserToCall(
		call: Optional<VideoConference, 'providerData'>,
		{ _id, username, name, avatarETag, ts }: AtLeast<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> & { ts?: Date },
	): Promise<void> {
		if (call.users.find((user) => user._id === _id)) {
			return;
		}

		await VideoConferenceModel.addUserById(call._id, { _id, username, name, avatarETag, ts });

		if (call.type === 'direct') {
			return this.updateDirectCall(call as IDirectVideoConference, _id);
		}

		this.notifyVideoConfUpdate(call.rid, call._id);
	}

	private async addAnonymousUser(call: Optional<IGroupVideoConference, 'providerData'>): Promise<void> {
		await VideoConferenceModel.increaseAnonymousCount(call._id);
	}

	private async updateDirectCall(call: IDirectVideoConference, newUserId: IUser['_id']): Promise<void> {
		// If it's an user that hasn't joined yet
		if (call.ringing && !call.users.find(({ _id }) => _id === newUserId)) {
			Notifications.notifyUser(call.createdBy._id, 'video-conference.join', { rid: call.rid, uid: newUserId, callId: call._id });
			if (newUserId !== call.createdBy._id) {
				Notifications.notifyUser(newUserId, 'video-conference.join', { rid: call.rid, uid: newUserId, callId: call._id });
				// If the callee joined the direct call, then we stopped ringing
				await VideoConferenceModel.setRingingById(call._id, false);
			}
		}

		if (call.status !== VideoConferenceStatus.CALLING) {
			return;
		}

		await VideoConferenceModel.setStatusById(call._id, VideoConferenceStatus.STARTED);
		this.notifyVideoConfUpdate(call.rid, call._id);

		await this.runVideoConferenceChangedEvent(call._id);
	}
}


import { ServiceClassInternal } from '@rocket.chat/core-services';
import type { IOmnichannelService } from '@rocket.chat/core-services';

import { Livechat } from '../../../app/livechat/server';

export class OmnichannelService extends ServiceClassInternal implements IOmnichannelService {
	protected name = 'omnichannel';

	async created() {
		this.onEvent('presence.status', async ({ user }): Promise<void> => {
			if (!user?._id) {
				return;
			}
			const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role));
			if (hasRole) {
				// TODO change `Livechat.notifyAgentStatusChanged` to a service call
				Livechat.notifyAgentStatusChanged(user._id, user.status);
			}
		});
	}
}


import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';

import { Messages } from '../../../models/server';
import { RateLimiter } from '../../../lib/server';
import { settings } from '../../../settings/server';
import { canAccessRoomId } from '../../../authorization/server';
import { follow } from '../functions';
import { Apps, AppEvents } from '../../../apps/server/orchestrator';

Meteor.methods({
	followMessage({ mid }) {
		check(mid, String);

		const uid = Meteor.userId();
		if (!uid) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'followMessage' });
		}

		if (mid && !settings.get('Threads_enabled')) {
			throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' });
		}

		const message = Messages.findOneById(mid);
		if (!message) {
			throw new Meteor.Error('error-invalid-message', 'Invalid message', {
				method: 'followMessage',
			});
		}

		if (!canAccessRoomId(message.rid, uid)) {
			throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' });
		}

		const followResult = follow({ tmid: message.tmid || message._id, uid });

		const isFollowed = true;
		Promise.await(Apps.triggerEvent(AppEvents.IPostMessageFollowed, message, Meteor.user(), isFollowed));

		return followResult;
	},
});

RateLimiter.limitMethod('followMessage', 5, 5000, {
	userId() {
		return true;
	},
});


export const isLightColor = (color: string): boolean => {
	const hex = color.replace('#', '');
	const r = parseInt(hex.substring(0, 0 + 2), 16);
	const g = parseInt(hex.substring(2, 2 + 2), 16);
	const b = parseInt(hex.substring(4, 4 + 2), 16);
	const brightness = (r * 299 + g * 587 + b * 114) / 1000;
	return brightness > 155;
};


{ _id: rid, uids }: AtLeast<IRoom, '_id' | 'uids'>,

const callee = uids?.filter((uid) => uid !== user._id).pop();

keep keep keep replace replace keep keep keep keep

const calleeId = uids?.filter((uid) => uid !== user._id).pop();


import type { FC } from 'react';
import { createContext, useContext } from 'react';

export type SurfaceContextValue = {
  type: 'message' | 'modal' | 'banner';
};

export const SurfaceContext = createContext<SurfaceContextValue | undefined>(
  undefined
);

export const Surface: FC<{ value: SurfaceContextValue }> = (props) => (
  <SurfaceContext.Provider {...props} />
);

export const useSurface = (): SurfaceContextValue => {
  const context = useContext(SurfaceContext);
  if (!context) {
    throw new Error('Invalid Surface Content');
  }
  return context;
};

export const useSurfaceType = (): SurfaceContextValue['type'] => {
  const context = useSurface();
  return context.type;
};


declare module 'meteor/konecty:user-presence' {
	namespace UserPresenceMonitor {
		function processUserSession(userSession: any, event: string): void;
		function onSetUserStatus(callback: any): void;
	}

	namespace UserPresence {
		let awayTime: number | undefined;
		function removeConnectionsByInstanceId(id: string): void;
		function start(): void;
		function stopTimer(): void;
		function setDefaultStatus(userId: string, status: string): void;
	}
}


import { registerModel } from '@rocket.chat/models';

import { db } from '../database/utils';
import { LivechatAgentActivityRaw } from './raw/LivechatAgentActivity';

registerModel('ILivechatAgentActivityModel', new LivechatAgentActivityRaw(db));