<commit_before>(function() {
  'use strict';

  // Check to make sure service workers are supported in the current browser,
  // and that the current page is accessed from a secure origin. Using a
  // service worker from an insecure origin will trigger JS console errors.
  const isLocalhost = Boolean(window.location.hostname === 'localhost' ||
      // [::1] is the IPv6 localhost address.
      window.location.hostname === '[::1]' ||
      // 127.0.0.1/8 is considered localhost for IPv4.
      window.location.hostname.match(
        /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
      )
    );

  window.addEventListener('load', function() {
      if ('serviceWorker' in navigator &&
          (window.location.protocol === 'https:' || isLocalhost)) {
        navigator.serviceWorker.register('service-worker.js')
        .then(function(registration) {
          // updatefound is fired if service-worker.js changes.
          registration.onupdatefound = function() {
            // updatefound is also fired the very first time the SW is installed,
            // and there's no need to prompt for a reload at that point.
            // So check here to see if the page is already controlled,
            // i.e. whether there's an existing service worker.
            if (navigator.serviceWorker.controller) {
              // The updatefound event implies that registration.installing is set
              const installingWorker = registration.installing;

              installingWorker.onstatechange = function() {
                switch (installingWorker.state) {
                  case 'installed':
                    // At this point, the old content will have been purged and the
                    // fresh content will have been added to the cache.
                    // It's the perfect time to display a "New content is
                    // available; please refresh." message in the page's interface.
                    break;

                  case 'redundant':
                    throw new Error('The installing ' +
                                    'service worker became redundant.');

                  default:
                    // Ignore
                }
              };
            }
          };
        }).catch(function(e) {
          console.error('Error during service worker registration:', e);
        });
      }
  });
})();
<commit_msg>SW at root, of course<commit_after>(function() {
  'use strict';

  // Check to make sure service workers are supported in the current browser,
  // and that the current page is accessed from a secure origin. Using a
  // service worker from an insecure origin will trigger JS console errors.
  const isLocalhost = Boolean(window.location.hostname === 'localhost' ||
      // [::1] is the IPv6 localhost address.
      window.location.hostname === '[::1]' ||
      // 127.0.0.1/8 is considered localhost for IPv4.
      window.location.hostname.match(
        /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
      )
    );

  window.addEventListener('load', function() {
      if ('serviceWorker' in navigator &&
          (window.location.protocol === 'https:' || isLocalhost)) {
        navigator.serviceWorker.register('/service-worker.js')
        .then(function(registration) {
          // updatefound is fired if service-worker.js changes.
          registration.onupdatefound = function() {
            // updatefound is also fired the very first time the SW is installed,
            // and there's no need to prompt for a reload at that point.
            // So check here to see if the page is already controlled,
            // i.e. whether there's an existing service worker.
            if (navigator.serviceWorker.controller) {
              // The updatefound event implies that registration.installing is set
              const installingWorker = registration.installing;

              installingWorker.onstatechange = function() {
                switch (installingWorker.state) {
                  case 'installed':
                    // At this point, the old content will have been purged and the
                    // fresh content will have been added to the cache.
                    // It's the perfect time to display a "New content is
                    // available; please refresh." message in the page's interface.
                    break;

                  case 'redundant':
                    throw new Error('The installing ' +
                                    'service worker became redundant.');

                  default:
                    // Ignore
                }
              };
            }
          };
        }).catch(function(e) {
          console.error('Error during service worker registration:', e);
        });
      }
  });
})();
<|endoftext|>


<commit_before>CustomUploadButton = {
    /*
     * Dummy version of Valum's file uploader widget.
     * Overrides most everything so I can use a simple file widget.
     */
    init: function(o) {
        this._options = {
            onChange: function(input) {}
        };

        qq.extend(this._options, o);

        this._input =  $("#upload-file")[0];

        qq.attach(this._input, 'change', _.bind(function() {
            this._options.onChange(this._input);
        }, this));

        return this;
    },

    reset: function() {
        // It appears unnecessary to implement this.
    }
}

