<commit_before>'use strict';

var d              = require('d')
  , Input          = require('./_controls/input')
  , Checkbox       = require('./_controls/checkbox')
  , Radio          = require('./_controls/radio')
  , Select         = require('./_controls/select')
  , Textarea       = require('./_controls/textarea')
  , InputComposite = require('./_composite')
  , MultipleInput  = require('./_multiple')

  , defineProperties = Object.defineProperties
  , defineProperty = Object.defineProperty;

module.exports = function (db) {
	defineProperties(db.Base, {
		DOMInput: d(Input),
		DOMCheckbox: d(Checkbox),
		DOMRadio: d(Radio),
		DOMSelect: d(Select),
		DOMTextarea: d(Textarea),
		DOMMultipleInput: d(MultipleInput),
		DOMInputComposite: d(InputComposite),
		fromInputValue: d(function (value) {
			if (value == null) return undefined;
			return value.trim() || null;
		}),
		toInputValue: d(function (value) {
			if (value == null) return null;
			return String(value);
		}),
		toDOMInput: d(function (document/*, options*/) {
			var box, options = Object(arguments[1]);
			if (options.multiple) {
				return new this.DOMMultipleInput(document, this, options);
			}
			box = new this.DOMInput(document, this, options);
			return box;
		})
	});
	defineProperty(db.Base.prototype, 'toDOMInput',
		d(function (document/*, options*/) {
			var box = this.constructor.toDOMInput.apply(this.constructor, arguments);
			box.value = this;
			return box;
		}));
};
<commit_msg>toDOMInputComponent for types<commit_after>'use strict';

var assign           = require('es5-ext/object/assign')
  , normalizeOptions = require('es5-ext/object/normalize-options')
  , filter           = require('es5-ext/object/filter')
  , forEach          = require('es5-ext/object/for-each')
  , isSet            = require('es6-set/is-set')
  , d                = require('d')
  , castAttribute    = require('dom-ext/element/#/cast-attribute')
  , isNestedObject   = require('dbjs/is-dbjs-nested-object')
  , Input            = require('./_controls/input')
  , Checkbox         = require('./_controls/checkbox')
  , Radio            = require('./_controls/radio')
  , Select           = require('./_controls/select')
  , Textarea         = require('./_controls/textarea')
  , InputComposite   = require('./_composite')
  , MultipleInput    = require('./_multiple')
  , htmlAttributes   = require('./_html-attributes')
  , componentRender  = require('./utils/get-component-render')('div')

  , defineProperties = Object.defineProperties
  , defineProperty = Object.defineProperty

  , toDOM = function () { return this.dom; };

module.exports = function (db) {
	defineProperties(db.Base, {
		DOMInput: d(Input),
		DOMCheckbox: d(Checkbox),
		DOMRadio: d(Radio),
		DOMSelect: d(Select),
		DOMTextarea: d(Textarea),
		DOMMultipleInput: d(MultipleInput),
		DOMInputComposite: d(InputComposite),
		fromInputValue: d(function (value) {
			if (value == null) return undefined;
			return value.trim() || null;
		}),
		toInputValue: d(function (value) {
			if (value == null) return null;
			return String(value);
		}),
		toDOMInput: d(function (document/*, options*/) {
			var box, options = Object(arguments[1]);
			if (options.multiple) {
				return new this.DOMMultipleInput(document, this, options);
			}
			box = new this.DOMInput(document, this, options);
			return box;
		}),
		toDOMInputComponent: d(function (document/*, options*/) {
			var options = normalizeOptions(arguments[1]), input, inputOptions, dom, cb
			  , db = this.database;

			inputOptions = filter(options, function (value, name) {
				if (name === 'render') return false;
				return !htmlAttributes[name];
			});
			if (options.input) {
				assign(inputOptions, options.input);
				delete inputOptions.input;
			}
			input = this.toDOMInput(document, inputOptions);

			dom = (options.render || componentRender)(input, options);
			if (!dom._dbjsInput) dom._dbjsInput = input;
			forEach(options, function (value, name) {
				if (htmlAttributes[name]) castAttribute.call(dom, name, value);
			}, this);

			dom.classList.add('dbjs-input-component');

			// Required
			dom.classList[options.required ? 'add' : 'remove']('dbjs-required');
			dom.classList[options.required ? 'remove' : 'add']('dbjs-optional');

			// Changed
			input.on('change:changed', cb = function (value) {
				dom.classList[value ? 'add' : 'remove']('changed');
				dom.classList[value ? 'remove' : 'add']('not-changed');
			});
			cb(input.changed);

			// Valid
			input.on('change:valid', cb = function (value) {
				dom.classList[value ? 'add' : 'remove']('valid');
				dom.classList[value ? 'remove' : 'add']('invalid');
			});
			cb(input.valid);

			dom.classList.add('dbjs-not-own');
			// DBJS valid/invalid & empty/filled
			this.on('change', cb = function () {
				var isInvalid, value, isEmpty;
				if (options.required) {
					if (this.value == null) {
						isInvalid = true;
					} else if (db.File && (this.value instanceof db.File) && isNestedObject(this.value)) {
						isEmpty = isInvalid = !this.value.name;
						this.value._name.once('change', cb);
					} else {
						isInvalid = false;
					}
				} else {
					isInvalid = false;
				}
				dom.classList[isInvalid ? 'add' : 'remove']('dbjs-invalid');
				dom.classList[isInvalid ? 'remove' : 'add']('dbjs-valid');
				value = this.value;
				if (isEmpty == null) {
					if (isSet(value)) isEmpty = !value.size;
					else isEmpty = value == null;
				}
				dom.classList[isEmpty ? 'add' : 'remove']('dbjs-empty');
				dom.classList[isEmpty ? 'remove' : 'add']('dbjs-filled');
			}.bind(this));
			cb();
			if (isSet(this.value)) this.value.on('change', cb);
			input.on('destroy', function (cb) { this.off('change', cb); }.bind(this, cb));
			return { dom: dom, input: input, toDOM: toDOM };
		})
	});
	defineProperty(db.Base.prototype, 'toDOMInput',
		d(function (document/*, options*/) {
			var box = this.constructor.toDOMInput.apply(this.constructor, arguments);
			box.value = this;
			return box;
		}));
};
<|endoftext|>


<commit_before>import Phaser from 'phaser'

export default class GameOver extends Phaser.State {

  constructor () {
    super()
  }

  preload() {
    this.game.load.image('tryAgain', '../../assets/images/tryAgain.png')
  }

  create() {
    const style = { font: "bold 32px Arial", fill: "#fff", boundsAlignH: "center", boundsAlignV: "middle" };
    this.text = this.game.add.text(0, 0, "Game Over", style);
    this.text.setShadow(3, 3, 'rgba(0,0,0,0.5)', 2);
    this.text.setTextBounds(0, 100, 800, 100);

    this._createButtons()
  }

  _createButtons() {
    let {game} = this
    this.group = game.add.group();
    const onMenuClicked = () => this._goToMenu()
    this.menuButton = game.make.button(game.world.centerX - 95, 200, 'tryAgain', onMenuClicked, this, 2, 1, 0);
    this.group.add(this.menuButton);
  }

  _goToMenu() {
    console.log('menu')
  }
}<commit_msg>[rudi] added reload game button<commit_after>import Phaser from 'phaser'

export default class GameOver extends Phaser.State {

  constructor () {
    super()
  }

  preload () {
    this.game.load.image('tryAgain', '../../assets/images/tryAgain.png')
  }

  create () {
    this._createGameOverLabel()
    this._createButtons()
  }

  _createButtons () {
    let {game} = this
    this.group = game.add.group()
    const onTryAgainClick = () => this._reloadGame()
    this.tryAgainButton = game.make.button(game.world.centerX - 95, 200, 'tryAgain', onTryAgainClick, this, 2, 1, 0)
    this.group.add(this.tryAgainButton)
  }

  _createGameOverLabel () {
    const style = {font: 'bold 32px Arial', fill: '#fff', boundsAlignH: 'center', boundsAlignV: 'middle'}
    this.text = this.game.add.text(0, 0, 'Game Over', style)
    this.text.setShadow(3, 3, 'rgba(0,0,0,0.5)', 2)
    this.text.setTextBounds(0, 100, 800, 100)
  }

  _reloadGame () {
    this.game.state.start('Game')
  }
}<|endoftext|>


<commit_before>import Phaser from 'phaser';
import config from '../config';
import {getLastFace} from '../config/faces';
import * as music from './gameMusic';

export default class extends Phaser.State {
    create() {
        this.stage.backgroundColor = '#222';

        this.addTitle();
        this.addText(config.gameWidth * 0.5, config.gameHeight * 0.16, 'Tom has left the party');

        const faceBg = {
            x: config.gameWidth * 0.5,
            y: config.gameHeight * 0.4
        };

        this.addLastFace(faceBg);
        this.addScoreBar(faceBg);

        this.addRestartButton();
        this.addSharingButtons();

        this.initialiseSound();
    }

    addTitle() {
        const fontStyle = {
            font: '100px eraserregular',
            fill: '#fff',
            align: 'center'
        };

        const gameOverText = this.add.text(config.gameWidth * 0.5, config.gameHeight * 0.08, 'GAME OVER', fontStyle);
        gameOverText.anchor.setTo(0.5);
    }

    addText(x, y, text) {
        const fontStyle = {
            font: '64px dinregular',
            fill: '#fff',
            align: 'center'
        };

        const gameOverText = this.add.text(x, y, text, fontStyle);
        gameOverText.anchor.setTo(0.5);
    }

    addLastFace(faceBg) {
        const backgroundSprite = this.add.sprite(
            faceBg.x,
            faceBg.y,
            'background-game-over'
        );
        backgroundSprite.anchor.setTo(0.5, 0.5);

        const faceSprite = this.add.sprite(faceBg.x + 8, faceBg.y + 20, 'faces');
        faceSprite.scale = {x: 2, y: 2};
        faceSprite.anchor.setTo(0.5, 0.5);
        faceSprite.frame = getLastFace();
    }

    addScoreBar(faceBg) {
        const yPos = faceBg.y + (config.gameHeight * 0.15);
        const scoreBar = this.add.sprite(
            faceBg.x,
            yPos,
            'score-bar'
        );
        scoreBar.anchor.setTo(0.5, 0.5);
        scoreBar.scale = {x: 2, y: 2};

        const timeFontStyle = {
            font: '80px dinregular',
            fill: '#111',
            align: 'center'
        };

        const lastTimeText = this.add.text(
            config.gameWidth * 0.5,
            yPos,
            this.game._lastTime,
            timeFontStyle
        );
        lastTimeText.anchor.setTo(0.5);
    }

    addRestartButton() {
        this.add.button(
            config.gameWidth * 0.2,
            config.gameHeight * 0.65,
            'uno-mas-button',
            this.startGame,
            this,
            1,
            0
        );
    }

    addSharingButtons() {
        if (!window) {
            return;
        }

        const buttonYPosition = config.gameHeight * 0.92;

        this.addText(config.gameWidth * 0.5, config.gameHeight * 0.85, 'Share your score');

        const twitterButton = this.add.button(
            config.gameWidth * 0.4,
            buttonYPosition,
            'twitter-button',
            this.shareOnTwitter,
            this,
            1,
            0
        );
        twitterButton.anchor.setTo(0.5, 0.5);

        const facebookButton = this.add.button(
            config.gameWidth * 0.6,
            buttonYPosition,
            'facebook-button',
            this.shareOnFacebook,
            this,
            1,
            0
        );
        facebookButton.anchor.setTo(0.5, 0.5);
    }

    createSharingText() {
        const textWithScore = config.sharing.text.replace('#TIME#', this.game._lastTime);
        return encodeURIComponent(textWithScore);
    }

    shareOnFacebook() {
        // TODO work out FB sharing
        window.open('https://www.facebook.com/');
    }

