<commit_before>"use strict";

const P = require('bluebird');
const uuid = require('cassandra-uuid').TimeUuid;
const HTTPError = require('hyperswitch').HTTPError;
const TopicsNotExistError = require('wmf-kafka-node/lib/errors').TopicsNotExistError;
const Task = require('./task');
const utils = require('./utils');
const TaskQueue = require('../lib/task_queue');

/**
 * The default size of the task queue
 *
 * @type {number}
 * @const
 */
const DEFAULT_TASK_QUEUE_SIZE = 30;

function decodeError(e) {
    if (Buffer.isBuffer(e.body)) {
        e.body = e.body.toString();
        try {
            e.body = JSON.parse(e.body);
        } catch (e) {
            // Not a JSON error
        }
    }
    return e;
}
/**
 * A rule executor managing matching and execution of a single rule
 */
class RuleExecutor {
    /**
     * Creates a new instance of a rule executor
     *
     * @param {Rule} rule
     * @param {KafkaFactory} kafkaFactory
     * @param {TaskQueue} taskQueue
     * @param {Object} hyper
     * @param {function} log
     * @constructor
     */
    constructor(rule, kafkaFactory, hyper, log, options) {
        this.rule = rule;
        this.kafkaFactory = kafkaFactory;
        this.hyper = hyper;
        this.log = log;
        this.taskQueue = new TaskQueue({
            size: rule.spec.concurrency || options.concurrency || DEFAULT_TASK_QUEUE_SIZE
        });
    }

    _setConsumerLoggers(consumer, ruleName, topic) {
        consumer.on('topics_changed', (topicList) => {
            // only one topic can be subscribed to by this client
            if (topicList && topicList.length) {
                this.log(`info/subscription/${ruleName}`, {
                    rule: { name: ruleName, topic },
                    msg: `Listening to ${topicList[0]}`
                });
                this._notifySubscribed(topic);
            } else {
                this.log(`info/subscription/${ruleName}`, {
                    rule: { name: ruleName, topic },
                    msg: `Lost ownership of ${topic}`
                });
            }
        });
        consumer.on('error', (err) => this.log(`warn/${ruleName}`, Object.assign(err, {
            rule: { name: ruleName, topic }
        })));
        return consumer;
    }

    _test(event) {
        try {
            const optionIndex = this.rule.test(event);
            if (optionIndex === -1) {
                // no match, drop the message
                this.log(`debug/${this.rule.name}`, {
                    msg: 'Dropping event message', event: event
                });
            }
            return optionIndex;
        } catch (e) {
            this.log(`error/${this.rule.name}`, e);
            return -1;
        }
    }

    _sampleLog(event, request, res) {
        const sampleLog = {
            message: 'Processed event sample',
            event: event,
            request: request,
            response: Object.assign({}, res)
        };
        // Don't include the full body in the log
        if (res.status === 200 || res.status === 201) {
            delete sampleLog.response.body;
        }
        this.hyper.log('trace/sample', sampleLog);
    }

    _exec(origEvent, optionIndex, statName, statDelayStartTime, retryEvent) {
        const rule = this.rule;

        this.log(`trace/${rule.name}`, { msg: 'Event message received', event: origEvent });

        // latency from the original event creation time to execution time
        this.hyper.metrics.endTiming([statName + '_delay'],
            statDelayStartTime || new Date(origEvent.meta.dt));

        const startTime = Date.now();
        const expander = {
            message: origEvent,
            match: rule.expand(optionIndex, origEvent)
        };
        return P.each(rule.getExec(optionIndex), (tpl) => {
            const request = tpl.expand(expander);
            request.headers = Object.assign(request.headers, {
                'x-request-id': origEvent.meta.request_id,
                'x-triggered-by': utils.triggeredBy(retryEvent || origEvent)
            });
            return this.hyper.request(request)
            .tap((res) => {
                if (res.status === 301) {
                    // As we don't follow redirects, and we must always use normalized titles,
                    // receiving 301 indicates some error. Log a warning.
                    this.log(`warn/${this.rule.name}`, {
                        message: '301 redirect received, used a non-normalized title',
                        rule: this.rule.name,
                        event: origEvent
                    });
                }
            })
            .tap(this._sampleLog.bind(this, retryEvent || origEvent, request))
            .catch((e) => {
                this._sampleLog(retryEvent || origEvent, request, e);
                throw e;
            });
        })
        .finally(() => {
            this.hyper.metrics.endTiming([statName + '_exec'], startTime);
        });
    }

    _safeParse(message) {
        try {
            return P.resolve(JSON.parse(message));
        } catch (e) {
            this.log(`error/${this.rule.name}`, e);
            return this.hyper.post({
                uri: '/sys/queue/events',
                body: [ this._constructErrorMessage(e, message) ]
            })
            .thenReturn(undefined);
        }
    }

    _retryTopicName() {
        return 'change-prop.retry.' + this.rule.topic;
    }

    /**
     * Checks whether retry limit for this rule is exceeded.
     *
     * @param {Object} message a retry message to check
     * @param {Error} [e] optional Error that caused a retry
     * @returns {boolean}
     * @private
     */
    _isLimitExceeded(message, e) {
        if (message.retries_left <= 0) {
            this.log(`error/${this.rule.name}`, {
                message: 'Retry count exceeded',
                event: message,
                error: e
            });
            return true;
        }
        return false;
    }

    /**
     * Notify master what this worker is doing for debugging purposes if it fails
     * In production normally one worker will succeed subscribing only to one rule,
     * so this will emit the only rule the worker is processing
     *
     * @param {string} topic topic name this worker subscribed too
     * @private
     */
    _notifySubscribed(topic) {
        process.emit('service_status', {
            type: 'subscription',
            rule: this.rule.name,
            topic: topic
        });
    }

    _catch(message, retryMessage, e) {
        const reportError = () => this.hyper.post({
            uri: '/sys/queue/events',
            body: [this._constructErrorMessage(e, message)]
        });

        if (e.constructor.name !== 'HTTPError') {
            // We've got an error, but it's not from the update request, it's
            // some bug in change-prop. Log and send a fatal error message.
            this.hyper.log(`error/${this.rule.name}`, {
                message: 'Internal error in change-prop',
                error: e,
                event: message
            });
            return reportError();
        } else if (this.rule.shouldRetry(e)
            && !this._isLimitExceeded(retryMessage, e)) {
            return this._retry(retryMessage);
        } else if (!this.rule.shouldIgnoreError(e)) {
            return reportError();
        }
    }

    /**
     * Set's up a consumer a retry queue
     *
     * @private
     */
    _setUpRetryTopic() {
        const retryTopicName = this._retryTopicName();

        return this.kafkaFactory.newConsumer(this.kafkaFactory.newClient(),
            retryTopicName,
            `change-prop-${retryTopicName}-${this.rule.name}`)
        .then((consumer) => {
            this.retryConsumer = this._setConsumerLoggers(consumer, this.rule.name, retryTopicName);
            this.retryConsumer.on('message', (msg) => {
                const statName = this.hyper.metrics.normalizeName(this.rule.name + '_retry');
                return this._safeParse(msg.value)
                .then((message) => {
                    if (!message) {
                        // Don't retry if we can't parse an event, just log.
                        return;
                    }

                    if (message.emitter_id !== this._consumerId()) {
                        // Not our business, don't care
                        return;
                    }

                    if (this._isLimitExceeded(message)) {
                        // We've don our best, give up
                        return;
                    }

                    const optionIndex = this._test(message.original_event);
                    if (optionIndex === -1) {
                        // doesn't match any more, possibly meaning
                        // the rule has been changed since we last
                        // executed it on the message
                        return;
                    }

                    return this.taskQueue.enqueue(new Task(consumer, message,
                        this._exec.bind(this, message.original_event,
                            optionIndex, statName, new Date(message.meta.dt), message),
                        (e) => {
                            e = decodeError(e);
                            const retryMessage = this._constructRetryMessage(message.original_event,
                                e, message.retries_left - 1, message);
                            return this._catch(message, retryMessage, e);
                        }
                    ));
                });
            });
        });
    }

    _retry(retryMessage) {
        const spec = this.rule.spec;
        const delay = spec.retry_delay *
            Math.pow(spec.retry_factor, spec.retry_limit - retryMessage.retries_left);
        return P.delay(delay)
        .then(() => this.hyper.post({
            uri: '/sys/queue/events',
            body: [ retryMessage ]
        }));
    }

    _consumerId() {
        return 'change-prop#' + this.rule.name;
    }

    _constructRetryMessage(event, errorRes, retriesLeft, retryEvent) {
        return {
            meta: {
                topic: this._retryTopicName(),
                schema_uri: 'retry/1',
                uri: event.meta.uri,
                request_id: event.meta.request_id,
                id: undefined, // will be filled later
                dt: undefined, // will be filled later
                domain: event.meta.domain
            },
            triggered_by: utils.triggeredBy(retryEvent || event),
            emitter_id: this._consumerId(),
            retries_left: retriesLeft === undefined ? this.rule.spec.retry_limit : retriesLeft,
            original_event: event,
            reason: errorRes && errorRes.body && errorRes.body.title
        };
    }

    /**
     * Create an error message for a special Kafka topic
     *
     * @param {Error} e an exception that caused a failure
     * @param {string|Object} event an original event. In case JSON parsing failed - it's a string.
     */
    _constructErrorMessage(e, event) {
        const eventUri = typeof event === 'string' ? '/error/uri' : event.meta.uri;
        const domain = typeof event === 'string' ? 'unknown' : event.meta.domain;
        const now = new Date();
        const errorEvent = {
            meta: {
                topic: 'change-prop.error',
                schema_uri: 'error/1',
                uri: eventUri,
                request_id: typeof event === 'string' ? undefined : event.meta.request_id,
                id: uuid.fromDate(now),
                dt: now.toISOString(),
                domain: domain
            },
            emitter_id: 'change-prop#' + this.rule.name,
            raw_event: typeof event === 'string' ? event : JSON.stringify(event),
            message: e.message,
            stack: e.stack
        };
        if (e.constructor === HTTPError) {
            errorEvent.details = {
                status: e.status,
                headers: e.headers,
                body: e.body
            };
        }
        return errorEvent;
    }

