<commit_before>$.extend(true, doaj, {

    publisherUpdatesSearch : {
        activeEdges : {},

        publisherStatusMap: function(value) {
            if (doaj.valueMaps.applicationStatus.hasOwnProperty(value)) {
                return doaj.valueMaps.applicationStatus[value];
            }
            return value;
        },

        editUpdateRequest : function (value, resultobj, renderer) {
            if (resultobj['suggestion']) {
                if (resultobj.admin && resultobj.admin.application_status) {
                    var status = resultobj.admin.application_status;
                    var result = "";
                    var view = '(view request)';
                    if (status === "update_request" || status == "revisions_required") {
                        var actionUrl = doaj.publisherUpdatesSearchConfig.journalUpdateUrl + resultobj.admin.current_journal;
                        result = 'Edit this update request | Delete this update request';
                    } else  if (status !== "rejected" && status !== "accepted") {
                        result = 'This update request is currently being reviewed by an Editor ' + view + '.';
                    } else if (status === "rejected") {
                        result = 'This update request has been rejected ' + view + '.';
                    } else if (status === "accepted") {
                        result = 'This update request has been accepted, and your journal in DOAJ updated ' + view + '.';
                    }
                    return result;
                }
            }
            return false;
        },

        init : function(params) {
            if (!params) { params = {} }

            var current_domain = document.location.host;
            var current_scheme = window.location.protocol;

            var selector = params.selector || "#publisher_update_requests";
            var search_url = current_scheme + "//" + current_domain + doaj.publisherUpdatesSearchConfig.searchPath;

            var countFormat = edges.numFormat({
                thousandsSeparator: ","
            });

            var components = [
                // facets
                edges.newRefiningANDTermSelector({
                    id: "application_status",
                    category: "facet",
                    field: "admin.application_status.exact",
                    display: "Application Status",
                    deactivateThreshold: 1,
                    valueFunction : doaj.publisherUpdatesSearch.publisherStatusMap,
                    renderer: edges.bs3.newRefiningANDTermSelectorRenderer({
                        controls: false,
                        open: true,
                        togglable: false,
                        countFormat: countFormat,
                        hideInactive: true
                    })
                }),

                // configure the search controller
                edges.newFullSearchController({
                    id: "search-controller",
                    category: "controller",
                    sortOptions: [
                        {'display': 'Date created', 'field': 'created_date'},
                        {'display': 'Last updated', 'field': 'last_manual_update'},
                        {'display': 'Title', 'field': 'index.unpunctitle.exact'}
                    ],
                    fieldOptions: [
                        {'display': 'Title', 'field': 'index.title'},
                        {'display': 'Alternative Title', 'field': 'bibjson.alternative_title'},
                        {'display': 'Subject', 'field': 'index.subject'},
                        {'display': 'Classification', 'field': 'index.classification'},
                        {'display': 'ISSN', 'field': 'index.issn.exact'},
                        {'display': 'Country of publisher', 'field': 'index.country'},
                        {'display': 'Journal Language', 'field': 'index.language'},
                        {'display': 'Publisher', 'field': 'index.publisher'},
                        {'display':'Platform, Host, Aggregator','field':'bibjson.provider'}
                    ],
                    defaultOperator: "AND",
                    renderer: doaj.renderers.newFullSearchControllerRenderer({
                        freetextSubmitDelay: 1000,
                        searchButton: true,
                        searchPlaceholder: "Search Update Requests in progress"
                    })
                }),

                // the pager, with the explicitly set page size options (see the openingQuery for the initial size)
                edges.newPager({
                    id: "top-pager",
                    category: "top-pager",
                    renderer: edges.bs3.newPagerRenderer({
                        sizeOptions: [10, 25, 50, 100],
                        numberFormat: countFormat,
                        scrollSelector: "html, body"
                    })
                }),
                edges.newPager({
                    id: "bottom-pager",
                    category: "bottom-pager",
                    renderer: edges.bs3.newPagerRenderer({
                        sizeOptions: [10, 25, 50, 100],
                        numberFormat: countFormat,
                        scrollSelector: "html, body"
                    })
                }),

                // results display
                edges.newResultsDisplay({
                    id: "results",
                    category: "results",
                    renderer: edges.bs3.newResultsFieldsByRowRenderer({
                        noResultsText: "
You do not have any active update requests that meet your search criteria
" +
                                        "
If you have not set any search criteria, you do not have any update requests at this time.
",
                        rowDisplay : [
                            [
                                {
                                    valueFunction: doaj.fieldRender.titleField
                                }
                            ],
                            [
                                {
                                    "pre": 'Alternative title: ',
                                    "field": "bibjson.alternative_title",
                                    "post": ""
                                }
                            ],
                            [
                                {
                                    "pre": "Date applied: ",
                                    valueFunction: doaj.fieldRender.suggestedOn
                                }
                            ],
                            [
                                {
                                    "pre": "Last updated: ",
                                    valueFunction: doaj.fieldRender.lastManualUpdate
                                }
                            ],
                            [
                                {
                                    "pre" : "ISSN(s): ",
                                    valueFunction: doaj.fieldRender.issns
                                }
                            ],
                            [
                                {
                                    "pre" : "Application status: ",
                                    valueFunction: doaj.fieldRender.applicationStatus
                                }
                            ],
                            [
                                {
                                    "pre" : "Description: ",
                                    "field" : "suggestion.description"
                                }
                            ],
                            [
                                {
                                    "pre" : "Contact: ",
                                    "field" : "admin.contact.name"
                                },
                                {
                                    "field" : "admin.contact.email"
                                }
                            ],
                            [
                                {
                                    "pre": "Application by: ",
                                    "field": "suggestion.suggester.name",
                                    "post" : " "
                                },
                                {
                                    "pre" : "Applicant email: ",
                                    "field": "suggestion.suggester.email"
                                }
                            ],
                            [
                                {
                                    "pre": "Classification: ",
                                    "field": "index.classification"
                                }
                            ],
                            [
                                {
                                    "pre": "Keywords: ",
                                    "field": "bibjson.keywords"
                                }
                            ],
                            [
                                {
                                    "pre": "Publisher: ",
                                    "field": "bibjson.publisher"
                                }
                            ],
                            [
                                {
                                    "pre": "Platform, Host, Aggregator: ",
                                    "field": "bibjson.provider"
                                }
                            ],
                            [
                                {
                                    "pre": "Publication charges?: ",
                                    valueFunction: doaj.fieldRender.authorPays
                                }
                            ],
                            [
                                {
                                    "pre": "Started publishing Open Access content in: ",
                                    "field": "bibjson.oa_start.year"
                                }
                            ],
                            [
                                {
                                    "pre": "Stopped publishing Open Access content in: ",
                                    "field": "bibjson.oa_end.year"
                                }
                            ],
                            [
                                {
                                    "pre": "Country of publisher: ",
                                    valueFunction: doaj.fieldRender.countryName
                                }
                            ],
                            [
                                {
                                    "pre": "Journal Language: ",
                                    "field": "bibjson.language"
                                }
                            ],
                            [
                                {
                                    "pre": "Journal License: ",
                                    valueFunction: doaj.fieldRender.journalLicense
                                }
                            ],
                            [
                                {
                                    valueFunction: doaj.fieldRender.links
                                }
                            ],
                            [
                                {
                                    valueFunction: doaj.publisherUpdatesSearch.editUpdateRequest
                                }
                            ]
                        ]
                    })
                }),

                // selected filters display, with all the fields given their display names
                edges.newSelectedFilters({
                    id: "selected-filters",
                    category: "selected-filters",
                    fieldDisplays: {
                        'admin.application_status.exact': 'Application Status'
                    }
                }),

                // the standard searching notification
                edges.newSearchingNotification({
                    id: "searching-notification",
                    category: "searching-notification"
                })
            ];

            var e = edges.newEdge({
                selector: selector,
                template: edges.bs3.newFacetview(),
                search_url: search_url,
                manageUrl: true,
                openingQuery : es.newQuery({
                    sort: {"field" : "last_manual_update", "order" : "asc"}
                }),
                components: components,
                callbacks : {
                    "edges:query-fail" : function() {
                        alert("There was an unexpected error.  Please reload the page and try again.  If the issue persists please contact us.");
                    }
                }
            });
            doaj.publisherUpdatesSearch.activeEdges[selector] = e;
        }
    }
});

