<commit_before>/* @flow */
import type { Auth } from '../../types';
import type {
  WebviewInputMessage,
  MessageInputContent,
  MessageInputFetching,
  MessageInputTyping,
} from '../webViewHandleUpdates';

// We pull out document.body in one place, and check it's not null, in order
// to provide that assertion to the type-checker.
const documentBody = document.body;
if (!documentBody) {
  throw new Error('No document.body element!');
}

const escapeHtml = (text: string): string => {
  const element = document.createElement('div');
  element.innerText = text;
  return element.innerHTML;
};

const sendMessage = (msg: Object) => {
  window.postMessage(JSON.stringify(msg), '*');
};

window.onerror = (message, source, line, column, error) => {
  if (window.enableWebViewErrorDisplay) {
    const elementJsError = document.getElementById('js-error-detailed');
    if (elementJsError) {
      elementJsError.innerHTML = [
        `Message: ${message}`,
        `Source: ${source}`,
        `Line: ${line}:${column}`,
        `Error: ${JSON.stringify(error)}`,
        '',
      ]
        .map(escapeHtml)
        .join('
');
    }
  } else {
    const elementJsError = document.getElementById('js-error-plain');
    const elementSheetGenerated = document.getElementById('generated-styles');
    const elementSheetHide = document.getElementById('style-hide-js-error-plain');
    if (
      elementJsError
      && elementSheetGenerated
      && elementSheetHide
      && elementSheetHide instanceof HTMLStyleElement
      && elementSheetHide.sheet
      && elementSheetGenerated instanceof HTMLStyleElement
      && elementSheetGenerated.sheet
    ) {
      elementSheetHide.sheet.disabled = true;
      const height = elementJsError.offsetHeight;
      // $FlowFixMe `sheet: StyleSheet` type is not comprehensive enough
      elementSheetGenerated.sheet.insertRule(`.header-wrapper { top: ${height}px; }`, 0);
    }
  }

  sendMessage({
    type: 'error',
    details: {
      message,
      source,
      line,
      column,
      error,
    },
  });

  return true;
};

const showHideElement = (elementId: string, show: boolean) => {
  const element = document.getElementById(elementId);
  if (element) {
    element.classList.toggle('hidden', !show);
  }
};

let height = documentBody.clientHeight;
window.addEventListener('resize', event => {
  const difference = height - documentBody.clientHeight;
  if (documentBody.scrollHeight !== documentBody.scrollTop + documentBody.clientHeight) {
    window.scrollBy({ left: 0, top: difference });
  }
  height = documentBody.clientHeight;
});

/*
 *
 * Identifying visible messages
 *
 */

const getMessageNode = (node: ?Node): ?Node => {
  let curNode = node;
  while (curNode && curNode.parentNode && curNode.parentNode !== documentBody) {
    curNode = curNode.parentNode;
  }
  return curNode;
};

const getMessageIdFromNode = (node: ?Node, defaultValue: number = -1): number => {
  const msgNode = getMessageNode(node);
  return msgNode && msgNode instanceof Element
    ? +msgNode.getAttribute('data-msg-id')
    : defaultValue;
};

const getStartAndEndNodes = (): { start: number, end: number } => {
  const startNode = getMessageNode(document.elementFromPoint(200, 20));
  const endNode = getMessageNode(document.elementFromPoint(200, window.innerHeight - 20));

  return {
    start: getMessageIdFromNode(startNode, Number.MAX_SAFE_INTEGER),
    end: getMessageIdFromNode(endNode, 0),
  };
};

/*
 *
 * Reporting scrolls to outside, to mark messages as read
 *
 */

let prevNodes = getStartAndEndNodes();

const sendScrollMessage = () => {
  const currentNodes = getStartAndEndNodes();

  sendMessage({
    type: 'scroll',
    // See MessageListEventScroll for the meanings of these properties.
    offsetHeight: documentBody.offsetHeight,
    innerHeight: window.innerHeight,
    scrollY: window.scrollY,
    startMessageId: Math.min(prevNodes.start, currentNodes.start),
    endMessageId: Math.max(prevNodes.end, currentNodes.end),
  });
  prevNodes = currentNodes;
};

// If the message list is too short to scroll, fake a scroll event
// in order to cause the messages to be marked as read.
const sendScrollMessageIfListShort = () => {
  if (documentBody.scrollHeight === documentBody.clientHeight) {
    sendScrollMessage();
  }
};

/**
 * Disable reporting scrolls to the outside to mark messages as read.
 *
 * This is set while we're first setting up after the content loads, and
 * while we're handling `message` events from the outside and potentially
 * rewriting the content.
 */
let scrollEventsDisabled = true;

let lastTouchEventTimestamp = 0;
let lastTouchPositionX = -1;
let lastTouchPositionY = -1;

const handleScrollEvent = () => {
  lastTouchEventTimestamp = 0;
  if (scrollEventsDisabled) {
    return;
  }

  sendScrollMessage();

  const nearEnd = documentBody.offsetHeight - window.scrollY - window.innerHeight > 100;
  showHideElement('scroll-bottom', nearEnd);
};

window.addEventListener('scroll', handleScrollEvent);

/*
 *
 * Updating message content, and re-scrolling
 *
 */

type ScrollTarget =
  | { type: 'none' }
  | { type: 'bottom' }
  | { type: 'anchor', anchor: number }
  | { type: 'preserve', msgId: number, prevBoundTop: number };

const scrollToBottom = () => {
  window.scroll({ left: 0, top: documentBody.scrollHeight, behavior: 'smooth' });
};

const isNearBottom = (): boolean =>
  documentBody.scrollHeight - 100 < documentBody.scrollTop + documentBody.clientHeight;

const scrollToBottomIfNearEnd = () => {
  if (isNearBottom()) {
    scrollToBottom();
  }
};

const scrollToAnchor = (anchor: number) => {
  const anchorNode = document.getElementById(`msg-${anchor}`);

  if (anchorNode) {
    anchorNode.scrollIntoView({ block: 'start' });
  } else {
    window.scroll({ left: 0, top: documentBody.scrollHeight + 200 });
  }
};

// Try to identify a message on screen and its location, so we can
// scroll the corresponding message to the same place afterward.
const findPreserveTarget = (): ScrollTarget => {
  // TODO magic numbers
  const msgNode = getMessageNode(document.elementFromPoint(200, 50));
  if (!msgNode || !(msgNode instanceof HTMLElement)) {
    // TODO log this -- it's an error which the user will notice.
    // (We don't attempt this unless there are messages already,
    // which we really want to keep steady in view.)
    return { type: 'none' };
  }
  const msgId = getMessageIdFromNode(msgNode);
  const prevBoundRect = msgNode.getBoundingClientRect();
  return { type: 'preserve', msgId, prevBoundTop: prevBoundRect.top };
};

// Scroll the given message to the same height it was at before.
const scrollToPreserve = (msgId: number, prevBoundTop: number) => {
  const newElement = document.getElementById(`msg-${msgId}`);
  if (!newElement) {
    // TODO log this -- it's an error which the user will notice.
    return;
  }
  const newBoundRect = newElement.getBoundingClientRect();
  window.scrollBy(0, newBoundRect.top - prevBoundTop);
};

const appendAuthToImages = auth => {
  const imageTags = document.getElementsByTagName('img');
  Array.from(imageTags).forEach(img => {
    if (!img.src.startsWith(auth.realm)) {
      return;
    }

    // This unusual form of authentication is only accepted by the server
    // for a small number of routes.  Rather than append the API key to all
    // kinds of URLs on the server, do so only for those routes.
    const srcPath = img.src.substring(auth.realm.length);
    if (!(srcPath.startsWith('/user_uploads/') || srcPath.startsWith('/thumbnail?'))) {
      return;
    }

    const delimiter = img.src.includes('?') ? '&' : '?';
    img.src += `${delimiter}api_key=${auth.apiKey}`;
  });
};

const handleMessageContent = (msg: MessageInputContent) => {
  let target: ScrollTarget;
  if (msg.updateStrategy === 'replace') {
    target = { type: 'none' };
  } else if (msg.updateStrategy === 'scroll-to-anchor') {
    target = { type: 'anchor', anchor: msg.anchor };
  } else if (
    msg.updateStrategy === 'scroll-to-bottom-if-near-bottom'
    && isNearBottom() /* align */
  ) {
    target = { type: 'bottom' };
  } else {
    // including 'default' and 'preserve-position'
    target = findPreserveTarget();
  }

  documentBody.innerHTML = msg.content;

  appendAuthToImages(msg.auth);

  if (target.type === 'bottom') {
    scrollToBottom();
  } else if (target.type === 'anchor') {
    scrollToAnchor(target.anchor);
  } else if (target.type === 'preserve') {
    scrollToPreserve(target.msgId, target.prevBoundTop);
  }

  sendScrollMessageIfListShort();
};

/** We call this when the webview's content first loads. */
const handleInitialLoad = /* eslint-disable-line */ (anchor: number, auth: Auth) => {
  scrollToAnchor(anchor);
  appendAuthToImages(auth);
  sendScrollMessageIfListShort();
  scrollEventsDisabled = false;
};

/*
 *
 * Handling other message-from-outside events
 *
 */

const handleMessageFetching = (msg: MessageInputFetching) => {
  showHideElement('message-loading', msg.showMessagePlaceholders);
  showHideElement('spinner-older', msg.fetchingOlder);
  showHideElement('spinner-newer', msg.fetchingNewer);
};

const handleMessageTyping = (msg: MessageInputTyping) => {
  const elementTyping = document.getElementById('typing');
  if (elementTyping) {
    elementTyping.innerHTML = msg.content;
    setTimeout(() => scrollToBottomIfNearEnd());
  }
};

const messageHandlers = {
  content: handleMessageContent,
  fetching: handleMessageFetching,
  typing: handleMessageTyping,
};

document.addEventListener('message', e => {
  scrollEventsDisabled = true;
  // $FlowFixMe
  const decodedData = decodeURIComponent(escape(window.atob(e.data)));
  const messages: WebviewInputMessage[] = JSON.parse(decodedData);
  messages.forEach((msg: WebviewInputMessage) => {
    // $FlowFixMe
    messageHandlers[msg.type](msg);
  });
  scrollEventsDisabled = false;
});

/*
 *
 * Handling user touches
 *
 */

documentBody.addEventListener('click', (e: MouseEvent) => {
  e.preventDefault();
  lastTouchEventTimestamp = 0;

  const { target } = e;

  if (!(target instanceof HTMLElement)) {
    return;
  }

  if (target.matches('.scroll-bottom')) {
    scrollToBottom();
    return;
  }

  if (target.matches('.avatar-img')) {
    sendMessage({
      type: 'avatar',
      fromEmail: target.getAttribute('data-email'),
    });
    return;
  }

  if (target.matches('.header')) {
    sendMessage({
      type: 'narrow',
      narrow: target.getAttribute('data-narrow'),
      id: target.getAttribute('data-id'),
    });
    return;
  }

  /* Should we pull up the lightbox?  For comparison, see the webapp's
   * static/js/lightbox.js , starting at the `#main_div` click handler. */
  const inlineImageLink = target.closest('.message_inline_image a');
  if (
    inlineImageLink
    /* The webapp displays certain videos inline, but on mobile
     * we'd rather let another app handle them, as links. */
    && !inlineImageLink.closest('.youtube-video, .vimeo-video')
  ) {
    sendMessage({
      type: 'image',
      src: inlineImageLink.getAttribute('href'), // TODO: should be `src` / `data-src-fullsize`.
      messageId: getMessageIdFromNode(inlineImageLink),
    });
    return;
  }

  if (target.matches('a')) {
    sendMessage({
      type: 'url',
      href: target.getAttribute('href'),
      messageId: getMessageIdFromNode(target),
    });
    return;
  }

  if (target.parentNode instanceof Element && target.parentNode.matches('a')) {
    sendMessage({
      type: 'url',
      href: target.parentNode.getAttribute('href'),
      messageId: getMessageIdFromNode(target.parentNode),
    });
    return;
  }

  if (target.matches('.reaction')) {
    sendMessage({
      type: 'reaction',
      name: target.getAttribute('data-name'),
      code: target.getAttribute('data-code'),
      reactionType: target.getAttribute('data-type'),
      messageId: getMessageIdFromNode(target),
      voted: target.classList.contains('self-voted'),
    });
  }
});

const handleLongPress = (target: Element) => {
  // The logic that defines a "long press" in terms of raw touch events
  // is pretty subtle.  The `lastTouchEventTimestamp` and surrounding logic
  // are an attempt to define a long press.
  //
  // TODO: The logic around this "timestamp" is a bit obscure; it's
  // sometimes a real timestamp, other times 0 as sort of a boolean flag.
  // It would be good to clean it up to be clearer.
  //
  // At the same time, the logic is believed not to cover all the cases it
  // should; for example, multitouch events.  Better would be to either find
  // a library we can use which strives to handle all that complexity, or
  // get long-press events from the platform.
  if (!lastTouchEventTimestamp || Date.now() - lastTouchEventTimestamp < 500) {
    return;
  }

  lastTouchEventTimestamp = 0;

  sendMessage({
    type: 'longPress',
    target: target.matches('.header') ? 'header' : 'message',
    messageId: getMessageIdFromNode(target),
  });
};

documentBody.addEventListener('touchstart', e => {
  const { target } = e;
  if (e.changedTouches[0].pageX < 20 || !(target instanceof Element)) {
    return;
  }

  lastTouchPositionX = e.changedTouches[0].pageX;
  lastTouchPositionY = e.changedTouches[0].pageY;
  lastTouchEventTimestamp = Date.now();
  setTimeout(() => handleLongPress(target), 500);
});

const isNearPositions = (x1: number = 0, y1: number = 0, x2: number = 0, y2: number = 0): boolean =>
  Math.abs(x1 - x2) < 10 && Math.abs(y1 - y2) < 10;

documentBody.addEventListener('touchend', e => {
  if (
    isNearPositions(
      lastTouchPositionX,
      lastTouchPositionY,
      e.changedTouches[0].pageX,
      e.changedTouches[0].pageY,
    )
  ) {
    lastTouchEventTimestamp = Date.now();
  }
});

documentBody.addEventListener('touchmove', e => {
  lastTouchEventTimestamp = 0;
});

documentBody.addEventListener('drag', e => {
  lastTouchEventTimestamp = 0;
});

/*
 *
 * Synchronizing setup with outside
 *
 */

const waitForBridge = () => {
  if (window.postMessage.length === 1) {
    sendMessage({ type: 'ready' });
  } else {
    setTimeout(waitForBridge, 10);
  }
};
waitForBridge();
<commit_msg>webview js: Specify types for several more event handlers.<commit_after>/* @flow */
import type { Auth } from '../../types';
import type {
  WebviewInputMessage,
  MessageInputContent,
  MessageInputFetching,
  MessageInputTyping,
} from '../webViewHandleUpdates';

// We pull out document.body in one place, and check it's not null, in order
// to provide that assertion to the type-checker.
const documentBody = document.body;
if (!documentBody) {
  throw new Error('No document.body element!');
}

const escapeHtml = (text: string): string => {
  const element = document.createElement('div');
  element.innerText = text;
  return element.innerHTML;
};

const sendMessage = (msg: Object) => {
  window.postMessage(JSON.stringify(msg), '*');
};

window.onerror = (message, source, line, column, error) => {
  if (window.enableWebViewErrorDisplay) {
    const elementJsError = document.getElementById('js-error-detailed');
    if (elementJsError) {
      elementJsError.innerHTML = [
        `Message: ${message}`,
        `Source: ${source}`,
        `Line: ${line}:${column}`,
        `Error: ${JSON.stringify(error)}`,
        '',
      ]
        .map(escapeHtml)
        .join('
');
    }
  } else {
    const elementJsError = document.getElementById('js-error-plain');
    const elementSheetGenerated = document.getElementById('generated-styles');
    const elementSheetHide = document.getElementById('style-hide-js-error-plain');
    if (
      elementJsError
      && elementSheetGenerated
      && elementSheetHide
      && elementSheetHide instanceof HTMLStyleElement
      && elementSheetHide.sheet
      && elementSheetGenerated instanceof HTMLStyleElement
      && elementSheetGenerated.sheet
    ) {
      elementSheetHide.sheet.disabled = true;
      const height = elementJsError.offsetHeight;
      // $FlowFixMe `sheet: StyleSheet` type is not comprehensive enough
      elementSheetGenerated.sheet.insertRule(`.header-wrapper { top: ${height}px; }`, 0);
    }
  }

  sendMessage({
    type: 'error',
    details: {
      message,
      source,
      line,
      column,
      error,
    },
  });

  return true;
};

const showHideElement = (elementId: string, show: boolean) => {
  const element = document.getElementById(elementId);
  if (element) {
    element.classList.toggle('hidden', !show);
  }
};

let height = documentBody.clientHeight;
window.addEventListener('resize', event => {
  const difference = height - documentBody.clientHeight;
  if (documentBody.scrollHeight !== documentBody.scrollTop + documentBody.clientHeight) {
    window.scrollBy({ left: 0, top: difference });
  }
  height = documentBody.clientHeight;
});

/*
 *
 * Identifying visible messages
 *
 */

const getMessageNode = (node: ?Node): ?Node => {
  let curNode = node;
  while (curNode && curNode.parentNode && curNode.parentNode !== documentBody) {
    curNode = curNode.parentNode;
  }
  return curNode;
};

const getMessageIdFromNode = (node: ?Node, defaultValue: number = -1): number => {
  const msgNode = getMessageNode(node);
  return msgNode && msgNode instanceof Element
    ? +msgNode.getAttribute('data-msg-id')
    : defaultValue;
};

const getStartAndEndNodes = (): { start: number, end: number } => {
  const startNode = getMessageNode(document.elementFromPoint(200, 20));
  const endNode = getMessageNode(document.elementFromPoint(200, window.innerHeight - 20));

  return {
    start: getMessageIdFromNode(startNode, Number.MAX_SAFE_INTEGER),
    end: getMessageIdFromNode(endNode, 0),
  };
};

/*
 *
 * Reporting scrolls to outside, to mark messages as read
 *
 */

let prevNodes = getStartAndEndNodes();

const sendScrollMessage = () => {
  const currentNodes = getStartAndEndNodes();

  sendMessage({
    type: 'scroll',
    // See MessageListEventScroll for the meanings of these properties.
    offsetHeight: documentBody.offsetHeight,
    innerHeight: window.innerHeight,
    scrollY: window.scrollY,
    startMessageId: Math.min(prevNodes.start, currentNodes.start),
    endMessageId: Math.max(prevNodes.end, currentNodes.end),
  });
  prevNodes = currentNodes;
};

// If the message list is too short to scroll, fake a scroll event
// in order to cause the messages to be marked as read.
const sendScrollMessageIfListShort = () => {
  if (documentBody.scrollHeight === documentBody.clientHeight) {
    sendScrollMessage();
  }
};

/**
 * Disable reporting scrolls to the outside to mark messages as read.
 *
 * This is set while we're first setting up after the content loads, and
 * while we're handling `message` events from the outside and potentially
 * rewriting the content.
 */
let scrollEventsDisabled = true;

let lastTouchEventTimestamp = 0;
let lastTouchPositionX = -1;
let lastTouchPositionY = -1;

const handleScrollEvent = () => {
  lastTouchEventTimestamp = 0;
  if (scrollEventsDisabled) {
    return;
  }

  sendScrollMessage();

  const nearEnd = documentBody.offsetHeight - window.scrollY - window.innerHeight > 100;
  showHideElement('scroll-bottom', nearEnd);
};

window.addEventListener('scroll', handleScrollEvent);

/*
 *
 * Updating message content, and re-scrolling
 *
 */

type ScrollTarget =
  | { type: 'none' }
  | { type: 'bottom' }
  | { type: 'anchor', anchor: number }
  | { type: 'preserve', msgId: number, prevBoundTop: number };

const scrollToBottom = () => {
  window.scroll({ left: 0, top: documentBody.scrollHeight, behavior: 'smooth' });
};

const isNearBottom = (): boolean =>
  documentBody.scrollHeight - 100 < documentBody.scrollTop + documentBody.clientHeight;

const scrollToBottomIfNearEnd = () => {
  if (isNearBottom()) {
    scrollToBottom();
  }
};

const scrollToAnchor = (anchor: number) => {
  const anchorNode = document.getElementById(`msg-${anchor}`);

  if (anchorNode) {
    anchorNode.scrollIntoView({ block: 'start' });
  } else {
    window.scroll({ left: 0, top: documentBody.scrollHeight + 200 });
  }
};

// Try to identify a message on screen and its location, so we can
// scroll the corresponding message to the same place afterward.
const findPreserveTarget = (): ScrollTarget => {
  // TODO magic numbers
  const msgNode = getMessageNode(document.elementFromPoint(200, 50));
  if (!msgNode || !(msgNode instanceof HTMLElement)) {
    // TODO log this -- it's an error which the user will notice.
    // (We don't attempt this unless there are messages already,
    // which we really want to keep steady in view.)
    return { type: 'none' };
  }
  const msgId = getMessageIdFromNode(msgNode);
  const prevBoundRect = msgNode.getBoundingClientRect();
  return { type: 'preserve', msgId, prevBoundTop: prevBoundRect.top };
};

// Scroll the given message to the same height it was at before.
const scrollToPreserve = (msgId: number, prevBoundTop: number) => {
  const newElement = document.getElementById(`msg-${msgId}`);
  if (!newElement) {
    // TODO log this -- it's an error which the user will notice.
    return;
  }
  const newBoundRect = newElement.getBoundingClientRect();
  window.scrollBy(0, newBoundRect.top - prevBoundTop);
};

const appendAuthToImages = auth => {
  const imageTags = document.getElementsByTagName('img');
  Array.from(imageTags).forEach(img => {
    if (!img.src.startsWith(auth.realm)) {
      return;
    }

    // This unusual form of authentication is only accepted by the server
    // for a small number of routes.  Rather than append the API key to all
    // kinds of URLs on the server, do so only for those routes.
    const srcPath = img.src.substring(auth.realm.length);
    if (!(srcPath.startsWith('/user_uploads/') || srcPath.startsWith('/thumbnail?'))) {
      return;
    }

    const delimiter = img.src.includes('?') ? '&' : '?';
    img.src += `${delimiter}api_key=${auth.apiKey}`;
  });
};

const handleMessageContent = (msg: MessageInputContent) => {
  let target: ScrollTarget;
  if (msg.updateStrategy === 'replace') {
    target = { type: 'none' };
  } else if (msg.updateStrategy === 'scroll-to-anchor') {
    target = { type: 'anchor', anchor: msg.anchor };
  } else if (
    msg.updateStrategy === 'scroll-to-bottom-if-near-bottom'
    && isNearBottom() /* align */
  ) {
    target = { type: 'bottom' };
  } else {
    // including 'default' and 'preserve-position'
    target = findPreserveTarget();
  }

  documentBody.innerHTML = msg.content;

  appendAuthToImages(msg.auth);

  if (target.type === 'bottom') {
    scrollToBottom();
  } else if (target.type === 'anchor') {
    scrollToAnchor(target.anchor);
  } else if (target.type === 'preserve') {
    scrollToPreserve(target.msgId, target.prevBoundTop);
  }

  sendScrollMessageIfListShort();
};

/** We call this when the webview's content first loads. */
const handleInitialLoad = /* eslint-disable-line */ (anchor: number, auth: Auth) => {
  scrollToAnchor(anchor);
  appendAuthToImages(auth);
  sendScrollMessageIfListShort();
  scrollEventsDisabled = false;
};

/*
 *
 * Handling other message-from-outside events
 *
 */

const handleMessageFetching = (msg: MessageInputFetching) => {
  showHideElement('message-loading', msg.showMessagePlaceholders);
  showHideElement('spinner-older', msg.fetchingOlder);
  showHideElement('spinner-newer', msg.fetchingNewer);
};

const handleMessageTyping = (msg: MessageInputTyping) => {
  const elementTyping = document.getElementById('typing');
  if (elementTyping) {
    elementTyping.innerHTML = msg.content;
    setTimeout(() => scrollToBottomIfNearEnd());
  }
};

const messageHandlers = {
  content: handleMessageContent,
  fetching: handleMessageFetching,
  typing: handleMessageTyping,
};

document.addEventListener('message', e => {
  scrollEventsDisabled = true;
  // $FlowFixMe
  const decodedData = decodeURIComponent(escape(window.atob(e.data)));
  const messages: WebviewInputMessage[] = JSON.parse(decodedData);
  messages.forEach((msg: WebviewInputMessage) => {
    // $FlowFixMe
    messageHandlers[msg.type](msg);
  });
  scrollEventsDisabled = false;
});

/*
 *
 * Handling user touches
 *
 */

documentBody.addEventListener('click', (e: MouseEvent) => {
  e.preventDefault();
  lastTouchEventTimestamp = 0;

  const { target } = e;

  if (!(target instanceof HTMLElement)) {
    return;
  }

  if (target.matches('.scroll-bottom')) {
    scrollToBottom();
    return;
  }

  if (target.matches('.avatar-img')) {
    sendMessage({
      type: 'avatar',
      fromEmail: target.getAttribute('data-email'),
    });
    return;
  }

  if (target.matches('.header')) {
    sendMessage({
      type: 'narrow',
      narrow: target.getAttribute('data-narrow'),
      id: target.getAttribute('data-id'),
    });
    return;
  }

  /* Should we pull up the lightbox?  For comparison, see the webapp's
   * static/js/lightbox.js , starting at the `#main_div` click handler. */
  const inlineImageLink = target.closest('.message_inline_image a');
  if (
    inlineImageLink
    /* The webapp displays certain videos inline, but on mobile
     * we'd rather let another app handle them, as links. */
    && !inlineImageLink.closest('.youtube-video, .vimeo-video')
  ) {
    sendMessage({
      type: 'image',
      src: inlineImageLink.getAttribute('href'), // TODO: should be `src` / `data-src-fullsize`.
      messageId: getMessageIdFromNode(inlineImageLink),
    });
    return;
  }

  if (target.matches('a')) {
    sendMessage({
      type: 'url',
      href: target.getAttribute('href'),
      messageId: getMessageIdFromNode(target),
    });
    return;
  }

  if (target.parentNode instanceof Element && target.parentNode.matches('a')) {
    sendMessage({
      type: 'url',
      href: target.parentNode.getAttribute('href'),
      messageId: getMessageIdFromNode(target.parentNode),
    });
    return;
  }

  if (target.matches('.reaction')) {
    sendMessage({
      type: 'reaction',
      name: target.getAttribute('data-name'),
      code: target.getAttribute('data-code'),
      reactionType: target.getAttribute('data-type'),
      messageId: getMessageIdFromNode(target),
      voted: target.classList.contains('self-voted'),
    });
  }
});

const handleLongPress = (target: Element) => {
  // The logic that defines a "long press" in terms of raw touch events
  // is pretty subtle.  The `lastTouchEventTimestamp` and surrounding logic
  // are an attempt to define a long press.
  //
  // TODO: The logic around this "timestamp" is a bit obscure; it's
  // sometimes a real timestamp, other times 0 as sort of a boolean flag.
  // It would be good to clean it up to be clearer.
  //
  // At the same time, the logic is believed not to cover all the cases it
  // should; for example, multitouch events.  Better would be to either find
  // a library we can use which strives to handle all that complexity, or
  // get long-press events from the platform.
  if (!lastTouchEventTimestamp || Date.now() - lastTouchEventTimestamp < 500) {
    return;
  }

  lastTouchEventTimestamp = 0;

  sendMessage({
    type: 'longPress',
    target: target.matches('.header') ? 'header' : 'message',
    messageId: getMessageIdFromNode(target),
  });
};

documentBody.addEventListener('touchstart', (e: TouchEvent) => {
  const { target } = e;
  if (e.changedTouches[0].pageX < 20 || !(target instanceof Element)) {
    return;
  }

  lastTouchPositionX = e.changedTouches[0].pageX;
  lastTouchPositionY = e.changedTouches[0].pageY;
  lastTouchEventTimestamp = Date.now();
  setTimeout(() => handleLongPress(target), 500);
});

const isNearPositions = (x1: number = 0, y1: number = 0, x2: number = 0, y2: number = 0): boolean =>
  Math.abs(x1 - x2) < 10 && Math.abs(y1 - y2) < 10;

documentBody.addEventListener('touchend', (e: TouchEvent) => {
  if (
    isNearPositions(
      lastTouchPositionX,
      lastTouchPositionY,
      e.changedTouches[0].pageX,
      e.changedTouches[0].pageY,
    )
  ) {
    lastTouchEventTimestamp = Date.now();
  }
});

documentBody.addEventListener('touchmove', (e: TouchEvent) => {
  lastTouchEventTimestamp = 0;
});

documentBody.addEventListener('drag', (e: DragEvent) => {
  lastTouchEventTimestamp = 0;
});

/*
 *
 * Synchronizing setup with outside
 *
 */

const waitForBridge = () => {
  if (window.postMessage.length === 1) {
    sendMessage({ type: 'ready' });
  } else {
    setTimeout(waitForBridge, 10);
  }
};
waitForBridge();
<|endoftext|>


<commit_before>/* eslint-disable react/default-props-match-prop-types */
/* eslint-disable react/forbid-prop-types */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import React from 'react';
import PropTypes from 'prop-types';

import IonIcon from 'react-native-vector-icons/Ionicons';
import FAIcon from 'react-native-vector-icons/FontAwesome';
import FA5Icon from 'react-native-vector-icons/FontAwesome5';
import EvilIcon from 'react-native-vector-icons/EvilIcons';
import EntypoIcon from 'react-native-vector-icons/Entypo';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons';

import {
  WHITE,
  FINALISE_GREEN,
  SUSSOL_ORANGE,
  FINALISED_RED,
  DARK_GREY,
  dataTableColors,
  GREY,
} from '../globalStyles';

export const SortAscIcon = () => <FAIcon name="sort-asc" size={15} style={{ marginRight: 10 }} />;
export const SortNeutralIcon = () => <FAIcon name="sort" size={15} style={{ marginRight: 10 }} />;
export const SortDescIcon = () => <FAIcon name="sort-desc" size={15} style={{ marginRight: 10 }} />;

export const CheckedIcon = () => (
  <IonIcon name="md-radio-button-on" size={15} color={dataTableColors.checkableCellChecked} />
);

export const UncheckedIcon = () => (
  <IonIcon name="md-radio-button-off" size={15} color={SUSSOL_ORANGE} />
);

export const DisabledCheckedIcon = () => (
  <IonIcon name="md-radio-button-on" size={15} color={dataTableColors.checkableCellDisabled} />
);
export const DisabledUncheckedIcon = () => <MaterialCommunityIcon name="cancel" size={15} />;

export const MagnifyingGlassIcon = React.memo(({ size, color }) => (
  <EvilIcon name="search" size={size} color={color} />
));
MagnifyingGlassIcon.propTypes = { size: PropTypes.number, color: PropTypes.string };
MagnifyingGlassIcon.defaultProps = { size: 40, color: SUSSOL_ORANGE };

export const CancelIcon = React.memo(() => (
  <EntypoIcon name="cross" color={FINALISED_RED} size={25} />
));

export const CloseIcon = ({ size, color }) => <IonIcon name="md-close" size={size} color={color} />;
CloseIcon.defaultProps = { color: WHITE, size: 36 };
CloseIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const CogIcon = ({ size, color }) => <FAIcon name="cog" size={size} color={color} />;
CogIcon.defaultProps = { color: GREY, size: 20 };
CogIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const ExpandIcon = React.memo(({ style, color, size }) => (
  <FAIcon name="external-link" style={style} size={size} color={color} />
));

ExpandIcon.defaultProps = { color: SUSSOL_ORANGE, size: 16, style: {} };
ExpandIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const ConfirmIcon = React.memo(({ style }) => <FAIcon name="check-circle" style={style} />);
ConfirmIcon.defaultProps = { style: { color: FINALISE_GREEN, fontSize: 40 } };
ConfirmIcon.propTypes = { style: PropTypes.object };

export const LockIcon = React.memo(({ style }) => <FAIcon name="lock" size={28} style={style} />);
LockIcon.defaultProps = { style: { color: FINALISED_RED } };
LockIcon.propTypes = { style: PropTypes.object };

export const LanguageIcon = ({ size, color }) => (
  <FAIcon name="language" size={size} color={color} />
);
LanguageIcon.defaultProps = { color: GREY, size: 30 };
LanguageIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const ChevronDownIcon = React.memo(({ color, size, style }) => (
  <FA5Icon name="chevron-down" color={color} size={size} style={style} />
));
ChevronDownIcon.defaultProps = { color: WHITE, size: 20, style: {} };
ChevronDownIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const ChevronUpIcon = React.memo(({ color, size, style }) => (
  <FA5Icon name="chevron-up" color={color} size={size} style={style} />
));
ChevronUpIcon.defaultProps = { color: WHITE, size: 20, style: {} };
ChevronUpIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const PowerIcon = React.memo(({ color, size }) => (
  <FAIcon name="power-off" color={color} size={size} />
));
PowerIcon.defaultProps = { color: GREY, size: 20 };
PowerIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const PencilIcon = React.memo(({ color, size }) => (
  <FAIcon name="pencil" color={color} size={size} />
));
PencilIcon.defaultProps = { color: WHITE, size: 20 };
PencilIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const HistoryIcon = ({ color, size }) => (
  <FA5Icon name="history" size={size} color={color} />
);
HistoryIcon.defaultProps = { color: WHITE, size: 20 };
HistoryIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const ChevronRightIcon = () => (
  <FA5Icon name="chevron-right" color={SUSSOL_ORANGE} size={20} />
);
export const FavouriteStarIcon = () => <FAIcon name="star-o" color={SUSSOL_ORANGE} size={20} />;
export const BurgerMenuIcon = () => <EntypoIcon name="menu" color={SUSSOL_ORANGE} size={30} />;

export const BookIcon = ({ color, size }) => (
  <EntypoIcon name="open-book" color={color} size={size} />
);
BookIcon.defaultProps = { color: WHITE, size: 20 };
BookIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const AddIcon = ({ size, color }) => <IonIcon name="ios-add" size={size} color={color} />;
AddIcon.defaultProps = { color: WHITE, size: 30 };
AddIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const MinusIcon = ({ size, color }) => (
  <IonIcon name="ios-remove" size={size} color={color} />
);
MinusIcon.defaultProps = { color: WHITE, size: 30 };
MinusIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const CalendarIcon = ({ color, size, style }) => (
  <FAIcon name="calendar" size={size} style={style} color={color} />
);
CalendarIcon.defaultProps = { color: WHITE, size: 20, style: {} };
CalendarIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const BackIcon = () => <FAIcon name="chevron-left" size={16} color="black" />;

export const CloudIcon = ({ color, size, style }) => (
  <IonIcon name="md-cloud" size={size} color={color} style={style} />
);
CloudIcon.defaultProps = { color: DARK_GREY, size: 30, style: {} };
CloudIcon.propTypes = { color: PropTypes.string, size: PropTypes.number, style: PropTypes.object };

export const ArrowIcon = ({ color, size, style }) => (
  <FAIcon name="exchange" size={size} style={style} color={color} />
);
ArrowIcon.defaultProps = { color: DARK_GREY, size: 16, style: { marginLeft: 10 } };
ArrowIcon.propTypes = { color: PropTypes.string, size: PropTypes.number, style: PropTypes.object };

export const WifiIcon = ({ color, size, style }) => (
  <IonIcon name="logo-rss" size={size} style={style} color={color} />
);
WifiIcon.defaultProps = { color: DARK_GREY, size: 22, style: { marginLeft: 10 } };
WifiIcon.propTypes = { color: PropTypes.string, size: PropTypes.number, style: PropTypes.object };

export const TemperatureIcon = ({ color, size, style }) => (
  <FA5Icon name="temperature-high" size={size} style={style} color={color} />
);
TemperatureIcon.defaultProps = { color: DARK_GREY, size: 25, style: { marginLeft: 10 } };
TemperatureIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const HazardIcon = ({ color, size, style }) => (
  <FA5Icon name="exclamation-triangle" size={size} style={style} color={color} />
);
HazardIcon.defaultProps = { color: SUSSOL_ORANGE, size: 25, style: {} };
HazardIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const SyncArrow = ({ size, style, color }) => (
  <MaterialIcon name="sync" size={size} style={style} color={color} />
);
SyncArrow.defaultProps = { size: 30, style: {}, color: GREY };
SyncArrow.propTypes = { size: PropTypes.number, style: PropTypes.object, color: PropTypes.string };

export const SyncArrowDisabled = ({ size, style, color }) => (
  <MaterialIcon name="sync-disabled" size={size} style={style} color={color} />
);
SyncArrowDisabled.defaultProps = { size: 30, style: {}, color: GREY };
SyncArrowDisabled.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const SyncArrowProblem = ({ size, style, color }) => (
  <MaterialIcon name="sync-problem" size={size} style={style} color={color} />
);
SyncArrowProblem.defaultProps = { size: 30, style: {}, color: GREY };
SyncArrowProblem.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const LowBatteryIcon = ({ size, style, color }) => (
  <FAIcon size={size} style={style} color={color} name="battery-1" />
);

LowBatteryIcon.defaultProps = { size: 40, style: {}, color: WHITE };
LowBatteryIcon.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const BatteryIcon = ({ size, style, color }) => (
  <FAIcon size={size} style={style} color={color} name="battery-full" />
);

BatteryIcon.defaultProps = { size: 20, style: {}, color: WHITE };
BatteryIcon.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const HotBreachIcon = ({ size, style, color }) => (
  <FAIcon size={size} style={style} color={color} name="thermometer" />
);

HotBreachIcon.defaultProps = { size: 40, style: {}, color: WHITE };
HotBreachIcon.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const ColdBreachIcon = ({ size, style, color }) => (
  <FAIcon size={size} style={style} color={color} name="snowflake-o" />
);

ColdBreachIcon.defaultProps = { size: 40, style: {}, color: WHITE };
ColdBreachIcon.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};
<commit_msg>Add props to ConfirmIcon<commit_after>/* eslint-disable react/default-props-match-prop-types */
/* eslint-disable react/forbid-prop-types */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import React from 'react';
import PropTypes from 'prop-types';

import IonIcon from 'react-native-vector-icons/Ionicons';
import FAIcon from 'react-native-vector-icons/FontAwesome';
import FA5Icon from 'react-native-vector-icons/FontAwesome5';
import EvilIcon from 'react-native-vector-icons/EvilIcons';
import EntypoIcon from 'react-native-vector-icons/Entypo';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons';

import {
  WHITE,
  FINALISE_GREEN,
  SUSSOL_ORANGE,
  FINALISED_RED,
  DARK_GREY,
  dataTableColors,
  GREY,
} from '../globalStyles';

export const SortAscIcon = () => <FAIcon name="sort-asc" size={15} style={{ marginRight: 10 }} />;
export const SortNeutralIcon = () => <FAIcon name="sort" size={15} style={{ marginRight: 10 }} />;
export const SortDescIcon = () => <FAIcon name="sort-desc" size={15} style={{ marginRight: 10 }} />;

export const CheckedIcon = () => (
  <IonIcon name="md-radio-button-on" size={15} color={dataTableColors.checkableCellChecked} />
);

export const UncheckedIcon = () => (
  <IonIcon name="md-radio-button-off" size={15} color={SUSSOL_ORANGE} />
);

export const DisabledCheckedIcon = () => (
  <IonIcon name="md-radio-button-on" size={15} color={dataTableColors.checkableCellDisabled} />
);
export const DisabledUncheckedIcon = () => <MaterialCommunityIcon name="cancel" size={15} />;

export const MagnifyingGlassIcon = React.memo(({ size, color }) => (
  <EvilIcon name="search" size={size} color={color} />
));
MagnifyingGlassIcon.propTypes = { size: PropTypes.number, color: PropTypes.string };
MagnifyingGlassIcon.defaultProps = { size: 40, color: SUSSOL_ORANGE };

export const CancelIcon = React.memo(() => (
  <EntypoIcon name="cross" color={FINALISED_RED} size={25} />
));

export const CloseIcon = ({ size, color }) => <IonIcon name="md-close" size={size} color={color} />;
CloseIcon.defaultProps = { color: WHITE, size: 36 };
CloseIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const CogIcon = ({ size, color }) => <FAIcon name="cog" size={size} color={color} />;
CogIcon.defaultProps = { color: GREY, size: 20 };
CogIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const ExpandIcon = React.memo(({ style, color, size }) => (
  <FAIcon name="external-link" style={style} size={size} color={color} />
));

ExpandIcon.defaultProps = { color: SUSSOL_ORANGE, size: 16, style: {} };
ExpandIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const ConfirmIcon = React.memo(({ style, color, size }) => (
  <FAIcon name="check-circle" style={style} color={color} size={size} />
));
ConfirmIcon.defaultProps = { color: FINALISE_GREEN, size: 40, style: {} };
ConfirmIcon.propTypes = {
  style: PropTypes.object,
  color: PropTypes.string,
  size: PropTypes.number,
};

export const LockIcon = React.memo(({ style }) => <FAIcon name="lock" size={28} style={style} />);
LockIcon.defaultProps = { style: { color: FINALISED_RED } };
LockIcon.propTypes = { style: PropTypes.object };

export const LanguageIcon = ({ size, color }) => (
  <FAIcon name="language" size={size} color={color} />
);
LanguageIcon.defaultProps = { color: GREY, size: 30 };
LanguageIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const ChevronDownIcon = React.memo(({ color, size, style }) => (
  <FA5Icon name="chevron-down" color={color} size={size} style={style} />
));
ChevronDownIcon.defaultProps = { color: WHITE, size: 20, style: {} };
ChevronDownIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const ChevronUpIcon = React.memo(({ color, size, style }) => (
  <FA5Icon name="chevron-up" color={color} size={size} style={style} />
));
ChevronUpIcon.defaultProps = { color: WHITE, size: 20, style: {} };
ChevronUpIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const PowerIcon = React.memo(({ color, size }) => (
  <FAIcon name="power-off" color={color} size={size} />
));
PowerIcon.defaultProps = { color: GREY, size: 20 };
PowerIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const PencilIcon = React.memo(({ color, size }) => (
  <FAIcon name="pencil" color={color} size={size} />
));
PencilIcon.defaultProps = { color: WHITE, size: 20 };
PencilIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const HistoryIcon = ({ color, size }) => (
  <FA5Icon name="history" size={size} color={color} />
);
HistoryIcon.defaultProps = { color: WHITE, size: 20 };
HistoryIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const ChevronRightIcon = () => (
  <FA5Icon name="chevron-right" color={SUSSOL_ORANGE} size={20} />
);
export const FavouriteStarIcon = () => <FAIcon name="star-o" color={SUSSOL_ORANGE} size={20} />;
export const BurgerMenuIcon = () => <EntypoIcon name="menu" color={SUSSOL_ORANGE} size={30} />;

export const BookIcon = ({ color, size }) => (
  <EntypoIcon name="open-book" color={color} size={size} />
);
BookIcon.defaultProps = { color: WHITE, size: 20 };
BookIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const AddIcon = ({ size, color }) => <IonIcon name="ios-add" size={size} color={color} />;
AddIcon.defaultProps = { color: WHITE, size: 30 };
AddIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const MinusIcon = ({ size, color }) => (
  <IonIcon name="ios-remove" size={size} color={color} />
);
MinusIcon.defaultProps = { color: WHITE, size: 30 };
MinusIcon.propTypes = { color: PropTypes.string, size: PropTypes.number };

export const CalendarIcon = ({ color, size, style }) => (
  <FAIcon name="calendar" size={size} style={style} color={color} />
);
CalendarIcon.defaultProps = { color: WHITE, size: 20, style: {} };
CalendarIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const BackIcon = () => <FAIcon name="chevron-left" size={16} color="black" />;

export const CloudIcon = ({ color, size, style }) => (
  <IonIcon name="md-cloud" size={size} color={color} style={style} />
);
CloudIcon.defaultProps = { color: DARK_GREY, size: 30, style: {} };
CloudIcon.propTypes = { color: PropTypes.string, size: PropTypes.number, style: PropTypes.object };

export const ArrowIcon = ({ color, size, style }) => (
  <FAIcon name="exchange" size={size} style={style} color={color} />
);
ArrowIcon.defaultProps = { color: DARK_GREY, size: 16, style: { marginLeft: 10 } };
ArrowIcon.propTypes = { color: PropTypes.string, size: PropTypes.number, style: PropTypes.object };

export const WifiIcon = ({ color, size, style }) => (
  <IonIcon name="logo-rss" size={size} style={style} color={color} />
);
WifiIcon.defaultProps = { color: DARK_GREY, size: 22, style: { marginLeft: 10 } };
WifiIcon.propTypes = { color: PropTypes.string, size: PropTypes.number, style: PropTypes.object };

export const TemperatureIcon = ({ color, size, style }) => (
  <FA5Icon name="temperature-high" size={size} style={style} color={color} />
);
TemperatureIcon.defaultProps = { color: DARK_GREY, size: 25, style: { marginLeft: 10 } };
TemperatureIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const HazardIcon = ({ color, size, style }) => (
  <FA5Icon name="exclamation-triangle" size={size} style={style} color={color} />
);
HazardIcon.defaultProps = { color: SUSSOL_ORANGE, size: 25, style: {} };
HazardIcon.propTypes = {
  color: PropTypes.string,
  size: PropTypes.number,
  style: PropTypes.object,
};

export const SyncArrow = ({ size, style, color }) => (
  <MaterialIcon name="sync" size={size} style={style} color={color} />
);
SyncArrow.defaultProps = { size: 30, style: {}, color: GREY };
SyncArrow.propTypes = { size: PropTypes.number, style: PropTypes.object, color: PropTypes.string };

export const SyncArrowDisabled = ({ size, style, color }) => (
  <MaterialIcon name="sync-disabled" size={size} style={style} color={color} />
);
SyncArrowDisabled.defaultProps = { size: 30, style: {}, color: GREY };
SyncArrowDisabled.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const SyncArrowProblem = ({ size, style, color }) => (
  <MaterialIcon name="sync-problem" size={size} style={style} color={color} />
);
SyncArrowProblem.defaultProps = { size: 30, style: {}, color: GREY };
SyncArrowProblem.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const LowBatteryIcon = ({ size, style, color }) => (
  <FAIcon size={size} style={style} color={color} name="battery-1" />
);

LowBatteryIcon.defaultProps = { size: 40, style: {}, color: WHITE };
LowBatteryIcon.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const BatteryIcon = ({ size, style, color }) => (
  <FAIcon size={size} style={style} color={color} name="battery-full" />
);

BatteryIcon.defaultProps = { size: 20, style: {}, color: WHITE };
BatteryIcon.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const HotBreachIcon = ({ size, style, color }) => (
  <FAIcon size={size} style={style} color={color} name="thermometer" />
);

HotBreachIcon.defaultProps = { size: 40, style: {}, color: WHITE };
HotBreachIcon.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};

