<commit_before>import { storiesOf } from '@storybook/html';
import { withHits } from '../.storybook/decorators';

storiesOf('ClearRefinements', module)
  .add(
    'default',
    withHits(
      ({ search, container, instantsearch }) => {
        search.addWidget(
          instantsearch.widgets.clearRefinements({
            container,
          })
        );
      },
      {
        searchParameters: {
          disjunctiveFacetsRefinements: { brand: ['Apple'] },
          disjunctiveFacets: ['brand'],
        },
      }
    )
  )
  .add(
    'with query only (via includedAttributes)',
    withHits(({ search, container, instantsearch }) => {
      search.addWidget(
        instantsearch.widgets.clearRefinements({
          container,
          includedAttributes: ['query'],
          templates: {
            resetLabel: 'Clear query',
          },
        })
      );
    })
  )
  .add(
    'with query only (via excludedAttributes)',
    withHits(({ search, container, instantsearch }) => {
      search.addWidget(
        instantsearch.widgets.clearRefinements({
          container,
          excludedAttributes: [],
          templates: {
            resetLabel: 'Clear refinements and query',
          },
        })
      );
    })
  )
  .add(
    'with refinements and query',
    withHits(
      ({ search, container, instantsearch }) => {
        const clearRefinementsContainer = document.createElement('div');
        container.appendChild(clearRefinementsContainer);
        const refinementListContainer = document.createElement('div');
        container.appendChild(refinementListContainer);
        const numericMenuContainer = document.createElement('div');
        container.appendChild(numericMenuContainer);

        search.addWidget(
          instantsearch.widgets.clearRefinements({
            container: clearRefinementsContainer,
            excludedAttributes: [],
            templates: {
              resetLabel: 'Clear refinements and query',
            },
          })
        );

        search.addWidget(
          instantsearch.widgets.refinementList({
            container: refinementListContainer,
            attribute: 'brand',
          })
        );

        search.addWidget(
          instantsearch.widgets.numericMenu({
            container: numericMenuContainer,
            attribute: 'price',
            items: [
              { label: 'All' },
              { end: 10, label: '≤ $10' },
              { start: 10, end: 100, label: '$10–$100' },
              { start: 100, end: 500, label: '$100–$500' },
              { start: 500, label: '≥ $500' },
            ],
          })
        );
      },
      {
        searchParameters: {
          disjunctiveFacetsRefinements: { brand: ['Apple'] },
          disjunctiveFacets: ['brand'],
        },
      }
    )
  )
  .add(
    'with nothing to clear',
    withHits(({ search, container, instantsearch }) => {
      search.addWidget(
        instantsearch.widgets.clearRefinements({
          container,
        })
      );
    })
  )
  .add(
    'with brands excluded (via transformItems)',
    withHits(
      ({ search, container, instantsearch }) => {
        const clearRefinementsContainer = document.createElement('div');
        container.appendChild(clearRefinementsContainer);
        const refinementListContainer = document.createElement('div');
        container.appendChild(refinementListContainer);
        const numericMenuContainer = document.createElement('div');
        container.appendChild(numericMenuContainer);

        search.addWidget(
          instantsearch.widgets.clearRefinements({
            container: clearRefinementsContainer,
            excludedAttributes: [],
            transformItems: items =>
              items.filter(attribute => attribute !== 'brand'),
          })
        );

        search.addWidget(
          instantsearch.widgets.refinementList({
            container: refinementListContainer,
            attribute: 'brand',
          })
        );

        search.addWidget(
          instantsearch.widgets.numericMenu({
            container: numericMenuContainer,
            attribute: 'price',
            items: [
              { label: 'All' },
              { end: 10, label: '≤ $10' },
              { start: 10, end: 100, label: '$10–$100' },
              { start: 100, end: 500, label: '$100–$500' },
              { start: 500, label: '≥ $500' },
            ],
          })
        );
      },
      {
        searchParameters: {
          disjunctiveFacetsRefinements: { brand: ['Apple'] },
          disjunctiveFacets: ['brand'],
        },
      }
    )
  );