jQuery(document).ready(function($) {
    doaj.publisherUpdatesSearch.init();
});
<commit_msg>add on click listener to publisher update request<commit_after>$.extend(true, doaj, {

    publisherUpdatesSearch : {
        activeEdges : {},

        publisherStatusMap: function(value) {
            if (doaj.valueMaps.applicationStatus.hasOwnProperty(value)) {
                return doaj.valueMaps.applicationStatus[value];
            }
            return value;
        },

        editUpdateRequest : function (value, resultobj, renderer) {
            if (resultobj['suggestion']) {
                if (resultobj.admin && resultobj.admin.application_status) {
                    var status = resultobj.admin.application_status;
                    var result = "";
                    var view = '(view request)';
                    if (status === "update_request" || status == "revisions_required") {
                        var actionUrl = doaj.publisherUpdatesSearchConfig.journalUpdateUrl + resultobj.admin.current_journal;
                        result = 'Edit this update request | Delete this update request';
                    } else  if (status !== "rejected" && status !== "accepted") {
                        result = 'This update request is currently being reviewed by an Editor ' + view + '.';
                    } else if (status === "rejected") {
                        result = 'This update request has been rejected ' + view + '.';
                    } else if (status === "accepted") {
                        result = 'This update request has been accepted, and your journal in DOAJ updated ' + view + '.';
                    }
                    return result;
                }
            }
            return false;
        },

        init : function(params) {
            if (!params) {
                params = {}
            }

            var current_domain = document.location.host;
            var current_scheme = window.location.protocol;

            var selector = params.selector || "#publisher_update_requests";
            var search_url = current_scheme + "//" + current_domain + doaj.publisherUpdatesSearchConfig.searchPath;

            var countFormat = edges.numFormat({
                thousandsSeparator: ","
            });

            var components = [
                // facets
                edges.newRefiningANDTermSelector({
                    id: "application_status",
                    category: "facet",
                    field: "admin.application_status.exact",
                    display: "Application Status",
                    deactivateThreshold: 1,
                    valueFunction: doaj.publisherUpdatesSearch.publisherStatusMap,
                    renderer: edges.bs3.newRefiningANDTermSelectorRenderer({
                        controls: false,
                        open: true,
                        togglable: false,
                        countFormat: countFormat,
                        hideInactive: true
                    })
                }),

                // configure the search controller
                edges.newFullSearchController({
                    id: "search-controller",
                    category: "controller",
                    sortOptions: [
                        {'display': 'Date created', 'field': 'created_date'},
                        {'display': 'Last updated', 'field': 'last_manual_update'},
                        {'display': 'Title', 'field': 'index.unpunctitle.exact'}
                    ],
                    fieldOptions: [
                        {'display': 'Title', 'field': 'index.title'},
                        {'display': 'Alternative Title', 'field': 'bibjson.alternative_title'},
                        {'display': 'Subject', 'field': 'index.subject'},
                        {'display': 'Classification', 'field': 'index.classification'},
                        {'display': 'ISSN', 'field': 'index.issn.exact'},
                        {'display': 'Country of publisher', 'field': 'index.country'},
                        {'display': 'Journal Language', 'field': 'index.language'},
                        {'display': 'Publisher', 'field': 'index.publisher'},
                        {'display': 'Platform, Host, Aggregator', 'field': 'bibjson.provider'}
                    ],
                    defaultOperator: "AND",
                    renderer: doaj.renderers.newFullSearchControllerRenderer({
                        freetextSubmitDelay: 1000,
                        searchButton: true,
                        searchPlaceholder: "Search Update Requests in progress"
                    })
                }),

                // the pager, with the explicitly set page size options (see the openingQuery for the initial size)
                edges.newPager({
                    id: "top-pager",
                    category: "top-pager",
                    renderer: edges.bs3.newPagerRenderer({
                        sizeOptions: [10, 25, 50, 100],
                        numberFormat: countFormat,
                        scrollSelector: "html, body"
                    })
                }),
                edges.newPager({
                    id: "bottom-pager",
                    category: "bottom-pager",
                    renderer: edges.bs3.newPagerRenderer({
                        sizeOptions: [10, 25, 50, 100],
                        numberFormat: countFormat,
                        scrollSelector: "html, body"
                    })
                }),

                // results display
                edges.newResultsDisplay({
                    id: "results",
                    category: "results",
                    renderer: edges.bs3.newResultsFieldsByRowRenderer({
                        noResultsText: "
You do not have any active update requests that meet your search criteria
" +
                            "
If you have not set any search criteria, you do not have any update requests at this time.
",
                        rowDisplay: [
                            [
                                {
                                    valueFunction: doaj.fieldRender.titleField
                                }
                            ],
                            [
                                {
                                    "pre": 'Alternative title: ',
                                    "field": "bibjson.alternative_title",
                                    "post": ""
                                }
                            ],
                            [
                                {
                                    "pre": "Date applied: ",
                                    valueFunction: doaj.fieldRender.suggestedOn
                                }
                            ],
                            [
                                {
                                    "pre": "Last updated: ",
                                    valueFunction: doaj.fieldRender.lastManualUpdate
                                }
                            ],
                            [
                                {
                                    "pre": "ISSN(s): ",
                                    valueFunction: doaj.fieldRender.issns
                                }
                            ],
                            [
                                {
                                    "pre": "Application status: ",
                                    valueFunction: doaj.fieldRender.applicationStatus
                                }
                            ],
                            [
                                {
                                    "pre": "Description: ",
                                    "field": "suggestion.description"
                                }
                            ],
                            [
                                {
                                    "pre": "Contact: ",
                                    "field": "admin.contact.name"
                                },
                                {
                                    "field": "admin.contact.email"
                                }
                            ],
                            [
                                {
                                    "pre": "Application by: ",
                                    "field": "suggestion.suggester.name",
                                    "post": " "
                                },
                                {
                                    "pre": "Applicant email: ",
                                    "field": "suggestion.suggester.email"
                                }
                            ],
                            [
                                {
                                    "pre": "Classification: ",
                                    "field": "index.classification"
                                }
                            ],
                            [
                                {
                                    "pre": "Keywords: ",
                                    "field": "bibjson.keywords"
                                }
                            ],
                            [
                                {
                                    "pre": "Publisher: ",
                                    "field": "bibjson.publisher"
                                }
                            ],
                            [
                                {
                                    "pre": "Platform, Host, Aggregator: ",
                                    "field": "bibjson.provider"
                                }
                            ],
                            [
                                {
                                    "pre": "Publication charges?: ",
                                    valueFunction: doaj.fieldRender.authorPays
                                }
                            ],
                            [
                                {
                                    "pre": "Started publishing Open Access content in: ",
                                    "field": "bibjson.oa_start.year"
                                }
                            ],
                            [
                                {
                                    "pre": "Stopped publishing Open Access content in: ",
                                    "field": "bibjson.oa_end.year"
                                }
                            ],
                            [
                                {
                                    "pre": "Country of publisher: ",
                                    valueFunction: doaj.fieldRender.countryName
                                }
                            ],
                            [
                                {
                                    "pre": "Journal Language: ",
                                    "field": "bibjson.language"
                                }
                            ],
                            [
                                {
                                    "pre": "Journal License: ",
                                    valueFunction: doaj.fieldRender.journalLicense
                                }
                            ],
                            [
                                {
                                    valueFunction: doaj.fieldRender.links
                                }
                            ],
                            [
                                {
                                    valueFunction: doaj.publisherUpdatesSearch.editUpdateRequest
                                }
                            ]
                        ]
                    })
                }),

                // selected filters display, with all the fields given their display names
                edges.newSelectedFilters({
                    id: "selected-filters",
                    category: "selected-filters",
                    fieldDisplays: {
                        'admin.application_status.exact': 'Application Status'
                    }
                }),

                // the standard searching notification
                edges.newSearchingNotification({
                    id: "searching-notification",
                    category: "searching-notification"
                })
            ];

            var e = edges.newEdge({
                selector: selector,
                template: edges.bs3.newFacetview(),
                search_url: search_url,
                manageUrl: true,
                openingQuery: es.newQuery({
                    sort: {"field": "last_manual_update", "order": "asc"}
                }),
                components: components,
                callbacks: {
                    "edges:query-fail": function () {
                        alert("There was an unexpected error.  Please reload the page and try again.  If the issue persists please contact us.");
                    }
                }
            });
            doaj.publisherUpdatesSearch.activeEdges[selector] = e;

            $(selector).on("edges:post-render", function () {
                $(".delete_suggestion_link").unbind("click").click(function (event) {
                    console.log("click registered")
                    event.preventDefault();

                    function success_callback(data) {
                        alert("The update request was successfully deleted");
                        doaj.publisherUpdatesSearch.activeEdges[selector].cycle();
                    }

                    function error_callback() {
                        alert("There was an error deleting the update request")
                    }

                    var c = confirm("Are you really really sure?  You can't undo this operation!");
                    if (c) {
                        var href = $(this).attr("href");
                        var obj = {"delete": "true"};
                        $.ajax({
                            type: "DELETE",
                            url: href,
                            data: obj,
                            success: success_callback,
                            error: error_callback
                        })
                    }
                });
            });
        }
    }
});

jQuery(document).ready(function($) {
    doaj.publisherUpdatesSearch.init();
});
<|endoftext|>


<commit_before>import React from 'react';
import { FormattedMessage } from 'react-intl';
import { ButtonToolbar, Button } from 'react-bootstrap';
// Import Style
import styles from './Footer.css';
import { withBaseIcon } from 'react-icons-kit';
import { facebook, twitter, instagram } from 'react-icons-kit/fa';

const IconGreen32 =
        withBaseIcon({ size: 32, style: { color: '#4caf50' } });

// Import Images
// import bg from './LOGO2.png';

const FontAwesome = require('react-fontawesome');
export function Footer() {
  return (

© 2017 · INVESTNAIRA ·

      <FontAwesome name='rocket' />

    {/* <FormattedMessage id="twitterMessage" /> : @mern_io */}
      Let's keep the conversation going
         <Button bsStyle="link" href="https://www.facebook.com/investnaira" target="_blank"><IconGreen32 icon={facebook} /></Button>
         <Button bsStyle="link" href="https://www.instagram.com/investnaira" target="_blank"><IconGreen32 icon={instagram} /></Button>
         <Button bsStyle="link" href="https://www.twitter.com/investnaira" target="_blank"><IconGreen32 icon={twitter} /></Button>

  );
}

export default Footer;
<commit_msg>forgot charger<commit_after>import React from 'react';
// import { FormattedMessage } from 'react-intl';
import { Button } from 'react-bootstrap';
// Import Style
import styles from './Footer.css';
import { withBaseIcon } from 'react-icons-kit';
import { facebook, twitter, instagram } from 'react-icons-kit/fa';

const IconGreen32 =
        withBaseIcon({ size: 32, style: { color: '#4caf50' } });

// Import Images
// import bg from './LOGO2.png';

export function Footer() {
  return (

© 2017 · INVESTNAIRA ·

    {/* <FormattedMessage id="twitterMessage" /> : @mern_io */}
      Let's keep the conversation going
         <Button bsStyle="link" href="https://www.facebook.com/investnaira" target="_blank"><IconGreen32 icon={facebook} /></Button>
         <Button bsStyle="link" href="https://www.instagram.com/investnaira" target="_blank"><IconGreen32 icon={instagram} /></Button>
         <Button bsStyle="link" href="https://www.twitter.com/investnaira" target="_blank"><IconGreen32 icon={twitter} /></Button>

  );
}

export default Footer;
<|endoftext|>


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

import styles from './styles.css';

import EnhancedStopwatch from '../../../utils/EnhancedStopwatch';

import StopWatchDisplay from './StopWatchDisplay';

export default class StopWatchPage extends Component {
    constructor() {
        super();
        this.state = {
            elapsedMs: 0
        }

        this.toggleTimer = this.toggleTimer.bind(this);
        this.resetTimer = this.resetTimer.bind(this);
        this.onTimerTick = this.onTimerTick.bind(this);

        this.timer = new EnhancedStopwatch();
        this.timer.onTime(this.onTimerTick);
    }

    onTimerTick({ ms }) {
        this.setState({
            elapsedMs: ms
        });
    }

    toggleTimer() {
        if (this.timer.isStarted) {
            this.timer.stop();
        } else {
            this.timer.start();
        }
    }