export const ColdBreachIcon = ({ size, style, color }) => (
  <FAIcon size={size} style={style} color={color} name="snowflake-o" />
);

ColdBreachIcon.defaultProps = { size: 40, style: {}, color: WHITE };
ColdBreachIcon.propTypes = {
  size: PropTypes.number,
  style: PropTypes.object,
  color: PropTypes.string,
};
<|endoftext|>


<commit_before>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

export { BackButton } from './BackButton';
export { BarChart } from './BarChart';
export { Button, ProgressBar } from 'react-native-ui-components';
export { DataTablePageView } from './DataTablePageView';
export { DropDown } from './DropDown';
export { ExpiryDateInput } from './ExpiryDateInput';
export { FinaliseButton } from './FinaliseButton';
export { Flag } from './Flag';
export { FlexColumn } from './FlexColumn';
export { FlexRow } from './FlexRow';
export { FlexView } from './FlexView';
export { FormTextInput } from './FormInputs/FormTextInput';
export { HeaderRight } from './HeaderRight';
export { HazardPoint } from './HazardPoint';
export { IconCell } from './IconCell';
export { IconButton } from './IconButton';
export { InfoBadge } from './InfoBadge';
export { LineChart } from './LineChart';
export { OnePressButton } from './OnePressButton';
export { PageButton } from './PageButton';
export { PageButtonWithOnePress } from './PageButtonWithOnePress';
export { PageInfo } from './PageInfo/PageInfo';
export { PieChart } from './PieChart';
export { ReportTable } from './ReportTable';
export { ResultRow } from './ResultRow';
export { SearchBar } from './SearchBar';
export { SimpleTable } from './SimpleTable';
export { Slider } from './Slider';
export { Spinner } from './Spinner';
export { Step } from './Step';
export { Stepper } from './Stepper';
export { StepperInput } from './StepperInput';
export { SyncState } from './SyncState';
export { TableShortcut } from './TableShortcuts';
export { TableShortcuts } from './TableShortcuts';
export { TabNavigator } from './TabNavigator';
export { TemperatureSyncState } from './TemperatureSyncState';
export { TextInput } from './TextInput';
export { ToggleBar } from './ToggleBar';
export { Wizard } from './Wizard';
export { FridgeDisplay } from './FridgeDisplay';
export { FridgeDisplayInfo } from './FridgeDisplayInfo';
export { KeyboardSpacing } from './KeyboardSpacing';