PANDA.views.Upload = Backbone.View.extend({
    el: $("#content"),

    events: {
        "click #upload-continue":      "continue_event"
    },

    template: PANDA.templates.upload,

    file_uploader: null,
    dataset: null,

    initialize: function() {
        _.bindAll(this, "render", "on_submit", "on_progress", "on_complete", "step_one", "step_two", "step_three", "continue_event");

        this.render();

        this.file_uploader = new qq.FileUploaderBasic({
            action: "/upload/",
            multiple: false,
            onSubmit: this.on_submit,
            onProgress: this.on_progress,
            onComplete: this.on_complete
        });

        this.create_upload_button();
    },

    render: function() {
        this.el.html(this.template());
    },

    create_upload_button: function() {
        $("#upload-file-wrapper").html('<input type="file" id="upload-file" />');

        btn = CustomUploadButton.init({
            onChange: _.bind(function(input) {
                this.file_uploader._onInputChange(input);
            }, this)
        });

        this.file_uploader._button = btn;
    },

    on_submit: function(id, fileName) {
        /*
         * Handler for when a file upload starts.
         */
        this.step_two();
    },

    on_progress: function(id, fileName, loaded, total) {
        /*
         * Handler for when a file upload reports its progress.
         */
        pct = Math.floor(loaded / total * 100);
        $("#upload-progress").text(pct);
    },

    on_complete: function(id, fileName, responseJSON) {
        /*
         * Handler for when a file upload is completed.
         */
        if(responseJSON.success) {
            // Create a dataset and relate it to the upload
            this.dataset = new PANDA.models.Dataset({
                name: fileName,
                data_upload: responseJSON
            });

            // Save the new dataset
            this.dataset.save({}, { success: _.bind(function() {
                // Once saved immediately begin importing it
                this.dataset.import_data(this.step_three);
            }, this)});
        } else {
            $("#step-2-alert").alert("error", '
Upload failed! <input id="start-over" type="button" class="btn" value="Try again" />
', false);
            $("#start-over").click(this.step_one);
        }
    },

    step_one: function() {
        $(".alert-message").hide();
        $("#step-2").addClass("disabled");
        $("#step-3").addClass("disabled");
        $("#upload-continue").attr("disabled", true);

        $("#step-1").removeClass("disabled");
        $("#upload-file").attr("disabled", false);

        $("#upload-file").remove();
        this.create_upload_button();
    },

    step_two: function() {
        $("#step-1").addClass("disabled");
        $("#upload-file").attr("disabled", true);
        $("#step-3").addClass("disabled");
        $("#upload-continue").attr("disabled", true);

        $("#step-2").removeClass("disabled");
    },

    step_three: function() {
        $("#step-2").addClass("disabled");

        $("#step-3").removeClass("disabled");
        $("#upload-continue").attr("disabled", false);
    },

    continue_event: function() {
        window.location = "#dataset/" + this.dataset.get("id");
    }
});

<commit_msg>Handle additional error cases in file upload.<commit_after>CustomUploadButton = {
    /*
     * Dummy version of Valum's file uploader widget.
     * Overrides most everything so I can use a simple file widget.
     */
    init: function(o) {
        this._options = {
            onChange: function(input) {}
        };

        qq.extend(this._options, o);

        this._input =  $("#upload-file")[0];

        qq.attach(this._input, 'change', _.bind(function() {
            this._options.onChange(this._input);
        }, this));

        return this;
    },

    reset: function() {
        // It appears unnecessary to implement this.
    }
}

PANDA.views.Upload = Backbone.View.extend({
    el: $("#content"),

    events: {
        "click #upload-continue":      "continue_event"
    },

    template: PANDA.templates.upload,

    file_uploader: null,
    dataset: null,

    initialize: function() {
        _.bindAll(this, "render", "on_submit", "on_progress", "on_complete", "step_one", "step_two", "step_three", "continue_event");

        this.render();

        this.file_uploader = new qq.FileUploaderBasic({
            action: "/upload/",
            multiple: false,
            onSubmit: this.on_submit,
            onProgress: this.on_progress,
            onComplete: this.on_complete,
            showMessage: this.error_message
        });

        this.create_upload_button();
    },

    render: function() {
        this.el.html(this.template());
    },

    create_upload_button: function() {
        $("#upload-file-wrapper").html('<input type="file" id="upload-file" />');

        btn = CustomUploadButton.init({
            onChange: _.bind(function(input) {
                this.file_uploader._onInputChange(input);
            }, this)
        });

        this.file_uploader._button = btn;
    },

    on_submit: function(id, fileName) {
        /*
         * Handler for when a file upload starts.
         */
        this.step_two();
    },

    on_progress: function(id, fileName, loaded, total) {
        /*
         * Handler for when a file upload reports its progress.
         */
        pct = Math.floor(loaded / total * 100);
        $("#upload-progress").text(pct);
    },

    on_complete: function(id, fileName, responseJSON) {
        /*
         * Handler for when a file upload is completed.
         */
        if(responseJSON.success) {
            // Create a dataset and relate it to the upload
            this.dataset = new PANDA.models.Dataset({
                name: fileName,
                data_upload: responseJSON
            });

            // Save the new dataset
            this.dataset.save({}, {
                success: _.bind(function() {
                    // Once saved immediately begin importing it
                    this.dataset.import_data(this.step_three);
                }, this),
                error: _.bind(function() {
                    this.error_message('Error creating dataset!');
                }, this)
            });
        } else {
            this.error_message('Upload failed!');
        }
    },

    error_message: function(message) {
        $("#step-2-alert").alert("error", "
" + message + ' <input id="start-over" type="button" class="btn" value="Try again" />
' , false);
        $("#start-over").click(this.step_one);
    },

    step_one: function() {
        $(".alert-message").hide();
        $("#step-2").addClass("disabled");
        $("#step-3").addClass("disabled");
        $("#upload-continue").attr("disabled", true);

        $("#step-1").removeClass("disabled");
        $("#upload-file").attr("disabled", false);

        $("#upload-file").remove();
        this.create_upload_button();
    },

    step_two: function() {
        $("#step-1").addClass("disabled");
        $("#upload-file").attr("disabled", true);
        $("#step-3").addClass("disabled");
        $("#upload-continue").attr("disabled", true);

        $("#step-2").removeClass("disabled");
    },

    step_three: function() {
        $("#step-2").addClass("disabled");

        $("#step-3").removeClass("disabled");
        $("#upload-continue").attr("disabled", false);
    },

    continue_event: function() {
        window.location = "#dataset/" + this.dataset.get("id");
    }
});

<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';
import { getDeleteMessage } from '../constants/lang';
import { getFilenameFromPath } from '../utils/helpers';

export default class FilePreview extends Component {

  handleClickDelete(path) {
    const { onClickDelete } = this.props;
    const filename = getFilenameFromPath(path);
    const confirm = window.confirm(getDeleteMessage(filename));
    if (confirm) {
      onClickDelete(filename);
    }
  }

  render() {
    const { file } = this.props;
    const extension = file.extname.substring(1);
    const image = /png|jpg|gif|jpeg|svg/.test(extension);
    return (

        {
          image &&
        }
        {
          !image &&

        }

        {file.path}
        <button onClick={() => this.handleClickDelete(file.path)} className="delete" title="Delete file">x</button>

    );
  }
}

FilePreview.propTypes = {
  file: PropTypes.object.isRequired,
  onClickDelete: PropTypes.func.isRequired
};
<commit_msg>allow previewing images with uppercase filenames<commit_after>import React, { Component, PropTypes } from 'react';
import { getDeleteMessage } from '../constants/lang';
import { getFilenameFromPath } from '../utils/helpers';

export default class FilePreview extends Component {

  handleClickDelete(path) {
    const { onClickDelete } = this.props;
    const filename = getFilenameFromPath(path);
    const confirm = window.confirm(getDeleteMessage(filename));
    if (confirm) {
      onClickDelete(filename);
    }
  }

  render() {
    const { file } = this.props;
    const extension = file.extname.substring(1);
    const image = /png|jpg|gif|jpeg|svg/i.test(extension);
    return (

        {
          image &&
        }
        {
          !image &&

        }

        {file.path}
        <button onClick={() => this.handleClickDelete(file.path)} className="delete" title="Delete file">x</button>

    );
  }
}

FilePreview.propTypes = {
  file: PropTypes.object.isRequired,
  onClickDelete: PropTypes.func.isRequired
};
<|endoftext|>


<commit_before>// First param is the library name, as defined in woosh.libs
woosh.addTests('glow2-src', {
	'$preTest': function(prevTest, nextTest) {
		if (!prevTest) {
			window.MyObj = function() {};
			MyObj.prototype = new glow.events.Target;
		}

		switch (nextTest) {
			case 'Firing Custom Listeners':
				window.testObj = new MyObj();
				window.eventsFired = 0;

				testObj.on('testEvent', function() {
					eventsFired++
				});
				testObj.on('testEvent', function() {
					eventsFired++
				});
				testObj.on('testEvent', function() {
					eventsFired++
				});
				break;
		}
	},
	'Adding Custom Listeners': new woosh.Test(10000, function() {
		var obj = new MyObj();
		obj.on('testEvent', function() {});
		obj.on('testEvent', function() {});
		obj.on('testEvent', function() {});
	}),
	'Firing Custom Listeners': new woosh.Test(20000, function() {
		testObj.fire('testEvent');
		return eventsFired;
	}),
	'Removing Custom Listeners': new woosh.Test(10000, function() {
		var obj = new MyObj();
		function testListener() {}
		obj.on('test', testListener);
		obj.detach('test', testListener);

		obj.on('test', testListener);
		obj.detach('test', testListener);

		obj.on('test', testListener);
		obj.detach('test', testListener);
	})
});<commit_msg>added speed tests for hasListener<commit_after>// First param is the library name, as defined in woosh.libs
woosh.addTests('glow2-src', {
	'$preTest': function(prevTest, nextTest) {
		if (!prevTest) {
			window.MyObj = function() {};
			MyObj.prototype = new glow.events.Target;
		}

		switch (nextTest) {
			case 'Firing Custom Listeners':
				window.testObj = new MyObj();
				window.eventsFired = 0;

				testObj.on('testEvent', function() {
					eventsFired++
				});
				testObj.on('testEvent', function() {
					eventsFired++
				});
				testObj.on('testEvent', function() {
					eventsFired++
				});
				break;
		}
	},
	'Adding Custom Listeners': new woosh.Test(10000, function() {
		var obj = new MyObj();
		obj.on('testEvent', function() {});
		obj.on('testEvent', function() {});
		obj.on('testEvent', function() {});
	}),
	'Firing Custom Listeners': new woosh.Test(20000, function() {
		testObj.fire('testEvent');
		return eventsFired;
	}),
	'Removing Custom Listeners': new woosh.Test(10000, function() {
		var obj = new MyObj();
		function testListener() {}
		obj.on('test', testListener);
		obj.detach('test', testListener);

		obj.on('test', testListener);
		obj.detach('test', testListener);

		obj.on('test', testListener);
		obj.detach('test', testListener);
	}),
	'Checking for a listener': new woosh.Test(10000, function() {
		var obj = new MyObj();
		function testListener() {}
		obj.on('test', testListener);
		glow.events.hasListener([obj],'test');

	})
});<|endoftext|>


<commit_before>/*
 * The SVG object extensions are admittedly overkill; I wanted to learn to play
 * with svg.js
 */
SVG.Target = function(parent) {
  this.constructor.call(this, SVG.create('target'))
  // from largest to smallest
  this.rings = parent.doc().set();
  this.stand = parent.rect(0,0).center(0,0).attr({fill : '#AD871C'});

  var self = this;
  this.colors.forEach(function(color) {
    self.rings.add(parent.circle(0)
                      .attr({fill : color, stroke : '#000'}));
  });

  var gradient = parent.gradient('radial', function(stop) {
    stop.at({ offset: 0, color: '#eee', opacity: 0.8 })
    stop.at({ offset: 1, color: '#000', opacity: 0.2 })
  });
  gradient.from(0.3,0.3).to(0.3,0.3).radius(0.3);
  this.gradientRing = parent.circle(0).attr({fill : gradient});
}

// Inherit from SVG.Container
SVG.Target.prototype = new SVG.Shape();

['cx','cy'].forEach(function(prop) {
  SVG.Target.prototype[prop] = function(val) {
    if (val == null) {
      return this.gradientRing[prop]();
    } else {
      this.rings[prop](val);
      this.gradientRing[prop](val);
      this.repositionStand();
      return this;
    }
  }
});

['x','y'].forEach(function(prop) {
  SVG.Target.prototype[prop] = function(val) {
    if (val == null) {
      return this.gradientRing[prop]();
    } else {
      var cProp = 'c' + prop;
      this[cProp](val + this.radius());
      console.log(this[cProp]());
      return this;
    }
  }
});

SVG.extend(SVG.Target, {
  // from largest to smallest
  colors: ['#eee', 'black', 'blue', 'red', 'yellow', 'yellow'],
  repositionStand: function() {
    this.stand.cx(this.gradientRing.cx());
    // not a typo, align top with center
    this.stand.y(this.gradientRing.cy());
  },
  size: function(r) {
    var currDiameter = 2*r;
    var step = 2*r / this.colors.length;

    this.stand.size(1.3*r, 1.3*r);
    this.gradientRing.size(2*r, 2*r);
    this.repositionStand();

    this.rings.each(function() {
      // `this` refers to each ring
      this.size(currDiameter, currDiameter);
      currDiameter -= step;
    });
    return this;
  },
  radius: function() {
    return this.gradientRing.attr('rx');
  },
  placeGround: function(cx, by) {
    return this.cx(cx).y(by - this.stand.attr('height') - this.gradientRing.attr('ry'));
  }
});

SVG.extend(SVG.Container, {
  target : function(r) {
    // ring colors from outside in
    return this.put(new SVG.Target(this)).size(r).move(0,0);
  }
});

// config
var config = {
  numSlots : 9,
  debug : false
}
$(function() {
  var svg = SVG('game');
  var w = $('#game').width(), h = $('#game').height();
  // line at which target can be placed
  var skyGradient = svg.gradient('linear', function(stop) {
    stop.at({ offset: 0, color: '#888FF7', opacity: 0.8 })
    stop.at({ offset: 1, color: 'darkblue', opacity: 0.8 })
  }).from(0.5,0).to(0.5,1);
  var grassGradient = svg.gradient('linear', function(stop) {
    stop.at({ offset: 0, color: '#18A300', opacity: 0.8 })
    stop.at({ offset: 1, color: '#28DB09', opacity: 0.8 })
  }).from(0.5,0).to(0.5,1);
  // var grass = svg.rect(w,h*0.7).y(0.3*h).attr({'fill' : '#84FF3D'});
  // var sky = svg.rect(w,h*0.3).attr({'fill' : '#888FF7'});
  var grass = svg.rect(w,h*0.7).y(0.3*h).attr({fill : grassGradient});
  var sky = svg.rect(w,h*0.3).attr({fill : skyGradient});
  var targetGround = svg.line(0,0.7*h,w,0.7*h)
  if (config.debug) targetGround.stroke({ width: 1 });

  var slots = new Array(config.numSlots);
  function Slot(index) {
    this.center = (index + 0.5)*this.width;
    this.rightLine = null;
  }
  Slot.prototype.bottom = targetGround.attr('y1');
  Slot.prototype.width = w / slots.length;

  for (var i = 0; i < config.numSlots; ++i) {
    var slot = new Slot(i);
    var rightLineX = (i+1) * slot.width;
    slot.rightLine = svg.line(rightLineX, 0, rightLineX, h);
    if (config.debug) slot.rightLine.stroke({width:1});
    slots[i] = slot;
  }

  function updateSlot(i) {
    currSlot = i;
    target.placeGround(slots[i].center, slots[i].bottom);
    updateArrow(i);
  }

  function updateArrow(slot) {
    arrow.attr({
      x1 : slots[slot].center,
      x2 : slots[slot].center
    });
    var p1 = (slots[slot].center - 7) + ',' + arrowEnd;
    var p2 = (slots[slot].center)     + ',' + (arrowEnd-10);
    var p3 = (slots[slot].center + 7) + ',' + arrowEnd;
    arrow.attr({
      y1: arrowStart,
      y2: arrowEnd
    })
    arrowHead.attr({
      points : p1 + ' ' + p2 + ' ' + p3
    });
    forceLine.attr({
      x1 : slots[slot].center - 50,
      x2 : slots[slot].center + 50
    })
  }

  var defaultSlot = Math.round(slots.length/2-0.5);
  var currSlot = defaultSlot;
  var origArrowEnd = h - (h - slots[0].bottom)/2;
  var arrowEnd = origArrowEnd;
  var arrowStart = h;
  var arrowLength = origArrowEnd - arrowStart;
  var forceLineY = h - (h - slots[0].bottom)/4;

  var target = svg.target(40);
  var arrow  = svg.line(0,arrowStart,0,arrowEnd).stroke({width:2});
  var arrowHead = svg.polygon('').fill('none').stroke({ width: 1 });
  var forceLine = svg.line(0,forceLineY,0,forceLineY).stroke({width:5, color: '#f00'});
  updateSlot(defaultSlot);

  $('#target-pos').attr({ min : 0, max : slots.length, step: 1 });
  $('#target-pos').change(function() {
    updateSlot($(this).val());
  });

  $('#draw-force').change(function() {
    arrowEnd = origArrowEnd + parseInt($(this).val(),10);
    if (arrowEnd > forceLineY) {
      // shoot arrow
      var timeout;
      var shootAnimator = function() {
        console.log("shooting");
        if (arrowEnd < target.cy()+5) {
          clearTimeout(timeout);
          svg.text("Congrats! You hit perfectly!")
            .x(w/2)
            .y(h/8)
            .font({
              size: 40,
              anchor: 'middle',
              weight:'lighter',
              family: 'Segoe UI'
            });
          return;
        }
        arrowEnd -= 2;
        arrowStart = arrowEnd - arrowLength;
        updateSlot(currSlot);
        timeout = setTimeout(shootAnimator,1);
      }
      timeout = setTimeout(shootAnimator, 1);
    }
    updateArrow(currSlot);
  });

  var aimedCircle = svg.circle(10).fill('red').center(-100,-100);
  $('#draw-angle').change(function() {
    var angle = $(this).val();
    // perfect angle: 50
    // angle: 0   -> bottom -> target.y() + 2*target.r()
    // angle: 100 -> top    -> target.y()
    aimedCircle.center(target.cx(), target.y() + ((angle/100)*(2*target.radius())));
  });

});
<commit_msg>actually move stuff<commit_after>// config
var config = {
  numSlots : 9,
  debug : false
}
$(function() {
  var svg = SVG('game');
  var w = $('#game').width(), h = $('#game').height();
  // line at which target can be placed
  var skyGradient = svg.gradient('linear', function(stop) {
    stop.at({ offset: 0, color: '#888FF7', opacity: 0.8 })
    stop.at({ offset: 1, color: 'darkblue', opacity: 0.8 })
  }).from(0.5,0).to(0.5,1);
  var grassGradient = svg.gradient('linear', function(stop) {
    stop.at({ offset: 0, color: '#18A300', opacity: 0.8 })
    stop.at({ offset: 1, color: '#28DB09', opacity: 0.8 })
  }).from(0.5,0).to(0.5,1);
  // var grass = svg.rect(w,h*0.7).y(0.3*h).attr({'fill' : '#84FF3D'});
  // var sky = svg.rect(w,h*0.3).attr({'fill' : '#888FF7'});
  var grass = svg.rect(w,h*0.7).y(0.3*h).attr({fill : grassGradient});
  var sky = svg.rect(w,h*0.3).attr({fill : skyGradient});
  var targetGround = svg.line(0,0.7*h,w,0.7*h)
  if (config.debug) targetGround.stroke({ width: 1 });

  var slots = new Array(config.numSlots);
  function Slot(index) {
    this.center = (index + 0.5)*this.width;
    this.rightLine = null;
  }
  Slot.prototype.bottom = targetGround.attr('y1');
  Slot.prototype.width = w / slots.length;

  for (var i = 0; i < config.numSlots; ++i) {
    var slot = new Slot(i);
    var rightLineX = (i+1) * slot.width;
    slot.rightLine = svg.line(rightLineX, 0, rightLineX, h);
    if (config.debug) slot.rightLine.stroke({width:1});
    slots[i] = slot;
  }

  function updateSlot(i) {
    currSlot = i;
    target.placeGround(slots[i].center, slots[i].bottom);
    updateArrow(i);
  }

  function updateArrow(slot) {
    arrow.attr({
      x1 : slots[slot].center,
      x2 : slots[slot].center
    });
    var p1 = (slots[slot].center - 7) + ',' + arrowEnd;
    var p2 = (slots[slot].center)     + ',' + (arrowEnd-10);
    var p3 = (slots[slot].center + 7) + ',' + arrowEnd;
    arrow.attr({
      y1: arrowStart,
      y2: arrowEnd
    })
    arrowHead.attr({
      points : p1 + ' ' + p2 + ' ' + p3
    });
    forceLine.attr({
      x1 : slots[slot].center - 50,
      x2 : slots[slot].center + 50
    })
  }

  var defaultSlot = Math.round(slots.length/2-0.5);
  var currSlot = defaultSlot;
  var origArrowEnd = h - (h - slots[0].bottom)/2;
  var arrowEnd = origArrowEnd;
  var arrowStart = h;
  var arrowLength = origArrowEnd - arrowStart;
  var forceLineY = h - (h - slots[0].bottom)/4;

  var target = svg.target(40);
  var arrow  = svg.line(0,arrowStart,0,arrowEnd).stroke({width:2});
  var arrowHead = svg.polygon('').fill('none').stroke({ width: 1 });
  var forceLine = svg.line(0,forceLineY,0,forceLineY).stroke({width:5, color: '#f00'});
  updateSlot(defaultSlot);

  $('#target-pos').attr({ min : 0, max : slots.length, step: 1 });
  $('#target-pos').change(function() {
    updateSlot($(this).val());
  });

  $('#draw-force').change(function() {
    arrowEnd = origArrowEnd + parseInt($(this).val(),10);
    if (arrowEnd > forceLineY) {
      // shoot arrow
      var timeout;
      var shootAnimator = function() {
        console.log("shooting");
        if (arrowEnd < target.cy()+5) {
          clearTimeout(timeout);
          svg.text("Congrats! You hit perfectly!")
            .x(w/2)
            .y(h/8)
            .font({
              size: 40,
              anchor: 'middle',
              weight:'lighter',
              family: 'Segoe UI'
            });
          return;
        }
        arrowEnd -= 2;
        arrowStart = arrowEnd - arrowLength;
        updateSlot(currSlot);
        timeout = setTimeout(shootAnimator,1);
      }
      timeout = setTimeout(shootAnimator, 1);
    }
    updateArrow(currSlot);
  });

  var aimedCircle = svg.circle(10).fill('red').center(-100,-100);
  $('#draw-angle').change(function() {
    var angle = $(this).val();
    // perfect angle: 50
    // angle: 0   -> bottom -> target.y() + 2*target.r()
    // angle: 100 -> top    -> target.y()
    aimedCircle.center(target.cx(), target.y() + ((angle/100)*(2*target.radius())));
  });

});
<|endoftext|>


<commit_before>/*! Pickyfill - Offline caching for picturefill responsive image polyfill. Author: Rich Trott | Copyright: Regents of University of California, 2012 | License: MIT */

(function( w ) {
    "use strict";

    var localStorage = w.localStorage,
        applicationCache = w.applicationCache,
        image,
        imageSrc,
        dataUri,
        pf_index_string,
        pf_index,
        canvasTest = document.createElement('canvas');

    // Don't run any of this stuff if application cache doesn't exist or isn't being used,
    //     or localStorage or canvas are not available.
    if ( (! applicationCache) || (applicationCache.status === applicationCache.UNCACHED) ||
        (! localStorage) || (!(canvasTest.getContext && canvasTest.getContext('2d'))) ) {
            return;
    }

    pf_index_string = localStorage.getItem('pf_index') || '{}';
    pf_index = JSON.parse(pf_index_string);

    // We'll use this to clear the pickyfill cache when appcache updates.
    var clearCache = function () {
        localStorage.removeItem('pf_index');
        for (var prop in pf_index) {
            localStorage.removeItem(prop);
        }
    };

    // If appcache updates, clear the pickyfill cache.
    // Appcache == IE10 or later == no need to worry about attachEvent (IE8 and earlier)
    // Anything that has appcache is going to have addEventListener.
    applicationCache.addEventListener('updateready', clearCache, false);
    applicationCache.addEventListener('obsolete', clearCache, false);

    // If the updateready event or obsolete event has already fired, clear the pickyfill cache.
    if((applicationCache.status === applicationCache.UPDATEREADY) ||
        (applicationCache.status === applicationCache.OBSOLETE)) {
            clearCache();
    }

    var srcFromCacheRan = false;
    var srcFromCache = function ( ps ) {
        var sources, src, newSrc;

        // Loop the pictures
        for( var i = 0, il = ps.length; i < il; i++ ){
            if( ps[ i ].getAttribute( "data-picture" ) !== null ){

                sources = ps[ i ].getElementsByTagName( "div" );
                // See which ones are cached in localStorage. Use the cached value.
                for( var j = 0, jl = sources.length; j < jl; j++ ){
                    if ((src = sources[j].getAttribute( "data-src" )) !== null ) {
                        if ( pf_index.hasOwnProperty('pf_s_' + src)) {
                            newSrc = localStorage.getItem('pf_s_' + src);
                            if (newSrc !== null) {
                                sources[j].setAttribute('data-src', localStorage.getItem('pf_s_' + src));
                            }
                        }
                    }
                }
            }
        }
    };

    var cacheImage = function () {
        var canvas, ctx, imageSrc;

        imageSrc = this.getAttribute("src");
        if ((imageSrc === null) || (imageSrc.length === 0)) {
            return;
        }

        canvas = w.document.createElement("canvas");
        canvas.width = this.width;
        canvas.height = this.height;

        ctx = canvas.getContext("2d");
        ctx.drawImage(this, 0, 0);
        try {
            dataUri = canvas.toDataURL();
        } catch (e) {
            // TODO: Improve error handling here. For now, if canvas.toDataURL()
            //   throws an exception, don't cache the image and move on.
            return;
        }

        pf_index["pf_s_"+imageSrc] = 1;

        try {
            localStorage.setItem("pf_s_"+imageSrc, dataUri);
            localStorage.setItem("pf_index", JSON.stringify(pf_index));
        } catch (e) {
            // Caching failed but there's nothing to be done about it at this point.
            // Previously cached items are still usable. Carry on.
        }
    };

    w.picturefillOrig = w.picturefill;
    w.picturefill = function () {
        var ps = w.document.getElementsByTagName( "div" );

        if (! srcFromCacheRan ) {
            srcFromCacheRan = true;
            srcFromCache( ps );
        }

        w.picturefillOrig();

        // Loop the pictures
        for( var i = 0, il = ps.length; i < il; i++ ){
            if( ps[ i ].getAttribute( "data-picture" ) !== null ){

                image = ps[ i ].getElementsByTagName( "img" )[0];
                if (image) {
                    if ((imageSrc = image.getAttribute("src")) !== null) {
                        if (imageSrc.substr(0,5) !== "data:") {
                            image.addEventListener("load", cacheImage, false);
                        }
                    }
                }
            }
        }
    };

}( this ));
<commit_msg>Do not cache items larger than 128 Kb. If an item fails to cache, remove it from the cache index.<commit_after>/*! Pickyfill - Offline caching for picturefill responsive image polyfill. Author: Rich Trott | Copyright: Regents of University of California, 2012 | License: MIT */

(function( w ) {
    "use strict";

    var localStorage = w.localStorage,
        applicationCache = w.applicationCache,
        image,
        imageSrc,
        dataUri,
        pf_index_string,
        pf_index,
        canvasTest = document.createElement('canvas');

    // Don't run any of this stuff if application cache doesn't exist or isn't being used,
    //     or localStorage or canvas are not available.
    if ( (! applicationCache) || (applicationCache.status === applicationCache.UNCACHED) ||
        (! localStorage) || (!(canvasTest.getContext && canvasTest.getContext('2d'))) ) {
            return;
    }

    pf_index_string = localStorage.getItem('pf_index') || '{}';
    pf_index = JSON.parse(pf_index_string);

    // We'll use this to clear the pickyfill cache when appcache updates.
    var clearCache = function () {
        localStorage.removeItem('pf_index');
        for (var prop in pf_index) {
            localStorage.removeItem(prop);
        }
    };

    // If appcache updates, clear the pickyfill cache.
    // Appcache == IE10 or later == no need to worry about attachEvent (IE8 and earlier)
    // Anything that has appcache is going to have addEventListener.
    applicationCache.addEventListener('updateready', clearCache, false);
    applicationCache.addEventListener('obsolete', clearCache, false);

    // If the updateready event or obsolete event has already fired, clear the pickyfill cache.
    if((applicationCache.status === applicationCache.UPDATEREADY) ||
        (applicationCache.status === applicationCache.OBSOLETE)) {
            clearCache();
    }

    var srcFromCacheRan = false;
    var srcFromCache = function ( ps ) {
        var sources, src, newSrc;

        // Loop the pictures
        for( var i = 0, il = ps.length; i < il; i++ ){
            if( ps[ i ].getAttribute( "data-picture" ) !== null ){

                sources = ps[ i ].getElementsByTagName( "div" );
                // See which ones are cached in localStorage. Use the cached value.
                for( var j = 0, jl = sources.length; j < jl; j++ ){
                    if ((src = sources[j].getAttribute( "data-src" )) !== null ) {
                        if ( pf_index.hasOwnProperty('pf_s_' + src)) {
                            newSrc = localStorage.getItem('pf_s_' + src);
                            if (newSrc !== null) {
                                sources[j].setAttribute('data-src', localStorage.getItem('pf_s_' + src));
                            }
                        }
                    }
                }
            }
        }
    };

    var cacheImage = function () {
        var canvas, ctx, imageSrc;

        imageSrc = this.getAttribute("src");
        if ((imageSrc === null) || (imageSrc.length === 0)) {
            return;
        }

        canvas = w.document.createElement("canvas");
        canvas.width = this.width;
        canvas.height = this.height;

        ctx = canvas.getContext("2d");
        ctx.drawImage(this, 0, 0);
        try {
            dataUri = canvas.toDataURL();

        } catch (e) {
            // TODO: Improve error handling here. For now, if canvas.toDataURL()
            //   throws an exception, don't cache the image and move on.
            return;
        }

        // Do not cache if the resulting cache item will take more than 128Kb.
        if (dataUri.length > 131072) {
            return;
        }

        pf_index["pf_s_"+imageSrc] = 1;

        try {
            localStorage.setItem("pf_s_"+imageSrc, dataUri);
            localStorage.setItem("pf_index", JSON.stringify(pf_index));
        } catch (e) {
            // Caching failed. Remove item from index object so next cached item
            //   doesn't wrongly indicate this item was successfully cached.
            delete pf_index["pf_s_"+imageSrc];
        }
    };

    w.picturefillOrig = w.picturefill;
    w.picturefill = function () {
        var ps = w.document.getElementsByTagName( "div" );

        if (! srcFromCacheRan ) {
            srcFromCacheRan = true;
            srcFromCache( ps );
        }

        w.picturefillOrig();

        // Loop the pictures
        for( var i = 0, il = ps.length; i < il; i++ ){
            if( ps[ i ].getAttribute( "data-picture" ) !== null ){

                image = ps[ i ].getElementsByTagName( "img" )[0];
                if (image) {
                    if ((imageSrc = image.getAttribute("src")) !== null) {
                        if (imageSrc.substr(0,5) !== "data:") {
                            image.addEventListener("load", cacheImage, false);
                        }
                    }
                }
            }
        }
    };

}( this ));
<|endoftext|>


<commit_before>import Controller from '@ember/controller';
import envConfig from 'ghost-admin/config/environment';
import {action} from '@ember/object';
import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';

const CURRENCIES = currencies.map((currency) => {
    return {
        value: currency.isoCode.toLowerCase(),
        label: `${currency.isoCode} - ${currency.name}`,
        isoCode: currency.isoCode
    };
});

export default class MembersAccessController extends Controller {
    @service config;
    @service membersUtils;
    @service settings;
    @service store;

    @tracked showLeavePortalModal = false;
    @tracked showLeaveRouteModal = false;
    @tracked showPortalSettings = false;
    @tracked showStripeConnect = false;

    @tracked product = null;
    @tracked stripePrices = [];
    @tracked paidSignupRedirect;
    @tracked freeSignupRedirect;
    @tracked stripeMonthlyAmount = 5;
    @tracked stripeYearlyAmount = 50;
    @tracked currency = 'usd';
    @tracked stripePlanError = '';

    @tracked portalPreviewUrl = '';
    @tracked portalPreviewGuid = Date.now().valueOf();

    queryParams = ['showPortalSettings'];

    get allCurrencies() {
        return getCurrencyOptions();
    }

    get siteUrl() {
        return this.config.get('blogUrl');
    }

    get selectedCurrency() {
        return CURRENCIES.findBy('value', this.currency);
    }

    get isConnectDisallowed() {
        const siteUrl = this.config.get('blogUrl');
        return envConfig.environment !== 'development' && !/^https:/.test(siteUrl);
    }

    get hasChangedPrices() {
        if (this.product) {
            this.stripePrices = this.product.get('stripePrices') || [];
            const activePrices = this.stripePrices.filter(price => !!price.active);
            const monthlyPrice = this.getPrice(activePrices, 'monthly');
            const yearlyPrice = this.getPrice(activePrices, 'yearly');

            if (monthlyPrice?.amount && parseInt(this.stripeMonthlyAmount, 10) !== (monthlyPrice.amount / 100)) {
                return true;
            }
            if (yearlyPrice?.amount && parseInt(this.stripeYearlyAmount, 10) !== (yearlyPrice.amount / 100)) {
                return true;
            }
        }

        return false;
    }

    @action
    setup() {
        this.fetchDefaultProduct.perform();
        this.updatePortalPreview();
    }

    leaveRoute(transition) {
        if (this.settings.get('hasDirtyAttributes') || this.hasChangedPrices) {
            transition.abort();
            this.leaveSettingsTransition = transition;
            this.showLeaveRouteModal = true;
        }
    }

    @action
    async confirmLeave() {
        this.settings.rollbackAttributes();
        this.resetPrices();
        this.leaveSettingsTransition.retry();
    }

    @action
    cancelLeave() {
        this.showLeaveRouteModal = false;
        this.leaveSettingsTransition = null;
    }

    @action
    async membersSubscriptionAccessChanged() {
        const oldValue = this.settings.changedAttributes().membersSignupAccess?.[0];

        if (oldValue === 'none') {
            // when saved value is 'none' the server won't inject the portal script
            // to work around that and show the expected portal preview we save and
            // force a refresh
            await this.saveSettingsTask.perform();
            this.updatePortalPreview();
            this.portalPreviewGuid = Date.now().valueOf();
        } else {
            this.updatePortalPreview();
        }
    }

    @action
    setStripePlansCurrency(event) {
        const newCurrency = event.value;
        this.currency = newCurrency;
    }

    @action
    setPaidSignupRedirect(url) {
        this.paidSignupRedirect = url;
    }

    @action
    setFreeSignupRedirect(url) {
        this.freeSignupRedirect = url;
    }

    @action
    validatePaidSignupRedirect() {
        return this._validateSignupRedirect(this.paidSignupRedirect, 'membersPaidSignupRedirect');
    }

    @action
    validateFreeSignupRedirect() {
        return this._validateSignupRedirect(this.freeSignupRedirect, 'membersFreeSignupRedirect');
    }

    @action
    validateStripePlans() {
        this.stripePlanError = undefined;

        try {
            const yearlyAmount = this.stripeYearlyAmount;
            const monthlyAmount = this.stripeMonthlyAmount;
            const symbol = getSymbol(this.currency);
            if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) {
                throw new TypeError(`Subscription amount must be at least ${symbol}1.00`);
            }

            this.updatePortalPreview();
        } catch (err) {
            this.stripePlanError = err.message;
        }
    }

    @action
    openStripeConnect() {
        this.stripeEnabledOnOpen = this.membersUtils.isStripeEnabled;
        this.showStripeConnect = true;
    }

    @action
    async closeStripeConnect() {
        if (this.stripeEnabledOnOpen !== this.membersUtils.isStripeEnabled) {
            await this.saveSettingsTask.perform();
            this.portalPreviewGuid = Date.now().valueOf();
        }
        this.showStripeConnect = false;
    }

    @action
    openPortalSettings() {
        this.saveSettingsTask.perform();
        this.showPortalSettings = true;
    }

    @action
    closePortalSettings() {
        const changedAttributes = this.settings.changedAttributes();
        if (changedAttributes && Object.keys(changedAttributes).length > 0) {
            this.showLeavePortalModal = true;
        } else {
            this.showPortalSettings = false;
            this.updatePortalPreview();
        }
    }

    @action
    async confirmClosePortalSettings() {
        this.settings.rollbackAttributes();
        this.showPortalSettings = false;
        this.showLeavePortalModal = false;
        this.updatePortalPreview();
    }

    @action
    cancelClosePortalSettings() {
        this.showLeavePortalModal = false;
    }

    @action
    updatePortalPreview() {
        // TODO: can these be worked out from settings in membersUtils?
        const monthlyPrice = this.stripeMonthlyAmount * 100;
        const yearlyPrice = this.stripeYearlyAmount * 100;
        let portalPlans = this.settings.get('portalPlans') || [];
        const currentMontlyPriceId = this.settings.get('membersMonthlyPriceId');
        const currentYearlyPriceId = this.settings.get('membersYearlyPriceId');
        let isMonthlyChecked = false;
        let isYearlyChecked = false;
        if (portalPlans.includes(currentMontlyPriceId)) {
            isMonthlyChecked = true;
        }

        if (portalPlans.includes(currentYearlyPriceId)) {
            isYearlyChecked = true;
        }

        this.portalPreviewUrl = this.membersUtils.getPortalPreviewUrl({
            button: false,
            monthlyPrice,
            yearlyPrice,
            currency: this.currency,
            isMonthlyChecked,
            isYearlyChecked,
            portalPlans: null
        });

        this.resizePortalPreviewTask.perform();
    }

    @action
    portalPreviewLoaded(event) {
        this.portalPreviewIframe = event.target;
        this.resizePortalPreviewTask.perform();
    }

    @action
    portalPreviewDestroyed() {
        this.portalPreviewIframe = null;
        this.resizePortalPreviewTask.cancelAll();
    }

    @task({restartable: true})
    *resizePortalPreviewTask() {
        if (this.portalPreviewIframe && this.portalPreviewIframe.contentWindow) {
            yield timeout(100); // give time for portal to re-render

            const portalIframe = this.portalPreviewIframe.contentWindow.document.querySelector('#ghost-portal-root iframe');
            if (!portalIframe) {
                return;
            }

            const portalContainer = portalIframe.contentWindow.document.querySelector('.gh-portal-popup-container');
            if (!portalContainer) {
                return;
            }

            const height = portalContainer.clientHeight;
            this.portalPreviewIframe.parentNode.style.height = `${height}px`;
        }
    }

    async saveProduct() {
        if (this.product) {
            const stripePrices = this.product.stripePrices || [];
            const monthlyAmount = this.stripeMonthlyAmount * 100;
            const yearlyAmount = this.stripeYearlyAmount * 100;
            const getActivePrice = (prices, type, amount) => {
                return prices.find((price) => {
                    return (
                        price.active && price.amount === amount && price.type === 'recurring' &&
                        price.interval === type && price.currency.toLowerCase() === this.currency.toLowerCase()
                    );
                });
            };
            const monthlyPrice = getActivePrice(stripePrices, 'month', monthlyAmount);
            const yearlyPrice = getActivePrice(stripePrices, 'year', yearlyAmount);

            if (!monthlyPrice) {
                stripePrices.push(
                    {
                        nickname: 'Monthly',
                        amount: monthlyAmount,
                        active: 1,
                        currency: this.currency,
                        interval: 'month',
                        type: 'recurring'
                    }
                );
            }
            if (!yearlyPrice) {
                stripePrices.push(
                    {
                        nickname: 'Yearly',
                        amount: this.stripeYearlyAmount * 100,
                        active: 1,
                        currency: this.currency,
                        interval: 'year',
                        type: 'recurring'
                    }
                );
            }
            if (monthlyPrice && yearlyPrice) {
                this.updatePortalPlans(monthlyPrice.id, yearlyPrice.id);
                this.settings.set('membersMonthlyPriceId', monthlyPrice.id);
                this.settings.set('membersYearlyPriceId', yearlyPrice.id);
                return this.product;
            } else {
                this.product.set('stripePrices', stripePrices);
                const savedProduct = await this.product.save();
                const updatedStripePrices = savedProduct.stripePrices || [];
                const updatedMonthlyPrice = getActivePrice(updatedStripePrices, 'month', monthlyAmount);
                const updatedYearlyPrice = getActivePrice(updatedStripePrices, 'year', yearlyAmount);
                this.updatePortalPlans(updatedMonthlyPrice.id, updatedYearlyPrice.id);
                this.settings.set('membersMonthlyPriceId', updatedMonthlyPrice.id);
                this.settings.set('membersYearlyPriceId', updatedYearlyPrice.id);
                return savedProduct;
            }
        }
    }

    updatePortalPlans(monthlyPriceId, yearlyPriceId) {
        let portalPlans = this.settings.get('portalPlans') || [];
        const currentMontlyPriceId = this.settings.get('membersMonthlyPriceId');
        const currentYearlyPriceId = this.settings.get('membersYearlyPriceId');
        if (portalPlans.includes(currentMontlyPriceId)) {
            portalPlans = portalPlans.filter(priceId => priceId !== currentMontlyPriceId);
            portalPlans.pushObject(monthlyPriceId);
        }

        if (portalPlans.includes(currentYearlyPriceId)) {
            portalPlans = portalPlans.filter(priceId => priceId !== currentYearlyPriceId);
            portalPlans.pushObject(yearlyPriceId);
        }
        this.settings.set('portalPlans', portalPlans);
    }

    getPrice(prices, type) {
        const monthlyPriceId = this.settings.get('membersMonthlyPriceId');
        const yearlyPriceId = this.settings.get('membersYearlyPriceId');

        if (type === 'monthly') {
            return (
                prices.find(price => price.id === monthlyPriceId) ||
                prices.find(price => price.nickname === 'Monthly') ||
                prices.find(price => price.interval === 'month')
            );
        }

        if (type === 'yearly') {
            return (
                prices.find(price => price.id === yearlyPriceId) ||
                prices.find(price => price.nickname === 'Yearly') ||
                prices.find(price => price.interval === 'year')
            );
        }
        return null;
    }

    @task({drop: true})
    *fetchDefaultProduct() {
        const products = yield this.store.query('product', {include: 'stripe_prices'});
        this.product = products.firstObject;
        this.stripePrices = [];
        if (this.product) {
            this.stripePrices = this.product.get('stripePrices') || [];
            const activePrices = this.stripePrices.filter(price => !!price.active);
            const monthlyPrice = this.getPrice(activePrices, 'monthly');
            const yearlyPrice = this.getPrice(activePrices, 'yearly');
            if (monthlyPrice && monthlyPrice.amount) {
                this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
                this.currency = monthlyPrice.currency;
            }
            if (yearlyPrice && yearlyPrice.amount) {
                this.stripeYearlyAmount = (yearlyPrice.amount / 100);
            }
            this.updatePortalPreview();
        }
    }

    @task({drop: true})
    *saveSettingsTask() {
        yield this.validateStripePlans();

        if (this.stripePlanError) {
            return;
        }

        if (this.settings.get('errors').length !== 0) {
            return;
        }

        yield this.saveProduct();
        const result = yield this.settings.save();

        this.updatePortalPreview();

        return result;
    }

    resetPrices() {
        const activePrices = this.stripePrices.filter(price => !!price.active);
        const monthlyPrice = this.getPrice(activePrices, 'monthly');
        const yearlyPrice = this.getPrice(activePrices, 'yearly');

        this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
        this.stripeYearlyAmount = (yearlyPrice.amount / 100);
    }

    reset() {
        this.showLeaveRouteModal = false;
        this.showLeavePortalModal = false;
        this.showPortalSettings = false;
    }

    _validateSignupRedirect(url, type) {
        const siteUrl = this.config.get('blogUrl');
        let errMessage = `Please enter a valid URL`;
        this.settings.get('errors').remove(type);
        this.settings.get('hasValidated').removeObject(type);

        if (url === null) {
            this.settings.get('errors').add(type, errMessage);
            this.settings.get('hasValidated').pushObject(type);
            return false;
        }

        if (url === undefined) {
            // Not initialised
            return;
        }

        if (url.href.startsWith(siteUrl)) {
            const path = url.href.replace(siteUrl, '');
            this.settings.set(type, path);
        } else {
            this.settings.set(type, url.href);
        }
    }
}
<commit_msg>Fixed price reset when no monthly/yearly prices<commit_after>import Controller from '@ember/controller';
import envConfig from 'ghost-admin/config/environment';
import {action} from '@ember/object';
import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';

const CURRENCIES = currencies.map((currency) => {
    return {
        value: currency.isoCode.toLowerCase(),
        label: `${currency.isoCode} - ${currency.name}`,
        isoCode: currency.isoCode
    };
});

export default class MembersAccessController extends Controller {
    @service config;
    @service membersUtils;
    @service settings;
    @service store;

    @tracked showLeavePortalModal = false;
    @tracked showLeaveRouteModal = false;
    @tracked showPortalSettings = false;
    @tracked showStripeConnect = false;

    @tracked product = null;
    @tracked stripePrices = [];
    @tracked paidSignupRedirect;
    @tracked freeSignupRedirect;
    @tracked stripeMonthlyAmount = 5;
    @tracked stripeYearlyAmount = 50;
    @tracked currency = 'usd';
    @tracked stripePlanError = '';

    @tracked portalPreviewUrl = '';
    @tracked portalPreviewGuid = Date.now().valueOf();

    queryParams = ['showPortalSettings'];

    get allCurrencies() {
        return getCurrencyOptions();
    }

    get siteUrl() {
        return this.config.get('blogUrl');
    }

    get selectedCurrency() {
        return CURRENCIES.findBy('value', this.currency);
    }

    get isConnectDisallowed() {
        const siteUrl = this.config.get('blogUrl');
        return envConfig.environment !== 'development' && !/^https:/.test(siteUrl);
    }

    get hasChangedPrices() {
        if (this.product) {
            this.stripePrices = this.product.get('stripePrices') || [];
            const activePrices = this.stripePrices.filter(price => !!price.active);
            const monthlyPrice = this.getPrice(activePrices, 'monthly');
            const yearlyPrice = this.getPrice(activePrices, 'yearly');

            if (monthlyPrice?.amount && parseInt(this.stripeMonthlyAmount, 10) !== (monthlyPrice.amount / 100)) {
                return true;
            }
            if (yearlyPrice?.amount && parseInt(this.stripeYearlyAmount, 10) !== (yearlyPrice.amount / 100)) {
                return true;
            }
        }

        return false;
    }

    @action
    setup() {
        this.fetchDefaultProduct.perform();
        this.updatePortalPreview();
    }

    leaveRoute(transition) {
        if (this.settings.get('hasDirtyAttributes') || this.hasChangedPrices) {
            transition.abort();
            this.leaveSettingsTransition = transition;
            this.showLeaveRouteModal = true;
        }
    }

    @action
    async confirmLeave() {
        this.settings.rollbackAttributes();
        this.resetPrices();
        this.leaveSettingsTransition.retry();
    }

    @action
    cancelLeave() {
        this.showLeaveRouteModal = false;
        this.leaveSettingsTransition = null;
    }

    @action
    async membersSubscriptionAccessChanged() {
        const oldValue = this.settings.changedAttributes().membersSignupAccess?.[0];

        if (oldValue === 'none') {
            // when saved value is 'none' the server won't inject the portal script
            // to work around that and show the expected portal preview we save and
            // force a refresh
            await this.saveSettingsTask.perform();
            this.updatePortalPreview();
            this.portalPreviewGuid = Date.now().valueOf();
        } else {
            this.updatePortalPreview();
        }
    }

    @action
    setStripePlansCurrency(event) {
        const newCurrency = event.value;
        this.currency = newCurrency;
    }

    @action
    setPaidSignupRedirect(url) {
        this.paidSignupRedirect = url;
    }

    @action
    setFreeSignupRedirect(url) {
        this.freeSignupRedirect = url;
    }

    @action
    validatePaidSignupRedirect() {
        return this._validateSignupRedirect(this.paidSignupRedirect, 'membersPaidSignupRedirect');
    }

    @action
    validateFreeSignupRedirect() {
        return this._validateSignupRedirect(this.freeSignupRedirect, 'membersFreeSignupRedirect');
    }

    @action
    validateStripePlans() {
        this.stripePlanError = undefined;

        try {
            const yearlyAmount = this.stripeYearlyAmount;
            const monthlyAmount = this.stripeMonthlyAmount;
            const symbol = getSymbol(this.currency);
            if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) {
                throw new TypeError(`Subscription amount must be at least ${symbol}1.00`);
            }

            this.updatePortalPreview();
        } catch (err) {
            this.stripePlanError = err.message;
        }
    }

    @action
    openStripeConnect() {
        this.stripeEnabledOnOpen = this.membersUtils.isStripeEnabled;
        this.showStripeConnect = true;
    }

    @action
    async closeStripeConnect() {
        if (this.stripeEnabledOnOpen !== this.membersUtils.isStripeEnabled) {
            await this.saveSettingsTask.perform();
            this.portalPreviewGuid = Date.now().valueOf();
        }
        this.showStripeConnect = false;
    }

    @action
    openPortalSettings() {
        this.saveSettingsTask.perform();
        this.showPortalSettings = true;
    }

    @action
    closePortalSettings() {
        const changedAttributes = this.settings.changedAttributes();
        if (changedAttributes && Object.keys(changedAttributes).length > 0) {
            this.showLeavePortalModal = true;
        } else {
            this.showPortalSettings = false;
            this.updatePortalPreview();
        }
    }

    @action
    async confirmClosePortalSettings() {
        this.settings.rollbackAttributes();
        this.showPortalSettings = false;
        this.showLeavePortalModal = false;
        this.updatePortalPreview();
    }

    @action
    cancelClosePortalSettings() {
        this.showLeavePortalModal = false;
    }

    @action
    updatePortalPreview() {
        // TODO: can these be worked out from settings in membersUtils?
        const monthlyPrice = this.stripeMonthlyAmount * 100;
        const yearlyPrice = this.stripeYearlyAmount * 100;
        let portalPlans = this.settings.get('portalPlans') || [];
        const currentMontlyPriceId = this.settings.get('membersMonthlyPriceId');
        const currentYearlyPriceId = this.settings.get('membersYearlyPriceId');
        let isMonthlyChecked = false;
        let isYearlyChecked = false;
        if (portalPlans.includes(currentMontlyPriceId)) {
            isMonthlyChecked = true;
        }

        if (portalPlans.includes(currentYearlyPriceId)) {
            isYearlyChecked = true;
        }

        this.portalPreviewUrl = this.membersUtils.getPortalPreviewUrl({
            button: false,
            monthlyPrice,
            yearlyPrice,
            currency: this.currency,
            isMonthlyChecked,
            isYearlyChecked,
            portalPlans: null
        });

        this.resizePortalPreviewTask.perform();
    }

    @action
    portalPreviewLoaded(event) {
        this.portalPreviewIframe = event.target;
        this.resizePortalPreviewTask.perform();
    }

    @action
    portalPreviewDestroyed() {
        this.portalPreviewIframe = null;
        this.resizePortalPreviewTask.cancelAll();
    }

    @task({restartable: true})
    *resizePortalPreviewTask() {
        if (this.portalPreviewIframe && this.portalPreviewIframe.contentWindow) {
            yield timeout(100); // give time for portal to re-render

            const portalIframe = this.portalPreviewIframe.contentWindow.document.querySelector('#ghost-portal-root iframe');
            if (!portalIframe) {
                return;
            }

            const portalContainer = portalIframe.contentWindow.document.querySelector('.gh-portal-popup-container');
            if (!portalContainer) {
                return;
            }

            const height = portalContainer.clientHeight;
            this.portalPreviewIframe.parentNode.style.height = `${height}px`;
        }
    }

    async saveProduct() {
        if (this.product) {
            const stripePrices = this.product.stripePrices || [];
            const monthlyAmount = this.stripeMonthlyAmount * 100;
            const yearlyAmount = this.stripeYearlyAmount * 100;
            const getActivePrice = (prices, type, amount) => {
                return prices.find((price) => {
                    return (
                        price.active && price.amount === amount && price.type === 'recurring' &&
                        price.interval === type && price.currency.toLowerCase() === this.currency.toLowerCase()
                    );
                });
            };
            const monthlyPrice = getActivePrice(stripePrices, 'month', monthlyAmount);
            const yearlyPrice = getActivePrice(stripePrices, 'year', yearlyAmount);

            if (!monthlyPrice) {
                stripePrices.push(
                    {
                        nickname: 'Monthly',
                        amount: monthlyAmount,
                        active: 1,
                        currency: this.currency,
                        interval: 'month',
                        type: 'recurring'
                    }
                );
            }
            if (!yearlyPrice) {
                stripePrices.push(
                    {
                        nickname: 'Yearly',
                        amount: this.stripeYearlyAmount * 100,
                        active: 1,
                        currency: this.currency,
                        interval: 'year',
                        type: 'recurring'
                    }
                );
            }
            if (monthlyPrice && yearlyPrice) {
                this.updatePortalPlans(monthlyPrice.id, yearlyPrice.id);
                this.settings.set('membersMonthlyPriceId', monthlyPrice.id);
                this.settings.set('membersYearlyPriceId', yearlyPrice.id);
                return this.product;
            } else {
                this.product.set('stripePrices', stripePrices);
                const savedProduct = await this.product.save();
                const updatedStripePrices = savedProduct.stripePrices || [];
                const updatedMonthlyPrice = getActivePrice(updatedStripePrices, 'month', monthlyAmount);
                const updatedYearlyPrice = getActivePrice(updatedStripePrices, 'year', yearlyAmount);
                this.updatePortalPlans(updatedMonthlyPrice.id, updatedYearlyPrice.id);
                this.settings.set('membersMonthlyPriceId', updatedMonthlyPrice.id);
                this.settings.set('membersYearlyPriceId', updatedYearlyPrice.id);
                return savedProduct;
            }
        }
    }

    updatePortalPlans(monthlyPriceId, yearlyPriceId) {
        let portalPlans = this.settings.get('portalPlans') || [];
        const currentMontlyPriceId = this.settings.get('membersMonthlyPriceId');
        const currentYearlyPriceId = this.settings.get('membersYearlyPriceId');
        if (portalPlans.includes(currentMontlyPriceId)) {
            portalPlans = portalPlans.filter(priceId => priceId !== currentMontlyPriceId);
            portalPlans.pushObject(monthlyPriceId);
        }

        if (portalPlans.includes(currentYearlyPriceId)) {
            portalPlans = portalPlans.filter(priceId => priceId !== currentYearlyPriceId);
            portalPlans.pushObject(yearlyPriceId);
        }
        this.settings.set('portalPlans', portalPlans);
    }

    getPrice(prices, type) {
        const monthlyPriceId = this.settings.get('membersMonthlyPriceId');
        const yearlyPriceId = this.settings.get('membersYearlyPriceId');

        if (type === 'monthly') {
            return (
                prices.find(price => price.id === monthlyPriceId) ||
                prices.find(price => price.nickname === 'Monthly') ||
                prices.find(price => price.interval === 'month')
            );
        }

        if (type === 'yearly') {
            return (
                prices.find(price => price.id === yearlyPriceId) ||
                prices.find(price => price.nickname === 'Yearly') ||
                prices.find(price => price.interval === 'year')
            );
        }
        return null;
    }

    @task({drop: true})
    *fetchDefaultProduct() {
        const products = yield this.store.query('product', {include: 'stripe_prices'});
        this.product = products.firstObject;
        this.stripePrices = [];
        if (this.product) {
            this.stripePrices = this.product.get('stripePrices') || [];
            const activePrices = this.stripePrices.filter(price => !!price.active);
            const monthlyPrice = this.getPrice(activePrices, 'monthly');
            const yearlyPrice = this.getPrice(activePrices, 'yearly');
            if (monthlyPrice && monthlyPrice.amount) {
                this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
                this.currency = monthlyPrice.currency;
            }
            if (yearlyPrice && yearlyPrice.amount) {
                this.stripeYearlyAmount = (yearlyPrice.amount / 100);
            }
            this.updatePortalPreview();
        }
    }

    @task({drop: true})
    *saveSettingsTask() {
        yield this.validateStripePlans();

        if (this.stripePlanError) {
            return;
        }

        if (this.settings.get('errors').length !== 0) {
            return;
        }

        yield this.saveProduct();
        const result = yield this.settings.save();

        this.updatePortalPreview();

        return result;
    }

    resetPrices() {
        const activePrices = this.stripePrices.filter(price => !!price.active);
        const monthlyPrice = this.getPrice(activePrices, 'monthly');
        const yearlyPrice = this.getPrice(activePrices, 'yearly');

        this.stripeMonthlyAmount = monthlyPrice ? (monthlyPrice.amount / 100) : 5;
        this.stripeYearlyAmount = yearlyPrice ? (yearlyPrice.amount / 100) : 50;
    }

    reset() {
        this.showLeaveRouteModal = false;
        this.showLeavePortalModal = false;
        this.showPortalSettings = false;
    }

    _validateSignupRedirect(url, type) {
        const siteUrl = this.config.get('blogUrl');
        let errMessage = `Please enter a valid URL`;
        this.settings.get('errors').remove(type);
        this.settings.get('hasValidated').removeObject(type);

        if (url === null) {
            this.settings.get('errors').add(type, errMessage);
            this.settings.get('hasValidated').pushObject(type);
            return false;
        }

        if (url === undefined) {
            // Not initialised
            return;
        }

        if (url.href.startsWith(siteUrl)) {
            const path = url.href.replace(siteUrl, '');
            this.settings.set(type, path);
        } else {
            this.settings.set(type, url.href);
        }
    }
}
<|endoftext|>


