<commit_before>/**
 * Copyright 2018-present Facebook.
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 * @format
 */

import type {
  TableBodyRow,
  TableColumnOrder,
  TableColumnSizes,
  TableColumns,
} from 'flipper';
import type {Counter} from './LogWatcher.js';
import type {DeviceLogEntry} from '../../devices/BaseDevice.js';
import type {Props as PluginProps} from '../../plugin';

import {
  Text,
  ManagedTable,
  Button,
  colors,
  ContextMenu,
  FlexColumn,
  Glyph,
  DetailSidebar,
  FlipperDevicePlugin,
  SearchableTable,
  styled,
  Device,
  createPaste,
  textContent,
} from 'flipper';
import LogWatcher from './LogWatcher';

const LOG_WATCHER_LOCAL_STORAGE_KEY = 'LOG_WATCHER_LOCAL_STORAGE_KEY';

type Entries = Array<{
  row: TableBodyRow,
  entry: DeviceLogEntry,
}>;

type State = {|
  rows: Array<TableBodyRow>,
  entries: Entries,
  key2entry: {[key: string]: DeviceLogEntry},
  highlightedRows: Set<string>,
  counters: Array<Counter>,
|};

type Actions = {||};

type PersistedState = {||};

const Icon = styled(Glyph)({
  marginTop: 5,
});

function getLineCount(str: string): number {
  let count = 1;
  if (!(typeof str === 'string')) {
    return 0;
  }
  for (let i = 0; i < str.length; i++) {
    if (str[i] === '\n') {
      count++;
    }
  }
  return count;
}

function keepKeys(obj, keys) {
  const result = {};
  for (const key in obj) {
    if (keys.includes(key)) {
      result[key] = obj[key];
    }
  }
  return result;
}

const COLUMN_SIZE = {
  type: 40,
  time: 120,
  pid: 60,
  tid: 60,
  tag: 120,
  app: 200,
  message: 'flex',
};

const COLUMNS = {
  type: {
    value: '',
  },
  time: {
    value: 'Time',
  },
  pid: {
    value: 'PID',
  },
  tid: {
    value: 'TID',
  },
  tag: {
    value: 'Tag',
  },
  app: {
    value: 'App',
  },
  message: {
    value: 'Message',
  },
};

const INITIAL_COLUMN_ORDER = [
  {
    key: 'type',
    visible: true,
  },
  {
    key: 'time',
    visible: false,
  },
  {
    key: 'pid',
    visible: false,
  },
  {
    key: 'tid',
    visible: false,
  },
  {
    key: 'tag',
    visible: true,
  },
  {
    key: 'app',
    visible: true,
  },
  {
    key: 'message',
    visible: true,
  },
];

const LOG_TYPES: {
  [level: string]: {
    label: string,
    color: string,
    icon?: React.Node,
    style?: Object,
  },
} = {
  verbose: {
    label: 'Verbose',
    color: colors.purple,
  },
  debug: {
    label: 'Debug',
    color: colors.grey,
  },
  info: {
    label: 'Info',
    icon: <Icon name="info-circle" color={colors.cyan} />,
    color: colors.cyan,
  },
  warn: {
    label: 'Warn',
    style: {
      backgroundColor: colors.yellowTint,
      color: colors.yellow,
      fontWeight: 500,
    },
    icon: <Icon name="caution-triangle" color={colors.yellow} />,
    color: colors.yellow,
  },
  error: {
    label: 'Error',
    style: {
      backgroundColor: colors.redTint,
      color: colors.red,
      fontWeight: 500,
    },
    icon: <Icon name="caution-octagon" color={colors.red} />,
    color: colors.red,
  },
  fatal: {
    label: 'Fatal',
    style: {
      backgroundColor: colors.redTint,
      color: colors.red,
      fontWeight: 700,
    },
    icon: <Icon name="stop" color={colors.red} />,
    color: colors.red,
  },
};

const DEFAULT_FILTERS = [
  {
    type: 'enum',
    enum: Object.keys(LOG_TYPES).map(value => ({
      label: LOG_TYPES[value].label,
      value,
    })),
    key: 'type',
    value: [],
    persistent: true,
  },
];

const HiddenScrollText = styled(Text)({
  alignSelf: 'baseline',
  userSelect: 'none',
  lineHeight: '130%',
  marginTop: 5,
  paddingBottom: 3,
  '&::-webkit-scrollbar': {
    display: 'none',
  },
});

const LogCount = styled('div')(({backgroundColor}) => ({
  backgroundColor,
  borderRadius: '999em',
  fontSize: 11,
  marginTop: 4,
  minWidth: 16,
  height: 16,
  color: colors.white,
  textAlign: 'center',
  lineHeight: '16px',
  paddingLeft: 4,
  paddingRight: 4,
  textOverflow: 'ellipsis',
  overflow: 'hidden',
  whiteSpace: 'nowrap',
}));

function pad(chunk: mixed, len: number): string {
  let str = String(chunk);
  while (str.length < len) {
    str = `0${str}`;
  }
  return str;
}

export function addEntriesToState(
  items: Entries,
  state: $Shape<State> = {
    rows: [],
    entries: [],
    key2entry: {},
  },
): $Shape<State> {
  const rows = [...state.rows];
  const entries = [...state.entries];
  const key2entry = {...state.key2entry};

  for (let i = 0; i < items.length; i++) {
    const {entry, row} = items[i];
    entries.push({row, entry});
    key2entry[row.key] = entry;

    let previousEntry: ?DeviceLogEntry = null;

    if (i > 0) {
      previousEntry = items[i - 1].entry;
    } else if (state.rows.length > 0 && state.entries.length > 0) {
      previousEntry = state.entries[state.entries.length - 1].entry;
    }

    addRowIfNeeded(rows, row, entry, previousEntry);
  }

  return {
    entries,
    rows,
    key2entry,
  };
}

export function addRowIfNeeded(
  rows: Array<TableBodyRow>,
  row: TableBodyRow,
  entry: DeviceLogEntry,
  previousEntry: ?DeviceLogEntry,
) {
  const previousRow = rows.length > 0 ? rows[rows.length - 1] : null;
  if (
    previousRow &&
    previousEntry &&
    entry.message === previousEntry.message &&
    entry.tag === previousEntry.tag &&
    previousRow.type != null
  ) {
    // duplicate log, increase counter
    const count =
      previousRow.columns.type.value &&
      previousRow.columns.type.value.props &&
      typeof previousRow.columns.type.value.props.children === 'number'
        ? previousRow.columns.type.value.props.children + 1
        : 2;
    const type = LOG_TYPES[previousRow.type] || LOG_TYPES.debug;
    previousRow.columns.type.value = (
      <LogCount backgroundColor={type.color}>{count}</LogCount>
    );
  } else {
    rows.push(row);
  }
}

export function processEntry(
  entry: DeviceLogEntry,
  key: string,
): {
  row: TableBodyRow,
  entry: DeviceLogEntry,
} {
  const {icon, style} = LOG_TYPES[(entry.type: string)] || LOG_TYPES.debug;
  // build the item, it will either be batched or added straight away
  return {
    entry,
    row: {
      columns: {
        type: {
          value: icon,
          align: 'center',
        },
        time: {
          value: (
            <HiddenScrollText code={true}>
              {entry.date.toTimeString().split(' ')[0] +
                '.' +
                pad(entry.date.getMilliseconds(), 3)}
            </HiddenScrollText>
          ),
        },
        message: {
          value: (
            <HiddenScrollText code={true}>{entry.message}</HiddenScrollText>
          ),
        },
        tag: {
          value: <HiddenScrollText code={true}>{entry.tag}</HiddenScrollText>,
          isFilterable: true,
        },
        pid: {
          value: (
            <HiddenScrollText code={true}>{String(entry.pid)}</HiddenScrollText>
          ),
          isFilterable: true,
        },
        tid: {
          value: (
            <HiddenScrollText code={true}>{String(entry.tid)}</HiddenScrollText>
          ),
          isFilterable: true,
        },
        app: {
          value: <HiddenScrollText code={true}>{entry.app}</HiddenScrollText>,
          isFilterable: true,
        },
      },
      height: getLineCount(entry.message) * 15 + 10, // 15px per line height + 8px padding
      style,
      type: entry.type,
      filterValue: entry.message,
      key,
    },
  };
}

export default class LogTable extends FlipperDevicePlugin<
  State,
  Actions,
  PersistedState,