    resetTimer() {
        this.timer.reset();
        this.setState({
            elapsedMs: 0
        });
    }

    render() {
        const { isStarted } = this.timer;
        const { elapsedMs } = this.state;

        const btnLabelClass = `stopwatch-page__btn-label--${
            isStarted ? 'pause' : 'play'
        }`;

        return (

                            <StopWatchDisplay elapsedMs={ elapsedMs } />

                            <button className="stopwatch-page__playback-btn" onClick={ this.toggleTimer }>

                            </button>
                            {/*{ elapsedMs > 0 &&
                                <button onClick={ this.resetTimer }>Reset</button>
                            }*/}

        );
    }
}
<commit_msg>Implement stopwatch session save action in component<commit_after>import React, { Component } from 'react';
import { connect } from 'react-redux';

import styles from './styles.css';

import { saveCurrentSessionTime } from '../../../../redux/actions';
import EnhancedStopwatch from '../../../utils/EnhancedStopwatch';

import StopWatchDisplay from './StopWatchDisplay';

class StopWatchPage extends Component {
    constructor() {
        super();
        this.state = {
            elapsedMs: 0
        }

        this.toggleTimer = this.toggleTimer.bind(this);
        this.resetTimer = this.resetTimer.bind(this);
        this.onTimerTick = this.onTimerTick.bind(this);

        this.timer = new EnhancedStopwatch();
        this.timer.onTime(this.onTimerTick);
    }

    onTimerTick({ ms }) {
        this.setState({
            elapsedMs: ms
        });
    }

    toggleTimer() {
        const { saveTime } = this.props;

        if (this.timer.isStarted) {
            this.timer.stop();
            saveTime(this.state.elapsedMs);
        } else {
            this.timer.start();
        }
    }

    resetTimer() {
        this.timer.reset();
        this.setState({
            elapsedMs: 0
        });
    }

    componentDidMount() {
        this.setState({ elapsedMs: this.props.currentSessionTime });
    }

    render() {
        const { isStarted } = this.timer;
        const { elapsedMs } = this.state;

        const btnLabelClass = `stopwatch-page__btn-label--${
            isStarted ? 'pause' : 'play'
        }`;

        return (

                            <StopWatchDisplay elapsedMs={ elapsedMs } />

                            <button className="stopwatch-page__playback-btn" onClick={ this.toggleTimer }>

                            </button>
                            {/*{ elapsedMs > 0 &&
                                <button onClick={ this.resetTimer }>Reset</button>
                            }*/}

        );
    }
}

export default connect(
    ({ root }) => ({ currentSessionTime: root.currentSessionTime }),
    { saveTime: saveCurrentSessionTime }
)(StopWatchPage);<|endoftext|>


<commit_before>class Ranking extends BlazeComponent {
	template() {
		return 'ranking';
	}

	onRendered() {
		var freshData = Meteor.users.find({}, {
			fields: {
				'profile.firstName': 1,
				'profile.lastName': 1,
				'profile.points': 1
			}
		}).fetch();
		var userData = [];
		for (var i = 0; i < freshData.length; i++) {
			var list = [];
			list.push(freshData[i].fullName());
			list = list.concat(freshData[i].last5Games());
			userData.push(list);
		}
		userData.sort(function(a, b) {
			if (a[0] > b[0]) {
				return 1;
			}
			if (a[0] < b[0]) {
				return -1;
			}
			return 0;
		});
		var chart = c3.generate({
			bindto: '#rankingGraph',
			size: {
				height: 480
			},
			data: {
				type: 'line',
				columns: userData
			},
			axis: {
				y: {
					label: {
						text: 'Points',
						position: 'outer-middle',
					}
				},
				x: {
					label: {
						text: 'Derniers 5 matchs',
						position: 'outer-center',
					}
				}
			},
			grid: {
				y: {
					show: true
				}
			}
		});
		chart.load({
			columns: userData
		});
		$('tbody tr:nth-child(-n+3)').addClass('success');
		$('tbody tr:nth-last-child(-n+3)').addClass('danger');
	}

	playerData() {
		var freshData = Meteor.users.find({}, {
			fields: {
				'profile.firstName': 1,
				'profile.lastName': 1,
				'profile.points': 1,
			}
		}).fetch();
		var newList = [];
		for (var i = 0; i < freshData.length; i++) {
			if (freshData[i].profile.points.length > 9) {
				newList.push(freshData[i]);
			}
		}
		newList.sort(function(a, b) {
			if (a.currentPoints() > b.currentPoints()) {
				return -1;
			}
			if (a.currentPoints() < b.currentPoints()) {
				return 1;
			}
			return 0;
		});
		return newList;
	}

	index() {
		return this.currentData('@index') + 1;
	}
}

Ranking.register('Ranking');
<commit_msg>Update ranking component<commit_after>class Ranking extends BlazeComponent {
	template() {
		return 'ranking';
	}

	onRendered() {
		super.onRendered();
		var freshData = Meteor.users.find({}, {
			fields: {
				'profile.firstName': 1,
				'profile.lastName': 1,
				'profile.points': 1
			}
		}).fetch();
		var userData = [];
		for (var i = 0; i < freshData.length; i++) {
			var list = [];
			list.push(freshData[i].fullName());
			list = list.concat(freshData[i].last5Games());
			userData.push(list);
		}
		userData.sort(function(a, b) {
			if (a[0] > b[0]) {
				return 1;
			}
			if (a[0] < b[0]) {
				return -1;
			}
			return 0;
		});
		var chart = c3.generate({
			bindto: '#rankingGraph',
			size: {
				height: 480
			},
			data: {
				type: 'line',
				columns: userData
			},
			axis: {
				y: {
					label: {
						text: 'Points',
						position: 'outer-middle',
					}
				},
				x: {
					label: {
						text: 'Derniers 5 matchs',
						position: 'outer-center',
					}
				}
			},
			grid: {
				y: {
					show: true
				}
			}
		});
		chart.load({
			columns: userData
		});
		$('tbody tr:nth-child(-n+3)').addClass('success');
		$('tbody tr:nth-last-child(-n+3)').addClass('danger');
	}

	playerData() {
		var freshData = Meteor.users.find({}, {
			fields: {
				'profile.firstName': 1,
				'profile.lastName': 1,
				'profile.points': 1,
			}
		}).fetch();
		var newList = [];
		for (var i = 0; i < freshData.length; i++) {
			if (freshData[i].profile.points.length > 9) {
				newList.push(freshData[i]);
			}
		}
		newList.sort(function(a, b) {
			if (a.currentPoints() > b.currentPoints()) {
				return -1;
			}
			if (a.currentPoints() < b.currentPoints()) {
				return 1;
			}
			return 0;
		});
		return newList;
	}

	index() {
		return this.currentData('@index') + 1;
	}
}

Ranking.register('Ranking');
<|endoftext|>


<commit_before>define([
	'compose',
	'../../base/_Destroyable',
	'../../base/_Evented',
	'../_WithSize'
], function(
	compose,
	_Destroyable,
	_Evented,
	_WithSize
){
	return compose(_Destroyable, _Evented, _WithSize, function() {
		this._root = this._own(this._rootFactory());
		this.domNode = this._root.domNode;
	}, {
		inDom: function(inDom) {
			this._root.inDom && this._root.inDom(inDom);
		},
		bounds: function(bounds) {
			this._root.bounds && this._root.bounds(bounds);
		},
		_addCmp: function(name, cmp, options) {
			if (typeof name !== 'string') {
				options = cmp;
				cmp = name;
			} else {
				var cmps = this._cmps || (this._cmps = {});
				cmps[name] = cmp;
			}
			this._own(cmp);
			if (options) {
				for (var key in options) {
					cmp[key](options[key]);
				}
			}
			return cmp;
		},
	});
});<commit_msg>remove experimental _Composite#_addCmp<commit_after>define([
	'compose',
	'../../base/_Destroyable',
	'../../base/_Evented',
	'../_WithSize'
], function(
	compose,
	_Destroyable,
	_Evented,
	_WithSize
){
	return compose(_Destroyable, _Evented, _WithSize, function() {
		this._root = this._own(this._rootFactory());
		this.domNode = this._root.domNode;
	}, {
		inDom: function(inDom) {
			this._root.inDom && this._root.inDom(inDom);
		},
		bounds: function(bounds) {
			this._root.bounds && this._root.bounds(bounds);
		},
	});
});<|endoftext|>


<commit_before>var twitter = require('twitter');
var client = new twitter({
  consumer_key: process.env.TWITTER_CONSUMER_KEY,
  consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
  access_token_key: process.env.TWITTER_ACCESS_TOKEN,
  access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET
});

var fs = require('fs')
var utility = require('../api/helpers/utilities.js')

// checkDataType parses the tweet object to find any matching influencers, hashtags, or mentions and then returns an array of each found in the tweet
var checkDataType = function(tweet, trackers) {
  var mentions = tweet.entities.user_mentions;
  var hashtags = tweet.entities.hashtags;

  // console.log(mentions)

  for(var i = 0; i < trackers.length; i++) {
    if(trackers[i].type === "influencer" && tweet.user.screen_name.toLowerCase() === trackers[i].name && tweet.text.trim()[0] !== '@') {
        return {tweet: tweet, tracker: trackers[i]}
    } else if(trackers[i].type === "mention" && mentions.length > 0 && !('retweeted_status' in tweet)) {
        for(var j = 0; j < mentions.length; j++) {
            if(trackers[i].name === mentions[j].screen_name.toLowerCase()) {
                return {tweet: tweet, tracker: trackers[i]}
            }
        }
    } else if(trackers[i].type === "hashtag" && hashtags.length > 0 && !('retweeted_status' in tweet)) {
        for(var j = 0; j < hashtags.length; j++) {
            if(trackers[i].name === hashtags[j].text.toLowerCase()) {
                return {tweet: tweet, tracker: trackers[i]}
            }
        }
    }

  }
  return false;
}