<commit_before>"use strict";

var Q = require("q");
var mongo = require("mongodb");
var ObjectID = mongo.ObjectID;
var MongoClient = mongo.MongoClient;
var _ = require("lodash");
var config = require("../config/config.json");
// TODO replace the default values in the configuration file (and please, do not push them into github :))
var connectionString = "mongodb://" + config.user + ":" + config.password + "@" + config.url + ":" + config.databasePort + "/" + config.database;
var database;

// Collections
// NOTE if the collections related code gets too out of hand, move it to its own module
var dashboard;
var components;

var init = function() {
    var deferred = Q.defer();

    Q.ninvoke(MongoClient, "connect", connectionString)
        .then(function(connectedDatabase) {
            database = connectedDatabase;
            dashboard = getCollection("dashboard")();
            components = getCollection("components")();
            deferred.resolve(true);
        })
        .catch(deferred.reject)
        .done();

    return deferred.promise;
};

var getCollection = function(collection) {
    if (!database) {
        throw "No connection to database.";
    }

    return function() {
        console.log("Returning requested collection.", collection);
        return database.collection(collection);
    };
};

var find = function(collection, query) {
   var deferred = Q.defer();

    if (collection.find === undefined) {
        deferred.reject("No find function in collection.");
        return;
    }

    collection
        .find(query)
        .toArray(function(error, documents) {
            if (error) {
                console.log("Error getting documents for collection.", error);
                deferred.reject(documents);
                return;
            }

            console.log("documents:", documents);
            deferred.resolve(documents);
        });

    return deferred.promise;
};