<commit_msg>refactor(clearRefinements): update stories (#3780)<commit_after>import { storiesOf } from '@storybook/html';
import { withHits } from '../.storybook/decorators';

storiesOf('ClearRefinements', module)
  .add(
    'default',
    withHits(
      ({ search, container, instantsearch }) => {
        search.addWidget(
          instantsearch.widgets.clearRefinements({
            container,
          })
        );
      },
      {
        searchParameters: {
          disjunctiveFacetsRefinements: { brand: ['Apple'] },
          disjunctiveFacets: ['brand'],
        },
      }
    )
  )
  .add(
    'with query only (via includedAttributes)',
    withHits(({ search, container, instantsearch }) => {
      search.addWidget(
        instantsearch.widgets.clearRefinements({
          container,
          includedAttributes: ['query'],
          templates: {
            resetLabel: 'Clear query',
          },
        })
      );
    })
  )
  .add(
    'with refinements and query (via excludedAttributes)',
    withHits(
      ({ search, container, instantsearch }) => {
        search.addWidget(
          instantsearch.widgets.clearRefinements({
            container,
            excludedAttributes: [],
            templates: {
              resetLabel: 'Clear refinements and query',
            },
          })
        );
      },
      {
        searchParameters: {
          disjunctiveFacetsRefinements: { brand: ['Apple'] },
          disjunctiveFacets: ['brand'],
        },
      }
    )
  )
  .add(
    'with brands excluded (via transformItems)',
    withHits(
      ({ search, container, instantsearch }) => {
        const clearRefinementsContainer = document.createElement('div');
        container.appendChild(clearRefinementsContainer);

        search.addWidget(
          instantsearch.widgets.clearRefinements({
            container: clearRefinementsContainer,
            excludedAttributes: [],
            transformItems: items =>
              items.filter(attribute => attribute !== 'brand'),
          })
        );
      },
      {
        searchParameters: {
          disjunctiveFacetsRefinements: { brand: ['Apple'] },
          disjunctiveFacets: ['brand'],
        },
      }
    )
  );
<|endoftext|>


<commit_before>/* @flow */
import React, { PureComponent } from 'react';
import { View } from 'react-native';

import type { Actions, Stream, Subscription } from '../types';
import connectWithActions from '../connectWithActions';
import { OptionRow, Screen, ZulipButton, OptionDivider } from '../common';
import { getIsAdmin, getStreams, getSubscriptions } from '../selectors';
import { NULL_STREAM, NULL_SUBSCRIPTION } from '../nullObjects';
import StreamCard from './StreamCard';

type Props = {
  actions: Actions,
  isAdmin: boolean,
  navigation: Object,
  streams: Stream[],
  subscriptions: Subscription[],
};

class StreamScreen extends PureComponent<Props> {
  props: Props;

  static contextTypes = {
    styles: () => null,
  };

  handleTogglePinStream = (newValue: boolean) => {
    const { actions, navigation } = this.props;
    const { streamId } = navigation.state.params;
    actions.doTogglePinStream(streamId, newValue);
  };

  handleToggleMuteStream = (newValue: boolean) => {
    const { actions, navigation } = this.props;
    const { streamId } = navigation.state.params;
    actions.doToggleMuteStream(streamId, newValue);
  };

  handleTopics = () => {
    const { actions, navigation } = this.props;
    actions.navigateToTopicList(navigation.state.params.streamId);
  };

  handleEdit = () => {
    const { actions, navigation } = this.props;
    actions.navigateToEditStream(navigation.state.params.streamId);
  };

  toggleStreamPushNotification = () => {
    const { subscriptions, navigation, actions } = this.props;
    const { streamId } = navigation.state.params;
    const subscription = subscriptions.find(x => x.stream_id === streamId) || NULL_SUBSCRIPTION;
    actions.toggleStreamNotification(streamId, !subscription.push_notifications);
  };

  render() {
    const { isAdmin, streams, subscriptions, navigation } = this.props;
    const { streamId } = navigation.state.params;
    const stream = streams.find(x => x.stream_id === streamId) || NULL_STREAM;
    const subscription = subscriptions.find(x => x.stream_id === streamId) || NULL_SUBSCRIPTION;
    const { styles } = this.context;

    return (
      <Screen title="Stream">
        <StreamCard stream={stream} subscription={subscription} />
        <OptionRow
          label="Pinned"
          defaultValue={subscription.pin_to_top}
          onValueChange={this.handleTogglePinStream}
        />
        <OptionRow
          label="Muted"
          defaultValue={stream.in_home_view === false}
          onValueChange={this.handleToggleMuteStream}
        />
        <OptionRow
          label="Notifications"
          defaultValue={subscription.push_notifications}
          onValueChange={this.toggleStreamPushNotification}
          customStyle={this.context.styles.backgroundColor}
        />
        <OptionDivider />
        <View style={styles.padding}>
          <ZulipButton text="Topics" onPress={this.handleTopics} />
          {isAdmin && (
            <ZulipButton style={styles.marginTop} text="Edit" onPress={this.handleEdit} />
          )}
        </View>
      </Screen>
    );
  }
}

export default connectWithActions(state => ({
  isAdmin: getIsAdmin(state),
  streams: getStreams(state),
  subscriptions: getSubscriptions(state),
}))(StreamScreen);
<commit_msg>fix: Show stream is muted correctly<commit_after>/* @flow */
import React, { PureComponent } from 'react';
import { View } from 'react-native';

import type { Actions, Stream, Subscription } from '../types';
import connectWithActions from '../connectWithActions';
import { OptionRow, Screen, ZulipButton, OptionDivider } from '../common';
import { getIsAdmin, getStreams, getSubscriptions } from '../selectors';
import { NULL_STREAM, NULL_SUBSCRIPTION } from '../nullObjects';
import StreamCard from './StreamCard';

type Props = {
  actions: Actions,
  isAdmin: boolean,
  navigation: Object,
  streams: Stream[],
  subscriptions: Subscription[],
};

class StreamScreen extends PureComponent<Props> {
  props: Props;

  static contextTypes = {
    styles: () => null,
  };

  handleTogglePinStream = (newValue: boolean) => {
    const { actions, navigation } = this.props;
    const { streamId } = navigation.state.params;
    actions.doTogglePinStream(streamId, newValue);
  };

  handleToggleMuteStream = (newValue: boolean) => {
    const { actions, navigation } = this.props;
    const { streamId } = navigation.state.params;
    actions.doToggleMuteStream(streamId, newValue);
  };

  handleTopics = () => {
    const { actions, navigation } = this.props;
    actions.navigateToTopicList(navigation.state.params.streamId);
  };

  handleEdit = () => {
    const { actions, navigation } = this.props;
    actions.navigateToEditStream(navigation.state.params.streamId);
  };

  toggleStreamPushNotification = () => {
    const { subscriptions, navigation, actions } = this.props;
    const { streamId } = navigation.state.params;
    const subscription = subscriptions.find(x => x.stream_id === streamId) || NULL_SUBSCRIPTION;
    actions.toggleStreamNotification(streamId, !subscription.push_notifications);
  };

  render() {
    const { isAdmin, streams, subscriptions, navigation } = this.props;
    const { streamId } = navigation.state.params;
    const stream = streams.find(x => x.stream_id === streamId) || NULL_STREAM;
    const subscription = subscriptions.find(x => x.stream_id === streamId) || NULL_SUBSCRIPTION;
    const { styles } = this.context;

    return (
      <Screen title="Stream">
        <StreamCard stream={stream} subscription={subscription} />
        <OptionRow
          label="Pinned"
          defaultValue={subscription.pin_to_top}
          onValueChange={this.handleTogglePinStream}
        />
        <OptionRow
          label="Muted"
          defaultValue={subscription.in_home_view === false}
          onValueChange={this.handleToggleMuteStream}
        />
        <OptionRow
          label="Notifications"
          defaultValue={subscription.push_notifications}
          onValueChange={this.toggleStreamPushNotification}
          customStyle={this.context.styles.backgroundColor}
        />
        <OptionDivider />
        <View style={styles.padding}>
          <ZulipButton text="Topics" onPress={this.handleTopics} />
          {isAdmin && (
            <ZulipButton style={styles.marginTop} text="Edit" onPress={this.handleEdit} />
          )}
        </View>
      </Screen>
    );
  }
}

export default connectWithActions(state => ({
  isAdmin: getIsAdmin(state),
  streams: getStreams(state),
  subscriptions: getSubscriptions(state),
}))(StreamScreen);
<|endoftext|>


<commit_before>const Feed = require('./db/Feed')
const DeliveryRecord = require('../models/DeliveryRecord.js')
const createLogger = require('../util/logger/create')
const configuration = require('../config.js')
const Supporter = require('./db/Supporter')
const GeneralStats = require('../models/GeneralStats.js')
const fetch = require('node-fetch')
const { Webhook } = require('discord.js')

/**
 * @typedef {Object} ArticleDetails
 * @property {Object<string, any>} newArticle
 * @property {import('./ArticleMessage')} articleMessage
*/

/**
 * Staggers the delivery of new articles
 */
class ArticleQueue {
  constructor (client) {
    /**
     * @type {ArticleDetails[]}
     */
    this.queue = []
    /**
     * @type {import('discord.js').Client}
     */
    this.client = client
    this.log = createLogger(this.client.shard.ids[0])
    const config = configuration.get()
    const dequeueRate = config.feeds.articleDequeueRate
    const dequeueAmount = dequeueRate <= 1 ? 1 : dequeueRate
    const intervalSeconds = dequeueRate <= 1 ? 1 / dequeueRate : 1
    setInterval(() => {
      this.dequeue(dequeueAmount)
    }, 1000 * intervalSeconds)
  }

  /**
   * @param {ArticleDetails} articleData
   * @param {string} message
   */
  _logDebug (articleData, message) {
    const { article, feedObject } = articleData.newArticle
    this.log.debug(`ArticleQueue ${message} (${article._id}, feed: ${feedObject._id})`)
  }

  /**
   * Dequeue a certain amount of articles from the queue
   * and send them in order
   *
   * @param {number} dequeueAmount
   */
  async dequeue (dequeueAmount) {
    // async must be used within the loop to main the order
    // in which articles are sent
    for (let i = 0; i < dequeueAmount; ++i) {
      if (this.queue.length === 0) {
        continue
      }
      const articleData = this.queue.shift()
      this._logDebug(articleData, 'Dequeuing')
      await this.send(articleData)
    }
  }

  /**
   * Send an article message
   *
   * @param {ArticleDetails} articleData
   */
  async send (articleData) {
    const deliveryServiceURL = configuration.get().deliveryServiceURL
    try {
      if (deliveryServiceURL) {
        this._logDebug(articleData, 'Sending by service')
        await this.sendByService(articleData, deliveryServiceURL)
      } else {
        this._logDebug(articleData, 'Sending by client')
        await articleData.articleMessage.send(this.client)
      }
      await this.recordSuccess(articleData.newArticle)
      ArticleQueue.sent++
    } catch (err) {
      await this.recordFailure(articleData.newArticle, err.message)
    }
  }

  /**
   * Send an article via the external service
   *
   * @param {ArticleDetails} articleData
   * @param {string} serviceURL
   */
  async sendByService (articleData, serviceURL) {
    const articleMessage = articleData.articleMessage
    const medium = await articleMessage.getMedium(this.client)
    if (!medium) {
      console.log('missing medium')
      throw new Error('Missing medium to send article via service')
    }
    const apiPayload = articleMessage.createAPIPayload(medium)
    const apiRoute = medium instanceof Webhook ? `/webhooks/${medium.id}/${medium.token}` : `/channels/${medium.id}/messages`
    const res = await fetch(`${serviceURL}/api/request`, {
      method: 'POST',
      body: JSON.stringify({
        method: 'POST',
        url: `https://discord.com/api${apiRoute}`,
        body: apiPayload
      }),
      headers: {
        // Authorization: `Bot ${this.client.token}`,
        'Content-Type': 'application/json',
        Accept: 'application/json'
      }
    })
    if (res.ok) {
      this._logDebug(articleData, 'Successfully sent via service')
      return
    }
    // Bad status code from service
    let json
    try {
      // The service should always send a message in the response
      json = await res.json()
      this.log.error(`Bad status code ${res.status} from service (${json.message})`)
    } catch (err) {
      this.log.error(err, `Bad status code ${res.status} from service`)
      throw new Error(`Bad status code (${res.status}) from service`)
    }
    if (json) {
      throw new Error(json.message)
    }
  }

  /**
   * Enqueue an article to be dequeued and sent at
   * the rate set in config
   *
   * @param {Object<string, any>} newArticle
   * @param {import('./ArticleMessage')} articleMessage
   */
  enqueue (newArticle, articleMessage) {
    this.queue.push({
      newArticle,
      articleMessage
    })
  }

  async recordFailure (newArticle, errorMessage) {
    if (!Feed.isMongoDatabase) {
      return
    }
    const { article, feedObject } = newArticle
    const channel = feedObject.channel
    const data = {
      articleID: article._id,
      feedURL: feedObject.url,
      channel,
      delivered: false,
      comment: errorMessage
    }
    this.log.debug({
      data
    }, 'Recording delivery record failure')
    try {
      const record = new DeliveryRecord.Model(data)
      await record.save()
    } catch (err) {
      this.log.error(err, `Failed to record article ${article._id} delivery failure in channel ${channel} (error: ${errorMessage}) in ArticleQueue`)
    }
  }

  async recordSuccess (newArticle) {
    if (!Feed.isMongoDatabase) {
      return
    }
    const { article, feedObject } = newArticle
    const channel = feedObject.channel
    const data = {
      articleID: article._id,
      feedURL: feedObject.url,
      delivered: true,
      channel
    }
    this.log.debug({
      data
    }, 'Recording delivery record success')
    try {
      const record = new DeliveryRecord.Model(data)
      await record.save()
    } catch (err) {
      this.log.error(err, `Failed to record article ${article._id} delivery success in channel ${channel} in ArticleQueue`)
    }
  }

  static async updateArticlesSent () {
    if (this.sent === 0 || !Supporter.isMongoDatabase) {
      return
    }
    /**
     * @type {import('mongoose').Document}
     */
    const found = await GeneralStats.Model.findById(GeneralStats.TYPES.ARTICLES_SENT)
    if (!found) {
      const stat = new GeneralStats.Model({
        _id: GeneralStats.TYPES.ARTICLES_SENT,
        data: ArticleQueue.sent
      })
      await stat.save()
    } else {
      await found.updateOne({
        $inc: {
          data: ArticleQueue.sent
        }
      })
    }
    ArticleQueue.sent = 0
  }
}

ArticleQueue.sent = 0

if (process.env.NODE_ENV !== 'test') {
  ArticleQueue.timer = setInterval(async () => {
    try {
      await ArticleQueue.updateArticlesSent()
    } catch (err) {
      const log = createLogger()
      log.error(err, 'Failed to update article stats')
    }
  }, 10000)
}

module.exports = ArticleQueue
<commit_msg>Fix missing Authorization header in service fetch<commit_after>const Feed = require('./db/Feed')
const DeliveryRecord = require('../models/DeliveryRecord.js')
const createLogger = require('../util/logger/create')
const configuration = require('../config.js')
const Supporter = require('./db/Supporter')
const GeneralStats = require('../models/GeneralStats.js')
const fetch = require('node-fetch')
const { Webhook } = require('discord.js')

/**
 * @typedef {Object} ArticleDetails
 * @property {Object<string, any>} newArticle
 * @property {import('./ArticleMessage')} articleMessage
*/

/**
 * Staggers the delivery of new articles
 */
class ArticleQueue {
  constructor (client) {
    /**
     * @type {ArticleDetails[]}
     */
    this.queue = []
    /**
     * @type {import('discord.js').Client}
     */
    this.client = client
    this.log = createLogger(this.client.shard.ids[0])
    const config = configuration.get()
    const dequeueRate = config.feeds.articleDequeueRate
    const dequeueAmount = dequeueRate <= 1 ? 1 : dequeueRate
    const intervalSeconds = dequeueRate <= 1 ? 1 / dequeueRate : 1
    setInterval(() => {
      this.dequeue(dequeueAmount)
    }, 1000 * intervalSeconds)
  }

  /**
   * @param {ArticleDetails} articleData
   * @param {string} message
   */
  _logDebug (articleData, message) {
    const { article, feedObject } = articleData.newArticle
    this.log.debug(`ArticleQueue ${message} (${article._id}, feed: ${feedObject._id})`)
  }

  /**
   * Dequeue a certain amount of articles from the queue
   * and send them in order
   *
   * @param {number} dequeueAmount
   */
  async dequeue (dequeueAmount) {
    // async must be used within the loop to main the order
    // in which articles are sent
    for (let i = 0; i < dequeueAmount; ++i) {
      if (this.queue.length === 0) {
        continue
      }
      const articleData = this.queue.shift()
      this._logDebug(articleData, 'Dequeuing')
      await this.send(articleData)
    }
  }

  /**
   * Send an article message
   *
   * @param {ArticleDetails} articleData
   */
  async send (articleData) {
    const deliveryServiceURL = configuration.get().deliveryServiceURL
    try {
      if (deliveryServiceURL) {
        this._logDebug(articleData, 'Sending by service')
        await this.sendByService(articleData, deliveryServiceURL)
      } else {
        this._logDebug(articleData, 'Sending by client')
        await articleData.articleMessage.send(this.client)
      }
      await this.recordSuccess(articleData.newArticle)
      ArticleQueue.sent++
    } catch (err) {
      await this.recordFailure(articleData.newArticle, err.message)
    }
  }

  /**
   * Send an article via the external service
   *
   * @param {ArticleDetails} articleData
   * @param {string} serviceURL
   */
  async sendByService (articleData, serviceURL) {
    const articleMessage = articleData.articleMessage
    const medium = await articleMessage.getMedium(this.client)
    if (!medium) {
      console.log('missing medium')
      throw new Error('Missing medium to send article via service')
    }
    const apiPayload = articleMessage.createAPIPayload(medium)
    const apiRoute = medium instanceof Webhook ? `/webhooks/${medium.id}/${medium.token}` : `/channels/${medium.id}/messages`
    const res = await fetch(`${serviceURL}/api/request`, {
      method: 'POST',
      body: JSON.stringify({
        method: 'POST',
        url: `https://discord.com/api${apiRoute}`,
        body: apiPayload
      }),
      headers: {
        Authorization: `Bot ${this.client.token}`,
        'Content-Type': 'application/json',
        Accept: 'application/json'
      }
    })
    if (res.ok) {
      this._logDebug(articleData, 'Successfully sent via service')
      return
    }
    // Bad status code from service
    let json
    try {
      // The service should always send a message in the response
      json = await res.json()
      this.log.error(`Bad status code ${res.status} from service (${json.message})`)
    } catch (err) {
      this.log.error(err, `Bad status code ${res.status} from service`)
      throw new Error(`Bad status code (${res.status}) from service`)
    }
    if (json) {
      throw new Error(json.message)
    }
  }

  /**
   * Enqueue an article to be dequeued and sent at
   * the rate set in config
   *
   * @param {Object<string, any>} newArticle
   * @param {import('./ArticleMessage')} articleMessage
   */
  enqueue (newArticle, articleMessage) {
    this.queue.push({
      newArticle,
      articleMessage
    })
  }

  async recordFailure (newArticle, errorMessage) {
    if (!Feed.isMongoDatabase) {
      return
    }
    const { article, feedObject } = newArticle
    const channel = feedObject.channel
    const data = {
      articleID: article._id,
      feedURL: feedObject.url,
      channel,
      delivered: false,
      comment: errorMessage
    }
    this.log.debug({
      data
    }, 'Recording delivery record failure')
    try {
      const record = new DeliveryRecord.Model(data)
      await record.save()
    } catch (err) {
      this.log.error(err, `Failed to record article ${article._id} delivery failure in channel ${channel} (error: ${errorMessage}) in ArticleQueue`)
    }
  }

  async recordSuccess (newArticle) {
    if (!Feed.isMongoDatabase) {
      return
    }
    const { article, feedObject } = newArticle
    const channel = feedObject.channel
    const data = {
      articleID: article._id,
      feedURL: feedObject.url,
      delivered: true,
      channel
    }
    this.log.debug({
      data
    }, 'Recording delivery record success')
    try {
      const record = new DeliveryRecord.Model(data)
      await record.save()
    } catch (err) {
      this.log.error(err, `Failed to record article ${article._id} delivery success in channel ${channel} in ArticleQueue`)
    }
  }

  static async updateArticlesSent () {
    if (this.sent === 0 || !Supporter.isMongoDatabase) {
      return
    }
    /**
     * @type {import('mongoose').Document}
     */
    const found = await GeneralStats.Model.findById(GeneralStats.TYPES.ARTICLES_SENT)
    if (!found) {
      const stat = new GeneralStats.Model({
        _id: GeneralStats.TYPES.ARTICLES_SENT,
        data: ArticleQueue.sent
      })
      await stat.save()
    } else {
      await found.updateOne({
        $inc: {
          data: ArticleQueue.sent
        }
      })
    }
    ArticleQueue.sent = 0
  }
}

ArticleQueue.sent = 0

if (process.env.NODE_ENV !== 'test') {
  ArticleQueue.timer = setInterval(async () => {
    try {
      await ArticleQueue.updateArticlesSent()
    } catch (err) {
      const log = createLogger()
      log.error(err, 'Failed to update article stats')
    }
  }, 10000)
}

module.exports = ArticleQueue
<|endoftext|>


<commit_before>module.exports = function(config) {
  if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
    console.error('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.');
    process.exit(1);
  }

  var customLaunchers = {
    sl_chrome_mac: {
      base: 'SauceLabs',
      browserName: 'chrome',
      platform: 'macOS 10.15',
      version: '81.0'
    }
  };

  config.set({
    frameworks: ['mocha', 'karma-typescript'],
    singleRun: true,

    files: [
      'node_modules/chai/chai.js',
      '**/*.ts'
    ],

    exclude: [
      'demo/**/*.ts',
      'dist/*.ts',
      'external/**',
      'node_modules/**/*.ts'
    ],

    preprocessors: {
      '**/*.ts': ['karma-typescript']
    },

    reporters: ['dots', 'saucelabs', 'karma-typescript'],
    port: 9876,
    colors: true,

    browsers: Object.keys(customLaunchers),

    karmaTypescriptConfig: {
      tsconfig: 'tsconfig.json',
      exclude: [
        'demo',
        'external',
        'node_modules'
      ]
    },

    concurrency: 1,

    sauceLabs: {
      testName: 'Lovefield TypeScript Port',
      tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER,
      startConnect: false,
      recordScreenshots: false,
      public: 'public'
    },

    client: {
      mocha: {
        timeout: 10000
      }
    },

    browserDisconnectTimeout: 10000,
    browserNoActivityTimeout: 20000,
    captureTimeout: 0,
    customLaunchers: customLaunchers
  })
}  // end of module
<commit_msg>Update karma config<commit_after>module.exports = function(config) {
  var customLaunchers = {
    sl_chrome_mac: {
      base: 'SauceLabs',
      browserName: 'chrome',
      platform: 'macOS 10.15',
      version: '91.0'
    }
  };

  config.set({
    frameworks: ['mocha', 'karma-typescript'],
    singleRun: true,

    files: [
      'node_modules/chai/chai.js',
      '**/*.ts'
    ],

    exclude: [
      'demo/**/*.ts',
      'dist/*.ts',
      'external/**',
      'node_modules/**/*.ts'
    ],

    preprocessors: {
      '**/*.ts': ['karma-typescript']
    },

    reporters: ['dots', 'saucelabs', 'karma-typescript'],
    port: 9876,
    colors: true,

    browsers: Object.keys(customLaunchers),

    karmaTypescriptConfig: {
      tsconfig: 'tsconfig.json',
      exclude: [
        'demo',
        'external',
        'node_modules'
      ]
    },

    concurrency: 1,

    sauceLabs: {
      testName: 'Lovefield TypeScript Port',
      tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER,
      startConnect: false,
      recordScreenshots: false,
      public: 'public'
    },

    client: {
      mocha: {
        timeout: 10000
      }
    },

    browserDisconnectTimeout: 10000,
    browserNoActivityTimeout: 20000,
    captureTimeout: 0,
    customLaunchers: customLaunchers
  })
}  // end of module
<|endoftext|>