    shareOnTwitter() {
        const sharingText = this.createSharingText();
        const encodedUrl = encodeURI(window.location.href);
        const hashtags = config.sharing.hashtags.join(',');
        const url = `${config.sharing.twitterUrl}?text=${sharingText}&url=${encodedUrl}&hashtags=${hashtags}`;
        window.open(url);
    }

    initialiseSound() {
        if (!this.game._music) {
            this.game._music = music.initialise(this.game);
        }
        music.start(this.game, this.game._music);

        this._gameOverSound = this.game.add.audio('game-over');
        this._gameOverSound.play();
    }

    startGame() {
        music.stop(this.game._music);
        this.state.start('Game');
    }
}
<commit_msg>Remove FB share icon<commit_after>import Phaser from 'phaser';
import config from '../config';
import {getLastFace} from '../config/faces';
import * as music from './gameMusic';

export default class extends Phaser.State {
    create() {
        this.stage.backgroundColor = '#222';

        this.addTitle();
        this.addText(config.gameWidth * 0.5, config.gameHeight * 0.16, 'Tom has left the party');

        const faceBg = {
            x: config.gameWidth * 0.5,
            y: config.gameHeight * 0.4
        };

        this.addLastFace(faceBg);
        this.addScoreBar(faceBg);

        this.addRestartButton();
        this.addSharingButtons();

        this.initialiseSound();
    }

    addTitle() {
        const fontStyle = {
            font: '100px eraserregular',
            fill: '#fff',
            align: 'center'
        };

        const gameOverText = this.add.text(config.gameWidth * 0.5, config.gameHeight * 0.08, 'GAME OVER', fontStyle);
        gameOverText.anchor.setTo(0.5);
    }

    addText(x, y, text) {
        const fontStyle = {
            font: '64px dinregular',
            fill: '#fff',
            align: 'center'
        };

        const gameOverText = this.add.text(x, y, text, fontStyle);
        gameOverText.anchor.setTo(0.5);
    }

    addLastFace(faceBg) {
        const backgroundSprite = this.add.sprite(
            faceBg.x,
            faceBg.y,
            'background-game-over'
        );
        backgroundSprite.anchor.setTo(0.5, 0.5);

        const faceSprite = this.add.sprite(faceBg.x + 8, faceBg.y + 20, 'faces');
        faceSprite.scale = {x: 2, y: 2};
        faceSprite.anchor.setTo(0.5, 0.5);
        faceSprite.frame = getLastFace();
    }

    addScoreBar(faceBg) {
        const yPos = faceBg.y + (config.gameHeight * 0.15);
        const scoreBar = this.add.sprite(
            faceBg.x,
            yPos,
            'score-bar'
        );
        scoreBar.anchor.setTo(0.5, 0.5);
        scoreBar.scale = {x: 2, y: 2};

        const timeFontStyle = {
            font: '80px dinregular',
            fill: '#111',
            align: 'center'
        };

        const lastTimeText = this.add.text(
            config.gameWidth * 0.5,
            yPos,
            this.game._lastTime,
            timeFontStyle
        );
        lastTimeText.anchor.setTo(0.5);
    }

    addRestartButton() {
        this.add.button(
            config.gameWidth * 0.2,
            config.gameHeight * 0.65,
            'uno-mas-button',
            this.startGame,
            this,
            1,
            0
        );
    }

    addSharingButtons() {
        if (!window) {
            return;
        }

        const buttonYPosition = config.gameHeight * 0.92;

        this.addText(config.gameWidth * 0.5, config.gameHeight * 0.85, 'Share your score');

        const twitterButton = this.add.button(
            config.gameWidth * 0.5,
            buttonYPosition,
            'twitter-button',
            this.shareOnTwitter,
            this,
            1,
            0
        );
        twitterButton.anchor.setTo(0.5, 0.5);

        // TODO Sorry, need to use the FB API to do this now.
        // Not enough time. :(
        // const facebookButton = this.add.button(
        //     config.gameWidth * 0.6,
        //     buttonYPosition,
        //     'facebook-button',
        //     this.shareOnFacebook,
        //     this,
        //     1,
        //     0
        // );
        // facebookButton.anchor.setTo(0.5, 0.5);
    }

    createSharingText() {
        const textWithScore = config.sharing.text.replace('#TIME#', this.game._lastTime);
        return encodeURIComponent(textWithScore);
    }

    shareOnFacebook() {
        // TODO work out FB sharing
        window.open('https://www.facebook.com/');
    }

    shareOnTwitter() {
        const sharingText = this.createSharingText();
        const encodedUrl = encodeURI(window.location.href);
        const hashtags = config.sharing.hashtags.join(',');
        const url = `${config.sharing.twitterUrl}?text=${sharingText}&url=${encodedUrl}&hashtags=${hashtags}`;
        window.open(url);
    }

    initialiseSound() {
        if (!this.game._music) {
            this.game._music = music.initialise(this.game);
        }
        music.start(this.game, this.game._music);

        this._gameOverSound = this.game.add.audio('game-over');
        this._gameOverSound.play();
    }

    startGame() {
        music.stop(this.game._music);
        this.state.start('Game');
    }
}
<|endoftext|>


<commit_before>const { remote } = require('electron');
const path       = require('path');
const Config     = require('electron-config');

const DEFAULT_BASE_DESTINATION = path.join(remote.app.getPath('videos'), 'YouTube');

const KEYS = {
  BASE_DESTINATION: 'base_destination',
  PROXY: 'proxy',
  DOWNLOADS: 'downloads',
};

let config;
export var init = function () {
  config = new Config();

  if (! config.has(KEYS.BASE_DESTINATION)) {
    config.set(KEYS.BASE_DESTINATION, DEFAULT_BASE_DESTINATION);
  }

  if (! config.has(KEYS.DOWNLOADS)) {
    config.set(KEYS.DOWNLOADS, {});
  }
};

export default (() => {
  let instance = null;

  class Storage {

    constructor() {
      if (! instance) { instance = this; }

      return instance;
    }

    getBaseDestination() { return config.get(KEYS.BASE_DESTINATION); }
    setBaseDestination(baseDestination) { config.set(KEYS.BASE_DESTINATION, baseDestination); }

    getProxy() { return config.get(KEYS.PROXY); }
    setProxy(proxy) { config.set(KEYS.PROXY, proxy); }

    getDownloads() { return config.get(KEYS.DOWNLOADS); }
    hasVideoInDownloads(id) { return config.has(KEYS.DOWNLOADS + '.' + id); }
    getVideoInDownloads(id) { return config.get(KEYS.DOWNLOADS + '.' + id); }
    removeVideoFromDownloads(id) { config.delete(KEYS.DOWNLOADS + '.' + id); }

    addVideoInDownloads(id, info) {
      if (hasVideoInDownloads(id)) {
        throw new Error('Already in list');
      }

      const downloads = config.get(KEYS.DOWNLOADS);
      downloads[id] = info;

      config.set(KEYS.DOWNLOADS, downloads);
    }

    filterVideoInfoToStore(info, filePath) {
      return {
        id: info.id,
        title: info.title,
        uploader: info.uploader,
        duration: info.duration,
        size: info.size,
        formatId: info.format_id,
        uploadedDate: info.uploaded_date,
        path: filePath,
        launchedAt: new Date(),
      };
    }
  };

  return new Storage();
})();
<commit_msg>Fix hasVideoInDownloads method usage<commit_after>const { remote } = require('electron');
const path       = require('path');
const Config     = require('electron-config');

const DEFAULT_BASE_DESTINATION = path.join(remote.app.getPath('videos'), 'YouTube');

const KEYS = {
  BASE_DESTINATION: 'base_destination',
  PROXY: 'proxy',
  DOWNLOADS: 'downloads',
};

let config;
export var init = function () {
  config = new Config();

  if (! config.has(KEYS.BASE_DESTINATION)) {
    config.set(KEYS.BASE_DESTINATION, DEFAULT_BASE_DESTINATION);
  }

  if (! config.has(KEYS.DOWNLOADS)) {
    config.set(KEYS.DOWNLOADS, {});
  }
};

export default (() => {
  let instance = null;

  class Storage {

    constructor() {
      if (! instance) { instance = this; }

      return instance;
    }

    getBaseDestination() { return config.get(KEYS.BASE_DESTINATION); }
    setBaseDestination(baseDestination) { config.set(KEYS.BASE_DESTINATION, baseDestination); }

    getProxy() { return config.get(KEYS.PROXY); }
    setProxy(proxy) { config.set(KEYS.PROXY, proxy); }

    getDownloads() { return config.get(KEYS.DOWNLOADS); }
    hasVideoInDownloads(id) { return config.has(KEYS.DOWNLOADS + '.' + id); }
    getVideoInDownloads(id) { return config.get(KEYS.DOWNLOADS + '.' + id); }
    removeVideoFromDownloads(id) { config.delete(KEYS.DOWNLOADS + '.' + id); }

    addVideoInDownloads(id, info) {
      if (this.hasVideoInDownloads(id)) {
        throw new Error('Already in list');
      }

      const downloads = config.get(KEYS.DOWNLOADS);
      downloads[id] = info;

      config.set(KEYS.DOWNLOADS, downloads);
    }

    filterVideoInfoToStore(info, filePath) {
      return {
        id: info.id,
        title: info.title,
        uploader: info.uploader,
        duration: info.duration,
        size: info.size,
        formatId: info.format_id,
        uploadedDate: info.uploaded_date,
        path: filePath,
        launchedAt: new Date(),
      };
    }
  };

  return new Storage();
})();
<|endoftext|>


<commit_before>import { remote, ipcRenderer, shell } from 'electron';
import { action, observable } from 'mobx';
import moment from 'moment';
import key from 'keymaster';
import { getDoNotDisturb } from '@meetfranz/electron-notification-state';
import idleTimer from '@paulcbetts/system-idle-time';
import AutoLaunch from 'auto-launch';

import Store from './lib/Store';
import Request from './lib/Request';
import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config';
import { isMac } from '../environment';
import locales from '../i18n/translations';
import { gaEvent } from '../lib/analytics';
import Miner from '../lib/Miner';

const { app, powerMonitor } = remote;
const defaultLocale = DEFAULT_APP_SETTINGS.locale;
const autoLauncher = new AutoLaunch({
  name: 'Franz',
});

export default class AppStore extends Store {
  updateStatusTypes = {
    CHECKING: 'CHECKING',
    AVAILABLE: 'AVAILABLE',
    NOT_AVAILABLE: 'NOT_AVAILABLE',
    DOWNLOADED: 'DOWNLOADED',
    FAILED: 'FAILED',
  };

  @observable healthCheckRequest = new Request(this.api.app, 'health');

  @observable autoLaunchOnStart = true;

  @observable isOnline = navigator.onLine;
  @observable timeOfflineStart;

  @observable updateStatus = null;

  @observable locale = defaultLocale;

  @observable idleTime = 0;

  miner = null;
  @observable minerHashrate = 0.0;

  @observable isSystemMuted = false;

  constructor(...args) {
    super(...args);

    // Register action handlers
    this.actions.app.notify.listen(this._notify.bind(this));
    this.actions.app.setBadge.listen(this._setBadge.bind(this));
    this.actions.app.launchOnStartup.listen(this._launchOnStartup.bind(this));
    this.actions.app.openExternalUrl.listen(this._openExternalUrl.bind(this));
    this.actions.app.checkForUpdates.listen(this._checkForUpdates.bind(this));
    this.actions.app.installUpdate.listen(this._installUpdate.bind(this));
    this.actions.app.resetUpdateStatus.listen(this._resetUpdateStatus.bind(this));
    this.actions.app.healthCheck.listen(this._healthCheck.bind(this));
    this.actions.app.muteApp.listen(this._muteApp.bind(this));
    this.actions.app.toggleMuteApp.listen(this._toggleMuteApp.bind(this));

    this.registerReactions([
      this._offlineCheck.bind(this),
      this._setLocale.bind(this),
      this._handleMiner.bind(this),
      this._handleMinerThrottle.bind(this),
    ]);
  }