export * from './icons';
export * from './images';
export * from './TextInputs';
<commit_msg>Add exporting of components<commit_after>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

export { BackButton } from './BackButton';
export { BarChart } from './BarChart';
export { Button, ProgressBar } from 'react-native-ui-components';
export { DataTablePageView } from './DataTablePageView';
export { DropDown } from './DropDown';
export { ExpiryDateInput } from './ExpiryDateInput';
export { FinaliseButton } from './FinaliseButton';
export { Flag } from './Flag';
export { FlexColumn } from './FlexColumn';
export { FlexRow } from './FlexRow';
export { FlexView } from './FlexView';
export { FormTextInput } from './FormInputs/FormTextInput';
export { HeaderRight } from './HeaderRight';
export { HazardPoint } from './HazardPoint';
export { IconCell } from './IconCell';
export { IconButton } from './IconButton';
export { InfoBadge } from './InfoBadge';
export { LineChart } from './LineChart';
export { OnePressButton } from './OnePressButton';
export { PageButton } from './PageButton';
export { PageButtonWithOnePress } from './PageButtonWithOnePress';
export { PageInfo } from './PageInfo/PageInfo';
export { PieChart } from './PieChart';
export { ReportTable } from './ReportTable';
export { ResultRow } from './ResultRow';
export { SearchBar } from './SearchBar';
export { SimpleTable } from './SimpleTable';
export { Slider } from './Slider';
export { Spinner } from './Spinner';
export { Step } from './Step';
export { Stepper } from './Stepper';
export { StepperInput } from './StepperInput';
export { SyncState } from './SyncState';
export { TableShortcut } from './TableShortcuts';
export { TableShortcuts } from './TableShortcuts';
export { TabNavigator } from './TabNavigator';
export { TemperatureSyncState } from './TemperatureSyncState';
export { TextInput } from './TextInput';
export { ToggleBar } from './ToggleBar';
export { Wizard } from './Wizard';
export { FridgeDisplay } from './FridgeDisplay';
export { FridgeDisplayInfo } from './FridgeDisplayInfo';
export { KeyboardSpacing } from './KeyboardSpacing';
export { Paper } from './Paper';
export { PaperSection } from './PaperSection';