var collectComponentsFromRows = function(rows) {
    // the columns are built of components (columns, single column = single component)

    var component_ids = _.flatten(rows.map(function(rowColumns) {
        return rowColumns.map(function(component_id) {
            return new ObjectID(component_id);
        });
    }));

    console.log("Collected component_ids for Dashboard.", component_ids);
    return component_ids;
};

module.exports = {
    init: init,
    getCollection: getCollection,

    findComponentsForDashboard: function(rows_component_ids, transformer) {
        if (transformer !== undefined) {
            var deferred = Q.defer();

            // transformer enabled where the results are projected/transformed into something else
            find(components, { _id: { $in: collectComponentsFromRows(rows_component_ids) } })
                .then(function(results) {
                    deferred.resolve(transformer(results));
                })
                .catch(function(error) {
                    console.error(error);
                    deferred.reject(error);
                })
                .done();

            return deferred.promise;
        } else {
            // raw resulting query
            return find(components, { _id: { $in: collectComponentsFromRows(rows_component_ids) } });
        }
    },

    findDashboardsForEmail: function(email) {
        return find(dashboard, { for_emails: { $in: [ email ] }});
    }
};<commit_msg>Refactored the excess debug code out<commit_after>"use strict";

var Q = require("q");
var mongo = require("mongodb");
var ObjectID = mongo.ObjectID;
var MongoClient = mongo.MongoClient;
var _ = require("lodash");
var config = require("../config/config.json");
// TODO replace the default values in the configuration file (and please, do not push them into github :))
var connectionString = "mongodb://" + config.user + ":" + config.password + "@" + config.url + ":" + config.databasePort + "/" + config.database;
var database;