var runTwitterstream = function() {
    Tracker.find().populateAll().exec(function(err, trackers) {
        if(!err) {
            var track = {};

            trackers.forEach(function(t) {
              if(t.twitter_accounts.length > 0) {
                if(t.type == "influencer" && t.twitter_id) {
                    if(track.follow) {
                      track.follow += ',' + t.twitter_id;
                    } else {
                      track.follow = t.twitter_id.toString();
                    }
                } else if(type == 'mention' || type == 'hashtag') {
                    if(track.track) {
                      track.track += ',' + t.name;
                    } else {
                      track.track = t.name;
                    }
                }
              }
            });

            if(Object.keys(track).length === 0) {
              track.track ='inclusivedesign';
            }

            client.stream('statuses/filter', track, function(stream) {
              stream.on('data', function(tweet) {
                console.log(tweet.text);
                var trackerData = checkDataType(tweet, trackers);
                console.log("Tracker Data:",trackerData !== null)
                if(trackerData) {
                    async.each(trackerData.tracker.twitter_accounts ,function(account, callback) {
                        Twitter_Account.findOne({id: account.id}).populate('user').exec(function(err, account) {
                            if(!err && account && account.user && account.user.emailToggle) {
                                console.log("sending email...",account)
                                callback(utility.sendEmail(account.user.email, "[TwitterMafiaApp] You have new updates!", trackerData, false));
                            } else if(err) {
                                callback(err)
                            }else if (!account.user.emailToggle) {
                              console.log('email was not sent!')
                            }
                        });
                    });
                }
                console.log("*************************************\n\n");
              });

              stream.on('error', function(error) {
                console.log("ERROR!!",error);
                console.log("DATA sent:",track);
                setTimeout(runTwitterstream, 90000);
              });
            });
        }
    });
}

module.exports.twitterstream = function() {
  runTwitterstream();
}<commit_msg>Fixed typo<commit_after>var twitter = require('twitter');
var client = new twitter({
  consumer_key: process.env.TWITTER_CONSUMER_KEY,
  consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
  access_token_key: process.env.TWITTER_ACCESS_TOKEN,
  access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET
});

var fs = require('fs')
var utility = require('../api/helpers/utilities.js')

// checkDataType parses the tweet object to find any matching influencers, hashtags, or mentions and then returns an array of each found in the tweet
var checkDataType = function(tweet, trackers) {
  var mentions = tweet.entities.user_mentions;
  var hashtags = tweet.entities.hashtags;

  // console.log(mentions)

  for(var i = 0; i < trackers.length; i++) {
    if(trackers[i].type === "influencer" && tweet.user.screen_name.toLowerCase() === trackers[i].name && tweet.text.trim()[0] !== '@') {
        return {tweet: tweet, tracker: trackers[i]}
    } else if(trackers[i].type === "mention" && mentions.length > 0 && !('retweeted_status' in tweet)) {
        for(var j = 0; j < mentions.length; j++) {
            if(trackers[i].name === mentions[j].screen_name.toLowerCase()) {
                return {tweet: tweet, tracker: trackers[i]}
            }
        }
    } else if(trackers[i].type === "hashtag" && hashtags.length > 0 && !('retweeted_status' in tweet)) {
        for(var j = 0; j < hashtags.length; j++) {
            if(trackers[i].name === hashtags[j].text.toLowerCase()) {
                return {tweet: tweet, tracker: trackers[i]}
            }
        }
    }

  }
  return false;
}

var runTwitterstream = function() {
    Tracker.find().populateAll().exec(function(err, trackers) {
        if(!err) {
            var track = {};

            trackers.forEach(function(t) {
              if(t.twitter_accounts.length > 0) {
                if(t.type == "influencer" && t.twitter_id) {
                    if(track.follow) {
                      track.follow += ',' + t.twitter_id;
                    } else {
                      track.follow = t.twitter_id.toString();
                    }
                } else if(t.type == 'mention' || t.type == 'hashtag') {
                    if(track.track) {
                      track.track += ',' + t.name;
                    } else {
                      track.track = t.name;
                    }
                }
              }
            });

            if(Object.keys(track).length === 0) {
              track.track ='inclusivedesign';
            }

            client.stream('statuses/filter', track, function(stream) {
              stream.on('data', function(tweet) {
                console.log(tweet.text);
                var trackerData = checkDataType(tweet, trackers);
                console.log("Tracker Data:",trackerData !== null)
                if(trackerData) {
                    async.each(trackerData.tracker.twitter_accounts ,function(account, callback) {
                        Twitter_Account.findOne({id: account.id}).populate('user').exec(function(err, account) {
                            if(!err && account && account.user && account.user.emailToggle) {
                                console.log("sending email...",account)
                                callback(utility.sendEmail(account.user.email, "[TwitterMafiaApp] You have new updates!", trackerData, false));
                            } else if(err) {
                                callback(err)
                            }else if (!account.user.emailToggle) {
                              console.log('email was not sent!')
                            }
                        });
                    });
                }
                console.log("*************************************\n\n");
              });

              stream.on('error', function(error) {
                console.log("ERROR!!",error);
                console.log("DATA sent:",track);
                setTimeout(runTwitterstream, 90000);
              });
            });
        }
    });
}

module.exports.twitterstream = function() {
  runTwitterstream();
}<|endoftext|>


<commit_before>/*
TODO: Add observer to put scripts installed, while the addons window is open,
      into the list.
*/

// Override some built-in functions, with a closure reference to the original
// function, to either handle or delegate the call.
(function() {
var _origShowView = showView;
showView = function(aView) {
  if ('userscripts' == aView) {
    greasemonkeyAddons.showView();
  } else {
    _origShowView(aView);
  }
};

var _origBuildContextMenu = buildContextMenu;
buildContextMenu = function(aEvent) {
  if ('userscripts' == gView) {
    greasemonkeyAddons.buildContextMenu(aEvent);
  } else {
    _origBuildContextMenu(aEvent);
  }
};
})();

// Set event listeners.
window.addEventListener('load', function() {
  greasemonkeyAddons.onAddonSelect();
  gExtensionsView.addEventListener('select', greasemonkeyAddons.onAddonSelect, false);
}, false);

var greasemonkeyAddons={
  showView: function() {
    if ('userscripts' == gView) return;
    updateLastSelected('userscripts');
    gView='userscripts';

    // Hide the native controls that don't work in the user scripts view.
    function $(id) { return document.getElementById(id); }
    function hide(el) { el=$(el); el && (el.hidden=true); }
    var elementIds=[
      'searchPanel', 'installFileButton', 'checkUpdatesAllButton',
      'skipDialogButton', 'themePreviewArea', 'themeSplitter',
      'showUpdateInfoButton', 'hideUpdateInfoButton',
      'installUpdatesAllButton'];
    elementIds.forEach(hide);

    var getMore = document.getElementById('getMore');
    getMore.setAttribute('getMoreURL', 'http://userscripts.org/');
    getMore.hidden = false;
    getMore.value = 'Get User Scripts';

    greasemonkeyAddons.fillList();
    gExtensionsView.selectedItem = gExtensionsView.children[0];
    // The setTimeout() here is for timing, to make sure the selection above
    // has really happened.
    setTimeout(greasemonkeyAddons.onAddonSelect, 0);
  },

  fillList: function() {
    // I'd much prefer to use the inbuilt templates/rules mechanism that the
    // native FFX bits of this dialog use, but this works as P.O.C.
    var config = GM_getConfig();
    var listbox = gExtensionsView;

    // Remove any pre-existing contents.
    while (listbox.firstChild) {
      listbox.removeChild(listbox.firstChild);
    }

    // Add a list item for each script.
    for (var i = 0, script = null; script = config.scripts[i]; i++) {
      var item = document.createElement('richlistitem');
      item.setAttribute('class', 'userscript');
      // Fake this for now.
      var id = script.namespace + script.name;
      // Setting these attributes inherits the values into the same place they
      // would go for extensions.
      item.setAttribute('addonId', id);
      item.setAttribute('name', script.name);
      item.setAttribute('description', script.description);
      item.setAttribute('id', 'urn:greasemonkey:item:'+id);
      item.setAttribute('isDisabled', !script.enabled);
      // These hide extension-specific bits we don't want to display.
      item.setAttribute('blocklisted', 'false');
      item.setAttribute('blocklistedsoft', 'false');
      item.setAttribute('compatible', 'true');
      item.setAttribute('locked', 'false');
      item.setAttribute('providesUpdatesSecurely', 'true');
      item.setAttribute('satisfiesDependencies', 'true');
      item.setAttribute('type', nsIUpdateItem.TYPE_EXTENSION);

      // Place this item in the container.
      listbox.appendChild(item);
    }
  },

  findSelectedScript: function() {
    var scripts = GM_getConfig().scripts;
    var selectedScriptId = gExtensionsView.selectedItem.getAttribute('addonId');
    for (var i = 0, script = null; script=scripts[i]; i++) {
      if (selectedScriptId == script.namespace+script.name) {
        return script;
      }
    }
    return null;
  },

  onAddonSelect: function(aEvent) {
    // We do all this work here, because the elements we want to change do
    // not exist until the item is selected.

    if ('userscripts' != gView) return;
    var script = greasemonkeyAddons.findSelectedScript();

    // Remove/change the anonymous nodes we don't want.
    var item = gExtensionsView.selectedItem;
    var button;

    // Replace 'preferences' with 'edit'.
    button = item.ownerDocument.getAnonymousElementByAttribute(
        item, 'command', 'cmd_options');
    if (!button) return;
    button.setAttribute('label', 'Edit');
    button.setAttribute('accesskey', 'E');
    button.setAttribute('command', 'cmd_userscript_edit');
    button.setAttribute('disabled', 'false');

    // Rewire enable, disable, uninstall.
    button = item.ownerDocument.getAnonymousElementByAttribute(
        item, 'command', 'cmd_enable');
    if (!button) return;
    button.setAttribute('command', 'cmd_userscript_enable');
    button.setAttribute('disabled', 'false');

    button = item.ownerDocument.getAnonymousElementByAttribute(
        item, 'command', 'cmd_disable');
    if (!button) return;
    button.setAttribute('command', 'cmd_userscript_disable');
    button.setAttribute('disabled', 'false');

    button = item.ownerDocument.getAnonymousElementByAttribute(
        item, 'command', 'cmd_uninstall');
    if (!button) return;
    button.setAttribute('command', 'cmd_userscript_uninstall');
    button.setAttribute('disabled', 'false');
  },

  doCommand: function(command) {
    var script = greasemonkeyAddons.findSelectedScript();
    if (!script) {
      alert('Crap, something went wrong!');
      return;
    }

    var selectedListitem = gExtensionsView.selectedItem;
    switch (command) {
    case 'cmd_userscript_edit':
      openInEditor(script);
      break;
    case 'cmd_userscript_enable':
      selectedListitem.setAttribute('isDisabled', 'false');
      script.enabled = true;
      break;
    case 'cmd_userscript_disable':
      selectedListitem.setAttribute('isDisabled', 'true');
      script.enabled = false;
      break;
    case 'cmd_userscript_uninstall':
      // todo, make this "true" configurable?
      GM_getConfig().uninstall(script, true);
      selectedListitem.parentNode.removeChild(selectedListitem);
      break;
    }
  },

  buildContextMenu: function(aEvent) {
    var script = greasemonkeyAddons.findSelectedScript();
    if (!script) {
      alert('Crap, something went wrong!');
      return;
    }

    var popup = document.getElementById('addonContextMenu');
    while (popup.hasChildNodes()) {
      popup.removeChild(popup.firstChild);
    }

    function addMenuItem(label, command) {
      var menuitem = document.createElement('menuitem');
      menuitem.setAttribute('label', label);
      menuitem.setAttribute('command', command);
      popup.appendChild(menuitem);
    }

    addMenuItem('Edit', 'cmd_userscript_edit');
    if (script.enabled) {
      addMenuItem('Disable', 'cmd_userscript_disable');
    } else {
      addMenuItem('Enable', 'cmd_userscript_enable');
    }
    addMenuItem('Uninstall', 'cmd_userscript_uninstall');
  }
};
<commit_msg>Load updated script details when opening the addons view.<commit_after>/*
TODO: Add observer to put scripts installed, while the addons window is open,
      into the list.
*/