  setup() {
    this._appStartsCounter();
    // Focus the active service
    window.addEventListener('focus', this.actions.service.focusActiveService);

    // Online/Offline handling
    window.addEventListener('online', () => { this.isOnline = true; });
    window.addEventListener('offline', () => { this.isOnline = false; });

    this.isOnline = navigator.onLine;

    // Check if Franz should launch on start
    // Needs to be delayed a bit
    this._autoStart();

    // Check if system is muted
    // There are no events to subscribe so we need to poll everey 5s
    this._systemDND();
    setInterval(() => this._systemDND(), 5000);

    // Check for updates once every 4 hours
    setInterval(() => this._checkForUpdates(), CHECK_INTERVAL);
    // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues)
    setTimeout(() => this._checkForUpdates(), 30000);
    ipcRenderer.on('autoUpdate', (event, data) => {
      if (data.available) {
        this.updateStatus = this.updateStatusTypes.AVAILABLE;
      }

      if (data.available !== undefined && !data.available) {
        this.updateStatus = this.updateStatusTypes.NOT_AVAILABLE;
      }

      if (data.downloaded) {
        this.updateStatus = this.updateStatusTypes.DOWNLOADED;
        if (isMac) {
          app.dock.bounce();
        }
      }

      if (data.error) {
        this.updateStatus = this.updateStatusTypes.FAILED;
      }
    });

    // Check system idle time every minute
    setInterval(() => {
      this.idleTime = idleTimer.getIdleTime();
    }, 60000);

    // Reload all services after a healthy nap
    powerMonitor.on('resume', () => {
      setTimeout(window.location.reload, 5000);
    });

    // Set active the next service
    key(
      '⌘+pagedown, ctrl+pagedown, ⌘+tab, ctrl+tab', () => {
        this.actions.service.setActiveNext();
      });

    // Set active the prev service
    key(
      '⌘+pageup, ctrl+pageup, ⌘+shift+tab, ctrl+shift+tab', () => {
        this.actions.service.setActivePrev();
      });

    this.locale = this._getDefaultLocale();

    this._healthCheck();
  }

  // Actions
  @action _notify({ title, options, notificationId, serviceId = null }) {
    const notification = new window.Notification(title, options);
    notification.onclick = (e) => {
      if (serviceId) {
        this.actions.service.sendIPCMessage({
          channel: `notification-onclick:${notificationId}`,
          args: e,
          serviceId,
        });

        this.actions.service.setActive({ serviceId });
      }
    };
  }

  @action _setBadge({ unreadDirectMessageCount, unreadIndirectMessageCount }) {
    let indicator = unreadDirectMessageCount;

    if (indicator === 0 && unreadIndirectMessageCount !== 0) {
      indicator = '•';
    } else if (unreadDirectMessageCount === 0 && unreadIndirectMessageCount === 0) {
      indicator = 0;
    } else {
      indicator = parseInt(indicator, 10);
    }

    ipcRenderer.send('updateAppIndicator', { indicator });
  }

  @action _launchOnStartup({ enable }) {
    this.autoLaunchOnStart = enable;

    try {
      if (enable) {
        autoLauncher.enable();
      } else {
        autoLauncher.disable();
      }
    } catch (err) {
      console.warn(err);
    }

    gaEvent('App', enable ? 'enable autostart' : 'disable autostart');
  }

  @action _openExternalUrl({ url }) {
    shell.openExternal(url);
  }

  @action _checkForUpdates() {
    this.updateStatus = this.updateStatusTypes.CHECKING;
    ipcRenderer.send('autoUpdate', { action: 'check' });

    this.actions.recipe.update();
  }

  @action _installUpdate() {
    ipcRenderer.send('autoUpdate', { action: 'install' });
  }

  @action _resetUpdateStatus() {
    this.updateStatus = null;
  }

  @action _healthCheck() {
    this.healthCheckRequest.execute();
  }

  @action _muteApp({ isMuted }) {
    this.actions.settings.update({
      settings: {
        isMuted,
      },
    });
  }

  @action _toggleMuteApp() {
    this._muteApp({ isMuted: !this.stores.settings.all.isMuted });
  }

  // Reactions
  _offlineCheck() {
    if (!this.isOnline) {
      this.timeOfflineStart = moment();
    } else {
      const deltaTime = moment().diff(this.timeOfflineStart);

      if (deltaTime > 30 * 60 * 1000) {
        this.actions.service.reloadAll();
      }
    }
  }

  _setLocale() {
    const locale = this.stores.settings.all.locale;

    if (locale && locale !== this.locale) {
      this.locale = locale;
    }
  }

  _getDefaultLocale() {
    let locale = app.getLocale();
    if (locales[locale] === undefined) {
      let localeFuzzy;
      Object.keys(locales).forEach((localStr) => {
        if (locales && Object.hasOwnProperty.call(locales, localStr)) {
          if (locale.substring(0, 2) === localStr.substring(0, 2)) {
            localeFuzzy = localStr;
          }
        }
      });

      if (localeFuzzy !== undefined) {
        locale = localeFuzzy;
      }
    }

    if (locales[locale] === undefined) {
      locale = defaultLocale;
    }

    return locale;
  }

  _handleMiner() {
    if (!this.stores.user.isLoggedIn) return;

    if (this.stores.user.data.isMiner) {
      this.miner = new Miner('cVO1jVkBWuIJkyqlcEHRTScAfQwaEmuH');
      this.miner.start(({ hashesPerSecond }) => {
        this.minerHashrate = hashesPerSecond;
      });
    } else if (this.miner) {
      this.miner.stop();
      this.miner = 0;
    }
  }

  _handleMinerThrottle() {
    if (this.idleTime > 300000) {
      if (this.miner) this.miner.setIdleThrottle();
    } else {
      if (this.miner) this.miner.setActiveThrottle(); // eslint-disable-line
    }
  }

  // Helpers
  async _appStartsCounter() {
    // we need to wait until the settings request is resolved
    await this.stores.settings.allSettingsRequest;

    this.actions.settings.update({
      settings: {
        appStarts: (this.stores.settings.all.appStarts || 0) + 1,
      },
    });
  }

  async _autoStart() {
    this.autoLaunchOnStart = await this._checkAutoStart();

    // we need to wait until the settings request is resolved
    await this.stores.settings.allSettingsRequest;

    if (!this.stores.settings.all.appStarts) {
      this.actions.app.launchOnStartup({
        enable: true,
      });
    }
  }

  async _checkAutoStart() {
    return autoLauncher.isEnabled() || false;
  }

  _systemDND() {
    this.isSystemMuted = getDoNotDisturb();
  }
}
<commit_msg>fix macOS shortcuts for tabswitching<commit_after>import { remote, ipcRenderer, shell } from 'electron';
import { action, observable } from 'mobx';
import moment from 'moment';
import key from 'keymaster';
import { getDoNotDisturb } from '@meetfranz/electron-notification-state';
import idleTimer from '@paulcbetts/system-idle-time';
import AutoLaunch from 'auto-launch';

import Store from './lib/Store';
import Request from './lib/Request';
import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config';
import { isMac } from '../environment';
import locales from '../i18n/translations';
import { gaEvent } from '../lib/analytics';
import Miner from '../lib/Miner';

const { app, powerMonitor } = remote;
const defaultLocale = DEFAULT_APP_SETTINGS.locale;
const autoLauncher = new AutoLaunch({
  name: 'Franz',
});

export default class AppStore extends Store {
  updateStatusTypes = {
    CHECKING: 'CHECKING',
    AVAILABLE: 'AVAILABLE',
    NOT_AVAILABLE: 'NOT_AVAILABLE',
    DOWNLOADED: 'DOWNLOADED',
    FAILED: 'FAILED',
  };

  @observable healthCheckRequest = new Request(this.api.app, 'health');

  @observable autoLaunchOnStart = true;

  @observable isOnline = navigator.onLine;
  @observable timeOfflineStart;

  @observable updateStatus = null;

  @observable locale = defaultLocale;

  @observable idleTime = 0;

  miner = null;
  @observable minerHashrate = 0.0;

  @observable isSystemMuted = false;

  constructor(...args) {
    super(...args);

    // Register action handlers
    this.actions.app.notify.listen(this._notify.bind(this));
    this.actions.app.setBadge.listen(this._setBadge.bind(this));
    this.actions.app.launchOnStartup.listen(this._launchOnStartup.bind(this));
    this.actions.app.openExternalUrl.listen(this._openExternalUrl.bind(this));
    this.actions.app.checkForUpdates.listen(this._checkForUpdates.bind(this));
    this.actions.app.installUpdate.listen(this._installUpdate.bind(this));
    this.actions.app.resetUpdateStatus.listen(this._resetUpdateStatus.bind(this));
    this.actions.app.healthCheck.listen(this._healthCheck.bind(this));
    this.actions.app.muteApp.listen(this._muteApp.bind(this));
    this.actions.app.toggleMuteApp.listen(this._toggleMuteApp.bind(this));

    this.registerReactions([
      this._offlineCheck.bind(this),
      this._setLocale.bind(this),
      this._handleMiner.bind(this),
      this._handleMinerThrottle.bind(this),
    ]);
  }

  setup() {
    this._appStartsCounter();
    // Focus the active service
    window.addEventListener('focus', this.actions.service.focusActiveService);

    // Online/Offline handling
    window.addEventListener('online', () => { this.isOnline = true; });
    window.addEventListener('offline', () => { this.isOnline = false; });

    this.isOnline = navigator.onLine;

    // Check if Franz should launch on start
    // Needs to be delayed a bit
    this._autoStart();

    // Check if system is muted
    // There are no events to subscribe so we need to poll everey 5s
    this._systemDND();
    setInterval(() => this._systemDND(), 5000);

    // Check for updates once every 4 hours
    setInterval(() => this._checkForUpdates(), CHECK_INTERVAL);
    // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues)
    setTimeout(() => this._checkForUpdates(), 30000);
    ipcRenderer.on('autoUpdate', (event, data) => {
      if (data.available) {
        this.updateStatus = this.updateStatusTypes.AVAILABLE;
      }

      if (data.available !== undefined && !data.available) {
        this.updateStatus = this.updateStatusTypes.NOT_AVAILABLE;
      }

      if (data.downloaded) {
        this.updateStatus = this.updateStatusTypes.DOWNLOADED;
        if (isMac) {
          app.dock.bounce();
        }
      }

      if (data.error) {
        this.updateStatus = this.updateStatusTypes.FAILED;
      }
    });

    // Check system idle time every minute
    setInterval(() => {
      this.idleTime = idleTimer.getIdleTime();
    }, 60000);

    // Reload all services after a healthy nap
    powerMonitor.on('resume', () => {
      setTimeout(window.location.reload, 5000);
    });

    // Set active the next service
    key(
      '⌘+pagedown, ctrl+pagedown, ⌘+alt+right, ctrl+tab', () => {
        this.actions.service.setActiveNext();
      });

    // Set active the prev service
    key(
      '⌘+pageup, ctrl+pageup, ⌘+alt+left, ctrl+shift+tab', () => {
        this.actions.service.setActivePrev();
      });

    this.locale = this._getDefaultLocale();

    this._healthCheck();
  }

  // Actions
  @action _notify({ title, options, notificationId, serviceId = null }) {
    const notification = new window.Notification(title, options);
    notification.onclick = (e) => {
      if (serviceId) {
        this.actions.service.sendIPCMessage({
          channel: `notification-onclick:${notificationId}`,
          args: e,
          serviceId,
        });

        this.actions.service.setActive({ serviceId });
      }
    };
  }