    subscribe() {
        const rule = this.rule;
        const client = this.kafkaFactory.newClient();
        return this._setUpRetryTopic()
        .then(() => {
            return this.kafkaFactory.newConsumer(client, rule.topic, `change-prop-${rule.name}`)
            .then((consumer) => {
                this.consumer = this._setConsumerLoggers(consumer, rule.name, rule.topic);
                this.taskQueue.addConsumer(this.consumer);
                this.consumer.on('message', (msg) => {
                    const statName = this.hyper.metrics.normalizeName(this.rule.name);
                    return this._safeParse(msg.value)
                    .then((message) => {
                        if (!message) {
                            // no message we are done here
                            return;
                        }

                        const optionIndex = this._test(message);
                        if (optionIndex === -1) {
                            // Message doesn't match any option for this rule, we're done
                            return;
                        }

                        return this.taskQueue.enqueue(new Task(
                            consumer,
                            msg,
                            this._exec.bind(this, message, optionIndex, statName),
                            (e) => {
                                const retryMessage = this._constructRetryMessage(message, e);
                                e = decodeError(e);
                                return this._catch(message, retryMessage, e);
                            }
                        ));
                    });
                });
            })
            .catch(TopicsNotExistError, (e) => {
                this.log('error/topic', e);
                // Exit async to let the logs get processed.
                setTimeout(() => process.exit(1), 100);
            });
        });
    }

    close() {
        return this.taskQueue.close()
        .then(this.retryConsumer.closeAsync.bind(this.retryConsumer))
        .then(this.consumer.closeAsync.bind(this.consumer));
    }
}

module.exports = RuleExecutor;
<commit_msg>Decode error before constructing a retry message (#65)<commit_after>"use strict";

const P = require('bluebird');
const uuid = require('cassandra-uuid').TimeUuid;
const HTTPError = require('hyperswitch').HTTPError;
const TopicsNotExistError = require('wmf-kafka-node/lib/errors').TopicsNotExistError;
const Task = require('./task');
const utils = require('./utils');
const TaskQueue = require('../lib/task_queue');

/**
 * The default size of the task queue
 *
 * @type {number}
 * @const
 */
const DEFAULT_TASK_QUEUE_SIZE = 30;

function decodeError(e) {
    if (Buffer.isBuffer(e.body)) {
        e.body = e.body.toString();
        try {
            e.body = JSON.parse(e.body);
        } catch (e) {
            // Not a JSON error
        }
    }
    return e;
}
/**
 * A rule executor managing matching and execution of a single rule
 */
class RuleExecutor {
    /**
     * Creates a new instance of a rule executor
     *
     * @param {Rule} rule
     * @param {KafkaFactory} kafkaFactory
     * @param {TaskQueue} taskQueue
     * @param {Object} hyper
     * @param {function} log
     * @constructor
     */
    constructor(rule, kafkaFactory, hyper, log, options) {
        this.rule = rule;
        this.kafkaFactory = kafkaFactory;
        this.hyper = hyper;
        this.log = log;
        this.taskQueue = new TaskQueue({
            size: rule.spec.concurrency || options.concurrency || DEFAULT_TASK_QUEUE_SIZE
        });
    }

    _setConsumerLoggers(consumer, ruleName, topic) {
        consumer.on('topics_changed', (topicList) => {
            // only one topic can be subscribed to by this client
            if (topicList && topicList.length) {
                this.log(`info/subscription/${ruleName}`, {
                    rule: { name: ruleName, topic },
                    msg: `Listening to ${topicList[0]}`
                });
                this._notifySubscribed(topic);
            } else {
                this.log(`info/subscription/${ruleName}`, {
                    rule: { name: ruleName, topic },
                    msg: `Lost ownership of ${topic}`
                });
            }
        });
        consumer.on('error', (err) => this.log(`warn/${ruleName}`, Object.assign(err, {
            rule: { name: ruleName, topic }
        })));
        return consumer;
    }

    _test(event) {
        try {
            const optionIndex = this.rule.test(event);
            if (optionIndex === -1) {
                // no match, drop the message
                this.log(`debug/${this.rule.name}`, {
                    msg: 'Dropping event message', event: event
                });
            }
            return optionIndex;
        } catch (e) {
            this.log(`error/${this.rule.name}`, e);
            return -1;
        }
    }

    _sampleLog(event, request, res) {
        const sampleLog = {
            message: 'Processed event sample',
            event: event,
            request: request,
            response: Object.assign({}, res)
        };
        // Don't include the full body in the log
        if (res.status === 200 || res.status === 201) {
            delete sampleLog.response.body;
        }
        this.hyper.log('trace/sample', sampleLog);
    }

    _exec(origEvent, optionIndex, statName, statDelayStartTime, retryEvent) {
        const rule = this.rule;

        this.log(`trace/${rule.name}`, { msg: 'Event message received', event: origEvent });

        // latency from the original event creation time to execution time
        this.hyper.metrics.endTiming([statName + '_delay'],
            statDelayStartTime || new Date(origEvent.meta.dt));

        const startTime = Date.now();
        const expander = {
            message: origEvent,
            match: rule.expand(optionIndex, origEvent)
        };
        return P.each(rule.getExec(optionIndex), (tpl) => {
            const request = tpl.expand(expander);
            request.headers = Object.assign(request.headers, {
                'x-request-id': origEvent.meta.request_id,
                'x-triggered-by': utils.triggeredBy(retryEvent || origEvent)
            });
            return this.hyper.request(request)
            .tap((res) => {
                if (res.status === 301) {
                    // As we don't follow redirects, and we must always use normalized titles,
                    // receiving 301 indicates some error. Log a warning.
                    this.log(`warn/${this.rule.name}`, {
                        message: '301 redirect received, used a non-normalized title',
                        rule: this.rule.name,
                        event: origEvent
                    });
                }
            })
            .tap(this._sampleLog.bind(this, retryEvent || origEvent, request))
            .catch((e) => {
                this._sampleLog(retryEvent || origEvent, request, e);
                throw e;
            });
        })
        .finally(() => {
            this.hyper.metrics.endTiming([statName + '_exec'], startTime);
        });
    }

    _safeParse(message) {
        try {
            return P.resolve(JSON.parse(message));
        } catch (e) {
            this.log(`error/${this.rule.name}`, e);
            return this.hyper.post({
                uri: '/sys/queue/events',
                body: [ this._constructErrorMessage(e, message) ]
            })
            .thenReturn(undefined);
        }
    }

    _retryTopicName() {
        return 'change-prop.retry.' + this.rule.topic;
    }

    /**
     * Checks whether retry limit for this rule is exceeded.
     *
     * @param {Object} message a retry message to check
     * @param {Error} [e] optional Error that caused a retry
     * @returns {boolean}
     * @private
     */
    _isLimitExceeded(message, e) {
        if (message.retries_left <= 0) {
            this.log(`error/${this.rule.name}`, {
                message: 'Retry count exceeded',
                event: message,
                error: e
            });
            return true;
        }
        return false;
    }

    /**
     * Notify master what this worker is doing for debugging purposes if it fails
     * In production normally one worker will succeed subscribing only to one rule,
     * so this will emit the only rule the worker is processing
     *
     * @param {string} topic topic name this worker subscribed too
     * @private
     */
    _notifySubscribed(topic) {
        process.emit('service_status', {
            type: 'subscription',
            rule: this.rule.name,
            topic: topic
        });
    }

    _catch(message, retryMessage, e) {
        const reportError = () => this.hyper.post({
            uri: '/sys/queue/events',
            body: [this._constructErrorMessage(e, message)]
        });

        if (e.constructor.name !== 'HTTPError') {
            // We've got an error, but it's not from the update request, it's
            // some bug in change-prop. Log and send a fatal error message.
            this.hyper.log(`error/${this.rule.name}`, {
                message: 'Internal error in change-prop',
                error: e,
                event: message
            });
            return reportError();
        } else if (this.rule.shouldRetry(e)
            && !this._isLimitExceeded(retryMessage, e)) {
            return this._retry(retryMessage);
        } else if (!this.rule.shouldIgnoreError(e)) {
            return reportError();
        }
    }

    /**
     * Set's up a consumer a retry queue
     *
     * @private
     */
    _setUpRetryTopic() {
        const retryTopicName = this._retryTopicName();

        return this.kafkaFactory.newConsumer(this.kafkaFactory.newClient(),
            retryTopicName,
            `change-prop-${retryTopicName}-${this.rule.name}`)
        .then((consumer) => {
            this.retryConsumer = this._setConsumerLoggers(consumer, this.rule.name, retryTopicName);
            this.retryConsumer.on('message', (msg) => {
                const statName = this.hyper.metrics.normalizeName(this.rule.name + '_retry');
                return this._safeParse(msg.value)
                .then((message) => {
                    if (!message) {
                        // Don't retry if we can't parse an event, just log.
                        return;
                    }

                    if (message.emitter_id !== this._consumerId()) {
                        // Not our business, don't care
                        return;
                    }

                    if (this._isLimitExceeded(message)) {
                        // We've don our best, give up
                        return;
                    }

                    const optionIndex = this._test(message.original_event);
                    if (optionIndex === -1) {
                        // doesn't match any more, possibly meaning
                        // the rule has been changed since we last
                        // executed it on the message
                        return;
                    }

                    return this.taskQueue.enqueue(new Task(consumer, message,
                        this._exec.bind(this, message.original_event,
                            optionIndex, statName, new Date(message.meta.dt), message),
                        (e) => {
                            e = decodeError(e);
                            const retryMessage = this._constructRetryMessage(message.original_event,
                                e, message.retries_left - 1, message);
                            return this._catch(message, retryMessage, e);
                        }
                    ));
                });
            });
        });
    }

    _retry(retryMessage) {
        const spec = this.rule.spec;
        const delay = spec.retry_delay *
            Math.pow(spec.retry_factor, spec.retry_limit - retryMessage.retries_left);
        return P.delay(delay)
        .then(() => this.hyper.post({
            uri: '/sys/queue/events',
            body: [ retryMessage ]
        }));
    }

    _consumerId() {
        return 'change-prop#' + this.rule.name;
    }

    _constructRetryMessage(event, errorRes, retriesLeft, retryEvent) {
        return {
            meta: {
                topic: this._retryTopicName(),
                schema_uri: 'retry/1',
                uri: event.meta.uri,
                request_id: event.meta.request_id,
                id: undefined, // will be filled later
                dt: undefined, // will be filled later
                domain: event.meta.domain
            },
            triggered_by: utils.triggeredBy(retryEvent || event),
            emitter_id: this._consumerId(),
            retries_left: retriesLeft === undefined ? this.rule.spec.retry_limit : retriesLeft,
            original_event: event,
            reason: errorRes && errorRes.body && errorRes.body.title
        };
    }

    /**
     * Create an error message for a special Kafka topic
     *
     * @param {Error} e an exception that caused a failure
     * @param {string|Object} event an original event. In case JSON parsing failed - it's a string.
     */
    _constructErrorMessage(e, event) {
        const eventUri = typeof event === 'string' ? '/error/uri' : event.meta.uri;
        const domain = typeof event === 'string' ? 'unknown' : event.meta.domain;
        const now = new Date();
        const errorEvent = {
            meta: {
                topic: 'change-prop.error',
                schema_uri: 'error/1',
                uri: eventUri,
                request_id: typeof event === 'string' ? undefined : event.meta.request_id,
                id: uuid.fromDate(now),
                dt: now.toISOString(),
                domain: domain
            },
            emitter_id: 'change-prop#' + this.rule.name,
            raw_event: typeof event === 'string' ? event : JSON.stringify(event),
            message: e.message,
            stack: e.stack
        };
        if (e.constructor === HTTPError) {
            errorEvent.details = {
                status: e.status,
                headers: e.headers,
                body: e.body
            };
        }
        return errorEvent;
    }

    subscribe() {
        const rule = this.rule;
        const client = this.kafkaFactory.newClient();
        return this._setUpRetryTopic()
        .then(() => {
            return this.kafkaFactory.newConsumer(client, rule.topic, `change-prop-${rule.name}`)
            .then((consumer) => {
                this.consumer = this._setConsumerLoggers(consumer, rule.name, rule.topic);
                this.taskQueue.addConsumer(this.consumer);
                this.consumer.on('message', (msg) => {
                    const statName = this.hyper.metrics.normalizeName(this.rule.name);
                    return this._safeParse(msg.value)
                    .then((message) => {
                        if (!message) {
                            // no message we are done here
                            return;
                        }

                        const optionIndex = this._test(message);
                        if (optionIndex === -1) {
                            // Message doesn't match any option for this rule, we're done
                            return;
                        }

                        return this.taskQueue.enqueue(new Task(
                            consumer,
                            msg,
                            this._exec.bind(this, message, optionIndex, statName),
                            (e) => {
                                e = decodeError(e);
                                const retryMessage = this._constructRetryMessage(message, e);
                                return this._catch(message, retryMessage, e);
                            }
                        ));
                    });
                });
            })
            .catch(TopicsNotExistError, (e) => {
                this.log('error/topic', e);
                // Exit async to let the logs get processed.
                setTimeout(() => process.exit(1), 100);
            });
        });
    }

    close() {
        return this.taskQueue.close()
        .then(this.retryConsumer.closeAsync.bind(this.retryConsumer))
        .then(this.consumer.closeAsync.bind(this.consumer));
    }
}

module.exports = RuleExecutor;
<|endoftext|>


<commit_before>import { Component } from 'react'
import factory from '../factory'
import Header from './header'
import Body from './body'

export default class Table extends Component {
  constructor(props) {
    super()

    this.state = {
      sortedRows: props.property.rows.slice(),
      currentColumnIndex: null,
      descending: false,
    }
  }

  render() {
    return (

        {this.props.property.title}

           this.sortBy(index)} />

)
  }

  sortBy(columnIndex) {
    const rowsToSort = this.state.sortedRows
    let sorted = rowsToSort.sort((row1, row2) => {
      if (this.getRowValue(row1, columnIndex) > this.getRowValue(row2, columnIndex)) return 1
      if (this.getRowValue(row1, columnIndex) < this.getRowValue(row2, columnIndex)) return -1
      return 0
    })

    if (this.state.currentColumnIndex == columnIndex && !this.state.descending) {
      sorted = sorted.reverse()
    }

    this.setState({
      sortedRows: sorted,
      descending: this.state.currentColumnIndex == columnIndex ? !this.state.descending : false,
      currentColumnIndex: columnIndex,
    })
  }

  getRowValue(row, columnIndex) {
    if (row[columnIndex].type == 'html') {
      const regex = /(<([^>]+)>)/ig
      return row[columnIndex].content.replace(regex, '')
    }

    if (row[columnIndex].type == 'number') {
      return parseFloat(row[columnIndex].value)
    }

    if (row[columnIndex].type == 'date' || row[columnIndex].type == 'datetime') {
      return new Date(row[columnIndex].value)
    }

    if (row[columnIndex].type == 'boolean') {
      return row[columnIndex].value.toLowerCase() === 'true'
    }

    return row[columnIndex].value
  }
}