export * from './icons';
export * from './images';
export * from './TextInputs';
<|endoftext|>


<commit_before>/** @jsx React.DOM */
(function(Perseus) {

var BaseRadio = React.createClass({
    render: function() {
        var radioGroupName = _.uniqueId("perseus_radio_");
        var inputType = this.props.multipleSelect ? "checkbox" : "radio";

        return

            {this.props.choices.map(function(choice, i) {
                return

                        <input
                            ref={"radio" + i}
                            type={inputType}
                            name={radioGroupName}
                            checked={choice.checked}
                            onChange={this.onChange.bind(this, i)} />
                        {choice.content}

;
            }, this)}

;
    },

    onChange: React.autoBind(function(radioIndex, e) {
        var newChecked = _.map(this.props.choices, function(choice, i) {
            if (i === radioIndex) {
                return e.target.checked;
            } else {
                if (this.props.multipleSelect) {
                    return choice.checked;
                } else {
                    return false;
                }
            }
        }, this);

        this.props.onCheckedChange(newChecked);
    }),

    focus: function(i) {
        this.refs["radio" + (i || 0)].getDOMNode().focus();
        return true;
    }
});

var Radio = React.createClass({
    getDefaultProps: function() {
        return {
            choices: [{}],
            randomize: false,
            multipleSelect: false
        };
    },

    render: function() {
        var isEditor = this.props.isEditor;
        var choices = this.props.choices.map(function(choice, i) {
            return {
                // We need to make a copy, which _.pick does
                content: Perseus.Renderer(_.pick(choice, "content")),
                checked: false,
                originalIndex: i
            };
        });
        choices = this.randomize(choices);

        return <BaseRadio
            ref="baseRadio"
            multipleSelect={this.props.multipleSelect}
            choices={choices.map(function(choice) {
                return _.pick(choice, "content", "checked");
            })}
            onCheckedChange={this.onCheckedChange} />
    },

    focus: function(i) {
        return this.refs.baseRadio.focus(i);
    },

    onCheckedChange: React.autoBind(function(checked) {
        this.props.onChange({values: this.derandomize(checked)});
    }),

    toJSON: function(skipValidation) {
        // Return checked inputs in the form {values: [bool]}. (Dear future
        // timeline implementers: this used to be {value: i} before multiple
        // select was added)
        if (this.props.values) {
            return _.pick(this.props, "values");
        } else {
            // Nothing checked
            return {
                values: _.map(this.props.choices, function() {
                    return false;
                })
            }
        }
    },

    simpleValidate: function(rubric) {
        return Radio.validate(this.toJSON(), rubric);
    },

    randomize: function(array) {
        if (this.props.randomize && this.props.problemNum) {
            return Perseus.Util.shuffle(array, this.props.problemNum);
        } else {
            return array;
        }
    },

    derandomize: function(array) {
        if (this.props.randomize && this.props.problemNum) {
            var map = Perseus.Util.shuffle(
                    _.range(array.length), this.props.problemNum);
            var derandomized = new Array(array.length);
            _.each(map, function(shuffledIndex, originalIndex) {
                derandomized[shuffledIndex] = array[originalIndex];
            });
            return derandomized;
        } else {
            return array;
        }
    }
});

_.extend(Radio, {
    validate: function(state, rubric) {
        if (!_.any(state.values)) {
            return {
                type: "invalid",
                message: null
            };
        } else {
            var correct = _.all(state.values, function(selected, i) {
                return rubric.choices[i].correct === selected;
            });

            return {
                type: "points",
                earned: correct ? 1 : 0,
                total: 1,
                message: null
            };
        }
    }
});

var RadioEditor = React.createClass({
    getDefaultProps: function() {
        return {
            choices: [{}],
            randomize: false,
            multipleSelect: false
        };
    },

    render: function() {
        return

            <BaseRadio
                ref="baseRadio"
                multipleSelect={this.props.multipleSelect}
                choices={this.props.choices.map(function(choice, i) {
                    var editor = Perseus.Editor({
                        ref: "editor" + i,
                        content: choice.content,
                        widgetEnabled: false,
                        onChange: function(newProps) {
                            if ("content" in newProps) {
                                this.onContentChange(i, newProps.content);
                            }
                        }.bind(this)
                    });
                    return {
                        content: editor,
                        checked: choice.correct
                    };
                }, this)}
                onCheckedChange={this.onCheckedChange} />

                    Add a choice

                <input
                    type="checkbox"
                    checked={this.props.randomize}
                    onChange={function(e) {
                        this.props.onChange({randomize: e.target.checked});
                    }.bind(this)} />
                Randomize answer order

                <input
                    type="checkbox"
                    checked={this.props.multipleSelect}
                    onChange={function(e) {
                        this.props.onChange({multipleSelect:
                                e.target.checked});
                    }.bind(this)} />
                Allow multiple selections

;
    },

    onCheckedChange: React.autoBind(function(checked) {
        var choices = _.map(this.props.choices, function(choice, i) {
            return _.extend({}, choice, {correct: checked[i]});
        });
        this.props.onChange({choices: choices});
    }),

    onContentChange: React.autoBind(function(choiceIndex, newContent, e) {
        var choices = this.props.choices.slice();
        choices[choiceIndex] = _.extend({}, choices[choiceIndex], {
            content: newContent
        });
        this.props.onChange({choices: choices});
    }),

    addChoice: React.autoBind(function(e) {
        e.preventDefault();

        var choices = this.props.choices;
        this.props.onChange({choices: choices.concat([{}])});
        this.refs["editor" + choices.length].focus();
    }),

    focus: function() {
        this.refs.editor0.focus();
        return true;
    },

    toJSON: function(skipValidation) {
        if (!skipValidation &&
                !_.some(_.pluck(this.props.choices, "correct"))) {
            alert("Warning: No choice is marked as correct.");
        }

        return _.pick(this.props, "choices", "randomize", "multipleSelect");
    }
});

Perseus.Widgets.register("radio", Radio);
Perseus.Widgets.register("radio-editor", RadioEditor);

})(Perseus);
<commit_msg>Fix multiple select<commit_after>/** @jsx React.DOM */
(function(Perseus) {

var BaseRadio = React.createClass({
    render: function() {
        var radioGroupName = _.uniqueId("perseus_radio_");
        var inputType = this.props.multipleSelect ? "checkbox" : "radio";

        return

            {this.props.choices.map(function(choice, i) {
                return

                        <input
                            ref={"radio" + i}
                            type={inputType}
                            name={radioGroupName}
                            checked={choice.checked}
                            onChange={this.onChange.bind(this, i)} />
                        {choice.content}

;
            }, this)}

;
    },

    onChange: React.autoBind(function(radioIndex, e) {
        var newChecked = _.map(this.props.choices, function(choice, i) {
            if (i === radioIndex) {
                return e.target.checked;
            } else {
                if (this.props.multipleSelect) {
                    return this.refs["radio" + i].getDOMNode().checked;
                } else {
                    return false;
                }
            }
        }, this);

        this.props.onCheckedChange(newChecked);
    }),

    focus: function(i) {
        this.refs["radio" + (i || 0)].getDOMNode().focus();
        return true;
    }
});

var Radio = React.createClass({
    getDefaultProps: function() {
        return {
            choices: [{}],
            randomize: false,
            multipleSelect: false
        };
    },

    render: function() {
        var isEditor = this.props.isEditor;
        var choices = this.props.choices.map(function(choice, i) {
            return {
                // We need to make a copy, which _.pick does
                content: Perseus.Renderer(_.pick(choice, "content")),
                checked: false,
                originalIndex: i
            };
        });
        choices = this.randomize(choices);

        return <BaseRadio
            ref="baseRadio"
            multipleSelect={this.props.multipleSelect}
            choices={choices.map(function(choice) {
                return _.pick(choice, "content", "checked");
            })}
            onCheckedChange={this.onCheckedChange} />
    },

    focus: function(i) {
        return this.refs.baseRadio.focus(i);
    },

    onCheckedChange: React.autoBind(function(checked) {
        this.props.onChange({values: this.derandomize(checked)});
    }),

    toJSON: function(skipValidation) {
        // Return checked inputs in the form {values: [bool]}. (Dear future
        // timeline implementers: this used to be {value: i} before multiple
        // select was added)
        if (this.props.values) {
            return _.pick(this.props, "values");
        } else {
            // Nothing checked
            return {
                values: _.map(this.props.choices, function() {
                    return false;
                })
            }
        }
    },

    simpleValidate: function(rubric) {
        return Radio.validate(this.toJSON(), rubric);
    },

    randomize: function(array) {
        if (this.props.randomize && this.props.problemNum) {
            return Perseus.Util.shuffle(array, this.props.problemNum);
        } else {
            return array;
        }
    },

    derandomize: function(array) {
        if (this.props.randomize && this.props.problemNum) {
            var map = Perseus.Util.shuffle(
                    _.range(array.length), this.props.problemNum);
            var derandomized = new Array(array.length);
            _.each(map, function(shuffledIndex, originalIndex) {
                derandomized[shuffledIndex] = array[originalIndex];
            });
            return derandomized;
        } else {
            return array;
        }
    }
});

_.extend(Radio, {
    validate: function(state, rubric) {
        if (!_.any(state.values)) {
            return {
                type: "invalid",
                message: null
            };
        } else {
            var correct = _.all(state.values, function(selected, i) {
                return !!rubric.choices[i].correct === selected;
            });

            return {
                type: "points",
                earned: correct ? 1 : 0,
                total: 1,
                message: null
            };
        }
    }
});

var RadioEditor = React.createClass({
    getDefaultProps: function() {
        return {
            choices: [{}],
            randomize: false,
            multipleSelect: false
        };
    },

    render: function() {
        return

            <BaseRadio
                ref="baseRadio"
                multipleSelect={this.props.multipleSelect}
                choices={this.props.choices.map(function(choice, i) {
                    var editor = Perseus.Editor({
                        ref: "editor" + i,
                        content: choice.content,
                        widgetEnabled: false,
                        onChange: function(newProps) {
                            if ("content" in newProps) {
                                this.onContentChange(i, newProps.content);
                            }
                        }.bind(this)
                    });
                    return {
                        content: editor,
                        checked: choice.correct
                    };
                }, this)}
                onCheckedChange={this.onCheckedChange} />

                    Add a choice

                <input
                    type="checkbox"
                    checked={this.props.randomize}
                    onChange={function(e) {
                        this.props.onChange({randomize: e.target.checked});
                    }.bind(this)} />
                Randomize answer order

                <input
                    type="checkbox"
                    checked={this.props.multipleSelect}
                    onChange={function(e) {
                        this.props.onChange({multipleSelect:
                                e.target.checked});
                    }.bind(this)} />
                Allow multiple selections

;
    },

    onCheckedChange: React.autoBind(function(checked) {
        var choices = _.map(this.props.choices, function(choice, i) {
            return _.extend({}, choice, {correct: checked[i]});
        });
        this.props.onChange({choices: choices});
    }),

    onContentChange: React.autoBind(function(choiceIndex, newContent, e) {
        var choices = this.props.choices.slice();
        choices[choiceIndex] = _.extend({}, choices[choiceIndex], {
            content: newContent
        });
        this.props.onChange({choices: choices});
    }),

    addChoice: React.autoBind(function(e) {
        e.preventDefault();

        var choices = this.props.choices;
        this.props.onChange({choices: choices.concat([{}])});
        this.refs["editor" + choices.length].focus();
    }),

    focus: function() {
        this.refs.editor0.focus();
        return true;
    },

    toJSON: function(skipValidation) {
        if (!skipValidation &&
                !_.some(_.pluck(this.props.choices, "correct"))) {
            alert("Warning: No choice is marked as correct.");
        }

        return _.pick(this.props, "choices", "randomize", "multipleSelect");
    }
});

Perseus.Widgets.register("radio", Radio);
Perseus.Widgets.register("radio-editor", RadioEditor);

})(Perseus);
<|endoftext|>