<commit_before>/**
 * @file Validation
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @private
 */

import { Vector3, Color } from "../../lib/three.es6.js";

import { Debug, Log } from "../globals.js";
import { defaults } from "../utils.js";
import { uniformArray3 } from "../math/array-utils.js";
import { guessElement } from "../structure/structure-utils.js";

function getSele( a, atomname, useAltcode ){
    const icode = a.icode.value;
    const chain = a.chain.value;
    const altcode = a.altcode.value;
    let sele = a.resnum.value;
    if( icode.trim() ) sele += "^" + icode;
    if( chain.trim() ) sele += ":" + chain;
    if( atomname ) sele += "." + atomname;
    if( useAltcode && altcode.trim() ) sele += "%" + altcode;
    sele += "/" + ( parseInt( a.model.value ) - 1 );
    return sele;
}

function setBitDict( dict, key, bit ){
    if( dict[ key ] === undefined ){
        dict[ key ] = bit;
    }else{
        dict[ key ] |= bit;
    }
}

function hasAttrValue( attr, value ){
    return attr !== undefined && attr.value === value;
}

function getAtomSele( ap ){
    const icode = ap.inscode;
    const chain = ap.chainname;
    const atomname = ap.atomname;
    const altcode = ap.altloc;
    let sele = ap.resno;
    if( icode ) sele += "^" + icode;
    if( chain ) sele += ":" + chain;
    if( atomname ) sele += "." + atomname;
    if( altcode ) sele += "%" + altcode;
    sele += "/" + ap.modelIndex;
    return sele;
}