<commit_msg>fix: ignore case when sorting column<commit_after>import { Component } from 'react'
import factory from '../factory'
import Header from './header'
import Body from './body'

export default class Table extends Component {
  constructor(props) {
    super()

    this.state = {
      sortedRows: props.property.rows.slice(),
      currentColumnIndex: null,
      descending: false,
    }
  }

  render() {
    return (

        {this.props.property.title}

           this.sortBy(index)} />

)
  }

  sortBy(columnIndex) {
    const rowsToSort = this.state.sortedRows
    let sorted = rowsToSort.sort((row1, row2) => {
      if (this.getRowValue(row1, columnIndex) > this.getRowValue(row2, columnIndex)) return 1
      if (this.getRowValue(row1, columnIndex) < this.getRowValue(row2, columnIndex)) return -1
      return 0
    })

    if (this.state.currentColumnIndex == columnIndex && !this.state.descending) {
      sorted = sorted.reverse()
    }

    this.setState({
      sortedRows: sorted,
      descending: this.state.currentColumnIndex == columnIndex ? !this.state.descending : false,
      currentColumnIndex: columnIndex,
    })
  }

  getRowValue(row, columnIndex) {
    if (row[columnIndex].type == 'html') {
      const regex = /(<([^>]+)>)/ig
      return row[columnIndex].content.replace(regex, '')
    }

    if (row[columnIndex].type == 'number') {
      return parseFloat(row[columnIndex].value)
    }

    if (row[columnIndex].type == 'date' || row[columnIndex].type == 'datetime') {
      return new Date(row[columnIndex].value)
    }

    if (row[columnIndex].type == 'boolean') {
      return row[columnIndex].value.toLowerCase() === 'true'
    }

    return row[columnIndex].value && row[columnIndex].value.toLowerCase()
  }
}

<|endoftext|>


<commit_before>import React, { Component } from 'react'
import { connect } from 'react-redux'
import { reduxForm } from 'redux-form'
import { submitBill, fetchBills } from '../../actions'
import DynamicForm from '../dynamic_form'
//export const fields = [ 'users', 'billName', 'billTotal', 'billDueDate' ]
//TODO: import necessary

class ChargeForm extends Component {
  // constructor(props) {
  //   super(props)
  // }

  onSubmit(props) {
    //TODO: change from createMessage to whatever action
    this.props.submitBill(props)
      .then(() => {
        //TODO: change to equiv of fetchMessages
        this.props.fetchBills()
        this.props.resetForm()
      })
  }

  render() {
    var userList = [];
    this.props.house.users.forEach(user =>
      userList.push(user.id)
    )
    const {
      fields: { billName, billTotal, billDueDate },
      handleSubmit,
      resetForm,
      submitting
    } = this.props
    return (
      <form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
        <div className={`form-group ${billName.touched && billName.invalid ? 'has-danger': ''}`} >
          <input placeholder="Type bill name..." type="text" className="form-control" {...billName} />

            {billName.touched ? billName.error : ''}

        <div className={`form-group ${billTotal.touched && billTotal.invalid ? 'has-danger': ''}`} >
          <input placeholder="Enter bill total..." type="number" className="form-control" {...billTotal} />

            {billTotal.touched ? billTotal.error : ''}

        {this.props.house.users.map(user =>
          <div key={user.id} className={`form-group ${user['id'].touched && user['id'].invalid ? 'has-danger': ''}`} >
            {user.name}
            <input placeholder={`Enter bill total for ${user.name}`} type="number" className="form-control" {...user['id']} />

              {user['id'].touched ? user['id'].error : ''}

        )}

        <div className={`form-group ${billDueDate.touched && billDueDate.invalid ? 'has-danger': ''}`} >
          <input placeholder="Select bill due date..." type="date" className="form-control" {...billDueDate} />

            {billDueDate.touched ? billDueDate.error : ''}

        <button type="submit" className="btn btn-primary">Split Evenly</button>
        <button type="submit" className="btn btn-primary">Submit</button>
      </form>
    )
  }
}

const UserCustomEntry = ({
  user
}) => (
  <div className={`form-group ${billTotal.touched && billTotal.invalid ? 'has-danger': ''}`} >
    <input placeholder="Enter bill total..." type="number" className="form-control" {...billTotal} />

      {billTotal.touched ? billTotal.error : ''}

)

function validate(values) {
  console.log('VALIDATION VALUES', values)
  const errors = {};
  if (!values.billName) {
    errors.billName = 'Enter a name';
  }
  return errors;
}