> {
  static keyboardActions = ['clear', 'goToBottom', 'createPaste'];

  initTimer: ?TimeoutID;
  batchTimer: ?TimeoutID;

  static supportsDevice(device: Device) {
    return device.os === 'iOS' || device.os === 'Android';
  }

  onKeyboardAction = (action: string) => {
    if (action === 'clear') {
      this.clearLogs();
    } else if (action === 'goToBottom') {
      this.goToBottom();
    } else if (action === 'createPaste') {
      this.createPaste();
    }
  };

  restoreSavedCounters = (): Array<Counter> => {
    const savedCounters =
      window.localStorage.getItem(LOG_WATCHER_LOCAL_STORAGE_KEY) || '[]';
    return JSON.parse(savedCounters).map((counter: Counter) => ({
      ...counter,
      expression: new RegExp(counter.label, 'gi'),
      count: 0,
    }));
  };

  calculateHighlightedRows = (
    deepLinkPayload: ?string,
    rows: Array<TableBodyRow>,
  ): Set<string> => {
    const highlightedRows = new Set();
    if (!deepLinkPayload) {
      return highlightedRows;
    }

    // Run through array from last to first, because we want to show the last
    // time it the log we are looking for appeared.
    for (let i = rows.length - 1; i >= 0; i--) {
      if (
        rows[i].filterValue &&
        rows[i].filterValue.includes(deepLinkPayload)
      ) {
        highlightedRows.add(rows[i].key);
        break;
      }
    }
    if (highlightedRows.size <= 0) {
      // Check if the individual lines in the deeplinkPayload is matched or not.
      const arr = deepLinkPayload.split('\n');
      for (let msg of arr) {
        for (let i = rows.length - 1; i >= 0; i--) {
          if (rows[i].filterValue && rows[i].filterValue.includes(msg)) {
            highlightedRows.add(rows[i].key);
            break;
          }
        }
      }
    }
    return highlightedRows;
  };

  tableRef: ?ManagedTable;
  columns: TableColumns;
  columnSizes: TableColumnSizes;
  columnOrder: TableColumnOrder;
  logListener: ?Symbol;

  batch: Entries = [];
  queued: boolean = false;
  counter: number = 0;

  constructor(props: PluginProps<PersistedState>) {
    super(props);
    const supportedColumns = this.device.supportedColumns();
    this.columns = keepKeys(COLUMNS, supportedColumns);
    this.columnSizes = keepKeys(COLUMN_SIZE, supportedColumns);
    this.columnOrder = INITIAL_COLUMN_ORDER.filter(obj =>
      supportedColumns.includes(obj.key),
    );

    const initialState = addEntriesToState(
      this.device
        .getLogs()
        .map(log => processEntry(log, String(this.counter++))),
    );
    this.state = {
      ...initialState,
      highlightedRows: this.calculateHighlightedRows(
        props.deepLinkPayload,
        initialState.rows,
      ),
      counters: this.restoreSavedCounters(),
    };

    this.logListener = this.device.addLogListener((entry: DeviceLogEntry) => {
      const processedEntry = processEntry(entry, String(this.counter++));
      this.incrementCounterIfNeeded(processedEntry.entry);
      this.scheudleEntryForBatch(processedEntry);
    });
  }

  incrementCounterIfNeeded = (entry: DeviceLogEntry) => {
    let counterUpdated = false;
    const counters = this.state.counters.map(counter => {
      if (entry.message.match(counter.expression)) {
        counterUpdated = true;
        if (counter.notify) {
          new window.Notification(`${counter.label}`, {
            body: 'The watched log message appeared',
          });
        }
        return {
          ...counter,
          count: counter.count + 1,
        };
      } else {
        return counter;
      }
    });
    if (counterUpdated) {
      this.setState({counters});
    }
  };

  scheudleEntryForBatch = (item: {
    row: TableBodyRow,
    entry: DeviceLogEntry,
  }) => {
    // batch up logs to be processed every 250ms, if we have lots of log
    // messages coming in, then calling an setState 200+ times is actually
    // pretty expensive
    this.batch.push(item);

    if (!this.queued) {
      this.queued = true;

      this.batchTimer = setTimeout(() => {
        const thisBatch = this.batch;
        this.batch = [];
        this.queued = false;
        this.setState(state => addEntriesToState(thisBatch, state));
      }, 100);
    }
  };

  componentWillUnmount() {
    if (this.batchTimer) {
      clearTimeout(this.batchTimer);
    }

    if (this.logListener) {
      this.device.removeLogListener(this.logListener);
    }
  }

  clearLogs = () => {
    this.device.clearLogs().catch(e => {
      console.error('Failed to clear logs: ', e);
    });
    this.setState({
      entries: [],
      rows: [],
      highlightedRows: new Set(),
      key2entry: {},
      counters: this.state.counters.map(counter => ({
        ...counter,
        count: 0,
      })),
    });
  };

  createPaste = () => {
    let paste = '';
    const mapFn = row =>
      Object.keys(COLUMNS)
        .map(key => textContent(row.columns[key].value))
        .join('\t');

    if (this.state.highlightedRows.size > 0) {
      // create paste from selection
      paste = this.state.rows
        .filter(row => this.state.highlightedRows.has(row.key))
        .map(mapFn)
        .join('\n');
    } else {
      // create paste with all rows
      paste = this.state.rows.map(mapFn).join('\n');
    }
    createPaste(paste);
  };

  setTableRef = (ref: React.ElementRef<typeof ManagedTable>) => {
    this.tableRef = ref;
  };

  goToBottom = () => {
    if (this.tableRef != null) {
      this.tableRef.scrollToBottom();
    }
  };

  onRowHighlighted = (highlightedRows: Array<string>) => {
    this.setState({
      ...this.state,
      highlightedRows: new Set(highlightedRows),
    });
  };

  renderSidebar = () => {
    return (
      <LogWatcher
        counters={this.state.counters}
        onChange={counters =>
          this.setState({counters}, () =>
            window.localStorage.setItem(
              LOG_WATCHER_LOCAL_STORAGE_KEY,
              JSON.stringify(this.state.counters),
            ),
          )
        }
      />
    );
  };

  static ContextMenu = styled(ContextMenu)({
    flex: 1,
  });

  buildContextMenuItems = () => [
    {
      type: 'separator',
    },
    {
      label: 'Clear all',
      click: this.clearLogs,
    },
  ];

  render() {
    return (
      <LogTable.ContextMenu
        buildItems={this.buildContextMenuItems}
        component={FlexColumn}>
        <SearchableTable
          innerRef={this.setTableRef}
          floating={false}
          multiline={true}
          columnSizes={this.columnSizes}
          columnOrder={this.columnOrder}
          columns={this.columns}
          rows={this.state.rows}
          highlightedRows={this.state.highlightedRows}
          onRowHighlighted={this.onRowHighlighted}
          multiHighlight={true}
          defaultFilters={DEFAULT_FILTERS}
          zebra={false}
          actions={<Button onClick={this.clearLogs}>Clear Logs</Button>}
          // If the logs is opened through deeplink, then don't scroll as the row is highlighted
          stickyBottom={
            !(this.props.deepLinkPayload && this.state.highlightedRows.size > 0)
          }
        />
        <DetailSidebar>{this.renderSidebar()}</DetailSidebar>
      </LogTable.ContextMenu>
    );
  }
}
<commit_msg>Show timestamp in logs by default<commit_after>/**
 * Copyright 2018-present Facebook.
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 * @format
 */

import type {
  TableBodyRow,
  TableColumnOrder,
  TableColumnSizes,
  TableColumns,
} from 'flipper';
import type {Counter} from './LogWatcher.js';
import type {DeviceLogEntry} from '../../devices/BaseDevice.js';
import type {Props as PluginProps} from '../../plugin';

import {
  Text,
  ManagedTable,
  Button,
  colors,
  ContextMenu,
  FlexColumn,
  Glyph,
  DetailSidebar,
  FlipperDevicePlugin,
  SearchableTable,
  styled,
  Device,
  createPaste,
  textContent,
} from 'flipper';
import LogWatcher from './LogWatcher';

const LOG_WATCHER_LOCAL_STORAGE_KEY = 'LOG_WATCHER_LOCAL_STORAGE_KEY';

type Entries = Array<{
  row: TableBodyRow,
  entry: DeviceLogEntry,
}>;

type State = {|
  rows: Array<TableBodyRow>,
  entries: Entries,
  key2entry: {[key: string]: DeviceLogEntry},
  highlightedRows: Set<string>,
  counters: Array<Counter>,
|};

type Actions = {||};

type PersistedState = {||};

const Icon = styled(Glyph)({
  marginTop: 5,
});

function getLineCount(str: string): number {
  let count = 1;
  if (!(typeof str === 'string')) {
    return 0;
  }
  for (let i = 0; i < str.length; i++) {
    if (str[i] === '\n') {
      count++;
    }
  }
  return count;
}

function keepKeys(obj, keys) {
  const result = {};
  for (const key in obj) {
    if (keys.includes(key)) {
      result[key] = obj[key];
    }
  }
  return result;
}

const COLUMN_SIZE = {
  type: 40,
  time: 120,
  pid: 60,
  tid: 60,
  tag: 120,
  app: 200,
  message: 'flex',
};

const COLUMNS = {
  type: {
    value: '',
  },
  time: {
    value: 'Time',
  },
  pid: {
    value: 'PID',
  },
  tid: {
    value: 'TID',
  },
  tag: {
    value: 'Tag',
  },
  app: {
    value: 'App',
  },
  message: {
    value: 'Message',
  },
};

const INITIAL_COLUMN_ORDER = [
  {
    key: 'type',
    visible: true,
  },
  {
    key: 'time',
    visible: true,
  },
  {
    key: 'pid',
    visible: false,
  },
  {
    key: 'tid',
    visible: false,
  },
  {
    key: 'tag',
    visible: true,
  },
  {
    key: 'app',
    visible: true,
  },
  {
    key: 'message',
    visible: true,
  },
];

const LOG_TYPES: {
  [level: string]: {
    label: string,
    color: string,
    icon?: React.Node,
    style?: Object,
  },
} = {
  verbose: {
    label: 'Verbose',
    color: colors.purple,
  },
  debug: {
    label: 'Debug',
    color: colors.grey,
  },
  info: {
    label: 'Info',
    icon: <Icon name="info-circle" color={colors.cyan} />,
    color: colors.cyan,
  },
  warn: {
    label: 'Warn',
    style: {
      backgroundColor: colors.yellowTint,
      color: colors.yellow,
      fontWeight: 500,
    },
    icon: <Icon name="caution-triangle" color={colors.yellow} />,
    color: colors.yellow,
  },
  error: {
    label: 'Error',
    style: {
      backgroundColor: colors.redTint,
      color: colors.red,
      fontWeight: 500,
    },
    icon: <Icon name="caution-octagon" color={colors.red} />,
    color: colors.red,
  },
  fatal: {
    label: 'Fatal',
    style: {
      backgroundColor: colors.redTint,
      color: colors.red,
      fontWeight: 700,
    },
    icon: <Icon name="stop" color={colors.red} />,
    color: colors.red,
  },
};

const DEFAULT_FILTERS = [
  {
    type: 'enum',
    enum: Object.keys(LOG_TYPES).map(value => ({
      label: LOG_TYPES[value].label,
      value,
    })),
    key: 'type',
    value: [],
    persistent: true,
  },
];

const HiddenScrollText = styled(Text)({
  alignSelf: 'baseline',
  userSelect: 'none',
  lineHeight: '130%',
  marginTop: 5,
  paddingBottom: 3,
  '&::-webkit-scrollbar': {
    display: 'none',
  },
});

const LogCount = styled('div')(({backgroundColor}) => ({
  backgroundColor,
  borderRadius: '999em',
  fontSize: 11,
  marginTop: 4,
  minWidth: 16,
  height: 16,
  color: colors.white,
  textAlign: 'center',
  lineHeight: '16px',
  paddingLeft: 4,
  paddingRight: 4,
  textOverflow: 'ellipsis',
  overflow: 'hidden',
  whiteSpace: 'nowrap',
}));

function pad(chunk: mixed, len: number): string {
  let str = String(chunk);
  while (str.length < len) {
    str = `0${str}`;
  }
  return str;
}

export function addEntriesToState(
  items: Entries,
  state: $Shape<State> = {
    rows: [],
    entries: [],
    key2entry: {},
  },
): $Shape<State> {
  const rows = [...state.rows];
  const entries = [...state.entries];
  const key2entry = {...state.key2entry};

  for (let i = 0; i < items.length; i++) {
    const {entry, row} = items[i];
    entries.push({row, entry});
    key2entry[row.key] = entry;

    let previousEntry: ?DeviceLogEntry = null;

    if (i > 0) {
      previousEntry = items[i - 1].entry;
    } else if (state.rows.length > 0 && state.entries.length > 0) {
      previousEntry = state.entries[state.entries.length - 1].entry;
    }

    addRowIfNeeded(rows, row, entry, previousEntry);
  }

  return {
    entries,
    rows,
    key2entry,
  };
}

export function addRowIfNeeded(
  rows: Array<TableBodyRow>,
  row: TableBodyRow,
  entry: DeviceLogEntry,
  previousEntry: ?DeviceLogEntry,
) {
  const previousRow = rows.length > 0 ? rows[rows.length - 1] : null;
  if (
    previousRow &&
    previousEntry &&
    entry.message === previousEntry.message &&
    entry.tag === previousEntry.tag &&
    previousRow.type != null
  ) {
    // duplicate log, increase counter
    const count =
      previousRow.columns.type.value &&
      previousRow.columns.type.value.props &&
      typeof previousRow.columns.type.value.props.children === 'number'
        ? previousRow.columns.type.value.props.children + 1
        : 2;
    const type = LOG_TYPES[previousRow.type] || LOG_TYPES.debug;
    previousRow.columns.type.value = (
      <LogCount backgroundColor={type.color}>{count}</LogCount>
    );
  } else {
    rows.push(row);
  }
}