function getProblemCount( clashDict, g, ga ){
    let geoProblemCount = 0;

    const clashes = g.getElementsByTagName( "clash" );
    for( let j = 0, jl = clashes.length; j < jl; ++j ){
        if( clashDict[ clashes[ j ].attributes.cid.value ] ){
            geoProblemCount += 1;
            break;
        }
    }

    const angleOutliers = g.getElementsByTagName( "angle-outlier" );
    if( angleOutliers.length > 0 ){
        geoProblemCount += 1;
    }

    const bondOutliers = g.getElementsByTagName( "bond-outlier" );
    if( bondOutliers.length > 0 ){
        geoProblemCount += 1;
    }

    const planeOutliers = g.getElementsByTagName( "plane-outlier" );
    if( planeOutliers.length > 0 ){
        geoProblemCount += 1;
    }

    if( hasAttrValue( ga.rota, "OUTLIER" ) ){
        geoProblemCount += 1;
    }

    if( hasAttrValue( ga.rama, "OUTLIER" ) ){
        geoProblemCount += 1;
    }

    if( hasAttrValue( ga.RNApucker, "outlier" ) ){
        geoProblemCount += 1;
    }

    return geoProblemCount;
}

class Validation{

    constructor( name, path ){

        this.name = name;
        this.path = path;

        this.rsrzDict = {};
        this.rsccDict = {};
        this.clashDict = {};
        this.geoDict = {};
        this.geoAtomDict = {};
        this.atomDict = {};
        this.clashSele = "NONE";

    }

    fromXml( xml ){

        if( Debug ) Log.time( "Validation.fromXml" );

        const rsrzDict = this.rsrzDict;
        const rsccDict = this.rsccDict;
        const clashDict = this.clashDict;
        const geoDict = this.geoDict;
        const geoAtomDict = this.geoAtomDict;
        const atomDict = this.atomDict;

        const groups = xml.getElementsByTagName( "ModelledSubgroup" );

        const _clashDict = {};
        const clashList = [];

        if( Debug ) Log.time( "Validation.fromXml#clashDict" );

        for( let i = 0, il = groups.length; i < il; ++i ){

            const g = groups[ i ];
            const ga = g.attributes;

            const sele = getSele( ga );
            if( ga.rsrz !== undefined ){
                rsrzDict[ sele ] = parseFloat( ga.rsrz.value );
            }
            if( ga.rscc !== undefined ){
                rsccDict[ sele ] = parseFloat( ga.rscc.value );
            }
            ga.sele = sele;

            const clashes = g.getElementsByTagName( "clash" );

            for( let j = 0, jl = clashes.length; j < jl; ++j ){

                const ca = clashes[ j ].attributes;
                const atom = ca.atom.value;

                if( guessElement( atom ) !== "H" ){

                    const cid = ca.cid.value;
                    const atomSele = getSele( ga, atom, true );
                    atomDict[ atomSele ] = true;

                    if( _clashDict[ cid ] === undefined ){
                        _clashDict[ cid ] = {
                            sele1: atomSele,
                            res1: sele
                        };
                    }else{
                        const c = _clashDict[ cid ];
                        if( c.res1 !== sele ){
                            c.sele2 = atomSele;
                            c.res2 = sele;
                            clashList.push( c.res1, sele );
                            clashDict[ cid ] = c;
                        }
                    }

                }

            }

        }

        if( Debug ) Log.timeEnd( "Validation.fromXml#clashDict" );

        for( let i = 0, il = groups.length; i < il; ++i ){

            const g = groups[ i ];
            const ga = g.attributes;

            const sele = ga.sele;
            const isPolymer = ga.seq.value !== ".";

            if( isPolymer ){

                const geoProblemCount = getProblemCount( clashDict, g, ga );
                if( geoProblemCount > 0 ){
                    geoDict[ sele ] = geoProblemCount;
                }

            }else{

                const clashes = g.getElementsByTagName( "clash" );
                const mogBondOutliers = g.getElementsByTagName( "mog-bond-outlier" );
                const mogAngleOutliers = g.getElementsByTagName( "mog-angle-outlier" );

                if( mogBondOutliers.length > 0 || mogAngleOutliers.length > 0 || clashes.length > 0 ){

                    const atomDict = {};
                    geoAtomDict[ sele ] = atomDict;

                    for( let j = 0, jl = clashes.length; j < jl; ++j ){
                        const ca = clashes[ j ].attributes;
                        if( clashDict[ ca.cid.value ] ){
                            setBitDict( atomDict, ca.atom.value, 1 );
                        }
                    }

                    for( let j = 0, jl = mogBondOutliers.length; j < jl; ++j ){
                        const mbo = mogBondOutliers[ j ].attributes;
                        mbo.atoms.value.split( "," ).forEach( function( atomname ){
                            setBitDict( atomDict, atomname, 2 );
                        } );
                    }

                    for( let j = 0, jl = mogAngleOutliers.length; j < jl; ++j ){
                        const mao = mogAngleOutliers[ j ].attributes;
                        mao.atoms.value.split( "," ).forEach( function( atomname ){
                            setBitDict( atomDict, atomname, 4 );
                        } );
                    }

                }

            }

        }

        this.clashSele = clashList.length ? clashList.join( " OR " ) : "NONE";

        if( Debug ) Log.timeEnd( "Validation.fromXml" );

    }

    getClashData( params ){

        if( Debug ) Log.time( "Validation.getClashData" );

        const p = params || {};

        const s = p.structure;
        const c = new Color( defaults( p.color, "#f0027f" ) );

        const ap1 = s.getAtomProxy();
        const ap2 = s.getAtomProxy();
        const vDir = new Vector3();
        const vPos1 = new Vector3();
        const vPos2 = new Vector3();

        const clashDict = this.clashDict;
        const n = Object.keys( clashDict ).length;

        const position1 = new Float32Array( n * 3 );
        const position2 = new Float32Array( n * 3 );
        const color = uniformArray3( n, c.r, c.g, c.b );
        const radius = new Float32Array( n );

        if( Debug ) Log.time( "Validation.getClashData#atomDict" );

        const atomDict = this.atomDict;

        s.eachAtom( function( ap ){
            const sele = getAtomSele( ap );
            if( atomDict[ sele ] === true ){
                atomDict[ sele ] = ap.index;
            }
        } );

        if( Debug ) Log.timeEnd( "Validation.getClashData#atomDict" );

        let i = 0;

        for( let k in clashDict ){

            const c = clashDict[ k ];
            ap1.index = atomDict[ c.sele1 ];
            ap2.index = atomDict[ c.sele2 ];

            if( ap1.index === undefined || ap2.index === undefined ) continue;

            vDir.subVectors( ap2, ap1 ).setLength( ap1.vdw );
            vPos1.copy( ap1 ).add( vDir );

            vDir.subVectors( ap1, ap2 ).setLength( ap2.vdw );
            vPos2.copy( ap2 ).add( vDir );

            const dHalf = ap1.distanceTo( ap2 ) / 2;
            const r1 = Math.sqrt( ap1.vdw * ap1.vdw - dHalf * dHalf );
            const r2 = Math.sqrt( ap2.vdw * ap2.vdw - dHalf * dHalf );

            vPos1.toArray( position1, i * 3 );
            vPos2.toArray( position2, i * 3 );
            radius[ i ] = ( r1 + r2 ) / 2;

            ++i;

        }

        if( Debug ) Log.timeEnd( "Validation.getClashData" );

        return {
            position1: position1,
            position2: position2,
            color: color,
            color2: color,
            radius: radius
        };

    }

}