function mapStateToProps (state) {
  return {
    house: state.house
  }
}

export default reduxForm({
  form: 'ChargeForm',
  fields: ['billName', 'billTotal', 'billDueDate'],
  validate
}, mapStateToProps, { submitBill, fetchBills })(ChargeForm);

<commit_msg>Reimplements charge form without redux form<commit_after>import React, { Component } from 'react'
import { connect } from 'react-redux'

class ChargeForm extends Component {
  constructor(props) {
    super(props)
  }

  // componentWillMount() {

  // }

  render() {
    //billname
    //total       due date
    //split button    custom button
                      //user list
                      //submit
    return (

        <form>

            Bill Name

              <input type="text" ref="billName" />

              Total

                <input ref="billName"/>

              Due Date

                <input ref="billName" type="date"/>

            <button className="charge-form-button btn-info">Split Evenly</button>

            {this.props.users.map((user, i) =>
              <UserCustomEntry key={i} user={user} />
            )}

          <button className="charge-form-button btn-info">Submit</button>
        </form>

    )
  }
}

const UserCustomEntry = ({
  user
}) => (

      {user.name}

$

      <input className="form-control custom-input" width="10" type='number'/>

)

function mapStateToProps (state) {
  return {
    users: state.house.users
  }
}

export default connect(mapStateToProps)(ChargeForm)

/*
.charge-form-container {
  border: 1px solid black;
  height: 80vh;
}
.charge-form-section > div {
  display: inline-block;
}
.user-custom-entry > div {
  display: inline-block;
  width: 15%;
}
*/<|endoftext|>


<commit_before>"use strict";

const chalk = require('chalk');
const _ = require('lodash');
const Promise = require('bluebird');
const cp = require('child_process');
const moment = require('moment');
const path = require('path');
const uuid = require('node-uuid');

const fs = require('fs-extra');

// Helper function for capturing the state of a directory
function getDirectoryTree(dir) {
    const items = [];
    return new Promise(function(resolve) {
        fs.walk(dir)
        .on('data', function (item) {
            items.push(item.path);
        })
        .on('end', function () {
            resolve(items);
        });
    });
}

module.exports = function *(lambdaPath, event, context, environment, assets) {
    const pathComponents = lambdaPath.split('.');
    const name = path.basename(path.dirname(lambdaPath));
    let handlerFunction = 'handler';

    assets = assets || {};

    if (pathComponents[pathComponents.length - 1] !== 'js') {
        handlerFunction = pathComponents[pathComponents.length - 1];
        pathComponents[pathComponents.length - 1] = 'js';
    }

    // Reasonable default values for context
    context = _.assign({
        timeout: 6,
        functionName: name,
        functionVersion: '$LATEST',
        invokedFunctionArn: `arn:lt:lambda:${name}/$LATEST`,
        memoryLimitInMB: 256,
        awsRequestId: uuid.v4(),
        logGroupName: 'local',
        logStreamName: '',
        identity: null,
        clientContext: null
    }, context);

    // Capture the state of the Lambda function directory before running
    // This allows us to clean up all files etc that were generated during execution
    // The idea behind this is simple, Lambda should not carry state between
    // executions.
    const directory = path.dirname(lambdaPath);

    return getDirectoryTree(directory).then(function(tree) {
        // If there are any assets, make sure to create them
        _.forOwn(assets, function(src, dst) {
            src = path.resolve(directory, src);
            dst = path.resolve(directory, dst);

            try {
                fs.ensureSymlinkSync(src, dst);
            } catch (e) {
                // Ignore
            }
        });

        // Grab the difference (i.e all the paths that were created
        // as part of the symlinking process)
        return getDirectoryTree(directory).then(function(newTree) {
            return _.difference(newTree, tree);
        });
    })
    .then(function(filesToCleanup) {
        return new Promise(function(resolve, reject) {
            const args = [pathComponents.join('.'), handlerFunction, JSON.stringify(event), JSON.stringify(context), context.timeout * 1000];

            const child = cp.fork(path.resolve(__dirname, './execution-wrapper'), args, {
                cwd: directory,
                env: _.assign({ HOME: process.env.HOME, PATH: process.env.PATH, USER: process.env.USER }, environment),
                silent: true
            });

            let result = null;
            child.on('message', function(res) {
                result = res;
            });

            child.stdout.on('data', function(data) {
                const dateString = moment().format('DD-MM-YYYY HH:mm:ss.SSS');
                const logString = data.toString().split('\n').join('\n\t\t').trim();

                console.log(`\t${chalk.gray('[' + dateString + ']')} ${logString}`);
            });

            child.stderr.on('data', function(data) {
                const dateString = moment().format('DD-MM-YYYY HH:mm:ss.SSS');
                const logString = data.toString().split('\n').join('\n\t\t').trim();

                console.log(`\t${chalk.gray('[' + dateString + ']')} ERROR: ${chalk.red(logString)}`);
            });

            child.on('exit', function() {
                console.log('');
                if (result && result.type === 'error') {
                    reject(new Error(result.result));
                } else {
                    resolve(result.result);
                }
            });
        }).then(function(result) {
            // Clean up after ourselves
            filesToCleanup.forEach(function(location) {
                fs.removeSync(location);
            });

            return result;
        }, function(error) {
            // Clean up after ourselves
            filesToCleanup.forEach(function(location) {
                fs.removeSync(location);
            });

            throw error;
        });
    });
};
<commit_msg>Add request ID to log outputs<commit_after>"use strict";

const chalk = require('chalk');
const _ = require('lodash');
const Promise = require('bluebird');
const cp = require('child_process');
const moment = require('moment');
const path = require('path');
const uuid = require('node-uuid');

const fs = require('fs-extra');

// Helper function for capturing the state of a directory
function getDirectoryTree(dir) {
    const items = [];
    return new Promise(function(resolve) {
        fs.walk(dir)
        .on('data', function (item) {
            items.push(item.path);
        })
        .on('end', function () {
            resolve(items);
        });
    });
}

module.exports = function *(lambdaPath, event, context, environment, assets) {
    const pathComponents = lambdaPath.split('.');
    const name = path.basename(path.dirname(lambdaPath));
    let handlerFunction = 'handler';

    assets = assets || {};

    if (pathComponents[pathComponents.length - 1] !== 'js') {
        handlerFunction = pathComponents[pathComponents.length - 1];
        pathComponents[pathComponents.length - 1] = 'js';
    }

    // Reasonable default values for context
    context = _.assign({
        timeout: 6,
        functionName: name,
        functionVersion: '$LATEST',
        invokedFunctionArn: `arn:lt:lambda:${name}/$LATEST`,
        memoryLimitInMB: 256,
        awsRequestId: uuid.v4(),
        logGroupName: 'local',
        logStreamName: '',
        identity: null,
        clientContext: null
    }, context);

    // Capture the state of the Lambda function directory before running
    // This allows us to clean up all files etc that were generated during execution
    // The idea behind this is simple, Lambda should not carry state between
    // executions.
    const directory = path.dirname(lambdaPath);

    return getDirectoryTree(directory).then(function(tree) {
        // If there are any assets, make sure to create them
        _.forOwn(assets, function(src, dst) {
            src = path.resolve(directory, src);
            dst = path.resolve(directory, dst);

            try {
                fs.ensureSymlinkSync(src, dst);
            } catch (e) {
                // Ignore
            }
        });

        // Grab the difference (i.e all the paths that were created
        // as part of the symlinking process)
        return getDirectoryTree(directory).then(function(newTree) {
            return _.difference(newTree, tree);
        });
    })
    .then(function(filesToCleanup) {
        return new Promise(function(resolve, reject) {
            const args = [pathComponents.join('.'), handlerFunction, JSON.stringify(event), JSON.stringify(context), context.timeout * 1000];

            const child = cp.fork(path.resolve(__dirname, './execution-wrapper'), args, {
                cwd: directory,
                env: _.assign({ HOME: process.env.HOME, PATH: process.env.PATH, USER: process.env.USER }, environment),
                silent: true
            });

            let result = null;
            child.on('message', function(res) {
                result = res;
            });

            child.stdout.on('data', function(data) {
                const dateString = moment().format('DD-MM-YYYY HH:mm:ss.SSS');
                const logString = data.toString().split('\n').join('\n\t\t').trim();

                console.log(`\t${chalk.gray('[' + dateString + ' ' + context.awsRequestId + ']')} ${logString}`);
            });

            child.stderr.on('data', function(data) {
                const dateString = moment().format('DD-MM-YYYY HH:mm:ss.SSS');
                const logString = data.toString().split('\n').join('\n\t\t').trim();

                console.log(`\t${chalk.gray('[' + dateString + ' ' + context.awsRequestId + ']')} ERROR: ${chalk.red(logString)}`);
            });

            child.on('exit', function() {
                console.log('');
                if (result && result.type === 'error') {
                    reject(new Error(result.result));
                } else {
                    resolve(result.result);
                }
            });
        }).then(function(result) {
            // Clean up after ourselves
            filesToCleanup.forEach(function(location) {
                fs.removeSync(location);
            });

            return result;
        }, function(error) {
            // Clean up after ourselves
            filesToCleanup.forEach(function(location) {
                fs.removeSync(location);
            });

            throw error;
        });
    });
};
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';

import Input from '../../containers/input';

const Checkbox = (props) => (

    <Input
      type={props.type}
      style={{
        float: 'left',
        width: '48%',
      }}

      data-form-id={props['data-form-id']}

      className={props.className}
      id={props.id}
      required={props.required}
      value={props.value}
      name={props.name}
      checked={props.checked}
    />
    {props.value}

);

Checkbox.defaultProps = {
  className: '',
  'data-form-id': '',
  id: '',
  name: '',
  required: 'false',
  type: 'checkbox',
  value: '',
  checked: false,
};

Checkbox.propTypes = {
  className: PropTypes.string,
  'data-form-id': PropTypes.string.isRequired,
  id: PropTypes.string,
  name: PropTypes.string,
  required: PropTypes.string,
  type: PropTypes.string,
  value: PropTypes.string,
  checked: PropTypes.bool,
};

