<commit_before>// bot本体

// https://github.com/ttezel/twit

var app = require('../app');
var Twit = require('twit');
var CronJob = require("cron").CronJob;
var moment = require('moment');
var tsv2json = require('node-tsv-json');
var logger = require('./logger');
var fs = require('fs');

// key情報
var T = new Twit({
  consumer_key: app.get('options').key,
  consumer_secret: app.get('options').secret,
  access_token: app.get('options').token,
  access_token_secret: app.get('options').token_secret
});

// botのユーザ名
var bot_name = app.get('options').bot_name;
var target_name = app.get('options').target_name;

// ランダムツイート一覧
var randomTweetList;
// 返信ツイート一覧
var replyTweetList;

var lastTweetId;

// ランダムツイートファイル名
var randomTweetListFile = app.get('options').rand_list_file;

// 返信ツイートファイル名
var replyTweetListFile = app.get('options').reply_list_file;

//--------------------------------------------------------------------------

// 特定のユーザのツイートを取得する
//　https://dev.twitter.com/rest/reference/get/application/rate_limit_status
function rate_limit() {
  var tweets = [];
  T.get('application/rate_limit_status', {}, function(err, data, response) {
    if (typeof data === 'undefined') {
      throw '取得失敗';
    }
    logger.system.info('[rate_limit]data');
    logger.system.info(JSON.stringify(data, null, 2));
  });
}

// つぶやく
//　https://dev.twitter.com/rest/reference/post/statuses/update
function postTweet(message, in_reply_to_status_id) {
  logger.system.info('[postTweet]:' + message);
  if (message) {
    T.post('statuses/update', { status: message, in_reply_to_status_id: in_reply_to_status_id, count: 200 }, function(err, data, response) {
      logger.system.trace('[tweet]data');
      logger.system.trace(data);
      logger.system.trace('[tweet]response');
      logger.system.trace(response);
      if (typeof data === 'undefined') {
        logger.system.error('ツイート失敗したかも');
      }
    });
  }
}

// 特定のユーザのツイートを取得してファイルを更新
//　https://dev.twitter.com/rest/reference/get/statuses/user_timeline
function getSpecUserTweetandUpdateFile(screen_name, since_id) {
  logger.system.debug('[getSpecUserTweetandUpdateFile]screen_name:' + screen_name + ' since_id:' + since_id);
  var tweets = [];

  T.get('statuses/user_timeline', { screen_name: screen_name, since_id: since_id, count: 200 }, function(err, data, response) {
    try {
      logger.system.trace('[tweet]data');
      logger.system.trace(data);
      logger.system.trace('[tweet]response');
      logger.system.trace(response);

      // 取得したツイートを格納
      data.forEach(function(val, i) {
        if (val.hasOwnProperty('id') && val.hasOwnProperty('text')) {
          tweets[i] = {
            'id': val.id,
            'text': val.text
          };
        }
      });

      // 最終ツイートIDを更新
      lastTweetId = tweets[0].id;

      // 新しいツイートを下にする
      tweets.reverse();

      // 各ツイートについて、いろいろ
      for (var i = 0; i < tweets.length - 1; i++) {

        // URL入りはとりあえず除外する
        if (/http/.test(tweets[i].text)) {
          continue;
        }

        /// リプライ系
        if (/@[a-zA-Z0-9]/.test(tweets[i].text)) {
          // ユーザ名は削除する
          var str = tweets[i].text.replace(/@[a-zA-Z0-9_]+\s?/g, '');
          fs.appendFile(replyTweetListFile, str + '\t' + tweets[i].id + '\n', 'utf-8');
        } else {
          fs.appendFile(randomTweetListFile, tweets[i].text + '\t' + tweets[i].id + '\n', 'utf-8');
        }
      }
      logger.system.info('ツイートファイルを更新しました。');

    } catch (e) {
      logger.system.error('[getSpecUserTweet]何かあった');
      logger.system.error(e);
    }
  });
}

// ストリーミングAPIを起動してツイートを受信する
// https://dev.twitter.com/overview/api/tweets
function startStreamingAPI() {
  logger.system.info('streamingAPI起動');
  //var stream = T.stream('user', { stringify_friend_ids: true });
  var stream = T.stream('user');

  stream.on('tweet', function(tweet) {
    try {
      logger.system.debug('[streamingAPI]name:' + tweet.user.name); // ツイートしたユーザの表示名
      logger.system.debug('[streamingAPI]screen_name:' + tweet.user.screen_name); // ユーザ名
      logger.system.debug('[streamingAPI]id:' + tweet.id); // ツイートID
      logger.system.debug('[streamingAPI]text:' + tweet.text);　 // ツイート本文
      logger.system.trace('[streamingAPI]in_reply_to_status_id:' + tweet.in_reply_to_status_id); // リプライ先のツイートID
      logger.system.trace('[streamingAPI]in_reply_to_status_id_str:' + tweet.in_reply_to_status_id_str);
      logger.system.trace('[streamingAPI]in_reply_to_user_id:' + tweet.in_reply_to_user_id); // 返信相手のユーザID(数字の方)
      logger.system.trace('[streamingAPI]in_reply_to_user_id_str:' + tweet.in_reply_to_user_id_str);
      logger.system.trace('[streamingAPI]in_reply_to_screen_name:' + tweet.in_reply_to_screen_name); // 返信相手のユーザ名

      // 自分に対するリプライだった時
      //if (tweet.in_reply_to_screen_name === bot_name) {
      // 本文中にIDが含まれていたら返信することにした。
      if (tweet.text.indexOf('@' + bot_name) != -1) {
        logger.system.debug('自分に返信が来た');

        // 返信相手
        var prefix = '';
        if (typeof tweet.user.screen_name !== undefined && tweet.user.screen_name !== null) {
          prefix = '@' + tweet.user.screen_name + ' ';
        }
        // 返信メッセージを決定
        var message = prefix + getReplyTweet();
        // 返信する
        postTweet(message, tweet.id);
      }

    } catch (e) {
      logger.system.error('[startStreamingAPI]何かあった');
      logger.system.error(e);
    }
  });
}

// yyyyMMddhhmmss時間を取得する
function getTime() {
  var dt = new Date();
  //年
  var year = dt.getFullYear();
  //月
  //1月が0、12月が11。そのため+1をする。
  var month = dt.getMonth() + 1;
  if (month < 10) {
    month = '0' + month;
  }
  //日
  var date = dt.getDate();
  if (date < 10) {
    date = '0' + date;
  }
  //時
  var hours = dt.getHours();
  if (hours < 10) {
    hours = '0' + hours;
  }
  //分
  var minutes = dt.getMinutes();
  if (minutes < 10) {
    minutes = '0' + minutes;
  }
  //秒
  var seconds = dt.getSeconds();
  if (seconds < 10) {
    seconds = '0' + seconds;
  }

  var time = '' + year + month + date + hours + minutes + seconds;
  return time;
}

// リプライ用のツイートをランダムに選定
function getReplyTweet() {
  var message = null;

  try {
    message = replyTweetList[Math.floor(Math.random() * replyTweetList.length)];
  } catch (e) {
    logger.system.error('[getReplyTweet]何かあった');
    logger.system.error(e);
  } finally {
    return message;
  }
}

// 普段つぶやくランダムツイート
function getRandomTweet() {
  var message = null;

  try {
    message = randomTweetList[Math.floor(Math.random() * randomTweetList.length)];
  } catch (e) {
    logger.system.error('[getReplyTweet]何かあった');
    logger.system.error(e);
  } finally {
    return message;
  }
}

// CSVからランダム分に格納する
function updateRandomTweetList() {
  logger.system.debug('[updateRandomTweetList]');
  var tmp = [];
  try {
    tsv2json({
      input: randomTweetListFile,
      output: null,
      parseRows: true
    }, function(err, result) {
      if (err) {
        logger.system.error(err);
      } else {
        var id;
        result.forEach(function(v) {
          if (v[1]) {
            id = v[1];
          }
          tmp.push(v[0]);
        });
        randomTweetList = tmp;
        // 最終ツイートIDを更新
        if (id) {
          lastTweetId = id;
        }
      }
      logger.system.info('最終ツイートID:' + lastTweetId);
      logger.system.trace(randomTweetList);
    });

  } catch (e) {
    logger.system.error('[updateRandomTweetList]何かあった');
    logger.system.error(e);
  }
}

// CSVから返信リストに格納する
function updateReplyTweetList() {
  logger.system.debug('[updateReplyTweetList]');
  var tmp = [];
  try {
    tsv2json({
      input: replyTweetListFile,
      output: null,
      parseRows: true
    }, function(err, result) {
      if (err) {
        logger.system.error(err);
      } else {
        result.forEach(function(v) {
          tmp.push(v[0]);
        });
        replyTweetList = tmp;
      }
      logger.system.trace(replyTweetList);
    });

  } catch (e) {
    logger.system.error('[updateReplyTweetList]何かあった');
    logger.system.error(e);
  }
}

//--------------------------------------------------------------------------
// 実行内容
// 定期実行する
// 秒 分 時 日 月 週
var cronTweet = new CronJob({
  cronTime: '0 */20 6-23 * * *',
  //cronTime: '0 * * * * *',
  onTick: function() {
    postTweet(getRandomTweet());
  },
  start: false,
});

var cronUpdate = new CronJob({
  cronTime: '0 0 0 * * *',
  onTick: function() {
    // ツイート取得とファイル更新
    // 非同期だから反映は翌日
    getSpecUserTweetandUpdateFile(target_name, lastTweetId);
    // ファイルから読み込み
    updateRandomTweetList();
    updateReplyTweetList();
  },
  start: false,
});
//--------------------------------------------------------------------------

// APIの残量
//rate_limit();

// cronを実行
cronUpdate.start();
cronTweet.start();

// ストリーミングAPI起動
startStreamingAPI();

// ツイート取得とファイル更新
//getSpecUserTweetandUpdateFile(target_name, lastTweetId);

// テキストから読み込む
updateRandomTweetList();
updateReplyTweetList();

//---------------------------------------------------------------------------

<commit_msg>id_strにしないと正しく数値を取り扱えないので修正<commit_after>// bot本体

// https://github.com/ttezel/twit

var app = require('../app');
var Twit = require('twit');
var CronJob = require("cron").CronJob;
var moment = require('moment');
var tsv2json = require('node-tsv-json');
var logger = require('./logger');
var fs = require('fs');

// key情報
var T = new Twit({
  consumer_key: app.get('options').key,
  consumer_secret: app.get('options').secret,
  access_token: app.get('options').token,
  access_token_secret: app.get('options').token_secret
});

// botのユーザ名
var bot_name = app.get('options').bot_name;
var target_name = app.get('options').target_name;

// ランダムツイート一覧
var randomTweetList;
// 返信ツイート一覧
var replyTweetList;

var lastTweetId;

// ランダムツイートファイル名
var randomTweetListFile = app.get('options').rand_list_file;

// 返信ツイートファイル名
var replyTweetListFile = app.get('options').reply_list_file;

//--------------------------------------------------------------------------

// 特定のユーザのツイートを取得する
//　https://dev.twitter.com/rest/reference/get/application/rate_limit_status
function rate_limit() {
  var tweets = [];
  T.get('application/rate_limit_status', {}, function (err, data, response) {
    if (typeof data === 'undefined') {
      throw '取得失敗';
    }
    logger.system.info('[rate_limit]data');
    logger.system.info(JSON.stringify(data, null, 2));
  });
}