export function processEntry(
  entry: DeviceLogEntry,
  key: string,
): {
  row: TableBodyRow,
  entry: DeviceLogEntry,
} {
  const {icon, style} = LOG_TYPES[(entry.type: string)] || LOG_TYPES.debug;
  // build the item, it will either be batched or added straight away
  return {
    entry,
    row: {
      columns: {
        type: {
          value: icon,
          align: 'center',
        },
        time: {
          value: (
            <HiddenScrollText code={true}>
              {entry.date.toTimeString().split(' ')[0] +
                '.' +
                pad(entry.date.getMilliseconds(), 3)}
            </HiddenScrollText>
          ),
        },
        message: {
          value: (
            <HiddenScrollText code={true}>{entry.message}</HiddenScrollText>
          ),
        },
        tag: {
          value: <HiddenScrollText code={true}>{entry.tag}</HiddenScrollText>,
          isFilterable: true,
        },
        pid: {
          value: (
            <HiddenScrollText code={true}>{String(entry.pid)}</HiddenScrollText>
          ),
          isFilterable: true,
        },
        tid: {
          value: (
            <HiddenScrollText code={true}>{String(entry.tid)}</HiddenScrollText>
          ),
          isFilterable: true,
        },
        app: {
          value: <HiddenScrollText code={true}>{entry.app}</HiddenScrollText>,
          isFilterable: true,
        },
      },
      height: getLineCount(entry.message) * 15 + 10, // 15px per line height + 8px padding
      style,
      type: entry.type,
      filterValue: entry.message,
      key,
    },
  };
}

export default class LogTable extends FlipperDevicePlugin<
  State,
  Actions,
  PersistedState,
> {
  static keyboardActions = ['clear', 'goToBottom', 'createPaste'];

  initTimer: ?TimeoutID;
  batchTimer: ?TimeoutID;

  static supportsDevice(device: Device) {
    return device.os === 'iOS' || device.os === 'Android';
  }

  onKeyboardAction = (action: string) => {
    if (action === 'clear') {
      this.clearLogs();
    } else if (action === 'goToBottom') {
      this.goToBottom();
    } else if (action === 'createPaste') {
      this.createPaste();
    }
  };

  restoreSavedCounters = (): Array<Counter> => {
    const savedCounters =
      window.localStorage.getItem(LOG_WATCHER_LOCAL_STORAGE_KEY) || '[]';
    return JSON.parse(savedCounters).map((counter: Counter) => ({
      ...counter,
      expression: new RegExp(counter.label, 'gi'),
      count: 0,
    }));
  };

  calculateHighlightedRows = (
    deepLinkPayload: ?string,
    rows: Array<TableBodyRow>,
  ): Set<string> => {
    const highlightedRows = new Set();
    if (!deepLinkPayload) {
      return highlightedRows;
    }

    // Run through array from last to first, because we want to show the last
    // time it the log we are looking for appeared.
    for (let i = rows.length - 1; i >= 0; i--) {
      if (
        rows[i].filterValue &&
        rows[i].filterValue.includes(deepLinkPayload)
      ) {
        highlightedRows.add(rows[i].key);
        break;
      }
    }
    if (highlightedRows.size <= 0) {
      // Check if the individual lines in the deeplinkPayload is matched or not.
      const arr = deepLinkPayload.split('\n');
      for (let msg of arr) {
        for (let i = rows.length - 1; i >= 0; i--) {
          if (rows[i].filterValue && rows[i].filterValue.includes(msg)) {
            highlightedRows.add(rows[i].key);
            break;
          }
        }
      }
    }
    return highlightedRows;
  };

  tableRef: ?ManagedTable;
  columns: TableColumns;
  columnSizes: TableColumnSizes;
  columnOrder: TableColumnOrder;
  logListener: ?Symbol;

  batch: Entries = [];
  queued: boolean = false;
  counter: number = 0;

  constructor(props: PluginProps<PersistedState>) {
    super(props);
    const supportedColumns = this.device.supportedColumns();
    this.columns = keepKeys(COLUMNS, supportedColumns);
    this.columnSizes = keepKeys(COLUMN_SIZE, supportedColumns);
    this.columnOrder = INITIAL_COLUMN_ORDER.filter(obj =>
      supportedColumns.includes(obj.key),
    );

    const initialState = addEntriesToState(
      this.device
        .getLogs()
        .map(log => processEntry(log, String(this.counter++))),
    );
    this.state = {
      ...initialState,
      highlightedRows: this.calculateHighlightedRows(
        props.deepLinkPayload,
        initialState.rows,
      ),
      counters: this.restoreSavedCounters(),
    };

    this.logListener = this.device.addLogListener((entry: DeviceLogEntry) => {
      const processedEntry = processEntry(entry, String(this.counter++));
      this.incrementCounterIfNeeded(processedEntry.entry);
      this.scheudleEntryForBatch(processedEntry);
    });
  }

  incrementCounterIfNeeded = (entry: DeviceLogEntry) => {
    let counterUpdated = false;
    const counters = this.state.counters.map(counter => {
      if (entry.message.match(counter.expression)) {
        counterUpdated = true;
        if (counter.notify) {
          new window.Notification(`${counter.label}`, {
            body: 'The watched log message appeared',
          });
        }
        return {
          ...counter,
          count: counter.count + 1,
        };
      } else {
        return counter;
      }
    });
    if (counterUpdated) {
      this.setState({counters});
    }
  };

  scheudleEntryForBatch = (item: {
    row: TableBodyRow,
    entry: DeviceLogEntry,
  }) => {
    // batch up logs to be processed every 250ms, if we have lots of log
    // messages coming in, then calling an setState 200+ times is actually
    // pretty expensive
    this.batch.push(item);

    if (!this.queued) {
      this.queued = true;

      this.batchTimer = setTimeout(() => {
        const thisBatch = this.batch;
        this.batch = [];
        this.queued = false;
        this.setState(state => addEntriesToState(thisBatch, state));
      }, 100);
    }
  };

  componentWillUnmount() {
    if (this.batchTimer) {
      clearTimeout(this.batchTimer);
    }

    if (this.logListener) {
      this.device.removeLogListener(this.logListener);
    }
  }

  clearLogs = () => {
    this.device.clearLogs().catch(e => {
      console.error('Failed to clear logs: ', e);
    });
    this.setState({
      entries: [],
      rows: [],
      highlightedRows: new Set(),
      key2entry: {},
      counters: this.state.counters.map(counter => ({
        ...counter,
        count: 0,
      })),
    });
  };

  createPaste = () => {
    let paste = '';
    const mapFn = row =>
      Object.keys(COLUMNS)
        .map(key => textContent(row.columns[key].value))
        .join('\t');

    if (this.state.highlightedRows.size > 0) {
      // create paste from selection
      paste = this.state.rows
        .filter(row => this.state.highlightedRows.has(row.key))
        .map(mapFn)
        .join('\n');
    } else {
      // create paste with all rows
      paste = this.state.rows.map(mapFn).join('\n');
    }
    createPaste(paste);
  };

  setTableRef = (ref: React.ElementRef<typeof ManagedTable>) => {
    this.tableRef = ref;
  };

  goToBottom = () => {
    if (this.tableRef != null) {
      this.tableRef.scrollToBottom();
    }
  };

  onRowHighlighted = (highlightedRows: Array<string>) => {
    this.setState({
      ...this.state,
      highlightedRows: new Set(highlightedRows),
    });
  };

  renderSidebar = () => {
    return (
      <LogWatcher
        counters={this.state.counters}
        onChange={counters =>
          this.setState({counters}, () =>
            window.localStorage.setItem(
              LOG_WATCHER_LOCAL_STORAGE_KEY,
              JSON.stringify(this.state.counters),
            ),
          )
        }
      />
    );
  };

  static ContextMenu = styled(ContextMenu)({
    flex: 1,
  });

  buildContextMenuItems = () => [
    {
      type: 'separator',
    },
    {
      label: 'Clear all',
      click: this.clearLogs,
    },
  ];

  render() {
    return (
      <LogTable.ContextMenu
        buildItems={this.buildContextMenuItems}
        component={FlexColumn}>
        <SearchableTable
          innerRef={this.setTableRef}
          floating={false}
          multiline={true}
          columnSizes={this.columnSizes}
          columnOrder={this.columnOrder}
          columns={this.columns}
          rows={this.state.rows}
          highlightedRows={this.state.highlightedRows}
          onRowHighlighted={this.onRowHighlighted}
          multiHighlight={true}
          defaultFilters={DEFAULT_FILTERS}
          zebra={false}
          actions={<Button onClick={this.clearLogs}>Clear Logs</Button>}
          // If the logs is opened through deeplink, then don't scroll as the row is highlighted
          stickyBottom={
            !(this.props.deepLinkPayload && this.state.highlightedRows.size > 0)
          }
        />
        <DetailSidebar>{this.renderSidebar()}</DetailSidebar>
      </LogTable.ContextMenu>
    );
  }
}
<|endoftext|>


<commit_before>let objBrowser = chrome ? chrome : browser;

class EtherAddressLookup {

    constructor(objWeb3)
    {
        console.log("Init EAL");
        this.objWeb3 = objWeb3;
        this.setDefaultExtensionSettings();
        this.init();
    }

    setDefaultExtensionSettings()
    {
        this.blHighlight = false;
        this.strBlockchainExplorer = "https://etherscan.io/address";
        this.strRpcProvider = "https://localhost:8545";

        this.intSettingsCount = 0;
        this.intSettingsTotalCount = 2;
    }

    /**
     * @name init
     * @desc Gets extension settings and applies DOM manipulation
     */
    init()
    {
        let objBrowser = chrome ? chrome : browser;
        //Get the highlight option for the user
        objBrowser.runtime.sendMessage({func: "highlight_option"}, function(objResponse) {
            if(objResponse && objResponse.hasOwnProperty("resp")) {
                this.blHighlight = (objResponse.resp == 1);
            }
            ++this.intSettingsCount;
        }.bind(this));

        //Get the blockchain explorer for the user
        objBrowser.runtime.sendMessage({func: "blockchain_explorer"}, function(objResponse) {
            this.strBlockchainExplorer = objResponse.resp;
            ++this.intSettingsCount;
        }.bind(this));

        //Update the DOM once all settings have been received...
        setTimeout(function() {
            // Needs to happen after user settings have been collected
            // and in the context of init();
            this.setSearchAndReplaceSettings();
            this.setWarningSettings();
            this.manipulateDOM();
        }.bind(this), 10);
    }

    /**
     * @name Set Search And Replace Settings
     * @desc
     */
    setSearchAndReplaceSettings()
    {
        // Check user is on their favourite block explorer, on fail target blank.
        this.target = (this.isBlockchainExplorerSite() ? '_self' : '_blank');

        // Register RegEx Patterns
        this.regExPatterns = [
            // Ethereum Address Regex
            /(^|\s|:|-)((?:0x)[0-9a-fA-F]{40})(\s|$)/gi,

            // ENS Address Regex
            /(^|\s|:|-)([a-z0-9][a-z0-9-.]+[a-z0-9](?:\.eth))(\s|$)/gi,

            // ENS With ZWCs
            /(^|\s|:|-)([a-z0-9][a-z0-9-.]*(\u200B|\u200C|\u200D|\uFEFF|\u2028|\u2029||)[a-z0-9]*(?:\.eth))(\s|$)/gi
        ];

        // Register RegEx Matching Patterns
        this.matchPatterns = [
            // Ethereum Match Pattern
            /((?:0x)[0-9a-fA-F]{40})/gi,

            // ENS Match Pattern
            this.regExPatterns[1],

            // ENS With ZWCs
            this.regExPatterns[2]
        ];

        // Register Replace Patterns
        this.replacePatterns = [
            // Ethereum Address Replace
            '$1' +
            '

 $2' +
            '$3',

            // ENS Address Replace
            '$1$2$3',

            // ENS With ZWCs Replace
            '$1<slot title="WARNING! This ENS address has ZWCs. Someone may be trying to scam you." ' +
            'class="ext-etheraddresslookup-warning">$2</slot>$3'
        ];
    }