export default Checkbox;
<commit_msg>Feat: add checkboxes<commit_after>import React from 'react';
import PropTypes from 'prop-types';

import Input from '../../containers/input';

const Checkbox = (props) => (

    <Input
      type={props.type}

      data-form-id={props['data-form-id']}

      className={props.className}
      id={props.id}
      required={props.required}
      value={props.value}
      name={props.name}
      checked={props.checked}
    />
    {props.value}

);

Checkbox.defaultProps = {
  className: '',
  'data-form-id': '',
  id: '',
  name: '',
  required: false,
  type: 'checkbox',
  value: '',
  checked: false,
};

Checkbox.propTypes = {
  className: PropTypes.string,
  'data-form-id': PropTypes.string.isRequired,
  id: PropTypes.string,
  name: PropTypes.string,
  required: PropTypes.bool,
  type: PropTypes.string,
  value: PropTypes.string,
  checked: PropTypes.bool,
};

export default Checkbox;
<|endoftext|>


<commit_before>// Native
const EventEmitter = require('events')

// Packages
const debug = require('debug')('runner-server')
const { Server } = require('ws')

class RacingServer extends EventEmitter {
  constructor(wss) {
    super()
    this.wss = wss.on('connection', ws => {
      this.ws = ws.on('message', payload => {
        this.onMessage(JSON.parse(payload))
      })
    })
  }

  acknowledge(type) {
    debug('ack %s', type)
    this.ws.send(JSON.stringify({ type: `${type}:ack` }))
  }

  close() {
    return new Promise(resolve => this.wss.close(resolve))
  }

  onMessage({ type, run, err }) {
    debug('rcv %s %s', type, run && run.title)
    this.emit(type, run, err)
  }
}

module.exports = ({ port }) =>
  new Promise(resolve => {
    const wss = new Server({ port })
    resolve(new RacingServer(wss))
  })
<commit_msg>:speaker: (runner-server) more logs<commit_after>// Native
const EventEmitter = require('events')

// Packages
const debug = require('debug')('runner-server')
const { Server } = require('ws')

class RacingServer extends EventEmitter {
  constructor(wss) {
    super()
    this.wss = wss.on('connection', ws => {
      debug('client connected')
      this.ws = ws.on('message', payload => {
        this.onMessage(JSON.parse(payload))
      })
    })
  }

  acknowledge(type) {
    debug('ack %s', type)
    this.ws.send(JSON.stringify({ type: `${type}:ack` }))
  }

  close() {
    return new Promise(resolve => this.wss.close(resolve))
  }

  onMessage({ type, run, err }) {
    debug('rcv %s %s', type, run && run.title)
    this.emit(type, run, err)
  }
}

module.exports = ({ port }) =>
  new Promise(resolve => {
    debug('listening on port: %d', port)
    const wss = new Server({ port })
    resolve(new RacingServer(wss))
  })
<|endoftext|>


<commit_before>import React      from 'react';
import _          from 'underscore';
import $          from 'jquery';
import classNames from 'classnames';

const NavigationComponent = React.createClass({
    propTypes: {
        linksData:  React.PropTypes.array.isRequired,
        activeMain: React.PropTypes.number.isRequired,
        activeSub:  React.PropTypes.number.isRequired,
    },

    getInitialState() {
        return {
            activeMain: 0,
            activeSub: 0,
        };
    },

    componentWillReceiveProps() {
        console.log('componentWillRecieveProps');
        console.log('this.props.activeMain', this.props.activeMain);
        console.log('this.state.activeMain', this.state.activeMain);
        if (this.props.activeMain != this.state.activeMain) {
            console.log('new main');
            this.setState({ activeMain: this.props.activeMain });
        }
        if (this.props.activeSub != this.state.activeSub) {
            console.log('new sub');
            this.setState({ activeSub: this.props.activeSub });
        }
    },

    componentDidUpdate() {
        this._animateSubLinks();
    },

    _animateSubLinks() {
        console.log('activeMain', this.state.activeMain);
        console.log('activeSub', this.state.activeSub);
        console.log('/////////////////////');

        let showingSubrefs = this.props.linksData[this.props.activeMain].length - 1;
        let $animateRef;

        // Loop through the showing sub links
        for (let i = 0; i < showingSubrefs; i++) {
            setTimeout(() => {
                $animateRef = $(this.refs['subLinkRef' + i]);
                $animateRef.addClass('sub-link-is-showing');
            }, (i + 1) * 100);
        };
    },

    _onMainClick(index) {
        if (typeof this.props.onChange === 'function') {
            this.props.onChange('main', index);
        }

        this.setState({
            activeMain: index,
            activeSub: 0,
        });
    },

    _onSubClick(index) {
        if (typeof this.props.onChange === 'function') {
            this.props.onChange('sub', index);
        }

        this.setState({ activeSub: index });
    },

    render() {
        console.log('Render');
        const mainLinks = _(this.props.linksData).map((key, value) => {
            const mainClass = classNames('main-link', {
                'active-link': (value === this.state.activeMain),
            });
            if (value > 0) {
                return
{key[0]}
;
            }
        });

        let subLinks = null;
        _(this.props.linksData).map((key, value) => {
            if (value === this.state.activeMain) {
                subLinks = _(key).map((value, index) => {
                    if (index > 0) {
                        let i = index - 1
                        const subClass = classNames('sub-link', {
                            'active-link': (i === this.state.activeSub),
                        });
                        return
{value}
;
                    }
                });
            }
        });

        return (

                        {mainLinks}

                        {subLinks}

        );
    },
});

export default NavigationComponent;<commit_msg>Fixed re-animation problem with sub links.<commit_after>import React      from 'react';
import _          from 'underscore';
import $          from 'jquery';
import classNames from 'classnames';

const NavigationComponent = React.createClass({
    propTypes: {
        linksData:  React.PropTypes.array.isRequired,
        activeMain: React.PropTypes.number.isRequired,
        activeSub:  React.PropTypes.number.isRequired,
    },

    getInitialState() {
        return {
            sameMain: false,
        };
    },

    componentDidUpdate() {
        if (!this.state.sameMain) {
            this._animateSubLinks();
        }
    },

    _animateSubLinks() {
        console.log('lets animate!');
        let showingSubrefs = this.props.linksData[this.props.activeMain].length - 1;
        let $animateRef;

        // Loop through the showing sub links
        for (let i = 0; i < showingSubrefs; i++) {
            setTimeout(() => {
                $animateRef = $(this.refs['subLinkRef' + i]);
                $animateRef.addClass('sub-link-is-showing');
            }, (i + 1) * 75);
        };
    },

    _onMainClick(index) {
        if (typeof this.props.onChange === 'function') {
            this.props.onChange('main', index);
        }

        this.setState({ sameMain: false });
    },

    _onSubClick(index, target) {

        if (typeof this.props.onChange === 'function') {
            this.props.onChange('sub', index);
        }

        this.setState({ sameMain: true });
    },

    render() {
        const mainLinks = _(this.props.linksData).map((key, value) => {
            const mainClass = classNames('main-link', {
                'active-link': (value === this.props.activeMain),
            });
            if (value > 0) {
                return
{key[0]}
;
            }
        });

        let subLinks;
        _(this.props.linksData).map((key, value) => {
            console.log('re rendering sublinks');
            if (value === this.props.activeMain) {
                subLinks = _(key).map((value, index) => {
                    if (index > 0) {
                        let i = index - 1
                        const subClass = classNames('sub-link', {
                            'active-link': (i === this.props.activeSub),
                            'sub-link-is-showing': this.state.sameMain,
                        });
                        return
{value}
;
                    }
                });
            }
        });

        return (

                        {mainLinks}

                        {subLinks}

        );
    },
});

export default NavigationComponent;<|endoftext|>


<commit_before>import {curry, isNil} from 'ramda'
import React from 'react'
import THREE from 'three'
import {randomMesh} from '../../utils/webGLHelpers'

const clamp = curry((min, max, value) => value < min ? min : value > max ? max : value)

const cameraZ = 16
const fallAwayVelocity = 1.2
const minZ = -160
const returnVelocity = 12

const sideLength = 1
const maxDepth = 3 * sideLength

let currentXYRatios = null
let controlPadElement = null
let renderLoopActive = null
let token = null
let renderer = null
let camera = null
let scene = null
let mouseInputEnabled = false
let controlPadActive = false

const setRendererSize = _ => {
  const rendererSize = window.innerWidth < window.innerHeight
    ? window.innerWidth
    : window.innerHeight * 0.8
  renderer.setSize(rendererSize, rendererSize)
}

const renderLoop = _ => {
  if (!renderLoopActive) return
  window.requestAnimationFrame(renderLoop)
  const controlPadHasNotBeenUsed = isNil(currentXYRatios)
  const {z} = token.position
  if (controlPadHasNotBeenUsed) return
  if (!controlPadActive) {
    if (z > minZ - maxDepth) token.position.z -= fallAwayVelocity
    renderer.render(scene, camera)
    return
  }
  const {xRatio, yRatio} = currentXYRatios
  const xMod = xRatio < 0.5
    ? -(xRatio - 0.5) ** 2
    : (xRatio - 0.5) ** 2
  const yMod = yRatio < 0.5
    ? -(yRatio - 0.5) ** 2
    : (yRatio - 0.5) ** 2
  const rotationBaseAmount = 0.01
  const rotationVelocityComponent = 1.5
  token.rotation.x += rotationBaseAmount + rotationVelocityComponent * xMod
  token.rotation.y += rotationBaseAmount + rotationVelocityComponent * yMod
  token.rotation.z += rotationBaseAmount + rotationVelocityComponent * xMod * yMod
  token.position.x = (xRatio - 0.5) * cameraZ
  token.position.y = (0.5 - yRatio) * cameraZ
  if (z < 0) token.position.z += z > -returnVelocity ? -z : returnVelocity
  renderer.render(scene, camera)
}