// Collections
// NOTE if the collections related code gets too out of hand, move it to its own module
var dashboard;
var components;

var init = function() {
    var deferred = Q.defer();

    Q.ninvoke(MongoClient, "connect", connectionString)
        .then(function(connectedDatabase) {
            database = connectedDatabase;
            dashboard = getCollection("dashboard")();
            components = getCollection("components")();
            deferred.resolve(true);
        })
        .catch(deferred.reject)
        .done();

    return deferred.promise;
};

var getCollection = function(collection) {
    if (!database) {
        throw "No connection to database.";
    }

    return function() {
        console.log("Returning requested collection.", collection);
        return database.collection(collection);
    };
};

var find = function(collection, query) {
   var deferred = Q.defer();

    if (collection.find === undefined) {
        deferred.reject("No find function in collection.");
        return;
    }

    collection
        .find(query)
        .toArray(function(error, documents) {
            if (error) {
                console.log("Error getting documents for collection.", error);
                deferred.reject(documents);
                return;
            }

            deferred.resolve(documents);
        });

    return deferred.promise;
};

var collectComponentsFromRows = function(rows) {
    // the columns are built of components (columns, single column = single component)

    var component_ids = _.flatten(rows.map(function(rowColumns) {
        return rowColumns.map(function(component_id) {
            return new ObjectID(component_id);
        });
    }));

    console.log("Collected component_ids for Dashboard.", component_ids);
    return component_ids;
};