// Override some built-in functions, with a closure reference to the original
// function, to either handle or delegate the call.
(function() {
var _origShowView = showView;
showView = function(aView) {
  if ('userscripts' == aView) {
    greasemonkeyAddons.showView();
  } else {
    _origShowView(aView);
  }
};

var _origBuildContextMenu = buildContextMenu;
buildContextMenu = function(aEvent) {
  if ('userscripts' == gView) {
    greasemonkeyAddons.buildContextMenu(aEvent);
  } else {
    _origBuildContextMenu(aEvent);
  }
};
})();

// Set event listeners.
window.addEventListener('load', function() {
  greasemonkeyAddons.onAddonSelect();
  gExtensionsView.addEventListener('select', greasemonkeyAddons.onAddonSelect, false);
}, false);

var greasemonkeyAddons={
  gmSvc: Components.classes["@greasemonkey.mozdev.org/greasemonkey-service;1"]
                   .getService(Components.interfaces.gmIGreasemonkeyService)
                   .wrappedJSObject,

  showView: function() {
    if ('userscripts' == gView) return;
    updateLastSelected('userscripts');
    gView='userscripts';

    // Update any possibly modified scripts.
    this.gmSvc.config.updateModifiedScripts();

    // Hide the native controls that don't work in the user scripts view.
    function $(id) { return document.getElementById(id); }
    function hide(el) { el=$(el); el && (el.hidden=true); }
    var elementIds=[
      'searchPanel', 'installFileButton', 'checkUpdatesAllButton',
      'skipDialogButton', 'themePreviewArea', 'themeSplitter',
      'showUpdateInfoButton', 'hideUpdateInfoButton',
      'installUpdatesAllButton'];
    elementIds.forEach(hide);

    var getMore = document.getElementById('getMore');
    getMore.setAttribute('getMoreURL', 'http://userscripts.org/');
    getMore.hidden = false;
    getMore.value = 'Get User Scripts';

    greasemonkeyAddons.fillList();
    gExtensionsView.selectedItem = gExtensionsView.children[0];
    // The setTimeout() here is for timing, to make sure the selection above
    // has really happened.
    setTimeout(greasemonkeyAddons.onAddonSelect, 0);
  },

  fillList: function() {
    // I'd much prefer to use the inbuilt templates/rules mechanism that the
    // native FFX bits of this dialog use, but this works as P.O.C.
    var config = GM_getConfig();
    var listbox = gExtensionsView;

    // Remove any pre-existing contents.
    while (listbox.firstChild) {
      listbox.removeChild(listbox.firstChild);
    }

    // Add a list item for each script.
    for (var i = 0, script = null; script = config.scripts[i]; i++) {
      var item = document.createElement('richlistitem');
      item.setAttribute('class', 'userscript');
      // Fake this for now.
      var id = script.namespace + script.name;
      // Setting these attributes inherits the values into the same place they
      // would go for extensions.
      item.setAttribute('addonId', id);
      item.setAttribute('name', script.name);
      item.setAttribute('description', script.description);
      item.setAttribute('id', 'urn:greasemonkey:item:'+id);
      item.setAttribute('isDisabled', !script.enabled);
      // These hide extension-specific bits we don't want to display.
      item.setAttribute('blocklisted', 'false');
      item.setAttribute('blocklistedsoft', 'false');
      item.setAttribute('compatible', 'true');
      item.setAttribute('locked', 'false');
      item.setAttribute('providesUpdatesSecurely', 'true');
      item.setAttribute('satisfiesDependencies', 'true');
      item.setAttribute('type', nsIUpdateItem.TYPE_EXTENSION);

      // Place this item in the container.
      listbox.appendChild(item);
    }
  },

  findSelectedScript: function() {
    var scripts = GM_getConfig().scripts;
    var selectedScriptId = gExtensionsView.selectedItem.getAttribute('addonId');
    for (var i = 0, script = null; script=scripts[i]; i++) {
      if (selectedScriptId == script.namespace+script.name) {
        return script;
      }
    }
    return null;
  },

  onAddonSelect: function(aEvent) {
    // We do all this work here, because the elements we want to change do
    // not exist until the item is selected.

    if ('userscripts' != gView) return;
    var script = greasemonkeyAddons.findSelectedScript();

    // Remove/change the anonymous nodes we don't want.
    var item = gExtensionsView.selectedItem;
    var button;

    // Replace 'preferences' with 'edit'.
    button = item.ownerDocument.getAnonymousElementByAttribute(
        item, 'command', 'cmd_options');
    if (!button) return;
    button.setAttribute('label', 'Edit');
    button.setAttribute('accesskey', 'E');
    button.setAttribute('command', 'cmd_userscript_edit');
    button.setAttribute('disabled', 'false');

    // Rewire enable, disable, uninstall.
    button = item.ownerDocument.getAnonymousElementByAttribute(
        item, 'command', 'cmd_enable');
    if (!button) return;
    button.setAttribute('command', 'cmd_userscript_enable');
    button.setAttribute('disabled', 'false');

    button = item.ownerDocument.getAnonymousElementByAttribute(
        item, 'command', 'cmd_disable');
    if (!button) return;
    button.setAttribute('command', 'cmd_userscript_disable');
    button.setAttribute('disabled', 'false');

    button = item.ownerDocument.getAnonymousElementByAttribute(
        item, 'command', 'cmd_uninstall');
    if (!button) return;
    button.setAttribute('command', 'cmd_userscript_uninstall');
    button.setAttribute('disabled', 'false');
  },

  doCommand: function(command) {
    var script = greasemonkeyAddons.findSelectedScript();
    if (!script) {
      alert('Crap, something went wrong!');
      return;
    }

    var selectedListitem = gExtensionsView.selectedItem;
    switch (command) {
    case 'cmd_userscript_edit':
      openInEditor(script);
      break;
    case 'cmd_userscript_enable':
      selectedListitem.setAttribute('isDisabled', 'false');
      script.enabled = true;
      break;
    case 'cmd_userscript_disable':
      selectedListitem.setAttribute('isDisabled', 'true');
      script.enabled = false;
      break;
    case 'cmd_userscript_uninstall':
      // todo, make this "true" configurable?
      GM_getConfig().uninstall(script, true);
      selectedListitem.parentNode.removeChild(selectedListitem);
      break;
    }
  },

  buildContextMenu: function(aEvent) {
    var script = greasemonkeyAddons.findSelectedScript();
    if (!script) {
      alert('Crap, something went wrong!');
      return;
    }

    var popup = document.getElementById('addonContextMenu');
    while (popup.hasChildNodes()) {
      popup.removeChild(popup.firstChild);
    }

    function addMenuItem(label, command) {
      var menuitem = document.createElement('menuitem');
      menuitem.setAttribute('label', label);
      menuitem.setAttribute('command', command);
      popup.appendChild(menuitem);
    }

    addMenuItem('Edit', 'cmd_userscript_edit');
    if (script.enabled) {
      addMenuItem('Disable', 'cmd_userscript_disable');
    } else {
      addMenuItem('Enable', 'cmd_userscript_enable');
    }
    addMenuItem('Uninstall', 'cmd_userscript_uninstall');
  }
};
<|endoftext|>