const validRatio = clamp(0, 1 - Number.EPSILON)

const calculateXAndYRatio = e => {
  const {top, right, bottom, left} = e.target.getBoundingClientRect()
  const [width, height] = [right - left, bottom - top]
  const {clientX, clientY} = e.changedTouches && e.changedTouches[0] || e
  const [x, y] = [clientX - left, clientY - top]
  return {xRatio: validRatio(x / width), yRatio: validRatio(y / height)}
}

export default class extends React.Component {
  componentDidMount () {
    const {inputStopHandler, inputStartHandler, inputModifyHandler} = this.props
    controlPadElement = this.refs['control-pad']

    const inputCallback = e => {
      mouseInputEnabled = e.type === 'mousedown' ? true : mouseInputEnabled
      if (e instanceof window.MouseEvent && !mouseInputEnabled) return
      currentXYRatios = calculateXAndYRatio(e)
      if (controlPadActive) {
        inputModifyHandler(currentXYRatios)
        return
      }
      controlPadActive = true
      inputStartHandler(currentXYRatios)
    }

    controlPadElement.addEventListener('touchstart', inputCallback)
    controlPadElement.addEventListener('touchmove', inputCallback)
    controlPadElement.addEventListener('mousedown', inputCallback)
    controlPadElement.addEventListener('mousemove', inputCallback)

    const inputEndCallback = _ => {
      controlPadActive = mouseInputEnabled = false
      inputStopHandler()
    }

    controlPadElement.addEventListener('touchend', inputEndCallback)
    controlPadElement.addEventListener('mouseup', inputEndCallback)

    renderLoopActive = true
    scene = new THREE.Scene()
    camera = new THREE.PerspectiveCamera(
      54,
      1,
      cameraZ - maxDepth,
      cameraZ - minZ
    )
    renderer = new THREE.WebGLRenderer({alpha: true, canvas: controlPadElement})
    const directionalLight = new THREE.DirectionalLight(0xffffff)
    token = randomMesh()
    token.position.z = minZ - maxDepth

    directionalLight.position.set(16, 16, 24).normalize()
    scene.add(new THREE.AmbientLight(0x333333))
      .add(directionalLight)
      .add(token)
    camera.position.z = cameraZ

    setRendererSize()
    window.onresize = setRendererSize

    controlPadElement.oncontextmenu = e => e.preventDefault()

    renderLoop()
  }

  componentWillUnmount () {
    renderLoopActive = false
    window.onresize = null
  }

  render () {
    return <canvas {...{
      className: 'control-pad',
      height: 768,
      ref: 'control-pad',
      width: 768,
    }}></canvas>
  }
}
<commit_msg>refactor(control-pad) remove isNil from codebase<commit_after>import {curry} from 'ramda'
import React from 'react'
import THREE from 'three'
import {randomMesh} from '../../utils/webGLHelpers'

const clamp = curry((min, max, value) => value < min ? min : value > max ? max : value)

const cameraZ = 16
const fallAwayVelocity = 1.2
const minZ = -160
const returnVelocity = 12

const sideLength = 1
const maxDepth = 3 * sideLength

let currentXYRatios = null
let controlPadElement = null
let renderLoopActive = null
let token = null
let renderer = null
let camera = null
let scene = null
let mouseInputEnabled = false
let controlPadActive = false

const setRendererSize = _ => {
  const rendererSize = window.innerWidth < window.innerHeight
    ? window.innerWidth
    : window.innerHeight * 0.8
  renderer.setSize(rendererSize, rendererSize)
}

const renderLoop = _ => {
  if (!renderLoopActive) return
  window.requestAnimationFrame(renderLoop)
  const controlPadHasNotBeenUsed = currentXYRatios === null
  const {z} = token.position
  if (controlPadHasNotBeenUsed) return
  if (!controlPadActive) {
    if (z > minZ - maxDepth) token.position.z -= fallAwayVelocity
    renderer.render(scene, camera)
    return
  }
  const {xRatio, yRatio} = currentXYRatios
  const xMod = xRatio < 0.5
    ? -(xRatio - 0.5) ** 2
    : (xRatio - 0.5) ** 2
  const yMod = yRatio < 0.5
    ? -(yRatio - 0.5) ** 2
    : (yRatio - 0.5) ** 2
  const rotationBaseAmount = 0.01
  const rotationVelocityComponent = 1.5
  token.rotation.x += rotationBaseAmount + rotationVelocityComponent * xMod
  token.rotation.y += rotationBaseAmount + rotationVelocityComponent * yMod
  token.rotation.z += rotationBaseAmount + rotationVelocityComponent * xMod * yMod
  token.position.x = (xRatio - 0.5) * cameraZ
  token.position.y = (0.5 - yRatio) * cameraZ
  if (z < 0) token.position.z += z > -returnVelocity ? -z : returnVelocity
  renderer.render(scene, camera)
}

const validRatio = clamp(0, 1 - Number.EPSILON)

const calculateXAndYRatio = e => {
  const {top, right, bottom, left} = e.target.getBoundingClientRect()
  const [width, height] = [right - left, bottom - top]
  const {clientX, clientY} = e.changedTouches && e.changedTouches[0] || e
  const [x, y] = [clientX - left, clientY - top]
  return {xRatio: validRatio(x / width), yRatio: validRatio(y / height)}
}

export default class extends React.Component {
  componentDidMount () {
    const {inputStopHandler, inputStartHandler, inputModifyHandler} = this.props
    controlPadElement = this.refs['control-pad']

    const inputCallback = e => {
      mouseInputEnabled = e.type === 'mousedown' ? true : mouseInputEnabled
      if (e instanceof window.MouseEvent && !mouseInputEnabled) return
      currentXYRatios = calculateXAndYRatio(e)
      if (controlPadActive) {
        inputModifyHandler(currentXYRatios)
        return
      }
      controlPadActive = true
      inputStartHandler(currentXYRatios)
    }

    controlPadElement.addEventListener('touchstart', inputCallback)
    controlPadElement.addEventListener('touchmove', inputCallback)
    controlPadElement.addEventListener('mousedown', inputCallback)
    controlPadElement.addEventListener('mousemove', inputCallback)

    const inputEndCallback = _ => {
      controlPadActive = mouseInputEnabled = false
      inputStopHandler()
    }

    controlPadElement.addEventListener('touchend', inputEndCallback)
    controlPadElement.addEventListener('mouseup', inputEndCallback)

    renderLoopActive = true
    scene = new THREE.Scene()
    camera = new THREE.PerspectiveCamera(
      54,
      1,
      cameraZ - maxDepth,
      cameraZ - minZ
    )
    renderer = new THREE.WebGLRenderer({alpha: true, canvas: controlPadElement})
    const directionalLight = new THREE.DirectionalLight(0xffffff)
    token = randomMesh()
    token.position.z = minZ - maxDepth

    directionalLight.position.set(16, 16, 24).normalize()
    scene.add(new THREE.AmbientLight(0x333333))
      .add(directionalLight)
      .add(token)
    camera.position.z = cameraZ

    setRendererSize()
    window.onresize = setRendererSize

    controlPadElement.oncontextmenu = e => e.preventDefault()

    renderLoop()
  }

  componentWillUnmount () {
    renderLoopActive = false
    window.onresize = null
  }

  render () {
    return <canvas {...{
      className: 'control-pad',
      height: 768,
      ref: 'control-pad',
      width: 768,
    }}></canvas>
  }
}
<|endoftext|>


<commit_before>import { h, Component } from 'preact';
import { skip, take, toArray, Buffer } from '../../utils/iteration-utils';

const STYLE_INNER = 'position: relative;overflow-y: scroll;height: 100%';
const STYLE_CONTENT = 'position: absolute;left: 0;width: 100%';

export default class StreamingList extends Component {

    state = {
        items: [],
        offset: 0,
    }

    /**
     * Handles callbacks from scroll and resize events, we throttle these high frequency
     * events via animation frame callbacks; we enqueue work to update the visible range.
     */
    handleScrollOrResize = () => requestAnimationFrame(this.updateVisibleRange);

    /**
     * Handles renderable frequency callbacks from high frequency source updates and
     * establishes the visible range of items that should be rendered; it does not
     * update the actual items to be rendered as these require asynchronous work, as
     * such we end by updating state and await a component state update pass, if the
     * state change is significant (i.e., it would change the items being rendered)
     * then we trigger async work to build this item list via shouldComponentUpdate.
     */
    updateVisibleRange = () => {

        const { itemHeight, itemGutter, pageSizer } = this.props;

        function getHeightForCount(count) {

            if (count === 0) return 0;

            const itemsHeight = count * itemHeight;
            const gutters = (count - 1) * itemGutter;

            return itemsHeight + gutters;

        }

        const height = this.base.offsetHeight;
        const y1 = this.base.firstElementChild.scrollTop;

        const visibleItems = Math.ceil(height / (itemHeight + itemGutter));
        const itemsPerTile = pageSizer(visibleItems);
        const tileHeight = getHeightForCount(itemsPerTile);

        const y2 = y1 + height;

        // calculate the tile page numbers that match the top and bottom points
        const startPage = Math.floor(y1 / tileHeight);
        const finishPage = Math.floor(y2 / tileHeight);

        this.setState({ startPage, finishPage, itemsPerTile, tileHeight });

    }

    /**
     * Handles triggering a tile update if significant changes have occured to the
     * visible items, if the current state items do not match the incoming state items
     * then the component should update, otherwise it should not; however if changes
     * to the visible range has occurred then async work is started via updateTiles
     */
    shouldComponentUpdate(_, { buffer, items, startPage, finishPage, itemsPerTile, tileHeight }) {

        if (this.state.items !== items) return true;

        // we need to rebuild tiles only if the buffer, tile range or tile size has changed
        const pendingChanges =
            this.state.buffer !== buffer ||
            this.state.startPage !== startPage ||
            this.state.finishPage !== finishPage ||
            this.state.itemsPerTile !== itemsPerTile ||
            this.state.tileHeight !== tileHeight;

        // if we have pending changes then trigger a tile rebuild; this should only
        // happen when resizing, updating tile size, tile factor or when scrolling
        // over a tile boundary
        if (pendingChanges) {
            this.updateTiles(buffer, startPage, finishPage, itemsPerTile);
        }

        return false;

    }

