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

import fetchWcl from 'common/fetchWclApi';
import CombatLogParser from 'parser/core/CombatLogParser';

class ThroughputPerformance extends React.PureComponent {
  static propTypes = {
    children: PropTypes.func.isRequired,
    metric: PropTypes.string.isRequired,
    throughput: PropTypes.number.isRequired,
  };
  static contextTypes = {
    parser: PropTypes.instanceOf(CombatLogParser).isRequired,
  };

  constructor() {
    super();
    this.state = {
      performance: null,
    };
  }
  componentDidMount() {
    this.load();
  }

  load() {
    return this.loadRankings()
      // Get the best rank
      .then(({ rankings }) => rankings[0])
      // Get the throughput
      .then(rank => rank ? rank.total : null)
      .then(topThroughput => {
        this.setState({
          performance: topThroughput ? this.props.throughput / topThroughput : null,
        });
      });
  }
  loadRankings() {
    const parser = this.context.parser;

    const now = new Date();
    const onejan = new Date(now.getFullYear(), 0, 1);
    const currentWeek = Math.ceil((((now - onejan) / 86400000) + onejan.getDay() + 1) / 7); // current calendar-week

    return fetchWcl(`rankings/encounter/${parser.fight.boss}`, {
      class: parser.selectedCombatant.spec.ranking.class,
      spec: parser.selectedCombatant.spec.ranking.spec,
      difficulty: parser.fight.difficulty,
      metric: this.props.metric,
      cache: currentWeek, // cache for a week
    });
  }

  render() {
    const { children } = this.props;

    return children(this.state.performance);
  }
}

export default ThroughputPerformance;
<commit_msg>Async await beats promises<commit_after>import React from 'react';
import PropTypes from 'prop-types';

import fetchWcl from 'common/fetchWclApi';
import CombatLogParser from 'parser/core/CombatLogParser';

class ThroughputPerformance extends React.PureComponent {
  static propTypes = {
    children: PropTypes.func.isRequired,
    metric: PropTypes.string.isRequired,
    throughput: PropTypes.number.isRequired,
  };
  static contextTypes = {
    parser: PropTypes.instanceOf(CombatLogParser).isRequired,
  };

  constructor() {
    super();
    this.state = {
      performance: null,
    };
  }
  componentDidMount() {
    this.load();
  }

  async load() {
    const { rankings } = await this.loadRankings();
    const topRank = rankings[0];
    if (!topRank) {
      return;
    }
    const topThroughput = topRank.total;
    this.setState({
      performance: this.props.throughput / topThroughput,
    });
  }
  async loadRankings() {
    const parser = this.context.parser;

    const now = new Date();
    const onejan = new Date(now.getFullYear(), 0, 1);
    const currentWeek = Math.ceil((((now - onejan) / 86400000) + onejan.getDay() + 1) / 7); // current calendar-week

    return fetchWcl(`rankings/encounter/${parser.fight.boss}`, {
      class: parser.selectedCombatant.spec.ranking.class,
      spec: parser.selectedCombatant.spec.ranking.spec,
      difficulty: parser.fight.difficulty,
      metric: this.props.metric,
      cache: currentWeek, // cache for a week
    });
  }

  render() {
    const { children } = this.props;

    return children(this.state.performance);
  }
}

export default ThroughputPerformance;
<|endoftext|>