export default Validation;
<commit_msg>filter validation data with respect to given structure view<commit_after>/**
 * @file Validation
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @private
 */

import { Vector3, Color } from "../../lib/three.es6.js";

import { Debug, Log } from "../globals.js";
import { defaults } from "../utils.js";
import { uniformArray3 } from "../math/array-utils.js";
import { guessElement } from "../structure/structure-utils.js";

function getSele( a, atomname, useAltcode ){
    const icode = a.icode.value;
    const chain = a.chain.value;
    const altcode = a.altcode.value;
    let sele = a.resnum.value;
    if( icode.trim() ) sele += "^" + icode;
    if( chain.trim() ) sele += ":" + chain;
    if( atomname ) sele += "." + atomname;
    if( useAltcode && altcode.trim() ) sele += "%" + altcode;
    sele += "/" + ( parseInt( a.model.value ) - 1 );
    return sele;
}

function setBitDict( dict, key, bit ){
    if( dict[ key ] === undefined ){
        dict[ key ] = bit;
    }else{
        dict[ key ] |= bit;
    }
}

function hasAttrValue( attr, value ){
    return attr !== undefined && attr.value === value;
}

function getAtomSele( ap ){
    const icode = ap.inscode;
    const chain = ap.chainname;
    const atomname = ap.atomname;
    const altcode = ap.altloc;
    let sele = ap.resno;
    if( icode ) sele += "^" + icode;
    if( chain ) sele += ":" + chain;
    if( atomname ) sele += "." + atomname;
    if( altcode ) sele += "%" + altcode;
    sele += "/" + ap.modelIndex;
    return sele;
}

function getProblemCount( clashDict, g, ga ){
    let geoProblemCount = 0;

    const clashes = g.getElementsByTagName( "clash" );
    for( let j = 0, jl = clashes.length; j < jl; ++j ){
        if( clashDict[ clashes[ j ].attributes.cid.value ] ){
            geoProblemCount += 1;
            break;
        }
    }

    const angleOutliers = g.getElementsByTagName( "angle-outlier" );
    if( angleOutliers.length > 0 ){
        geoProblemCount += 1;
    }

    const bondOutliers = g.getElementsByTagName( "bond-outlier" );
    if( bondOutliers.length > 0 ){
        geoProblemCount += 1;
    }

    const planeOutliers = g.getElementsByTagName( "plane-outlier" );
    if( planeOutliers.length > 0 ){
        geoProblemCount += 1;
    }

    if( hasAttrValue( ga.rota, "OUTLIER" ) ){
        geoProblemCount += 1;
    }

    if( hasAttrValue( ga.rama, "OUTLIER" ) ){
        geoProblemCount += 1;
    }

    if( hasAttrValue( ga.RNApucker, "outlier" ) ){
        geoProblemCount += 1;
    }

    return geoProblemCount;
}

class Validation{

    constructor( name, path ){

        this.name = name;
        this.path = path;

        this.rsrzDict = {};
        this.rsccDict = {};
        this.clashDict = {};
        this.geoDict = {};
        this.geoAtomDict = {};
        this.atomDict = {};
        this.clashSele = "NONE";

    }

    fromXml( xml ){

        if( Debug ) Log.time( "Validation.fromXml" );

        const rsrzDict = this.rsrzDict;
        const rsccDict = this.rsccDict;
        const clashDict = this.clashDict;
        const geoDict = this.geoDict;
        const geoAtomDict = this.geoAtomDict;
        const atomDict = this.atomDict;

        const groups = xml.getElementsByTagName( "ModelledSubgroup" );

        const _clashDict = {};
        const clashList = [];

        if( Debug ) Log.time( "Validation.fromXml#clashDict" );

        for( let i = 0, il = groups.length; i < il; ++i ){

            const g = groups[ i ];
            const ga = g.attributes;

            const sele = getSele( ga );
            if( ga.rsrz !== undefined ){
                rsrzDict[ sele ] = parseFloat( ga.rsrz.value );
            }
            if( ga.rscc !== undefined ){
                rsccDict[ sele ] = parseFloat( ga.rscc.value );
            }
            ga.sele = sele;

            const clashes = g.getElementsByTagName( "clash" );

            for( let j = 0, jl = clashes.length; j < jl; ++j ){

                const ca = clashes[ j ].attributes;
                const atom = ca.atom.value;

                if( guessElement( atom ) !== "H" ){

                    const cid = ca.cid.value;
                    const atomSele = getSele( ga, atom, true );
                    atomDict[ atomSele ] = true;

                    if( _clashDict[ cid ] === undefined ){
                        _clashDict[ cid ] = {
                            sele1: atomSele,
                            res1: sele
                        };
                    }else{
                        const c = _clashDict[ cid ];
                        if( c.res1 !== sele ){
                            c.sele2 = atomSele;
                            c.res2 = sele;
                            clashList.push( c.res1, sele );
                            clashDict[ cid ] = c;
                        }
                    }

                }

            }

        }

        if( Debug ) Log.timeEnd( "Validation.fromXml#clashDict" );

        for( let i = 0, il = groups.length; i < il; ++i ){

            const g = groups[ i ];
            const ga = g.attributes;

            const sele = ga.sele;
            const isPolymer = ga.seq.value !== ".";

            if( isPolymer ){

                const geoProblemCount = getProblemCount( clashDict, g, ga );
                if( geoProblemCount > 0 ){
                    geoDict[ sele ] = geoProblemCount;
                }

            }else{

                const clashes = g.getElementsByTagName( "clash" );
                const mogBondOutliers = g.getElementsByTagName( "mog-bond-outlier" );
                const mogAngleOutliers = g.getElementsByTagName( "mog-angle-outlier" );

                if( mogBondOutliers.length > 0 || mogAngleOutliers.length > 0 || clashes.length > 0 ){

                    const atomDict = {};
                    geoAtomDict[ sele ] = atomDict;

                    for( let j = 0, jl = clashes.length; j < jl; ++j ){
                        const ca = clashes[ j ].attributes;
                        if( clashDict[ ca.cid.value ] ){
                            setBitDict( atomDict, ca.atom.value, 1 );
                        }
                    }

                    for( let j = 0, jl = mogBondOutliers.length; j < jl; ++j ){
                        const mbo = mogBondOutliers[ j ].attributes;
                        mbo.atoms.value.split( "," ).forEach( function( atomname ){
                            setBitDict( atomDict, atomname, 2 );
                        } );
                    }

                    for( let j = 0, jl = mogAngleOutliers.length; j < jl; ++j ){
                        const mao = mogAngleOutliers[ j ].attributes;
                        mao.atoms.value.split( "," ).forEach( function( atomname ){
                            setBitDict( atomDict, atomname, 4 );
                        } );
                    }

                }

            }

        }

        this.clashSele = clashList.length ? clashList.join( " OR " ) : "NONE";

        if( Debug ) Log.timeEnd( "Validation.fromXml" );

    }

    getClashData( params ){

        if( Debug ) Log.time( "Validation.getClashData" );

        const p = params || {};

        const s = p.structure;
        const as = s.atomSet;
        const c = new Color( defaults( p.color, "#f0027f" ) );

        const ap1 = s.getAtomProxy();
        const ap2 = s.getAtomProxy();
        const vDir = new Vector3();
        const vPos1 = new Vector3();
        const vPos2 = new Vector3();

        const clashDict = this.clashDict;
        const n = Object.keys( clashDict ).length;

        const position1 = new Float32Array( n * 3 );
        const position2 = new Float32Array( n * 3 );
        const color = uniformArray3( n, c.r, c.g, c.b );
        const radius = new Float32Array( n );

        if( Debug ) Log.time( "Validation.getClashData#atomDict" );

        const atomDict = this.atomDict;

        s.eachAtom( function( ap ){
            const sele = getAtomSele( ap );
            if( atomDict[ sele ] === true ){
                atomDict[ sele ] = ap.index;
            }
        } );

        if( Debug ) Log.timeEnd( "Validation.getClashData#atomDict" );

        let i = 0;

        for( let k in clashDict ){

            const c = clashDict[ k ];
            ap1.index = atomDict[ c.sele1 ];
            ap2.index = atomDict[ c.sele2 ];

            if( !as.has( ap1.index ) || !as.has( ap1.index ) ||
                ap1.index === undefined || ap2.index === undefined ) continue;

            vDir.subVectors( ap2, ap1 ).setLength( ap1.vdw );
            vPos1.copy( ap1 ).add( vDir );

            vDir.subVectors( ap1, ap2 ).setLength( ap2.vdw );
            vPos2.copy( ap2 ).add( vDir );

            const dHalf = ap1.distanceTo( ap2 ) / 2;
            const r1 = Math.sqrt( ap1.vdw * ap1.vdw - dHalf * dHalf );
            const r2 = Math.sqrt( ap2.vdw * ap2.vdw - dHalf * dHalf );

            vPos1.toArray( position1, i * 3 );
            vPos2.toArray( position2, i * 3 );
            radius[ i ] = ( r1 + r2 ) / 2;

            ++i;

        }

        if( Debug ) Log.timeEnd( "Validation.getClashData" );

        return {
            position1: position1,
            position2: position2,
            color: color,
            color2: color,
            radius: radius
        };

    }

}