<commit_before>import WorkerStream from './WorkerStream'
import WebWorkify from 'WebWorkify'
//import foo from './foo'
export default function workerSpawner () {
  //const worker = new Worker('./stlStreamWorker.src.js')
  const worker = WebWorkify(require('./stlStreamWorker.src.js'))
  const ws = new WorkerStream(worker)
  return ws
}
<commit_msg>fix(webWorkify): fixes lower/upper case import of webWorkify<commit_after>import WorkerStream from './WorkerStream'
import WebWorkify from 'webWorkify'
//import foo from './foo'
export default function workerSpawner () {
  //const worker = new Worker('./stlStreamWorker.src.js')
  const worker = WebWorkify(require('./stlStreamWorker.src.js'))
  const ws = new WorkerStream(worker)
  return ws
}
<|endoftext|>


<commit_before>var ssllabs = require('node-ssllabs');
var https = require('https');
var file = 'public/top-1m.csv';
var port = 80;
var url = {
	name: null,
	valid_to: null,
	cipher: null,
	grade: null,
	ssl: true
}
var EXPIRING = 0;
var STABLE = 1;
var NO_SSL = 2;
var SHA1 = 3;

function scan_urls(url_array, stable_array, expiring_array, no_ssl_array, sha1_array, i, j, date) {

	var time = new Date();
	var rl = require('readline').createInterface({
		input: require('fs').createReadStream(file)
	});

	rl.on('line', function (line) {
		i++;
		var x = line.split(',')[1];
		check_ssl(x);
	});

	rl.on('close', function (line) {
		done();
	});

	function done() {
		if (((j/1.0) / i ) > 0.95)  {
			//console.log(url_array.length);
			//console.log((new Date()) - time  );
			//console.log(url_array[url_array.length-1]);
		} else {
			//console.log(i + " " + j);
			setTimeout(done,100);
		}
	}

	function check_ssl(target) {
		var new_url = Object.create(url);
		new_url.name = target;
		var options = {
		    host: target,
		    port: 443,
		    method: 'GET'
		};
		https.request(options, function(res) {
			new_url.valid_to = res.connection.getPeerCertificate().valid_to;
			new_url.cipher = res.connection.getCipher().name;
			url_array.push(new_url);
			if (true) {
				//if cipher is sha1
				sha1_array.push(new_url);
			}
			if (true) {
				//if within date
				expiring_array.push(new_url);
			} else {
				//if outside range
				stable_array.push(new_url);
			}
			j++;
			console.log(new_url.cipher);
		}).on('error', function(e) {
			new_url.ssl = false;
			url_array.push(new_url);
			no_ssl_array.push(new_url);
			j++;
			return;
		}).end();

		ssllabs.scan(target, function (err, host) {
		    if (err || !(host.endpoints)) {
			    return;
			}
			host.endpoints.forEach(function (endpoint) {
			    new_url.grade = endpoint.grade;
			});
		});
	}
}