    /**
     * Handles building an array of visible items from source data and a given
     * starting and finishing page; we do this by lazily iterating over the
     * async data source, first skipping n amount of items we don't need,
     * taking the next n items we do need and then finally building an array
     * of items from this subset
     */
    async updateTiles(buffer, startPage, finishPage, itemsPerTile) {

        const start = startPage * itemsPerTile;
        const count = (Math.max(finishPage - startPage, 1) * itemsPerTile) + itemsPerTile;

        const items = await toArray(take(skip(buffer, start), count));

        this.setState({ items });

    }

    /**
     * When our component mounts we want to begin listening to scrolling and resize
     * events so that we can update our subset of visible items when needed. We also
     * create a buffer from the current data source; a buffer is important when
     * iterating our data source as we will need to iterate multiple times as we move
     * around the virtual set of items, the buffer facilitates this by recording
     * all items that it sees being iterated from a given source iterable and then
     * re-iterating them for subsequent iterations
     */
    componentDidMount() {

        const scrollingContainer = this.base.querySelector('[data-scrolling-container]');
        const window = this.base.ownerDocument.defaultView;

        scrollingContainer.addEventListener('scroll', this.handleScrollOrResize, { passive: true });
        window.addEventListener('resize', this.handleScrollOrResize, { passive: true });

        const buffer = new Buffer(this.props.source);

        this.setState({ buffer });
        this.handleScrollOrResize();

    }

    componentWillReceiveProps(newProps) {

        if (newProps.source === this.props.source) return;

        const buffer = new Buffer(newProps.source);

        this.setState({ buffer });

        this.handleScrollOrResize(); // todo: review

    }

    componentWillUnmount() {

        const scrollingContainer = this.base.querySelector('[data-scrolling-container]');
        const window = this.base.ownerDocument.defaultView;

        scrollingContainer.removeEventListener('scroll', this.handleScrollOrResize);
        window.removeEventListener('resize', this.handleScrollOrResize);

    }

    // devnote, we bring our known props into scope so they are excluded
    // for 'props', which we then bind to our top level container

    // eslint-disable-next-line no-unused-vars
    render({ source, total, overscan, itemTemplate, itemHeight, itemGutter, ...props }, { buffer, items, startPage, itemsPerTile, tileHeight }) {

        const offset = startPage * tileHeight;
        const maximumHeight = buffer == null ? 0 : buffer.pending.length * itemHeight;

        const startIndex = startPage * itemsPerTile;

        return (

                <div data-scrolling-container style={`${STYLE_INNER}`}>
                    <div style={`${STYLE_CONTENT};transform: translateY(${offset}px);min-height: ${maximumHeight - offset}px`}>
                        {items.map((item, i) => (
                            <div key={startIndex + i} style={`height: ${itemHeight}px;margin-top: ${itemGutter}px`}>
                                {itemTemplate(item, startIndex + i)}

                        ))}

        );
    }
}
<commit_msg>update streaming list to avoid using state apart from render changing actions<commit_after>import { h, Component } from 'preact';
import { skip, take, toArray, Buffer } from '../../utils/iteration-utils';

const STYLE_INNER = 'position: relative;overflow-y: scroll;height: 100%';
const STYLE_CONTENT = 'position: absolute;left: 0;width: 100%';

export default class StreamingList extends Component {

    startPage;
    finishPage;
    itemsPerTile;
    tileHeight;
    buffer;

    state = {
        items: [],
    }

    /**
     * Handles callbacks from scroll and resize events, we throttle these high frequency
     * events via animation frame callbacks; we enqueue work to update the visible range.
     */
    handleScrollOrResize = () => requestAnimationFrame(this.updateVisibleRange);

    /**
     * Handles renderable frequency callbacks from high frequency source updates and
     * establishes the visible range of items that should be rendered; it does not
     * update the actual items to be rendered as these require asynchronous work, as
     * such we end by updating state and await a component state update pass, if the
     * state change is significant (i.e., it would change the items being rendered)
     * then we trigger async work to build this item list via shouldComponentUpdate.
     */
    updateVisibleRange = () => {

        const { itemHeight, itemGutter, pageSizer } = this.props;

        function getHeightForCount(count) {

            if (count === 0) return 0;

            const itemsHeight = count * itemHeight;
            const gutters = (count - 1) * itemGutter;

            return itemsHeight + gutters;

        }

        const height = this.base.offsetHeight;
        const y1 = this.base.firstElementChild.scrollTop;

        const visibleItems = Math.ceil(height / (itemHeight + itemGutter));
        const itemsPerTile = pageSizer(visibleItems);
        const tileHeight = getHeightForCount(itemsPerTile);

        const y2 = y1 + height;

        // calculate the tile page numbers that match the top and bottom points
        const startPage = Math.floor(y1 / tileHeight);
        const finishPage = Math.floor(y2 / tileHeight);

        const hasPendingChanges =
            this.startPage !== startPage ||
            this.finishPage !== finishPage ||
            this.itemsPerTile !== itemsPerTile ||
            this.tileHeight !== tileHeight;

        this.startPage = startPage;
        this.finishPage = finishPage;
        this.itemsPerTile = itemsPerTile;
        this.tileHeight = tileHeight;

        // we need to rebuild tiles only if the buffer, tile range or tile size has changed
        // if we have pending changes then trigger a tile rebuild; this should only
        // happen when resizing, updating tile size, tile factor or when scrolling
        // over a tile boundary
        if (hasPendingChanges) {
            this.updateTiles();
        }

    }

    /**
     * Handles triggering a tile update if significant changes have occured to the
     * visible items, if the current state items do not match the incoming state items
     * then the component should update, otherwise it should not; however if changes
     * to the visible range has occurred then async work is started via updateTiles
     */
    shouldComponentUpdate(_, { items }) {
        return this.state.items !== items;
    }

    /**
     * Handles building an array of visible items from source data and a given
     * starting and finishing page; we do this by lazily iterating over the
     * async data source, first skipping n amount of items we don't need,
     * taking the next n items we do need and then finally building an array
     * of items from this subset
     */
    async updateTiles() {

        const { buffer, startPage, finishPage, itemsPerTile } = this;

        const start = startPage * itemsPerTile;
        const count = (Math.max(finishPage - startPage, 1) * itemsPerTile) + itemsPerTile;

        const items = await toArray(take(skip(buffer, start), count));

        this.setState({ items });

    }

    /**
     * When our component mounts we want to begin listening to scrolling and resize
     * events so that we can update our subset of visible items when needed. We also
     * create a buffer from the current data source; a buffer is important when
     * iterating our data source as we will need to iterate multiple times as we move
     * around the virtual set of items, the buffer facilitates this by recording
     * all items that it sees being iterated from a given source iterable and then
     * re-iterating them for subsequent iterations
     */
    componentDidMount() {

        const scrollingContainer = this.base.querySelector('[data-scrolling-container]');
        const window = this.base.ownerDocument.defaultView;

        scrollingContainer.addEventListener('scroll', this.handleScrollOrResize, { passive: true });
        window.addEventListener('resize', this.handleScrollOrResize, { passive: true });

        this.buffer = new Buffer(this.props.source);

        this.handleScrollOrResize(); // todo: review the name of this method

    }

    componentWillReceiveProps({ source }) {

        if (source === this.props.source) return;

        this.buffer = new Buffer(source);

        this.updateTiles();

    }

    componentWillUnmount() {

        const scrollingContainer = this.base.querySelector('[data-scrolling-container]');
        const window = this.base.ownerDocument.defaultView;

        scrollingContainer.removeEventListener('scroll', this.handleScrollOrResize);
        window.removeEventListener('resize', this.handleScrollOrResize);

    }

    // devnote, we bring our known props into scope so they are excluded
    // for 'props', which we then bind to our top level container

    // eslint-disable-next-line no-unused-vars
    render({ source, total, overscan, itemTemplate, itemHeight, itemGutter, ...props }, { buffer, items, startPage, itemsPerTile, tileHeight }) {

        const offset = startPage * tileHeight;
        const maximumHeight = buffer == null ? 0 : buffer.pending.length * itemHeight;

        const startIndex = startPage * itemsPerTile;

        return (

                <div data-scrolling-container style={`${STYLE_INNER}`}>
                    <div style={`${STYLE_CONTENT};transform: translateY(${offset}px);min-height: ${maximumHeight - offset}px`}>
                        {items.map((item, i) => (
                            <div key={startIndex + i} style={`height: ${itemHeight}px;margin-top: ${itemGutter}px`}>
                                {itemTemplate(item, startIndex + i)}

                        ))}

        );
    }
}
<|endoftext|>


<commit_before>/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer';
import { _t } from '../../../languageHandler';
import classNames from 'classnames';
import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore";