// つぶやく
//　https://dev.twitter.com/rest/reference/post/statuses/update
function postTweet(message, in_reply_to_status_id) {
  logger.system.info('[postTweet]:' + message);
  if (message) {
    T.post('statuses/update', { status: message, in_reply_to_status_id: in_reply_to_status_id, count: 200 }, function (err, data, response) {
      logger.system.trace('[tweet]data');
      logger.system.trace(data);
      logger.system.trace('[tweet]response');
      logger.system.trace(response);
      if (typeof data === 'undefined') {
        logger.system.error('ツイート失敗したかも');
      }
    });
  }
}

// 特定のユーザのツイートを取得してファイルを更新
//　https://dev.twitter.com/rest/reference/get/statuses/user_timeline
function getSpecUserTweetandUpdateFile(screen_name, since_id) {
  logger.system.debug('[getSpecUserTweetandUpdateFile]screen_name:' + screen_name + ' since_id:' + since_id);
  var tweets = [];

  T.get('statuses/user_timeline', { screen_name: screen_name, since_id: since_id, count: 200 }, function (err, data, response) {
    try {
      logger.system.trace('[tweet]data');
      logger.system.trace(data);
      logger.system.trace('[tweet]response');
      logger.system.trace(response);

      // 取得したツイートを格納
      data.forEach(function (val, i) {
        if (val.hasOwnProperty('id') && val.hasOwnProperty('text')) {
          tweets[i] = {
            'id_str': val.id_str,
            'text': val.text
          };
        }
      });

      // 最終ツイートIDを更新
      lastTweetId = tweets[0].id_str;

      // 新しいツイートを下にする
      tweets.reverse();

      // 各ツイートについて、いろいろ
      for (var i = 0; i < tweets.length - 1; i++) {

        // URL入りはとりあえず除外する
        if (/http/.test(tweets[i].text)) {
          continue;
        }

        // 改行入りも除外する
        if (/\n/.test(tweets[i].text)) {
          continue;
        }

        // 非公式RTも除外する
        if (/RT\s/.test(tweets[i].text)) {
          continue;
        }

        /// リプライ系
        if (/@[a-zA-Z0-9\-]/.test(tweets[i].text)) {
          // ユーザ名は削除する
          var str = tweets[i].text.replace(/@[a-zA-Z0-9_\-]+\s?/g, '');
          // 空白を除去
          str = str.trim();
          // 空文字になったら登録しない
          if (str === '') {
            continue;
          }
          fs.appendFile(replyTweetListFile, str + '\t' + tweets[i].id_str + '\n', 'utf-8');
        } else {
          fs.appendFile(randomTweetListFile, tweets[i].text + '\t' + tweets[i].id_str + '\n', 'utf-8');
        }
      }
      logger.system.info('ツイートファイルを更新しました。');

    } catch (e) {
      logger.system.error('[getSpecUserTweet]何かあった');
      logger.system.error(e);
    }
  });
}

// ストリーミングAPIを起動してツイートを受信する
// https://dev.twitter.com/overview/api/tweets
function startStreamingAPI() {
  logger.system.info('streamingAPI起動');
  //var stream = T.stream('user', { stringify_friend_ids: true });
  var stream = T.stream('user');

  stream.on('tweet', function (tweet) {
    try {
      logger.system.debug('[streamingAPI]name:' + tweet.user.name); // ツイートしたユーザの表示名
      logger.system.debug('[streamingAPI]screen_name:' + tweet.user.screen_name); // ユーザ名
      logger.system.debug('[streamingAPI]id:' + tweet.id); // ツイートID
      logger.system.debug('[streamingAPI]text:' + tweet.text);　 // ツイート本文
      logger.system.trace('[streamingAPI]in_reply_to_status_id:' + tweet.in_reply_to_status_id); // リプライ先のツイートID
      logger.system.trace('[streamingAPI]in_reply_to_status_id_str:' + tweet.in_reply_to_status_id_str);
      logger.system.trace('[streamingAPI]in_reply_to_user_id:' + tweet.in_reply_to_user_id); // 返信相手のユーザID(数字の方)
      logger.system.trace('[streamingAPI]in_reply_to_user_id_str:' + tweet.in_reply_to_user_id_str);
      logger.system.trace('[streamingAPI]in_reply_to_screen_name:' + tweet.in_reply_to_screen_name); // 返信相手のユーザ名

      // 自分に対するリプライだった時
      //if (tweet.in_reply_to_screen_name === bot_name) {
      // 本文中にIDが含まれていたら返信することにした。
      if (tweet.text.indexOf('@' + bot_name) != -1) {
        logger.system.debug('自分に返信が来た');

        // 返信相手
        var prefix = '';
        if (typeof tweet.user.screen_name !== undefined && tweet.user.screen_name !== null) {
          prefix = '@' + tweet.user.screen_name + ' ';
        }
        // 返信メッセージを決定
        var message = prefix + getReplyTweet();
        // 返信する
        postTweet(message, tweet.id_str);
      }

    } catch (e) {
      logger.system.error('[startStreamingAPI]何かあった');
      logger.system.error(e);
    }
  });
}

// yyyyMMddhhmmss時間を取得する
function getTime() {
  var dt = new Date();
  //年
  var year = dt.getFullYear();
  //月
  //1月が0、12月が11。そのため+1をする。
  var month = dt.getMonth() + 1;
  if (month < 10) {
    month = '0' + month;
  }
  //日
  var date = dt.getDate();
  if (date < 10) {
    date = '0' + date;
  }
  //時
  var hours = dt.getHours();
  if (hours < 10) {
    hours = '0' + hours;
  }
  //分
  var minutes = dt.getMinutes();
  if (minutes < 10) {
    minutes = '0' + minutes;
  }
  //秒
  var seconds = dt.getSeconds();
  if (seconds < 10) {
    seconds = '0' + seconds;
  }

  var time = '' + year + month + date + hours + minutes + seconds;
  return time;
}

// リプライ用のツイートをランダムに選定
function getReplyTweet() {
  var message = null;

  try {
    message = replyTweetList[Math.floor(Math.random() * replyTweetList.length)];
  } catch (e) {
    logger.system.error('[getReplyTweet]何かあった');
    logger.system.error(e);
  } finally {
    return message;
  }
}

// 普段つぶやくランダムツイート
function getRandomTweet() {
  var message = null;

  try {
    message = randomTweetList[Math.floor(Math.random() * randomTweetList.length)];
  } catch (e) {
    logger.system.error('[getReplyTweet]何かあった');
    logger.system.error(e);
  } finally {
    return message;
  }
}

// CSVからランダム分に格納する
function updateRandomTweetList() {
  logger.system.debug('[updateRandomTweetList]');
  var tmp = [];
  try {
    tsv2json({
      input: randomTweetListFile,
      output: null,
      parseRows: true
    }, function (err, result) {
      if (err) {
        logger.system.error(err);
      } else {
        var id;
        result.forEach(function (v) {
          if (v[1]) {
            id = v[1];
          }
          tmp.push(v[0]);
        });
        randomTweetList = tmp;
        // 最終ツイートIDを更新
        if (id) {
          lastTweetId = id;
        }
      }
      logger.system.info('最終ツイートID:' + lastTweetId);
      logger.system.trace(randomTweetList);
    });

  } catch (e) {
    logger.system.error('[updateRandomTweetList]何かあった');
    logger.system.error(e);
  }
}

// CSVから返信リストに格納する
function updateReplyTweetList() {
  logger.system.debug('[updateReplyTweetList]');
  var tmp = [];
  try {
    tsv2json({
      input: replyTweetListFile,
      output: null,
      parseRows: true
    }, function (err, result) {
      if (err) {
        logger.system.error(err);
      } else {
        result.forEach(function (v) {
          tmp.push(v[0]);
        });
        replyTweetList = tmp;
      }
      logger.system.trace(replyTweetList);
    });

  } catch (e) {
    logger.system.error('[updateReplyTweetList]何かあった');
    logger.system.error(e);
  }
}

//--------------------------------------------------------------------------
// 実行内容
// 定期実行する
// 秒 分 時 日 月 週
var cronTweet = new CronJob({
  cronTime: '0 */20 6-23 * * *',
  //cronTime: '0 * * * * *',
  onTick: function () {
    postTweet(getRandomTweet());
  },
  start: false,
});

var cronUpdate = new CronJob({
  cronTime: '0 0 0 * * *',
  onTick: function () {
    // ツイート取得とファイル更新
    // 非同期だから反映は翌日
    getSpecUserTweetandUpdateFile(target_name, lastTweetId);
    // ファイルから読み込み
    updateRandomTweetList();
    updateReplyTweetList();
  },
  start: false,
});
//--------------------------------------------------------------------------

// APIの残量
//rate_limit();

// cronを実行
cronUpdate.start();
cronTweet.start();

// ストリーミングAPI起動
startStreamingAPI();

// ツイート取得とファイル更新
//getSpecUserTweetandUpdateFile(target_name, lastTweetId);

// テキストから読み込む
updateRandomTweetList();
updateReplyTweetList();

//---------------------------------------------------------------------------

<|endoftext|>


<commit_before>import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import emojify from '../../../emoji';
import escapeTextContentForBrowser from 'escape-html';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Avatar from '../../../components/avatar';
import ImmutablePureComponent from 'react-immutable-pure-component';

const messages = defineMessages({
  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
  follow: { id: 'account.follow', defaultMessage: 'Follow' },
  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
});

/*
  THIS IS A MESS BECAUSE EFFING MASTODON AND ITS EFFING HTML BIOS
  INSTEAD OF JUST STORING EVERYTHING IN PLAIN EFFING TEXT ! ! ! !
  BLANK LINES ALSO WON'T WORK BECAUSE RIGHT NOW MASTODON CONVERTS
  THOSE INTO `<P>` ELEMENTS INSTEAD OF LEAVING IT AS `<BR><BR>` !
  TL:DR; THIS IS LARGELY A HACK. WITH BETTER BACKEND STUFF WE CAN
  IMPROVE THIS BY BETTER PREDICTING HOW THE METADATA WILL BE SENT
  WHILE MAINTAINING BASIC PLAIN-TEXT PROCESSING. THE OTHER OPTION
  IS TO TURN ALL BIOS INTO PLAIN-TEXT VIA A TREE-WALKER, AND THEN
  PROCESS THE YAML AND LINKS AND EVERYTHING OURSELVES. THIS WOULD
  BE INCREDIBLY COMPLICATED, AND IT WOULD BE A MILLION TIMES LESS
  DIFFICULT IF MASTODON JUST GAVE US PLAIN-TEXT BIOS (WHICH QUITE
  FRANKLY MAKES THE MOST SENSE SINCE THAT'S WHAT USERS PROVIDE IN
  SETTINGS) TO BEGIN WITH AND LEFT ALL PROCESSING TO THE FRONTEND
  TO HANDLE ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
  ANYWAY I KNOW WHAT NEEDS TO BE DONE REGARDING BACKEND STUFF BUT
  I'M NOT SMART ENOUGH TO FIGURE OUT HOW TO ACTUALLY IMPLEMENT IT
  SO FEEL FREE TO @ ME IF YOU NEED MY IDEAS REGARDING THAT. UNTIL
  THEN WE'LL JUST HAVE TO MAKE DO WITH THIS MESSY AND UNFORTUNATE
  HACKING ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

                                           with love,
                                           @kibi@glitch.social <3
*/