module.exports = {
    init: init,
    getCollection: getCollection,

    findComponentsForDashboard: function(rows_component_ids, transformer) {
        if (transformer !== undefined) {
            var deferred = Q.defer();

            // transformer enabled where the results are projected/transformed into something else
            find(components, { _id: { $in: collectComponentsFromRows(rows_component_ids) } })
                .then(function(results) {
                    deferred.resolve(transformer(results));
                })
                .catch(function(error) {
                    console.error(error);
                    deferred.reject(error);
                })
                .done();

            return deferred.promise;
        } else {
            // raw resulting query
            return find(components, { _id: { $in: collectComponentsFromRows(rows_component_ids) } });
        }
    },

    findDashboardsForEmail: function(email) {
        return find(dashboard, { for_emails: { $in: [ email ] }});
    }
};<|endoftext|>


<commit_before>
// #################
// # Configuration #
// #################

// colors for the colornumber property
var senderColors = {
	0: "#1abc9c",
	1: "#2ecc71",
	2: "#3498db",
	3: "#9b59b6",
	4: "#e74c3c",
	5: "#e67e22",
	6: "#f1c40f",
	7: "#953163"
}

// last sender
var lastSenderName;

// grey Message Block for alternating colors to have a better visibility
var greyBlock = false;

// channel topic
var channelTopic = "";