var app = require('express').createServer();
var io = require('socket.io')(app);

app.listen(port);

app.get('/', function (req, res) {
  res.sendfile(__dirname + '/index.html');
});

io.on('connection', function (socket) {
	socket.url_array = [];
	socket.stable_array = [];
	socket.expiring_array = [];
	socket.no_ssl_array = [];
	socket.sha1_array = [];
	socket.i = 0;
	socket.j = 0;

	socket.on('start', function (data) {
		var date = data.date;
		scan_urls(url_array, stable_array, expiring_array, no_ssl_array, sha1_array, socket.i, socket.j, date);
	});

	socket.on('update_me', function (data) {
		var urls_to_send - null;
		data = JSON.parse(data);
		if (data.hasOwnProperty('last_index')) {
			var last_index = data.lastIndex;
			new_index = socket.url_array.length;
			if (new_index > last_index) {
				new_urls = url_array.slice(last_index, new_index);
			}
		}
		var first = data.page * 20;
		switch (data.type) {
			case EXPIRING:
				var last = Math.min((data.page + 1) * 20, socket.expiring_array.length);
				urls_to_send = socket.expiring_array.slice( first, last );
				break;
			case STABLE:
				var last = Math.min((data.page + 1) * 20, socket.stable_array.length);
				urls_to_send = socket.stable_array.slice( first, last );
				break;
			case NO_SSL:
				var last = Math.min((data.page + 1) * 20, socket.no_ssl_array.length);
				urls_to_send = socket.no_ssl_array.slice( first, last );
				break;
			case SHA1:
				var last = Math.min((data.page + 1) * 20, socket.sha1_array.length);
				urls_to_send = socket.sha1_array.slice( first, last );
				break;
		}
		var return_data = {
			urls_to_send: urls_to_send,
			num_total: socket.url_array.length,
			num_expired: socket.expiring_array.length,
			num_stable: socket.stable_array.length,
			num_sha1: socket.sha1_array.length
		}

		console.log(data);
	});
});
<commit_msg>Added =<commit_after>var ssllabs = require('node-ssllabs');
var https = require('https');
var file = 'public/top-1m.csv';
var port = 80;
var url = {
	name: null,
	valid_to: null,
	cipher: null,
	grade: null,
	ssl: true
}
var EXPIRING = 0;
var STABLE = 1;
var NO_SSL = 2;
var SHA1 = 3;