const NEW_LINE    = /(?:^|\r?\n|
)/g;
const YAML_OPENER = /---/;
const YAML_CLOSER = /(?:---|\.\.\.)/;
const YAML_STRING = /(?:"(?:[^"\n]){1,32}"|'(?:[^'\n]){1,32}'|(?:[^'":\n]){1,32})/g;
const YAML_LINE = new RegExp('\\s*' + YAML_STRING.source + '\\s*:\\s*' + YAML_STRING.source + '\\s*', 'g');
const BIO_REGEX = new RegExp(NEW_LINE.source + '*' + YAML_OPENER.source + NEW_LINE.source + '+(?:' + YAML_LINE.source + NEW_LINE.source + '+){0,4}' + YAML_CLOSER.source + NEW_LINE.source + '*');

const processBio = (data) => {
  let props = { text: data, metadata: [] };
  let yaml = data.match(BIO_REGEX);
  if (!yaml) return props;
  else yaml = yaml[0];
  let start = props.text.indexOf(yaml);
  let end = start + yaml.length;
  props.text = props.text.substr(0, start) + props.text.substr(end);
  yaml = yaml.replace(NEW_LINE, '\n');
  let metadata = (yaml ? yaml.match(YAML_LINE) : []) || [];
  for (let i = 0; i < metadata.length; i++) {
    let result = metadata[i].match(YAML_STRING);
    if (result[0][0] === '"' || result[0][0] === '\'') result[0] = result[0].substr(1, result[0].length - 2);
    if (result[1][0] === '"' || result[1][0] === '\'') result[0] = result[1].substr(1, result[1].length - 2);
    props.metadata.push(result);
  }
  return props;
};

const makeMapStateToProps = () => {
  const mapStateToProps = state => ({
    autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
  });

  return mapStateToProps;
};

class Avatar extends ImmutablePureComponent {

  static propTypes = {
    account: ImmutablePropTypes.map.isRequired,
    autoPlayGif: PropTypes.bool.isRequired,
  };

  state = {
    isHovered: false,
  };

  handleMouseOver = () => {
    if (this.state.isHovered) return;
    this.setState({ isHovered: true });
  }

  handleMouseOut = () => {
    if (!this.state.isHovered) return;
    this.setState({ isHovered: false });
  }

  render () {
    const { account, autoPlayGif }   = this.props;
    const { isHovered } = this.state;

    return (
      <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
        {({ radius }) =>
          <a // eslint-disable-line jsx-a11y/anchor-has-content
            href={account.get('url')}
            className='account__header__avatar'
            target='_blank'
            rel='noopener'
            style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
            onMouseOver={this.handleMouseOver}
            onMouseOut={this.handleMouseOut}
            onFocus={this.handleMouseOver}
            onBlur={this.handleMouseOut}
          />
        }
      </Motion>
    );
  }

}

@connect(makeMapStateToProps)
@injectIntl
export default class Header extends ImmutablePureComponent {

  static propTypes = {
    account: ImmutablePropTypes.map,
    me: PropTypes.number.isRequired,
    onFollow: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };

  render () {
    const { account, me, intl } = this.props;

    if (!account) {
      return null;
    }

    let displayName = account.get('display_name');
    let info        = '';
    let actionBtn   = '';
    let lockedIcon  = '';

    if (displayName.length === 0) {
      displayName = account.get('username');
    }

    if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
      info = <FormattedMessage id='account.follows_you' defaultMessage='Follows you' />;
    }

    if (me !== account.get('id')) {
      if (account.getIn(['relationship', 'requested'])) {
        actionBtn = (

            <IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />

        );
      } else if (!account.getIn(['relationship', 'blocking'])) {
        actionBtn = (

            <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />

        );
      }
    }

    if (account.get('locked')) {
      lockedIcon = ;
    }

    const displayNameHTML    = { __html: emojify(escapeTextContentForBrowser(displayName)) };
    const { text, metadata } = processBio(account.get('note'));

    return (

        <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>

              <Avatar src={account.get('avatar')} animate size={90} />

            @{account.get('acct')} {lockedIcon}

            {info}
            {actionBtn}

        {metadata.length && (

            {(() => {
              let data = [];
              for (let i = 0; i < metadata.length; i++) {
                data.push(

                );
              }
              return data;
            })()}

        ) || null}

    );
  }

}
<commit_msg>clean up old avatar class<commit_after>import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import emojify from '../../../emoji';
import escapeTextContentForBrowser from 'escape-html';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Avatar from '../../../components/avatar';
import ImmutablePureComponent from 'react-immutable-pure-component';

const messages = defineMessages({
  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
  follow: { id: 'account.follow', defaultMessage: 'Follow' },
  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
});

/*
  THIS IS A MESS BECAUSE EFFING MASTODON AND ITS EFFING HTML BIOS
  INSTEAD OF JUST STORING EVERYTHING IN PLAIN EFFING TEXT ! ! ! !
  BLANK LINES ALSO WON'T WORK BECAUSE RIGHT NOW MASTODON CONVERTS
  THOSE INTO `<P>` ELEMENTS INSTEAD OF LEAVING IT AS `<BR><BR>` !
  TL:DR; THIS IS LARGELY A HACK. WITH BETTER BACKEND STUFF WE CAN
  IMPROVE THIS BY BETTER PREDICTING HOW THE METADATA WILL BE SENT
  WHILE MAINTAINING BASIC PLAIN-TEXT PROCESSING. THE OTHER OPTION
  IS TO TURN ALL BIOS INTO PLAIN-TEXT VIA A TREE-WALKER, AND THEN
  PROCESS THE YAML AND LINKS AND EVERYTHING OURSELVES. THIS WOULD
  BE INCREDIBLY COMPLICATED, AND IT WOULD BE A MILLION TIMES LESS
  DIFFICULT IF MASTODON JUST GAVE US PLAIN-TEXT BIOS (WHICH QUITE
  FRANKLY MAKES THE MOST SENSE SINCE THAT'S WHAT USERS PROVIDE IN
  SETTINGS) TO BEGIN WITH AND LEFT ALL PROCESSING TO THE FRONTEND
  TO HANDLE ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
  ANYWAY I KNOW WHAT NEEDS TO BE DONE REGARDING BACKEND STUFF BUT
  I'M NOT SMART ENOUGH TO FIGURE OUT HOW TO ACTUALLY IMPLEMENT IT
  SO FEEL FREE TO @ ME IF YOU NEED MY IDEAS REGARDING THAT. UNTIL
  THEN WE'LL JUST HAVE TO MAKE DO WITH THIS MESSY AND UNFORTUNATE
  HACKING ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

                                           with love,
                                           @kibi@glitch.social <3
*/

const NEW_LINE    = /(?:^|\r?\n|
)/g;
const YAML_OPENER = /---/;
const YAML_CLOSER = /(?:---|\.\.\.)/;
const YAML_STRING = /(?:"(?:[^"\n]){1,32}"|'(?:[^'\n]){1,32}'|(?:[^'":\n]){1,32})/g;
const YAML_LINE = new RegExp('\\s*' + YAML_STRING.source + '\\s*:\\s*' + YAML_STRING.source + '\\s*', 'g');
const BIO_REGEX = new RegExp(NEW_LINE.source + '*' + YAML_OPENER.source + NEW_LINE.source + '+(?:' + YAML_LINE.source + NEW_LINE.source + '+){0,4}' + YAML_CLOSER.source + NEW_LINE.source + '*');

const processBio = (data) => {
  let props = { text: data, metadata: [] };
  let yaml = data.match(BIO_REGEX);
  if (!yaml) return props;
  else yaml = yaml[0];
  let start = props.text.indexOf(yaml);
  let end = start + yaml.length;
  props.text = props.text.substr(0, start) + props.text.substr(end);
  yaml = yaml.replace(NEW_LINE, '\n');
  let metadata = (yaml ? yaml.match(YAML_LINE) : []) || [];
  for (let i = 0; i < metadata.length; i++) {
    let result = metadata[i].match(YAML_STRING);
    if (result[0][0] === '"' || result[0][0] === '\'') result[0] = result[0].substr(1, result[0].length - 2);
    if (result[1][0] === '"' || result[1][0] === '\'') result[0] = result[1].substr(1, result[1].length - 2);
    props.metadata.push(result);
  }
  return props;
};

@injectIntl
export default class Header extends ImmutablePureComponent {

  static propTypes = {
    account: ImmutablePropTypes.map,
    me: PropTypes.number.isRequired,
    onFollow: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };

  render () {
    const { account, me, intl } = this.props;

    if (!account) {
      return null;
    }

    let displayName = account.get('display_name');
    let info        = '';
    let actionBtn   = '';
    let lockedIcon  = '';

    if (displayName.length === 0) {
      displayName = account.get('username');
    }

    if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
      info = <FormattedMessage id='account.follows_you' defaultMessage='Follows you' />;
    }

    if (me !== account.get('id')) {
      if (account.getIn(['relationship', 'requested'])) {
        actionBtn = (

            <IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />

        );
      } else if (!account.getIn(['relationship', 'blocking'])) {
        actionBtn = (

            <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />

        );
      }
    }

    if (account.get('locked')) {
      lockedIcon = ;
    }

    const displayNameHTML    = { __html: emojify(escapeTextContentForBrowser(displayName)) };
    const { text, metadata } = processBio(account.get('note'));

    return (

        <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>

              <Avatar src={account.get('avatar')} animate size={90} />

            @{account.get('acct')} {lockedIcon}

            {info}
            {actionBtn}

        {metadata.length && (

            {(() => {
              let data = [];
              for (let i = 0; i < metadata.length; i++) {
                data.push(

                );
              }
              return data;
            })()}

        ) || null}

    );
  }

}
<|endoftext|>


<commit_before>
const urlChar = '[^\\s"<>\\]\\[\\(\\)]'
const urlCharEnd = urlChar.replace(/\]$/, '.,\']') // insert bad chars to end on
const imagePath = '(?:(?:\\.(?:tiff?|jpe?g|gif|png|svg|ico)|ipfs/[a-z\\d]{40,}))'
const domainPath = '(?:[-a-zA-Z0-9\\._]*[-a-zA-Z0-9])'
const urlChars = '(?:' + urlChar + '*' + urlCharEnd + ')?'

const urlSet = ({domain = domainPath, path} = {}) => {
     // urlChars is everything but html or markdown stop chars
    return `https?:\/\/${domain}(?::\\d{2,5})?(?:[/\\?#]${urlChars}${path ? path : ''})${path ? '' : '?'}`
}