export default Validation;
<|endoftext|>


<commit_before>app.directive('post', function(LocalStorage, ApiService){
    return {
        restrict : 'E',
        scope: {
            photo: '='
        },
        templateUrl : 'directives/postView.html',
        controller: function($scope) {

            $scope.lastCommentsLoaded = true;
            $scope.showMessages = false;
            $scope.page = 0;

            console.info($scope.photo);

            if ($scope.photo.comments.length == 10) {
                $scope.lastCommentsLoaded = false;
                // set page to 1
                $scope.page = 1;

            }

            $scope.loadComments = function() {

                // make click listener.
                // increase page ++1
                $scope.page = $scope.page + 1;

                var page = $scope.page;
                var itemsPerPage = 10;
                var offset = (page - 1) * itemsPerPage + 1;

                // get comments request.
                ApiService.getComments($scope.photo.id, offset, 10).then(
                    function(response){
                        console.info(response);

                        //  : if loaded comments are size of 10 then lastCommentsLoaded stays false, else true
                        if (response.length == 10) {
                            $scope.lastCommentsLoaded = false;
                        } else {
                            $scope.lastCommentsLoaded = true;
                        }

                        $scope.photo.comments = $.merge($scope.photo.comments, response);
                    }, function(error) {
                        console.error(error);
                    }
                );

            };

            $scope.toggleMessages = function() {
                $scope.showMessages = !$scope.showMessages;
            };

            $scope.buildImageUrl = function (filename) {
                return ApiService.urlbuilder.photo('/images/' + filename);
            };

            $scope.displayMoment = function (createdAt) {
                $mom = moment(createdAt, "YYYY-MM-DDTHH:mm:ssZ");
                return $mom.fromNow();
            };

            $scope.upvote = function (photo) {
                console.log('upvote button clicked');
                if (photo.upvote) {
                    console.log('You already upvoted');
                    return;
                }
                var usr = LocalStorage.getUser();
                if (!usr) {
                    // TODO : maybe redirect to login?
                    // Or show a message with : 'you need to signup buddy'
                    console.info('you need to singin buddy');
                    return;
                }

                ApiService.upvote(usr.id, photo.id).then(function (resp) {
                    // If this is the first vote from this user then increase the number of total votes.
                    if (!photo.downvote && !photo.upvote) {
                        photo.totalVotes++;
                        photo.upvote_count++;
                    }

                    // If the previous vote was a upvote, then recalculate the counts
                    if (photo.downvote) {
                        photo.downvote_count--;
                        photo.upvote_count++;
                    }

                    // Let angular know which button to show on page
                    photo.downvote = false;
                    photo.upvote = true;

                }, function (error) {
                    console.error(error);
                });

            };
            $scope.downvote = function(photo) {
                console.log('downvote button clicked');
                if (photo.downvote) {
                    console.log('You already downvoted');
                    return;
                }
                var usr = LocalStorage.getUser();
                if (!usr) {
                    console.info('you need to singin buddy');
                    // TODO : maybe redirect to login?
                    // Or show a message with : 'you need to signup buddy'
                    return;
                }

                ApiService.downvote(usr.id, photo.id).then(function (resp) {
                    // If this is the first vote from this user then increase the number of total votes.
                    if (!photo.downvote && !photo.upvote) {
                        photo.totalVotes++;
                        photo.downvote_count++;
                    }

                    // If the previous vote was a upvote, then recalculate the counts
                    if (photo.upvote) {
                        photo.upvote_count--;
                        photo.downvote_count++;
                    }

                    // Let angular know which button to show on page
                    photo.upvote = false;
                    photo.downvote = true;

                }, function (error) {
                    console.error(error);
                });
            };

            $scope.comment = function(photo) {
                var usr = LocalStorage.getUser();
                if (!usr) {
                    // TODO : maybe redirect to login?
                    // Or show a message with : 'you need to signup buddy'
                    console.info('you need to singin buddy');
                    return;
                }

                ApiService.comment(usr.id, photo.id, $scope.comment_text).then(
                    function (response) {
                        console.info(response);
                        $scope.comment_text = '';
                        $scope.photo.comment_count ++;
                        if ($scope.photo && $scope.photo.comments) {
                            $scope.photo.comments.push(response);
                        }
                    },
                    function (error) {
                        console.error(error);
                    }
                );
            }
            $scope.calcPerc = function(photo) {

                var down = photo.downvote_count < 1 ? 0 : photo.downvote_count * 100;
                var up = photo.upvote_count < 1 ? 0 : photo.upvote_count * 100;

                if (down < 1 && up < 1) {
                    return '-';
                } else if (down < 1) {
                    return '100 %'
                } else if (up < 1) {
                    return '0 %'
                } else {
                    return Math.floor((up / (up+down) * 100)) + " %"
                }

            }
        }
    }
});<commit_msg>moment fromNow fix<commit_after>app.directive('post', function(LocalStorage, ApiService){
    return {
        restrict : 'E',
        scope: {
            photo: '='
        },
        templateUrl : 'directives/postView.html',
        controller: function($scope) {

            $scope.lastCommentsLoaded = true;
            $scope.showMessages = false;
            $scope.page = 0;

            if ($scope.photo.comments.length == 10) {
                $scope.lastCommentsLoaded = false;
                // set page to 1
                $scope.page = 1;

            }

            $scope.loadComments = function() {

                // make click listener.
                // increase page ++1
                $scope.page = $scope.page + 1;

                var page = $scope.page;
                var itemsPerPage = 10;
                var offset = (page - 1) * itemsPerPage + 1;

                // get comments request.
                ApiService.getComments($scope.photo.id, offset, 10).then(
                    function(response){
                        console.info(response);

                        //  : if loaded comments are size of 10 then lastCommentsLoaded stays false, else true
                        if (response.length == 10) {
                            $scope.lastCommentsLoaded = false;
                        } else {
                            $scope.lastCommentsLoaded = true;
                        }

                        $scope.photo.comments = $.merge($scope.photo.comments, response);
                    }, function(error) {
                        console.error(error);
                    }
                );

            };

            $scope.toggleMessages = function() {
                $scope.showMessages = !$scope.showMessages;
            };

            $scope.buildImageUrl = function (filename) {
                return ApiService.urlbuilder.photo('/images/' + filename);
            };

            $scope.displayMoment = function (createdAt) {
                return moment(createdAt, "YYYY-MM-DD HH:mm:ss").fromNow();
            };

            $scope.upvote = function (photo) {
                console.log('upvote button clicked');
                if (photo.upvote) {
                    console.log('You already upvoted');
                    return;
                }
                var usr = LocalStorage.getUser();
                if (!usr) {
                    // TODO : maybe redirect to login?
                    // Or show a message with : 'you need to signup buddy'
                    console.info('you need to singin buddy');
                    return;
                }

                ApiService.upvote(usr.id, photo.id).then(function (resp) {
                    // If this is the first vote from this user then increase the number of total votes.
                    if (!photo.downvote && !photo.upvote) {
                        photo.totalVotes++;
                        photo.upvote_count++;
                    }

                    // If the previous vote was a upvote, then recalculate the counts
                    if (photo.downvote) {
                        photo.downvote_count--;
                        photo.upvote_count++;
                    }

                    // Let angular know which button to show on page
                    photo.downvote = false;
                    photo.upvote = true;

                }, function (error) {
                    console.error(error);
                });

            };
            $scope.downvote = function(photo) {
                console.log('downvote button clicked');
                if (photo.downvote) {
                    console.log('You already downvoted');
                    return;
                }
                var usr = LocalStorage.getUser();
                if (!usr) {
                    console.info('you need to singin buddy');
                    // TODO : maybe redirect to login?
                    // Or show a message with : 'you need to signup buddy'
                    return;
                }

                ApiService.downvote(usr.id, photo.id).then(function (resp) {
                    // If this is the first vote from this user then increase the number of total votes.
                    if (!photo.downvote && !photo.upvote) {
                        photo.totalVotes++;
                        photo.downvote_count++;
                    }

                    // If the previous vote was a upvote, then recalculate the counts
                    if (photo.upvote) {
                        photo.upvote_count--;
                        photo.downvote_count++;
                    }

                    // Let angular know which button to show on page
                    photo.upvote = false;
                    photo.downvote = true;

                }, function (error) {
                    console.error(error);
                });
            };

            $scope.comment = function(photo) {
                var usr = LocalStorage.getUser();
                if (!usr) {
                    // TODO : maybe redirect to login?
                    // Or show a message with : 'you need to signup buddy'
                    console.info('you need to singin buddy');
                    return;
                }

                ApiService.comment(usr.id, photo.id, $scope.comment_text).then(
                    function (response) {
                        console.info(response);
                        $scope.comment_text = '';
                        $scope.photo.comment_count ++;
                        if ($scope.photo && $scope.photo.comments) {
                            $scope.photo.comments.push(response);
                        }
                    },
                    function (error) {
                        console.error(error);
                    }
                );
            }
            $scope.calcPerc = function(photo) {

                var down = photo.downvote_count < 1 ? 0 : photo.downvote_count * 100;
                var up = photo.upvote_count < 1 ? 0 : photo.upvote_count * 100;

                if (down < 1 && up < 1) {
                    return '-';
                } else if (down < 1) {
                    return '100 %'
                } else if (up < 1) {
                    return '0 %'
                } else {
                    return Math.floor((up / (up+down) * 100)) + " %"
                }

            }
        }
    }
});<|endoftext|>