function scan_urls(url_array, stable_array, expiring_array, no_ssl_array, sha1_array, i, j, date) {

	var time = new Date();
	var rl = require('readline').createInterface({
		input: require('fs').createReadStream(file)
	});

	rl.on('line', function (line) {
		i++;
		var x = line.split(',')[1];
		check_ssl(x);
	});

	rl.on('close', function (line) {
		done();
	});

	function done() {
		if (((j/1.0) / i ) > 0.95)  {
			//console.log(url_array.length);
			//console.log((new Date()) - time  );
			//console.log(url_array[url_array.length-1]);
		} else {
			//console.log(i + " " + j);
			setTimeout(done,100);
		}
	}

	function check_ssl(target) {
		var new_url = Object.create(url);
		new_url.name = target;
		var options = {
		    host: target,
		    port: 443,
		    method: 'GET'
		};
		https.request(options, function(res) {
			new_url.valid_to = res.connection.getPeerCertificate().valid_to;
			new_url.cipher = res.connection.getCipher().name;
			url_array.push(new_url);
			if (true) {
				//if cipher is sha1
				sha1_array.push(new_url);
			}
			if (true) {
				//if within date
				expiring_array.push(new_url);
			} else {
				//if outside range
				stable_array.push(new_url);
			}
			j++;
			console.log(new_url.cipher);
		}).on('error', function(e) {
			new_url.ssl = false;
			url_array.push(new_url);
			no_ssl_array.push(new_url);
			j++;
			return;
		}).end();

		ssllabs.scan(target, function (err, host) {
		    if (err || !(host.endpoints)) {
			    return;
			}
			host.endpoints.forEach(function (endpoint) {
			    new_url.grade = endpoint.grade;
			});
		});
	}
}