/**
    Unless your using a 'g' (glob) flag you can store and re-use your regular expression.  Use the cache below.  If your using a glob (for example: replace all), the regex object becomes stateful and continues where it left off when called with the same string so naturally the regexp object can't be cached for long.
*/
export const any = (flags = 'i') => new RegExp(urlSet(), flags)
export const local = (flags = 'i') => new RegExp(urlSet({domain: '(?:localhost|(?:.*\\.)?steemit.com)'}), flags)
export const remote = (flags = 'i') => new RegExp(urlSet({domain: `(?!localhost|(?:.*\\.)?steemit.com)${domainPath}`}), flags)
export const youTube = (flags = 'i') => new RegExp(urlSet({domain: '(?:(?:.*\.)?youtube.com|youtu.be)'}), flags)
export const image = (flags = 'i') => new RegExp(urlSet({path: imagePath}), flags)
export const imageFile = (flags = 'i') => new RegExp(imagePath, flags)
// export const nonImage = (flags = 'i') => new RegExp(urlSet({path: '!' + imageFile}), flags)
// export const markDownImageRegExp = (flags = 'i') => new RegExp('\!\[[\w\s]*\]\(([^\)]+)\)', flags);

export default {
    any: any(),
    local: local(),
    remote: remote(),
    image: image(),
    imageFile: imageFile(),
    youTube: youTube(),
    youTubeId: /(?:(?:youtube.com\/watch\?v=)|(?:youtu.be\/)|(?:youtube.com\/embed\/))([A-Za-z0-9\_\-]+)/i,
    vimeoId: /(?:vimeo.com\/|player.vimeo.com\/video\/)([0-9]+)/,
    // simpleLink: new RegExp(`<a href="(.*)">(.*)<\/a>`, 'ig'),
    ipfsPrefix: /(https?:\/\/.*)?\/ipfs/i,
}

//possible this should go somewhere else.
export const makeParams = (params, prefix) => {
    let paramsList = [];
    if(params.constructor === Array) {
        paramsList = params;
    } else {
        Object.entries(params).forEach(([key, value]) => {
            paramsList.push(`${key}=${value}`);
        });
    }
    if(paramsList.length > 0) {
        return ((prefix !== false)? ((typeof prefix === 'string')? prefix : '?') : '') + paramsList.join('&');
    }
    return '';
}

// Original regex
// const urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';

// About performance
// Using exec on the same regex object requires a new regex to be created and compile for each text (ex: post).  Instead replace can be used `body.replace(remoteRe, l => {` discarding the result for better performance`}).  Re-compiling is a chrome bottleneck but did not effect nodejs.

<commit_msg>#1989 put addParams in Links for multi-file access<commit_after>
const urlChar = '[^\\s"<>\\]\\[\\(\\)]'
const urlCharEnd = urlChar.replace(/\]$/, '.,\']') // insert bad chars to end on
const imagePath = '(?:(?:\\.(?:tiff?|jpe?g|gif|png|svg|ico)|ipfs/[a-z\\d]{40,}))'
const domainPath = '(?:[-a-zA-Z0-9\\._]*[-a-zA-Z0-9])'
const urlChars = '(?:' + urlChar + '*' + urlCharEnd + ')?'

const urlSet = ({domain = domainPath, path} = {}) => {
     // urlChars is everything but html or markdown stop chars
    return `https?:\/\/${domain}(?::\\d{2,5})?(?:[/\\?#]${urlChars}${path ? path : ''})${path ? '' : '?'}`
}

/**
    Unless your using a 'g' (glob) flag you can store and re-use your regular expression.  Use the cache below.  If your using a glob (for example: replace all), the regex object becomes stateful and continues where it left off when called with the same string so naturally the regexp object can't be cached for long.
*/
export const any = (flags = 'i') => new RegExp(urlSet(), flags)
export const local = (flags = 'i') => new RegExp(urlSet({domain: '(?:localhost|(?:.*\\.)?steemit.com)'}), flags)
export const remote = (flags = 'i') => new RegExp(urlSet({domain: `(?!localhost|(?:.*\\.)?steemit.com)${domainPath}`}), flags)
export const youTube = (flags = 'i') => new RegExp(urlSet({domain: '(?:(?:.*\.)?youtube.com|youtu.be)'}), flags)
export const image = (flags = 'i') => new RegExp(urlSet({path: imagePath}), flags)
export const imageFile = (flags = 'i') => new RegExp(imagePath, flags)
// export const nonImage = (flags = 'i') => new RegExp(urlSet({path: '!' + imageFile}), flags)
// export const markDownImageRegExp = (flags = 'i') => new RegExp('\!\[[\w\s]*\]\(([^\)]+)\)', flags);

export default {
    any: any(),
    local: local(),
    remote: remote(),
    image: image(),
    imageFile: imageFile(),
    youTube: youTube(),
    youTubeId: /(?:(?:youtube.com\/watch\?v=)|(?:youtu.be\/)|(?:youtube.com\/embed\/))([A-Za-z0-9\_\-]+)/i,
    vimeoId: /(?:vimeo.com\/|player.vimeo.com\/video\/)([0-9]+)/,
    // simpleLink: new RegExp(`<a href="(.*)">(.*)<\/a>`, 'ig'),
    ipfsPrefix: /(https?:\/\/.*)?\/ipfs/i,
}

//todo: possible this should go somewhere else.
/**
 * Returns a new object extended from outputParams with [key] == inputParams[key] if the value is in allowedValues
 * @param outputParams
 * @param inputParams
 * @param key
 * @param allowedValues
 * @returns {*}
 */
export const addToParams = (outputParams, inputParams, key, allowedValues) => {
    const respParams = Object.assign({}, outputParams);
    if(inputParams[key] && allowedValues.indexOf(inputParams[key]) > -1) {
        respParams[key] = inputParams[key];
    }
    return respParams;
}

//todo: possible this should go somewhere else.
export const makeParams = (params, prefix) => {
    let paramsList = [];
    if(params.constructor === Array) {
        paramsList = params;
    } else {
        Object.entries(params).forEach(([key, value]) => {
            paramsList.push(`${key}=${value}`);
        });
    }
    if(paramsList.length > 0) {
        return ((prefix !== false)? ((typeof prefix === 'string')? prefix : '?') : '') + paramsList.join('&');
    }
    return '';
}

// Original regex
// const urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';

// About performance
// Using exec on the same regex object requires a new regex to be created and compile for each text (ex: post).  Instead replace can be used `body.replace(remoteRe, l => {` discarding the result for better performance`}).  Re-compiling is a chrome bottleneck but did not effect nodejs.

<|endoftext|>


<commit_before>#!/usr/bin/env node
var uncss   = require('../lib/uncss.js'),
    utility = require('../lib/lib.js'),
    program = require('commander'),
    buffer  = '',
    options;

program
    .version('0.4.4')
    .usage('[options] <file ...>')
    .option('-c, --compress', 'Compress CSS output')
    .option('-i, --ignore <selector, ...>', 'Do not remove given selectors')
    .parse(process.argv);

options = {
    compress: program.compress || false,
    ignore: program.ignore
        ? program.ignore.split(', ')
        : []
};

if (program.args.length === 0) {
    // No files were specified, read html from stdin
    process.stdin.resume();
    process.stdin.setEncoding('utf8');

    process.stdin.on('data', function (chunk) {
        buffer += chunk;
    });

    process.stdin.on('end', function () {
        uncss(buffer, options, function (uncss) {
            console.log(uncss);
        });
    });
} else {
    uncss(program.args, options, function (uncss) {
        console.log(uncss);
    });
}

<commit_msg>Added the two new options to the command line<commit_after>#!/usr/bin/env node
var uncss   = require('../lib/uncss.js'),
    utility = require('../lib/lib.js'),
    program = require('commander'),
    buffer  = '',
    options;

program
    .version('0.4.4')
    .usage('[options] <file, ...>')
    .option('-c, --compress', 'Compress CSS output')
    .option('-i, --ignore <selector, ...>', 'Do not remove given selectors')
    .option('-C, --csspath <path>', 'Relative path where the CSS files are located')
    .option('-s, --stylesheets <file, ...>', 'Specify additional stylesheets to process')
    .parse(process.argv);

options = {
    compress: program.compress || false,
    ignore: program.ignore
        ? program.ignore.split(', ')
        : []
};

if (program.args.length === 0) {
    // No files were specified, read html from stdin
    process.stdin.resume();
    process.stdin.setEncoding('utf8');

    process.stdin.on('data', function (chunk) {
        buffer += chunk;
    });

    process.stdin.on('end', function () {
        uncss(buffer, options, function (uncss) {
            console.log(uncss);
        });
    });
} else {
    uncss(program.args, options, function (uncss) {
        console.log(uncss);
    });
}

<|endoftext|>


<commit_before>"use strict";

angular.module('arethusa.relation').directive('nestedMenuCollection', [
  '$window',
  function($window) {
    return {
      restrict: 'A',
      replace: 'true',
      scope: {
        current: '=',
        all: '=',
        property: '=',
        ancestors: '=',
        emptyVal: '@',
        labelAs: "=",
      },
      link: function(scope, element, attrs) {
        var win = angular.element($window);

        scope.emptyLabel = "";
        scope.emptyObj = {};

        scope.labelView = function(labelObj) {
          if (scope.labelAs) {
            return labelObj[scope.labelAs];
          } else {
            return labelObj.short;
          }
        };

        if (element.hasClass('top-menu')) {
          var items = Object.keys(scope.all || {}).length + 1; // an empty val
          var menuHeight = items * 18; // hard to access, we therefore hardcode...
          var maxHeight = win.height() - 15;
          var topPos = element.parent().offset().top;
          var bottom = topPos + menuHeight;
          if (bottom > maxHeight) {
            element.css({ top: 'auto', bottom: '100%'});
          }
        }
      },
      template: '\

\

\

\

\

\

\
      '
    };
  }
]);
<commit_msg>Don't reposition nMCollection for huge lists<commit_after>"use strict";

angular.module('arethusa.relation').directive('nestedMenuCollection', [
  '$window',
  function($window) {
    return {
      restrict: 'A',
      replace: 'true',
      scope: {
        current: '=',
        all: '=',
        property: '=',
        ancestors: '=',
        emptyVal: '@',
        labelAs: "=",
      },
      link: function(scope, element, attrs) {
        var win = angular.element($window);

        scope.emptyLabel = "";
        scope.emptyObj = {};

        scope.labelView = function(labelObj) {
          if (scope.labelAs) {
            return labelObj[scope.labelAs];
          } else {
            return labelObj.short;
          }
        };

        if (element.hasClass('top-menu')) {
          var items = Object.keys(scope.all || {}).length + 1; // an empty val
          // Don't try to be clever when the list is really long. Chances are
          // that repositioning would cause the menu to go beyond the upper
          // border of the viewport, which is even worse.
          if (items < 20) {
            var menuHeight = items * 18; // hard to access, we therefore hardcode...
            var maxHeight = win.height() - 15;
            var topPos = element.parent().offset().top;
            var bottom = topPos + menuHeight;
            if (bottom > maxHeight) {
              element.css({ top: 'auto', bottom: '100%'});
            }
          }
        }
      },
      template: '\

\

\

\

\

\

\
      '
    };
  }
]);
<|endoftext|>


<commit_before>'use strict';