<commit_before>import React from 'react';
import { Modal, Button } from 'react-bootstrap';
import magellan from 'magellan-coords';
import config from './../../server/config.js';

//Components
import Map from './Map';
import UberInfo from './UberInfo';

class RestaurantPref extends React.Component {
  constructor(){
    super();
    this.openUberModal = this.openUberModal.bind(this);
    this.closeUberModal = this.closeUberModal.bind(this);
    this.selectRestaurant = this.selectRestaurant.bind(this);
    this.selectNext = this.selectNext.bind(this);
    this.displayLoadingSpinner = this.displayLoadingSpinner.bind(this);
    this.displayTopRestaurant = this.displayTopRestaurant.bind(this);
    this.closeFavoriteModal = this.closeFavoriteModal.bind(this);
    this.state = {
      showFavoriteModal: false
    }
  }

  componentWillMount(){
    const { fetchSnapPeaData } = this.props.dinerActions;
    const { diners, location } = this.props;
    const dinerInfo = [diners[0]];

    for(var i = 1; i < diners.length; i++){
      dinerInfo.push(diners[i].friendName);
    }

    fetchSnapPeaData(dinerInfo, location);
  }

  openUberModal(){
    this.setState({
      showUberModal: true
    })
  }

  closeUberModal(){
    this.setState({
      showUberModal: false
    })
  }

  selectRestaurant(){
    const { username } = this.props;
    const { addToHistory } = this.props.dinerActions;
    const { name, id } = this.props.topRestaurant;
    const restaurantInfo = {
      username: username,
      restaurantName: name,
      restaurantId: id,
      date: new Date()
    }

    addToHistory(restaurantInfo);
    this.setState({showFavoriteModal: true})
  }

  closeFavoriteModal(){
    this.setState({showFavoriteModal: false})
  }

  selectNext(e){
    e.preventDefault();
    const { updateTopRestaurant } = this.props.dinerActions;
    const { fetchUberData } = this.props.dinerActions;
    const { clearUberData } = this.props.dinerActions;

    this.setState({
      isFetchingUberData: true
    })

    updateTopRestaurant();
    clearUberData();
    fetchUberData(this.props.topRestaurant.location.coordinate.latitude, this.props.topRestaurant.location.coordinate.longitude)
  }

  displayLoadingSpinner(){
    if(this.props.isLoadingResults){
      return (

Our algorithm is crunching numbers. Your recommendation will be ready in a snap!

          <image src='./../static/assets/spinner.gif' />

      )
    } else {
      return null;
    }
  }

  displayTopRestaurant(){
    if (this.props.topRestaurant.name) {

    // format start location for direction url
    let destination = '';
    this.props.topRestaurant.location.display_address.forEach(function (line, index, array) {
      destination = destination.concat(line)
      if (array[++index]) {
        destination = destination.concat(' ')
      }
    })
    destination = destination.replace(/\s/g, '+');

    // convert coordinates to DMS format
    let startLat = this.props.location[0].toString();
    let startLng = this.props.location[1].toString();

    if (startLat.charAt(0) === '-') {
      startLat = '-' + magellan(startLat.slice(1)).toDMS()
    } else {
      startLat = magellan(startLat).toDMS()
    }

    if (startLng.charAt(0) === '-') {
      startLng = '-' + magellan(startLng.slice(1)).toDMS()
    } else {
      startLng = magellan(startLng).toDMS()
    }

    let startLoc = startLat + ',' + startLng;

    let directionsUrl = 'https://www.google.com/maps/dir/' + startLoc + '/' + destination;
      let categories = this.props.topRestaurant.categories.map(function(cat){
        return cat[0];
      }).join(', ')

      let reviews = 'reviews';
      if (this.props.topRestaurant.review_count === 1) {
        reviews = 'review'
      }

      return (

            <Modal
              show={this.state.showFavoriteModal}
              onHide={this.closeFavoriteModal}
              className='loginmodal register'>
              <Modal.Header closeButton className="close-btn">
              </Modal.Header>
              <Modal.Body className='modalbody'>

Restaurant added to history

              </Modal.Body>
            </Modal>

              <Button className='btn top-button' onClick={this.selectRestaurant}>Save to History</Button>
              <Button className='btn top-button' onClick={this.selectNext}>Next suggestion</Button>

Based on your input, we think you'll really like eating at...

{this.props.topRestaurant.name}

 {this.props.topRestaurant.review_count} {reviews}

{categories}

              {this.props.topRestaurant.location.display_address[0]}

              {this.props.topRestaurant.location.display_address[1]}

              {this.props.topRestaurant.location.display_address[2]}

              {this.props.topRestaurant.display_phone}

              <Map lat={this.props.topRestaurant.location.coordinate.latitude} lng={this.props.topRestaurant.location.coordinate.longitude}/>

              <Button onClick={this.openUberModal} className='top-button'>
                Ride there with Uber
                <UberInfo
                  {...this.props}
                  showUberModal={this.state.showUberModal}
                  closeUberModal={this.closeUberModal} />
              </Button>

              <Button className='top-button'>
                Get Directions
              </Button>

      )
    } else {
      return null;
    }
  }

  render(){
  	return (

        {this.displayLoadingSpinner()}
        {this.displayTopRestaurant()}

 	  )
  }
}

export default RestaurantPref;
<commit_msg>[style] styles spinner for restaurantpref<commit_after>import React from 'react';
import { Modal, Button } from 'react-bootstrap';
import magellan from 'magellan-coords';
import config from './../../server/config.js';

//Components
import Map from './Map';
import UberInfo from './UberInfo';

class RestaurantPref extends React.Component {
  constructor(){
    super();
    this.openUberModal = this.openUberModal.bind(this);
    this.closeUberModal = this.closeUberModal.bind(this);
    this.selectRestaurant = this.selectRestaurant.bind(this);
    this.selectNext = this.selectNext.bind(this);
    this.displayLoadingSpinner = this.displayLoadingSpinner.bind(this);
    this.displayTopRestaurant = this.displayTopRestaurant.bind(this);
    this.closeFavoriteModal = this.closeFavoriteModal.bind(this);
    this.state = {
      showFavoriteModal: false
    }
  }

  componentWillMount(){
    const { fetchSnapPeaData } = this.props.dinerActions;
    const { diners, location } = this.props;
    const dinerInfo = [diners[0]];

    for(var i = 1; i < diners.length; i++){
      dinerInfo.push(diners[i].friendName);
    }

    fetchSnapPeaData(dinerInfo, location);
  }

  openUberModal(){
    this.setState({
      showUberModal: true
    })
  }

  closeUberModal(){
    this.setState({
      showUberModal: false
    })
  }

  selectRestaurant(){
    const { username } = this.props;
    const { addToHistory } = this.props.dinerActions;
    const { name, id } = this.props.topRestaurant;
    const restaurantInfo = {
      username: username,
      restaurantName: name,
      restaurantId: id,
      date: new Date()
    }

    addToHistory(restaurantInfo);
    this.setState({showFavoriteModal: true})
  }

  closeFavoriteModal(){
    this.setState({showFavoriteModal: false})
  }

  selectNext(e){
    e.preventDefault();
    const { updateTopRestaurant } = this.props.dinerActions;
    const { fetchUberData } = this.props.dinerActions;
    const { clearUberData } = this.props.dinerActions;

    this.setState({
      isFetchingUberData: true
    })

    updateTopRestaurant();
    clearUberData();
    fetchUberData(this.props.topRestaurant.location.coordinate.latitude, this.props.topRestaurant.location.coordinate.longitude)
  }

  displayLoadingSpinner(){
    if(this.props.isLoadingResults){
      return (

Our algorithm is crunching numbers. Your recommendation will be ready in a snap!

          <image src='./../static/assets/spinner.gif' />

      )
    } else {
      return null;
    }
  }