var app = require('express').createServer();
var io = require('socket.io')(app);

app.listen(port);

app.get('/', function (req, res) {
  res.sendfile(__dirname + '/index.html');
});

io.on('connection', function (socket) {
	socket.url_array = [];
	socket.stable_array = [];
	socket.expiring_array = [];
	socket.no_ssl_array = [];
	socket.sha1_array = [];
	socket.i = 0;
	socket.j = 0;

	socket.on('start', function (data) {
		var date = data.date;
		scan_urls(url_array, stable_array, expiring_array, no_ssl_array, sha1_array, socket.i, socket.j, date);
	});

	socket.on('update_me', function (data) {
		var urls_to_send = null;
		data = JSON.parse(data);
		if (data.hasOwnProperty('last_index')) {
			var last_index = data.lastIndex;
			new_index = socket.url_array.length;
			if (new_index > last_index) {
				new_urls = url_array.slice(last_index, new_index);
			}
		}
		var first = data.page * 20;
		switch (data.type) {
			case EXPIRING:
				var last = Math.min((data.page + 1) * 20, socket.expiring_array.length);
				urls_to_send = socket.expiring_array.slice( first, last );
				break;
			case STABLE:
				var last = Math.min((data.page + 1) * 20, socket.stable_array.length);
				urls_to_send = socket.stable_array.slice( first, last );
				break;
			case NO_SSL:
				var last = Math.min((data.page + 1) * 20, socket.no_ssl_array.length);
				urls_to_send = socket.no_ssl_array.slice( first, last );
				break;
			case SHA1:
				var last = Math.min((data.page + 1) * 20, socket.sha1_array.length);
				urls_to_send = socket.sha1_array.slice( first, last );
				break;
		}
		var return_data = {
			urls_to_send: urls_to_send,
			num_total: socket.url_array.length,
			num_expired: socket.expiring_array.length,
			num_stable: socket.stable_array.length,
			num_sha1: socket.sha1_array.length
		}

		console.log(data);
	});
});
<|endoftext|>