angular.module('ddsApp').controller('FormInfosComplementairesSituationProCtrl', function($scope, $state, $stateParams, situation, SituationService) {
    $scope.situation = situation;

    $scope.individusRef = [{id: 'demandeur', label: 'Vous', situationsPro: []}];
    if (situation.conjoint) {
        $scope.individusRef.push({id: 'conjoint', label: 'Votre partenaire', situationsPro: []});
    }

    $scope.situationsPro = [
        {
            id: 'sans_activite',
            label: 'Sans activité'
        },
        {
            id: 'salarie',
            label: 'Salarié'
        },
        {
            id: 'auto_entrepreneur',
            label: 'Auto-entrepreneur'
        },
        {
            id: 'travailleur_saisonnier',
            label: 'Travailleur saisonnier'
        },
        {
            id: 'apprenti',
            label: 'Apprenti'
        },
        {
            id: 'stagiaire',
            label: 'Stagiaire de la formation professionnelle'
        },
        {
            id: 'independant',
            label: 'Travailleur indépendant ou employeur, y compris exploitant agricole'
        },
        {
            id: 'gerant_salarie',
            label: 'Gérant salarié'
        },
        {
            id: 'demandeur_emploi',
            label: 'Demandeur d\'emploi'
        },
        {
            id: 'etudiant',
            label: 'Étudiant'
        },
        {
            id: 'retraite',
            label: 'Retraité'
        }
    ];

    $scope.individusRef.forEach(function(individuRef) {
        $scope.situationsPro.forEach(function(situationPro) {
            individuRef.situationsPro.push({situation: situationPro.id, label: situationPro.label, selected: false});
        });
    });

    $scope.submit = function() {
        situation.demandeur.situationsPro = _.filter($scope.individusRef[0].situationsPro, 'selected');
        if (situation.conjoint) {
            situation.conjoint.situationsPro = _.filter($scope.individusRef[1].situationsPro, 'selected');
        }

        SituationService.update(situation).then(function() {
            situation.infosComplementairesCaptured = true;
            $state.go('download_cerfa', {cerfa: $stateParams.requestedCerfa});
        });
    };
});
<commit_msg>fix erreurs capture situation pro<commit_after>'use strict';

angular.module('ddsApp').controller('FormInfosComplementairesSituationProCtrl', function($scope, $state, $stateParams, situation, SituationService) {
    $scope.situation = situation;

    $scope.individusRef = [{id: 'demandeur', label: 'Vous', individu: situation.demandeur, situationsPro: []}];
    if (situation.conjoint) {
        $scope.individusRef.push({id: 'conjoint', label: 'Votre partenaire', individu: situation.conjoint, situationsPro: []});
    }

    $scope.situationsPro = [
        {
            id: 'sans_activite',
            label: 'Sans activité'
        },
        {
            id: 'salarie',
            label: 'Salarié'
        },
        {
            id: 'auto_entrepreneur',
            label: 'Auto-entrepreneur'
        },
        {
            id: 'travailleur_saisonnier',
            label: 'Travailleur saisonnier'
        },
        {
            id: 'apprenti',
            label: 'Apprenti'
        },
        {
            id: 'stagiaire',
            label: 'Stagiaire de la formation professionnelle'
        },
        {
            id: 'independant',
            label: 'Travailleur indépendant ou employeur, y compris exploitant agricole'
        },
        {
            id: 'gerant_salarie',
            label: 'Gérant salarié'
        },
        {
            id: 'demandeur_emploi',
            label: 'Demandeur d\'emploi'
        },
        {
            id: 'etudiant',
            label: 'Étudiant'
        },
        {
            id: 'retraite',
            label: 'Retraité'
        }
    ];

    $scope.individusRef.forEach(function(individuRef) {
        $scope.situationsPro.forEach(function(situationPro) {
            var situationProIndividu = _.find(individuRef.individu.situationsPro, {situation: situationPro.id});
            if (situationProIndividu) {
                situationProIndividu.selected = true;
                individuRef.situationsPro.push(situationProIndividu);
            } else {
                individuRef.situationsPro.push({situation: situationPro.id, label: situationPro.label, selected: false});
            }
        });
    });

    $scope.submit = function() {
        $scope.individusRef.forEach(function(individuRef) {
            individuRef.individu.situationsPro = _.filter(individuRef.situationsPro, 'selected');
        });

        SituationService.update(situation).then(function() {
            situation.infosComplementairesCaptured = true;
            $state.go('download_cerfa', {cerfa: $stateParams.requestedCerfa});
        });
    };
});
<|endoftext|>


<commit_before>/**
 * Copyright 2019 Google LLC
 *
 * 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
 *
 *   https://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 { matchPrecache } from 'workbox-precaching';

/**
 * Normalizes requests that end with a backslash (/) to request the same resource
 * From https://github.com/GoogleChrome/workbox/issues/2217#issuecomment-528023769
 *
 * @param {object} param0 - An object containing a Workbox request object
 * @return {request} Either the original request or a normalized request
 */
async function normalizeIfNeeded({ request }) {
  // Clean out query parameters, and add a trailing '/' if missing.
  const url = new URL(request.url);
  let cleanUrl = url.origin + url.pathname;
  if (!cleanUrl.endsWith('/')) {
    cleanUrl += '/';
  }
  return new Request(cleanUrl);
}