    /**
     * @name Set Warning Settings
     * @desc
     */
    setWarningSettings()
    {
        // The block explorers that can handle ENS addresses
        this.ENSCompatiableExplorers = [
            "https://etherscan.io/address",
            "https://etherchain.org/account"
        ];

        // Does the user's favorite explorer support an ENS address
        for(var i=0; i<this.ENSCompatiableExplorers.length; i++){
            if(this.strBlockchainExplorer == this.ENSCompatiableExplorers[i]){
                this.ENSCompatiable = true;
                break;
            }
            this.ENSCompatiable = false;
        }

        // On failure give the user a warning.
        if(!this.ENSCompatiable){
            this.replacePatterns[1] = '$1$2$3';
        }
    }

    manipulateDOM()
    {
        if(true || this.intSettingsCount === this.intSettingsTotalCount) {
            if(this.blBlacklistDomains) {
                this.blacklistedDomainCheck();
            }
            this.convertAddressToLink();

            this.setAddressOnHoverBehaviour();
        }
    }

    /**
     * @name Convert Address To Link
     * @desc Finds Ethereum addresses and converts to a link to a block explorer
     */
    convertAddressToLink()
    {
        var arrWhitelistedTags = ["code", "span", "p", "td", "li", "em", "i", "b", "strong", "small"];

        //Get the whitelisted nodes
        for(var i=0; i<arrWhitelistedTags.length; i++) {
            var objNodes = document.getElementsByTagName(arrWhitelistedTags[i]);
            //Loop through the whitelisted content
            for(var x=0; x<objNodes.length; x++) {

                if(this.hasIgnoreAttributes(objNodes[x])){
                    continue;
                }

                this.convertAddresses(objNodes[x]);
            }
        }

        this.tidyUpSlots();
        this.addBlockies();

        if(this.blHighlight) {
            this.addHighlightStyle();
        }
    }

    /**
     * @name Convert Addresses
     * @desc Takes a Node and checks if any of its children are textNodes. On success replace textNode with slot node
     * @desc slot node contains regex replaced content; see generateReplacementContent()
     * @param {Node} objNode
     */
    convertAddresses(objNode)
    {
        // Some nodes have non-textNode children
        // we need to ensure regex is applied only to text otherwise we will mess the html up
        for(var i=0; i < objNode.childNodes.length; i++){
            // Only check textNodes to prevent applying RegEx against element attributes
            if(objNode.childNodes[i].nodeType == 3){ // nodeType 3 = a text node

                var child = objNode.childNodes[i];
                var childContent = child.textContent;

                // Only start replacing stuff if the we get a RegEx match.
                if(this.isPatternMatched(childContent)) {
                    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot
                    var replacement = document.createElement('slot');
                    replacement.setAttribute('class', 'ext-etheraddresslookup-temporary');
                    replacement.innerHTML = this.generateReplacementContent(childContent);
                    objNode.replaceChild(replacement, child);
                }
            }
        }
    }

    /**
     * @name Generate Replacement Content
     * @desc Takes string and replaces any regex pattern matches with the associated replace patterns
     * @param {string} content
     * @returns {string}
     */
    generateReplacementContent(content)
    {
        for(var i=0; i < this.regExPatterns.length; i++){
            content = content.replace(this.regExPatterns[i], this.replacePatterns[i]);
        }
        return content;
    }

    /**
     * @name Has Ignore Attributes
     * @desc Checks if a node contains any attribute that we want to avoid manipulating
     * @param {Element} node
     * @returns {boolean}
     */
    hasIgnoreAttributes(node)
    {
        var ignoreAttributes = {
            "class": ["ng-binding"]
        };

        // Loop through all attributes we want to test for ignoring
        for(var attributeName in ignoreAttributes){
            // Filter out the object's default properties
            if (ignoreAttributes.hasOwnProperty(attributeName)) {

                // Check this node has the attribute we are currently checking for
                if(node.hasAttribute(attributeName)){

                    // This node's value for the attribute we are checking
                    var nodeAttributeValue = node.getAttribute(attributeName);
                    // The values we want to ignore for this attribute
                    var badAttributeValueList = ignoreAttributes[attributeName];

                    // Loop through the attribute values we want to ignore
                    for(var i=0; i < badAttributeValueList.length; i++){
                        // If we find an indexOf, this value is present in the attribute
                        if(nodeAttributeValue.indexOf(badAttributeValueList[i]) !== -1){
                            return true;
                        }
                    }

                }

            }
        }

        return false;
    }

    /**
     * @name Is Pattern Matched
     * @desc Checks content matches any of the object's matchPatterns
     * @param {string} content
     * @returns {boolean}
     */
    isPatternMatched(content)
    {
        for(var i=0; i < this.matchPatterns.length; i++){
            if(this.matchPatterns[i].exec(content) !== null){
                return true;
            }
        }
        return false;
    }

    /**
     * @name Is Blockchain Explorer Site
     * @desc Check if the current website is the user's selected block explorer
     * @returns {boolean}
     */
    isBlockchainExplorerSite()
    {
        var objBlockchainExplorer = document.createElement("a");
        objBlockchainExplorer.href = this.strBlockchainExplorer;
        return (objBlockchainExplorer.hostname === window.location.hostname);
    }

    /**
     * @name Tidy Slots
     * @desc Searches document for slots adds the slot's child nodes to its parent then removes the slot
     */
    tidyUpSlots()
    {
        var slots = document.querySelectorAll("slot.ext-etheraddresslookup-temporary");
        for(var i=0; i < slots.length; i++){
            while(slots[i].childNodes.length > 0){
                slots[i].parentNode.insertBefore(slots[i].firstChild, slots[i]);
            }
            slots[i].parentNode.removeChild(slots[i]);
        }
    }

    addBlockies()
    {
        var blockieDivs = document.querySelectorAll("div.ext-etheraddresslookup-blockie");
        for(var i = 0; i < blockieDivs.length; i++){

            blockieDivs[i].style.backgroundImage = 'url(' + blockies.create({
                // toLowerCase is used because standard blockies are based on none-checksum Ethereum addresses
                seed:blockieDivs[i].getAttribute('data-ether-address').toLowerCase(),
                size: 8,
                scale: 16
            }).toDataURL() +')';
        }
    }

    //Removes the highlight style from Ethereum addresses
    removeHighlightStyle()
    {
        var objEtherAddresses = document.getElementsByClassName("ext-etheraddresslookup-link");
        for (var i = 0; i < objEtherAddresses.length; i++) {
            objEtherAddresses[i].classList.add("ext-etheraddresslookup-link-no_highlight");
            objEtherAddresses[i].classList.add("ext-etheraddresslookup-link-highlight");
        }
        return false;
    }

    //Adds the highlight style
    addHighlightStyle()
    {
        var objEtherAddresses = document.getElementsByClassName("ext-etheraddresslookup-link");
        for (var i = 0; i < objEtherAddresses.length; i++) {
            objEtherAddresses[i].classList.add("ext-etheraddresslookup-link-highlight");
            objEtherAddresses[i].classList.remove("ext-etheraddresslookup-link-no_highlight");
        }
        return false;
    }

    //Sets the on hover behaviour for the address
    // - get the address stats with rpc
    setAddressOnHoverBehaviour()
    {
        var objNodes = document.getElementsByClassName("ext-etheraddresslookup-0xaddress");
        for (var i = 0; i < objNodes.length; i++) {
            objNodes[i].addEventListener('mouseover', this.event_0xAddressHover, false);
        }
    }

    //The event handler for 0x address mouseover.
    //It will do the logic to call the web3 rpc to get the address balance.
    event_0xAddressHover()
    {
        if(this.children.length > 1 && this.children[1].className == "ext-etheraddresslookup-address_stats_hover") {
            return false;
        }

        var objHoverNode = document.createElement("div");
        objHoverNode.className = "ext-etheraddresslookup-address_stats_hover";
        var objHoverNodeContent = document.createElement("div");
        objHoverNodeContent.className = "ext-etheraddresslookup-address_stats_hover_content";
        objHoverNodeContent.innerHTML = "
Fetching Data...
";
        objHoverNodeContent.innerHTML += "";

        objHoverNode.appendChild(objHoverNodeContent);
        this.appendChild(objHoverNode);

        //Get the RPC provider for the user
        objBrowser.runtime.sendMessage({func: "rpc_provider"}, function(objResponse) {
            var web3 = new Web3(new Web3.providers.HttpProvider(objResponse.resp));
            var str0xAddress = this.getAttribute("data-address");
            var strAccountBalance = parseFloat(web3.fromWei(web3.eth.getBalance(str0xAddress).toString(10), "ether")).toLocaleString();
            var intTransactionCount = parseInt(web3.eth.getTransactionCount(str0xAddress)).toLocaleString();
            var blIsContractAddress = web3.eth.getCode(str0xAddress) == "0x" ? false: true;

            var objHoverNodeContent = this.children[1].children[0];
            objHoverNodeContent.innerHTML = "
ETH: "+ strAccountBalance +"
";
            objHoverNodeContent.innerHTML += "
Transactions out: "+ intTransactionCount +"
";
            if(blIsContractAddress) {
                objHoverNodeContent.innerHTML += "
This is a contract address
";
            }
            objHoverNodeContent.innerHTML += "";

            return false;
        }.bind(this));
    }
}

window.addEventListener("load", function() {
    let objEtherAddressLookup = new EtherAddressLookup(Web3);
});

//Send message from the extension to here.
objBrowser.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
        let objEtherAddressLookup = new EtherAddressLookup();
        if(typeof request.func !== "undefined") {
            if(typeof objEtherAddressLookup[request.func] == "function") {
                objEtherAddressLookup[request.func]();
                sendResponse({status: "ok"});
                return true;
            }
        }
        sendResponse({status: "fail"});
    }
);
<commit_msg>Added more significant digits to the ETH balance<commit_after>let objBrowser = chrome ? chrome : browser;

class EtherAddressLookup {

    constructor(objWeb3)
    {
        console.log("Init EAL");
        this.objWeb3 = objWeb3;
        this.setDefaultExtensionSettings();
        this.init();
    }

    setDefaultExtensionSettings()
    {
        this.blHighlight = false;
        this.strBlockchainExplorer = "https://etherscan.io/address";
        this.strRpcProvider = "https://localhost:8545";

        this.intSettingsCount = 0;
        this.intSettingsTotalCount = 2;
    }

    /**
     * @name init
     * @desc Gets extension settings and applies DOM manipulation
     */
    init()
    {
        let objBrowser = chrome ? chrome : browser;
        //Get the highlight option for the user
        objBrowser.runtime.sendMessage({func: "highlight_option"}, function(objResponse) {
            if(objResponse && objResponse.hasOwnProperty("resp")) {
                this.blHighlight = (objResponse.resp == 1);
            }
            ++this.intSettingsCount;
        }.bind(this));

        //Get the blockchain explorer for the user
        objBrowser.runtime.sendMessage({func: "blockchain_explorer"}, function(objResponse) {
            this.strBlockchainExplorer = objResponse.resp;
            ++this.intSettingsCount;
        }.bind(this));

        //Update the DOM once all settings have been received...
        setTimeout(function() {
            // Needs to happen after user settings have been collected
            // and in the context of init();
            this.setSearchAndReplaceSettings();
            this.setWarningSettings();
            this.manipulateDOM();
        }.bind(this), 10);
    }