  displayTopRestaurant(){
    if (this.props.topRestaurant.name) {

    // format start location for direction url
    let destination = '';
    this.props.topRestaurant.location.display_address.forEach(function (line, index, array) {
      destination = destination.concat(line)
      if (array[++index]) {
        destination = destination.concat(' ')
      }
    })
    destination = destination.replace(/\s/g, '+');

    // convert coordinates to DMS format
    let startLat = this.props.location[0].toString();
    let startLng = this.props.location[1].toString();

    if (startLat.charAt(0) === '-') {
      startLat = '-' + magellan(startLat.slice(1)).toDMS()
    } else {
      startLat = magellan(startLat).toDMS()
    }

    if (startLng.charAt(0) === '-') {
      startLng = '-' + magellan(startLng.slice(1)).toDMS()
    } else {
      startLng = magellan(startLng).toDMS()
    }

    let startLoc = startLat + ',' + startLng;

    let directionsUrl = 'https://www.google.com/maps/dir/' + startLoc + '/' + destination;
      let categories = this.props.topRestaurant.categories.map(function(cat){
        return cat[0];
      }).join(', ')

      let reviews = 'reviews';
      if (this.props.topRestaurant.review_count === 1) {
        reviews = 'review'
      }

      return (

            <Modal
              show={this.state.showFavoriteModal}
              onHide={this.closeFavoriteModal}
              className='loginmodal register'>
              <Modal.Header closeButton className="close-btn">
              </Modal.Header>
              <Modal.Body className='modalbody'>

Restaurant added to history

              </Modal.Body>
            </Modal>

              <Button className='btn top-button' onClick={this.selectRestaurant}>Save to History</Button>
              <Button className='btn top-button' onClick={this.selectNext}>Next suggestion</Button>

Based on your input, we think you'll really like eating at...

{this.props.topRestaurant.name}

 {this.props.topRestaurant.review_count} {reviews}

{categories}

              {this.props.topRestaurant.location.display_address[0]}

              {this.props.topRestaurant.location.display_address[1]}

              {this.props.topRestaurant.location.display_address[2]}

              {this.props.topRestaurant.display_phone}

              <Map lat={this.props.topRestaurant.location.coordinate.latitude} lng={this.props.topRestaurant.location.coordinate.longitude}/>

              <Button onClick={this.openUberModal} className='top-button'>
                Ride there with Uber
                <UberInfo
                  {...this.props}
                  showUberModal={this.state.showUberModal}
                  closeUberModal={this.closeUberModal} />
              </Button>

              <Button className='top-button'>
                Get Directions
              </Button>

      )
    } else {
      return null;
    }
  }

  render(){
  	return (

        {this.displayLoadingSpinner()}
        {this.displayTopRestaurant()}

 	  )
  }
}

export default RestaurantPref;
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { NavLink, Link} from 'react-router-dom';
import {connect} from 'react-redux';
import store from '../store';
import {Button} from 'react-bootstrap';

class SingleArtist extends Component {

  constructor(props) {
    super(props);

    const currentArtistId = this.props.match.params.id;
    const artists = this.props.artistsCollection;
    const currentArtist = artists.length && artists.filter(artist => +artist.id === +currentArtistId)[0];

    const {name, profileImageUrl, bio, email, galleries} = currentArtist;

    this.state = {
      name,
      profileImageUrl,
      bio,
      email,
      galleries,
    };

    this.handleChange = this.handleChange.bind(this);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.artistsCollection.length !== this.props.artistsCollection.length) {
      const currentArtistId = nextProps.match.params.id;
      const artists = nextProps.artistsCollection;
      const currentArtist = artists.length && artists.filter(artist => +artist.id === +currentArtistId)[0];
      const galleries = currentArtist.galleries;

      const {name, profileImageUrl, bio, email} = currentArtist;

      this.setState({
        name,
        profileImageUrl,
        bio,
        email,
        galleries
      });
    }
  }

  handleChange(evt) {
    const name = evt.target.name;
    const value = evt.target.value;
    this.setState({
      [name]: value
    });
  }

  render(){

    const currentArtist = this.state;
    const currentUser = this.props.currentUser;
   // TODO: Add user input version of name, biography, and email
   // TODO: Allow to delete galleries and images from dashboard
   // TODO: Dispatch a thunk to put information
    return (

           {
             currentUser.isLoggedIn ? (

                <input
                autoFocus
                type="text"
                name="name"
                className="singleArtistDashboardNameInput"
                value={currentArtist.name}
                onChange={this.handleChange}
                />

             ) : (

{currentArtist.name}

             )
           }

Biography

          {
            currentUser.isLoggedIn ? (

              <input
              type="text"
              name="bio"
              className="singleArtistDashboardBioInput"
              value={currentArtist.bio}
              onChange={this.handleChange}
              />

            ) : (

{currentArtist.bio}

            )
          }

          {
            currentUser.isLoggedIn ? (

              <input
                type="text"
                name="bio"
                className="singleArtistDashboardEmailInput"
                value={currentArtist.email}
                onChange={this.handleChange}
              />

            ) : (

{currentArtist.email}

            )
          }

        {
          currentUser.isLoggedIn && <Link className="btn btn-default" to="/gallery-create">New Gallery</Link>
        }

Galleries

            {
              currentArtist.galleries && currentArtist.galleries.map(gallery=>{
                return (

                  <Link className="singleUserGalleryLink" to={`/galleries/${gallery.id}`}>{gallery.title}</Link>

                  {
                    currentUser.isLoggedIn && <Link className="btn btn-warning edit-gallery-btn" to={`/gallery-edit/${gallery.id}`}>Edit</Link>
                  }

                );
              })
            }

Paintings

            {
              currentArtist.galleries && currentArtist.galleries.map(gallery =>{
                return gallery.paintings.map(painting =>{

                  return (

                      <Link to={`/paintings/${painting.id}`} className="singleUserPaintingName">{painting.name}</Link>

                  );
                });
              })
            }

    );
  }

}

const mapState = (state, ownProps) => {
  return {
    artistsCollection: state.users.artistsCollection,
    currentUser: state.currentUser
  };
};

export default connect(mapState)(SingleArtist);

// {
//   currentUser.isLoggedIn &&
//   (

//     {/*TODO: Use a modal to edit user bio? */}
//     <Button>Edit Profile</Button>
//

//   )
// }
<commit_msg>added conditional rendering for ui controls only if the single artist page belongs to the user<commit_after>import React, { Component } from 'react';
import { NavLink, Link} from 'react-router-dom';
import {connect} from 'react-redux';
import store from '../store';
import {Button} from 'react-bootstrap';

class SingleArtist extends Component {

  constructor(props) {
    super(props);

    const currentArtistId = this.props.match.params.id;
    const artists = this.props.artistsCollection;
    const currentArtist = artists.length && artists.filter(artist => +artist.id === +currentArtistId)[0];

    const {name, profileImageUrl, bio, email, galleries} = currentArtist;

    this.state = {
      name,
      profileImageUrl,
      bio,
      email,
      galleries,
    };

    this.checkIfOwnProfile = this.checkIfOwnProfile.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleOnBlur = this.handleOnBlur.bind(this);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.artistsCollection.length !== this.props.artistsCollection.length) {
      const currentArtistId = nextProps.match.params.id;
      const artists = nextProps.artistsCollection;
      const currentArtist = artists.length && artists.filter(artist => +artist.id === +currentArtistId)[0];
      const galleries = currentArtist.galleries;

      const {name, profileImageUrl, bio, email} = currentArtist;

      this.setState({
        name,
        profileImageUrl,
        bio,
        email,
        galleries
      });
    }
  }

  checkIfOwnProfile() {
    // check if logged in user id is the same as the one on the url
    const currentArtistId = +this.props.match.params.id;
    const currentUser = this.props.currentUser;
    if (currentUser.isLoggedIn && currentUser.id === currentArtistId) {
      return true;
    }
    return false;
  }

  handleChange(evt) {
    const name = evt.target.name;
    const value = evt.target.value;
    this.setState({
      [name]: value
    });
  }

  handleOnBlur(evt) {
    const name = evt.target.name;
    const value = evt.target.value;
    console.log('blurred', name, value);
  }

  render(){

    const currentArtist = this.state;
    const currentUser = this.props.currentUser;
   // TODO: Add user input version of name, biography, and email
   // TODO: Allow to delete galleries and images from dashboard
   // TODO: Dispatch a thunk to put information
    return (

           {
             this.checkIfOwnProfile() ? (

                <input
                autoFocus
                type="text"
                name="name"
                className="singleArtistDashboardNameInput"
                value={currentArtist.name}
                onChange={this.handleChange}
                onBlur={this.handleOnBlur}
                />

             ) : (

{currentArtist.name}

             )
           }

Biography

          {
            this.checkIfOwnProfile() ? (

              <input
              type="text"
              name="bio"
              className="singleArtistDashboardBioInput"
              value={currentArtist.bio}
              onChange={this.handleChange}
              />

            ) : (

{currentArtist.bio}

            )
          }

          {
            this.checkIfOwnProfile() ? (

              <input
                type="text"
                name="bio"
                className="singleArtistDashboardEmailInput"
                value={currentArtist.email}
                onChange={this.handleChange}
              />

            ) : (

{currentArtist.email}

            )
          }

        {
          this.checkIfOwnProfile() && <Link className="btn btn-default" to="/gallery-create">New Gallery</Link>
        }

Galleries

            {
              currentArtist.galleries && currentArtist.galleries.map(gallery=>{
                return (

                  <Link className="singleUserGalleryLink" to={`/galleries/${gallery.id}`}>{gallery.title}</Link>

                  {
                    this.checkIfOwnProfile() && <Link className="btn btn-warning edit-gallery-btn" to={`/gallery-edit/${gallery.id}`}>Edit</Link>
                  }

                );
              })
            }

Paintings

            {
              currentArtist.galleries && currentArtist.galleries.map(gallery =>{
                return gallery.paintings.map(painting =>{

                  return (

                      <Link to={`/paintings/${painting.id}`} className="singleUserPaintingName">{painting.name}</Link>

                  );
                });
              })
            }

    );
  }

}

const mapState = (state, ownProps) => {
  return {
    artistsCollection: state.users.artistsCollection,
    currentUser: state.currentUser
  };
};

export default connect(mapState)(SingleArtist);

// {
//   currentUser.isLoggedIn &&
//   (

//     {/*TODO: Use a modal to edit user bio? */}
//     <Button>Edit Profile</Button>
//

//   )
// }
<|endoftext|>