<commit_before>var DOC = {

    'menu':['api','configuration','recipe','core'],

    'vars':{sass:{},rake:{}},

    'modules':{},

    'go':function(name, fade){

        var page,
            fadeTime = typeof fade == 'undefined' ? 400 : parseInt(fade);
        name = name.toLowerCase()

        $('#content').fadeOut(fadeTime, function(){

            $('#content').html('')
            if($('#menu.active').length > 0)
            $('#menu .toggle').click()
            $('body').scrollTop(0)
            try{
                page = new EJS({url: 'page/'+name+'.ejs'})
            }catch(e){
                page = new EJS({url: 'page/construction.ejs'})
            }
            if($.inArray(name.split('/')[0], DOC.menu) < 0){
                $('#content').append('
'+page.render(DOC)+'
')
            }else{
                var menu = new EJS({url: 'component/menu/'+name.split('/')[0]+'.ejs'}),
                    menuContents = menu.render(DOC),
                    menuSet = '
'
                                +menuContents
                                +'
'
                                +'
'
                                    +'
'
                                        +'Show Articles'
                                    +'
'
                                    +'
'
                                        +menuContents
                                    +'
'
                                +'
'

                $('#content').append('
'+menuSet+'
')
                $('#content').append('
'+page.render(DOC)+'
')
            }

            $('#content').efx()
            prettyPrint()

            $('#content').fadeIn(fadeTime)

        })
    }
}<commit_msg>Fix #385 - Add prefetching for documentation pages<commit_after>var DOC = {

    'menu':['api','configuration','recipe','core'],

    'vars':{sass:{},rake:{}},

    'modules':{},

    'loading': false,

    'prefetched': [],

    'prefetch':function(name){

        if(DOC.prefetched.indexOf(name) >= 0)
            return 0

        try{
            (new EJS({url: 'page/'+name+'.ejs'})).render(DOC);
            DOC.prefetched.push(name)
            return 1
        }catch(e){
            return -1
        }

    },

    'go':function(name, fade){

        DOC.loading = true

        var page,
            fadeTime = typeof fade == 'undefined' ? 400 : parseInt(fade);
        name = name.toLowerCase()

        $('#content').fadeOut(fadeTime, function(){

            $('#content').html('')
            if($('#menu.active').length > 0)
            $('#menu .toggle').click()
            $('body').scrollTop(0)
            try{
                page = new EJS({url: 'page/'+name+'.ejs'})
            }catch(e){
                page = new EJS({url: 'page/construction.ejs'})
            }
            if($.inArray(name.split('/')[0], DOC.menu) < 0){
                $('#content').append('
'+page.render(DOC)+'
')
            }else{
                var menu = new EJS({url: 'component/menu/'+name.split('/')[0]+'.ejs'}),
                    menuContents = menu.render(DOC),
                    menuSet = '
'
                                +menuContents
                                +'
'
                                +'
'
                                    +'
'
                                        +'Show Articles'
                                    +'
'
                                    +'
'
                                        +menuContents
                                    +'
'
                                +'
'

                $('#content').append('
'+menuSet+'
')
                $('#content').append('
'+page.render(DOC)+'
')
            }

            $('#content').efx()
            prettyPrint()

            $('#content').fadeIn(fadeTime)

            DOC.loading = false

            var currentUrl = window.location.href,
                hashPos = currentUrl.indexOf('#'),
                loading = false,
                prefetchUrl, prefetchAnchor, status
            if(hashPos >= 0)
                currentUrl = currentUrl.substring(0,hashPos)

            $('body a').each(function(){

                if(DOC.loading)
                    loading = true // prepare to aboart because new DOC.go() request has been made

                if(loading)
                    return false; // abort because a new loading request occurred

                prefetchUrl = $(this).attr('href')

                if(typeof prefetchUrl != 'string')
                    return; // abort because no href

                if(prefetchUrl.substring(0,hashPos) == currentUrl){
                    prefetchAnchor = prefetchUrl.substring(hashPos)
                }else if(prefetchUrl.substring(0,1) == '#'){
                    prefetchAnchor = prefetchUrl.substring(1)
                }else{
                    return; // abort because not an anchor prefetch
                }

                status = DOC.prefetch(prefetchAnchor)

                if(window.console)
                    switch(status){
                        case 1: console.log('Prefetch PASS: '+prefetchAnchor); break;
                        case 0: console.log('Prefetch SKIP: '+prefetchAnchor); break;
                        default: console.log('Prefetch FAIL: '+prefetchAnchor); break;
                    }

            })

        })

    }
}<|endoftext|>


<commit_before><commit_msg>fix(cli): Throw a better error message in case of a failed dataset copy job (#3314)<commit_after><|endoftext|>


<commit_before>
import Observer from '../observer/Observer';
import ObservableHandler from '../observer/ObservableHandler';
import InsertTransaction from './InsertTransaction';
import UpdateTransaction from './UpdateTransaction';
import DeleteTransaction from './DeleteTransaction';
import LoginTransaction from './LoginTransaction';
import RegisterTransaction from './RegisterTransaction';

class TransactionHandler extends ObservableHandler {

    constructor(client) {
        this._client = client;
        this._transactions = {};
        this._transactionNumbers = {};
    }

    _nextTransactionId(itemType, type) {
        if (!this._transactionNumbers.hasOwnProperty(itemType)) {
            this._transactionNumbers[itemType] = 1;
        }
        const transactionId = itemType + '-' this._transactionNumbers[itemType] + '@' + type;
        this._transactionNumbers[itemType] += 1;
        return transactionId;
    }

    _register(transaction) {
        const itemType = transaction.itemType;
        if (!this._transactions.hasOwnProperty(itemType))) {
            this._transactions[itemType] = {};
        }
        this._transactions[itemType][transaction.transactionId] = transaction;
        return transaction;
    }

    createObserver() {
        return new Observer('transaction');
    }

    get listenersToDispatch() {
        const listeners = [];
        for (let itemType in this._transactions) {
            for (let transactionId in this._transactions[itemType]) {
                const transaction = this._transactions[itemType][transactionId];
                if (transaction.changed()) {
                    listeners = listeners.concat(transaction.listeners);
                }
            }
        }
        return listeners;
    }

    clearAllChanges() {
        for (let itemType in this._transactions) {
            for (let transactionId in this._transactions[itemType]) {
                this._transactions[itemType][transactionId].clearAllChanges();
            }
        }
        return this;
    }

    find(itemType, transactionId) {
        return this.hasRegisteredTransaction()
            ? this._transactions[itemType][transactionId] : undefined;
    }

    createInsert(itemType, data) {
        const transactionId = this._nextTransactionId(itemType, 'insert');
        return this._register(
            new InsertTransaction(transactionId, itemType, data, this));
    }

    createUpdate(itemType, data) {
        const transactionId = this._nextTransactionId(itemType, 'update');
        return this._register(
            new UpdateTransaction(transactionId, itemType, data, this));
    }

    createDelete(itemType, dataId) {
        const transactionId = this._nextTransactionId(itemType, 'delete');
        return this._register(
            new DeleteTransaction(transactionId, itemType, dataId, this));
    }

    createLogin(itemType, credentials) {
        const transactionId = this._nextTransactionId(itemType, 'login');
        return this._register(
            new LoginTransaction(transactionId, itemType, credentials, this));
    }

    createRegister(itemType, credentials) {
        const transactionId = this._nextTransactionId(itemType, 'register');
        return this._register(
            new RegisterTransaction(transactionId, itemType, credentials, this));
    }

    start(transaction) {
        this._client.start(transaction);
        return this;
    }

    cancel(transaction) {
        this._client.cancel(transaction);
        return this;
    }
}

export default TransactionHandler;
<commit_msg>flattened transaction map<commit_after>
import Observer from '../observer/Observer';
import ObservableHandler from '../observer/ObservableHandler';
import InsertTransaction from './InsertTransaction';
import UpdateTransaction from './UpdateTransaction';
import DeleteTransaction from './DeleteTransaction';
import LoginTransaction from './LoginTransaction';
import RegisterTransaction from './RegisterTransaction';

class TransactionHandler extends ObservableHandler {

    constructor(client) {
        this._client = client;
        this._transactions = {};
        this._transactionNumbers = {};
    }

    _nextTransactionId(itemType, type) {
        if (!this._transactionNumbers.hasOwnProperty(itemType)) {
            this._transactionNumbers[itemType] = 1;
        }
        const transactionId = itemType + '-' this._transactionNumbers[itemType] + '@' + type;
        this._transactionNumbers[itemType] += 1;
        return transactionId;
    }

    _register(transaction) {
        this._transactions[transaction.transactionId] = transaction;
        return transaction;
    }

    createObserver() {
        return new Observer('transaction');
    }

    get listenersToDispatch() {
        const listeners = [];
        for (let itemType in this._transactions) {
            for (let transactionId in this._transactions[itemType]) {
                const transaction = this._transactions[itemType][transactionId];
                if (transaction.changed()) {
                    listeners = listeners.concat(transaction.listeners);
                }
            }
        }
        return listeners;
    }

    clearAllChanges() {
        for (let itemType in this._transactions) {
            for (let transactionId in this._transactions[itemType]) {
                this._transactions[itemType][transactionId].clearAllChanges();
            }
        }
        return this;
    }

    exists(transactionId) {
        return this._transactions.hasOwnProperty(transactionId);
    }

    find(transactionId) {
        return this.exists(transactionId)
            ? this._transactions[transactionId] : undefined;
    }

    createInsert(itemType, data) {
        const transactionId = this._nextTransactionId(itemType, 'insert');
        return this._register(
            new InsertTransaction(transactionId, itemType, data, this));
    }

    createUpdate(itemType, data) {
        const transactionId = this._nextTransactionId(itemType, 'update');
        return this._register(
            new UpdateTransaction(transactionId, itemType, data, this));
    }

    createDelete(itemType, dataId) {
        const transactionId = this._nextTransactionId(itemType, 'delete');
        return this._register(
            new DeleteTransaction(transactionId, itemType, dataId, this));
    }

    createLogin(itemType, credentials) {
        const transactionId = this._nextTransactionId(itemType, 'login');
        return this._register(
            new LoginTransaction(transactionId, itemType, credentials, this));
    }

    createRegister(itemType, credentials) {
        const transactionId = this._nextTransactionId(itemType, 'register');
        return this._register(
            new RegisterTransaction(transactionId, itemType, credentials, this));
    }

    start(transaction) {
        this._client.start(transaction);
        return this;
    }

    cancel(transaction) {
        this._client.cancel(transaction);
        return this;
    }
}

export default TransactionHandler;
<|endoftext|>


<commit_before>//<!--  Contains functions to create, draw and modify towers -->
//

var towers = [];

function tower(posX,posY){
	this.posX = posX;
	this.posY = posY;
	this.direction = 0;
	this.directionSetting = 0;
	this.thrustPolar = 0;
	this.thrustX = 0;
	this.thrustY = 0;
	this.inertiaX = 0;
	this.inertiaY = 0;
	this.size = 10;
	this.image = new Image();
	this.image.src = imgRoundArrow;
	this.image.height = 10;
  this.image.width = 10;
	this.randomShort = 5;
	this.randomMedium = 5;
	this.randomLong = 5;
	this.targetCreep = 0;
	this.towerRange = 15; // this could be an arguement
	this.bornCycle = gameLoopCounter;
	this.explodeCycle = undefined;

	this.drawTower = drawTower;

	function drawTower(ctx){
		ctx.save();
	  ctx.translate(this.posX, this.posY);
	  ctx.rotate(this.direction);

	  if (this.explodeCycle == undefined){
	  	drawDot(ctx);
	  }
	  else {
    	ctx.drawImage(this.image,-50,-50); // SVG
	  };

	  ctx.restore();
	};

};

function drawDot(ctx){ // Shorthand for a square dot on canvas
	ctx.beginPath();
  ctx.lineWidth = 5
  ctx.moveTo(0,0);
  ctx.lineTo(0, 5);
  ctx.stroke();
};

function drawAllTowers(ctx){
	for (var i = 0; i < towers.length; i++) {
		if (towers[i] != undefined) {
      towers[i].drawTower(ctx);
    };
  };
};

function updateTowers(){
	createTower();
	for (var i = 0; i < towers.length; i++) {
		if (towers[i] != undefined) {
			towers[i].targetCreep = findClosestTarget(i);
			towers[i].direction = aimTower(i);
			chaseTargets(i);
			explode(i);
		};

	};
};

function createTower(){
	if (gameLoopCounter % 100 == 1){
		console.log("createTower if!")
		towers.push(new tower(250,250));
	};
};

function explode(i) {
	if (creeps[towers[i].targetCreep] == undefined) {
		console.log("explode returns early due to undefined targetCreep")
		return
	};

	var _distance = getDistance(i, towers[i].targetCreep);

	if ( (_distance < towers[i].towerRange) && (towers[i].explodeCycle == undefined) ) {
		towers[i].image.src = imgBang;
		towers[i].explodeCycle = gameLoopCounter;

		splatterCircle(towers[i].posX, towers[i].posY, 'green', 50);

		creeps[towers[i].targetCreep].image.src = imgUnicorn;
		creeps[towers[i].targetCreep].hitPoints = creeps[towers[i].targetCreep].hitPoints - 10;

		// console.log("Explode triggered! Tower: " + i)
		// towers[i].direction = towers[i].direction + 5
	};

	// if ( towers[i].bornCycle + 100 == ){};

	if ( towers[i].explodeCycle < gameLoopCounter - 5)  {
		towers[i] = undefined;
		// towers[i].image.src = imgBang;
		// towers[i].image.src = imgKaleidoscope;
		// console.log("Explode ended! Tower: " + i)
	};

};

function updateThrustX(i) {
	// console.log("direction " + towers[i].direction)
  return (towers[i].thrustPolar * Math.sin(towers[i].direction));
};

function updateThrustY(i) {
	// console.log(towers[i].direction)
  return (-1 * towers[i].thrustPolar * Math.cos(towers[i].direction));
};

function chaseTargets(i){
	towers[i].thrustPolar = 2; // hardcoded for now

	// console.log("tower " + i + ", thrustX = " + updateThrustX(i));

	towers[i].inertiaX = towers[i].inertiaX + updateThrustX(i);
	towers[i].inertiaY = towers[i].inertiaY + updateThrustY(i);

	// console.log("tower " + i + ", inertiaX = " + towers[i].inertiaX);

	towers[i].posX += towers[i].inertiaX;
	towers[i].posY += towers[i].inertiaY;
};

function findClosestTarget(towerIndex) { // finds nearest creep
	var creepDistance = undefined;
	for (var i = 0; i < creeps.length; i++) {
		if (creeps[i] != undefined) {

			var _distance =	getDistance(towerIndex, i)

			if (creepDistance > _distance || creepDistance == undefined) {
				index = i;
				creepDistance = _distance;
			};
		};
	};
	return index
};

function getDistance(towerIndex, creepIndex){ // FIND DISTANCE FROM TOWER TO CREEP

	if (towers[towerIndex] == undefined) {
		console.log("getDistance aborted early due to undefined tower Object")
		console.log("towerIndex = " + towerIndex)
		return
	};

	if (creeps[creepIndex] == undefined) {
		console.log("getDistance aborted early due to undefined creep Object")
		console.log("creepIndex = " + creepIndex)
		return
	};

	var distanceX = creeps[creepIndex].posX - towers[towerIndex].posX
	var distanceY = creeps[creepIndex].posY - towers[towerIndex].posY
	var distance = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY))
	return distance
};

function aimTower(i) {
	if (towers[i] != undefined && creeps[towers[i].targetCreep] != undefined){
		_target = towers[i].targetCreep
		var _distanceX = (creeps[_target].posX) - towers[i].posX;
		var _distanceY = ((creeps[_target].posY) - towers[i].posY ) * -1;
	 	var _direction = Math.atan2(_distanceX, _distanceY)
	 	return _direction
  };
};
<commit_msg>Created splatterSplash<commit_after>//<!--  Contains functions to create, draw and modify towers -->
//

var towers = [];

function tower(posX,posY){
	this.posX = posX;
	this.posY = posY;
	this.direction = 0;
	this.directionSetting = 0;
	this.thrustPolar = 0;
	this.thrustX = 0;
	this.thrustY = 0;
	this.inertiaX = 0;
	this.inertiaY = 0;
	this.size = 10;
	this.image = new Image();
	this.image.src = imgRoundArrow;
	this.image.height = 10;
  this.image.width = 10;
	this.randomShort = 5;
	this.randomMedium = 5;
	this.randomLong = 5;
	this.targetCreep = 0;
	this.towerRange = 15; // this could be an arguement
	this.bornCycle = gameLoopCounter;
	this.explodeCycle = undefined;

	this.drawTower = drawTower;

	function drawTower(ctx){
		ctx.save();
	  ctx.translate(this.posX, this.posY);
	  ctx.rotate(this.direction);

	  if (this.explodeCycle == undefined){
	  	drawDot(ctx);
	  }
	  else {
    	ctx.drawImage(this.image,-50,-50); // SVG
	  };

	  ctx.restore();
	};

};

function drawDot(ctx){ // Shorthand for a square dot on canvas
	ctx.beginPath();
  ctx.lineWidth = 5
  ctx.moveTo(0,0);
  ctx.lineTo(0, 5);
  ctx.stroke();
};

function drawAllTowers(ctx){
	for (var i = 0; i < towers.length; i++) {
		if (towers[i] != undefined) {
      towers[i].drawTower(ctx);
    };
  };
};

function updateTowers(){
	createTower();
	for (var i = 0; i < towers.length; i++) {
		if (towers[i] != undefined) {
			towers[i].targetCreep = findClosestTarget(i);
			towers[i].direction = aimTower(i);
			chaseTargets(i);
			explode(i);
		};

	};
};

function createTower(){
	if (gameLoopCounter % 100 == 1){
		console.log("createTower if!")
		towers.push(new tower(250,250));
	};
};

function explode(i) {
	if (creeps[towers[i].targetCreep] == undefined) {
		console.log("explode returns early due to undefined targetCreep")
		return
	};

	var _distance = getDistance(i, towers[i].targetCreep);

	if ( (_distance < towers[i].towerRange) && (towers[i].explodeCycle == undefined) ) {
		towers[i].image.src = imgBang;
		towers[i].explodeCycle = gameLoopCounter;

		splatterSplash(towers[i].posX, towers[i].posY, 'green', 50);

		creeps[towers[i].targetCreep].image.src = imgUnicorn;
		creeps[towers[i].targetCreep].hitPoints = creeps[towers[i].targetCreep].hitPoints - 10;

		// console.log("Explode triggered! Tower: " + i)
		// towers[i].direction = towers[i].direction + 5
	};

	// if ( towers[i].bornCycle + 100 == ){};

	if ( towers[i].explodeCycle < gameLoopCounter - 5)  {
		towers[i] = undefined;
		// towers[i].image.src = imgBang;
		// towers[i].image.src = imgKaleidoscope;
		// console.log("Explode ended! Tower: " + i)
	};

};

function updateThrustX(i) {
	// console.log("direction " + towers[i].direction)
  return (towers[i].thrustPolar * Math.sin(towers[i].direction));
};

function updateThrustY(i) {
	// console.log(towers[i].direction)
  return (-1 * towers[i].thrustPolar * Math.cos(towers[i].direction));
};

function chaseTargets(i){
	towers[i].thrustPolar = 2; // hardcoded for now

	// console.log("tower " + i + ", thrustX = " + updateThrustX(i));

	towers[i].inertiaX = towers[i].inertiaX + updateThrustX(i);
	towers[i].inertiaY = towers[i].inertiaY + updateThrustY(i);

	// console.log("tower " + i + ", inertiaX = " + towers[i].inertiaX);

	towers[i].posX += towers[i].inertiaX;
	towers[i].posY += towers[i].inertiaY;
};

function findClosestTarget(towerIndex) { // finds nearest creep
	var creepDistance = undefined;
	for (var i = 0; i < creeps.length; i++) {
		if (creeps[i] != undefined) {

			var _distance =	getDistance(towerIndex, i)

			if (creepDistance > _distance || creepDistance == undefined) {
				index = i;
				creepDistance = _distance;
			};
		};
	};
	return index
};

function getDistance(towerIndex, creepIndex){ // FIND DISTANCE FROM TOWER TO CREEP

	if (towers[towerIndex] == undefined) {
		console.log("getDistance aborted early due to undefined tower Object")
		console.log("towerIndex = " + towerIndex)
		return
	};

	if (creeps[creepIndex] == undefined) {
		console.log("getDistance aborted early due to undefined creep Object")
		console.log("creepIndex = " + creepIndex)
		return
	};

	var distanceX = creeps[creepIndex].posX - towers[towerIndex].posX
	var distanceY = creeps[creepIndex].posY - towers[towerIndex].posY
	var distance = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY))
	return distance
};

function aimTower(i) {
	if (towers[i] != undefined && creeps[towers[i].targetCreep] != undefined){
		_target = towers[i].targetCreep
		var _distanceX = (creeps[_target].posX) - towers[i].posX;
		var _distanceY = ((creeps[_target].posY) - towers[i].posY ) * -1;
	 	var _direction = Math.atan2(_distanceX, _distanceY)
	 	return _direction
  };
};
<|endoftext|>


<commit_before>// @flow
import { PAGE_SIZE } from 'constants/claim';
import * as ACTIONS from 'constants/action_types';
import * as SETTINGS from 'constants/settings';
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { Lbry, buildURI, parseURI, doResolveUris } from 'lbry-redux';
import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content';

const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;

export const doSetViewMode = (viewMode: ViewMode) => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.SET_VIEW_MODE,
    data: viewMode,
  });