// # ################# #
// # Textual Callbacks #
// # ################# #

Textual.newMessagePostedToView = function (lineNumber) {

	// get the message object
	var message = document.getElementById('line-' + lineNumber);

	// # ---------------- #
	// # Private Messages #
	if(message.getAttribute("ltype") === "privmsg") {

		let senderName = message.querySelector('.sender').textContent;
		// first check if the message is a correction
		var messageContent = message.getElementsByClassName('innerMessage')[0].textContent.trim();
		// if it's a single character it most likely was a correction to the previous line
		if(messageContent.length == 1){
			// add it to the previous line
			let prevMessage = previousMessage(message);

			if(prevMessage){
				// get previous message type
				let prevType  = prevMessage.getAttribute("ltype");
				// if the previous message was from the same sender as this message
				// add the single character to the previous message
				if(prevType === "privmsg" && prevMessage.querySelector('.sender').textContent === senderName){
					prevMessage = prevMessage.getElementsByClassName('innerMessage')[0];
					prevMessage.innerHTML = prevMessage.textContent.trim() + "" + messageContent + "";
					prevMessage.getElementsByClassName('correct')[0].classList.add("letterCorrection");
					// animate the previous line
					previousMessage(message).classList.add("lineCorrection");
					// delete the original message
					message.parentNode.removeChild(message);
				}
			}
		}
		// if it is longer, treat it as a normal message
		else{
			// color the sender
			colorizeColorNumber(message.querySelector('.sender'));

			// color any inline_nicknames if present
			var inline_nicknames = message.querySelectorAll('.inline_nickname');
			if(inline_nicknames.length > 0) {
				app.localUserNickname(function(username) {
					for (var i = 0; i < inline_nicknames.length; i++) {
						// check if it is the users name and color it in the user color
						if(inline_nicknames[i].textContent.toLowerCase().match(username.toLowerCase())){
							inline_nicknames[i].style.color = "#00cccc";
						}
						// color other users according to their color number
						else{
							// colorize the nickname
							colorizeColorNumber(inline_nicknames[i]);
						}
					}
				});
			}

			if(senderName === lastSenderName){
				// remove the sender name
				message.querySelector('.senderContainer').style.display = "none";
				// remove the time
				message.querySelector('.time').style.display = "none";
				// thin the padding to visually cluster the lines together
				message.style.paddingTop = "0.1em";
				previousMessage(message).style.paddingBottom = "0.1em";
			}
			// Sender has changed
			else{
				greyBlock = !greyBlock;
			}

			// add wrapper for the zoom animation effect to any inline image
			var possibleInlineImages = message.getElementsByClassName('inlineImageCell');
			if(possibleInlineImages.length > 0){
				for(var i = 0; i < possibleInlineImages.length; i++){
					// add the wrapper
					addInlineImageWrapper(possibleInlineImages[i]);
				}
			}

			// update last sender
			lastSenderName = senderName;

		}
	}
	// message was not a private message
	else{
		// whatever message it was, the sender was no user but the client/server
		// set lastSenderName to "textualClient" to continue the color alternation through
		// client messages

		if(!(lastSenderName === "textualClient")){
			lastSenderName = "textualClient";
			greyBlock = !greyBlock;
		}
	}

	// # ------------ #
	// # Topic Change #
	if(message.getAttribute("ltype") === "topic"){
		// get the command code
		var command = message.getAttribute("command");

		// 333 is a "Set By ..." message, which means we had a "Topic is ..." Message before
		// might be the same as before
		if(command === "333") {
			// get the topic message
			var topicMessage = message.previousSibling;
			// just to make sure we really had a 332 ("Topic set ...") message
			if(topicMessage && topicMessage.getAttribute("command") === "332"){
				// get the topic
				var topic = topicMessage.querySelector('.message').textContent.replace('Topic is', '').trim()
				// generalize the whitespaces
				topic = topic.replace(/\s/g, '');

				// the topic was already set
				if(topic === channelTopic) {
					// remove both messages
					topicMessage.parentNode.removeChild(topicMessage);
					message.parentNode.removeChild(message);
				}
			}
		}

	}

	// # ----------- #
	// # Join / Part #
	if(message.getAttribute("ltype") === "join" || message.getAttribute("ltype") === "part" || message.getAttribute("ltype") === "quit"){
		// hide your own join and part messages
		app.localUserNickname(function(nickname){
			// query Nickname and Nickname_
			var nicknameRegex = new RegExp(nickname + "_?");
			if(message.querySelector('b').textContent.search(nicknameRegex) != -1){
				message.parentNode.removeChild(message);
			}
		});
	}

	// if the previous message was a debug notice set greyBlock to false to color
	// the message in white
	var prevMessage = previousMessage(message);
	if(prevMessage && (typeof prevMessage === "object") && (prevMessage.getAttribute("ltype") === "debug")){
		greyBlock = false;
	}

	// color the message if needed
	if(greyBlock){
		message.className += " greyBackground";
	}

}