    /**
     * @name Set Search And Replace Settings
     * @desc
     */
    setSearchAndReplaceSettings()
    {
        // Check user is on their favourite block explorer, on fail target blank.
        this.target = (this.isBlockchainExplorerSite() ? '_self' : '_blank');

        // Register RegEx Patterns
        this.regExPatterns = [
            // Ethereum Address Regex
            /(^|\s|:|-)((?:0x)[0-9a-fA-F]{40})(\s|$)/gi,

            // ENS Address Regex
            /(^|\s|:|-)([a-z0-9][a-z0-9-.]+[a-z0-9](?:\.eth))(\s|$)/gi,

            // ENS With ZWCs
            /(^|\s|:|-)([a-z0-9][a-z0-9-.]*(\u200B|\u200C|\u200D|\uFEFF|\u2028|\u2029||)[a-z0-9]*(?:\.eth))(\s|$)/gi
        ];

        // Register RegEx Matching Patterns
        this.matchPatterns = [
            // Ethereum Match Pattern
            /((?:0x)[0-9a-fA-F]{40})/gi,

            // ENS Match Pattern
            this.regExPatterns[1],

            // ENS With ZWCs
            this.regExPatterns[2]
        ];

        // Register Replace Patterns
        this.replacePatterns = [
            // Ethereum Address Replace
            '$1' +
            '

 $2' +
            '$3',

            // ENS Address Replace
            '$1$2$3',

            // ENS With ZWCs Replace
            '$1<slot title="WARNING! This ENS address has ZWCs. Someone may be trying to scam you." ' +
            'class="ext-etheraddresslookup-warning">$2</slot>$3'
        ];
    }

    /**
     * @name Set Warning Settings
     * @desc
     */
    setWarningSettings()
    {
        // The block explorers that can handle ENS addresses
        this.ENSCompatiableExplorers = [
            "https://etherscan.io/address",
            "https://etherchain.org/account"
        ];

        // Does the user's favorite explorer support an ENS address
        for(var i=0; i<this.ENSCompatiableExplorers.length; i++){
            if(this.strBlockchainExplorer == this.ENSCompatiableExplorers[i]){
                this.ENSCompatiable = true;
                break;
            }
            this.ENSCompatiable = false;
        }

        // On failure give the user a warning.
        if(!this.ENSCompatiable){
            this.replacePatterns[1] = '$1$2$3';
        }
    }

    manipulateDOM()
    {
        if(true || this.intSettingsCount === this.intSettingsTotalCount) {
            if(this.blBlacklistDomains) {
                this.blacklistedDomainCheck();
            }
            this.convertAddressToLink();

            this.setAddressOnHoverBehaviour();
        }
    }

    /**
     * @name Convert Address To Link
     * @desc Finds Ethereum addresses and converts to a link to a block explorer
     */
    convertAddressToLink()
    {
        var arrWhitelistedTags = ["code", "span", "p", "td", "li", "em", "i", "b", "strong", "small"];

        //Get the whitelisted nodes
        for(var i=0; i<arrWhitelistedTags.length; i++) {
            var objNodes = document.getElementsByTagName(arrWhitelistedTags[i]);
            //Loop through the whitelisted content
            for(var x=0; x<objNodes.length; x++) {

                if(this.hasIgnoreAttributes(objNodes[x])){
                    continue;
                }

                this.convertAddresses(objNodes[x]);
            }
        }

        this.tidyUpSlots();
        this.addBlockies();

        if(this.blHighlight) {
            this.addHighlightStyle();
        }
    }

    /**
     * @name Convert Addresses
     * @desc Takes a Node and checks if any of its children are textNodes. On success replace textNode with slot node
     * @desc slot node contains regex replaced content; see generateReplacementContent()
     * @param {Node} objNode
     */
    convertAddresses(objNode)
    {
        // Some nodes have non-textNode children
        // we need to ensure regex is applied only to text otherwise we will mess the html up
        for(var i=0; i < objNode.childNodes.length; i++){
            // Only check textNodes to prevent applying RegEx against element attributes
            if(objNode.childNodes[i].nodeType == 3){ // nodeType 3 = a text node

                var child = objNode.childNodes[i];
                var childContent = child.textContent;

                // Only start replacing stuff if the we get a RegEx match.
                if(this.isPatternMatched(childContent)) {
                    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot
                    var replacement = document.createElement('slot');
                    replacement.setAttribute('class', 'ext-etheraddresslookup-temporary');
                    replacement.innerHTML = this.generateReplacementContent(childContent);
                    objNode.replaceChild(replacement, child);
                }
            }
        }
    }

    /**
     * @name Generate Replacement Content
     * @desc Takes string and replaces any regex pattern matches with the associated replace patterns
     * @param {string} content
     * @returns {string}
     */
    generateReplacementContent(content)
    {
        for(var i=0; i < this.regExPatterns.length; i++){
            content = content.replace(this.regExPatterns[i], this.replacePatterns[i]);
        }
        return content;
    }

    /**
     * @name Has Ignore Attributes
     * @desc Checks if a node contains any attribute that we want to avoid manipulating
     * @param {Element} node
     * @returns {boolean}
     */
    hasIgnoreAttributes(node)
    {
        var ignoreAttributes = {
            "class": ["ng-binding"]
        };

        // Loop through all attributes we want to test for ignoring
        for(var attributeName in ignoreAttributes){
            // Filter out the object's default properties
            if (ignoreAttributes.hasOwnProperty(attributeName)) {

                // Check this node has the attribute we are currently checking for
                if(node.hasAttribute(attributeName)){

                    // This node's value for the attribute we are checking
                    var nodeAttributeValue = node.getAttribute(attributeName);
                    // The values we want to ignore for this attribute
                    var badAttributeValueList = ignoreAttributes[attributeName];

                    // Loop through the attribute values we want to ignore
                    for(var i=0; i < badAttributeValueList.length; i++){
                        // If we find an indexOf, this value is present in the attribute
                        if(nodeAttributeValue.indexOf(badAttributeValueList[i]) !== -1){
                            return true;
                        }
                    }

                }

            }
        }

        return false;
    }

    /**
     * @name Is Pattern Matched
     * @desc Checks content matches any of the object's matchPatterns
     * @param {string} content
     * @returns {boolean}
     */
    isPatternMatched(content)
    {
        for(var i=0; i < this.matchPatterns.length; i++){
            if(this.matchPatterns[i].exec(content) !== null){
                return true;
            }
        }
        return false;
    }

    /**
     * @name Is Blockchain Explorer Site
     * @desc Check if the current website is the user's selected block explorer
     * @returns {boolean}
     */
    isBlockchainExplorerSite()
    {
        var objBlockchainExplorer = document.createElement("a");
        objBlockchainExplorer.href = this.strBlockchainExplorer;
        return (objBlockchainExplorer.hostname === window.location.hostname);
    }

    /**
     * @name Tidy Slots
     * @desc Searches document for slots adds the slot's child nodes to its parent then removes the slot
     */
    tidyUpSlots()
    {
        var slots = document.querySelectorAll("slot.ext-etheraddresslookup-temporary");
        for(var i=0; i < slots.length; i++){
            while(slots[i].childNodes.length > 0){
                slots[i].parentNode.insertBefore(slots[i].firstChild, slots[i]);
            }
            slots[i].parentNode.removeChild(slots[i]);
        }
    }

    addBlockies()
    {
        var blockieDivs = document.querySelectorAll("div.ext-etheraddresslookup-blockie");
        for(var i = 0; i < blockieDivs.length; i++){

            blockieDivs[i].style.backgroundImage = 'url(' + blockies.create({
                // toLowerCase is used because standard blockies are based on none-checksum Ethereum addresses
                seed:blockieDivs[i].getAttribute('data-ether-address').toLowerCase(),
                size: 8,
                scale: 16
            }).toDataURL() +')';
        }
    }

    //Removes the highlight style from Ethereum addresses
    removeHighlightStyle()
    {
        var objEtherAddresses = document.getElementsByClassName("ext-etheraddresslookup-link");
        for (var i = 0; i < objEtherAddresses.length; i++) {
            objEtherAddresses[i].classList.add("ext-etheraddresslookup-link-no_highlight");
            objEtherAddresses[i].classList.add("ext-etheraddresslookup-link-highlight");
        }
        return false;
    }

    //Adds the highlight style
    addHighlightStyle()
    {
        var objEtherAddresses = document.getElementsByClassName("ext-etheraddresslookup-link");
        for (var i = 0; i < objEtherAddresses.length; i++) {
            objEtherAddresses[i].classList.add("ext-etheraddresslookup-link-highlight");
            objEtherAddresses[i].classList.remove("ext-etheraddresslookup-link-no_highlight");
        }
        return false;
    }

    //Sets the on hover behaviour for the address
    // - get the address stats with rpc
    setAddressOnHoverBehaviour()
    {
        var objNodes = document.getElementsByClassName("ext-etheraddresslookup-0xaddress");
        for (var i = 0; i < objNodes.length; i++) {
            objNodes[i].addEventListener('mouseover', this.event_0xAddressHover, false);
        }
    }

    //The event handler for 0x address mouseover.
    //It will do the logic to call the web3 rpc to get the address balance.
    event_0xAddressHover()
    {
        if(this.children.length > 1 && this.children[1].className == "ext-etheraddresslookup-address_stats_hover") {
            return false;
        }

        var objHoverNode = document.createElement("div");
        objHoverNode.className = "ext-etheraddresslookup-address_stats_hover";
        var objHoverNodeContent = document.createElement("div");
        objHoverNodeContent.className = "ext-etheraddresslookup-address_stats_hover_content";
        objHoverNodeContent.innerHTML = "
Fetching Data...
";
        objHoverNodeContent.innerHTML += "";

        objHoverNode.appendChild(objHoverNodeContent);
        this.appendChild(objHoverNode);

        //Get the RPC provider for the user
        objBrowser.runtime.sendMessage({func: "rpc_provider"}, function(objResponse) {
            var web3 = new Web3(new Web3.providers.HttpProvider(objResponse.resp));
            var str0xAddress = this.getAttribute("data-address");
            var strAccountBalance = parseFloat(web3.fromWei(web3.eth.getBalance(str0xAddress).toString(10), "ether")).toLocaleString('en-US', {maximumSignificantDigits: 9});
            var intTransactionCount = parseInt(web3.eth.getTransactionCount(str0xAddress)).toLocaleString();
            var blIsContractAddress = web3.eth.getCode(str0xAddress) == "0x" ? false: true;

            var objHoverNodeContent = this.children[1].children[0];
            objHoverNodeContent.innerHTML = "
ETH: "+ strAccountBalance +"
";
            objHoverNodeContent.innerHTML += "
Transactions out: "+ intTransactionCount +"
";
            if(blIsContractAddress) {
                objHoverNodeContent.innerHTML += "
This is a contract address
";
            }
            objHoverNodeContent.innerHTML += "";

            return false;
        }.bind(this));
    }
}

window.addEventListener("load", function() {
    let objEtherAddressLookup = new EtherAddressLookup(Web3);
});

//Send message from the extension to here.
objBrowser.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
        let objEtherAddressLookup = new EtherAddressLookup();
        if(typeof request.func !== "undefined") {
            if(typeof objEtherAddressLookup[request.func] == "function") {
                objEtherAddressLookup[request.func]();
                sendResponse({status: "ok"});
                return true;
            }
        }
        sendResponse({status: "fail"});
    }
);
<|endoftext|>


<commit_before>/**
 * Created by long on 14-4-25.
 */
var flax = flax || {};