export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetState) => {
  const state: { subscriptions: SubscriptionState, settings: any } = getState();
  const { subscriptions: reduxSubscriptions } = state.subscriptions;
  const { share_usage_data: isSharingData } = state.settings.daemonSettings;

  if (!isSharingData && isSharingData !== undefined) {
    // They aren't sharing their data, subscriptions will be handled by persisted redux state
    return;
  }

  // most of this logic comes from scenarios where the db isn't synced with redux
  // this will happen if the user stops sharing data
  dispatch({ type: ACTIONS.FETCH_SUBSCRIPTIONS_START });

  Lbryio.call('subscription', 'list')
    .then(dbSubscriptions => {
      const storedSubscriptions = dbSubscriptions || [];

      // User has no subscriptions in db or redux
      if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) {
        return [];
      }

      // There is some mismatch between redux state and db state
      // If something is in the db, but not in redux, add it to redux
      // If something is in redux, but not in the db, add it to the db
      if (storedSubscriptions.length !== reduxSubscriptions.length) {
        const dbSubMap = {};
        const reduxSubMap = {};
        const subsNotInDB = [];
        const subscriptionsToReturn = reduxSubscriptions.slice();

        storedSubscriptions.forEach(sub => {
          dbSubMap[sub.claim_id] = 1;
        });

        reduxSubscriptions.forEach(sub => {
          const { claimId } = parseURI(sub.uri);
          reduxSubMap[claimId] = 1;

          if (!dbSubMap[claimId]) {
            subsNotInDB.push({
              claim_id: claimId,
              channel_name: sub.channelName,
            });
          }
        });

        storedSubscriptions.forEach(sub => {
          if (!reduxSubMap[sub.claim_id]) {
            const uri = `lbry://${sub.channel_name}#${sub.claim_id}`;
            subscriptionsToReturn.push({ uri, channelName: sub.channel_name });
          }
        });

        return Promise.all(subsNotInDB.map(payload => Lbryio.call('subscription', 'new', payload)))
          .then(() => subscriptionsToReturn)
          .catch(
            () =>
              // let it fail, we will try again when the navigate to the subscriptions page
              subscriptionsToReturn
          );
      }

      // DB is already synced, just return the subscriptions in redux
      return reduxSubscriptions;
    })
    .then((subscriptions: Array<Subscription>) => {
      dispatch({
        type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
        data: subscriptions,
      });

      dispatch(doResolveUris(subscriptions.map(({ uri }) => uri)));
      dispatch(doCheckSubscriptions());
    })
    .catch(() => {
      dispatch({
        type: ACTIONS.FETCH_SUBSCRIPTIONS_FAIL,
      });
    });
};

export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
  dispatch: Dispatch
) =>
  dispatch({
    type: ACTIONS.SET_SUBSCRIPTION_LATEST,
    data: {
      subscription,
      uri,
    },
  });

// Populate a channels unread subscriptions or update the type
export const doUpdateUnreadSubscriptions = (
  channelUri: string,
  uris: ?Array<string>,
  type: ?SubscriptionNotificationType
) => (dispatch: Dispatch, getState: GetState) => {
  const state = getState();
  const unreadByChannel = selectUnreadByChannel(state);
  const currentUnreadForChannel: UnreadSubscription = unreadByChannel[channelUri];

  let newUris;
  let newType;

  if (!currentUnreadForChannel) {
    newUris = uris;
    newType = type;
  } else {
    if (uris) {
      // If a channel currently has no unread uris, just add them all
      if (!currentUnreadForChannel.uris || !currentUnreadForChannel.uris.length) {
        newUris = uris;
      } else {
        // They already have unreads and now there are new ones
        // Add the new ones to the beginning of the list
        // Make sure there are no duplicates
        const currentUnreadUris = currentUnreadForChannel.uris;
        newUris = uris.filter(uri => !currentUnreadUris.includes(uri)).concat(currentUnreadUris);
      }
    } else {
      newUris = currentUnreadForChannel.uris;
    }

    newType = type || currentUnreadForChannel.type;
  }

  dispatch({
    type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
    data: {
      channel: channelUri,
      uris: newUris,
      type: newType,
    },
  });
};

// Remove multiple files (or all) from a channels unread subscriptions
export const doRemoveUnreadSubscriptions = (channelUri: ?string, readUris: ?Array<string>) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const state = getState();
  const unreadByChannel = selectUnreadByChannel(state);

  // If no channel is passed in, remove all unread subscriptions from all channels
  if (!channelUri) {
    return dispatch({
      type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
      data: { channel: null },
    });
  }

  const currentChannelUnread = unreadByChannel[channelUri];
  if (!currentChannelUnread || !currentChannelUnread.uris) {
    // Channel passed in doesn't have any unreads
    return;
  }

  // For each uri passed in, remove it from the list of unread uris
  // If no uris are passed in, remove them all
  let newUris;
  if (readUris) {
    const urisToRemoveMap = readUris.reduce(
      (acc, val) => ({
        ...acc,
        [val]: true,
      }),
      {}
    );

    const filteredUris = currentChannelUnread.uris.filter(uri => !urisToRemoveMap[uri]);
    newUris = filteredUris.length ? filteredUris : null;
  } else {
    newUris = null;
  }

  dispatch({
    type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
    data: {
      channel: channelUri,
      uris: newUris,
    },
  });
};

// Remove a single file from a channels unread subscriptions
export const doRemoveUnreadSubscription = (channelUri: string, readUri: string) => (
  dispatch: Dispatch
) => {
  dispatch(doRemoveUnreadSubscriptions(channelUri, [readUri]));
};

export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: boolean) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  // no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>

  const state = getState();
  const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
  const savedSubscription = state.subscriptions.subscriptions.find(
    sub => sub.uri === subscriptionUri
  );

  if (!savedSubscription) {
    throw Error(
      `Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`
    );
  }

  const { claimId } = parseURI(subscriptionUri);

  // We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
  Lbry.claim_search({ channel_id: claimId, page: 1, page_size: PAGE_SIZE }).then(
    claimListByChannel => {
      const { items: claimsInChannel } = claimListByChannel;

      // may happen if subscribed to an abandoned channel or an empty channel
      if (!claimsInChannel || !claimsInChannel.length) {
        return;
      }

      // Determine if the latest subscription currently saved is actually the latest subscription
      const latestIndex = claimsInChannel.findIndex(
        claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
      );

      // If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
      const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;

      // If latest is 0, nothing has changed
      // Do not download/notify about new content, it would download/notify 10 claims per channel
      if (latestIndex !== 0 && savedSubscription.latest) {
        let downloadCount = 0;

        const newUnread = [];
        claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
          const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
          const shouldDownload =
            shouldAutoDownload &&
            Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.stream.fee);

          // Add the new content to the list of "un-read" subscriptions
          if (shouldNotify) {
            newUnread.push(uri);
          }

          if (shouldDownload) {
            downloadCount += 1;
            dispatch(doPurchaseUri(uri, { cost: 0 }, true));
          }
        });

        dispatch(
          doUpdateUnreadSubscriptions(
            subscriptionUri,
            newUnread,
            downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
          )
        );
      }

      // Set the latest piece of content for a channel
      // This allows the app to know if there has been new content since it was last set
      dispatch(
        setSubscriptionLatest(
          {
            channelName: claimsInChannel[0].channel_name,
            uri: buildURI(
              {
                channelName: claimsInChannel[0].channel_name,
                claimId: claimsInChannel[0].claim_id,
              },
              false
            ),
          },
          buildURI(
            { contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
            false
          )
        )
      );

      // calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
      // means it will delete a non-existant fetchingChannelClaims[uri]
      dispatch({
        type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
        data: {
          uri: subscriptionUri,
          claims: claimsInChannel || [],
          page: 1,
        },
      });
    }
  );
};

export const doChannelSubscribe = (subscription: Subscription) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const {
    settings: { daemonSettings },
  } = getState();
  const { share_usage_data: isSharingData } = daemonSettings;

  const subscriptionUri = subscription.uri;
  if (!subscriptionUri.startsWith('lbry://')) {
    throw Error(
      `Subscription uris must inclue the "lbry://" prefix.\nTried to subscribe to ${subscriptionUri}`
    );
  }

  dispatch({
    type: ACTIONS.CHANNEL_SUBSCRIBE,
    data: subscription,
  });

  // if the user isn't sharing data, keep the subscriptions entirely in the app
  if (isSharingData) {
    const { claimId } = parseURI(subscription.uri);
    // They are sharing data, we can store their subscriptions in our internal database
    Lbryio.call('subscription', 'new', {
      channel_name: subscription.channelName,
      claim_id: claimId,
    });

    dispatch(doClaimRewardType(rewards.TYPE_SUBSCRIPTION, { failSilently: true }));
  }

  dispatch(doCheckSubscription(subscription.uri, true));
};