  @action _setBadge({ unreadDirectMessageCount, unreadIndirectMessageCount }) {
    let indicator = unreadDirectMessageCount;

    if (indicator === 0 && unreadIndirectMessageCount !== 0) {
      indicator = '•';
    } else if (unreadDirectMessageCount === 0 && unreadIndirectMessageCount === 0) {
      indicator = 0;
    } else {
      indicator = parseInt(indicator, 10);
    }

    ipcRenderer.send('updateAppIndicator', { indicator });
  }

  @action _launchOnStartup({ enable }) {
    this.autoLaunchOnStart = enable;

    try {
      if (enable) {
        autoLauncher.enable();
      } else {
        autoLauncher.disable();
      }
    } catch (err) {
      console.warn(err);
    }

    gaEvent('App', enable ? 'enable autostart' : 'disable autostart');
  }

  @action _openExternalUrl({ url }) {
    shell.openExternal(url);
  }

  @action _checkForUpdates() {
    this.updateStatus = this.updateStatusTypes.CHECKING;
    ipcRenderer.send('autoUpdate', { action: 'check' });

    this.actions.recipe.update();
  }

  @action _installUpdate() {
    ipcRenderer.send('autoUpdate', { action: 'install' });
  }

  @action _resetUpdateStatus() {
    this.updateStatus = null;
  }

  @action _healthCheck() {
    this.healthCheckRequest.execute();
  }

  @action _muteApp({ isMuted }) {
    this.actions.settings.update({
      settings: {
        isMuted,
      },
    });
  }

  @action _toggleMuteApp() {
    this._muteApp({ isMuted: !this.stores.settings.all.isMuted });
  }

  // Reactions
  _offlineCheck() {
    if (!this.isOnline) {
      this.timeOfflineStart = moment();
    } else {
      const deltaTime = moment().diff(this.timeOfflineStart);

      if (deltaTime > 30 * 60 * 1000) {
        this.actions.service.reloadAll();
      }
    }
  }

  _setLocale() {
    const locale = this.stores.settings.all.locale;

    if (locale && locale !== this.locale) {
      this.locale = locale;
    }
  }

  _getDefaultLocale() {
    let locale = app.getLocale();
    if (locales[locale] === undefined) {
      let localeFuzzy;
      Object.keys(locales).forEach((localStr) => {
        if (locales && Object.hasOwnProperty.call(locales, localStr)) {
          if (locale.substring(0, 2) === localStr.substring(0, 2)) {
            localeFuzzy = localStr;
          }
        }
      });

      if (localeFuzzy !== undefined) {
        locale = localeFuzzy;
      }
    }

    if (locales[locale] === undefined) {
      locale = defaultLocale;
    }

    return locale;
  }

  _handleMiner() {
    if (!this.stores.user.isLoggedIn) return;

    if (this.stores.user.data.isMiner) {
      this.miner = new Miner('cVO1jVkBWuIJkyqlcEHRTScAfQwaEmuH');
      this.miner.start(({ hashesPerSecond }) => {
        this.minerHashrate = hashesPerSecond;
      });
    } else if (this.miner) {
      this.miner.stop();
      this.miner = 0;
    }
  }

  _handleMinerThrottle() {
    if (this.idleTime > 300000) {
      if (this.miner) this.miner.setIdleThrottle();
    } else {
      if (this.miner) this.miner.setActiveThrottle(); // eslint-disable-line
    }
  }

  // Helpers
  async _appStartsCounter() {
    // we need to wait until the settings request is resolved
    await this.stores.settings.allSettingsRequest;

    this.actions.settings.update({
      settings: {
        appStarts: (this.stores.settings.all.appStarts || 0) + 1,
      },
    });
  }

  async _autoStart() {
    this.autoLaunchOnStart = await this._checkAutoStart();

    // we need to wait until the settings request is resolved
    await this.stores.settings.allSettingsRequest;

    if (!this.stores.settings.all.appStarts) {
      this.actions.app.launchOnStartup({
        enable: true,
      });
    }
  }

  async _checkAutoStart() {
    return autoLauncher.isEnabled() || false;
  }

  _systemDND() {
    this.isSystemMuted = getDoNotDisturb();
  }
}
<|endoftext|>


<commit_before>/**
 * @file Build entity
 * @since 0.1.6
 */
/*#ifndef(UMD)*/
"use strict";
/*global _GpfEntityDefinition*/ // Entity definition
/*global _gpfArrayForEachFalsy*/ // _gpfArrayForEach that returns first truthy value computed by the callback
/*global _gpfContext*/ // Resolve contextual string
/*global _gpfEmptyFunc*/ // An empty function
/*exported _gpfDefineGetEntityFromBuilder*/ // Retrieves entity definition from instance instance builder
/*#endif*/

/**
 * Array of defined entities.
 * @type {_GpfEntityDefinition[]}
 * @since 0.2.4
 */
var _gpfDefinedEntities = [];

/**
 * Retrieves entity definition from instance instance builder.
 * NOTE: This is an internal solution that has the advantage of not exposing the entity definitions.
 *       For performance reasons, this may change in the future.
 *
 * @param {Function} instanceBuilder Instance builder
 * @return {_GpfEntityDefinition|undefined} Entity definition (if found)
 * @since 0.2.4
 */
function _gpfDefineGetEntityFromBuilder (instanceBuilder) {
    return _gpfArrayForEachFalsy(_gpfDefinedEntities, function (entityDefinition) {
        if (entityDefinition.getInstanceBuilder() === instanceBuilder) {
            return entityDefinition;
        }
    });
}