var ButtonState = {
    UP:"up",
    OVER:"over",
    DOWN:"down",
    SELECTED:"selected",
    SELECTED_OVER:"selected_over",
    SELECTED_DOWN:"selected_down",
    DISABLED:"disabled"
};

MOUSE_DOWN_SCALE = 0.95;

flax._radioButtons = {};

flax._buttonDefine = {
    clickSound:null,//The sound will play when click
    _radioGroup:"",//All the button with the same _radioGroup will only have only one button selected!
    _state:null,
    _initScale:null,
    __isButton:true,

    onEnter:function(){
        this._super();
        this._initScale = {x: this.scaleX, y : this.scaleY};
        flax.inputManager.addListener(this, this._onPress, InputType.press);
        flax.inputManager.addListener(this, this._onClick, InputType.click);
        flax.inputManager.addListener(this, this._onMove, InputType.move);
    },
    onExit:function(){
        this._removeRadioGroup();
        this._super();
    },
    setState:function(state)
    {
        if(this._state == state) return;
        this._state = state;
        if(!this.gotoAndStop(this._state))
        {
            var optionState = this.isSelected() ? ButtonState.SELECTED : ButtonState.UP;
            if(!this.gotoAndStop(optionState)){
                //if there is only one frame, we auto implement the button down effect
                if(this.totalFrames == 1 && this._initScale){
                    if(this._state.indexOf("down") > -1) {
                        this.scaleX = this._initScale.x*MOUSE_DOWN_SCALE;
                        this.scaleY = this._initScale.y*MOUSE_DOWN_SCALE;
                    } else {
                        this.scaleX = this._initScale.x;
                        this.scaleY = this._initScale.y;
                    }
                }
                this.gotoAndStop(0);
            }
        }
        //auto play the children's animation on new state
        var i = this.childrenCount;
        while(i--){
            var child = this.children[i];
            if(child.play) {
                child.autoPlayChildren = true;
                child.play();
            }
        }
    },
    getState:function()
    {
        return this._state;
    },
    setRadioGroup:function(radioGroup)
    {
        if(!radioGroup || radioGroup == this._radioGroup) return;
        if(this._radioGroup) {
            cc.log("Button's radioGroup could be set only once!");
            return;
        }
        this._radioGroup = radioGroup;
        if(flax._radioButtons[this._radioGroup] == null) flax._radioButtons[this._radioGroup] = [];
        flax._radioButtons[this._radioGroup].push(this);
    },
    getRadioGroup:function()
    {
        return this._radioGroup;
    },
    isSelected:function()
    {
        return this._state == ButtonState.SELECTED || this._state == ButtonState.SELECTED_OVER || this._state == ButtonState.SELECTED_DOWN;
    },
    isSelectable:function()
    {
        return this.hasLabel(ButtonState.SELECTED);
    },
    setMouseEnabled:function(enable)
    {
        if(this.isMouseEnabled() == enable) return false;
        this.setState(enable ? ButtonState.UP : ButtonState.DISABLED);
        return true;
    },
    isMouseEnabled:function()
    {
        return this._state != ButtonState.DISABLED;
    },
    _removeRadioGroup:function()
    {
        if(this._radioGroup){
            var arr = flax._radioButtons[this._radioGroup];
            if(arr == null) return;
            var i = arr.indexOf(this);
            if(i > -1) arr.splice(i, 1);
        }
    },
    _onPress:function(touch, event)
    {
        var sound = this.clickSound || flax.buttonSound;
        if(sound) flax.playSound(sound);
        this._toSetState(ButtonState.DOWN);
    },
    _onClick:function(touch, event)
    {
        if(this.isSelectable())
        {
            if (!this.isSelected() || this._radioGroup){
                if(!this.isSelected() && this._radioGroup){
                    var arr = flax._radioButtons[this._radioGroup];
                    if(arr && arr.length > 1){
                        for(var i = 0; i < arr.length; i++){
                            if(arr[i] != this){
                                arr[i].setState(ButtonState.UP);
                            }
                        }
                    }
                }
                this.setState(ButtonState.SELECTED);
            }else {
                this.setState(ButtonState.UP);
            }
        }else{
            this.setState(ButtonState.UP);
        }
    },
    _onMove:function(touch, event)
    {
        if(flax.ifTouched(this, touch.getLocation())){
            this._toSetState(ButtonState.DOWN);
        }else{
            this._toSetState(ButtonState.UP);
        }
    },
    _toSetState:function(state)
    {
        if(this.isSelectable() && this.isSelected())
        {
            if(state == ButtonState.UP) state = ButtonState.SELECTED;
            else state = "selected_"+state;
        }
        this.setState(state);
    }
}

flax.SimpleButton = flax.Animator.extend(flax._buttonDefine);
flax.SimpleButton.create = function(assetsFile, assetID)
{
    var btn = new flax.SimpleButton(assetsFile, assetID);
    btn.clsName = "flax.SimpleButton";
    btn.setState(ButtonState.UP);
    return btn;
};

window._p = flax.SimpleButton.prototype;
/** @expose */
_p.state;
cc.defineGetterSetter(_p, "state", _p.getState, _p.setState);
/** @expose */
_p.radioGroup;
cc.defineGetterSetter(_p, "radioGroup", _p.getRadioGroup, _p.setRadioGroup);
delete window._p;

flax.Button = flax.MovieClip.extend(flax._buttonDefine);
flax.Button.create = function(assetsFile, assetID)
{
    var btn = new flax.Button(assetsFile, assetID);
    btn.clsName = "flax.Button";
    btn.setState(ButtonState.UP);
    return btn;
};
flax.Button.reset = function()
{
    flax._radioButtons = {};
}

window._p = flax.Button.prototype;
/** @expose */
_p.state;
cc.defineGetterSetter(_p, "state", _p.getState, _p.setState);
/** @expose */
_p.radioGroup;
cc.defineGetterSetter(_p, "radioGroup", _p.getRadioGroup, _p.setRadioGroup);
delete window._p;<commit_msg>Add playChildrenOnState for button<commit_after>/**
 * Created by long on 14-4-25.
 */
var flax = flax || {};

var ButtonState = {
    UP:"up",
    OVER:"over",
    DOWN:"down",
    SELECTED:"selected",
    SELECTED_OVER:"selected_over",
    SELECTED_DOWN:"selected_down",
    DISABLED:"disabled"
};

MOUSE_DOWN_SCALE = 0.95;

flax._radioButtons = {};

flax._buttonDefine = {
    clickSound:null,//The sound will play when click
    playChildrenOnState:false,//If auto paly children's animation when change state
    _radioGroup:"",//All the button with the same _radioGroup will only have only one button selected!
    _state:null,
    _initScale:null,
    __isButton:true,

    onEnter:function(){
        this._super();
        this._initScale = {x: this.scaleX, y : this.scaleY};
        flax.inputManager.addListener(this, this._onPress, InputType.press);
        flax.inputManager.addListener(this, this._onClick, InputType.click);
        flax.inputManager.addListener(this, this._onMove, InputType.move);
    },
    onExit:function(){
        this._removeRadioGroup();
        this._super();
    },
    setState:function(state)
    {
        if(this._state == state) return;
        this._state = state;
        if(!this.gotoAndStop(this._state))
        {
            var optionState = this.isSelected() ? ButtonState.SELECTED : ButtonState.UP;
            if(!this.gotoAndStop(optionState)){
                //if there is only one frame, we auto implement the button down effect
                if(this.totalFrames == 1 && this._initScale){
                    if(this._state.indexOf("down") > -1) {
                        this.scaleX = this._initScale.x*MOUSE_DOWN_SCALE;
                        this.scaleY = this._initScale.y*MOUSE_DOWN_SCALE;
                    } else {
                        this.scaleX = this._initScale.x;
                        this.scaleY = this._initScale.y;
                    }
                }
                this.gotoAndStop(0);
            }
        }
        //auto play the children's animation on new state
        if(this.playChildrenOnState){
            var i = this.childrenCount;
            while(i--){
                var child = this.children[i];
                if(child.play) {
                    child.autoPlayChildren = true;
                    child.play();
                }
            }
        }
    },
    getState:function()
    {
        return this._state;
    },
    setRadioGroup:function(radioGroup)
    {
        if(!radioGroup || radioGroup == this._radioGroup) return;
        if(this._radioGroup) {
            cc.log("Button's radioGroup could be set only once!");
            return;
        }
        this._radioGroup = radioGroup;
        if(flax._radioButtons[this._radioGroup] == null) flax._radioButtons[this._radioGroup] = [];
        flax._radioButtons[this._radioGroup].push(this);
    },
    getRadioGroup:function()
    {
        return this._radioGroup;
    },
    isSelected:function()
    {
        return this._state == ButtonState.SELECTED || this._state == ButtonState.SELECTED_OVER || this._state == ButtonState.SELECTED_DOWN;
    },
    isSelectable:function()
    {
        return this.hasLabel(ButtonState.SELECTED);
    },
    setMouseEnabled:function(enable)
    {
        if(this.isMouseEnabled() == enable) return false;
        this.setState(enable ? ButtonState.UP : ButtonState.DISABLED);
        return true;
    },
    isMouseEnabled:function()
    {
        return this._state != ButtonState.DISABLED;
    },
    _removeRadioGroup:function()
    {
        if(this._radioGroup){
            var arr = flax._radioButtons[this._radioGroup];
            if(arr == null) return;
            var i = arr.indexOf(this);
            if(i > -1) arr.splice(i, 1);
        }
    },
    _onPress:function(touch, event)
    {
        var sound = this.clickSound || flax.buttonSound;
        if(sound) flax.playSound(sound);
        this._toSetState(ButtonState.DOWN);
    },
    _onClick:function(touch, event)
    {
        if(this.isSelectable())
        {
            if (!this.isSelected() || this._radioGroup){
                if(!this.isSelected() && this._radioGroup){
                    var arr = flax._radioButtons[this._radioGroup];
                    if(arr && arr.length > 1){
                        for(var i = 0; i < arr.length; i++){
                            if(arr[i] != this){
                                arr[i].setState(ButtonState.UP);
                            }
                        }
                    }
                }
                this.setState(ButtonState.SELECTED);
            }else {
                this.setState(ButtonState.UP);
            }
        }else{
            this.setState(ButtonState.UP);
        }
    },
    _onMove:function(touch, event)
    {
        if(flax.ifTouched(this, touch.getLocation())){
            this._toSetState(ButtonState.DOWN);
        }else{
            this._toSetState(ButtonState.UP);
        }
    },
    _toSetState:function(state)
    {
        if(this.isSelectable() && this.isSelected())
        {
            if(state == ButtonState.UP) state = ButtonState.SELECTED;
            else state = "selected_"+state;
        }
        this.setState(state);
    }
}

flax.SimpleButton = flax.Animator.extend(flax._buttonDefine);
flax.SimpleButton.create = function(assetsFile, assetID)
{
    var btn = new flax.SimpleButton(assetsFile, assetID);
    btn.clsName = "flax.SimpleButton";
    btn.setState(ButtonState.UP);
    return btn;
};

window._p = flax.SimpleButton.prototype;
/** @expose */
_p.state;
cc.defineGetterSetter(_p, "state", _p.getState, _p.setState);
/** @expose */
_p.radioGroup;
cc.defineGetterSetter(_p, "radioGroup", _p.getRadioGroup, _p.setRadioGroup);
delete window._p;

flax.Button = flax.MovieClip.extend(flax._buttonDefine);
flax.Button.create = function(assetsFile, assetID)
{
    var btn = new flax.Button(assetsFile, assetID);
    btn.clsName = "flax.Button";
    btn.setState(ButtonState.UP);
    return btn;
};
flax.Button.reset = function()
{
    flax._radioButtons = {};
}