export const doChannelUnsubscribe = (subscription: Subscription) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const {
    settings: { daemonSettings },
  } = getState();
  const { share_usage_data: isSharingData } = daemonSettings;

  dispatch({
    type: ACTIONS.CHANNEL_UNSUBSCRIBE,
    data: subscription,
  });

  if (isSharingData) {
    const { claimId } = parseURI(subscription.uri);
    Lbryio.call('subscription', 'delete', {
      claim_id: claimId,
    });
  }
};

export const doCheckSubscriptions = () => (dispatch: Dispatch, getState: GetState) => {
  const state = getState();
  const subscriptions = selectSubscriptions(state);

  subscriptions.forEach((sub: Subscription) => {
    dispatch(doCheckSubscription(sub.uri, true));
  });
};

export const doCheckSubscriptionsInit = () => (dispatch: Dispatch) => {
  // doCheckSubscriptionsInit is called by doDaemonReady
  // setTimeout below is a hack to ensure redux is hydrated when subscriptions are checked
  // this will be replaced with <PersistGate> which reqiures a package upgrade
  setTimeout(() => dispatch(doFetchMySubscriptions()), 5000);
  const checkSubscriptionsTimer = setInterval(
    () => dispatch(doCheckSubscriptions()),
    CHECK_SUBSCRIPTIONS_INTERVAL
  );
  dispatch({
    type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
    data: { checkSubscriptionsTimer },
  });
};

export const doFetchRecommendedSubscriptions = () => (dispatch: Dispatch) => {
  dispatch({
    type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
  });

  return Lbryio.call('subscription', 'suggest')
    .then(suggested =>
      dispatch({
        type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS,
        data: suggested,
      })
    )
    .catch(error =>
      dispatch({
        type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL,
        error,
      })
    );
};

export const doCompleteFirstRun = () => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED,
  });

export const doShowSuggestedSubs = () => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS,
  });
<commit_msg>Fix: fee value on sub page<commit_after>// @flow
import { PAGE_SIZE } from 'constants/claim';
import * as ACTIONS from 'constants/action_types';
import * as SETTINGS from 'constants/settings';
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { Lbry, buildURI, parseURI, doResolveUris } from 'lbry-redux';
import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content';

const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;

export const doSetViewMode = (viewMode: ViewMode) => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.SET_VIEW_MODE,
    data: viewMode,
  });

export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetState) => {
  const state: { subscriptions: SubscriptionState, settings: any } = getState();
  const { subscriptions: reduxSubscriptions } = state.subscriptions;
  const { share_usage_data: isSharingData } = state.settings.daemonSettings;

  if (!isSharingData && isSharingData !== undefined) {
    // They aren't sharing their data, subscriptions will be handled by persisted redux state
    return;
  }

  // most of this logic comes from scenarios where the db isn't synced with redux
  // this will happen if the user stops sharing data
  dispatch({ type: ACTIONS.FETCH_SUBSCRIPTIONS_START });

  Lbryio.call('subscription', 'list')
    .then(dbSubscriptions => {
      const storedSubscriptions = dbSubscriptions || [];

      // User has no subscriptions in db or redux
      if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) {
        return [];
      }

      // There is some mismatch between redux state and db state
      // If something is in the db, but not in redux, add it to redux
      // If something is in redux, but not in the db, add it to the db
      if (storedSubscriptions.length !== reduxSubscriptions.length) {
        const dbSubMap = {};
        const reduxSubMap = {};
        const subsNotInDB = [];
        const subscriptionsToReturn = reduxSubscriptions.slice();

        storedSubscriptions.forEach(sub => {
          dbSubMap[sub.claim_id] = 1;
        });

        reduxSubscriptions.forEach(sub => {
          const { claimId } = parseURI(sub.uri);
          reduxSubMap[claimId] = 1;

          if (!dbSubMap[claimId]) {
            subsNotInDB.push({
              claim_id: claimId,
              channel_name: sub.channelName,
            });
          }
        });

        storedSubscriptions.forEach(sub => {
          if (!reduxSubMap[sub.claim_id]) {
            const uri = `lbry://${sub.channel_name}#${sub.claim_id}`;
            subscriptionsToReturn.push({ uri, channelName: sub.channel_name });
          }
        });

        return Promise.all(subsNotInDB.map(payload => Lbryio.call('subscription', 'new', payload)))
          .then(() => subscriptionsToReturn)
          .catch(
            () =>
              // let it fail, we will try again when the navigate to the subscriptions page
              subscriptionsToReturn
          );
      }

      // DB is already synced, just return the subscriptions in redux
      return reduxSubscriptions;
    })
    .then((subscriptions: Array<Subscription>) => {
      dispatch({
        type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
        data: subscriptions,
      });

      dispatch(doResolveUris(subscriptions.map(({ uri }) => uri)));
      dispatch(doCheckSubscriptions());
    })
    .catch(() => {
      dispatch({
        type: ACTIONS.FETCH_SUBSCRIPTIONS_FAIL,
      });
    });
};

export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
  dispatch: Dispatch
) =>
  dispatch({
    type: ACTIONS.SET_SUBSCRIPTION_LATEST,
    data: {
      subscription,
      uri,
    },
  });

// Populate a channels unread subscriptions or update the type
export const doUpdateUnreadSubscriptions = (
  channelUri: string,
  uris: ?Array<string>,
  type: ?SubscriptionNotificationType
) => (dispatch: Dispatch, getState: GetState) => {
  const state = getState();
  const unreadByChannel = selectUnreadByChannel(state);
  const currentUnreadForChannel: UnreadSubscription = unreadByChannel[channelUri];

  let newUris;
  let newType;

  if (!currentUnreadForChannel) {
    newUris = uris;
    newType = type;
  } else {
    if (uris) {
      // If a channel currently has no unread uris, just add them all
      if (!currentUnreadForChannel.uris || !currentUnreadForChannel.uris.length) {
        newUris = uris;
      } else {
        // They already have unreads and now there are new ones
        // Add the new ones to the beginning of the list
        // Make sure there are no duplicates
        const currentUnreadUris = currentUnreadForChannel.uris;
        newUris = uris.filter(uri => !currentUnreadUris.includes(uri)).concat(currentUnreadUris);
      }
    } else {
      newUris = currentUnreadForChannel.uris;
    }

    newType = type || currentUnreadForChannel.type;
  }

  dispatch({
    type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
    data: {
      channel: channelUri,
      uris: newUris,
      type: newType,
    },
  });
};

// Remove multiple files (or all) from a channels unread subscriptions
export const doRemoveUnreadSubscriptions = (channelUri: ?string, readUris: ?Array<string>) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const state = getState();
  const unreadByChannel = selectUnreadByChannel(state);

  // If no channel is passed in, remove all unread subscriptions from all channels
  if (!channelUri) {
    return dispatch({
      type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
      data: { channel: null },
    });
  }

  const currentChannelUnread = unreadByChannel[channelUri];
  if (!currentChannelUnread || !currentChannelUnread.uris) {
    // Channel passed in doesn't have any unreads
    return;
  }

  // For each uri passed in, remove it from the list of unread uris
  // If no uris are passed in, remove them all
  let newUris;
  if (readUris) {
    const urisToRemoveMap = readUris.reduce(
      (acc, val) => ({
        ...acc,
        [val]: true,
      }),
      {}
    );

    const filteredUris = currentChannelUnread.uris.filter(uri => !urisToRemoveMap[uri]);
    newUris = filteredUris.length ? filteredUris : null;
  } else {
    newUris = null;
  }

  dispatch({
    type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
    data: {
      channel: channelUri,
      uris: newUris,
    },
  });
};

// Remove a single file from a channels unread subscriptions
export const doRemoveUnreadSubscription = (channelUri: string, readUri: string) => (
  dispatch: Dispatch
) => {
  dispatch(doRemoveUnreadSubscriptions(channelUri, [readUri]));
};

export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: boolean) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  // no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>

  const state = getState();
  const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
  const savedSubscription = state.subscriptions.subscriptions.find(
    sub => sub.uri === subscriptionUri
  );

  if (!savedSubscription) {
    throw Error(
      `Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`
    );
  }

  const { claimId } = parseURI(subscriptionUri);

  // We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
  Lbry.claim_search({ channel_id: claimId, page: 1, page_size: PAGE_SIZE }).then(
    claimListByChannel => {
      const { items: claimsInChannel } = claimListByChannel;

      // may happen if subscribed to an abandoned channel or an empty channel
      if (!claimsInChannel || !claimsInChannel.length) {
        return;
      }

      // Determine if the latest subscription currently saved is actually the latest subscription
      const latestIndex = claimsInChannel.findIndex(
        claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
      );

      // If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
      const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;

      // If latest is 0, nothing has changed
      // Do not download/notify about new content, it would download/notify 10 claims per channel
      if (latestIndex !== 0 && savedSubscription.latest) {
        let downloadCount = 0;

        const newUnread = [];
        claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
          const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
          const shouldDownload =
            shouldAutoDownload &&
            Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee;

          // Add the new content to the list of "un-read" subscriptions
          if (shouldNotify) {
            newUnread.push(uri);
          }

          if (shouldDownload) {
            downloadCount += 1;
            dispatch(doPurchaseUri(uri, { cost: 0 }, true));
          }
        });

        dispatch(
          doUpdateUnreadSubscriptions(
            subscriptionUri,
            newUnread,
            downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
          )
        );
      }

      // Set the latest piece of content for a channel
      // This allows the app to know if there has been new content since it was last set
      dispatch(
        setSubscriptionLatest(
          {
            channelName: claimsInChannel[0].channel_name,
            uri: buildURI(
              {
                channelName: claimsInChannel[0].channel_name,
                claimId: claimsInChannel[0].claim_id,
              },
              false
            ),
          },
          buildURI(
            { contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
            false
          )
        )
      );

      // calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
      // means it will delete a non-existant fetchingChannelClaims[uri]
      dispatch({
        type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
        data: {
          uri: subscriptionUri,
          claims: claimsInChannel || [],
          page: 1,
        },
      });
    }
  );
};

export const doChannelSubscribe = (subscription: Subscription) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const {
    settings: { daemonSettings },
  } = getState();
  const { share_usage_data: isSharingData } = daemonSettings;

  const subscriptionUri = subscription.uri;
  if (!subscriptionUri.startsWith('lbry://')) {
    throw Error(
      `Subscription uris must inclue the "lbry://" prefix.\nTried to subscribe to ${subscriptionUri}`
    );
  }

  dispatch({
    type: ACTIONS.CHANNEL_SUBSCRIBE,
    data: subscription,
  });

  // if the user isn't sharing data, keep the subscriptions entirely in the app
  if (isSharingData) {
    const { claimId } = parseURI(subscription.uri);
    // They are sharing data, we can store their subscriptions in our internal database
    Lbryio.call('subscription', 'new', {
      channel_name: subscription.channelName,
      claim_id: claimId,
    });

    dispatch(doClaimRewardType(rewards.TYPE_SUBSCRIPTION, { failSilently: true }));
  }

  dispatch(doCheckSubscription(subscription.uri, true));
};