module.exports = React.createClass({
    displayName: 'AuxPanel',

    propTypes: {
        // js-sdk room object
        room: PropTypes.object.isRequired,
        userId: PropTypes.string.isRequired,
        showApps: PropTypes.bool, // Render apps
        hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)

        // Conference Handler implementation
        conferenceHandler: PropTypes.object,

        // set to true to show the file drop target
        draggingFile: PropTypes.bool,

        // set to true to show the 'active conf call' banner
        displayConfCallNotification: PropTypes.bool,

        // maxHeight attribute for the aux panel and the video
        // therein
        maxHeight: PropTypes.number,

        // a callback which is called when the content of the aux panel changes
        // content in a way that is likely to make it change size.
        onResize: PropTypes.func,
        fullHeight: PropTypes.bool,
    },

    defaultProps: {
        showApps: true,
        hideAppsDrawer: false,
    },

    getInitialState: function() {
        return { counters: this._computeCounters() };
    },

    componentDidMount: function() {
        const cli = MatrixClientPeg.get();
        cli.on("RoomState.events", this._rateLimitedUpdate);
    },

    componentWillUnmount: function() {
        const cli = MatrixClientPeg.get();
        if (cli) {
            cli.removeListener("RoomState.events", this._rateLimitedUpdate);
        }
    },

    shouldComponentUpdate: function(nextProps, nextState) {
        return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
                !ObjectUtils.shallowEqual(this.state, nextState));
    },

    componentDidUpdate: function(prevProps, prevState) {
        // most changes are likely to cause a resize
        if (this.props.onResize) {
            this.props.onResize();
        }
    },

    onConferenceNotificationClick: function(ev, type) {
        dis.dispatch({
            action: 'place_call',
            type: type,
            room_id: this.props.room.roomId,
        });
        ev.stopPropagation();
        ev.preventDefault();
    },

    _rateLimitedUpdate: new RateLimitedFunc(function() {
        if (SettingsStore.isFeatureEnabled("feature_state_counters")) {
            this.setState({counters: this._computeCounters()});
        }
    }, 500),

    _computeCounters: function() {
        let counters = [];

        if (this.props.room && SettingsStore.isFeatureEnabled("feature_state_counters")) {
            const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
            stateEvs.sort((a, b) => {
                return a.getStateKey()  {
                const title = ev.getContent().title;
                const value = ev.getContent().value;
                const link = ev.getContent().link;
                const severity = ev.getContent().severity || "normal";
                const stateKey = ev.getStateKey();

                // We want a non-empty title but can accept falsey values (e.g.
                // zero)
                if (title && value !== undefined) {
                    counters.push({
                        "title": title,
                        "value": value,
                        "link": link,
                        "severity": severity,
                        "stateKey": stateKey
                    })
                }
            });
        }

        return counters;
    },

    render: function() {
        const CallView = sdk.getComponent("voip.CallView");
        const TintableSvg = sdk.getComponent("elements.TintableSvg");

        let fileDropTarget = null;
        if (this.props.draggingFile) {
            fileDropTarget = (

                        <TintableSvg src={require("../../../../res/img/upload-big.svg")} width="45" height="59" />

                        { _t("Drop file here to upload") }

            );
        }

        let conferenceCallNotification = null;
        if (this.props.displayConfCallNotification) {
            let supportedText = '';
            let joinNode;
            if (!MatrixClientPeg.get().supportsVoip()) {
                supportedText = _t(" (unsupported)");
            } else {
                joinNode = (
                    { _t(
                        "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
                        {},
                        {
                            'voiceText': (sub) => { this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub },
                            'videoText': (sub) => { this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub },
                        },
                    ) }
                );
            }
            // XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
            // but there are translations for this in the languages we do have so I'm leaving it for now.
            conferenceCallNotification = (

                    { _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }

                    { joinNode }

            );
        }

        const callView = (
            <CallView ref="callView" room={this.props.room}
                ConferenceHandler={this.props.conferenceHandler}
                onResize={this.props.onResize}
                maxVideoHeight={this.props.maxHeight}
            />
        );

        const appsDrawer = <AppsDrawer ref="appsDrawer"
            room={this.props.room}
            userId={this.props.userId}
            maxHeight={this.props.maxHeight}
            showApps={this.props.showApps}
            hide={this.props.hideAppsDrawer}
        />;

        let stateViews = null;
        if (this.state.counters && SettingsStore.isFeatureEnabled("feature_state_counters")) {
            let counters = [];

            this.state.counters.forEach((counter, idx) => {
                const title = counter.title;
                const value = counter.value;
                const link = counter.link;
                const severity = counter.severity;
                const stateKey = counter.stateKey;

                if (title && value && severity) {
                    let span = { title }: { value }

                    if (link) {
                        span = (

                                { span }

                        );
                    }

                    span = (

                            {span}

                    );

                    counters.push(span);
                    counters.push(
                         ─
                    );
                }
            });

            if (counters.length > 0) {
                counters.pop(); // remove last deliminator
                stateViews = (

                        { counters }

                );
            }
        }

        const classes = classNames({
            "mx_RoomView_auxPanel": true,
            "mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
        });
        const style = {};
        if (!this.props.fullHeight) {
            style.maxHeight = this.props.maxHeight;
        }

        return (

                { stateViews }
                { appsDrawer }
                { fileDropTarget }
                { callView }
                { conferenceCallNotification }
                { this.props.children }

        );
    },
});
<commit_msg>Fix simple header counters to correctly handle zero, take two<commit_after>/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer';
import { _t } from '../../../languageHandler';
import classNames from 'classnames';
import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore";

module.exports = React.createClass({
    displayName: 'AuxPanel',

    propTypes: {
        // js-sdk room object
        room: PropTypes.object.isRequired,
        userId: PropTypes.string.isRequired,
        showApps: PropTypes.bool, // Render apps
        hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)

        // Conference Handler implementation
        conferenceHandler: PropTypes.object,

        // set to true to show the file drop target
        draggingFile: PropTypes.bool,

        // set to true to show the 'active conf call' banner
        displayConfCallNotification: PropTypes.bool,

        // maxHeight attribute for the aux panel and the video
        // therein
        maxHeight: PropTypes.number,

        // a callback which is called when the content of the aux panel changes
        // content in a way that is likely to make it change size.
        onResize: PropTypes.func,
        fullHeight: PropTypes.bool,
    },

    defaultProps: {
        showApps: true,
        hideAppsDrawer: false,
    },

    getInitialState: function() {
        return { counters: this._computeCounters() };
    },

    componentDidMount: function() {
        const cli = MatrixClientPeg.get();
        cli.on("RoomState.events", this._rateLimitedUpdate);
    },

    componentWillUnmount: function() {
        const cli = MatrixClientPeg.get();
        if (cli) {
            cli.removeListener("RoomState.events", this._rateLimitedUpdate);
        }
    },

    shouldComponentUpdate: function(nextProps, nextState) {
        return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
                !ObjectUtils.shallowEqual(this.state, nextState));
    },

    componentDidUpdate: function(prevProps, prevState) {
        // most changes are likely to cause a resize
        if (this.props.onResize) {
            this.props.onResize();
        }
    },

    onConferenceNotificationClick: function(ev, type) {
        dis.dispatch({
            action: 'place_call',
            type: type,
            room_id: this.props.room.roomId,
        });
        ev.stopPropagation();
        ev.preventDefault();
    },

    _rateLimitedUpdate: new RateLimitedFunc(function() {
        if (SettingsStore.isFeatureEnabled("feature_state_counters")) {
            this.setState({counters: this._computeCounters()});
        }
    }, 500),

    _computeCounters: function() {
        let counters = [];

        if (this.props.room && SettingsStore.isFeatureEnabled("feature_state_counters")) {
            const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
            stateEvs.sort((a, b) => {
                return a.getStateKey()  {
                const title = ev.getContent().title;
                const value = ev.getContent().value;
                const link = ev.getContent().link;
                const severity = ev.getContent().severity || "normal";
                const stateKey = ev.getStateKey();

                // We want a non-empty title but can accept falsey values (e.g.
                // zero)
                if (title && value !== undefined) {
                    counters.push({
                        "title": title,
                        "value": value,
                        "link": link,
                        "severity": severity,
                        "stateKey": stateKey
                    })
                }
            });
        }

        return counters;
    },

    render: function() {
        const CallView = sdk.getComponent("voip.CallView");
        const TintableSvg = sdk.getComponent("elements.TintableSvg");

        let fileDropTarget = null;
        if (this.props.draggingFile) {
            fileDropTarget = (

                        <TintableSvg src={require("../../../../res/img/upload-big.svg")} width="45" height="59" />

                        { _t("Drop file here to upload") }

            );
        }

        let conferenceCallNotification = null;
        if (this.props.displayConfCallNotification) {
            let supportedText = '';
            let joinNode;
            if (!MatrixClientPeg.get().supportsVoip()) {
                supportedText = _t(" (unsupported)");
            } else {
                joinNode = (
                    { _t(
                        "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
                        {},
                        {
                            'voiceText': (sub) => { this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub },
                            'videoText': (sub) => { this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub },
                        },
                    ) }
                );
            }
            // XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
            // but there are translations for this in the languages we do have so I'm leaving it for now.
            conferenceCallNotification = (

                    { _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }

                    { joinNode }

            );
        }

        const callView = (
            <CallView ref="callView" room={this.props.room}
                ConferenceHandler={this.props.conferenceHandler}
                onResize={this.props.onResize}
                maxVideoHeight={this.props.maxHeight}
            />
        );

        const appsDrawer = <AppsDrawer ref="appsDrawer"
            room={this.props.room}
            userId={this.props.userId}
            maxHeight={this.props.maxHeight}
            showApps={this.props.showApps}
            hide={this.props.hideAppsDrawer}
        />;

        let stateViews = null;
        if (this.state.counters && SettingsStore.isFeatureEnabled("feature_state_counters")) {
            let counters = [];

            this.state.counters.forEach((counter, idx) => {
                const title = counter.title;
                const value = counter.value;
                const link = counter.link;
                const severity = counter.severity;
                const stateKey = counter.stateKey;

                let span = { title }: { value }

                if (link) {
                    span = (

                            { span }

                    );
                }

                span = (

                        {span}

                );

                counters.push(span);
                counters.push(
                     ─
                );
            });

            if (counters.length > 0) {
                counters.pop(); // remove last deliminator
                stateViews = (

                        { counters }

                );
            }
        }

        const classes = classNames({
            "mx_RoomView_auxPanel": true,
            "mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
        });
        const style = {};
        if (!this.props.fullHeight) {
            style.maxHeight = this.props.maxHeight;
        }

        return (

                { stateViews }
                { appsDrawer }
                { fileDropTarget }
                { callView }
                { conferenceCallNotification }
                { this.props.children }

        );
    },
});
<|endoftext|>