window._p = flax.Button.prototype;
/** @expose */
_p.state;
cc.defineGetterSetter(_p, "state", _p.getState, _p.setState);
/** @expose */
_p.radioGroup;
cc.defineGetterSetter(_p, "radioGroup", _p.getRadioGroup, _p.setRadioGroup);
delete window._p;<|endoftext|>


<commit_before>var Memory = function() {
    this.addresses = {
        VRAM_START : 0x8000,
        VRAM_END   : 0x9FFF,

        EXTRAM_START : 0xA000,
        EXTRAM_END   : 0xBFFF,

        SPRITE_START : 0xFE00,
        SPRITE_END   : 0xFEFF
    };

    this.MEM_SIZE = 65536; // 64KB

    this.romBankNumber = 0;
    this.MBCtype = 0;
    this.banksize = 0x3FFF;
    this.rom = null;
};

Memory.prototype = new Array();

Memory.prototype.reset = function() {
    this.length = this.MEM_SIZE;
    for (var i = this.addresses.VRAM_START; i < this.addresses.VRAM_END; i++) {
        this[i] = 0;
    }
};

Memory.prototype.setRomData = function(data) {
    this.rom = data;

    this.loadRomBank(0);
    this.loadRomBank(1);
};

Memory.prototype.loadRomBank = function(index) {
    start = index ? 0x4000 : 0x0;
    var romStart = index * 0x3FFF;
    for (var i = 0; i < this.banksize; i++) {
        this[i + start] = this.rom[romStart + i];
    }
};

Memory.prototype.vram = function(address) {
    if (address < this.addresses.VRAM_START || address > this.addresses.VRAM_END) {
        throw 'VRAM access in out of bounds address ' + address;
    }

    return this[address];
};

Memory.prototype.deviceram = function(address) {
    if (address < this.addresses.DEVICERAM_START || address > this.addresses.DEVICERAM_END) {
        throw 'Device RAM access in out of bounds address ' + address;
    }

    return this[address];
};
Memory.prototype.wb = function(addr, value) {
    switch (addr & 0xF000) {
        case 0x0000: case 0x1000: // enable RAM
            break;
        case 0x2000: case 0x3000: // ROM bank number lower 5 bits
            value &= 0x1F;
            if (value == 0) value = 1;
            this.romBankNumber = (this.romBankNumber&0xE0) +value;
            this.loadRomBank(this.romBankNumber);
            break;
        default:
            this[addr] = value;
    }
}

Memory.readBit = function(byte, index) {
    return (byte >> index) & 1;
};
<commit_msg>fix rom banking<commit_after>var Memory = function() {
    this.addresses = {
        VRAM_START : 0x8000,
        VRAM_END   : 0x9FFF,

        EXTRAM_START : 0xA000,
        EXTRAM_END   : 0xBFFF,

        SPRITE_START : 0xFE00,
        SPRITE_END   : 0xFEFF
    };

    this.MEM_SIZE = 65536; // 64KB

    this.romBankNumber = 0;
    this.MBCtype = 0;
    this.banksize = 0x3FFF;
    this.rom = null;
};

Memory.prototype = new Array();

Memory.prototype.reset = function() {
    this.length = this.MEM_SIZE;
    for (var i = this.addresses.VRAM_START; i < this.addresses.VRAM_END; i++) {
        this[i] = 0;
    }
};

Memory.prototype.setRomData = function(data) {
    this.rom = data;

    this.loadRomBank(0);
    this.loadRomBank(1);
};

Memory.prototype.loadRomBank = function(index) {
    var start = index ? 0x4000 : 0x0;
    var romStart = index * 0x4000;
    for (var i = 0; i < this.banksize; i++) {
        this[i + start] = this.rom[romStart + i];
    }
};

Memory.prototype.vram = function(address) {
    if (address < this.addresses.VRAM_START || address > this.addresses.VRAM_END) {
        throw 'VRAM access in out of bounds address ' + address;
    }

    return this[address];
};

Memory.prototype.deviceram = function(address) {
    if (address < this.addresses.DEVICERAM_START || address > this.addresses.DEVICERAM_END) {
        throw 'Device RAM access in out of bounds address ' + address;
    }

    return this[address];
};
Memory.prototype.wb = function(addr, value) {
    switch (addr & 0xF000) {
        case 0x0000: case 0x1000: // enable RAM
            break;
        case 0x2000: case 0x3000: // ROM bank number lower 5 bits
            value &= 0x1F;
            if (value == 0) value = 1;
            this.romBankNumber = (this.romBankNumber&0xE0) +value;
            this.loadRomBank(this.romBankNumber);
            break;
        default:
            this[addr] = value;
    }
}

Memory.readBit = function(byte, index) {
    return (byte >> index) & 1;
};
<|endoftext|>


<commit_before>import React, { Component } from 'react';

export default class MainNavigation extends Component {
    render() {
        return (

                        Home
                        New features
                        Press
                        New hires
                        About

        );
    }
}<commit_msg>Make MainNavigation working by <Link><commit_after>import React, { Component } from 'react';
import { Link } from 'react-router';

export default class MainNavigation extends Component {
    render() {
        return (

                        <Link to="/" className="nav-link">Home</Link>
                        <Link to="/articles" className="nav-link">Articles</Link>
                        <Link to="/2015/02/some-article" className="nav-link">Some Article</Link>

        );
    }
}<|endoftext|>


<commit_before>const lineCarouselTemplate = require('../../LINE/utils')
  .carouselMessageFormatter
const MessageFormatError = require('../../../exceptions').MessageFormatError

module.exports = () => {
  return async (ctx, next) => {
    const lineClient = ctx.clients.LINE
    const rawNotification = ctx.state.incomingEvent

    isRawNotificationValid(rawNotification)

    const pushNotifications = notificationFactory(rawNotification)
    try {
      let serviceResponse
      if (pushNotifications.altText) {
        serviceResponse = await lineClient.pushMessage(rawNotification.userId, [
          { type: 'text', text: pushNotifications.altText },
          pushNotifications
        ])
      } else {
        serviceResponse = await lineClient.pushMessage(
          rawNotification.userId,
          pushNotifications
        )
      }
      ctx.state.serviceResponses = [serviceResponse]

      if (
        ctx.store.onboard.fire(
          rawNotification.userId,
          'receivedNotification'
        ) === 'engaged'
      ) {
        if (pushNotifications.altText) {
          serviceResponse = await lineClient.pushMessage(
            rawNotification.userId,
            {
              type: 'text',
              text: '我是生活 Chat 寶:blush:\n幫助您搶先知道未來停水、停電、以及道路搶修時間！記得點擊下方<預報及設定>選單進行個人化設定喔:point_down:'
            }
          )
        } else {
          serviceResponse = await lineClient.pushMessage(
            rawNotification.userId,
            {
              type: 'text',
              text: '咦？您所選擇的地點附近好像是安全地帶...\n不過沒關係，我是生活 Chat 寶:blush:\n幫助您搶先知道未來停水、停電、以及道路搶修時間！記得點擊下方<預報及設定>選單進行個人化設定喔:point_down:'
            }
          )
        }
      }

      ctx.state.serviceResponses = [
        ...ctx.state.serviceResponses,
        serviceResponse
      ]
      ctx.body = {}
    } catch (err) {
      ctx.response.status = 400
    }
  }
}

/**
 * Validate incoming raw data for pushing notification and throw exception if failed
 * @param {Object} rawNotification - Incoming raw data for pushing
 */
function isRawNotificationValid(rawNotification) {
  if (!rawNotification.category) {
    throw new MessageFormatError('body must contain category attribute!')
  }
  if (
    (!rawNotification.notifications ||
      rawNotification.notifications.length <= 0) &&
    rawNotification.category !== 'userRequested'
  ) {
    throw new MessageFormatError('notification must existed and contain data')
  }
}

/**
 * Generate pushing message depending on the category of request
 * @param {Object} rawNotification - Incoming raw data for pushing
 * @return {Object}
 */
function notificationFactory(rawNotification) {
  switch (rawNotification.category) {
    case 'userRequested':
      if (
        !rawNotification.notifications ||
        rawNotification.notifications.length === 0
      ) {
        return noNotificationsMessage()
      } else {
        return userRequestedFormMessage(rawNotification)
      }
    case 'userScheduled':
    case 'systemScheduled':
    case 'broadcast':
      return userRequestedFormMessage(rawNotification)
    default:
      throw new TypeError('Unknown type of notification!')
  }
}

/**
 * Generate the no notification message object
 * @return {Object}
 */
function noNotificationsMessage() {
  return {
    type: 'text',
    text: '關於您所選擇的地點，目前附近沒有任何民生公告喔！'
  }
}

/**
 * Generate the userRequested category of push notification
 * @param {Object} raw - Incoming raw data for pushing
 * @return {Object}
 */
function userRequestedFormMessage(raw) {
  const notificationsByTypes = {}
  raw.notifications.forEach(notification => {
    if (!notificationsByTypes[notification.type])
      notificationsByTypes[notification.type] = []
    notificationsByTypes[notification.type].push(notification)
  })
  const altText = altTextTemplate(notificationsByTypes)
  const cards = Object.keys(notificationsByTypes).map(type => {
    const notificationType = notificationTypeFactory(type)
    const notificationDetail = notificationDetailFactory(
      type,
      notificationsByTypes[type]
    )
    return {
      thumbnailImageUrl: thumbnailFactory(type),
      title: `明天要${notificationType}`,
      text: notificationDetail.length >= 60
        ? notificationDetail.slice(0, 60)
        : notificationDetail,
      actions: [
        {
          type: 'uri',
          label: `看看${notificationType}範圍`,
          uri: raw.mapURL[type]
        }
      ]
    }
  })
  return lineCarouselTemplate(altText, cards)
}

/**
 * Generate the description of card of carousel message depending on type of the notification
 * @param {String} type - type of the notification
 * @param {Object} notifications - notifications in same types
 * @return {String}
 */
function notificationDetailFactory(type, notifications) {
  switch (type) {
    case 'water_outage':
    case 'power_outage':
      return waterPowerOutageContent(notifications)
    case 'road_work':
      return roadWorkContent(notifications)
    default:
      throw new TypeError('Unknown type of notification!')
  }
}

/**
 * Generate the description of card of carousel message on waterOutage and powerOutage type
 * @param {Array} notifications - notifications in same waterOutage and powerOutage type
 * @return {String}
 */
function waterPowerOutageContent(notifications) {
  return notifications.reduce((output, notify) => {
    if (output) output += '\n'
    return (output += `${notify.startDate.slice(5)}~${notify.endDate.slice(5)} ${notify.startTime}~${notify.endTime}\n${notify.addrRoad}`)
  }, '')
}

/**
 * Generate the description of card of carousel message on roadWork type
 * @param {Array} notifications - notifications in same roadWork type
 * @return {String}
 */
function roadWorkContent(notifications) {
  return notifications.reduce((output, notify) => {
    if (output) output += '\n'
    const timeContent = notify.startTime && notify.endTime
      ? `${notify.startTime}~${notify.endTime}`
      : '依交通管制時間施工'
    return (output += `${notify.startDate.slice(5)}~${notify.endDate.slice(5)} ${timeContent}\n${notify.addrRoad}`)
  }, '')
}

/**
 * Generate the altText of the carousel message
 * @param {Object} notificationsByTypes - notifications with different types as its attribute
 * @return {String}
 */