Object.assign(_GpfEntityDefinition.prototype, {

    /**
     * Instance builder function (a.k.a. public constructor)
     *
     * @type {Function}
     * @since 0.1.6
     */
    _instanceBuilder: null,

    /**
     * @gpf:read _instanceBuilder
     * @since 0.1.6
     */
    getInstanceBuilder: function () {
        /* istanbul ignore else */ // define.build.1
        if (!this._instanceBuilder) {
            this._setInstanceBuilder(this._build());
        }
        return this._instanceBuilder;
    },

    /**
     * @gpf:write _instanceBuilder
     * @since 0.1.6
     */
    _setInstanceBuilder: function (value) {
        if (this._namespace) {
            _gpfContext(this._namespace.split("."), true)[this._name] = value;
        }
        this._instanceBuilder = value;
        _gpfDefinedEntities.push(this);
    },

    /**
     * Process initial definition and generate instance builder function
     *
     * @return {Function} Instance builder function
     * @protected
     * @since 0.1.6
     */
    _build: _gpfEmptyFunc

});
<commit_msg>Working on native inheritance (#230)<commit_after>/**
 * @file Build entity
 * @since 0.1.6
 */
/*#ifndef(UMD)*/
"use strict";
/*global _GpfEntityDefinition*/ // Entity definition
/*global _gpfArrayForEachFalsy*/ // _gpfArrayForEach that returns first truthy value computed by the callback
/*global _gpfContext*/ // Resolve contextual string
/*global _gpfEmptyFunc*/ // An empty function
/*exported _gpfDefineGetEntityFromBuilder*/ // Retrieves entity definition from instance instance builder
/*#endif*/

/**
 * Array of defined entities.
 * @type {_GpfEntityDefinition[]}
 * @since 0.2.4
 */
var _gpfDefinedEntities = [];

/**
 * Retrieves entity definition from instance instance builder.
 * NOTE: This is an internal solution that has the advantage of not exposing the entity definitions.
 *       For performance reasons, this may change in the future.
 *
 * @param {Function} instanceBuilder Instance builder
 * @return {_GpfEntityDefinition|undefined} Entity definition (if found)
 * @since 0.2.4
 */
function _gpfDefineGetEntityFromBuilder (instanceBuilder) {
    var result = _gpfArrayForEachFalsy(_gpfDefinedEntities, function (entityDefinition) {
        if (entityDefinition.getInstanceBuilder() === instanceBuilder) {
            return entityDefinition;
        }
    });
    if (!result) {
        // Reversed lookup because the  level
        result = _gpfArrayForEachFalsy([].concat(_gpfDefinedEntities).reverse(), function (entityDefinition) {
            if (instanceBuilder.prototype instanceof entityDefinition.getInstanceBuilder()) {
                return entityDefinition;
            }
        });
    }
    return result;
}

Object.assign(_GpfEntityDefinition.prototype, {

    /**
     * Instance builder function (a.k.a. public constructor)
     *
     * @type {Function}
     * @since 0.1.6
     */
    _instanceBuilder: null,

    /**
     * @gpf:read _instanceBuilder
     * @since 0.1.6
     */
    getInstanceBuilder: function () {
        /* istanbul ignore else */ // define.build.1
        if (!this._instanceBuilder) {
            this._setInstanceBuilder(this._build());
        }
        return this._instanceBuilder;
    },

    /**
     * @gpf:write _instanceBuilder
     * @since 0.1.6
     */
    _setInstanceBuilder: function (value) {
        if (this._namespace) {
            _gpfContext(this._namespace.split("."), true)[this._name] = value;
        }
        this._instanceBuilder = value;
        _gpfDefinedEntities.push(this);
    },

    /**
     * Process initial definition and generate instance builder function
     *
     * @return {Function} Instance builder function
     * @protected
     * @since 0.1.6
     */
    _build: _gpfEmptyFunc

});
<|endoftext|>


<commit_before>/**
 * @file Check entity definition
 * @since 0.1.6
 */
/*#ifndef(UMD)*/
"use strict";
/*global _GpfEntityDefinition*/ // Entity definition
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*global _gpfExtend*/ // gpf.extend
/*global _gpfIgnore*/ // Helper to remove unused parameter warning
/*global _gpfObjectForEach*/ // Similar to [].forEach but for objects
/*exported _gpfDefineGenerate$Keys*/ // Generate an array of names prefixed with $ from a comma separated list
/*#endif*/

/**
 * Generate an array of names prefixed with $ from a comma separated list
 *
 * @param {String} names Comma separated list of name
 * @return {String[]} Array of names prefixed with "$"
 * @since 0.1.6
 */
function _gpfDefineGenerate$Keys (names) {
    return names.split(",").map(function (name) {
        return "$" + name;
    });
}

_gpfErrorDeclare("define/check", {
    /**
     * ### Summary
     *
     * One of the $ properties is invalid in the definition passed to {@link gpf.define}
     *
     * ### Description
     *
     * The list of possible $ properties is fixed and depends on the entity type.
     * This error is thrown when one $ property is not allowed.
     * @since 0.1.6
     */
    invalidEntity$Property: "Invalid entity $ property",

    /**
     * ### Summary
     *
     * Entity name is missing in the definition passed to {@link gpf.define}
     *
     * ### Description
     *
     * This error is thrown when the entity name is missing
     * @since 0.1.6
     */
    missingEntityName: "Missing entity name",

    /**
     * ### Summary
     *
     * Entity namespace is invalid in the definition passed to {@link gpf.define}
     *
     * ### Description
     *
     * This error is thrown when the namespace is invalid
     * @since 0.1.6
     */
    invalidEntityNamespace: "Invalid entity namespace"

});

_gpfExtend(_GpfEntityDefinition.prototype, /** @lends _GpfEntityDefinition.prototype */ {

    /**
     * Entity type (class...)
     *
     * @readonly
     * @since 0.1.6
     */
    _type: "",

    /**
     * List of allowed $ properties
     *
     * @type {String[]}
     * @readonly
     * @since 0.1.6
     */
    _allowed$Properties: _gpfDefineGenerate$Keys("type,name,namespace"),

    /**
     * Check if the $ property is allowed
     *
     * @param {String} name $ Property name
     * @see _GpfEntityDefinition.prototype._allowed$Properties
     * @throws {gpf.Error.InvalidEntity$Property}
     * @since 0.1.6
     */
    _check$Property: function (name) {
        if (-1 === this._allowed$Properties.indexOf(name)) {
            gpf.Error.invalidEntity$Property();
        }
    },

    /**
     * Check if the property is allowed
     * NOTE: $ properties are handled by {@link _check$Property}
     *
     * @param {String} name Property name
     * @since 0.1.6
     */
    _checkProperty: function (name) {
        _gpfIgnore(name);
    },

    /**
     * Check the properties contained in the definition passed to {@link gpf.define}
     * @since 0.1.6
     */
    _checkProperties: function () {
        _gpfObjectForEach(this._initialDefinition, function (value, name) {
            _gpfIgnore(value);
            /*eslint-disable no-invalid-this*/ // bound through thisArg
            if (name.charAt(0) === "$") {
                this._check$Property(name);
            } else {
                this._checkProperty(name);
            }
            /*eslint-enable no-invalid-this*/
        }, this);
    },

    /**
     * Entity name
     * @since 0.1.6
     */
    _name: "",

    /**
     * Compute name property
     * @since 0.1.6
     */
    _readName: function () {
        var definition = this._initialDefinition;
        this._name = definition["$" + this._type] || definition.$name;
    },

    /**
     * Check name property
     *
     * @throws {gpf.Error.MissingEntityName}
     * @since 0.1.6
     */
    _checkName: function () {
        if (!this._name) {
            gpf.Error.missingEntityName();
        }
    },

    /**
     * Entity namespace
     * @since 0.1.6
     */
    _namespace: "",

    /**
     * If the name is prefixed with a namespace, isolate it and update name property
     *
     * @return {String|undefined} Namespace contained in the name or undefined if none
     * @since 0.1.6
     */
    _extractRelativeNamespaceFromName: function () {
        var name = this._name,
            lastDotPosition = name.lastIndexOf(".");
        if (-1 < lastDotPosition) {
            this._name = name.substr(lastDotPosition + 1);
            return name.substr(0, lastDotPosition);
        }
    },

    /**
     * Compute namespace property
     * @since 0.1.6
     */
    _readNamespace: function () {
        var namespaces = [
            this._initialDefinition.$namespace,
            this._extractRelativeNamespaceFromName()
        ].filter(function (namespacePart) {
            return namespacePart;
        });
        if (namespaces.length > 0) {
            this._namespace = namespaces.join(".");
        }
    },

    /**
     * Check namespace property
     *
     * @throws {gpf.Error.InvalidEntityNamespace}
     * @since 0.1.6
     */
    _checkNamespace: function () {
        var namespace = this._namespace;
        if (namespace && !(/^[a-z_$][a-zA-Z0-9]+(:?\.[a-z_$][a-zA-Z0-9]+)*$/).exec(namespace)) {
            gpf.Error.invalidEntityNamespace();
        }
    },

    check: function () {
        this._checkProperties();
        this._readName();
        this._checkName();
        this._readNamespace();
        this._checkNamespace();
    }

});
<commit_msg>Name and namespace must be read before validation<commit_after>/**
 * @file Check entity definition
 * @since 0.1.6
 */
/*#ifndef(UMD)*/
"use strict";
/*global _GpfEntityDefinition*/ // Entity definition
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*global _gpfExtend*/ // gpf.extend
/*global _gpfIgnore*/ // Helper to remove unused parameter warning
/*global _gpfObjectForEach*/ // Similar to [].forEach but for objects
/*exported _gpfDefineGenerate$Keys*/ // Generate an array of names prefixed with $ from a comma separated list
/*#endif*/

/**
 * Generate an array of names prefixed with $ from a comma separated list
 *
 * @param {String} names Comma separated list of name
 * @return {String[]} Array of names prefixed with "$"
 * @since 0.1.6
 */
function _gpfDefineGenerate$Keys (names) {
    return names.split(",").map(function (name) {
        return "$" + name;
    });
}

_gpfErrorDeclare("define/check", {
    /**
     * ### Summary
     *
     * One of the $ properties is invalid in the definition passed to {@link gpf.define}
     *
     * ### Description
     *
     * The list of possible $ properties is fixed and depends on the entity type.
     * This error is thrown when one $ property is not allowed.
     * @since 0.1.6
     */
    invalidEntity$Property: "Invalid entity $ property",

    /**
     * ### Summary
     *
     * Entity name is missing in the definition passed to {@link gpf.define}
     *
     * ### Description
     *
     * This error is thrown when the entity name is missing
     * @since 0.1.6
     */
    missingEntityName: "Missing entity name",

    /**
     * ### Summary
     *
     * Entity namespace is invalid in the definition passed to {@link gpf.define}
     *
     * ### Description
     *
     * This error is thrown when the namespace is invalid
     * @since 0.1.6
     */
    invalidEntityNamespace: "Invalid entity namespace"

});

_gpfExtend(_GpfEntityDefinition.prototype, /** @lends _GpfEntityDefinition.prototype */ {

    /**
     * Entity type (class...)
     *
     * @readonly
     * @since 0.1.6
     */
    _type: "",

    /**
     * List of allowed $ properties
     *
     * @type {String[]}
     * @readonly
     * @since 0.1.6
     */
    _allowed$Properties: _gpfDefineGenerate$Keys("type,name,namespace"),

    /**
     * Check if the $ property is allowed
     *
     * @param {String} name $ Property name
     * @see _GpfEntityDefinition.prototype._allowed$Properties
     * @throws {gpf.Error.InvalidEntity$Property}
     * @since 0.1.6
     */
    _check$Property: function (name) {
        if (-1 === this._allowed$Properties.indexOf(name)) {
            gpf.Error.invalidEntity$Property();
        }
    },

    /**
     * Check if the property is allowed
     * NOTE: $ properties are handled by {@link _check$Property}
     *
     * @param {String} name Property name
     * @since 0.1.6
     */
    _checkProperty: function (name) {
        _gpfIgnore(name);
    },

    /**
     * Check the properties contained in the definition passed to {@link gpf.define}
     * @since 0.1.6
     */
    _checkProperties: function () {
        _gpfObjectForEach(this._initialDefinition, function (value, name) {
            _gpfIgnore(value);
            /*eslint-disable no-invalid-this*/ // bound through thisArg
            if (name.charAt(0) === "$") {
                this._check$Property(name);
            } else {
                this._checkProperty(name);
            }
            /*eslint-enable no-invalid-this*/
        }, this);
    },

    /**
     * Entity name
     * @since 0.1.6
     */
    _name: "",

    /**
     * Compute name property
     * @since 0.1.6
     */
    _readName: function () {
        var definition = this._initialDefinition;
        this._name = definition["$" + this._type] || definition.$name;
    },

    /**
     * Check name property
     *
     * @throws {gpf.Error.MissingEntityName}
     * @since 0.1.6
     */
    _checkName: function () {
        if (!this._name) {
            gpf.Error.missingEntityName();
        }
    },

    /**
     * Entity namespace
     * @since 0.1.6
     */
    _namespace: "",

    /**
     * If the name is prefixed with a namespace, isolate it and update name property
     *
     * @return {String|undefined} Namespace contained in the name or undefined if none
     * @since 0.1.6
     */
    _extractRelativeNamespaceFromName: function () {
        var name = this._name,
            lastDotPosition = name.lastIndexOf(".");
        if (-1 < lastDotPosition) {
            this._name = name.substr(lastDotPosition + 1);
            return name.substr(0, lastDotPosition);
        }
    },

    /**
     * Compute namespace property
     * @since 0.1.6
     */
    _readNamespace: function () {
        var namespaces = [
            this._initialDefinition.$namespace,
            this._extractRelativeNamespaceFromName()
        ].filter(function (namespacePart) {
            return namespacePart;
        });
        if (namespaces.length > 0) {
            this._namespace = namespaces.join(".");
        }
    },

    /**
     * Check namespace property
     *
     * @throws {gpf.Error.InvalidEntityNamespace}
     * @since 0.1.6
     */
    _checkNamespace: function () {
        var namespace = this._namespace;
        if (namespace && !(/^[a-z_$][a-zA-Z0-9]+(:?\.[a-z_$][a-zA-Z0-9]+)*$/).exec(namespace)) {
            gpf.Error.invalidEntityNamespace();
        }
    },

    check: function () {
        this._checkProperties();
        this._readName();
        this._readNamespace();
        this._checkName();
        this._checkNamespace();
    }

});
<|endoftext|>


<commit_before>const Base = require('./Base.js')
const FilterBase = require('./FilterBase.js')
const FeedModel = require('../../models/Feed.js').model
const log = require('../../util/logger.js')
const dbOpsSchedules = require('../../util/db/schedules.js')
const ArticleModel = require('../../models/Article.js')
const Format = require('./Format.js')
const Subscriber = require('./Subscriber.js')

class Feed extends FilterBase {
  /**
   * @param {import('mongoose').Model|Object<string, any>} data - Data
   * @param {string} data.title - Feed meta title
   * @param {string} data.url - Feed URL
   * @param {string} data.guild - Guild ID
   * @param {string} data.channel - Channel ID
   * @param {Date} data.addedAt - Date the feed was added
   * @param {boolean} data.checkTitles - Check titles for determining article newness
   * @param {boolean} data.imgPreviews - Have Discord automatically show embeds for image links
   * @param {boolean} data.imgLinksExistence - Show image links in messages
   * @param {boolean} data.checkDates - Check dates for determining article newness
   * @param {boolean} data.formatTables - Format messages as if they're tables
   * @param {boolean} data.toggleRoleMentions - Toggle role mentions for subscribers when messages are sent
   */
  constructor (data, _saved) {
    super(data, _saved)

    /**
     * Feed name
     * @type {string}
     */
    this.title = this.getField('title')
    if (!this.title) {
      throw new Error('Undefined title')
    }

    /**
     * Feed URL
     * @type {string}
     */
    this.url = this.getField('url')
    if (!this.url) {
      throw new Error('Undefined url')
    }

    /**
     * Guild ID
     * @type {string}
     */
    this.guild = this.getField('guild')
    if (!this.guild) {
      throw new Error('Undefined guild')
    }

    /**
     * Feed channel ID
     * @type {string}
     */
    this.channel = this.getField('channel')
    if (!this.channel) {
      throw new Error('Undefined channel')
    }

    /**
     * Date the feed was added. No need to initialize since
     * mongoose will add a default when it saves.
     * @type {Date}
     */
    this.addedAt = this.getField('addedAt')

    /**
     * Check titles for determining article newness
     * @type {boolean}
     */
    this.checkTitles = this.getField('checkTitles')

    /**
     * Have Discord automatically show embeds for image links
     * @type {boolean}
     */
    this.imgPreviews = this.getField('imgPreviews')

    /**
     * Show image links in messages
     * @type {boolean}
     */
    this.imgLinksExistence = this.getField('imgLinksExistence')

    /**
     * Check dates for determining article newness
     * @type {boolean}
     */
    this.checkDates = this.getField('checkDates')

    /**
     * Format messages as if they're tables
     * @type {boolean}
     */
    this.formatTables = this.getField('formatTables')

    /**
     * Toggle role mentions for subscribers when messages
     * are sent
     * @type {boolean}
     */
    this.toggleRoleMentions = this.getField('toggleRoleMentions')

    /**
     * Disabled status
     * @type {String}
     */
    this.disabled = this.getField('disabled')

    /**
     * This feed's webhook. Default value is an empty object,
     * as enforced by mongoose. Cannot be empty/null.
     * @type {Object<string, string>}
     */
    this._webhook = this.getField('webhook')

    /**
     * Split messages that are >2000 chars into multiple
     * messages
     * @type {Object<string, string|number>}
     */
    this._split = this.getField('split')
  }

  static get SPLIT_KEYS () {
    return ['char', 'prepend', 'append', 'maxLength']
  }

  static get WEBHOOK_KEYS () {
    return ['id', 'name', 'avatar']
  }

  toObject () {
    /**
     * Use this.webhook instead of this._webhook since mongoose
     * will return an empty object when we don't want it to
     */
    return {
      ...super.toObject(),
      title: this.title,
      url: this.url,
      guild: this.guild,
      channel: this.channel,
      addedAt: this.addedAt,
      checkTitles: this.checkTitles,
      checkDates: this.checkDates,
      imgPreviews: this.imgPreviews,
      imgLinksExistence: this.imgLinksExistence,
      formatTables: this.formatTables,
      toggleRoleMentions: this.toggleRoleMentions,
      disabled: this.disabled,
      webhook: this.webhook,
      split: this.split
    }
  }

  /**
   * Webhook getter
   * @returns {Object<string, string>|undefined}
   */
  get webhook () {
    return Base.resolveObject(this._webhook)
  }

  /**
   * Split options getter
   * @returns {Object<string, string>|undefined}
   */
  get split () {
    return Base.resolveObject(this._split)
  }

  set webhook (value) {
    this._webhook = value
  }

  set split (value) {
    this._split = value
  }

  /**
   * Remove the feed and delete its schedule
   * @param {string} shardID
   */
  async remove (shardID) {
    await this.delete()
    await this.removeSchedule(shardID)
  }

  /**
   * Remove the schedule of this feed
   * @param {string} shardID
   */
  async removeSchedule (shardID) {
    const assigned = await dbOpsSchedules.assignedSchedules.get(this.id)
    if (!assigned) {
      return
    }
    // const shardID = this.bot.shard ? this.bot.shard.id : 0
    await dbOpsSchedules.assignedSchedules.remove(this.id)
    const assignedSchedules = await dbOpsSchedules.assignedSchedules.getMany(shardID, assigned.schedule, this.url)
    if (assignedSchedules.length === 0 && Base.isMongoDatabase) {
      ArticleModel.model(this.url, shardID, assigned.schedule).collection.drop().catch(err => err.code === 26 ? null : log.general.error('Failed to drop unused collection after feed removal', err))
    }
  }

  /**
   * Gets the message format of this feed. There is only
   * one format per feed.
   * @returns {Format}
   */
  async getFormat () {
    return Format.getBy('feed', this._id)
  }

  /**
   * Returns both role and user subscribers of this feed.
   * @returns {Subscriber}
   */
  async getSubscribers () {
    return Subscriber.getManyBy('feed', this._id)
  }

  static get Model () {
    return FeedModel
  }
}

module.exports = Feed
<commit_msg>Add delete method to Feed<commit_after>const Base = require('./Base.js')
const FilterBase = require('./FilterBase.js')
const FeedModel = require('../../models/Feed.js').model
const log = require('../../util/logger.js')
const dbOpsSchedules = require('../../util/db/schedules.js')
const ArticleModel = require('../../models/Article.js')
const Format = require('./Format.js')
const Subscriber = require('./Subscriber.js')

class Feed extends FilterBase {
  /**
   * @param {import('mongoose').Model|Object<string, any>} data - Data
   * @param {string} data.title - Feed meta title
   * @param {string} data.url - Feed URL
   * @param {string} data.guild - Guild ID
   * @param {string} data.channel - Channel ID
   * @param {Date} data.addedAt - Date the feed was added
   * @param {boolean} data.checkTitles - Check titles for determining article newness
   * @param {boolean} data.imgPreviews - Have Discord automatically show embeds for image links
   * @param {boolean} data.imgLinksExistence - Show image links in messages
   * @param {boolean} data.checkDates - Check dates for determining article newness
   * @param {boolean} data.formatTables - Format messages as if they're tables
   * @param {boolean} data.toggleRoleMentions - Toggle role mentions for subscribers when messages are sent
   */
  constructor (data, _saved) {
    super(data, _saved)

    /**
     * Feed name
     * @type {string}
     */
    this.title = this.getField('title')
    if (!this.title) {
      throw new Error('Undefined title')
    }

    /**
     * Feed URL
     * @type {string}
     */
    this.url = this.getField('url')
    if (!this.url) {
      throw new Error('Undefined url')
    }

    /**
     * Guild ID
     * @type {string}
     */
    this.guild = this.getField('guild')
    if (!this.guild) {
      throw new Error('Undefined guild')
    }

    /**
     * Feed channel ID
     * @type {string}
     */
    this.channel = this.getField('channel')
    if (!this.channel) {
      throw new Error('Undefined channel')
    }

    /**
     * Date the feed was added. No need to initialize since
     * mongoose will add a default when it saves.
     * @type {Date}
     */
    this.addedAt = this.getField('addedAt')

    /**
     * Check titles for determining article newness
     * @type {boolean}
     */
    this.checkTitles = this.getField('checkTitles')

    /**
     * Have Discord automatically show embeds for image links
     * @type {boolean}
     */
    this.imgPreviews = this.getField('imgPreviews')

    /**
     * Show image links in messages
     * @type {boolean}
     */
    this.imgLinksExistence = this.getField('imgLinksExistence')

    /**
     * Check dates for determining article newness
     * @type {boolean}
     */
    this.checkDates = this.getField('checkDates')

    /**
     * Format messages as if they're tables
     * @type {boolean}
     */
    this.formatTables = this.getField('formatTables')

    /**
     * Toggle role mentions for subscribers when messages
     * are sent
     * @type {boolean}
     */
    this.toggleRoleMentions = this.getField('toggleRoleMentions')

    /**
     * Disabled status
     * @type {String}
     */
    this.disabled = this.getField('disabled')

    /**
     * This feed's webhook. Default value is an empty object,
     * as enforced by mongoose. Cannot be empty/null.
     * @type {Object<string, string>}
     */
    this._webhook = this.getField('webhook')

    /**
     * Split messages that are >2000 chars into multiple
     * messages
     * @type {Object<string, string|number>}
     */
    this._split = this.getField('split')
  }

  static get SPLIT_KEYS () {
    return ['char', 'prepend', 'append', 'maxLength']
  }

  static get WEBHOOK_KEYS () {
    return ['id', 'name', 'avatar']
  }

  toObject () {
    /**
     * Use this.webhook instead of this._webhook since mongoose
     * will return an empty object when we don't want it to
     */
    return {
      ...super.toObject(),
      title: this.title,
      url: this.url,
      guild: this.guild,
      channel: this.channel,
      addedAt: this.addedAt,
      checkTitles: this.checkTitles,
      checkDates: this.checkDates,
      imgPreviews: this.imgPreviews,
      imgLinksExistence: this.imgLinksExistence,
      formatTables: this.formatTables,
      toggleRoleMentions: this.toggleRoleMentions,
      disabled: this.disabled,
      webhook: this.webhook,
      split: this.split
    }
  }

  /**
   * Webhook getter
   * @returns {Object<string, string>|undefined}
   */
  get webhook () {
    return Base.resolveObject(this._webhook)
  }

  /**
   * Split options getter
   * @returns {Object<string, string>|undefined}
   */
  get split () {
    return Base.resolveObject(this._split)
  }

  set webhook (value) {
    this._webhook = value
  }

  set split (value) {
    this._split = value
  }

  /**
   * Remove the feed and delete its schedule
   * @param {string} shardID
   */
  async remove (shardID) {
    await this.delete()
    await this.removeSchedule(shardID)
  }

  /**
   * Remove the schedule of this feed
   * @param {string} shardID
   */
  async removeSchedule (shardID) {
    const assigned = await dbOpsSchedules.assignedSchedules.get(this.id)
    if (!assigned) {
      return
    }
    // const shardID = this.bot.shard ? this.bot.shard.id : 0
    await dbOpsSchedules.assignedSchedules.remove(this.id)
    const assignedSchedules = await dbOpsSchedules.assignedSchedules.getMany(shardID, assigned.schedule, this.url)
    if (assignedSchedules.length === 0 && Base.isMongoDatabase) {
      ArticleModel.model(this.url, shardID, assigned.schedule).collection.drop().catch(err => err.code === 26 ? null : log.general.error('Failed to drop unused collection after feed removal', err))
    }
  }

  /**
   * Gets the message format of this feed. There is only
   * one format per feed.
   * @returns {Format}
   */
  async getFormat () {
    return Format.getBy('feed', this._id)
  }

  /**
   * Returns both role and user subscribers of this feed.
   * @returns {Subscriber}
   */
  async getSubscribers () {
    return Subscriber.getManyBy('feed', this._id)
  }

  async delete () {
    const [ format, subscribers ] = await Promise.all([
      this.getFormat(),
      this.getSubscribers()
    ])
    await Promise.all(subscribers.map(sub => sub.delete()).concat([format.delete()]))
    return super.delete()
  }

  static get Model () {
    return FeedModel
  }
}

module.exports = Feed
<|endoftext|>


<commit_before>const TextBasedChannel = require('./interface/TextBasedChannel');
const Constants = require('../util/Constants');
const Presence = require('./Presence').Presence;

/**
 * Represents a user on Discord.
 * @implements {TextBasedChannel}
 */
class User {
  constructor(client, data) {
    /**
     * The Client that created the instance of the the User.
     * @name User#client
     * @type {Client}
     * @readonly
     */
    Object.defineProperty(this, 'client', { value: client });

    if (data) this.setup(data);
  }

  setup(data) {
    /**
     * The ID of the user
     * @type {string}
     */
    this.id = data.id;

    /**
     * The username of the user
     * @type {string}
     */
    this.username = data.username;

    /**
     * A discriminator based on username for the user
     * @type {string}
     */
    this.discriminator = data.discriminator;

    /**
     * The ID of the user's avatar
     * @type {string}
     */
    this.avatar = data.avatar;

    /**
     * Whether or not the user is a bot.
     * @type {boolean}
     */
    this.bot = Boolean(data.bot);

    /**
     * The ID of the last message sent by the user, if one was sent.
     * @type {?string}
     */
    this.lastMessageID = null;
  }

  patch(data) {
    for (const prop of ['id', 'username', 'discriminator', 'avatar', 'bot']) {
      if (typeof data[prop] !== 'undefined') this[prop] = data[prop];
    }
  }

  /**
   * The timestamp the user was created at
   * @type {number}
   * @readonly
   */
  get createdTimestamp() {
    return (this.id / 4194304) + 1420070400000;
  }

  /**
   * The time the user was created
   * @type {Date}
   * @readonly
   */
  get createdAt() {
    return new Date(this.createdTimestamp);
  }

  /**
   * The presence of this user
   * @type {Presence}
   * @readonly
   */
  get presence() {
    if (this.client.presences.has(this.id)) return this.client.presences.get(this.id);
    for (const guild of this.client.guilds.values()) {
      if (guild.presences.has(this.id)) return guild.presences.get(this.id);
    }
    return new Presence();
  }

  /**
   * A link to the user's avatar (if they have one, otherwise null)
   * @type {?string}
   * @readonly
   */
  get avatarURL() {
    if (!this.avatar) return null;
    return Constants.Endpoints.avatar(this.id, this.avatar);
  }

  /**
   * A link to the user's default avatar
   * @type {string}
   * @readonly
   */
  get defaultAvatarURL() {
    let defaultAvatars = Object.values(Constants.DefaultAvatars);
    let defaultAvatar = this.discriminator % defaultAvatars.length;
    return Constants.Endpoints.assets(`${defaultAvatars[defaultAvatar]}.png`);
  }

  /**
   * A link to the user's avatar if they have one. Otherwise a link to their default avatar will be returned
   * @type {string}
   * @readonly
   */
  get displayAvatarURL() {
    return this.avatarURL || this.defaultAvatarURL;
  }

  /**
   * The note that is set for the user
   * <warn>This is only available when using a user account.</warn>
   * @type {?string}
   * @readonly
   */
  get note() {
    return this.client.user.notes.get(this.id) || null;
  }

  /**
   * Check whether the user is typing in a channel.
   * @param {ChannelResolvable} channel The channel to check in
   * @returns {boolean}
   */
  typingIn(channel) {
    channel = this.client.resolver.resolveChannel(channel);
    return channel._typing.has(this.id);
  }

  /**
   * Get the time that the user started typing.
   * @param {ChannelResolvable} channel The channel to get the time in
   * @returns {?Date}
   */
  typingSinceIn(channel) {
    channel = this.client.resolver.resolveChannel(channel);
    return channel._typing.has(this.id) ? new Date(channel._typing.get(this.id).since) : null;
  }

  /**
   * Get the amount of time the user has been typing in a channel for (in milliseconds), or -1 if they're not typing.
   * @param {ChannelResolvable} channel The channel to get the time in
   * @returns {number}
   */
  typingDurationIn(channel) {
    channel = this.client.resolver.resolveChannel(channel);
    return channel._typing.has(this.id) ? channel._typing.get(this.id).elapsedTime : -1;
  }

  /**
   * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful.
   * @returns {Promise<DMChannel>}
   */
  deleteDM() {
    return this.client.rest.methods.deleteChannel(this);
  }

  /**
   * Sends a friend request to the user
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<User>}
   */
  addFriend() {
    return this.client.rest.methods.addFriend(this);
  }

  /**
   * Removes the user from your friends
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<User>}
   */
  removeFriend() {
    return this.client.rest.methods.removeFriend(this);
  }

  /**
   * Blocks the user
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<User>}
   */
  block() {
    return this.client.rest.methods.blockUser(this);
  }

  /**
   * Unblocks the user
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<User>}
   */
  unblock() {
    return this.client.rest.methods.unblockUser(this);
  }

  /**
   * Get the profile of the user
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<UserProfile>}
   */
  fetchProfile() {
    return this.client.rest.methods.fetchUserProfile(this);
  }

  /**
   * Sets a note for the user
   * <warn>This is only available when using a user account.</warn>
   * @param {string} note The note to set for the user
   * @returns {Promise<User>}
   */
  setNote(note) {
    return this.client.rest.methods.setNote(this, note);
  }

  /**
   * Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
   * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
   * @param {User} user User to compare with
   * @returns {boolean}
   */
  equals(user) {
    let equal = user &&
      this.id === user.id &&
      this.username === user.username &&
      this.discriminator === user.discriminator &&
      this.avatar === user.avatar &&
      this.bot === Boolean(user.bot);

    return equal;
  }

  /**
   * When concatenated with a string, this automatically concatenates the user's mention instead of the User object.
   * @returns {string}
   * @example
   * // logs: Hello from <@123456789>!
   * console.log(`Hello from ${user}!`);
   */
  toString() {
    return `<@${this.id}>`;
  }

  // These are here only for documentation purposes - they are implemented by TextBasedChannel
  sendMessage() { return; }
  sendTTSMessage() { return; }
  sendEmbed() { return; }
  sendFile() { return; }
  sendCode() { return; }
}

TextBasedChannel.applyToClass(User);

module.exports = User;
<commit_msg>add User#dmChannel (#1005)<commit_after>const TextBasedChannel = require('./interface/TextBasedChannel');
const Constants = require('../util/Constants');
const Presence = require('./Presence').Presence;

/**
 * Represents a user on Discord.
 * @implements {TextBasedChannel}
 */
class User {
  constructor(client, data) {
    /**
     * The Client that created the instance of the the User.
     * @name User#client
     * @type {Client}
     * @readonly
     */
    Object.defineProperty(this, 'client', { value: client });

    if (data) this.setup(data);
  }

  setup(data) {
    /**
     * The ID of the user
     * @type {string}
     */
    this.id = data.id;

    /**
     * The username of the user
     * @type {string}
     */
    this.username = data.username;

    /**
     * A discriminator based on username for the user
     * @type {string}
     */
    this.discriminator = data.discriminator;

    /**
     * The ID of the user's avatar
     * @type {string}
     */
    this.avatar = data.avatar;

    /**
     * Whether or not the user is a bot.
     * @type {boolean}
     */
    this.bot = Boolean(data.bot);

    /**
     * The ID of the last message sent by the user, if one was sent.
     * @type {?string}
     */
    this.lastMessageID = null;
  }

  patch(data) {
    for (const prop of ['id', 'username', 'discriminator', 'avatar', 'bot']) {
      if (typeof data[prop] !== 'undefined') this[prop] = data[prop];
    }
  }

  /**
   * The timestamp the user was created at
   * @type {number}
   * @readonly
   */
  get createdTimestamp() {
    return (this.id / 4194304) + 1420070400000;
  }

  /**
   * The time the user was created
   * @type {Date}
   * @readonly
   */
  get createdAt() {
    return new Date(this.createdTimestamp);
  }

  /**
   * The presence of this user
   * @type {Presence}
   * @readonly
   */
  get presence() {
    if (this.client.presences.has(this.id)) return this.client.presences.get(this.id);
    for (const guild of this.client.guilds.values()) {
      if (guild.presences.has(this.id)) return guild.presences.get(this.id);
    }
    return new Presence();
  }

  /**
   * A link to the user's avatar (if they have one, otherwise null)
   * @type {?string}
   * @readonly
   */
  get avatarURL() {
    if (!this.avatar) return null;
    return Constants.Endpoints.avatar(this.id, this.avatar);
  }

  /**
   * A link to the user's default avatar
   * @type {string}
   * @readonly
   */
  get defaultAvatarURL() {
    let defaultAvatars = Object.values(Constants.DefaultAvatars);
    let defaultAvatar = this.discriminator % defaultAvatars.length;
    return Constants.Endpoints.assets(`${defaultAvatars[defaultAvatar]}.png`);
  }

  /**
   * A link to the user's avatar if they have one. Otherwise a link to their default avatar will be returned
   * @type {string}
   * @readonly
   */
  get displayAvatarURL() {
    return this.avatarURL || this.defaultAvatarURL;
  }

  /**
   * The note that is set for the user
   * <warn>This is only available when using a user account.</warn>
   * @type {?string}
   * @readonly
   */
  get note() {
    return this.client.user.notes.get(this.id) || null;
  }

  /**
   * Check whether the user is typing in a channel.
   * @param {ChannelResolvable} channel The channel to check in
   * @returns {boolean}
   */
  typingIn(channel) {
    channel = this.client.resolver.resolveChannel(channel);
    return channel._typing.has(this.id);
  }

  /**
   * Get the time that the user started typing.
   * @param {ChannelResolvable} channel The channel to get the time in
   * @returns {?Date}
   */
  typingSinceIn(channel) {
    channel = this.client.resolver.resolveChannel(channel);
    return channel._typing.has(this.id) ? new Date(channel._typing.get(this.id).since) : null;
  }

  /**
   * Get the amount of time the user has been typing in a channel for (in milliseconds), or -1 if they're not typing.
   * @param {ChannelResolvable} channel The channel to get the time in
   * @returns {number}
   */
  typingDurationIn(channel) {
    channel = this.client.resolver.resolveChannel(channel);
    return channel._typing.has(this.id) ? channel._typing.get(this.id).elapsedTime : -1;
  }

  /**
   * The DM between the client's user and this user
   * @type {?DMChannel}
   */
  get dmChannel() {
    return this.client.channels.filter(c => c.type === 'dm').find(c => c.recipient.id === this.id);
  }

  /**
   * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful.
   * @returns {Promise<DMChannel>}
   */
  deleteDM() {
    return this.client.rest.methods.deleteChannel(this);
  }

  /**
   * Sends a friend request to the user
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<User>}
   */
  addFriend() {
    return this.client.rest.methods.addFriend(this);
  }

  /**
   * Removes the user from your friends
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<User>}
   */
  removeFriend() {
    return this.client.rest.methods.removeFriend(this);
  }

  /**
   * Blocks the user
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<User>}
   */
  block() {
    return this.client.rest.methods.blockUser(this);
  }

  /**
   * Unblocks the user
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<User>}
   */
  unblock() {
    return this.client.rest.methods.unblockUser(this);
  }

  /**
   * Get the profile of the user
   * <warn>This is only available when using a user account.</warn>
   * @returns {Promise<UserProfile>}
   */
  fetchProfile() {
    return this.client.rest.methods.fetchUserProfile(this);
  }

  /**
   * Sets a note for the user
   * <warn>This is only available when using a user account.</warn>
   * @param {string} note The note to set for the user
   * @returns {Promise<User>}
   */
  setNote(note) {
    return this.client.rest.methods.setNote(this, note);
  }

  /**
   * Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
   * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
   * @param {User} user User to compare with
   * @returns {boolean}
   */
  equals(user) {
    let equal = user &&
      this.id === user.id &&
      this.username === user.username &&
      this.discriminator === user.discriminator &&
      this.avatar === user.avatar &&
      this.bot === Boolean(user.bot);

    return equal;
  }

  /**
   * When concatenated with a string, this automatically concatenates the user's mention instead of the User object.
   * @returns {string}
   * @example
   * // logs: Hello from <@123456789>!
   * console.log(`Hello from ${user}!`);
   */
  toString() {
    return `<@${this.id}>`;
  }

  // These are here only for documentation purposes - they are implemented by TextBasedChannel
  sendMessage() { return; }
  sendTTSMessage() { return; }
  sendEmbed() { return; }
  sendFile() { return; }
  sendCode() { return; }
}

TextBasedChannel.applyToClass(User);

module.exports = User;
<|endoftext|>


<commit_before>import { create } from 'jss';
import global from 'jss-global';
import isolate from 'jss-isolate';
import nested from 'jss-nested';
import camelCase from 'jss-camel-case';
import defaultUnit from 'jss-default-unit';
import compose from 'jss-compose';
import nonInheritedProps from './nonInheritedProps';

const createGenerateClassName = () => {
	let counter = 0;
	return rule => `rsg--${rule.key}-${counter++}`;
};

const jss = create({
	createGenerateClassName,
	plugins: [
		global(),
		isolate({
			reset: {
				// Reset all inherited and non-inherited properties
				...nonInheritedProps,

				// “Global” styles for all components
				boxSizing: 'border-box',

				// Allow inheritance because it may be set on body and should be available for user components
				fontFamily: 'inherit',
				lineHeight: 'inherit',
				fontSize: 'inherit',
			},
		}),
		nested(),
		camelCase(),
		defaultUnit(),
		compose(),
	],
});

export default jss;
<commit_msg>Fix: Inherit color (#652) (close #656)<commit_after>import { create } from 'jss';
import global from 'jss-global';
import isolate from 'jss-isolate';
import nested from 'jss-nested';
import camelCase from 'jss-camel-case';
import defaultUnit from 'jss-default-unit';
import compose from 'jss-compose';
import nonInheritedProps from './nonInheritedProps';

const createGenerateClassName = () => {
	let counter = 0;
	return rule => `rsg--${rule.key}-${counter++}`;
};

const jss = create({
	createGenerateClassName,
	plugins: [
		global(),
		isolate({
			reset: {
				// Reset all inherited and non-inherited properties
				...nonInheritedProps,

				// “Global” styles for all components
				boxSizing: 'border-box',

				// Allow inheritance because it may be set on body and should be available for user components
				color: 'inherit',
				fontFamily: 'inherit',
				lineHeight: 'inherit',
				fontSize: 'inherit',
			},
		}),
		nested(),
		camelCase(),
		defaultUnit(),
		compose(),
	],
});

export default jss;
<|endoftext|>


<commit_before>function getCharacterDOMWidth(string, font) {
  const span = document.createElement('span');
    span.style.font = 'Times, "Times New Roman", serif';
    span.appendChild(document.createTextNode(string));

  document.body.appendChild(span);
  const { width } = span.getBoundingClientRect();
  document.body.removeChild(span);

  return Math.round(width);
}

export default function detectScale(font='1rem Times, "Times New Roman", serif') {
  return (
    getCharacterDOMWidth('👩' /* woman */, font) / getCharacterDOMWidth('J!i', font)
  );
}
<commit_msg>🖍 Fix some linting issues<commit_after>function getCharacterDOMWidth(string, font) {
  const span = document.createElement('span');
  span.style.font = font;
  span.appendChild(document.createTextNode(string));

  document.body.appendChild(span);
  const { width } = span.getBoundingClientRect();
  document.body.removeChild(span);

  return Math.round(width);
}

export default function detectScale(font = '1rem Times, "Times New Roman", serif') {
  return (
    getCharacterDOMWidth('👩' /* woman */, font) / getCharacterDOMWidth('J!i', font)
  );
}
<|endoftext|>


<commit_before>jIRCs.prototype.irc_PING = function(prefix, args) {
    this.send('PONG',args);
};

jIRCs.prototype.irc_NICK = function(prefix, args) {
    var oldNick = this.getNick(prefix),
        newNick = args.pop().substr(1);
    if(oldNick == this.nickname) {
        this.nickname = newNick;
    }
    for(var channel in this.channels) {
        if(this.channels.hasOwnProperty(channel) && 'names' in this.channels[channel] && oldNick in this.channels[channel].names) {
            this.renderLine(channel,'',oldNick + ' is now known as ' + newNick);
            this.channels[channel].names[newNick] = this.channels[channel].names[oldNick];
            delete(this.channels[channel].names[oldNick]);
        }
    }
};

jIRCs.prototype.irc_JOIN = function(prefix, args) {
    var channel = args.pop().substr(1);
    this.renderLine(channel, '', prefix + " joined " + channel);
    if(!this.channels[channel].names) {
        this.channels[channel].names = {};
    }
    this.channels[channel].names[prefix] = "";
    this.displays.forEach(function(disobj) {
        if(document.activeElement == disobj.messagebox && prefix == this.nickname) {
            this.activateChan(channel, disobj);
        }
        if(disobj.window == channel) {
            this.renderUserlist(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_PART = function(prefix, args) {
    var channel = args.pop();
    this.renderLine(channel, '', prefix + " left " + channel);
    delete(this.channels[channel].names[prefix]);
    if(this.getNick(prefix) == this.nickname) {
        this.destroyChan(channel);
    } else {
        this.displays.forEach(function(disobj) {
            if(disobj.window == channel) {
                this.renderUserlist(disobj);
            }
        }, this);
    }
};

jIRCs.prototype.irc_QUIT = function(prefix, args) {
    var reason = args.pop().substr(1);
    for(var channel in this.channels) {
        if(channel == "Status") {
            continue;
        }
        if(prefix in this.channels[channel].names) {
            this.renderLine(channel, '', prefix + " quit (" + reason + ")");
            delete(this.channels[channel].names[prefix]);
        }
    }
    if(this.getNick(prefix) == this.nickname) {
        // This is never triggered
        for(var channel in this.channels) {
            if(channel != 'Status') {
                this.destroyChan(channel);
            }
        }
    } else {
        this.displays.forEach(function(disobj) {
            this.renderUserlist(disobj);
        }, this);
    }
};

jIRCs.prototype.irc_PRIVMSG = function(prefix, args) {
    var channel = args.shift();
    var message = args.pop().substr(1);
    //account for private messages
    if (channel == this.nickname) {
        channel = prefix;
    }
    if(message.charAt(0) == '\u0001') {
        message = message.split('\u0001')[1];
        if(message.substr(0,6).toUpperCase() == 'ACTION') {
            message = prefix + message.substr(6);
            prefix = '';
        }
    }
    this.renderLine(channel, prefix, message);
};

jIRCs.prototype.irc_005 = function(prefix, args) {
    for (var i = 1; i < args.length - 1; i++) { // skip the nickname and the "is supported by this server" message
        if (args[i].substr(0,7) == "PREFIX=") {
            this.statuses = {};
            var modes = args[i].substr(8).split(')'); // exclude the open paren, split close paren
            for (var j = 0; j < modes[0].length; j++) {
                this.statuses[modes[1][j]] = modes[0][j]; // mapping of symbols to modes
                this.statusOrder.push(modes[0][j]); // most important first
            }
            this.statuses[''] = '';
            this.statusOrder.push('');
        }
    }
};

jIRCs.prototype.irc_353 = function(prefix, args) {
    if (!this.channels[args[2]].moreNames) {
        this.channels[args[2]].names = {};
        this.channels[args[2]].moreNames = true;
    }
    var names = args[3].substr(1).split(' '); // Strip the colon and split the names out
    names.forEach(function(name) {
        var statusList = '';
        while (name.charAt(0) in this.statuses) {
            statusList += name.charAt(0);
            name = name.substr(1);
        }
        this.channels[args[2]].names[name] = statusList;
    }, this);
};

jIRCs.prototype.irc_366 = function(prefix, args) {
    this.channels[args[1]].moreNames = false;
    this.displays.forEach(function(disobj) {
        if(disobj.window == args[1]) {
            this.renderUserlist(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_332 = function(prefix, args) {
    if(!this.channels[args[1]].topic) {
        this.channels[args[1]].topic = {};
    }
    this.channels[args[1]].topic.message = args[2].substr(1);
    this.displays.forEach(function(disobj) { if(disobj.window == args[1]) this.renderTopic(disobj); }, this);
};

jIRCs.prototype.irc_333 = function(prefix, args) {
    if(!this.channels[args[1]].topic) {
        this.channels[args[1]].topic = {};
    }
    this.channels[args[1]].topic.creator = args[2];
    this.channels[args[1]].topic.time = new Date(args[3] * 1000);
    this.displays.forEach(function(disobj) { if(disobj.window == args[1]) this.renderTopic(disobj); }, this);
};

jIRCs.prototype.irc_TOPIC = function(prefix, args) {
    if(!this.channels[args[0]].topic) {
        this.channels[args[0]].topic = {};
    }
    this.channels[args[0]].topic.creator = prefix;
    this.channels[args[0]].topic.time = new Date();
    this.channels[args[0]].topic.message = args[1].substr(1);
    this.displays.forEach(function(disobj) { if(disobj.window == args[0]) this.renderTopic(disobj); }, this);
};
<commit_msg>Join, Part, and Quit fixes<commit_after>jIRCs.prototype.irc_PING = function(prefix, args) {
    this.send('PONG',args);
};

jIRCs.prototype.irc_NICK = function(prefix, args) {
    var oldNick = this.getNick(prefix),
        newNick = args.pop().substr(1);
    if(oldNick == this.nickname) {
        this.nickname = newNick;
    }
    for(var channel in this.channels) {
        if(this.channels.hasOwnProperty(channel) && 'names' in this.channels[channel] && oldNick in this.channels[channel].names) {
            this.renderLine(channel,'',oldNick + ' is now known as ' + newNick);
            this.channels[channel].names[newNick] = this.channels[channel].names[oldNick];
            delete(this.channels[channel].names[oldNick]);
        }
    }
};

jIRCs.prototype.irc_JOIN = function(prefix, args) {
    var channel = args.pop().substr(1);
    if(prefix != this.nickname) {
        this.renderLine(channel, '', prefix + " joined " + channel);
    } else {
        this.renderLine(channel, '', "You have joined " + channel);
    }
    if(!this.channels[channel].names) {
        this.channels[channel].names = {};
    }
    this.channels[channel].names[prefix] = "";
    this.displays.forEach(function(disobj) {
        if(document.activeElement == disobj.messagebox && prefix == this.nickname) {
            this.activateChan(channel, disobj);
        }
        if(disobj.window == channel) {
            this.renderUserlist(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_PART = function(prefix, args) {
    var channel = args.pop();
    if(this.getNick(prefix) == this.nickname) {
        this.destroyChan(channel);
    } else {
        this.renderLine(channel, '', prefix + " left " + channel);
        delete(this.channels[channel].names[prefix]);
        this.displays.forEach(function(disobj) {
            if(disobj.window == channel) {
                this.renderUserlist(disobj);
            }
        }, this);
    }
};

jIRCs.prototype.irc_QUIT = function(prefix, args) {
    var reason = args.pop().substr(1);
    for(var channel in this.channels) {
        if(channel == "Status") {
            continue;
        }
        if(this.channels[channel].names && prefix in this.channels[channel].names) {
            this.renderLine(channel, '', prefix + " quit (" + reason + ")");
            delete(this.channels[channel].names[prefix]);
        }
    }
    if(this.getNick(prefix) == this.nickname) {
        // This is never triggered
        for(var channel in this.channels) {
            if(channel != 'Status') {
                this.destroyChan(channel);
            }
        }
    } else {
        this.displays.forEach(function(disobj) {
            this.renderUserlist(disobj);
        }, this);
    }
};

jIRCs.prototype.irc_PRIVMSG = function(prefix, args) {
    var channel = args.shift();
    var message = args.pop().substr(1);
    //account for private messages
    if (channel == this.nickname) {
        channel = prefix;
    }
    if(message.charAt(0) == '\u0001') {
        message = message.split('\u0001')[1];
        if(message.substr(0,6).toUpperCase() == 'ACTION') {
            message = prefix + message.substr(6);
            prefix = '';
        }
    }
    this.renderLine(channel, prefix, message);
};

jIRCs.prototype.irc_005 = function(prefix, args) {
    for (var i = 1; i < args.length - 1; i++) { // skip the nickname and the "is supported by this server" message
        if (args[i].substr(0,7) == "PREFIX=") {
            this.statuses = {};
            var modes = args[i].substr(8).split(')'); // exclude the open paren, split close paren
            for (var j = 0; j < modes[0].length; j++) {
                this.statuses[modes[1][j]] = modes[0][j]; // mapping of symbols to modes
                this.statusOrder.push(modes[0][j]); // most important first
            }
            this.statuses[''] = '';
            this.statusOrder.push('');
        }
    }
};

jIRCs.prototype.irc_353 = function(prefix, args) {
    if (!this.channels[args[2]].moreNames) {
        this.channels[args[2]].names = {};
        this.channels[args[2]].moreNames = true;
    }
    var names = args[3].substr(1).split(' '); // Strip the colon and split the names out
    names.forEach(function(name) {
        var statusList = '';
        while (name.charAt(0) in this.statuses) {
            statusList += name.charAt(0);
            name = name.substr(1);
        }
        this.channels[args[2]].names[name] = statusList;
    }, this);
};

jIRCs.prototype.irc_366 = function(prefix, args) {
    this.channels[args[1]].moreNames = false;
    this.displays.forEach(function(disobj) {
        if(disobj.window == args[1]) {
            this.renderUserlist(disobj);
        }
    }, this);
};

jIRCs.prototype.irc_332 = function(prefix, args) {
    if(!this.channels[args[1]].topic) {
        this.channels[args[1]].topic = {};
    }
    this.channels[args[1]].topic.message = args[2].substr(1);
    this.displays.forEach(function(disobj) { if(disobj.window == args[1]) this.renderTopic(disobj); }, this);
};

jIRCs.prototype.irc_333 = function(prefix, args) {
    if(!this.channels[args[1]].topic) {
        this.channels[args[1]].topic = {};
    }
    this.channels[args[1]].topic.creator = args[2];
    this.channels[args[1]].topic.time = new Date(args[3] * 1000);
    this.displays.forEach(function(disobj) { if(disobj.window == args[1]) this.renderTopic(disobj); }, this);
};

jIRCs.prototype.irc_TOPIC = function(prefix, args) {
    if(!this.channels[args[0]].topic) {
        this.channels[args[0]].topic = {};
    }
    this.channels[args[0]].topic.creator = prefix;
    this.channels[args[0]].topic.time = new Date();
    this.channels[args[0]].topic.message = args[1].substr(1);
    this.displays.forEach(function(disobj) { if(disobj.window == args[0]) this.renderTopic(disobj); }, this);
};
<|endoftext|>