<commit_before>/**
 * `core/modules` data store: module sharing settings
 *
 * Site Kit by Google, Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { createFetchStore } from '../../data/create-fetch-store';

// Actions
const SET_OWNER_ID = 'SET_OWNER_ID';
const SET_SHARING_MANAGEMENT = 'SET_SHARING_MANAGEMENT';
const SET_SHARED_ROLES = 'SET_SHARED_ROLES';
const RECEIVE_GET_SHARING_SETTINGS = 'RECEIVE_GET_SHARING_SETTINGS';

const baseInitialState = {
	sharingSettings: undefined,
	savedSharingSettings: undefined,
};

const fetchSaveSharingSettingsStore = createFetchStore( {
	baseName: 'saveSharingSettings',
	controlCallback: ( { savedSharingSettings } ) => {
		return API.set(
			'core',
			'modules',
			'sharing-settings',
			{ savedSharingSettings },
			{
				useCache: false,
			}
		);
	},
	reducerCallback: ( state, { settings } ) => {
		return {
			...state,
			savedSharingSettings: settings,
			sharingSettings: settings,
		};
	},
	argsToParams: ( savedSharingSettings ) => ( { savedSharingSettings } ),
	validateParams: ( { savedSharingSettings } = {} ) => {
		invariant( savedSharingSettings, 'savedSharingSettings is required.' );
	},
} );

const baseActions = {
	/**
	 * Sets ownerID for the given modules.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} newOwnerIDs New owner IDs, [moduleSlug]: Number.
	 * @return {Object} Action for SET_OWNER_ID.
	 */
	setOwnerID( newOwnerIDs ) {
		invariant( newOwnerIDs, 'newOwnerIDs is required.' );
		return {
			payload: {
				newOwnerIDs,
			},
			type: SET_OWNER_ID,
		};
	},

	/**
	 * Sets the sharing settings management role of given module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string} moduleSlug Module slug.
	 * @param {string} management New management role for module, one of: all_admins, owner.
	 * @return {Object} Action for SET_SHARING_MANAGEMENT.
	 */
	setSharingManagement( moduleSlug, management ) {
		invariant( moduleSlug, 'moduleSlug is required.' );
		const managementRoles = [ 'all_admins', 'owner' ];
		invariant(
			managementRoles.includes( management ),
			`management must be one of: ${ managementRoles.join( ', ' ) }.`
		);
		return {
			payload: {
				moduleSlug,
				management,
			},
			type: SET_SHARING_MANAGEMENT,
		};
	},

	/**
	 * Sets the sharing settings management role of given module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string}   moduleSlug Module slug.
	 * @param {string[]} roles      List of roles the module is shared with.
	 * @return {Object} Action for SET_SHARED_ROLES.
	 */
	setSharedRoles( moduleSlug, roles ) {
		invariant( moduleSlug, 'moduleSlug is required.' );
		invariant(
			Array.isArray( roles ) &&
				roles.every( ( role ) => typeof role === 'string' ),
			'roles must be an array of strings.'
		);
		return {
			payload: {
				moduleSlug,
				roles,
			},
			type: SET_SHARED_ROLES,
		};
	},

	/**
	 * Saves sharingSettings for dashboard sharing.
	 *
	 * Save sharingSettings for dashboard sharing.
	 * Update ownerID from the response for the modules in the sharingSettings state.
	 *
	 * @since n.e.x.t
	 *
	 * @return {Object} Object with `{response, error}`.
	 */
	*saveSharingSettings() {
		// TODO: Refactor to use `getSharingSettings` selector
		// to obtain the `sharingSettings` from the state.
		const sharingSettings = {
			'search-console': {
				sharedRoles: [ 'editor', 'subscriber' ],
				management: 'all_admins',
			},
			analytics: {
				sharedRoles: [ 'editor' ],
				management: 'owner',
			},
			'pagespeed-insights': {
				sharedRoles: [ 'editor' ],
				management: 'all_admins',
			},
		};

		const {
			response,
			error,
		} = yield fetchSaveSharingSettingsStore.actions.fetchSaveSharingSettings(
			sharingSettings
		);

		// Update module owner IDs in the sharing settings modules.
		if ( response?.newOwnerIDs ) {
			yield baseActions.setOwnerID( response.newOwnerIDs );
		}

		return { response, error };
	},

	/**
	 * Receives sharingSettings for dashboard sharing.
	 * Stores sharingSettings in the datastore.
	 *
	 * Because this is frequently-accessed data, this is usually sourced
	 * from a global variable (`_googlesitekitDashboardSharingData`), set by PHP
	 * in the `before_print` callback for `googlesitekit-datastore-site`.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} sharingSettings Sharing settings for modules with `management` and `sharedRoles` properties.
	 * @return {Object} Action for RECEIVE_GET_SHARING_SETTINGS.
	 */
	receiveGetSharingSettings( sharingSettings ) {
		invariant( sharingSettings, 'sharingSettings is required.' );
		return {
			payload: { sharingSettings },
			type: RECEIVE_GET_SHARING_SETTINGS,
		};
	},
};

const baseReducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case SET_SHARING_MANAGEMENT: {
			const { moduleSlug, management } = payload;

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						management,
					},
				},
				savedSharingSettings: {
					...state.savedSharingSettings,
					[ moduleSlug ]: {
						...state.savedSharingSettings[ moduleSlug ],
						management,
					},
				},
			};
		}

		case SET_SHARED_ROLES: {
			const { moduleSlug, roles } = payload;

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						sharedRoles: roles,
					},
				},
				savedSharingSettings: {
					...state.savedSharingSettings,
					[ moduleSlug ]: {
						...state.savedSharingSettings[ moduleSlug ],
						sharedRoles: roles,
					},
				},
			};
		}

		case SET_OWNER_ID: {
			const { newOwnerIDs } = payload;

			const sharingSettingsWithOnwerID = Object.keys(
				newOwnerIDs
			).reduce(
				( modules, moduleSlug ) => ( {
					...modules,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						ownerID: newOwnerIDs[ moduleSlug ],
					},
				} ),
				{}
			);

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					...sharingSettingsWithOnwerID,
				},
				savedSharingSettings: {
					...state.savedSharingSettings,
					...sharingSettingsWithOnwerID,
				},
			};
		}

		case RECEIVE_GET_SHARING_SETTINGS: {
			const { sharingSettings } = payload;

			return {
				...state,
				sharingSettings,
				savedSharingSettings: sharingSettings,
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {};

const baseSelectors = {};

const store = Data.combineStores( fetchSaveSharingSettingsStore, {
	initialState: baseInitialState,
	actions: baseActions,
	selectors: baseSelectors,
	reducer: baseReducer,
	resolvers: baseResolvers,
} );

export const initialState = store.initialState;
export const actions = store.actions;
export const selectors = store.selectors;
export const reducer = store.reducer;

export default store;
<commit_msg>Update todo comment.<commit_after>/**
 * `core/modules` data store: module sharing settings
 *
 * Site Kit by Google, Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { createFetchStore } from '../../data/create-fetch-store';

// Actions
const SET_OWNER_ID = 'SET_OWNER_ID';
const SET_SHARING_MANAGEMENT = 'SET_SHARING_MANAGEMENT';
const SET_SHARED_ROLES = 'SET_SHARED_ROLES';
const RECEIVE_GET_SHARING_SETTINGS = 'RECEIVE_GET_SHARING_SETTINGS';

const baseInitialState = {
	sharingSettings: undefined,
	savedSharingSettings: undefined,
};

const fetchSaveSharingSettingsStore = createFetchStore( {
	baseName: 'saveSharingSettings',
	controlCallback: ( { savedSharingSettings } ) => {
		return API.set(
			'core',
			'modules',
			'sharing-settings',
			{ savedSharingSettings },
			{
				useCache: false,
			}
		);
	},
	reducerCallback: ( state, { settings } ) => {
		return {
			...state,
			savedSharingSettings: settings,
			sharingSettings: settings,
		};
	},
	argsToParams: ( savedSharingSettings ) => ( { savedSharingSettings } ),
	validateParams: ( { savedSharingSettings } = {} ) => {
		invariant( savedSharingSettings, 'savedSharingSettings is required.' );
	},
} );

const baseActions = {
	/**
	 * Sets ownerID for the given modules.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} newOwnerIDs New owner IDs, [moduleSlug]: Number.
	 * @return {Object} Action for SET_OWNER_ID.
	 */
	setOwnerID( newOwnerIDs ) {
		invariant( newOwnerIDs, 'newOwnerIDs is required.' );
		return {
			payload: {
				newOwnerIDs,
			},
			type: SET_OWNER_ID,
		};
	},

	/**
	 * Sets the sharing settings management role of given module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string} moduleSlug Module slug.
	 * @param {string} management New management role for module, one of: all_admins, owner.
	 * @return {Object} Action for SET_SHARING_MANAGEMENT.
	 */
	setSharingManagement( moduleSlug, management ) {
		invariant( moduleSlug, 'moduleSlug is required.' );
		const managementRoles = [ 'all_admins', 'owner' ];
		invariant(
			managementRoles.includes( management ),
			`management must be one of: ${ managementRoles.join( ', ' ) }.`
		);
		return {
			payload: {
				moduleSlug,
				management,
			},
			type: SET_SHARING_MANAGEMENT,
		};
	},

	/**
	 * Sets the sharing settings management role of given module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string}   moduleSlug Module slug.
	 * @param {string[]} roles      List of roles the module is shared with.
	 * @return {Object} Action for SET_SHARED_ROLES.
	 */
	setSharedRoles( moduleSlug, roles ) {
		invariant( moduleSlug, 'moduleSlug is required.' );
		invariant(
			Array.isArray( roles ) &&
				roles.every( ( role ) => typeof role === 'string' ),
			'roles must be an array of strings.'
		);
		return {
			payload: {
				moduleSlug,
				roles,
			},
			type: SET_SHARED_ROLES,
		};
	},

	/**
	 * Saves sharingSettings for dashboard sharing.
	 *
	 * Save sharingSettings for dashboard sharing.
	 * Update ownerID from the response for the modules in the sharingSettings state.
	 *
	 * @since n.e.x.t
	 *
	 * @return {Object} Object with `{response, error}`.
	 */
	*saveSharingSettings() {
		// TODO: Refactor to use `getSharingSettings` selector
		// to obtain the `sharingSettings` from the state in 4795.
		const sharingSettings = {
			'search-console': {
				sharedRoles: [ 'editor', 'subscriber' ],
				management: 'all_admins',
			},
			analytics: {
				sharedRoles: [ 'editor' ],
				management: 'owner',
			},
			'pagespeed-insights': {
				sharedRoles: [ 'editor' ],
				management: 'all_admins',
			},
		};

		const {
			response,
			error,
		} = yield fetchSaveSharingSettingsStore.actions.fetchSaveSharingSettings(
			sharingSettings
		);

		// Update module owner IDs in the sharing settings modules.
		if ( response?.newOwnerIDs ) {
			yield baseActions.setOwnerID( response.newOwnerIDs );
		}

		return { response, error };
	},

	/**
	 * Receives sharingSettings for dashboard sharing.
	 * Stores sharingSettings in the datastore.
	 *
	 * Because this is frequently-accessed data, this is usually sourced
	 * from a global variable (`_googlesitekitDashboardSharingData`), set by PHP
	 * in the `before_print` callback for `googlesitekit-datastore-site`.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} sharingSettings Sharing settings for modules with `management` and `sharedRoles` properties.
	 * @return {Object} Action for RECEIVE_GET_SHARING_SETTINGS.
	 */
	receiveGetSharingSettings( sharingSettings ) {
		invariant( sharingSettings, 'sharingSettings is required.' );
		return {
			payload: { sharingSettings },
			type: RECEIVE_GET_SHARING_SETTINGS,
		};
	},
};

const baseReducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case SET_SHARING_MANAGEMENT: {
			const { moduleSlug, management } = payload;

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						management,
					},
				},
				savedSharingSettings: {
					...state.savedSharingSettings,
					[ moduleSlug ]: {
						...state.savedSharingSettings[ moduleSlug ],
						management,
					},
				},
			};
		}

		case SET_SHARED_ROLES: {
			const { moduleSlug, roles } = payload;

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						sharedRoles: roles,
					},
				},
				savedSharingSettings: {
					...state.savedSharingSettings,
					[ moduleSlug ]: {
						...state.savedSharingSettings[ moduleSlug ],
						sharedRoles: roles,
					},
				},
			};
		}

		case SET_OWNER_ID: {
			const { newOwnerIDs } = payload;

			const sharingSettingsWithOnwerID = Object.keys(
				newOwnerIDs
			).reduce(
				( modules, moduleSlug ) => ( {
					...modules,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						ownerID: newOwnerIDs[ moduleSlug ],
					},
				} ),
				{}
			);

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					...sharingSettingsWithOnwerID,
				},
				savedSharingSettings: {
					...state.savedSharingSettings,
					...sharingSettingsWithOnwerID,
				},
			};
		}

		case RECEIVE_GET_SHARING_SETTINGS: {
			const { sharingSettings } = payload;

			return {
				...state,
				sharingSettings,
				savedSharingSettings: sharingSettings,
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {};

const baseSelectors = {};

const store = Data.combineStores( fetchSaveSharingSettingsStore, {
	initialState: baseInitialState,
	actions: baseActions,
	selectors: baseSelectors,
	reducer: baseReducer,
	resolvers: baseResolvers,
} );

export const initialState = store.initialState;
export const actions = store.actions;
export const selectors = store.selectors;
export const reducer = store.reducer;

export default store;
<|endoftext|>


<commit_before>'use strict'

// This file handles application-level state (as opposed to component-level
// state) per: http://vuex.vuejs.org/en/intro.html

import Vue from 'vue'
import Vuex from 'vuex'
import * as db from './database'
import * as Events from '../../../shared/events'
import backend from '../js/backend'
// babel transforms lodash imports: https://github.com/lodash/babel-plugin-lodash#faq
// for diff between 'lodash/map' and 'lodash/fp/map'
// see: https://github.com/lodash/lodash/wiki/FP-Guide
import debounce from 'lodash/debounce'

Vue.use(Vuex)
var store // this is set and made the default export at the bottom of the file.
          // we have it declared here to make it accessible in mutations
// 'state' is the Vuex state object
// NOTE: THE STATE CAN ONLY STORE *SERIALIZABLE* OBJECTS! THAT MEANS IF YOU TRY
//       TO STORE AN INSTANCE OF A CLASS (LIKE A CONTRACT), IT WILL NOT STORE
//       THE ACTUAL CONTRACT, BUT JSON.STRINGIFY(CONTRACT) INSTEAD!
const state = {
  position: null, // TODO: get rid of this?
  currentGroupId: null,
  contracts: {}, // contractIds => type (for contracts we've successfully subscribed to)
  pending: [], // contractIds we've just published but haven't received back yet
  loggedIn: false // TODO: properly handle this
}

// Mutations must be synchronous! Never call these directly!
// http://vuex.vuejs.org/en/mutations.html
const mutations = {
  login (state, user) {
    state.loggedIn = user
  },
  logout (state) {
    state.loggedIn = false
  },
  addContract (state, {contractId, type, data}) {
    // "Mutations Follow Vue's Reactivity Rules" - important for modifying objects
    // See: https://vuex.vuejs.org/en/mutations.html
    Vue.set(state.contracts, contractId, type)
    store.registerModule(contractId, {...Events[type].vuex, ...{state: data}})
    // we've successfully received it back, so remove it from expectation pending
    let index = state.pending.indexOf(contractId)
    index > -1 && state.pending.splice(index, 1)
  },
  removeContract (state, contractId) {
    store.unregisterModule(contractId)
    Vue.delete(state.contracts, contractId)
  },
  setContracts (state, contracts) {
    for (let contract of contracts) {
      mutations.addContract(state, contract)
    }
  },
  deleteMessage (state, hash) {
    let mailboxContract = store.getters.mailboxContract
    let index = mailboxContract && mailboxContract.messages.findIndex(msg => msg.hash === hash)
    if (index > -1) { mailboxContract.messages.splice(index, 1) }
  },
  markMessageAsRead (state, hash) {
    let mailboxContract = store.getters.mailboxContract
    let index = mailboxContract && mailboxContract.messages.findIndex(msg => msg.hash === hash)
    if (index > -1) { mailboxContract.messages[index].read = true }
  },
  setCurrentGroupId (state, currentGroupId) {
    state.currentGroupId = currentGroupId
    state.position = currentGroupId
  },
  setPosition (state, position) {
    state.position = position
  },
  pending (state, contractId) {
    if (!state.contracts[contractId]) {
      let index = state.pending.indexOf(contractId)
      index === -1 && state.pending.push(contractId)
    }
  }
}
// https://vuex.vuejs.org/en/getters.html
// https://vuex.vuejs.org/en/modules.html
const getters = {
  currentGroup (state) {
    return state[state.currentGroupId]
  },
  mailboxContract (state) {
    // TODO: If we a user is ever subscribed to multiple mailboxes, this will have to be rewritten
    for (let [key, value] of Object.entries(state.contracts)) {
      if (value === 'MailboxContract') {
        return state[key]
      }
    }
    return null
  },
  mailbox (state) {
    let mailboxContract = store.getters.mailboxContract
    return mailboxContract && mailboxContract.messages
  },
  unreadMessageCount (state) {
    let mailboxContract = store.getters.mailboxContract
    return mailboxContract && mailboxContract.messages.filter((msg) => !msg.read).length
  }
}

const actions = {
  // Used to update contracts to the current state that the server is aware of
  async syncContractWithServer ({dispatch}: {dispatch: Function}, contractId: string) {
    let latest = await backend.latestHash(contractId)
    let recent = await db.recentHash(contractId)
    if (latest !== recent) {
      // TODO Do we need a since call that is inclusive? Since does not imply inclusion
      let events = await backend.eventsSince(contractId, recent || contractId)
      // remove the first element in cases where we are not getting the contract for the first time
      recent && events.shift()
      for (let i = 0; i < events.length; i++) {
        let event = events[i]
        event.contractId = contractId
        await dispatch('handleEvent', event)
      }
    }
  },
  async login (
    {dispatch, commit, state}: {dispatch: Function, commit: Function, state: Object},
    user: string
  ) {
    commit('login', user)
    await dispatch('loadSettings')
    await db.saveCurrentUser(user)
    // This may seem unintuitive to use the state from the global store object
    // but the state object in scope is a copy that becomes stale if something modifies it
    // like an outside dispatch
    for (let key of Object.keys(store.state.contracts)) {
      await backend.subscribe(key)
      await dispatch('syncContractWithServer', key)
    }
    Vue.events.$emit('login', user)
  },
  async logout (
    {dispatch, commit, state}: {dispatch: Function, commit: Function, state: Object},
  ) {
    await dispatch('saveSettings')
    await db.clearCurrentUser()
    for (let key of Object.keys(state.contracts)) {
      await backend.unsubscribe(key)
    }
    commit('logout')
    Vue.events.$emit('logout')
  },
  deleteMessage ({dispatch, commit}, hash) {
    commit('deleteMessage', hash)
    debouncedSave(dispatch)
  },
  markMessageAsRead ({dispatch, commit}, hash) {
    commit('markMessageAsRead', hash)
    debouncedSave(dispatch)
  },
  // this function is called from ./pubsub.js and is the entry point
  // for getting events into the log.
  // mirrors `handleEvent` in backend/server.js
  async handleEvent (
    {dispatch, commit, state}: {dispatch: Function, commit: Function, state: Object},
    {contractId, hash, entry}: {contractId: string, hash: string, entry: Object}
  ) {
    // verify we're expecting to hear from this contract
    if (state.pending.indexOf(contractId) === -1 && !state.contracts[contractId]) {
      // TODO: use a global notification system to both display a notification
      //       and throw an exception and write a log message.
      return console.error(`NOT EXPECTING EVENT!`, contractId, entry)
    }

    const type = entry.type
    entry = Events[type].fromObject(entry, hash)
    if (entry instanceof Events.HashableContract) {
      console.log(`handleEvent for new ${type}:`, entry)
      if (entry.toObject().parentHash) {
        // TODO: use a global notification object to handle this and all other errors
        return console.error(`${type} has non-null parentHash!`, entry)
      }
      await db.addLogEntry(contractId, entry)
      commit('addContract', {contractId, type, data: entry.toVuexState()})
    } else if (entry instanceof Events.HashableAction) {
      const contractType = state.contracts[contractId]
      console.log(`handleEvent for ${type} on ${contractType}:`, entry)
      if (!Events[contractType].isActionAllowed(state[contractType], entry)) {
        // TODO: implement isActionAllowed in all actions, and handle error better
        return console.error(`bad action ${type} on ${contractType} (${contractId}):`, entry)
      }
      // TODO: verify each entry is signed by a group member
      let hash = await db.addLogEntry(contractId, entry)
      // If this is a duplicate the hash will be null
      // This may occur if we get duplicate events over the network
      if (!hash) { return }
      commit(`${contractId}/${type}`, entry.data)

      // NOTE: this is to support EventLog.vue + TimeTravel.vue
      //       it's not super important and we'll probably get rid of it later
      if (contractId === state.currentGroupId) {
        commit('setPosition', hash)
      }
    } else {
      return console.error(`UNKNOWN EVENT TYPE!`, contractId, entry)
    }
    // handleEvent might be called very frequently, so save only after a pause
    debouncedSave(dispatch)
    // let any listening components know that we've received, processed, and stored the event
    Vue.events.$emit(hash, contractId, entry)
    Vue.events.$emit('eventHandled', contractId, entry)
  },

  // persisting the state
  async saveSettings (
    {state}: {state: Object}
  ) {
    // TODO: encrypt these
    const settings = {
      position: state.position,
      currentGroupId: state.currentGroupId,
      contracts: Object.keys(state.contracts).map(contractId => ({
        contractId,
        type: state.contracts[contractId],
        data: state[contractId]
      }))
    }
    await db.saveSettings(state.loggedIn, settings)
    console.log('saveSettings:', settings)
  },
  async loadSettings (
    {commit, state}: {commit: Function, state: Object}
  ) {
    const settings = await db.loadSettings(state.loggedIn)
    if (settings) {
      console.log('loadSettings:', settings)
      commit('setCurrentGroupId', settings.currentGroupId)
      commit('setContracts', settings.contracts || [])
    }
  }
}

const debouncedSave = debounce(dispatch => dispatch('saveSettings'), 500)

store = new Vuex.Store({state, mutations, getters, actions})
export default store
// This will build the current contract state from applying all its actions
export async function latestContractState (contractId) {
  let events = await backend.eventsSince(contractId, contractId)
  let [contract, ...actions] = events.map(e => {
    return Events[e.entry.type].fromObject(e.entry, e.hash)
  })
  let state = contract.toVuexState()
  actions.forEach(action => {
    let type = action.constructor.name
    contract.constructor.vuex.mutations[type](state, action.data)
  })
  return state
}
<commit_msg>Convert indexOf logic to includes.<commit_after>'use strict'

// This file handles application-level state (as opposed to component-level
// state) per: http://vuex.vuejs.org/en/intro.html

import Vue from 'vue'
import Vuex from 'vuex'
import * as db from './database'
import * as Events from '../../../shared/events'
import backend from '../js/backend'
// babel transforms lodash imports: https://github.com/lodash/babel-plugin-lodash#faq
// for diff between 'lodash/map' and 'lodash/fp/map'
// see: https://github.com/lodash/lodash/wiki/FP-Guide
import debounce from 'lodash/debounce'

Vue.use(Vuex)
var store // this is set and made the default export at the bottom of the file.
          // we have it declared here to make it accessible in mutations
// 'state' is the Vuex state object
// NOTE: THE STATE CAN ONLY STORE *SERIALIZABLE* OBJECTS! THAT MEANS IF YOU TRY
//       TO STORE AN INSTANCE OF A CLASS (LIKE A CONTRACT), IT WILL NOT STORE
//       THE ACTUAL CONTRACT, BUT JSON.STRINGIFY(CONTRACT) INSTEAD!
const state = {
  position: null, // TODO: get rid of this?
  currentGroupId: null,
  contracts: {}, // contractIds => type (for contracts we've successfully subscribed to)
  pending: [], // contractIds we've just published but haven't received back yet
  loggedIn: false // TODO: properly handle this
}

// Mutations must be synchronous! Never call these directly!
// http://vuex.vuejs.org/en/mutations.html
const mutations = {
  login (state, user) {
    state.loggedIn = user
  },
  logout (state) {
    state.loggedIn = false
  },
  addContract (state, {contractId, type, data}) {
    // "Mutations Follow Vue's Reactivity Rules" - important for modifying objects
    // See: https://vuex.vuejs.org/en/mutations.html
    Vue.set(state.contracts, contractId, type)
    store.registerModule(contractId, {...Events[type].vuex, ...{state: data}})
    // we've successfully received it back, so remove it from expectation pending
    const index = state.pending.indexOf(contractId)
    state.pending.includes(contractId) && state.pending.splice(index, 1)
  },
  removeContract (state, contractId) {
    store.unregisterModule(contractId)
    Vue.delete(state.contracts, contractId)
  },
  setContracts (state, contracts) {
    for (let contract of contracts) {
      mutations.addContract(state, contract)
    }
  },
  deleteMessage (state, hash) {
    let mailboxContract = store.getters.mailboxContract
    let index = mailboxContract && mailboxContract.messages.findIndex(msg => msg.hash === hash)
    if (index > -1) { mailboxContract.messages.splice(index, 1) }
  },
  markMessageAsRead (state, hash) {
    let mailboxContract = store.getters.mailboxContract
    let index = mailboxContract && mailboxContract.messages.findIndex(msg => msg.hash === hash)
    if (index > -1) { mailboxContract.messages[index].read = true }
  },
  setCurrentGroupId (state, currentGroupId) {
    state.currentGroupId = currentGroupId
    state.position = currentGroupId
  },
  setPosition (state, position) {
    state.position = position
  },
  pending (state, contractId) {
    if (!state.contracts[contractId]) {
      !state.pending.includes(contractId) && state.pending.push(contractId)
    }
  }
}
// https://vuex.vuejs.org/en/getters.html
// https://vuex.vuejs.org/en/modules.html
const getters = {
  currentGroup (state) {
    return state[state.currentGroupId]
  },
  mailboxContract (state) {
    // TODO: If we a user is ever subscribed to multiple mailboxes, this will have to be rewritten
    for (let [key, value] of Object.entries(state.contracts)) {
      if (value === 'MailboxContract') {
        return state[key]
      }
    }
    return null
  },
  mailbox (state) {
    let mailboxContract = store.getters.mailboxContract
    return mailboxContract && mailboxContract.messages
  },
  unreadMessageCount (state) {
    let mailboxContract = store.getters.mailboxContract
    return mailboxContract && mailboxContract.messages.filter((msg) => !msg.read).length
  }
}

const actions = {
  // Used to update contracts to the current state that the server is aware of
  async syncContractWithServer ({dispatch}: {dispatch: Function}, contractId: string) {
    let latest = await backend.latestHash(contractId)
    let recent = await db.recentHash(contractId)
    if (latest !== recent) {
      // TODO Do we need a since call that is inclusive? Since does not imply inclusion
      let events = await backend.eventsSince(contractId, recent || contractId)
      // remove the first element in cases where we are not getting the contract for the first time
      recent && events.shift()
      for (let i = 0; i < events.length; i++) {
        let event = events[i]
        event.contractId = contractId
        await dispatch('handleEvent', event)
      }
    }
  },
  async login (
    {dispatch, commit, state}: {dispatch: Function, commit: Function, state: Object},
    user: string
  ) {
    commit('login', user)
    await dispatch('loadSettings')
    await db.saveCurrentUser(user)
    // This may seem unintuitive to use the state from the global store object
    // but the state object in scope is a copy that becomes stale if something modifies it
    // like an outside dispatch
    for (let key of Object.keys(store.state.contracts)) {
      await backend.subscribe(key)
      await dispatch('syncContractWithServer', key)
    }
    Vue.events.$emit('login', user)
  },
  async logout (
    {dispatch, commit, state}: {dispatch: Function, commit: Function, state: Object},
  ) {
    await dispatch('saveSettings')
    await db.clearCurrentUser()
    for (let key of Object.keys(state.contracts)) {
      await backend.unsubscribe(key)
    }
    commit('logout')
    Vue.events.$emit('logout')
  },
  deleteMessage ({dispatch, commit}, hash) {
    commit('deleteMessage', hash)
    debouncedSave(dispatch)
  },
  markMessageAsRead ({dispatch, commit}, hash) {
    commit('markMessageAsRead', hash)
    debouncedSave(dispatch)
  },
  // this function is called from ./pubsub.js and is the entry point
  // for getting events into the log.
  // mirrors `handleEvent` in backend/server.js
  async handleEvent (
    {dispatch, commit, state}: {dispatch: Function, commit: Function, state: Object},
    {contractId, hash, entry}: {contractId: string, hash: string, entry: Object}
  ) {
    // verify we're expecting to hear from this contract
    if (!state.pending.includes(contractId) && !state.contracts[contractId]) {
      // TODO: use a global notification system to both display a notification
      //       and throw an exception and write a log message.
      return console.error(`NOT EXPECTING EVENT!`, contractId, entry)
    }

    const type = entry.type
    entry = Events[type].fromObject(entry, hash)
    if (entry instanceof Events.HashableContract) {
      console.log(`handleEvent for new ${type}:`, entry)
      if (entry.toObject().parentHash) {
        // TODO: use a global notification object to handle this and all other errors
        return console.error(`${type} has non-null parentHash!`, entry)
      }
      await db.addLogEntry(contractId, entry)
      commit('addContract', {contractId, type, data: entry.toVuexState()})
    } else if (entry instanceof Events.HashableAction) {
      const contractType = state.contracts[contractId]
      console.log(`handleEvent for ${type} on ${contractType}:`, entry)
      if (!Events[contractType].isActionAllowed(state[contractType], entry)) {
        // TODO: implement isActionAllowed in all actions, and handle error better
        return console.error(`bad action ${type} on ${contractType} (${contractId}):`, entry)
      }
      // TODO: verify each entry is signed by a group member
      let hash = await db.addLogEntry(contractId, entry)
      // If this is a duplicate the hash will be null
      // This may occur if we get duplicate events over the network
      if (!hash) { return }
      commit(`${contractId}/${type}`, entry.data)

      // NOTE: this is to support EventLog.vue + TimeTravel.vue
      //       it's not super important and we'll probably get rid of it later
      if (contractId === state.currentGroupId) {
        commit('setPosition', hash)
      }
    } else {
      return console.error(`UNKNOWN EVENT TYPE!`, contractId, entry)
    }
    // handleEvent might be called very frequently, so save only after a pause
    debouncedSave(dispatch)
    // let any listening components know that we've received, processed, and stored the event
    Vue.events.$emit(hash, contractId, entry)
    Vue.events.$emit('eventHandled', contractId, entry)
  },

  // persisting the state
  async saveSettings (
    {state}: {state: Object}
  ) {
    // TODO: encrypt these
    const settings = {
      position: state.position,
      currentGroupId: state.currentGroupId,
      contracts: Object.keys(state.contracts).map(contractId => ({
        contractId,
        type: state.contracts[contractId],
        data: state[contractId]
      }))
    }
    await db.saveSettings(state.loggedIn, settings)
    console.log('saveSettings:', settings)
  },
  async loadSettings (
    {commit, state}: {commit: Function, state: Object}
  ) {
    const settings = await db.loadSettings(state.loggedIn)
    if (settings) {
      console.log('loadSettings:', settings)
      commit('setCurrentGroupId', settings.currentGroupId)
      commit('setContracts', settings.contracts || [])
    }
  }
}

const debouncedSave = debounce(dispatch => dispatch('saveSettings'), 500)

store = new Vuex.Store({state, mutations, getters, actions})
export default store
// This will build the current contract state from applying all its actions
export async function latestContractState (contractId) {
  let events = await backend.eventsSince(contractId, contractId)
  let [contract, ...actions] = events.map(e => {
    return Events[e.entry.type].fromObject(e.entry, e.hash)
  })
  let state = contract.toVuexState()
  actions.forEach(action => {
    let type = action.constructor.name
    contract.constructor.vuex.mutations[type](state, action.data)
  })
  return state
}
<|endoftext|>