export const doChannelUnsubscribe = (subscription: Subscription) => (
  dispatch: Dispatch,
  getState: GetState
) => {
  const {
    settings: { daemonSettings },
  } = getState();
  const { share_usage_data: isSharingData } = daemonSettings;

  dispatch({
    type: ACTIONS.CHANNEL_UNSUBSCRIBE,
    data: subscription,
  });

  if (isSharingData) {
    const { claimId } = parseURI(subscription.uri);
    Lbryio.call('subscription', 'delete', {
      claim_id: claimId,
    });
  }
};

export const doCheckSubscriptions = () => (dispatch: Dispatch, getState: GetState) => {
  const state = getState();
  const subscriptions = selectSubscriptions(state);

  subscriptions.forEach((sub: Subscription) => {
    dispatch(doCheckSubscription(sub.uri, true));
  });
};

export const doCheckSubscriptionsInit = () => (dispatch: Dispatch) => {
  // doCheckSubscriptionsInit is called by doDaemonReady
  // setTimeout below is a hack to ensure redux is hydrated when subscriptions are checked
  // this will be replaced with <PersistGate> which reqiures a package upgrade
  setTimeout(() => dispatch(doFetchMySubscriptions()), 5000);
  const checkSubscriptionsTimer = setInterval(
    () => dispatch(doCheckSubscriptions()),
    CHECK_SUBSCRIPTIONS_INTERVAL
  );
  dispatch({
    type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
    data: { checkSubscriptionsTimer },
  });
};

export const doFetchRecommendedSubscriptions = () => (dispatch: Dispatch) => {
  dispatch({
    type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
  });

  return Lbryio.call('subscription', 'suggest')
    .then(suggested =>
      dispatch({
        type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS,
        data: suggested,
      })
    )
    .catch(error =>
      dispatch({
        type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL,
        error,
      })
    );
};

export const doCompleteFirstRun = () => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED,
  });

export const doShowSuggestedSubs = () => (dispatch: Dispatch) =>
  dispatch({
    type: ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS,
  });
<|endoftext|>


<commit_before>async function checkCanMakePayment(request) {
  if (!request.canMakePayment) {
    return;
  }

  try {
    const result = await request.canMakePayment();
    info(result ? 'Can make payment.' : 'Cannot make payment.');
  } catch (e) {
    error('Can make payment: ' + e.toString());
  }
}

async function checkHasEnrolledInstrument(request) {
  if (!request.hasEnrolledInstrument) {
    return;
  }

  try {
    const result = await request.hasEnrolledInstrument();
    info(result ? 'Has enrolled instrument.' : 'No enrolled instrument.');
  } catch (e) {
    error('Has enrolled instrument: ' + e.toString());
  }
}

function buildPaymentRequest() {
  if (!window.PaymentRequest) {
    error('PaymentRequest API is not supported.');
    return null;
  }

  const supportedInstruments = [{
    supportedMethods: 'https://rsolomakhin.github.io/pr/apps/pmc',
  }];

  const details = {
    total: {
      label: 'Payment',
      amount: {
        currency: 'USD',
        value: '0.99',
      },
    },
  };

  const globalDetails = {
    total: {
      label: 'Donation',
      amount: {
        currency: 'EUR',
        value: '1.11',
      },
    },
  };

  let request = null;

  try {
    request = new PaymentRequest(supportedInstruments, details);
  } catch (e) {
    error('Payment request: ' + e.toString());
    return null;
  }

  if (request.onpaymentmethodchange !== undefined) {
    request.addEventListener('paymentmethodchange', (evt) => {
      console.log('Payment method change event: ' + JSON.stringify({'methodName': evt.methodName, 'methodDetails': evt.methodDetails}, undefined, 2));
      evt.respondWith(respondAsync(evt, details, globalDetails));
    });
  }

  checkCanMakePayment(request);
  checkHasEnrolledInstrument(request);

  return request;
}

function respondAsync(evt, details, globalDetails) {
  return new Promise(function(resolve, reject) {
    window.setTimeout(() => {
      if (evt.methodDetails && evt.methodDetails.billingAddress && evt.methodDetails.billingAddress.country) {
        if (evt.methodDetails.billingAddress.country === 'US') {
          evt.updateWith(details);
        } else {
          evt.updateWith(globalDetails);
        }
      }
    }, 1000);
  });
}

let request = buildPaymentRequest();

async function onBuyClicked() { // eslint-disable-line no-unused-vars
  if (!request) {
    return;
  }

  clearAllMessages();

  try {
    const instrumentResponse = await request.show()
    await instrumentResponse.complete('success');
    done('This is a demo website. No payment will be processed.', instrumentResponse);
  } catch (e) {
    error(e.toString());
    request = buildPaymentRequest();
  }
}
<commit_msg>Fix JS<commit_after>async function checkCanMakePayment(request) {
  if (!request.canMakePayment) {
    return;
  }

  try {
    const result = await request.canMakePayment();
    info(result ? 'Can make payment.' : 'Cannot make payment.');
  } catch (e) {
    error('Can make payment: ' + e.toString());
  }
}

async function checkHasEnrolledInstrument(request) {
  if (!request.hasEnrolledInstrument) {
    return;
  }

  try {
    const result = await request.hasEnrolledInstrument();
    info(result ? 'Has enrolled instrument.' : 'No enrolled instrument.');
  } catch (e) {
    error('Has enrolled instrument: ' + e.toString());
  }
}

function buildPaymentRequest() {
  if (!window.PaymentRequest) {
    error('PaymentRequest API is not supported.');
    return null;
  }

  const supportedInstruments = [{
    supportedMethods: 'https://rsolomakhin.github.io/pr/apps/pmc',
  }];

  const details = {
    total: {
      label: 'Payment',
      amount: {
        currency: 'USD',
        value: '0.99',
      },
    },
  };

  const globalDetails = {
    total: {
      label: 'Donation',
      amount: {
        currency: 'EUR',
        value: '1.11',
      },
    },
  };

  let request = null;

  try {
    request = new PaymentRequest(supportedInstruments, details);
  } catch (e) {
    error('Payment request: ' + e.toString());
    return null;
  }

  if (request.onpaymentmethodchange !== undefined) {
    request.addEventListener('paymentmethodchange', (evt) => {
      console.log('Payment method change event: ' + JSON.stringify({'methodName': evt.methodName, 'methodDetails': evt.methodDetails}, undefined, 2));
      evt.updateWith(respondAsync(evt, details, globalDetails));
    });
  }

  checkCanMakePayment(request);
  checkHasEnrolledInstrument(request);

  return request;
}

function respondAsync(evt, details, globalDetails) {
  return new Promise(function(resolve, reject) {
    window.setTimeout(() => {
      if (evt.methodDetails && evt.methodDetails.billingAddress && evt.methodDetails.billingAddress.country) {
        if (evt.methodDetails.billingAddress.country === 'US') {
          resolve(details);
        } else {
          resolve(globalDetails);
        }
      }
    }, 1000);
  });
}

let request = buildPaymentRequest();

async function onBuyClicked() { // eslint-disable-line no-unused-vars
  if (!request) {
    return;
  }

  clearAllMessages();

  try {
    const instrumentResponse = await request.show()
    await instrumentResponse.complete('success');
    done('This is a demo website. No payment will be processed.', instrumentResponse);
  } catch (e) {
    error(e.toString());
    request = buildPaymentRequest();
  }
}
<|endoftext|>


<commit_before>var TIMELINE = {};