function altTextTemplate(notificationsByTypes) {
  const typeCounts =
    (notificationsByTypes['water_outage'] ? 1 : 0) +
    (notificationsByTypes['power_outage'] ? 1 : 0) +
    (notificationsByTypes['road_work'] ? 1 : 0)

  const waterOutageCount = notificationsByTypes['water_outage']
    ? notificationsByTypes['water_outage'].length
    : 0
  const powerOutageCount = notificationsByTypes['power_outage']
    ? notificationsByTypes['power_outage'].length
    : 0
  const roadWorkCount = notificationsByTypes['road_work']
    ? notificationsByTypes['road_work'].length
    : 0

  const waterOutageNotify = waterOutageCount > 0
    ? `${waterOutageCount} 則 <停水通知>`
    : ''
  const powerOutageNotify = powerOutageCount > 0
    ? `${waterOutageNotify ? '，' : ''}${powerOutageCount} 則 <停電通知>`
    : ''
  const roadWorkNotify = roadWorkCount > 0
    ? `${waterOutageNotify || powerOutageNotify ? '，' : ''}${roadWorkCount} 則 <修路通知>`
    : ''

  const totalCounts = waterOutageCount + powerOutageCount + roadWorkCount
  const totalNotifyText = typeCounts > 1 ? `，共 ${totalCounts} 則公告訊息` : ''

  return `您明天有 ${waterOutageNotify}${powerOutageNotify}${roadWorkNotify}${totalNotifyText}。`
}

/**
 * Return the thumbnail image URL for different type of notifications
 * @param {String} type - type of the notification
 * @return {String}
 */
function thumbnailFactory(type) {
  switch (type) {
    case 'water_outage':
      return 'https://glacial-falls-53180.herokuapp.com/img/water_outage.jpg'
    case 'power_outage':
      return 'https://glacial-falls-53180.herokuapp.com/img/power_outage.jpg'
    case 'road_work':
      return 'https://glacial-falls-53180.herokuapp.com/img/road_work.jpg'
    default:
      throw new TypeError('Unknown type of notification!')
  }
}

/**
 * Return the Chinese name of different type of notifications
 * @param {String} type - type of the notification
 * @return {String}
 */
function notificationTypeFactory(type) {
  switch (type) {
    case 'water_outage':
      return '停水'
    case 'power_outage':
      return '停電'
    case 'road_work':
      return '修路'
    default:
      throw new TypeError('Unknown type of notification!')
  }
}
<commit_msg>CB-22 add emoji in pushHandler<commit_after>const lineCarouselTemplate = require('../../LINE/utils')
  .carouselMessageFormatter
const MessageFormatError = require('../../../exceptions').MessageFormatError
const emoji = require('node-emoji')

module.exports = () => {
  return async (ctx, next) => {
    const lineClient = ctx.clients.LINE
    const rawNotification = ctx.state.incomingEvent

    isRawNotificationValid(rawNotification)

    const pushNotifications = notificationFactory(rawNotification)
    try {
      let serviceResponse
      if (pushNotifications.altText) {
        serviceResponse = await lineClient.pushMessage(rawNotification.userId, [
          { type: 'text', text: pushNotifications.altText },
          pushNotifications
        ])
      } else {
        serviceResponse = await lineClient.pushMessage(
          rawNotification.userId,
          pushNotifications
        )
      }
      ctx.state.serviceResponses = [serviceResponse]

      if (
        ctx.store.onboard.fire(
          rawNotification.userId,
          'receivedNotification'
        ) === 'engaged'
      ) {
        if (pushNotifications.altText) {
          serviceResponse = await lineClient.pushMessage(
            rawNotification.userId,
            {
              type: 'text',
              text: emoji.emojify(
                '嗯！我是生活 Chat 寶:blush:\n幫助您搶先知道未來停水、停電、以及道路搶修時間！'
              )
            },
            {
              type: 'text',
              text: emoji.emojify(
                '現在就點開下方選單 -> 進入個人化設定，你可以就收到特別為你設計的民生推播通知。（也有你家外面馬路要施工的預報通知喔！）:point_down:'
              )
            }
          )
        } else {
          serviceResponse = await lineClient.pushMessage(
            rawNotification.userId,
            {
              type: 'text',
              text: emoji.emojify('咦？您所選擇的地點附近好像是安全地帶...')
            },
            {
              type: 'text',
              text: emoji.emojify(
                '不過沒關係，我是生活 Chat 寶:blush:\n幫助您搶先知道未來停水、停電、以及道路搶修時間！'
              )
            },
            {
              type: 'text',
              text: emoji.emojify(
                '現在就點開下方選單 -> 進入個人化設定，你可以就收到特別為你設計的民生推播通知。（也有你家外面馬路要施工的預報通知喔！）:point_down:'
              )
            }
          )
        }
      }

      ctx.state.serviceResponses = [
        ...ctx.state.serviceResponses,
        serviceResponse
      ]
      ctx.body = {}
    } catch (err) {
      ctx.response.status = 400
    }
  }
}

/**
 * Validate incoming raw data for pushing notification and throw exception if failed
 * @param {Object} rawNotification - Incoming raw data for pushing
 */
function isRawNotificationValid(rawNotification) {
  if (!rawNotification.category) {
    throw new MessageFormatError('body must contain category attribute!')
  }
  if (
    (!rawNotification.notifications ||
      rawNotification.notifications.length <= 0) &&
    rawNotification.category !== 'userRequested'
  ) {
    throw new MessageFormatError('notification must existed and contain data')
  }
}

/**
 * Generate pushing message depending on the category of request
 * @param {Object} rawNotification - Incoming raw data for pushing
 * @return {Object}
 */
function notificationFactory(rawNotification) {
  switch (rawNotification.category) {
    case 'userRequested':
      if (
        !rawNotification.notifications ||
        rawNotification.notifications.length === 0
      ) {
        return noNotificationsMessage()
      } else {
        return userRequestedFormMessage(rawNotification)
      }
    case 'userScheduled':
    case 'systemScheduled':
    case 'broadcast':
      return userRequestedFormMessage(rawNotification)
    default:
      throw new TypeError('Unknown type of notification!')
  }
}

/**
 * Generate the no notification message object
 * @return {Object}
 */
function noNotificationsMessage() {
  return {
    type: 'text',
    text: '關於您所選擇的地點，目前附近沒有任何民生公告喔！'
  }
}

/**
 * Generate the userRequested category of push notification
 * @param {Object} raw - Incoming raw data for pushing
 * @return {Object}
 */
function userRequestedFormMessage(raw) {
  const notificationsByTypes = {}
  raw.notifications.forEach(notification => {
    if (!notificationsByTypes[notification.type])
      notificationsByTypes[notification.type] = []
    notificationsByTypes[notification.type].push(notification)
  })
  const altText = altTextTemplate(notificationsByTypes)
  const cards = Object.keys(notificationsByTypes).map(type => {
    const notificationType = notificationTypeFactory(type)
    const notificationDetail = notificationDetailFactory(
      type,
      notificationsByTypes[type]
    )
    return {
      thumbnailImageUrl: thumbnailFactory(type),
      title: `明天要${notificationType}`,
      text: notificationDetail.length >= 60
        ? notificationDetail.slice(0, 60)
        : notificationDetail,
      actions: [
        {
          type: 'uri',
          label: `看看${notificationType}範圍`,
          uri: raw.mapURL[type]
        }
      ]
    }
  })
  return lineCarouselTemplate(altText, cards)
}

/**
 * Generate the description of card of carousel message depending on type of the notification
 * @param {String} type - type of the notification
 * @param {Object} notifications - notifications in same types
 * @return {String}
 */
function notificationDetailFactory(type, notifications) {
  switch (type) {
    case 'water_outage':
    case 'power_outage':
      return waterPowerOutageContent(notifications)
    case 'road_work':
      return roadWorkContent(notifications)
    default:
      throw new TypeError('Unknown type of notification!')
  }
}

/**
 * Generate the description of card of carousel message on waterOutage and powerOutage type
 * @param {Array} notifications - notifications in same waterOutage and powerOutage type
 * @return {String}
 */
function waterPowerOutageContent(notifications) {
  return notifications.reduce((output, notify) => {
    if (output) output += '\n'
    return (output += `${notify.startDate.slice(5)}~${notify.endDate.slice(5)} ${notify.startTime}~${notify.endTime}\n${notify.addrRoad}`)
  }, '')
}

/**
 * Generate the description of card of carousel message on roadWork type
 * @param {Array} notifications - notifications in same roadWork type
 * @return {String}
 */
function roadWorkContent(notifications) {
  return notifications.reduce((output, notify) => {
    if (output) output += '\n'
    const timeContent = notify.startTime && notify.endTime
      ? `${notify.startTime}~${notify.endTime}`
      : '依交通管制時間施工'
    return (output += `${notify.startDate.slice(5)}~${notify.endDate.slice(5)} ${timeContent}\n${notify.addrRoad}`)
  }, '')
}

/**
 * Generate the altText of the carousel message
 * @param {Object} notificationsByTypes - notifications with different types as its attribute
 * @return {String}
 */
function altTextTemplate(notificationsByTypes) {
  const typeCounts =
    (notificationsByTypes['water_outage'] ? 1 : 0) +
    (notificationsByTypes['power_outage'] ? 1 : 0) +
    (notificationsByTypes['road_work'] ? 1 : 0)

  const waterOutageCount = notificationsByTypes['water_outage']
    ? notificationsByTypes['water_outage'].length
    : 0
  const powerOutageCount = notificationsByTypes['power_outage']
    ? notificationsByTypes['power_outage'].length
    : 0
  const roadWorkCount = notificationsByTypes['road_work']
    ? notificationsByTypes['road_work'].length
    : 0

  const waterOutageNotify = waterOutageCount > 0
    ? `${waterOutageCount} 則 <停水通知>`
    : ''
  const powerOutageNotify = powerOutageCount > 0
    ? `${waterOutageNotify ? '，' : ''}${powerOutageCount} 則 <停電通知>`
    : ''
  const roadWorkNotify = roadWorkCount > 0
    ? `${waterOutageNotify || powerOutageNotify ? '，' : ''}${roadWorkCount} 則 <修路通知>`
    : ''

  const totalCounts = waterOutageCount + powerOutageCount + roadWorkCount
  const totalNotifyText = typeCounts > 1 ? `，共 ${totalCounts} 則公告訊息` : ''

  return `您明天有 ${waterOutageNotify}${powerOutageNotify}${roadWorkNotify}${totalNotifyText}。`
}

/**
 * Return the thumbnail image URL for different type of notifications
 * @param {String} type - type of the notification
 * @return {String}
 */
function thumbnailFactory(type) {
  switch (type) {
    case 'water_outage':
      return 'https://glacial-falls-53180.herokuapp.com/img/water_outage.jpg'
    case 'power_outage':
      return 'https://glacial-falls-53180.herokuapp.com/img/power_outage.jpg'
    case 'road_work':
      return 'https://glacial-falls-53180.herokuapp.com/img/road_work.jpg'
    default:
      throw new TypeError('Unknown type of notification!')
  }
}

/**
 * Return the Chinese name of different type of notifications
 * @param {String} type - type of the notification
 * @return {String}
 */
function notificationTypeFactory(type) {
  switch (type) {
    case 'water_outage':
      return '停水'
    case 'power_outage':
      return '停電'
    case 'road_work':
      return '修路'
    default:
      throw new TypeError('Unknown type of notification!')
  }
}
<|endoftext|>