Textual.viewFinishedLoading = function () {
	Textual.fadeOutLoadingScreen(1.00, 1.00);

	setTimeout(function () {
		Textual.scrollToBottomOfView();
	}, 300);
}

Textual.viewFinishedReload = function () {
	Textual.viewFinishedLoading();
}

Textual.viewInitiated = function(viewType, serverHash, channelHash, channelName) {
	// inserts the spinner to the loading screen
	document.getElementById('loading_screen').innerHTML = "

";

	var tBar = document.getElementById('topic_bar');
	if(tBar){
		// make the content below the topic bar visible
		document.body.style.marginTop = tBar.clientHeight + "px";

		// set the channelTopic variable
		channelTopic =  tBar.textContent.replace(/\s/g, '');
	}
}

Textual.topicBarValueChanged = function(newTopic) {
	// update the channelTopic state variable
	channelTopic = newTopic.replace(/\s/g, '');;
}

// #############
// # Functions #
// #############

/* takes the colornumber property of the sender object, and looks up the color for it
 * in the colorNumbers dictionary.
 *
 * @param  Object    the object to be colored
 * @return void
 */
var colorizeColorNumber = function(object) {
	// get the color number
	var colorNumber = object.getAttribute("colornumber");
	// sometimes negative colornumbers may appear
	if(colorNumber < 0){
		colorNumber *= -1;
	}
	// set the color to the senderColors dictionary accordingly
	object.style.color = senderColors[colorNumber % 8];
}

/* takes an .inlineImageCell DOM Node and insertes a .imageWrapper div around the image
 * to enable a zoom effect for the image
 *
 * @param  .inlineImageCell DOM object
 * @return void
 */
var addInlineImageWrapper = function(inlineImageCell) {
	// get the link containing the image
	var imgLinkNode = inlineImageCell.querySelector('a');
	// create a wrapper element
	var wrapper = document.createElement("div");
	wrapper.className += "imageWrapper";

	// put the wrapper around the image
	inlineImageCell.replaceChild(wrapper, imgLinkNode);
	wrapper.appendChild(imgLinkNode);
}

/* gets the previous message
 * sometimes non-message divs are between messages and this function will skip them and get
 * the message before that div
 *
 * @param  the message div object
 * @return the div object of the previous message
 */
var previousMessage =  function(message) {
	var prev = message.previousSibling;
	if(prev && (prev.id === "mark" || prev.id === "historic_messages")){
		return prev.previousSibling;
	}
	else{
		return prev;
	}
}
<commit_msg>Fixed a bug where the message of the same sender changed the background color<commit_after>
// #################
// # Configuration #
// #################

// colors for the colornumber property
var senderColors = {
	0: "#1abc9c",
	1: "#2ecc71",
	2: "#3498db",
	3: "#9b59b6",
	4: "#e74c3c",
	5: "#e67e22",
	6: "#f1c40f",
	7: "#953163"
}

// last sender
var lastSenderName;

// grey Message Block for alternating colors to have a better visibility
var greyBlock = false;

// channel topic
var channelTopic = "";

// # ################# #
// # Textual Callbacks #
// # ################# #

Textual.newMessagePostedToView = function (lineNumber) {

	// get the message object
	var message = document.getElementById('line-' + lineNumber);

	// # ---------------- #
	// # Private Messages #
	if(message.getAttribute("ltype") === "privmsg") {

		let senderName     = message.querySelector('.sender').textContent;
		let prevMessage    = previousMessage(message);
		let prevType       = prevMessage.getAttribute("ltype");
		let lastSenderName = prevMessage && prevType === "privmsg" ? prevMessage.querySelector('.sender').textContent : null;

		// first check if the message is a correction
		var messageContent = message.getElementsByClassName('innerMessage')[0].textContent.trim();
		// if it's a single character it most likely was a correction to the previous line
		if(messageContent.length == 1){
			// add it to the previous line
			if(lastSenderName){
				// if the previous message was from the same sender as this message
				// add the single character to the previous message
				prevMessage = prevMessage.getElementsByClassName('innerMessage')[0];
				prevMessage.innerHTML = prevMessage.textContent.trim() + "" + messageContent + "";
				prevMessage.getElementsByClassName('correct')[0].classList.add("letterCorrection");
				// animate the previous line
				previousMessage(message).classList.add("lineCorrection");
				// delete the original message
				message.parentNode.removeChild(message);
			}
		}
		// if it is longer, treat it as a normal message
		else{
			// color the sender
			colorizeColorNumber(message.querySelector('.sender'));

			// color any inline_nicknames if present
			var inline_nicknames = message.querySelectorAll('.inline_nickname');
			if(inline_nicknames.length > 0) {
				app.localUserNickname(function(username) {
					for (var i = 0; i < inline_nicknames.length; i++) {
						// check if it is the users name and color it in the user color
						if(inline_nicknames[i].textContent.toLowerCase().match(username.toLowerCase())){
							inline_nicknames[i].style.color = "#00cccc";
						}
						// color other users according to their color number
						else{
							// colorize the nickname
							colorizeColorNumber(inline_nicknames[i]);
						}
					}
				});
			}

			if(lastSenderName){
				if(senderName === lastSenderName){
					// remove the sender name
					message.querySelector('.senderContainer').style.display = "none";
					// remove the time
					message.querySelector('.time').style.display = "none";
					// thin the padding to visually cluster the lines together
					message.style.paddingTop = "0.1em";
					previousMessage(message).style.paddingBottom = "0.1em";
				}
				// Sender has changed
				else{
					greyBlock = !greyBlock;
				}
			}

			// add wrapper for the zoom animation effect to any inline image
			var possibleInlineImages = message.getElementsByClassName('inlineImageCell');
			if(possibleInlineImages.length > 0){
				for(var i = 0; i < possibleInlineImages.length; i++){
					// add the wrapper
					addInlineImageWrapper(possibleInlineImages[i]);
				}
			}
		}
	}
	// message was not a private message
	else{
		// whatever message it was, the sender was no user but the client/server
		// set lastSenderName to "textualClient" to continue the color alternation through
		// client messages

		if(!(lastSenderName === "textualClient")){
			lastSenderName = "textualClient";
			greyBlock = !greyBlock;
		}
	}

	// # ------------ #
	// # Topic Change #
	if(message.getAttribute("ltype") === "topic"){
		// get the command code
		var command = message.getAttribute("command");

		// 333 is a "Set By ..." message, which means we had a "Topic is ..." Message before
		// might be the same as before
		if(command === "333") {
			// get the topic message
			var topicMessage = message.previousSibling;
			// just to make sure we really had a 332 ("Topic set ...") message
			if(topicMessage && topicMessage.getAttribute("command") === "332"){
				// get the topic
				var topic = topicMessage.querySelector('.message').textContent.replace('Topic is', '').trim()
				// generalize the whitespaces
				topic = topic.replace(/\s/g, '');

				// the topic was already set
				if(topic === channelTopic) {
					// remove both messages
					topicMessage.parentNode.removeChild(topicMessage);
					message.parentNode.removeChild(message);
				}
			}
		}

	}

	// # ----------- #
	// # Join / Part #
	if(message.getAttribute("ltype") === "join" || message.getAttribute("ltype") === "part" || message.getAttribute("ltype") === "quit"){
		// hide your own join and part messages
		app.localUserNickname(function(nickname){
			// query Nickname and Nickname_
			var nicknameRegex = new RegExp(nickname + "_?");
			if(message.querySelector('b').textContent.search(nicknameRegex) != -1){
				message.parentNode.removeChild(message);
			}
		});
	}

	// if the previous message was a debug notice set greyBlock to false to color
	// the message in white
	var prevMessage = previousMessage(message);
	if(prevMessage && (typeof prevMessage === "object") && (prevMessage.getAttribute("ltype") === "debug")){
		greyBlock = false;
	}

	// color the message if needed
	if(greyBlock){
		message.className += " greyBackground";
	}

}

Textual.viewFinishedLoading = function () {
	Textual.fadeOutLoadingScreen(1.00, 1.00);

	setTimeout(function () {
		Textual.scrollToBottomOfView();
	}, 300);
}

Textual.viewFinishedReload = function () {
	Textual.viewFinishedLoading();
}

Textual.viewInitiated = function(viewType, serverHash, channelHash, channelName) {
	// inserts the spinner to the loading screen
	document.getElementById('loading_screen').innerHTML = "

";

	var tBar = document.getElementById('topic_bar');
	if(tBar){
		// make the content below the topic bar visible
		document.body.style.marginTop = tBar.clientHeight + "px";

		// set the channelTopic variable
		channelTopic =  tBar.textContent.replace(/\s/g, '');
	}
}

Textual.topicBarValueChanged = function(newTopic) {
	// update the channelTopic state variable
	channelTopic = newTopic.replace(/\s/g, '');;
}

// #############
// # Functions #
// #############

/* takes the colornumber property of the sender object, and looks up the color for it
 * in the colorNumbers dictionary.
 *
 * @param  Object    the object to be colored
 * @return void
 */
var colorizeColorNumber = function(object) {
	// get the color number
	var colorNumber = object.getAttribute("colornumber");
	// sometimes negative colornumbers may appear
	if(colorNumber < 0){
		colorNumber *= -1;
	}
	// set the color to the senderColors dictionary accordingly
	object.style.color = senderColors[colorNumber % 8];
}

/* takes an .inlineImageCell DOM Node and insertes a .imageWrapper div around the image
 * to enable a zoom effect for the image
 *
 * @param  .inlineImageCell DOM object
 * @return void
 */
var addInlineImageWrapper = function(inlineImageCell) {
	// get the link containing the image
	var imgLinkNode = inlineImageCell.querySelector('a');
	// create a wrapper element
	var wrapper = document.createElement("div");
	wrapper.className += "imageWrapper";

	// put the wrapper around the image
	inlineImageCell.replaceChild(wrapper, imgLinkNode);
	wrapper.appendChild(imgLinkNode);
}

/* gets the previous message
 * sometimes non-message divs are between messages and this function will skip them and get
 * the message before that div
 *
 * @param  the message div object
 * @return the div object of the previous message
 */
var previousMessage =  function(message) {
	var prev = message.previousSibling;
	if(prev){
		if(prev.id === "mark" || prev.id === "historic_messages"){
			return previousMessage(prev.previousSibling);
		}
		else{
			return prev;
		}
	}
	else{
		return null;
	}
}
<|endoftext|>