TIMELINE.generatePostElement = function(post) {
	return $('
')
	.attr({
		title: post.createdAt + '
via ' + post.app.name,
		'data-id': post.id,
		'data-userId': post.userId,
		'data-userComment': post.user.comment,
		'data-userColor': post.user.color,
		style: post.isReply ? 'border-color: ' + post.reply.user.color + ';' : ''
	})
	.append(post.isReply ? generateReplyTo() : null)
	.append(generateArticle(post))
	.append(generateFooter());

	function generateReplyTo() {
		return $('
')
		.append(generateArticle(post.reply));
	}

	function generateArticle(post) {
		return $('
')
		.append(generateIcon())
		.append(generateHeader())
		.append($('
').html((post.isReply ? ' ' : '') + parseText(post.text)))
		.append(post.isImageAttached ? generateImage() : null);

		function generateIcon() {
			return $('')
			.attr('href', conf.url + '/' + post.user.screenName)
			.append(
			$('')
			.attr('src', conf.url + '/img/icon/' + post.user.screenName)
			);
		}

		function generateHeader() {
			return generateHeader()
			.append(generateScreenName())
			.append(generateTime());

			function generateHeader() {
				return $('').prepend($('
').prepend(generateName()));
			}

			function generateName() {
				return $('')
				.attr('href', conf.url + '/' + post.user.screenName)
				.text(escapeHtml(post.user.name));
			}

			function generateScreenName() {
				return $('')
				.text('@' + post.user.screenName);
			}

			function generateTime() {
				return $('<time>')
				.attr('datetime', post.createdAt)
				.text(post.createdAt);
			}
		}

		function generateImage() {
			return $('')
			.attr('src', conf.url + '/img/post/' + post.id);
		}
	}

	function generateFooter() {
		return $('')
		.append(generateForm());

		function generateForm() {
			return $('<form class="replyForm">')
			.append(generateTextArea())
			.append(generateInReplyToPostId())
			.append(generateSubmitButton())
			.append(generateImageAttacher());

			function generateTextArea() {
				return $('<textarea name="text">')
				.text('@' + post.user.screenName);
			}

			function generateInReplyToPostId() {
				return $('<input name="in_reply_to_post_id" type="hidden">')
				.attr('value', post.id);
			}

			function generateSubmitButton() {
				return $('<input type="submit" value="Reply">');
			}

			function generateImageAttacher() {
				return $('
')
				.append($('

'))
				.append($('<input name="image" type="file">'));
			}
		}
	}

	function parseText(text) {
		text = escapeHtml(text);
		text = parseURL(text);
		text = parseReply(text);
		text = parseNewLine(text);
		return text;

		function parseURL(text) {
			return text.replace(/https?:\/\/[-_.!~*a-zA-Z0-9;\/?:\@&=+\$,%#]+/g, function(url) {
				return '' + url + '';
			});
		}

		function parseReply(text) {
			return text.replace(/@([a-zA-Z0-9_]+)/g, function(_, screenName) {
				return '@' + screenName + '';
			});
		}

		function parseNewLine(text) {
			return text.replace(/(\r\n|\r|\n)/g, '
');
		}
	}

	function escapeHtml(text) {
		return $('
').text(text).html();
	}
}

TIMELINE.setEventPost = function($post) {
	$post.find('.replyForm').submit(function(event) {
		event.preventDefault();
		var $form = $(this);
		var $submitButton = $form.find('[type=submit]');
		$submitButton.attr('disabled', true);
		$.ajax({
			url: 'https://api.misskey.xyz/post/create',
			type: 'post',
			data: new FormData($form[0]),
			processData: false,
			contentType: false,
			dataType: 'json',
			xhrFields: {
				withCredentials: true
			}
		}).done(function() {
			$submitButton.attr('disabled', false);
			$form.text('送信しました');
		}).fail(function() {
			$submitButton.attr('disabled', false);
		});
	});

	/*
	$post.find('.fr').submit(function (event) {
	event.preventDefault();
	var $form = $(this);
	var $submitButton = $form.find('[type=image]');
	$submitButton.attr('disabled', true);
	$.ajax({
	url: $form.attr('action'),
	type: $form.attr('method'),
	data: new FormData($form[0]),
	processData: false,
	contentType: false,
	}).done(function () {
	$submitButton.attr('disabled', false);
	if ($post.attr('data-is_favorite')) {
	$post.attr('data-is_favorite', false);
	$submitButton.attr('src', 'https://misskey.xyz/img/misskey/unfavorite.svg');
	$form.attr('action', 'https://misskey.xyz/api/post/favorite/create');
	} else {
	$post.attr('data-is_favorite', true);
	$submitButton.attr('src', 'https://misskey.xyz/img/misskey/favorite.svg');
	$form.attr('action', 'https://misskey.xyz/api/post/favorite/delete');
	}
	}).fail(function () {
	$submitButton.attr('disabled', false);
	});
	});
	*/

	$post.find('.imageAttacher input[name=image]').change(function() {
		var $input = $(this);
		var file = $(this).prop('files')[0];
		if (!file.type.match('image.*')) return;
		var reader = new FileReader();
		reader.onload = function() {
			var $img = $('').attr('src', reader.result);
			$input.parent('.imageAttacher').find('p, img').remove();
			$input.parent('.imageAttacher').append($img);
		};
		reader.readAsDataURL(file);
	});

	$post.click(function(event) {
		if (document.getSelection().toString() == '') {
			if ($(event.target).is('input') || $(event.target).is('textarea')) return;
			if ($(this).find('footer').css('display') === 'none') {
				$('.timeline > .posts > .post footer').each(function() {
					$(this).hide(200);
				});
				$(this).find('footer').show(200);
				var text = $(this).find('footer .replyForm textarea').val();
				$(this).find('footer .replyForm textarea').val('');
				$(this).find('footer .replyForm textarea').focus().val(text);
			} else {
				$(this).find('footer').hide(200);
			}
		}
	});

	/*
	$post.dblclick(function (event) {
	if ($(event.target).is('input') || $(event.target).is('textarea')) return;
	window.open('https://misskey.xyz/' + $(this).attr('data-user-screen_name') + '/post/' + $(this).attr('data-id'));
	});
	*/

}

$(function() {
	$('.timeline .post').each(function() {
		TIMELINE.setEventPost($(this));
	});
});<commit_msg>Add space<commit_after>var TIMELINE = {};

TIMELINE.generatePostElement = function(post) {
	return $('
')
	.attr({
		title: post.createdAt + '
via ' + post.app.name,
		'data-id': post.id,
		'data-userId': post.userId,
		'data-userComment': post.user.comment,
		'data-userColor': post.user.color,
		style: post.isReply ? 'border-color: ' + post.reply.user.color + ';' : ''
	})
	.append(post.isReply ? generateReplyTo() : null)
	.append(generateArticle(post))
	.append(generateFooter());

	function generateReplyTo() {
		return $('
')
		.append(generateArticle(post.reply));
	}

	function generateArticle(post) {
		return $('
')
		.append(generateIcon())
		.append(generateHeader())
		.append($('
').html((post.isReply ? ' ' : '') + parseText(post.text)))
		.append(post.isImageAttached ? generateImage() : null);

		function generateIcon() {
			return $('')
			.attr('href', conf.url + '/' + post.user.screenName)
			.append(
			$('')
			.attr('src', conf.url + '/img/icon/' + post.user.screenName)
			);
		}

		function generateHeader() {
			return generateHeader()
			.append(generateScreenName())
			.append(generateTime());

			function generateHeader() {
				return $('').prepend($('
').prepend(generateName()));
			}

			function generateName() {
				return $('')
				.attr('href', conf.url + '/' + post.user.screenName)
				.text(escapeHtml(post.user.name));
			}

			function generateScreenName() {
				return $('')
				.text('@' + post.user.screenName);
			}

			function generateTime() {
				return $('<time>')
				.attr('datetime', post.createdAt)
				.text(post.createdAt);
			}
		}

		function generateImage() {
			return $('')
			.attr('src', conf.url + '/img/post/' + post.id);
		}
	}

	function generateFooter() {
		return $('')
		.append(generateForm());

		function generateForm() {
			return $('<form class="replyForm">')
			.append(generateTextArea())
			.append(generateInReplyToPostId())
			.append(generateSubmitButton())
			.append(generateImageAttacher());

			function generateTextArea() {
				return $('<textarea name="text">')
				.text('@' + post.user.screenName + ' ');
			}

			function generateInReplyToPostId() {
				return $('<input name="in_reply_to_post_id" type="hidden">')
				.attr('value', post.id);
			}

			function generateSubmitButton() {
				return $('<input type="submit" value="Reply">');
			}

			function generateImageAttacher() {
				return $('
')
				.append($('

'))
				.append($('<input name="image" type="file">'));
			}
		}
	}

	function parseText(text) {
		text = escapeHtml(text);
		text = parseURL(text);
		text = parseReply(text);
		text = parseNewLine(text);
		return text;

		function parseURL(text) {
			return text.replace(/https?:\/\/[-_.!~*a-zA-Z0-9;\/?:\@&=+\$,%#]+/g, function(url) {
				return '' + url + '';
			});
		}

		function parseReply(text) {
			return text.replace(/@([a-zA-Z0-9_]+)/g, function(_, screenName) {
				return '@' + screenName + '';
			});
		}

		function parseNewLine(text) {
			return text.replace(/(\r\n|\r|\n)/g, '
');
		}
	}

	function escapeHtml(text) {
		return $('
').text(text).html();
	}
}

TIMELINE.setEventPost = function($post) {
	$post.find('.replyForm').submit(function(event) {
		event.preventDefault();
		var $form = $(this);
		var $submitButton = $form.find('[type=submit]');
		$submitButton.attr('disabled', true);
		$.ajax({
			url: 'https://api.misskey.xyz/post/create',
			type: 'post',
			data: new FormData($form[0]),
			processData: false,
			contentType: false,
			dataType: 'json',
			xhrFields: {
				withCredentials: true
			}
		}).done(function() {
			$submitButton.attr('disabled', false);
			$form.text('送信しました');
		}).fail(function() {
			$submitButton.attr('disabled', false);
		});
	});

	/*
	$post.find('.fr').submit(function (event) {
	event.preventDefault();
	var $form = $(this);
	var $submitButton = $form.find('[type=image]');
	$submitButton.attr('disabled', true);
	$.ajax({
	url: $form.attr('action'),
	type: $form.attr('method'),
	data: new FormData($form[0]),
	processData: false,
	contentType: false,
	}).done(function () {
	$submitButton.attr('disabled', false);
	if ($post.attr('data-is_favorite')) {
	$post.attr('data-is_favorite', false);
	$submitButton.attr('src', 'https://misskey.xyz/img/misskey/unfavorite.svg');
	$form.attr('action', 'https://misskey.xyz/api/post/favorite/create');
	} else {
	$post.attr('data-is_favorite', true);
	$submitButton.attr('src', 'https://misskey.xyz/img/misskey/favorite.svg');
	$form.attr('action', 'https://misskey.xyz/api/post/favorite/delete');
	}
	}).fail(function () {
	$submitButton.attr('disabled', false);
	});
	});
	*/

	$post.find('.imageAttacher input[name=image]').change(function() {
		var $input = $(this);
		var file = $(this).prop('files')[0];
		if (!file.type.match('image.*')) return;
		var reader = new FileReader();
		reader.onload = function() {
			var $img = $('').attr('src', reader.result);
			$input.parent('.imageAttacher').find('p, img').remove();
			$input.parent('.imageAttacher').append($img);
		};
		reader.readAsDataURL(file);
	});

	$post.click(function(event) {
		if (document.getSelection().toString() == '') {
			if ($(event.target).is('input') || $(event.target).is('textarea')) return;
			if ($(this).find('footer').css('display') === 'none') {
				$('.timeline > .posts > .post footer').each(function() {
					$(this).hide(200);
				});
				$(this).find('footer').show(200);
				var text = $(this).find('footer .replyForm textarea').val();
				$(this).find('footer .replyForm textarea').val('');
				$(this).find('footer .replyForm textarea').focus().val(text);
			} else {
				$(this).find('footer').hide(200);
			}
		}
	});

	/*
	$post.dblclick(function (event) {
	if ($(event.target).is('input') || $(event.target).is('textarea')) return;
	window.open('https://misskey.xyz/' + $(this).attr('data-user-screen_name') + '/post/' + $(this).attr('data-id'));
	});
	*/

}

$(function() {
	$('.timeline .post').each(function() {
		TIMELINE.setEventPost($(this));
	});
});<|endoftext|>


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

import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';

import currency from '../../localization/currency';

import { ROW_BLUE, dataTableStyles, SUSSOL_ORANGE } from '../../globalStyles';

import Row from './Row';
import Cell from './Cell';

import CheckableCell from './CheckableCell';
import TouchableCell from './TouchableCell';
import DropDownCell from '../DropDownCell';
import TextInputCell from './TextInputCell';
import { ExpiryDateInput } from '../ExpiryDateInput';
import {
  CheckedIcon,
  UncheckedIcon,
  DisabledCheckedIcon,
  DisabledUncheckedIcon,
  ChevronRightIcon,
  HistoryIcon,
  PencilIcon,
  HazardIcon,
  BookIcon,
} from '../icons';

import { COLUMN_TYPES, COLUMN_KEYS } from '../../pages/dataTableUtilities';
import { generalStrings, tableStrings } from '../../localization';
import { formatStatus, formatDate } from '../../utilities';
import { formatType } from '../../utilities/formatStatus';
import { useDebounce } from '../../hooks';

/**
 * Wrapper component for a mSupply DataTable page row.
 * Wraps Row component with a contained RenderCells, which
 * will generate the appropriate cell for a given columnKey.
 * Doesn't need to be used, but is a convenience component.
 *
 * @param {Object} rowData     Data object for a row i.e. ItemBatch object
 * @param {Object} rowState    State object for a row, see: Row.js
 * @param {Object} style       Style object the be passed to inner Row
 * @param {String} rowKey      Unique key for a row
 * @param {Array}  columns     Array of column objects, see: columns.js
 * @param {Bool}   isFinalised Boolean indicating if the DataTable page is finalised.
 * @param {Func}   rowIndex    index of this row.
 * @param {Func}   onPress     On press callback for the row itself.
 * @param {Func}   getCallback Function to return a cells callback
 *                             (colKey, propName) => callback
 */
const DataTableRow = React.memo(
  ({
    rowData,
    rowState,
    rowKey,
    columns,
    isFinalised,
    getCallback,
    onPress,
    rowIndex,
    getCellError,
    isValidated,
  }) => {
    const {
      cellText,
      cellContainer,
      warningCellContainer,
      errorCellContainer,
      touchableCellContainer,
      editableCellText,
      editableCellTextView,
      editableCellUnfocused,
      selectedRow: selectedRowStyle,
      alternateRow: alternateRowStyle,
      row: basicRowStyle,
      iconCell,
    } = dataTableStyles;

    const { isSelected = false } = rowState || {};

    const [hasFocus, setFocused] = React.useState(false);
    const focus = useDebounce(shouldFocus => setFocused(shouldFocus), 50, false);

    const rowStyle = useMemo(
      () => ({
        ...(rowIndex % 2 === 0 ? alternateRowStyle : basicRowStyle),
        ...(isSelected && selectedRowStyle),
        ...(!isValidated && { borderColor: SUSSOL_ORANGE, borderWidth: 1, borderRadius: 5 }),
        ...(hasFocus && { backgroundColor: ROW_BLUE }),
      }),
      [rowIndex, isSelected, isValidated, hasFocus]
    );

    // Callback for rendering a row of cells.
    const renderCells = useCallback(
      () =>
        // Map each column to an appropriate cell for a given row.
        columns.map(({ key: columnKey, type, width, alignText, icon }, index) => {
          // Indicator if the right hand border should be removed from styles for this cell.
          const isLastCell = index === columns.length - 1;

          // This cell is disabled if:
          // - the page is finalised.
          // - the row has been explicitly set as disabled.
          const rowIsDisabled = rowState?.isDisabled ?? false;
          const isDisabled = isFinalised || rowIsDisabled || rowData?.isFinalised;

          // Alignment of this particular column. Default to left hand ide.
          const cellAlignment = alignText || 'left';

          switch (type) {
            case COLUMN_TYPES.EDITABLE_STRING:
            case COLUMN_TYPES.EDITABLE_NUMERIC: {
              const { isVaccine, hasBeenCounted } = rowData;

              // Placeholder values which are determined on a per row basis. For example, display
              // "Not counted" as a placeholder on a stocktake row. Placeholders are used under
              // the condition [placeholder && !value] which can be a source of bugs with JS
              // falsey values. I.e. if a value is 0, the placeholder will display rather than
              // the value.
              const extraPlaceholders = {
                [COLUMN_KEYS.COUNTED_TOTAL_QUANTITY]: hasBeenCounted
                  ? ''
                  : tableStrings.not_counted,
              };

              // Extra disabled conditions for columns which can be determined per row. For example,
              // only allow the doses column for vaccine items.
              const disabledCondition = {
                [COLUMN_KEYS.DOSES]: !isVaccine,
              };

              // Extra text which can be displayed for a column on a per row basis when the
              // cell is disabled. For example display N/A on a row for the doses column rather
              // than the underlying value "0".
              const disabledText = {
                [COLUMN_KEYS.DOSES]: isVaccine ? '' : generalStrings.not_available,
              };

              const value = disabledText[columnKey] ? disabledText[columnKey] : rowData[columnKey];
              const placeholder = extraPlaceholders[columnKey] ?? '';

              const inputIsDisabled = isDisabled || !!disabledCondition[columnKey];

              const cellErrors = getCellError?.(rowData, columnKey);
              const alternateCellStyleLookup = {
                warning: warningCellContainer,
                error: errorCellContainer,
              };
              const cellContainerStyle = alternateCellStyleLookup[cellErrors] ?? cellContainer;

              return (
                <TextInputCell
                  onFocus={() => focus(true)}
                  onBlur={() => focus(false)}
                  key={columnKey}
                  value={value}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  onChangeText={getCallback(columnKey)}
                  isDisabled={inputIsDisabled}
                  width={width}
                  viewStyle={cellContainerStyle[cellAlignment]}
                  textViewStyle={editableCellTextView}
                  isLastCell={isLastCell}
                  keyboardType={type === COLUMN_TYPES.EDITABLE_NUMERIC ? 'numeric' : 'default'}
                  textInputStyle={cellText[cellAlignment]}
                  textStyle={editableCellUnfocused[cellAlignment]}
                  cellTextStyle={editableCellText}
                  rowIndex={rowIndex}
                  placeholder={placeholder}
                />
              );
            }
            case COLUMN_TYPES.EDITABLE_EXPIRY_DATE:
              return (
                <ExpiryDateInput
                  onFocus={() => focus(true)}
                  onBlur={() => focus(false)}
                  key={columnKey}
                  value={rowData[columnKey]}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  onEndEditing={getCallback(columnKey)}
                  isDisabled={isDisabled}
                  width={width}
                  isLastCell={isLastCell}
                  rowIndex={rowIndex}
                />
              );

            case COLUMN_TYPES.CHECKABLE:
              return (
                <CheckableCell
                  key={columnKey}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  isChecked={rowState && rowState.isSelected}
                  isDisabled={isDisabled}
                  CheckedComponent={CheckedIcon}
                  UncheckedComponent={UncheckedIcon}
                  DisabledCheckedComponent={DisabledCheckedIcon}
                  DisabledUncheckedComponent={DisabledUncheckedIcon}
                  onCheck={getCallback(columnKey, 'onCheck')}
                  onUncheck={getCallback(columnKey, 'onUncheck')}
                  containerStyle={touchableCellContainer}
                  width={width}
                  isLastCell={isLastCell}
                />
              );

            case COLUMN_TYPES.CURRENCY: {
              const value = currency(rowData[columnKey]).format(false);

              return (
                <Cell
                  key={columnKey}
                  value={value}
                  width={width}
                  viewStyle={cellContainer[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );
            }

            case COLUMN_TYPES.STRING: {
              const value = rowData[columnKey];
              let displayValue = columnKey === 'status' ? formatStatus(value) : value;
              displayValue = columnKey === 'type' ? formatType(displayValue) : displayValue;

              return (
                <Cell
                  key={columnKey}
                  value={displayValue}
                  width={width}
                  viewStyle={cellContainer[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );
            }

            case COLUMN_TYPES.NUMERIC: {
              // Special condition for stocktake difference cells.
              // Use the placeholder 'Not counted' when a stocktake item or batch
              // has not been counted yet.
              const value =
                columnKey === COLUMN_KEYS.DIFFERENCE && !rowData.hasBeenCounted
                  ? generalStrings.not_available
                  : Math.round(rowData[columnKey]);

              const cellErrors = getCellError?.(rowData, columnKey);
              const alternateCellStyleLookup = {
                warning: warningCellContainer,
                error: errorCellContainer,
              };
              const cellContainerStyle = alternateCellStyleLookup[cellErrors] ?? cellContainer;

              return (
                <Cell
                  key={columnKey}
                  value={value}
                  width={width}
                  viewStyle={cellContainerStyle[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );
            }

            case COLUMN_TYPES.DATE:
              return (
                <Cell
                  key={columnKey}
                  value={formatDate(rowData[columnKey], 'll') ?? generalStrings.not_available}
                  width={width}
                  viewStyle={cellContainer[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );

            case COLUMN_TYPES.ICON: {
              const { hasBreached, isVaccine } = rowData ?? {};

              const icons = {
                chevron_right: ChevronRightIcon,
                history: () => <HistoryIcon color={SUSSOL_ORANGE} />,
                pencil: () => <PencilIcon color={SUSSOL_ORANGE} />,
                breach: () => (hasBreached ? <HazardIcon color={SUSSOL_ORANGE} /> : null),
                book: () => <BookIcon color={SUSSOL_ORANGE} />,
              };

              const disabledConditions = {
                [COLUMN_KEYS.HAS_BREACHED]: !(hasBreached && isVaccine),
              };

              const isEditReadOnlyRecord =
                (columnKey === COLUMN_KEYS.PATIENT_EDIT ||
                  columnKey === COLUMN_KEYS.PRESCRIBER_EDIT) &&
                !rowData.isEditable;
              const iconComponent = isEditReadOnlyRecord ? icons.book : icons[icon];

              const isDisabledIcon =
                (isDisabled || disabledConditions[columnKey]) && columnKey !== COLUMN_KEYS.BATCH;

              return (
                <TouchableCell
                  key={columnKey}
                  renderChildren={iconComponent}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  onPress={getCallback(columnKey)}
                  width={width}
                  isLastCell={isLastCell}
                  isDisabled={isDisabledIcon}
                  containerStyle={iconCell}
                />
              );
            }

            case COLUMN_TYPES.DROP_DOWN: {
              const { isVaccine = false, hasVariance = false } = rowData ?? {};

              const disabledConditions = {
                [COLUMN_KEYS.CURRENT_VVM_STATUS]: !isVaccine,
                [COLUMN_KEYS.REASON_TITLE]: !hasVariance,
              };

              const extraDisabledCondition = disabledConditions[columnKey];

              return (
                <DropDownCell
                  key={columnKey}
                  isDisabled={isDisabled || extraDisabledCondition}
                  onPress={getCallback(columnKey)}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  value={rowData[columnKey]}
                  isLastCell={isLastCell}
                  width={width}
                />
              );
            }

            default: {
              return (
                <Cell
                  key={columnKey}
                  value={rowData[columnKey]}
                  width={width}
                  viewStyle={cellContainer[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );
            }
          }
        }),
      [isFinalised, rowState, rowData, rowIndex, columns]
    );

    return (
      <Row
        onPress={onPress}
        style={rowStyle}
        renderCells={renderCells}
        rowKey={rowKey}
        rowData={rowData}
        rowState={rowState}
        rowIndex={rowIndex}
      />
    );
  }
);

DataTableRow.defaultProps = {
  isFinalised: false,
  getCallback: null,
  onPress: null,
  rowState: null,
  getCellError: () => null,
  isValidated: true,
};

DataTableRow.propTypes = {
  onPress: PropTypes.func,
  rowData: PropTypes.object.isRequired,
  rowState: PropTypes.object,
  rowKey: PropTypes.string.isRequired,
  columns: PropTypes.array.isRequired,
  isFinalised: PropTypes.bool,
  getCallback: PropTypes.func,
  rowIndex: PropTypes.number.isRequired,
  getCellError: PropTypes.func,
  isValidated: PropTypes.bool,
};

export default DataTableRow;
<commit_msg>Add correct condition in datatablerow<commit_after>/* eslint-disable react/forbid-prop-types */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';

import currency from '../../localization/currency';

import { ROW_BLUE, dataTableStyles, SUSSOL_ORANGE } from '../../globalStyles';

import Row from './Row';
import Cell from './Cell';

import CheckableCell from './CheckableCell';
import TouchableCell from './TouchableCell';
import DropDownCell from '../DropDownCell';
import TextInputCell from './TextInputCell';
import { ExpiryDateInput } from '../ExpiryDateInput';
import {
  CheckedIcon,
  UncheckedIcon,
  DisabledCheckedIcon,
  DisabledUncheckedIcon,
  ChevronRightIcon,
  HistoryIcon,
  PencilIcon,
  HazardIcon,
  BookIcon,
} from '../icons';

import { COLUMN_TYPES, COLUMN_KEYS } from '../../pages/dataTableUtilities';
import { generalStrings, tableStrings } from '../../localization';
import { formatStatus, formatDate } from '../../utilities';
import { formatType } from '../../utilities/formatStatus';
import { useDebounce } from '../../hooks';

/**
 * Wrapper component for a mSupply DataTable page row.
 * Wraps Row component with a contained RenderCells, which
 * will generate the appropriate cell for a given columnKey.
 * Doesn't need to be used, but is a convenience component.
 *
 * @param {Object} rowData     Data object for a row i.e. ItemBatch object
 * @param {Object} rowState    State object for a row, see: Row.js
 * @param {Object} style       Style object the be passed to inner Row
 * @param {String} rowKey      Unique key for a row
 * @param {Array}  columns     Array of column objects, see: columns.js
 * @param {Bool}   isFinalised Boolean indicating if the DataTable page is finalised.
 * @param {Func}   rowIndex    index of this row.
 * @param {Func}   onPress     On press callback for the row itself.
 * @param {Func}   getCallback Function to return a cells callback
 *                             (colKey, propName) => callback
 */
const DataTableRow = React.memo(
  ({
    rowData,
    rowState,
    rowKey,
    columns,
    isFinalised,
    getCallback,
    onPress,
    rowIndex,
    getCellError,
    isValidated,
  }) => {
    const {
      cellText,
      cellContainer,
      warningCellContainer,
      errorCellContainer,
      touchableCellContainer,
      editableCellText,
      editableCellTextView,
      editableCellUnfocused,
      selectedRow: selectedRowStyle,
      alternateRow: alternateRowStyle,
      row: basicRowStyle,
      iconCell,
    } = dataTableStyles;

    const { isSelected = false } = rowState || {};

    const [hasFocus, setFocused] = React.useState(false);
    const focus = useDebounce(shouldFocus => setFocused(shouldFocus), 50, false);

    const rowStyle = useMemo(
      () => ({
        ...(rowIndex % 2 === 0 ? alternateRowStyle : basicRowStyle),
        ...(isSelected && selectedRowStyle),
        ...(!isValidated && { borderColor: SUSSOL_ORANGE, borderWidth: 1, borderRadius: 5 }),
        ...(hasFocus && { backgroundColor: ROW_BLUE }),
      }),
      [rowIndex, isSelected, isValidated, hasFocus]
    );

    // Callback for rendering a row of cells.
    const renderCells = useCallback(
      () =>
        // Map each column to an appropriate cell for a given row.
        columns.map(({ key: columnKey, type, width, alignText, icon }, index) => {
          // Indicator if the right hand border should be removed from styles for this cell.
          const isLastCell = index === columns.length - 1;

          const { isLinkedToTransaction, isFinalised: rowIsFinalised } = rowData;
          // This cell is disabled if:
          // - the page is finalised.
          // - the row has been explicitly set as disabled.
          // - The row itself is finalised.
          // - The row is linked to another transaction: i.e. an SI created on the primary.
          const rowIsDisabled = rowState?.isDisabled ?? false;
          const isDisabled =
            isFinalised || rowIsDisabled || rowIsFinalised || isLinkedToTransaction;

          // Alignment of this particular column. Default to left hand ide.
          const cellAlignment = alignText || 'left';

          switch (type) {
            case COLUMN_TYPES.EDITABLE_STRING:
            case COLUMN_TYPES.EDITABLE_NUMERIC: {
              const { isVaccine, hasBeenCounted } = rowData;

              // Placeholder values which are determined on a per row basis. For example, display
              // "Not counted" as a placeholder on a stocktake row. Placeholders are used under
              // the condition [placeholder && !value] which can be a source of bugs with JS
              // falsey values. I.e. if a value is 0, the placeholder will display rather than
              // the value.
              const extraPlaceholders = {
                [COLUMN_KEYS.COUNTED_TOTAL_QUANTITY]: hasBeenCounted
                  ? ''
                  : tableStrings.not_counted,
              };

              // Extra disabled conditions for columns which can be determined per row. For example,
              // only allow the doses column for vaccine items.
              const disabledCondition = {
                [COLUMN_KEYS.DOSES]: !isVaccine,
              };

              // Extra text which can be displayed for a column on a per row basis when the
              // cell is disabled. For example display N/A on a row for the doses column rather
              // than the underlying value "0".
              const disabledText = {
                [COLUMN_KEYS.DOSES]: isVaccine ? '' : generalStrings.not_available,
              };

              const value = disabledText[columnKey] ? disabledText[columnKey] : rowData[columnKey];
              const placeholder = extraPlaceholders[columnKey] ?? '';

              const inputIsDisabled = isDisabled || !!disabledCondition[columnKey];

              const cellErrors = getCellError?.(rowData, columnKey);
              const alternateCellStyleLookup = {
                warning: warningCellContainer,
                error: errorCellContainer,
              };
              const cellContainerStyle = alternateCellStyleLookup[cellErrors] ?? cellContainer;

              return (
                <TextInputCell
                  onFocus={() => focus(true)}
                  onBlur={() => focus(false)}
                  key={columnKey}
                  value={value}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  onChangeText={getCallback(columnKey)}
                  isDisabled={inputIsDisabled}
                  width={width}
                  viewStyle={cellContainerStyle[cellAlignment]}
                  textViewStyle={editableCellTextView}
                  isLastCell={isLastCell}
                  keyboardType={type === COLUMN_TYPES.EDITABLE_NUMERIC ? 'numeric' : 'default'}
                  textInputStyle={cellText[cellAlignment]}
                  textStyle={editableCellUnfocused[cellAlignment]}
                  cellTextStyle={editableCellText}
                  rowIndex={rowIndex}
                  placeholder={placeholder}
                />
              );
            }
            case COLUMN_TYPES.EDITABLE_EXPIRY_DATE:
              return (
                <ExpiryDateInput
                  onFocus={() => focus(true)}
                  onBlur={() => focus(false)}
                  key={columnKey}
                  value={rowData[columnKey]}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  onEndEditing={getCallback(columnKey)}
                  isDisabled={isDisabled}
                  width={width}
                  isLastCell={isLastCell}
                  rowIndex={rowIndex}
                />
              );

            case COLUMN_TYPES.CHECKABLE:
              return (
                <CheckableCell
                  key={columnKey}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  isChecked={rowState && rowState.isSelected}
                  isDisabled={isDisabled}
                  CheckedComponent={CheckedIcon}
                  UncheckedComponent={UncheckedIcon}
                  DisabledCheckedComponent={DisabledCheckedIcon}
                  DisabledUncheckedComponent={DisabledUncheckedIcon}
                  onCheck={getCallback(columnKey, 'onCheck')}
                  onUncheck={getCallback(columnKey, 'onUncheck')}
                  containerStyle={touchableCellContainer}
                  width={width}
                  isLastCell={isLastCell}
                />
              );

            case COLUMN_TYPES.CURRENCY: {
              const value = currency(rowData[columnKey]).format(false);

              return (
                <Cell
                  key={columnKey}
                  value={value}
                  width={width}
                  viewStyle={cellContainer[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );
            }

            case COLUMN_TYPES.STRING: {
              const value = rowData[columnKey];
              let displayValue = columnKey === 'status' ? formatStatus(value) : value;
              displayValue = columnKey === 'type' ? formatType(displayValue) : displayValue;

              return (
                <Cell
                  key={columnKey}
                  value={displayValue}
                  width={width}
                  viewStyle={cellContainer[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );
            }

            case COLUMN_TYPES.NUMERIC: {
              // Special condition for stocktake difference cells.
              // Use the placeholder 'Not counted' when a stocktake item or batch
              // has not been counted yet.
              const value =
                columnKey === COLUMN_KEYS.DIFFERENCE && !rowData.hasBeenCounted
                  ? generalStrings.not_available
                  : Math.round(rowData[columnKey]);

              const cellErrors = getCellError?.(rowData, columnKey);
              const alternateCellStyleLookup = {
                warning: warningCellContainer,
                error: errorCellContainer,
              };
              const cellContainerStyle = alternateCellStyleLookup[cellErrors] ?? cellContainer;

              return (
                <Cell
                  key={columnKey}
                  value={value}
                  width={width}
                  viewStyle={cellContainerStyle[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );
            }

            case COLUMN_TYPES.DATE:
              return (
                <Cell
                  key={columnKey}
                  value={formatDate(rowData[columnKey], 'll') ?? generalStrings.not_available}
                  width={width}
                  viewStyle={cellContainer[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );

            case COLUMN_TYPES.ICON: {
              const { hasBreached, isVaccine } = rowData ?? {};

              const icons = {
                chevron_right: ChevronRightIcon,
                history: () => <HistoryIcon color={SUSSOL_ORANGE} />,
                pencil: () => <PencilIcon color={SUSSOL_ORANGE} />,
                breach: () => (hasBreached ? <HazardIcon color={SUSSOL_ORANGE} /> : null),
                book: () => <BookIcon color={SUSSOL_ORANGE} />,
              };

              const disabledConditions = {
                [COLUMN_KEYS.HAS_BREACHED]: !(hasBreached && isVaccine),
              };

              const isEditReadOnlyRecord =
                (columnKey === COLUMN_KEYS.PATIENT_EDIT ||
                  columnKey === COLUMN_KEYS.PRESCRIBER_EDIT) &&
                !rowData.isEditable;
              const iconComponent = isEditReadOnlyRecord ? icons.book : icons[icon];

              const isDisabledIcon =
                (isDisabled || disabledConditions[columnKey]) && columnKey !== COLUMN_KEYS.BATCH;

              return (
                <TouchableCell
                  key={columnKey}
                  renderChildren={iconComponent}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  onPress={getCallback(columnKey)}
                  width={width}
                  isLastCell={isLastCell}
                  isDisabled={isDisabledIcon}
                  containerStyle={iconCell}
                />
              );
            }

            case COLUMN_TYPES.DROP_DOWN: {
              const { isVaccine = false, hasVariance = false } = rowData ?? {};

              const disabledConditions = {
                [COLUMN_KEYS.CURRENT_VVM_STATUS]: !isVaccine,
                [COLUMN_KEYS.REASON_TITLE]: !hasVariance,
              };

              const extraDisabledCondition = disabledConditions[columnKey];

              return (
                <DropDownCell
                  key={columnKey}
                  isDisabled={isDisabled || extraDisabledCondition}
                  onPress={getCallback(columnKey)}
                  rowKey={rowKey}
                  columnKey={columnKey}
                  value={rowData[columnKey]}
                  isLastCell={isLastCell}
                  width={width}
                />
              );
            }

            default: {
              return (
                <Cell
                  key={columnKey}
                  value={rowData[columnKey]}
                  width={width}
                  viewStyle={cellContainer[cellAlignment]}
                  textStyle={cellText[cellAlignment]}
                  isLastCell={isLastCell}
                />
              );
            }
          }
        }),
      [isFinalised, rowState, rowData, rowIndex, columns]
    );

    return (
      <Row
        onPress={onPress}
        style={rowStyle}
        renderCells={renderCells}
        rowKey={rowKey}
        rowData={rowData}
        rowState={rowState}
        rowIndex={rowIndex}
      />
    );
  }
);

DataTableRow.defaultProps = {
  isFinalised: false,
  getCallback: null,
  onPress: null,
  rowState: null,
  getCellError: () => null,
  isValidated: true,
};

DataTableRow.propTypes = {
  onPress: PropTypes.func,
  rowData: PropTypes.object.isRequired,
  rowState: PropTypes.object,
  rowKey: PropTypes.string.isRequired,
  columns: PropTypes.array.isRequired,
  isFinalised: PropTypes.bool,
  getCallback: PropTypes.func,
  rowIndex: PropTypes.number.isRequired,
  getCellError: PropTypes.func,
  isValidated: PropTypes.bool,
};

export default DataTableRow;
<|endoftext|>