const includesRegExp = /(<!--\s*#include\s*virtual=['|"]\S*['|"]-->)/gm;
const includesFileRegExp = /<!--\s*#include\s*virtual=['|"](\S*)['|"]-->/gm;
const endIncludeRegExp = /<!--\s*#endinclude\s*-->/gm;
const endIncludeWithLeadingRegExp = /[\s\S]*<!--\s*#endinclude\s*-->/gm;

/**
 * Takes a cached response and replaces includes with precached items
 *
 * @param {response} param0 - The response from the cache
 */
async function serviceWorkerInclude({ cachedResponse }) {
  if (!cachedResponse) {
    return null;
  }

  const content = await cachedResponse.text();
  const matches = [...new Set(content.match(includesRegExp))];
  const neededIncludes = await Promise.all(
    matches
      .map(i => i.split(includesFileRegExp)[1])
      .map(async key => {
        const cachedInclude = await matchPrecache(key);
        return cachedInclude.text();
      }),
  );

  const includes = {};

  matches.forEach((include, i) => (includes[include] = neededIncludes[i]));

  const rebuild = content
    .split(includesRegExp)
    .map(i => {
      if (matches.includes(i)) {
        return includes[i];
      }

      return i;
    })
    .join('');

  return new Response(rebuild, { headers: cachedResponse.headers });
}

/**
 * Removes service worker includes before saving items to cache
 *
 * @param {response} param0 - The response that will update the cache
 */
async function swiCleanup({ response }) {
  const content = await response.text();

  const matches = content.match(includesRegExp);
  // const neededIncludes = [...new Set(matches)].map(i => i.split(includesFileRegExp)[1]);
  let open = 0;
  const rebuild = content
    .split(includesRegExp)
    .map(i => {
      // If the current item is the include and it's not included from within
      if (matches && i === matches[0]) {
        matches.shift();
        open++;
        if (open > 1) return '';
        return i;
      }

      const endIncludeSplit = i.split(endIncludeWithLeadingRegExp);
      if (endIncludeSplit.length === 1 && open !== 0) {
        return '';
      }

      const count = i.match(endIncludeRegExp);

      open = open - (count ? count.length : 0);

      return endIncludeSplit.join('');
    })
    .join('');

  return new Response(rebuild, { headers: response.headers });
}

export const serviceWorkerIncludePlugin = {
  cacheKeyWillBeUsed: normalizeIfNeeded,
  requestWillFetch: normalizeIfNeeded,
  cachedResponseWillBeUsed: serviceWorkerInclude,
  cacheWillUpdate: swiCleanup,
};
<commit_msg>🐛 (service-worker-includes) Fix normalization of resources<commit_after>/**
 * Copyright 2019 Google LLC
 *
 * 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
 *
 *   https://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 { matchPrecache } from 'workbox-precaching';

/**
 * Normalizes requests that end with a backslash (/) to request the same resource
 * From https://github.com/GoogleChrome/workbox/issues/2217#issuecomment-528023769
 *
 * @param {object} param0 - An object containing a Workbox request object
 * @return {request} Either the original request or a normalized request
 */
async function normalizeIfNeeded({ request }) {
  // Clean out query parameters, and removes a trailing '/' if there.
  const url = new URL(request.url);
  let cleanUrl = url.origin + url.pathname;
  if (cleanUrl.endsWith('/')) {
    cleanUrl = cleanUrl.slice(0, -1);
  }
  return new Request(cleanUrl);
}

const includesRegExp = /(<!--\s*#include\s*virtual=['|"]\S*['|"]-->)/gm;
const includesFileRegExp = /<!--\s*#include\s*virtual=['|"](\S*)['|"]-->/gm;
const endIncludeRegExp = /<!--\s*#endinclude\s*-->/gm;
const endIncludeWithLeadingRegExp = /[\s\S]*<!--\s*#endinclude\s*-->/gm;

/**
 * Takes a cached response and replaces includes with precached items
 *
 * @param {response} param0 - The response from the cache
 */
async function serviceWorkerInclude({ cachedResponse }) {
  if (!cachedResponse) {
    return null;
  }

  const content = await cachedResponse.text();
  const matches = [...new Set(content.match(includesRegExp))];
  const neededIncludes = await Promise.all(
    matches
      .map(i => i.split(includesFileRegExp)[1])
      .map(async key => {
        const cachedInclude = await matchPrecache(key);
        return cachedInclude.text();
      }),
  );

  const includes = {};

  matches.forEach((include, i) => (includes[include] = neededIncludes[i]));

  const rebuild = content
    .split(includesRegExp)
    .map(i => {
      if (matches.includes(i)) {
        return includes[i];
      }

      return i;
    })
    .join('');

  return new Response(rebuild, { headers: cachedResponse.headers });
}

/**
 * Removes service worker includes before saving items to cache
 *
 * @param {response} param0 - The response that will update the cache
 */
async function swiCleanup({ response }) {
  const content = await response.text();

  const matches = content.match(includesRegExp);
  // const neededIncludes = [...new Set(matches)].map(i => i.split(includesFileRegExp)[1]);
  let open = 0;
  const rebuild = content
    .split(includesRegExp)
    .map(i => {
      // If the current item is the include and it's not included from within
      if (matches && i === matches[0]) {
        matches.shift();
        open++;
        if (open > 1) return '';
        return i;
      }

      const endIncludeSplit = i.split(endIncludeWithLeadingRegExp);
      if (endIncludeSplit.length === 1 && open !== 0) {
        return '';
      }

      const count = i.match(endIncludeRegExp);

      open = open - (count ? count.length : 0);

      return endIncludeSplit.join('');
    })
    .join('');

  return new Response(rebuild, { headers: response.headers });
}

export const serviceWorkerIncludePlugin = {
  cacheKeyWillBeUsed: normalizeIfNeeded,
  requestWillFetch: normalizeIfNeeded,
  cachedResponseWillBeUsed: serviceWorkerInclude,
  cacheWillUpdate: swiCleanup,
};
<|endoftext|>


<commit_before>import React from 'react'
import { connect } from 'react-redux'
import { setCurrentSeries, setButtonZoom, setGameZoom } from '../actions'
import { PropTypes } from 'prop-types'
import Nav from 'react-bootstrap/lib/Nav'
import NavItem from 'react-bootstrap/lib/NavItem'
import Grid from 'react-bootstrap/lib/Grid'
import Col from 'react-bootstrap/lib/Col'
import Row from 'react-bootstrap/lib/Row'
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup'
import Button from 'react-bootstrap/lib/Button'
import PacmanLoader from 'halogen/PacmanLoader'
import moment from 'moment'
import { LineChart, Line, Tooltip, ResponsiveContainer, XAxis, YAxis } from 'recharts'
import VerticalLabel from './VerticalLabel'
import GamesTooltip from './GamesTooltip'
import GRAPHS from '../graph-definitions'
import Select from 'react-select'
import { movingAverage } from '../utils'

const zoomButtons = [
  { label: '1h', hours: 1 },
  { label: '3h', hours: 3 },
  { label: '6h', hours: 6 },
  { label: '12h', hours: 12 },
  { label: '1d', hours: 24 },
  { label: '3d', hours: 72 }
]

class GraphContainer extends React.PureComponent {
  static propTypes = {
    setCurrentSeries: PropTypes.func.isRequired,
    activeSeries: PropTypes.number.isRequired,
    timeseries: PropTypes.array.isRequired,
    schedule: PropTypes.array.isRequired,
    activeButtonZoomIndex: PropTypes.number,
    activeGameZoom: PropTypes.object,
    setGameZoom: PropTypes.func.isRequired,
    setButtonZoom: PropTypes.func.isRequired
  }

  constructor (props) {
    super(props)
    this.onSelect = this.onSelect.bind(this)
  }

  onSelect (idx) {
    this.props.setCurrentSeries(idx)
  }

  // Creates "synthetic" series by aggregating a base series
  //  Aggregations are either accumulations or 'derivations'
  //  Accumulate transforms "per minute" series to cumulative
  //  Derive transforms cumulative series to "per minute"
  createSyntheticSeries (key, series) {
    const baseKey = key.slice(0, key.indexOf('_'))
    const accumulate = (acc, val) => {
      const newVal = val[baseKey] + (acc.length ? acc[acc.length - 1][key] : 0)
      let newObj = {time: val.time}
      newObj[key] = newVal
      acc.push(newObj)
      return acc
    }
    const derive = (acc, val) => {
      const newVal = acc.length ? (val[baseKey] - acc[acc.length - 1][baseKey]) : 0
      let newObj = {time: val.time}
      newObj[key] = newVal
      newObj[baseKey] = val[baseKey]
      acc.push(newObj)
      return acc
    }
    const reduceFunc = key.slice(baseKey.length + 1) === 'acc' ? accumulate : derive
    return series.reduce(reduceFunc, [])
  }

  // Returns [min, max] moment times of domain based on current state of graph options
  getDomain () {
    const { activeButtonZoomIndex, activeGameZoom } = this.props
    const maxTime = this.props.timeseries[this.props.timeseries.length - 1].time
    let min = moment.unix(0)
    let max = moment(maxTime).clone()
    if (activeButtonZoomIndex >= 0) {
      const zoomHours = zoomButtons[activeButtonZoomIndex].hours
      min = moment(maxTime).clone().subtract(zoomHours, 'hours')
    } else if (activeGameZoom) {
      const [hours, minutes, seconds] = activeGameZoom.duration.split(':')
      min = activeGameZoom.moment
      max = activeGameZoom.moment.clone()
        .add(hours, 'hours')
        .add(minutes, 'minutes')
        .add(seconds, 'seconds')
    }
    return [min, max]
  }

  getDateFormatter (domain)
  {
    const [min, max] = domain
    const format = min.clone().add(1, 'days').isBefore(max) ? 'ddd, hA' : 'h:mm A'
    return (d) => moment.unix(d).format(format)
  }

  getGraph () {
    if (!this.props.timeseries || !this.props.timeseries.length) {
      return null
    }

    const domain = this.getDomain()

    const activeGraph = GRAPHS[this.props.activeSeries]
    const tooltipFormat = GRAPHS[this.props.activeSeries].tooltipFormat || activeGraph.format
    const dateFormat = this.getDateFormatter(domain)

    let series = this.props.timeseries
    if (activeGraph.key.indexOf('_') !== -1) {
      series = this.createSyntheticSeries(activeGraph.key, this.props.timeseries)
    }

    const trimmedTimeseries = series
      // Filter to correct domain
      .filter((obj) => {
        const objMoment = moment(obj.time)
        return objMoment.isAfter(domain[0]) && objMoment.isBefore(domain[1])
      })

    const rate = Math.ceil(trimmedTimeseries.length / 500)
    let resampleSeries = trimmedTimeseries
      // Resample
      .filter((d, idx) => idx % rate === 0 && d[activeGraph.key] >= 0)
      .map((o) => {
        return { ...o, time: moment(o.time).unix() }
      })
      .sort((a, b) => a.time - b.time)

    if(activeGraph.movingAverage) {
      resampleSeries = movingAverage(resampleSeries, activeGraph.key, Math.ceil(trimmedTimeseries.length / 100))
    }

    const yAxisLabel = (
      <VerticalLabel
        axisType='yAxis'
        fill='#333'
        fontWeight={300}
        fontSize={13}>
        {activeGraph.name}
      </VerticalLabel>
    )

    const selectOptions = this.props.schedule.filter((obj) => obj.moment.isBefore())

    return (
      <Grid>
        <Row>
          <Col sm={4} md={2} className='graph-series-chooser'>

              {GRAPHS.map((obj, idx) => <NavItem eventKey={idx} key={idx}>{obj.name}</NavItem>)}

          </Col>

          <Col sm={8} md={10} className='graph-container'>
            <ResponsiveContainer width='100%' height={500}>
              <LineChart data={resampleSeries} margin={{top: 20}}>
                <Line
                  type='basis'
                  dataKey={activeGraph.key}
                  name={activeGraph.name}
                  stroke='#00AEEF'
                  strokeWidth={1.5}
                  dot={false}
                  activeDot />
                <Tooltip
                  content={<GamesTooltip schedule={this.props.schedule} format={tooltipFormat} />}
                  animationDuration={250} />
                <XAxis
                  dataKey='time'
                  type='number'
                  scale='time'
                  axisLine={{stroke: '#ddd'}}
                  tickLine={{stroke: '#ddd'}}
                  tickFormatter={dateFormat}
                  tick={{fill: '#333', fontWeight: 300, fontSize: 13}}
                  interval='preserveStart'
                  domain={['dataMin', 'dataMax']}
                  minTickGap={50} />
                <YAxis
                  dataKey={activeGraph.key}
                  tickFormatter={activeGraph.format}
                  axisLine={{stroke: '#ddd'}}
                  tickLine={{stroke: '#ddd'}}
                  tick={{fill: '#333', fontWeight: 300, fontSize: 13}}
                  domain={['dataMin', 'dataMax']}
                  interval='preserveStartEnd'
                  minTickGap={0}
                  label={yAxisLabel}
                  orientation='left' />
              </LineChart>
            </ResponsiveContainer>
          </Col>
        </Row>
        <Row className='series-options'>
          <Col sm={4} style={{height: 32}}>
            Options
          </Col>
          <Col sm={3} style={{fontFamily: 'Open Sans'}}>
            <Select
              options={selectOptions}
              labelKey='name'
              valueKey='name'
              placeholder='Zoom to Game...'
              value={this.props.activeGameZoom ? this.props.activeGameZoom.name : null}
              onChange={this.props.setGameZoom}
            />
          </Col>
          <Col sm={5} style={{fontFamily: 'Open Sans'}}>
            <ButtonGroup>
              {zoomButtons.map((obj, idx) =>
                <Button
                  key={idx}
                  active={this.props.activeButtonZoomIndex === idx}
                  onClick={() => this.props.setButtonZoom(idx)}>
                  {obj.label}
                </Button>
              )}
            </ButtonGroup>
          </Col>
        </Row>
      </Grid>
    )
  }

  render () {
    const graph = this.getGraph()
    return (

Live Stats

        {graph ? graph : <PacmanLoader color='#00AEEF' className='graph-loader'/>}

    )
  }
}

function mapStateToProps (state) {
  return {
    activeSeries: state.gdq.series,
    timeseries: state.gdq.timeseries,
    schedule: state.gdq.schedule,
    activeButtonZoomIndex: state.gdq.activeButtonZoomIndex,
    activeGameZoom: state.gdq.activeGameZoom
  }
}

export default connect(mapStateToProps, { setCurrentSeries, setButtonZoom, setGameZoom })(GraphContainer)
<commit_msg>Fix time domain lower bound; Make graphs 0-bounded for now<commit_after>import React from 'react'
import { connect } from 'react-redux'
import { setCurrentSeries, setButtonZoom, setGameZoom } from '../actions'
import { PropTypes } from 'prop-types'
import Nav from 'react-bootstrap/lib/Nav'
import NavItem from 'react-bootstrap/lib/NavItem'
import Grid from 'react-bootstrap/lib/Grid'
import Col from 'react-bootstrap/lib/Col'
import Row from 'react-bootstrap/lib/Row'
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup'
import Button from 'react-bootstrap/lib/Button'
import PacmanLoader from 'halogen/PacmanLoader'
import moment from 'moment'
import { LineChart, Line, Tooltip, ResponsiveContainer, XAxis, YAxis } from 'recharts'
import VerticalLabel from './VerticalLabel'
import GamesTooltip from './GamesTooltip'
import GRAPHS from '../graph-definitions'
import Select from 'react-select'
import { movingAverage } from '../utils'

const zoomButtons = [
  { label: '1h', hours: 1 },
  { label: '3h', hours: 3 },
  { label: '6h', hours: 6 },
  { label: '12h', hours: 12 },
  { label: '1d', hours: 24 },
  { label: '3d', hours: 72 }
]

class GraphContainer extends React.PureComponent {
  static propTypes = {
    setCurrentSeries: PropTypes.func.isRequired,
    activeSeries: PropTypes.number.isRequired,
    timeseries: PropTypes.array.isRequired,
    schedule: PropTypes.array.isRequired,
    activeButtonZoomIndex: PropTypes.number,
    activeGameZoom: PropTypes.object,
    setGameZoom: PropTypes.func.isRequired,
    setButtonZoom: PropTypes.func.isRequired
  }

  constructor (props) {
    super(props)
    this.onSelect = this.onSelect.bind(this)
  }

  onSelect (idx) {
    this.props.setCurrentSeries(idx)
  }

  // Creates "synthetic" series by aggregating a base series
  //  Aggregations are either accumulations or 'derivations'
  //  Accumulate transforms "per minute" series to cumulative
  //  Derive transforms cumulative series to "per minute"
  createSyntheticSeries (key, series) {
    const baseKey = key.slice(0, key.indexOf('_'))
    const accumulate = (acc, val) => {
      const newVal = val[baseKey] + (acc.length ? acc[acc.length - 1][key] : 0)
      let newObj = {time: val.time}
      newObj[key] = newVal
      acc.push(newObj)
      return acc
    }
    const derive = (acc, val) => {
      const newVal = acc.length ? (val[baseKey] - acc[acc.length - 1][baseKey]) : 0
      let newObj = {time: val.time}
      newObj[key] = newVal
      newObj[baseKey] = val[baseKey]
      acc.push(newObj)
      return acc
    }
    const reduceFunc = key.slice(baseKey.length + 1) === 'acc' ? accumulate : derive
    return series.reduce(reduceFunc, [])
  }

  // Returns [min, max] moment times of domain based on current state of graph options
  getDomain () {
    const { activeButtonZoomIndex, activeGameZoom } = this.props
    const maxTime = this.props.timeseries[this.props.timeseries.length - 1].time
    const minTime = this.props.timeseries[0].time
    let min = moment(minTime).clone()
    let max = moment(maxTime).clone()
    if (activeButtonZoomIndex >= 0) {
      const zoomHours = zoomButtons[activeButtonZoomIndex].hours
      min = moment(maxTime).clone().subtract(zoomHours, 'hours')
    } else if (activeGameZoom) {
      const [hours, minutes, seconds] = activeGameZoom.duration.split(':')
      min = activeGameZoom.moment
      max = activeGameZoom.moment.clone()
        .add(hours, 'hours')
        .add(minutes, 'minutes')
        .add(seconds, 'seconds')
    }
    return [min, max]
  }

  getDateFormatter (domain)
  {
    const [min, max] = domain
    const format = min.clone().add(1, 'days').isBefore(max) ? 'ddd, hA' : 'h:mm A'
    return (d) => moment.unix(d).format(format)
  }

  getGraph () {
    if (!this.props.timeseries || !this.props.timeseries.length) {
      return null
    }

    const domain = this.getDomain()

    const activeGraph = GRAPHS[this.props.activeSeries]
    const tooltipFormat = GRAPHS[this.props.activeSeries].tooltipFormat || activeGraph.format
    const dateFormat = this.getDateFormatter(domain)

    let series = this.props.timeseries
    if (activeGraph.key.indexOf('_') !== -1) {
      series = this.createSyntheticSeries(activeGraph.key, this.props.timeseries)
    }

    const trimmedTimeseries = series
      // Filter to correct domain
      .filter((obj) => {
        const objMoment = moment(obj.time)
        return objMoment.isAfter(domain[0]) && objMoment.isBefore(domain[1])
      })

    const rate = Math.ceil(trimmedTimeseries.length / 500)
    let resampleSeries = trimmedTimeseries
      // Resample
      .filter((d, idx) => idx % rate === 0 && d[activeGraph.key] >= 0)
      .map((o) => {
        return { ...o, time: moment(o.time).unix() }
      })
      .sort((a, b) => a.time - b.time)

    if(activeGraph.movingAverage) {
      resampleSeries = movingAverage(resampleSeries, activeGraph.key, Math.ceil(trimmedTimeseries.length / 100))
    }

    const yAxisLabel = (
      <VerticalLabel
        axisType='yAxis'
        fill='#333'
        fontWeight={300}
        fontSize={13}>
        {activeGraph.name}
      </VerticalLabel>
    )

    const selectOptions = this.props.schedule.filter((obj) => obj.moment.isBefore())

    return (
      <Grid>
        <Row>
          <Col sm={4} md={2} className='graph-series-chooser'>

              {GRAPHS.map((obj, idx) => <NavItem eventKey={idx} key={idx}>{obj.name}</NavItem>)}

          </Col>

          <Col sm={8} md={10} className='graph-container'>
            <ResponsiveContainer width='100%' height={500}>
              <LineChart data={resampleSeries} margin={{top: 20}}>
                <Line
                  type='basis'
                  dataKey={activeGraph.key}
                  name={activeGraph.name}
                  stroke='#00AEEF'
                  strokeWidth={1.5}
                  dot={false}
                  activeDot />
                <Tooltip
                  content={<GamesTooltip schedule={this.props.schedule} format={tooltipFormat} />}
                  animationDuration={250} />
                <XAxis
                  dataKey='time'
                  type='number'
                  scale='time'
                  axisLine={{stroke: '#ddd'}}
                  tickLine={{stroke: '#ddd'}}
                  tickFormatter={dateFormat}
                  tick={{fill: '#333', fontWeight: 300, fontSize: 13}}
                  interval='preserveStart'
                  domain={['dataMin', 'dataMax']}
                  minTickGap={50} />
                <YAxis
                  dataKey={activeGraph.key}
                  tickFormatter={activeGraph.format}
                  axisLine={{stroke: '#ddd'}}
                  tickLine={{stroke: '#ddd'}}
                  tick={{fill: '#333', fontWeight: 300, fontSize: 13}}
                  domain={['dataMin', 'dataMax']}
                  interval='preserveStartEnd'
                  minTickGap={0}
                  label={yAxisLabel}
                  orientation='left' />
              </LineChart>
            </ResponsiveContainer>
          </Col>
        </Row>
        <Row className='series-options'>
          <Col sm={4} style={{height: 32}}>
            Options
          </Col>
          <Col sm={3} style={{fontFamily: 'Open Sans'}}>
            <Select
              options={selectOptions}
              labelKey='name'
              valueKey='name'
              placeholder='Zoom to Game...'
              value={this.props.activeGameZoom ? this.props.activeGameZoom.name : null}
              onChange={this.props.setGameZoom}
            />
          </Col>
          <Col sm={5} style={{fontFamily: 'Open Sans'}}>
            <ButtonGroup>
              {zoomButtons.map((obj, idx) =>
                <Button
                  key={idx}
                  active={this.props.activeButtonZoomIndex === idx}
                  onClick={() => this.props.setButtonZoom(idx)}>
                  {obj.label}
                </Button>
              )}
            </ButtonGroup>
          </Col>
        </Row>
      </Grid>
    )
  }

  render () {
    const graph = this.getGraph()
    return (

Live Stats

        {graph ? graph : <PacmanLoader color='#00AEEF' className='graph-loader'/>}

    )
  }
}

function mapStateToProps (state) {
  return {
    activeSeries: state.gdq.series,
    timeseries: state.gdq.timeseries,
    schedule: state.gdq.schedule,
    activeButtonZoomIndex: state.gdq.activeButtonZoomIndex,
    activeGameZoom: state.gdq.activeGameZoom
  }
}

export default connect(mapStateToProps, { setCurrentSeries, setButtonZoom, setGameZoom })(GraphContainer)
<|endoftext|>


<commit_before>
$(function(){

    var strategy = {
        question: {
            //question
            add:function(event){
                //add question, focus first input
                var sortable = $(event.target).attr('data-target');

                var newItem = $(sortable).find('.template').first().clone().removeClass('template');
                newItem.find('input, select').removeAttr('disabled');
                $(sortable).find('>.trash').before(newItem);

                // display/focus the new element's text field.
                newItem.find('.edit:first').trigger('click');

                //fix the input post order
                fix_sub_sortable_target(newItem);
            }
        },
        answer:{
            0:{//short answer
                add:function(event){
                    //add answer, don't focus
                    //add question, return question
                    var sortable = $(event.target).closest('li').find('.sortable');
                    var newItem = sortable.find('.template').first().clone();
                    newItem.removeClass('template').find('input, select').removeAttr('disabled');
                    $(sortable).find('.trash').last().before(newItem.hide());
                    return newItem;
                },
                change:function(event){
                    //add blank answer,
                    this.add(event);
                    //hide add-sortable button
                    $(event.target).closest('li').find('.add-sortable').hide();
                }
            },
            1:{//true false
                add:function(event){
                    //add question, don't focus
                    var sortable = $(event.target).closest('li').find('.sortable');
                    var newItem = sortable.find('.template').first().clone();
                    newItem.removeClass('template').find('input, select').removeAttr('disabled');
                    newItem.find('.delete').hide();
                    $(sortable).find('.trash').last().before(newItem);
                    return newItem;
                },
                change:function(event){
                    //add two answers, focus first;
                    var true_answer = this.add(event);
                    true_answer.find('input[type=text]').val('True');
                    true_answer.find('.text').html('True');
                    var false_answer = this.add(event);
                    false_answer.find('input[type=text]').val('False');
                    false_answer.find('.text').html('False');
                    //hide add sortable button
                    $(event.target).closest('li').find('.add-sortable').hide();
                    //hide add-sortable button
                }
            },
            2:{//multiple choice
                add:function(event){
                    strategy.question.add(event);
                },
                change:function(event){
                    $(event.target).find('.add-sortable').show();
                }
            }
        }
   }
   //mutiple slect behaves the same as mutlple choice;
   strategy.answer[3] = strategy.answer[2];

    /*Initialize the sortables.*/
    do_sortables();

    /**
     * Deactivates actively editing questions when clicking outside the sortable element.
     */
	$(window).on('click',function(e){
		var clickedActiveQuestion = $(e.target).closest('.active').length || $(e.target).hasClass('active');
		if(!clickedActiveQuestion){
			$('.active a.edit').trigger('click');
		}
	});

    /**
     * Event handler for editing a list item. Triggered by clicking the edit link.
     * This function toggles the text editing of quesitons.
     */
	$('.sortable').on('click','a.edit',function(e){
		e.stopPropagation();//allows us to catch window click outside event.
		e.preventDefault();//stops the link from following the url
        var editable = $(this).closest('[data-editable]');
		if(!editable.hasClass('active')){
            //close other editors.
			$('.active a.edit').trigger('click');
			editable.addClass('active');
            editable.find('[data-hide-on-edit]').hide();
            editable.find('[data-show-on-edit]').show().first().focus();
		}else{
            //stop editing function.
			editable.removeClass('active');
            editable.find('[data-show-on-edit]').hide().each(function(idx,elem){
                var html = $(elem).val();
                if($(elem).is('select')){
                    html = $(elem).find('option[value='+$(elem).val()+']').text();
                }
                editable.find($(elem).attr('data-source')).html(html);
            });
            editable.find('[data-hide-on-edit]').show();
		}
	});

    /**
     * This event handles the deletion of list items. It hides the item
     * apends it after the .trash item.
     */
    $('.sortable').on('click','a.delete',function(e){
		e.preventDefault();//stops the link from following the url
        e.stopPropagation();
		var listItem = $(this).closest('li');
		listItem.hide().attr('data-deleted',"True").find('input[name*=delete]').val("True");
        //move deleted items after trash item to allows the preservation of order_num.
        listItem.siblings('.trash').after(listItem.detach());
        fix_sortable_input_names();
        var sortable_id = listItem.closest('.sortable').attr('id');
        $('[data-target=#'+sortable_id+']').show();
	});

    /**
     * Adds a sortable element by coping the hidden template at the head of the sortable.
     * (Delegates function call to 'add' strategy. It then updates the naem attributes
     * of all form elements.
     */
	$('form').on('click',':not(.template) .add-sortable', function(event){
        event.preventDefault();
        event.stopPropagation();

        //add sortable functionality to the newly added item.
        do_sortables();

        //let the strategy pattern handle the non-default actions.
        if($(event.target).data('model') == 'question'){
            strategy.question.add(event);
        }else{
            var questionType = $(event.target).closest('li').find('[name*=type]').val();
            strategy.answer[questionType].add(event);
        }
        //add the new element before the delete selements that are stored at the end of the list
        fix_sortable_input_names();
	});

    /**
     * OnChange event of a any input named type, deletes all existing sub_sortable elemnts.
     * Delegates the function call to the appropriate change strategy.
     */
    $('#questions').on('change', '[name*=type]',function(event){
        var question = $(this).closest('li');
        //delete all previous answers.
        question.find('.sortable li:not(.template) .delete').trigger('click');
        //update add-sortable button.
        question.find('.add-sortable').attr('data-parent-type',$(this).val());
        strategy.answer[$(this).val()].change(event);
        //add the new element before the delete selements that are stored at the end of the list
        fix_sortable_input_names();
    });

    /**
     * Handels the draggable items enabled by jquery-ui.
     */
    function do_sortables(){
        $('.sortable').sortable({
            items: '> li',
            start:function(event, ui){
                $(ui.item).addClass('dragging');
            },
            stop:function(event, ui){
                $(ui.item).removeClass('dragging');
                fix_sortable_input_names();
            }
        });
    };

    /**
     * Change the name of attribute to reflect survey new order_number which are
     * Propogated into 'name' attribute. This function makes sure to format the
     * name attribute to make the resulting $_POST array contains a list of sortable
     * items with each index reflecting the element's order_number and respective parent
     * model. This function should be called whenever a sort update is made to the list.
     *
     * The expected name schem should be:
     *      sortable1[sortable1_indexes][sortable2][sortable2_indexes][[...][...]etc...]['attributes']
     * (ie. expected name attributers are: SurveyQuestion[1][SurveyAnswer][2][choice_letter]['text'])
     *
     */
    function fix_sortable_input_names(){
        $('#questions').find('li:not(.template, .trash)').find('input, select').each(function(idx, input){
            //get the old name
            var old_name = $(input).attr('name');

            //fix top level order number...
            var farthest_li = $(input).parents('.sortable li').last();
            var top_order = farthest_li.siblings().andSelf().not('.trash, .template').index( farthest_li );
            top_fixed = old_name;
            if(top_order > -1){
                var top_fixed = old_name.split('[').slice(0,1) + "[" + top_order + "]" + old_name.split(']').slice(1).join(']');
            }

            //fix sub-sortable specific order number.
            var nearest_li = $(input).parents('.sortable li').first();
            var bottom_order = nearest_li.siblings().andSelf().not('.trash, .template').index( nearest_li );
            new_name = top_fixed;
            if(bottom_order > -1){
                var nesting = 2 * ($(input).parents('.sortable').length) - 1;
                var new_name = top_fixed.split('[').slice(0,nesting).join('[') + "[" + bottom_order + "]" + top_fixed.split(']').slice(nesting).join(']');
            }
            $(input).attr('name',new_name);
        });
    }

    /**
     * This function fixes the repeated sortable id and add-sortable data-target inherited from
     * the generating template. This funtion generates a new unique sortable id for the add-sortable
     * button to have work again.
     *
     * @param  $(.sortable li) sortable_item
     * @return null
     */
    function fix_sub_sortable_target(sortable_item){
        var addSortablebutton = sortable_item.find('.add-sortable[data-target]');
        var old_target = addSortablebutton.attr('data-target');
        //if a sub-sortable exists..
        if(addSortablebutton.length){
            //define the new target id.
            var new_target = old_target.split('_').slice(0,1) +'_'+Math.floor(Math.random() * 1000000000);
            //set the target and respective id to match.
            addSortablebutton.attr('data-target',new_target);
            sortable_item.find(old_target).attr('id',new_target.substr(1));
        }
    }
});<commit_msg>multiple choice no longer creates a non-existant answer.<commit_after>
$(function(){

    var strategy = {
        question: {
            //question
            add:function(event){
                //add question, focus first input
                var sortable = $(event.target).attr('data-target');

                var newItem = $(sortable).find('.template').first().clone().removeClass('template');

                //enable all non-template input/select
                newItem.find('input, select').filter(function(){
                    return !$(this).closest('.template').length;
                }).removeAttr('disabled');

                $(sortable).find('>.trash').before(newItem);

                // display/focus the new element's text field.
                newItem.find('.edit:first').trigger('click');

                //fix the input post order
                fix_sub_sortable_target(newItem);
            }
        },
        answer:{
            0:{//short answer
                add:function(event){
                    //add answer, don't focus
                    //add question, return question
                    var sortable = $(event.target).closest('li').find('.sortable');
                    var newItem = sortable.find('.template').first().clone();
                    newItem.removeClass('template').find('input, select').removeAttr('disabled');
                    $(sortable).find('.trash').last().before(newItem.hide());
                    return newItem;
                },
                change:function(event){
                    //add blank answer,
                    this.add(event);
                    //hide add-sortable button
                    $(event.target).closest('li').find('.add-sortable').hide();
                }
            },
            1:{//true false
                add:function(event){
                    //add question, don't focus
                    var sortable = $(event.target).closest('li').find('.sortable');
                    var newItem = sortable.find('.template').first().clone();
                    newItem.removeClass('template').find('input, select').removeAttr('disabled');
                    newItem.find('.delete').hide();
                    $(sortable).find('.trash').last().before(newItem);
                    return newItem;
                },
                change:function(event){
                    //add two answers, focus first;
                    var true_answer = this.add(event);
                    true_answer.find('input[type=text]').val('True');
                    true_answer.find('.text').html('True');
                    var false_answer = this.add(event);
                    false_answer.find('input[type=text]').val('False');
                    false_answer.find('.text').html('False');
                    //hide add sortable button
                    $(event.target).closest('li').find('.add-sortable').hide();
                    //hide add-sortable button
                }
            },
            2:{//multiple choice
                add:function(event){
                    strategy.question.add(event);
                },
                change:function(event){
                    $(event.target).find('.add-sortable').show();
                }
            }
        }
   }
   //mutiple slect behaves the same as mutlple choice;
   strategy.answer[3] = strategy.answer[2];

    /*Initialize the sortables.*/
    do_sortables();

    /**
     * Deactivates actively editing questions when clicking outside the sortable element.
     */
	$(window).on('click',function(e){
		var clickedActiveQuestion = $(e.target).closest('.active').length || $(e.target).hasClass('active');
		if(!clickedActiveQuestion){
			$('.active a.edit').trigger('click');
		}
	});

    /**
     * Event handler for editing a list item. Triggered by clicking the edit link.
     * This function toggles the text editing of quesitons.
     */
	$('.sortable').on('click','a.edit',function(e){
		e.stopPropagation();//allows us to catch window click outside event.
		e.preventDefault();//stops the link from following the url
        var editable = $(this).closest('[data-editable]');
		if(!editable.hasClass('active')){
            //close other editors.
			$('.active a.edit').trigger('click');
			editable.addClass('active');
            editable.find('[data-hide-on-edit]').hide();
            editable.find('[data-show-on-edit]').show().first().focus();
		}else{
            //stop editing function.
			editable.removeClass('active');
            editable.find('[data-show-on-edit]').hide().each(function(idx,elem){
                var html = $(elem).val();
                if($(elem).is('select')){
                    html = $(elem).find('option[value='+$(elem).val()+']').text();
                }
                editable.find($(elem).attr('data-source')).html(html);
            });
            editable.find('[data-hide-on-edit]').show();
		}
	});

    /**
     * This event handles the deletion of list items. It hides the item
     * apends it after the .trash item.
     */
    $('.sortable').on('click','a.delete',function(e){
		e.preventDefault();//stops the link from following the url
        e.stopPropagation();
		var listItem = $(this).closest('li');
		listItem.hide().attr('data-deleted',"True").find('input[name*=delete]').val("True");
        //move deleted items after trash item to allows the preservation of order_num.
        listItem.siblings('.trash').after(listItem.detach());
        fix_sortable_input_names();
        var sortable_id = listItem.closest('.sortable').attr('id');
        $('[data-target=#'+sortable_id+']').show();
	});

    /**
     * Adds a sortable element by coping the hidden template at the head of the sortable.
     * (Delegates function call to 'add' strategy. It then updates the naem attributes
     * of all form elements.
     */
	$('form').on('click',':not(.template) .add-sortable', function(event){
        event.preventDefault();
        event.stopPropagation();

        //add sortable functionality to the newly added item.
        do_sortables();

        //let the strategy pattern handle the non-default actions.
        if($(event.target).data('model') == 'question'){
            strategy.question.add(event);
        }else{
            var questionType = $(event.target).closest('li').find('[name*=type]').val();
            strategy.answer[questionType].add(event);
        }
        //add the new element before the delete selements that are stored at the end of the list
        fix_sortable_input_names();
	});

    /**
     * OnChange event of a any input named type, deletes all existing sub_sortable elemnts.
     * Delegates the function call to the appropriate change strategy.
     */
    $('#questions').on('change', '[name*=type]',function(event){
        var question = $(this).closest('li');
        //delete all previous answers.
        question.find('.sortable li:not(.template) .delete').trigger('click');
        //update add-sortable button.
        question.find('.add-sortable').attr('data-parent-type',$(this).val());
        strategy.answer[$(this).val()].change(event);
        //add the new element before the delete selements that are stored at the end of the list
        fix_sortable_input_names();
    });

    /**
     * Handels the draggable items enabled by jquery-ui.
     */
    function do_sortables(){
        $('.sortable').sortable({
            items: '> li',
            start:function(event, ui){
                $(ui.item).addClass('dragging');
            },
            stop:function(event, ui){
                $(ui.item).removeClass('dragging');
                fix_sortable_input_names();
            }
        });
    };

    /**
     * Change the name of attribute to reflect survey new order_number which are
     * Propogated into 'name' attribute. This function makes sure to format the
     * name attribute to make the resulting $_POST array contains a list of sortable
     * items with each index reflecting the element's order_number and respective parent
     * model. This function should be called whenever a sort update is made to the list.
     *
     * The expected name schem should be:
     *      sortable1[sortable1_indexes][sortable2][sortable2_indexes][[...][...]etc...]['attributes']
     * (ie. expected name attributers are: SurveyQuestion[1][SurveyAnswer][2][choice_letter]['text'])
     *
     */
    function fix_sortable_input_names(){
        $('#questions').find('li:not(.template, .trash)').find('input, select').each(function(idx, input){
            //get the old name
            var old_name = $(input).attr('name');

            //fix top level order number...
            var farthest_li = $(input).parents('.sortable li').last();
            var top_order = farthest_li.siblings().andSelf().not('.trash, .template').index( farthest_li );
            top_fixed = old_name;
            if(top_order > -1){
                var top_fixed = old_name.split('[').slice(0,1) + "[" + top_order + "]" + old_name.split(']').slice(1).join(']');
            }

            //fix sub-sortable specific order number.
            var nearest_li = $(input).parents('.sortable li').first();
            var bottom_order = nearest_li.siblings().andSelf().not('.trash, .template').index( nearest_li );
            new_name = top_fixed;
            if(bottom_order > -1){
                var nesting = 2 * ($(input).parents('.sortable').length) - 1;
                var new_name = top_fixed.split('[').slice(0,nesting).join('[') + "[" + bottom_order + "]" + top_fixed.split(']').slice(nesting).join(']');
            }
            $(input).attr('name',new_name);
        });
    }

    /**
     * This function fixes the repeated sortable id and add-sortable data-target inherited from
     * the generating template. This funtion generates a new unique sortable id for the add-sortable
     * button to have work again.
     *
     * @param  $(.sortable li) sortable_item
     * @return null
     */
    function fix_sub_sortable_target(sortable_item){
        var addSortablebutton = sortable_item.find('.add-sortable[data-target]');
        var old_target = addSortablebutton.attr('data-target');
        //if a sub-sortable exists..
        if(addSortablebutton.length){
            //define the new target id.
            var new_target = old_target.split('_').slice(0,1) +'_'+Math.floor(Math.random() * 1000000000);
            //set the target and respective id to match.
            addSortablebutton.attr('data-target',new_target);
            sortable_item.find(old_target).attr('id',new_target.substr(1));
        }
    }
});<|endoftext|>