<commit_before>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
({
    /**
     * Anchors with href without fragment are rendered as is.
     */
    testAnchorNoFragment: {
        test: function(component){
            var tag = component.find("nohash").getElement();
            $A.test.assertEquals("salesforce", $A.test.getText(tag), "textContent not expected");
            $A.test.assertTrue($A.test.contains(tag.href, "http://www.salesforce.com/"), "href not expected");
        }
    },

    /**
     * Anchors with href with only hash are rendered with modified href.
     */
    testAnchorFragment: {
        test: function(component){
            var tag = component.find("hash").getElement();
            $A.test.assertEquals("hash", $A.test.getText(tag), "textContent not expected");
            $A.test.assertEquals("javascript:void(0/*#*/);", tag.href, "href not expected");
        }
    },

    /**
     * Anchors with href with fragment are rendered with modified href.
     */
    testAnchorFragmentString: {
        test: function(component){
            var tag = component.find("hashString").getElement();
            $A.test.assertEquals("layout", $A.test.getText(tag), "textContent not expected");
            $A.test.assertEquals("javascript:void(0/*#layout*/);", tag.href, "href not expected");
        }
    },

    /**
     * Area with href should not render modified href with or without hash
     */
    testAreaNoFragment:{
        test: function(component){
            var tag = component.find("noHashArea").getElement();
            $A.test.assertEquals("http://www.salesforce.com/", tag.href, "Area href not expected");
        }
    },

    testAreaFragment:{
        test: function(component){
            var tag = component.find("hashStringArea").getElement();
            var expectedHrefValue = "#layout"
            $A.test.assertTrue(tag.href.substr(tag.href.length-expectedHrefValue.length) === expectedHrefValue);
        }
    },

    testAreaFragmentString:{
        test: function(component){
            var tag = component.find("hashArea").getElement();
            var expectedHrefValue = "#";
            $A.test.assertTrue(tag.href.substr(tag.href.length-expectedHrefValue.length) === expectedHrefValue);
        }
    },

    /**
     * Break tags are output as single self-contained tags.
     */
    testBreak: {
        test: function(component){
            var elems = component.find("hasBr").getElement().getElementsByTagName("br");
            $A.test.assertEquals(1, elems.length, "should only be 1 br tag");
            $A.test.assertEquals(0, elems[0].children.length, "br should not have any children");
        }
    },

    /**
     * Attributes on HTML tags are case-insensitive.
     */
    testAttributeCasing: {
        test: function(component){
            var tag = component.find("case").getElement();
            $A.test.assertTrue(typeof tag.ReadOnly === "undefined" && tag.readOnly === true, "readOnly was not cased properly");
            $A.test.assertTrue(typeof tag.maxlength === "undefined" && tag.maxLength == 11, "maxLength was not cased properly");
            $A.test.assertTrue(typeof tag.AccessKey === "undefined" && tag.accessKey === "x", "accessKey was not cased properly");
            $A.test.assertTrue(typeof tag.TABINDEX === "undefined" && tag.tabIndex === 1, "tabIndex was not cased properly");
            $A.test.assertTrue(typeof tag.ColSpaN === "undefined" && tag.colSpan === "2", "colSpan was not cased properly");
            $A.test.assertTrue(typeof tag.rOWsPAN === "undefined" && tag.rowSpan === "2", "rowSpan was not cased properly");
            $A.test.assertTrue(typeof tag.BGColor === "undefined" && tag.bgColor === "#FFFFFF", "bgColor was not cased properly");
            $A.test.assertTrue(typeof tag.USEmap === "undefined" && tag.useMap === "true", "useMap was not cased properly");
            $A.test.assertTrue(typeof tag.Class === "undefined" && tag.className === "low", "className was not converted properly");
            $A.test.assertTrue(typeof tag.FOR === "undefined" && tag.htmlFor === "ground", "htmlFor was not converted properly");
            $A.test.assertTrue(typeof tag.PLACEHOLDER === "undefined" && tag.placeholder === "Casper", "placeholder was not cased properly");
            $A.test.assertTrue(typeof tag.ValuE === "undefined" && tag.value === "infamous ghost", "value was not cased properly");
        }
    },

    assertClassUpdate : function(component, newValue) {
		component.getValue("v.classValue").setValue(newValue);
		$A.rerender(component);
		$A.test.assertEquals(newValue ? newValue : "", component.find("hasClass").getElement().className);
    },

    /**
     * class attribute will be rerendered when initially not set
     */
    testRerenderAddedClassAttribute: {
    	test: function(component) {
    		$A.test.assertEquals("", component.find("hasClass").getElement().className, "initial class not set");
    		this.assertClassUpdate(component, "inner");
    	}
    },

    /**
     * class attribute can be removed and restored in rerender
     */
    testRerenderUpdatedClassAttribute: {
    	attributes: { classValue : "upper" },
    	test: function(component) {
    		$A.test.assertEquals("upper", component.find("hasClass").getElement().className, "initial class not set");
	    	this.assertClassUpdate(component, "");
	    	this.assertClassUpdate(component, "middle");
	    	this.assertClassUpdate(component, null);
	    	this.assertClassUpdate(component, "lower");
	    	this.assertClassUpdate(component);
    	}
    },

    /**
     * Verify rerender of special html attributes
     * type, href, style and data attributes must be set by using setAttribute() on dom elements
     */
    testRerenderSpecialHtmlAttributes:{
		test:function(component){
		    var input = component.find("specialAttributes_input").getElement();
		    $A.test.assertEquals("textElement" , input.getAttribute("data-name"), "Failed to render data attribute");
		    $A.test.assertEquals("color:blue" , input.getAttribute("style").replace(/[ ;]/g,"").toLowerCase(), "Failed to render style attribute");

		    var a = component.find("specialAttributes_a").getElement();
		    $A.test.assertEquals("http://bazinga.com/" , a.getAttribute("href"), "Failed to render href attribute");

		    component.getAttributes().setValue("style", "color:green");
		    component.getAttributes().setValue("dataName", "inputElement");
		    component.getAttributes().setValue("href", "http://bbt.com/");

		    $A.rerender(component);
		    input = component.find("specialAttributes_input").getElement();
		    $A.test.assertEquals("inputElement" , input.getAttribute("data-name"), "Failed to rerender data attribute");
		    $A.test.assertEquals("color:green" , input.getAttribute("style").replace(/[ ;]/g,"").toLowerCase(), "Failed to rerender style attribute");

		    a = component.find("specialAttributes_a").getElement();
		    $A.test.assertEquals("http://bbt.com/" , a.getAttribute("href"), "Failed to rerender href attribute");
		}
    },

    /**
     * Change type of input element (non-IE)
     */
    testChangeTypeOfInputElement:{
    	browsers: ["-IE7","-IE8","-IE9"],
		test:function(component){
		    var input = component.find("specialAttributes_input").getElement();
		    $A.test.assertEquals("text" , input.getAttribute("type"), "Failed to render type attribute");
		    component.getAttributes().setValue("type", "input");
		    $A.rerender(component);
		    input = component.find("specialAttributes_input").getElement();
		    $A.test.assertEquals("input" , input.getAttribute("type"), "Failed to rerender type attribute");
		}
    },

    /**
     * Verify that touchend event handlers is used if present before using onclick.
     * Automation for W-1564377
     */
    testTouchEndHandlerUsedWhenPresent:{
	browsers:["IPAD"],
	testLabels : ["UnAdaptableTest"],
	test: [
	  //Both click handler and touch end handler defined
	  function(component){
	    component._TouchEndHandler = false;
	    component._OnClickHandler = false;
	    var targetElement = component.find("bothTouchEndAndClickHandlers").getElement();
	    this.fireTouchEndEventOnElement(component, targetElement);
	    $A.test.addWaitFor(true,
		    function(){return component._TouchEndHandler},
		    function(){ $A.test.assertFalse(component._OnClickHandler); });
	},
	  //Both touch end handler defined
	  function(component){
	    var targetElement = component.find("onlyTouchEndHandler").getElement();
	    this.fireTouchEndEventOnElement(component, targetElement);
	    $A.test.addWaitFor(true, function(){return component._TouchEndHandler;},
		    function(){$A.test.assertFalse(component._OnClickHandler);})
	},
	 //Only click handler defined
//	 function(component){
//	    var targetElement = component.find("onlyClickHandler").getElement();
//	    this.fireTouchEndEventOnElement(component, targetElement);
//	    $A.test.addWaitFor(true, function(){return component._OnClickHandler;},
//		    function(){$A.test.assertFalse(component._TouchEndHandler);})
//	}
	]
    },
    fireTouchEndEventOnElement:function(component, targetElement){
	component._OnClickHandler = false;
	component._TouchEndHandler = false;
	var touchEndEvt =  document.createEvent("TouchEvent");
	touchEndEvt.initTouchEvent("touchend", true, true);
	targetElement.dispatchEvent(touchEndEvt);
    }
})
<commit_msg>Merging origin/mike/master-pending-fixForTestRerenderSpecialHtmlAttributes into master<commit_after>/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
({
    /**
     * Anchors with href without fragment are rendered as is.
     */
    testAnchorNoFragment: {
        test: function(component){
            var tag = component.find("nohash").getElement();
            $A.test.assertEquals("salesforce", $A.test.getText(tag), "textContent not expected");
            $A.test.assertTrue($A.test.contains(tag.href, "http://www.salesforce.com/"), "href not expected");
        }
    },

    /**
     * Anchors with href with only hash are rendered with modified href.
     */
    testAnchorFragment: {
        test: function(component){
            var tag = component.find("hash").getElement();
            $A.test.assertEquals("hash", $A.test.getText(tag), "textContent not expected");
            $A.test.assertEquals("javascript:void(0/*#*/);", tag.href, "href not expected");
        }
    },

    /**
     * Anchors with href with fragment are rendered with modified href.
     */
    testAnchorFragmentString: {
        test: function(component){
            var tag = component.find("hashString").getElement();
            $A.test.assertEquals("layout", $A.test.getText(tag), "textContent not expected");
            $A.test.assertEquals("javascript:void(0/*#layout*/);", tag.href, "href not expected");
        }
    },

    /**
     * Area with href should not render modified href with or without hash
     */
    testAreaNoFragment:{
        test: function(component){
            var tag = component.find("noHashArea").getElement();
            $A.test.assertEquals("http://www.salesforce.com/", tag.href, "Area href not expected");
        }
    },

    testAreaFragment:{
        test: function(component){
            var tag = component.find("hashStringArea").getElement();
            var expectedHrefValue = "#layout"
            $A.test.assertTrue(tag.href.substr(tag.href.length-expectedHrefValue.length) === expectedHrefValue);
        }
    },

    testAreaFragmentString:{
        test: function(component){
            var tag = component.find("hashArea").getElement();
            var expectedHrefValue = "#";
            $A.test.assertTrue(tag.href.substr(tag.href.length-expectedHrefValue.length) === expectedHrefValue);
        }
    },

    /**
     * Break tags are output as single self-contained tags.
     */
    testBreak: {
        test: function(component){
            var elems = component.find("hasBr").getElement().getElementsByTagName("br");
            $A.test.assertEquals(1, elems.length, "should only be 1 br tag");
            $A.test.assertEquals(0, elems[0].children.length, "br should not have any children");
        }
    },

    /**
     * Attributes on HTML tags are case-insensitive.
     */
    testAttributeCasing: {
        test: function(component){
            var tag = component.find("case").getElement();
            $A.test.assertTrue(typeof tag.ReadOnly === "undefined" && tag.readOnly === true, "readOnly was not cased properly");
            $A.test.assertTrue(typeof tag.maxlength === "undefined" && tag.maxLength == 11, "maxLength was not cased properly");
            $A.test.assertTrue(typeof tag.AccessKey === "undefined" && tag.accessKey === "x", "accessKey was not cased properly");
            $A.test.assertTrue(typeof tag.TABINDEX === "undefined" && tag.tabIndex === 1, "tabIndex was not cased properly");
            $A.test.assertTrue(typeof tag.ColSpaN === "undefined" && tag.colSpan === "2", "colSpan was not cased properly");
            $A.test.assertTrue(typeof tag.rOWsPAN === "undefined" && tag.rowSpan === "2", "rowSpan was not cased properly");
            $A.test.assertTrue(typeof tag.BGColor === "undefined" && tag.bgColor === "#FFFFFF", "bgColor was not cased properly");
            $A.test.assertTrue(typeof tag.USEmap === "undefined" && tag.useMap === "true", "useMap was not cased properly");
            $A.test.assertTrue(typeof tag.Class === "undefined" && tag.className === "low", "className was not converted properly");
            $A.test.assertTrue(typeof tag.FOR === "undefined" && tag.htmlFor === "ground", "htmlFor was not converted properly");
            $A.test.assertTrue(typeof tag.PLACEHOLDER === "undefined" && tag.placeholder === "Casper", "placeholder was not cased properly");
            $A.test.assertTrue(typeof tag.ValuE === "undefined" && tag.value === "infamous ghost", "value was not cased properly");
        }
    },

    assertClassUpdate : function(component, newValue) {
		component.getValue("v.classValue").setValue(newValue);
		$A.rerender(component);
		$A.test.assertEquals(newValue ? newValue : "", component.find("hasClass").getElement().className);
    },

    /**
     * class attribute will be rerendered when initially not set
     */
    testRerenderAddedClassAttribute: {
    	test: function(component) {
    		$A.test.assertEquals("", component.find("hasClass").getElement().className, "initial class not set");
    		this.assertClassUpdate(component, "inner");
    	}
    },

    /**
     * class attribute can be removed and restored in rerender
     */
    testRerenderUpdatedClassAttribute: {
    	attributes: { classValue : "upper" },
    	test: function(component) {
    		$A.test.assertEquals("upper", component.find("hasClass").getElement().className, "initial class not set");
	    	this.assertClassUpdate(component, "");
	    	this.assertClassUpdate(component, "middle");
	    	this.assertClassUpdate(component, null);
	    	this.assertClassUpdate(component, "lower");
	    	this.assertClassUpdate(component);
    	}
    },

    /**
     * Verify rerender of special html attributes
     * type, href, style and data attributes must be set by using setAttribute() on dom elements
     *
     * Ie7 does not support the inline style attribute.
     */
    testRerenderSpecialHtmlAttributes:{
	browsers: ["-IE7"],
		test:function(component){
		    var input = component.find("specialAttributes_input").getElement();
		    $A.test.assertEquals("textElement" , input.getAttribute("data-name"), "Failed to render data attribute");
		    $A.test.assertEquals("color:blue" , input.getAttribute("style").replace(/[ ;]/g,"").toLowerCase(), "Failed to render style attribute");

		    var a = component.find("specialAttributes_a").getElement();
		    $A.test.assertEquals("http://bazinga.com/" , a.getAttribute("href"), "Failed to render href attribute");

		    component.getAttributes().setValue("style", "color:green");
		    component.getAttributes().setValue("dataName", "inputElement");
		    component.getAttributes().setValue("href", "http://bbt.com/");

		    $A.rerender(component);
		    input = component.find("specialAttributes_input").getElement();
		    $A.test.assertEquals("inputElement" , input.getAttribute("data-name"), "Failed to rerender data attribute");
		    $A.test.assertEquals("color:green" , input.getAttribute("style").replace(/[ ;]/g,"").toLowerCase(), "Failed to rerender style attribute");

		    a = component.find("specialAttributes_a").getElement();
		    $A.test.assertEquals("http://bbt.com/" , a.getAttribute("href"), "Failed to rerender href attribute");
		}
    },

    /**
     * Change type of input element (non-IE)
     */
    testChangeTypeOfInputElement:{
    	browsers: ["-IE7","-IE8","-IE9"],
		test:function(component){
		    var input = component.find("specialAttributes_input").getElement();
		    $A.test.assertEquals("text" , input.getAttribute("type"), "Failed to render type attribute");
		    component.getAttributes().setValue("type", "input");
		    $A.rerender(component);
		    input = component.find("specialAttributes_input").getElement();
		    $A.test.assertEquals("input" , input.getAttribute("type"), "Failed to rerender type attribute");
		}
    },

    /**
     * Verify that touchend event handlers is used if present before using onclick.
     * Automation for W-1564377
     */
    testTouchEndHandlerUsedWhenPresent:{
	browsers:["IPAD"],
	testLabels : ["UnAdaptableTest"],
	test: [
	  //Both click handler and touch end handler defined
	  function(component){
	    component._TouchEndHandler = false;
	    component._OnClickHandler = false;
	    var targetElement = component.find("bothTouchEndAndClickHandlers").getElement();
	    this.fireTouchEndEventOnElement(component, targetElement);
	    $A.test.addWaitFor(true,
		    function(){return component._TouchEndHandler},
		    function(){ $A.test.assertFalse(component._OnClickHandler); });
	},
	  //Both touch end handler defined
	  function(component){
	    var targetElement = component.find("onlyTouchEndHandler").getElement();
	    this.fireTouchEndEventOnElement(component, targetElement);
	    $A.test.addWaitFor(true, function(){return component._TouchEndHandler;},
		    function(){$A.test.assertFalse(component._OnClickHandler);})
	},
	 //Only click handler defined
//	 function(component){
//	    var targetElement = component.find("onlyClickHandler").getElement();
//	    this.fireTouchEndEventOnElement(component, targetElement);
//	    $A.test.addWaitFor(true, function(){return component._OnClickHandler;},
//		    function(){$A.test.assertFalse(component._TouchEndHandler);})
//	}
	]
    },
    fireTouchEndEventOnElement:function(component, targetElement){
	component._OnClickHandler = false;
	component._TouchEndHandler = false;
	var touchEndEvt =  document.createEvent("TouchEvent");
	touchEndEvt.initTouchEvent("touchend", true, true);
	targetElement.dispatchEvent(touchEndEvt);
    }
})
<|endoftext|>


<commit_before>/**
    Show an error modal to notify the user.
    @param options:Object {
        message:string DEPRECATED. Optional. Informative error message.
        error:string Optional. Error/Exception/XHR object, used to auto
            generate the error message. It takes precedence over 'message'
            option, discarding an error object/string is passed.
            It replaces 'message' since can do the same and more.
        title:string Optional. The text to show in the modal's header,
            with fallback to the Modal's default title.
    }
    @returns Promise. It resolves when the modal is dismissed/closed.
    No formal rejection happens.
**/
// TODO jsdocs
'use strict';

var ariaHideElements = require('./utils/ariaHideElements');
var fixFocus = require('./utils/fixFocus');
var getErrorMessageFrom = require('./utils/getErrorMessageFrom');
var TEMPLATE = require('../../html/modals/error.html');
var createElement = require('./utils/createElement');
require('../utils/jquery.multiline');

exports.show = function (options) {
    var modal = createElement(TEMPLATE);
    fixFocus(modal);
    var header = modal.find('#errorModal-label');
    var body = modal.find('#errorModal-body');

    options = options || {};

    // Fallback error message
    var msg = body.data('default-text');

    // Error message from given error object, with fallback to default one.
    // DEPRECATED temporarly using the 'message' option.
    msg = getErrorMessageFrom(options.error || options.message, msg);

    body.multiline(msg);

    header.text(options.title || header.data('default-text'));

    return new Promise(function(resolve) {
        modal.modal('show');
        // Increased accessibility:
        // NOTE: must be reverted BEFORE we fullfill
        var handle = ariaHideElements.keep(modal.get(0));

        modal
        .off('hide.bs.modal')
        .on('hide.bs.modal', function() {
            handle.revert();
            resolve();
        });
    });
};
<commit_msg>Documented with jsdocs and added errorMessage as returning value to the error modal<commit_after>/**
 * Show an error modal to notify the user.
 */
'use strict';

var ariaHideElements = require('./utils/ariaHideElements');
var fixFocus = require('./utils/fixFocus');
var getErrorMessageFrom = require('./utils/getErrorMessageFrom');
var TEMPLATE = require('../../html/modals/error.html');
var createElement = require('./utils/createElement');
require('../utils/jquery.multiline');

/**
 * Display a modal with an error message
 * @param {Object} options
 * @param {(string|Exception|Error|Object)} [options.error='There was an error'] Error message
 * or error object that will be analyzed to extract an error
 * message (@see /utils/getErrorMessageFrom for object patterns supported).
 * @param {string} [options.title='There was an error'] Text for the modal title bar.
 * @returns {Promise<string,any>} It resolves when the modal is closed returning
 * the error text that was displayed (usefull when the message was inferred from
 * a given object and want to be persisted or displayed in an additional way
 * after the modal). It rejects only on internal errors at displaying the
 * modal, errors at set-up throws.
 */
exports.show = function (options) {
    var modal = createElement(TEMPLATE);
    fixFocus(modal);
    var header = modal.find('#errorModal-label');
    var body = modal.find('#errorModal-body');

    options = options || {};

    // Fallback error message
    var msg = body.data('default-text');

    // Error message from given error object, with fallback to default one.
    // DEPRECATED temporarly using the 'message' option.
    msg = getErrorMessageFrom(options.error || options.message, msg);

    body.multiline(msg);

    header.text(options.title || header.data('default-text'));

    return new Promise(function(resolve) {
        modal.modal('show');
        // Increased accessibility:
        // NOTE: must be reverted BEFORE we fullfill
        var handle = ariaHideElements.keep(modal.get(0));

        modal
        .off('hide.bs.modal')
        .on('hide.bs.modal', function() {
            handle.revert();
            resolve(msg);
        });
    });
};
<|endoftext|>


<commit_before>define(function (require, exports, module) {
  var Actions = require('app/resources/Actions');
  var ThirdVendor = require('util/ThirdVendor');
  var BasicModel = require('app/model/Model');
  var BasicView = require('app/view/View');

  function Controller() {
    this.models = {
      Basic: BasicModel
    };
    this.views = {
      Basic: BasicView
    };
    //所有视图初始化前，需要获得客户端的用户登录信息
    //Core.Router.onReady(onUserinfo);

    Core.Router.onChanged(onViewChanged);

    var CTRL = this,
      isApp = Core.NativeBridge.isApp(),
      params = Core.localParam(),
      _userid = params.search['userid'];
    ///*Todo: debug user
    _userid && CTRL.models.Basic.setUserId(_userid);
    //*/

    //更新数据缓存时间
    Core.Event.on('resetModelUpdateTimeout', CTRL.models.Basic.modelUpdate.timer.resetAll);
    //通过API名称，调用客户API
    Core.Event.on('appAPI', appAPI);
    //分享
    Core.Event.on('share', appShare);
    //去下载
    Core.Event.on('appDownload', redirectToDownload);
    //去更新
    Core.Event.on('appUpdate', appUpdate);
    //去deja.me
    Core.Event.on('redirectToDejame', redirectToDejame);
    //跳转出商城
    Core.Event.on('redirect', redirectToPage);
    //去登录
    Core.Event.on('login', onLogin);
    //去WEB登录
    Core.Event.on('webLogin', webLogin);
    //去App登录
    Core.Event.on('appLogin', appLogin);
    //去反馈
    Core.Event.on('feedback', onFeedback);
    //复制文本
    Core.Event.on('appCopyText', appCopyText);
    //更新标题
    Core.Event.on('appModifyTitle', appModifyTitle);
    //修改右上角功能菜单
    Core.Event.on('appActionbutton', appActionButton);
    //修改右上角功能菜单成默认
    Core.Event.on('appActionDefaultButton', appActionDefaultButton);
    //修改右上角功能菜单成分享
    Core.Event.on('appActionShareButton', appActionShareButton);
    //注册webview关闭事件
    Core.Event.on('appOnUnload', appOnUnload);
    //关闭webview
    Core.Event.on('appCloseWebView', appCloseWebView);
    //打开 Selfie Test
    Core.Event.on('appSelfieTest', appSelfieTest);
    //Update Profile
    Core.Event.on('appUpdateProfile', appUpdateProfile);
    //tab 切换
    Core.Event.on('switchTab', switchTab);
    //触发暂时性动画
    Core.Event.on('trigerAnimate', trigerAnimate);
    //text 收展
    Core.Event.on('toggleTextSectionExpand', toggleTextSectionExpand);
    //统计
    Core.Event.on('analytics', analytics);

    //滚动到顶部
    Core.Event.on('scrollTop', scrollTop);
    //back
    Core.Event.on('back', function (action) {
      Core.Router.back(action || -1);
    });
    function analytics(params, title) {
      setTimeout(function () {
        var url = Actions.analytics + '?devevent=1' + (params ? ('&' + params) : '');
        //android和iOS
        if ($.os.ios && !$.os.android) {
          url += '&ios';
        } else if ($.os.android) {
          url += '&android';
        }
        if (ThirdVendor) {
          url += '&plf=' + ThirdVendor.code;
        }
        if(CTRL.models.Basic.isLogined()){
          url += '&logined';
        }
        Core.Navigator.protocol(url, true);
      }, 0);
    }

    function scrollTop() {
      var top = Math.min(Math.min(window.pageYOffset, document.documentElement.scrollTop || document.body.scrollTop), window.scrollY),
        start = top,
        to = 0,
        timer = 0,
        change = to - start,
        currentTime = 0,
        increment = 20,
        duration = 500;
      (function animloop() {
        // increment the time
        currentTime += increment;
        if (start < 2 || CTRL.views.Basic.GlobalTouch.touched || currentTime > duration) {
          if (start < 2) {
            window.scrollTo(0, 1);
          }
          cancelRequestAnimFrame(timer);
          return;
        }
        window.scrollTo(0, Math.easeInOutQuad(currentTime, start, change, duration));
        timer = requestAnimFrame(animloop);
      })();
    }

    function onViewChanged() {
      appModifyTitle();
      CTRL.views.Basic.msgbox.hideLoading();
    }

    function onUserinfo() {
      if (isApp) {
        Core.NativeBridge.userInfo(null, function (rs) {
          CTRL.models.Basic.setAppUserMeta(rs);
          if (rs && !!rs.userid) {
            if (CTRL.models.Basic.verifyLoginCookie() && CTRL.models.Basic.verifyLoginCookieTimeout(30)) {
              Core.Router.run();
            } else {
              onLogin();
            }
          } else {
            Core.Router.run();
          }
        });
        //Core.NativeBridge.device(function(rs){
        //    if(rs){
        //        CTRL.models.Basic.setNativeBridgeDeviceMeta(rs);
        //    }
        //});
      } else {
        Core.Router.run();
      }
    }

    function onLogin(arg,msg,callback) {
      if (isApp) {
        appLogin(callback);
      } else {
        CTRL.views.Basic.msgbox.showSignin({
          msg: msg,
          yesCallback: function (plf) {
            webLogin( Actions.main + (arg || ''), null, plf );
          }
        });
        //CTRL.views.Basic.msgbox.showDownload({
        //  yesCallback: function () {
        //    redirectToDownload(Actions.main + (arg || ''));
        //  }
        //});
      }
    }

    function onFeedback(email) {
      Core.Navigator.protocol('mailto:'+(email||'mozat@mozat.com?subject=Suggestion'), true);
    }

    function switchTab(el, tabs, tabContents) {
      if (!tabs || !tabContents) {
        return;
      }
      var isClicked = !!el;
      el = el || tabs[0];
      for (var i = 0; i < tabs.length; i++) {
        if (tabs[i] == el) {
          tabs[i].classList.add('on');
          trigerAnimate(tabContents.eq(i));
          tabContents[i] && tabContents[i].classList.add('show');
          isClicked && Core.Event.trigger('analyticsCurView', 'tab=' + i);
        } else {
          tabs[i].classList.remove('on');
          tabContents[i] && tabContents[i].classList.remove('show');
        }
      }
      Core.Event.trigger('analyticsCurView');
    }

    function trigerAnimate(el, classname, timeout) {
      if (!el) {
        return;
      }
      classname = classname || 'animated';
      timeout = timeout || 1200;
      el.animTimer && clearTimeout(el.animTimer);
      el.addClass(classname);
      el.animTimer = setTimeout(function () {
        el.removeClass(classname);
      }, timeout);
    }

    function toggleTextSectionExpand(el) {
      el && el.classList.toggle('expand');
    }

    function webLogin(surl, furl, pf) {
      var murl = window.location.href;
      surl = surl || murl;
      furl = furl || murl;
      pf = pf || 'fb';

      redirectToPage(Actions.login
        .replace('{SURL}', encodeURIComponent(surl))
        .replace('{FURL}', encodeURIComponent(furl))
        .replace('{PF}', pf));
    }

    function appLogin(callback){
      Core.NativeBridge.login(null, function (rs) {
        if (rs) {
          CTRL.models.Basic.saveLoginCookieTimeout();
          CTRL.models.Basic.modelUpdate.timer.resetAll();
          CTRL.models.Basic.setAppUserMeta(rs);
          if(callback){
            callback();
          }else{
            Core.Router.run();
          }
        }
      });
    }

    function appUpdate(msg,force) {
      redirectToApp(function () {
        CTRL.views.Basic.msgbox.showDialog({
          msg: msg || 'Please up to date your App',
          noText: force?null:'Close',
          yesText: 'Update',
          yesCallback: function () {
            downloadDejaInApp();
          }
        });
      });
    }

    function appShare(callback, plf) {
      redirectToApp(function () {
        var fn = Core.NativeBridge['share' + (plf ? ('_' + plf) : '')];
        fn && fn(null, callback);
      });
    }

    function appCopyText(text) {
      if (isApp) {
        Core.NativeBridge.copy(text);
      }
    }

    function appModifyTitle(title) {
      title = title || document.title;
      document.title = title;
      if (isApp) {
        Core.NativeBridge.modifytitle(title);
      }
    }

    function appActionButton(name, callback) {
      if (isApp) {
        Core.NativeBridge.updateBarButton(name, callback);
      }
    }

    function appActionShareButton() {
      appActionButton('share');
    }

    function appActionDefaultButton() {
      appActionButton('', function () {
      });
    }
    function appOnUnload(callback){
      if (isApp) {
        Core.NativeBridge.set_before_for_unload(callback);
      }
    }
    function appCloseWebView(){
      if (isApp) {
        Core.NativeBridge.closeweb();
      }
    }
    function appSelfieTest(callback){
      appAPI('selfieTest',null,callback)
    }

    /**
     * @param subProtocol String creationLike,creationDelete,productLike,follow
     */
    function appUpdateProfile(subProtocol){
      appAPI('updateProfile',null,null,subProtocol);
    }
    function appAPI(name, data, callback, subProtocol){
      if (isApp) {
        Core.NativeBridge.trigger.apply(null,arguments);
      }else{
        var proto = [name];
        subProtocol && proto.push(subProtocol);
        redirectToDownload(null,true,Actions.dejafashionSchema+proto.join('/'));
      }
    }
    function downloadDejaInApp() {
      var url = Actions.dejaAppAndroid;
      if ($.os.ios && !$.os.android) {
        url = Actions.dejaAppIos;
      }
      window.location = url;
    }

    function redirectToDejame() {
      redirectToPage(Actions.dejame);
    }

    //打开客户端原生视图
    function redirectToApp(callback, link) {
      if (isApp) {
        callback && callback();
      } else {
        CTRL.views.Basic.msgbox.showDownload({
          yesCallback: function () {
            redirectToDownload(link || window.location.href);
          }
        });
      }
    }

    function redirectToDownload(link, autoopen, schema) {
      link = !!link && link!='0'? ('#url=dejafashion://web/' + link) : '';
      link = !!schema?('#url='+schema): link;
      redirectToPage(Actions.dejaDwonloadBridge + (autoopen ? '?autoopen=1' : '') + link);
    }

    function redirectToPage(link) {
      if (link) {
        !(/__NativeBridge_target/g.test(link)) && appActionDefaultButton();
        window.location = link;
      }
    }

  }//end Controller
  return new Controller;
});
<commit_msg>fix appAPI<commit_after>define(function (require, exports, module) {
  var Actions = require('app/resources/Actions');
  var ThirdVendor = require('util/ThirdVendor');
  var BasicModel = require('app/model/Model');
  var BasicView = require('app/view/View');

  function Controller() {
    this.models = {
      Basic: BasicModel
    };
    this.views = {
      Basic: BasicView
    };
    //所有视图初始化前，需要获得客户端的用户登录信息
    //Core.Router.onReady(onUserinfo);

    Core.Router.onChanged(onViewChanged);

    var CTRL = this,
      isApp = Core.NativeBridge.isApp(),
      params = Core.localParam(),
      _userid = params.search['userid'];
    ///*Todo: debug user
    _userid && CTRL.models.Basic.setUserId(_userid);
    //*/

    //更新数据缓存时间
    Core.Event.on('resetModelUpdateTimeout', CTRL.models.Basic.modelUpdate.timer.resetAll);
    //通过API名称，调用客户API
    Core.Event.on('appAPI', appAPI);
    //分享
    Core.Event.on('share', appShare);
    //去下载
    Core.Event.on('appDownload', redirectToDownload);
    //去更新
    Core.Event.on('appUpdate', appUpdate);
    //去deja.me
    Core.Event.on('redirectToDejame', redirectToDejame);
    //跳转出商城
    Core.Event.on('redirect', redirectToPage);
    //去登录
    Core.Event.on('login', onLogin);
    //去WEB登录
    Core.Event.on('webLogin', webLogin);
    //去App登录
    Core.Event.on('appLogin', appLogin);
    //去反馈
    Core.Event.on('feedback', onFeedback);
    //复制文本
    Core.Event.on('appCopyText', appCopyText);
    //更新标题
    Core.Event.on('appModifyTitle', appModifyTitle);
    //修改右上角功能菜单
    Core.Event.on('appActionbutton', appActionButton);
    //修改右上角功能菜单成默认
    Core.Event.on('appActionDefaultButton', appActionDefaultButton);
    //修改右上角功能菜单成分享
    Core.Event.on('appActionShareButton', appActionShareButton);
    //注册webview关闭事件
    Core.Event.on('appOnUnload', appOnUnload);
    //关闭webview
    Core.Event.on('appCloseWebView', appCloseWebView);
    //打开 Selfie Test
    Core.Event.on('appSelfieTest', appSelfieTest);
    //Update Profile
    Core.Event.on('appUpdateProfile', appUpdateProfile);
    //tab 切换
    Core.Event.on('switchTab', switchTab);
    //触发暂时性动画
    Core.Event.on('trigerAnimate', trigerAnimate);
    //text 收展
    Core.Event.on('toggleTextSectionExpand', toggleTextSectionExpand);
    //统计
    Core.Event.on('analytics', analytics);

    //滚动到顶部
    Core.Event.on('scrollTop', scrollTop);
    //back
    Core.Event.on('back', function (action) {
      Core.Router.back(action || -1);
    });
    function analytics(params, title) {
      setTimeout(function () {
        var url = Actions.analytics + '?devevent=1' + (params ? ('&' + params) : '');
        //android和iOS
        if ($.os.ios && !$.os.android) {
          url += '&ios';
        } else if ($.os.android) {
          url += '&android';
        }
        if (ThirdVendor) {
          url += '&plf=' + ThirdVendor.code;
        }
        if(CTRL.models.Basic.isLogined()){
          url += '&logined';
        }
        Core.Navigator.protocol(url, true);
      }, 0);
    }

    function scrollTop() {
      var top = Math.min(Math.min(window.pageYOffset, document.documentElement.scrollTop || document.body.scrollTop), window.scrollY),
        start = top,
        to = 0,
        timer = 0,
        change = to - start,
        currentTime = 0,
        increment = 20,
        duration = 500;
      (function animloop() {
        // increment the time
        currentTime += increment;
        if (start < 2 || CTRL.views.Basic.GlobalTouch.touched || currentTime > duration) {
          if (start < 2) {
            window.scrollTo(0, 1);
          }
          cancelRequestAnimFrame(timer);
          return;
        }
        window.scrollTo(0, Math.easeInOutQuad(currentTime, start, change, duration));
        timer = requestAnimFrame(animloop);
      })();
    }

    function onViewChanged() {
      appModifyTitle();
      CTRL.views.Basic.msgbox.hideLoading();
    }

    function onUserinfo() {
      if (isApp) {
        Core.NativeBridge.userInfo(null, function (rs) {
          CTRL.models.Basic.setAppUserMeta(rs);
          if (rs && !!rs.userid) {
            if (CTRL.models.Basic.verifyLoginCookie() && CTRL.models.Basic.verifyLoginCookieTimeout(30)) {
              Core.Router.run();
            } else {
              onLogin();
            }
          } else {
            Core.Router.run();
          }
        });
        //Core.NativeBridge.device(function(rs){
        //    if(rs){
        //        CTRL.models.Basic.setNativeBridgeDeviceMeta(rs);
        //    }
        //});
      } else {
        Core.Router.run();
      }
    }

    function onLogin(arg,msg,callback) {
      if (isApp) {
        appLogin(callback);
      } else {
        CTRL.views.Basic.msgbox.showSignin({
          msg: msg,
          yesCallback: function (plf) {
            webLogin( Actions.main + (arg || ''), null, plf );
          }
        });
        //CTRL.views.Basic.msgbox.showDownload({
        //  yesCallback: function () {
        //    redirectToDownload(Actions.main + (arg || ''));
        //  }
        //});
      }
    }

    function onFeedback(email) {
      Core.Navigator.protocol('mailto:'+(email||'mozat@mozat.com?subject=Suggestion'), true);
    }

    function switchTab(el, tabs, tabContents) {
      if (!tabs || !tabContents) {
        return;
      }
      var isClicked = !!el;
      el = el || tabs[0];
      for (var i = 0; i < tabs.length; i++) {
        if (tabs[i] == el) {
          tabs[i].classList.add('on');
          trigerAnimate(tabContents.eq(i));
          tabContents[i] && tabContents[i].classList.add('show');
          isClicked && Core.Event.trigger('analyticsCurView', 'tab=' + i);
        } else {
          tabs[i].classList.remove('on');
          tabContents[i] && tabContents[i].classList.remove('show');
        }
      }
      Core.Event.trigger('analyticsCurView');
    }

    function trigerAnimate(el, classname, timeout) {
      if (!el) {
        return;
      }
      classname = classname || 'animated';
      timeout = timeout || 1200;
      el.animTimer && clearTimeout(el.animTimer);
      el.addClass(classname);
      el.animTimer = setTimeout(function () {
        el.removeClass(classname);
      }, timeout);
    }

    function toggleTextSectionExpand(el) {
      el && el.classList.toggle('expand');
    }

    function webLogin(surl, furl, pf) {
      var murl = window.location.href;
      surl = surl || murl;
      furl = furl || murl;
      pf = pf || 'fb';

      redirectToPage(Actions.login
        .replace('{SURL}', encodeURIComponent(surl))
        .replace('{FURL}', encodeURIComponent(furl))
        .replace('{PF}', pf));
    }

    function appLogin(callback){
      Core.NativeBridge.login(null, function (rs) {
        if (rs) {
          CTRL.models.Basic.saveLoginCookieTimeout();
          CTRL.models.Basic.modelUpdate.timer.resetAll();
          CTRL.models.Basic.setAppUserMeta(rs);
          if(callback){
            callback();
          }else{
            Core.Router.run();
          }
        }
      });
    }

    function appUpdate(msg,force) {
      redirectToApp(function () {
        CTRL.views.Basic.msgbox.showDialog({
          msg: msg || 'Please up to date your App',
          noText: force?null:'Close',
          yesText: 'Update',
          yesCallback: function () {
            downloadDejaInApp();
          }
        });
      });
    }

    function appShare(callback, plf) {
      redirectToApp(function () {
        var fn = Core.NativeBridge['share' + (plf ? ('_' + plf) : '')];
        fn && fn(null, callback);
      });
    }

    function appCopyText(text) {
      if (isApp) {
        Core.NativeBridge.copy(text);
      }
    }

    function appModifyTitle(title) {
      title = title || document.title;
      document.title = title;
      if (isApp) {
        Core.NativeBridge.modifytitle(title);
      }
    }

    function appActionButton(name, callback) {
      if (isApp) {
        Core.NativeBridge.updateBarButton(name, callback);
      }
    }

    function appActionShareButton() {
      appActionButton('share');
    }

    function appActionDefaultButton() {
      appActionButton('', function () {
      });
    }
    function appOnUnload(callback){
      if (isApp) {
        Core.NativeBridge.set_before_for_unload(callback);
      }
    }
    function appCloseWebView(){
      if (isApp) {
        Core.NativeBridge.closeweb();
      }
    }
    function appSelfieTest(callback){
      appAPI('selfieTest',null,callback)
    }

    /**
     * @param subProtocol String creationLike,creationDelete,productLike,follow
     */
    function appUpdateProfile(subProtocol){
      appAPI('updateProfile',null,null,subProtocol);
    }
    function appAPI(name, data, callback, subProtocol,redirect){
      if (isApp) {
        Core.NativeBridge.trigger.apply(null,arguments);
      }else if(redirect){
        var proto = [name];
        subProtocol && proto.push(subProtocol);
        redirectToDownload(null,true,Actions.dejafashionSchema+proto.join('/'));
      }
    }
    function downloadDejaInApp() {
      var url = Actions.dejaAppAndroid;
      if ($.os.ios && !$.os.android) {
        url = Actions.dejaAppIos;
      }
      window.location = url;
    }

    function redirectToDejame() {
      redirectToPage(Actions.dejame);
    }

    //打开客户端原生视图
    function redirectToApp(callback, link) {
      if (isApp) {
        callback && callback();
      } else {
        CTRL.views.Basic.msgbox.showDownload({
          yesCallback: function () {
            redirectToDownload(link || window.location.href);
          }
        });
      }
    }

    function redirectToDownload(link, autoopen, schema) {
      link = !!link && link!='0'? ('#url=dejafashion://web/' + link) : '';
      link = !!schema?('#url='+schema): link;
      redirectToPage(Actions.dejaDwonloadBridge + (autoopen ? '?autoopen=1' : '') + link);
    }

    function redirectToPage(link) {
      if (link) {
        !(/__NativeBridge_target/g.test(link)) && appActionDefaultButton();
        window.location = link;
      }
    }

  }//end Controller
  return new Controller;
});
<|endoftext|>


<commit_before>'use strict';

const request = require('request-promise');
const Promise = require('bluebird');

const config = require('../../config');
const scraper = require('./scraper');

module.exports = {
  getFirstMessagingEntry: (body) => {
    const val = body.object == 'page' &&
      body.entry &&
      Array.isArray(body.entry) &&
      body.entry.length > 0 &&
      body.entry[0] &&
      body.entry[0].id == config.Facebook.pageId &&
      body.entry[0].messaging &&
      Array.isArray(body.entry[0].messaging) &&
      body.entry[0].messaging.length > 0 &&
      body.entry[0].messaging[0]
      ;
    return val || null;
  },
  sendTextMessage: (sender, elements) => {
    const urls = elements.map((e) => {
      const url = e.urls[0].expanded_url;
      const metadata = scraper(url);
      return metadata.metadata().then((meta) => {
        return {
          title: meta.title,
          image_url: meta.image,
          subtitle: meta.description,
          buttons: [{
            type: 'web_url',
            url: url,
            title: 'View Article'
          }]
        }
      });
    });

    return Promise.all(urls).then((values) => {
      let messageData = {
        attachment: {
          type: 'template',
          payload: {
            template_type: 'generic',
            elements: values
          }
        }
      };

      return request({
        uri: 'https://graph.facebook.com/v2.6/me/messages',
        qs: {access_token: config.Facebook.pageToken},
        method: 'POST',
        body: {
          recipient: {id: sender},
          message: messageData,
        },
        json: true
      });
    });
  }
};
<commit_msg>Debug<commit_after>'use strict';

const request = require('request-promise');
const Promise = require('bluebird');

const config = require('../../config');
const scraper = require('./scraper');

module.exports = {
  getFirstMessagingEntry: (body) => {
    const val = body.object == 'page' &&
      body.entry &&
      Array.isArray(body.entry) &&
      body.entry.length > 0 &&
      body.entry[0] &&
      body.entry[0].id == config.Facebook.pageId &&
      body.entry[0].messaging &&
      Array.isArray(body.entry[0].messaging) &&
      body.entry[0].messaging.length > 0 &&
      body.entry[0].messaging[0]
      ;
    return val || null;
  },
  sendTextMessage: (sender, elements) => {
    console.log(elements);
    const urls = elements.map((e) => {
      const url = e.urls[0].expanded_url;
      const metadata = scraper(url);
      return metadata.metadata().then((meta) => {
        return {
          title: meta.title,
          image_url: meta.image,
          subtitle: meta.description,
          buttons: [{
            type: 'web_url',
            url: url,
            title: 'View Article'
          }]
        }
      });
    });

    return Promise.all(urls).then((values) => {
      let messageData = {
        attachment: {
          type: 'template',
          payload: {
            template_type: 'generic',
            elements: values
          }
        }
      };

      return request({
        uri: 'https://graph.facebook.com/v2.6/me/messages',
        qs: {access_token: config.Facebook.pageToken},
        method: 'POST',
        body: {
          recipient: {id: sender},
          message: messageData,
        },
        json: true
      });
    });
  }
};
<|endoftext|>


<commit_before>const access = require('../db/model/access');
const check = require('../db/model/check');
let httpStatus = require('http-status');
let projectAccessCache = require('./project-access-cache')(access);

function* validateProjectAccess(next) {
    let projectID = this.params.project_id;
    if (projectID) {
        let projects = yield projectAccessCache.find(projectID);

        if (!projects) {
            this.throw(httpStatus.BAD_REQUEST, "Unknown project");
        }

        if (!projectAccessCache.validateAccess(projectID, this.reqctx.user)) {
            this.throw(httpStatus.UNAUTHORIZED, `No access to project ${projectID}`);
        }

        this.reqctx.project = {
            id: projectID,
            name: projects[0].project_name
        };
    }

    yield next;
}

function* validateExperimentInProject(next) {
    let projectId = this.params.project_id;
    let experimentId = this.params.experiment_id;
    let isInProject = yield check.experimentExistsInProject(projectId, experimentId);
    if (!isInProject) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such experiment ${experimentId}`};
        return this.status;
    }
    yield next;
}

function* validateDatasetInExperiment(next) {
    let experimentId = this.params.experiment_id;
    let datasetId = this.params.dataset_id;
    let isInExperiment = yield check.experimentHasDataset(experimentId, datasetId);
    if (!isInExperiment) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such dataset ${datasetId}`};
        return this.status;
    }
    yield next;
}

function* validateSampleInExperiment(next) {
    let isInExperiment = yield check.sampleInExperiment(this.params.experiment_id, this.params.sample_id);
    if (!isInExperiment) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such sample in experiment ${this.params.sample_id}`};
        return this.status;
    }
    yield next;
}

function* validateProcessInExperiment(next) {
    if (this.params.process_id !== 'templates') {
        let isInExperiment = yield check.processInExperiment(this.params.experiment_id, this.params.process_id);
        if (!isInExperiment) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such process in experiment ${this.params.process_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateSampleInProject(next) {
    if (this.params.sample_id !== 'measurements') {
        let isInProject = yield check.sampleInProject(this.params.project_id, this.params.sample_id);
        if (!isInProject) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such sample in project ${this.params.sample_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateDirectoryInProject(next) {
    if (this.params.directory_id !== 'top') {
        let isInProject = yield check.directoryInProject(this.params.project_id, this.params.directory_id);
        if (!isInProject) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such directory in project ${this.params.directory_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateProcessInProject(next) {
    if (this.params.process_id !== 'templates') {
        let isInProject = yield check.processInProject(this.params.project_id, this.params.process_id);
        if (!isInProject) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such process in project ${this.params.process_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateFileInProject(next) {
    if (this.param.file_id !== 'by_path') {
        let isInProject = yield check.fileInProject(this.params.file_id, this.params.project_id);
        if (!isInProject) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such file in project ${this.params.file_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateTaskInExperiment(next) {
    let isInExperiment = yield check.taskInExperiment(this.params.experiment_id, this.params.task_id);
    if (!isInExperiment) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such task in experiment ${this.params.task_id}`};
        return this.status;
    }
    yield next;
}

function* validateTemplateExists(next) {
    let templateExists = yield check.templateExists(this.params.template_id);
    if (!templateExists) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such template ${this.params.template_id}`};
        return this.status;
    }
    yield next;
}

function* validateNoteInExperiment(next) {
    let isInExperiment = yield check.noteInExperiment(this.params.experiment_id, this.params.note_id);
    if (!isInExperiment) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such note in experiment ${this.params.note_id}`};
        return this.status;
    }
    yield next;
}

module.exports = {
    validateProjectAccess,
    validateExperimentInProject,
    validateDatasetInExperiment,
    validateSampleInExperiment,
    validateProcessInExperiment,
    validateSampleInProject,
    validateDirectoryInProject,
    validateProcessInProject,
    validateFileInProject,
    validateTaskInExperiment,
    validateTemplateExists,
    validateNoteInExperiment
};
<commit_msg>Should reference this.params and not this.param.<commit_after>const access = require('../db/model/access');
const check = require('../db/model/check');
let httpStatus = require('http-status');
let projectAccessCache = require('./project-access-cache')(access);

function* validateProjectAccess(next) {
    let projectID = this.params.project_id;
    if (projectID) {
        let projects = yield projectAccessCache.find(projectID);

        if (!projects) {
            this.throw(httpStatus.BAD_REQUEST, "Unknown project");
        }

        if (!projectAccessCache.validateAccess(projectID, this.reqctx.user)) {
            this.throw(httpStatus.UNAUTHORIZED, `No access to project ${projectID}`);
        }

        this.reqctx.project = {
            id: projectID,
            name: projects[0].project_name
        };
    }

    yield next;
}

function* validateExperimentInProject(next) {
    let projectId = this.params.project_id;
    let experimentId = this.params.experiment_id;
    let isInProject = yield check.experimentExistsInProject(projectId, experimentId);
    if (!isInProject) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such experiment ${experimentId}`};
        return this.status;
    }
    yield next;
}

function* validateDatasetInExperiment(next) {
    let experimentId = this.params.experiment_id;
    let datasetId = this.params.dataset_id;
    let isInExperiment = yield check.experimentHasDataset(experimentId, datasetId);
    if (!isInExperiment) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such dataset ${datasetId}`};
        return this.status;
    }
    yield next;
}

function* validateSampleInExperiment(next) {
    let isInExperiment = yield check.sampleInExperiment(this.params.experiment_id, this.params.sample_id);
    if (!isInExperiment) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such sample in experiment ${this.params.sample_id}`};
        return this.status;
    }
    yield next;
}

function* validateProcessInExperiment(next) {
    if (this.params.process_id !== 'templates') {
        let isInExperiment = yield check.processInExperiment(this.params.experiment_id, this.params.process_id);
        if (!isInExperiment) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such process in experiment ${this.params.process_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateSampleInProject(next) {
    if (this.params.sample_id !== 'measurements') {
        let isInProject = yield check.sampleInProject(this.params.project_id, this.params.sample_id);
        if (!isInProject) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such sample in project ${this.params.sample_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateDirectoryInProject(next) {
    if (this.params.directory_id !== 'top') {
        let isInProject = yield check.directoryInProject(this.params.project_id, this.params.directory_id);
        if (!isInProject) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such directory in project ${this.params.directory_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateProcessInProject(next) {
    if (this.params.process_id !== 'templates') {
        let isInProject = yield check.processInProject(this.params.project_id, this.params.process_id);
        if (!isInProject) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such process in project ${this.params.process_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateFileInProject(next) {
    if (this.params.file_id !== 'by_path') {
        let isInProject = yield check.fileInProject(this.params.file_id, this.params.project_id);
        if (!isInProject) {
            this.status = httpStatus.BAD_REQUEST;
            this.body = {error: `No such file in project ${this.params.file_id}`};
            return this.status;
        }
    }
    yield next;
}

function* validateTaskInExperiment(next) {
    let isInExperiment = yield check.taskInExperiment(this.params.experiment_id, this.params.task_id);
    if (!isInExperiment) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such task in experiment ${this.params.task_id}`};
        return this.status;
    }
    yield next;
}

function* validateTemplateExists(next) {
    let templateExists = yield check.templateExists(this.params.template_id);
    if (!templateExists) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such template ${this.params.template_id}`};
        return this.status;
    }
    yield next;
}

function* validateNoteInExperiment(next) {
    let isInExperiment = yield check.noteInExperiment(this.params.experiment_id, this.params.note_id);
    if (!isInExperiment) {
        this.status = httpStatus.BAD_REQUEST;
        this.body = {error: `No such note in experiment ${this.params.note_id}`};
        return this.status;
    }
    yield next;
}

module.exports = {
    validateProjectAccess,
    validateExperimentInProject,
    validateDatasetInExperiment,
    validateSampleInExperiment,
    validateProcessInExperiment,
    validateSampleInProject,
    validateDirectoryInProject,
    validateProcessInProject,
    validateFileInProject,
    validateTaskInExperiment,
    validateTemplateExists,
    validateNoteInExperiment
};
<|endoftext|>


<commit_before>/**
  This view is for rendering an icon representing the status of a topic

  @class TopicView
  @extends Discourse.View
  @namespace Discourse
  @uses Discourse.Scrolling
  @module Discourse
**/
Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
  templateName: 'topic',
  topicBinding: 'controller.model',
  userFiltersBinding: 'controller.userFilters',
  classNameBindings: ['controller.multiSelect:multi-select',
                      'topic.archetype',
                      'topic.category.read_restricted:read_restricted',
                      'topic.deleted:deleted-topic'],
  menuVisible: true,
  SHORT_POST: 1200,

  postStream: Em.computed.alias('controller.postStream'),

  updateBar: function() {
    Em.run.scheduleOnce('afterRender', this, 'updateProgressBar');
  }.observes('controller.streamPercentage'),

  updateProgressBar: function() {
    var $topicProgress = $('#topic-progress');
    if (!$topicProgress.length) return;

    var totalWidth = $topicProgress.width();
    var progressWidth = this.get('controller.streamPercentage') * totalWidth;

    $topicProgress.find('.bg')
                  .css("border-right-width", (progressWidth === totalWidth) ? "0px" : "1px")
                  .width(progressWidth);
  },

  updateTitle: function() {
    var title = this.get('topic.title');
    if (title) return Discourse.set('title', title);
  }.observes('topic.loaded', 'topic.title'),

  currentPostChanged: function() {
    var current = this.get('controller.currentPost');

    var topic = this.get('topic');
    if (!(current && topic)) return;

    if (current > (this.get('maxPost') || 0)) {
      this.set('maxPost', current);
    }

    var postUrl = topic.get('url');
    if (current > 1) { postUrl += "/" + current; }
    // TODO: @Robin, this should all be integrated into the router,
    //  the view should not be performing routing work
    //
    //  This workaround ensures the router is aware the route changed,
    //    without it, the up button was broken on long topics.
    //  To repro, go to a topic with 50 posts, go to first post,
    //    scroll to end, click up button ... nothing happens
    var handler =_.first(
          _.where(Discourse.URL.get("router.router.currentHandlerInfos"),
              function(o) {
                return o.name === "topic.fromParams";
              })
          );
    if(handler){
      handler.context = {nearPost: current};
    }
    Discourse.URL.replaceState(postUrl);
  }.observes('controller.currentPost', 'highest_post_number'),

  composeChanged: function() {
    var composerController = Discourse.get('router.composerController');
    composerController.clearState();
    composerController.set('topic', this.get('topic'));
  }.observes('composer'),

  enteredTopic: function() {
    if (this.present('controller.enteredAt')) {
      var topicView = this;
      Em.run.schedule('afterRender', function() {
        topicView.updatePosition();
      });
    }
  }.observes('controller.enteredAt'),

  didInsertElement: function(e) {
    this.bindScrolling({debounce: 0});

    var topicView = this;
    Em.run.schedule('afterRender', function () {
      $(window).resize('resize.discourse-on-scroll', function() {
        topicView.updatePosition();
      });
    });

    this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
      if ($(e.target).hasClass('mention')) { return false; }
      return Discourse.ClickTrack.trackClick(e);
    });
  },

  // This view is being removed. Shut down operations
  willDestroyElement: function() {

    this.unbindScrolling();
    $(window).unbind('resize.discourse-on-scroll');

    // Unbind link tracking
    this.$().off('mouseup.discourse-redirect', '.cooked a, a.track-link');

    this.resetExamineDockCache();

    // this happens after route exit, stuff could have trickled in
    this.set('controller.controllers.header.showExtraInfo', false);
  },

  debounceLoadSuggested: Discourse.debounce(function(){
    if (this.get('isDestroyed') || this.get('isDestroying')) { return; }

    var incoming = this.get('topicTrackingState.newIncoming');
    var suggested = this.get('topic.details.suggested_topics');
    var topicId = this.get('topic.id');

    if(suggested) {

      var existing = _.invoke(suggested, 'get', 'id');

      var lookup = _.chain(incoming)
        .last(5)
        .reverse()
        .union(existing)
        .uniq()
        .without(topicId)
        .first(5)
        .value();

      Discourse.TopicList.loadTopics(lookup, "").then(function(topics){
        suggested.clear();
        suggested.pushObjects(topics);
      });
    }
  }, 1000),

  hasNewSuggested: function(){
    this.debounceLoadSuggested();
  }.observes('topicTrackingState.incomingCount'),

  gotFocus: function(){
    if (Discourse.get('hasFocus')){
      this.scrolled();
    }
  }.observes("Discourse.hasFocus"),

  getPost: function($post){
    var post, postView;
    postView = Ember.View.views[$post.prop('id')];
    if (postView) {
      return postView.get('post');
    }
    return null;
  },

  // Called for every post seen, returns the post number
  postSeen: function($post) {
    var post = this.getPost($post);

    if (post) {
      var postNumber = post.get('post_number');
      if (postNumber > (this.get('controller.last_read_post_number') || 0)) {
        this.set('controller.last_read_post_number', postNumber);
      }
      if (!post.get('read')) {
        post.set('read', true);
      }
      return post.get('post_number');
    }
  },

  resetExamineDockCache: function() {
    this.set('docAt', false);
  },

  updateDock: function(postView) {
    if (!postView) return;
    var post = postView.get('post');
    if (!post) return;

    this.set('controller.progressPosition', this.get('postStream').indexOf(post) + 1);
  },

  throttledPositionUpdate: Discourse.debounce(function() {
    Discourse.ScreenTrack.current().scrolled();
    var model = this.get('controller.model');
    if (model && this.get('nextPositionUpdate')) {
      this.set('controller.currentPost', this.get('nextPositionUpdate'));
    }
  },500),

  scrolled: function(){
    this.updatePosition();
  },

  /**
    Process the posts the current user has seen in the topic.

    @private
    @method processSeenPosts
  **/
  processSeenPosts: function() {
    var rows = $('.topic-post.ready');
    if (!rows || rows.length === 0) { return; }

    // if we have no rows
    var info = Discourse.Eyeline.analyze(rows);
    if(!info) { return; }

    // are we scrolling upwards?
    if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
      var $body = $('body');
      var $elem = $(rows[0]);
      var distToElement = $body.scrollTop() - $elem.position().top;
      this.get('postStream').prependMore().then(function() {
        Em.run.next(function () {
          $('html, body').scrollTop($elem.position().top + distToElement);
        });
      });
    }

    // are we scrolling down?
    var currentPost;
    if(info.bottom === rows.length-1) {
      currentPost = this.postSeen($(rows[info.bottom]));
      this.get('postStream').appendMore();
    }

    // update dock
    this.updateDock(Ember.View.views[rows[info.bottom].id]);

    // mark everything on screen read
    var topicView = this;
    _.each(info.onScreen,function(item){
      var seen = topicView.postSeen($(rows[item]));
      currentPost = currentPost || seen;
    });

    var currentForPositionUpdate = currentPost;
    if (!currentForPositionUpdate) {
      var postView = this.getPost($(rows[info.bottom]));
      if (postView) { currentForPositionUpdate = postView.get('post_number'); }
    }

    if (currentForPositionUpdate) {
      this.set('nextPositionUpdate', currentPost || currentForPositionUpdate);
      this.throttledPositionUpdate();
    } else {
      console.error("can't update position ");
    }
  },

  /**
    The user has scrolled the window, or it is finished rendering and ready for processing.

    @method updatePosition
  **/
  updatePosition: function() {
    this.processSeenPosts();

    var offset = window.pageYOffset || $('html').scrollTop();
    if (!this.get('docAt')) {
      var title = $('#topic-title');
      if (title && title.length === 1) {
        this.set('docAt', title.offset().top);
      }
    }

    var headerController = this.get('controller.controllers.header'),
        topic = this.get('controller.model');
    if (this.get('docAt')) {
      headerController.set('showExtraInfo', offset >= this.get('docAt') || topic.get('postStream.firstPostNotLoaded'));
    } else {
      headerController.set('showExtraInfo', topic.get('postStream.firstPostNotLoaded'));
    }

    // Dock the counter if necessary
    var $lastPost = $('article[data-post-id=' + topic.get('postStream.lastPostId') + "]");
    var lastPostOffset = $lastPost.offset();
    if (!lastPostOffset) {
      this.set('controller.dockedCounter', false);
      return;
    }
    this.set('controller.dockedCounter', (offset >= (lastPostOffset.top + $lastPost.height()) - $(window).height()));
  },

  topicTrackingState: function() {
    return Discourse.TopicTrackingState.current();
  }.property(),

  browseMoreMessage: function() {
    var opts = {
      latestLink: "" + (I18n.t("topic.view_latest_topics")) + ""
    };

    var category = this.get('controller.content.category');
    if (category) {
      opts.catLink = Discourse.Utilities.categoryLink(category);
    } else {
      opts.catLink = "" + (I18n.t("topic.browse_all_categories")) + "";
    }

    var tracking = this.get('topicTrackingState');

    var unreadTopics = tracking.countUnread();
    var newTopics = tracking.countNew();

    if (newTopics + unreadTopics > 0) {
      var hasBoth = unreadTopics > 0 && newTopics > 0;

      return I18n.messageFormat("topic.read_more_MF", {
        "BOTH": hasBoth,
        "UNREAD": unreadTopics,
        "NEW": newTopics,
        "CATEGORY": category ? true : false,
        latestLink: opts.latestLink,
        catLink: opts.catLink
      });
    }
    else if (category) {
      return I18n.t("topic.read_more_in_category", opts);
    } else {
      return I18n.t("topic.read_more", opts);
    }
  }.property('topicTrackingState.messageCount')

});

Discourse.TopicView.reopenClass({

  // Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not.
  jumpToPost: function(topicId, postNumber, avoidScrollIfPossible) {
    Em.run.scheduleOnce('afterRender', function() {
      var rows = $('.topic-post.ready');

      // Make sure we're looking at the topic we want to scroll to
      if (topicId !== parseInt($('#topic').data('topic-id'), 10)) { return false; }

      var $post = $("#post_" + postNumber);
      if ($post.length) {

        var postTop = $post.offset().top;
        var highlight = true;

        var header = $('header');
        var title = $('#topic-title');
        var expectedOffset = title.height() - header.find('.contents').height();

        if (expectedOffset < 0) {
          expectedOffset = 0;
        }

        var offset = (header.outerHeight(true) + expectedOffset);
        var windowScrollTop = $('html, body').scrollTop();

        if (avoidScrollIfPossible && postTop > windowScrollTop + offset && postTop < windowScrollTop + $(window).height() + 100) {
          // in view
        } else {
          // not in view ... bring into view
          if (postNumber === 1) {
            $(window).scrollTop(0);
            highlight = false;
          } else {
            var desired = $post.offset().top - offset;
            $(window).scrollTop(desired);

            // TODO @Robin, I am seeing multiple events in chrome issued after
            // jumpToPost if I refresh a page, sometimes I see 2, sometimes 3
            //
            // 1. Where are they coming from?
            // 2. On refresh we should only issue a single scrollTop
            // 3. If you are scrolled down in BoingBoing desired sometimes is wrong
            //      due to vanishing header, we should not be rendering it imho until after
            //      we render the posts

            var first = true;
            var t = new Date();
            // console.log("DESIRED:" + desired);
            var enforceDesired = function(){
              if($(window).scrollTop() !== desired) {
                console.log("GOT EVENT " + $(window).scrollTop());
                console.log("Time " + (new Date() - t));
                console.trace();
                if(first) {
                  $(window).scrollTop(desired);
                  first = false;
                }
                // $(document).unbind("scroll", enforceDesired);
              }
            };

            // uncomment this line to help debug this issue.
            // $(document).scroll(enforceDesired);
          }
        }

        if(highlight) {
          var $contents = $('.topic-body .contents', $post);
          var origColor = $contents.data('orig-color') || $contents.css('backgroundColor');

          $contents.data("orig-color", origColor);
          $contents
            .addClass('highlighted')
            .stop()
            .animate({ backgroundColor: origColor }, 2500, 'swing', function(){
              $contents.removeClass('highlighted');
            });
        }
      }
    });
  }
});
<commit_msg>speed up scrolling on android<commit_after>/**
  This view is for rendering an icon representing the status of a topic

  @class TopicView
  @extends Discourse.View
  @namespace Discourse
  @uses Discourse.Scrolling
  @module Discourse
**/
Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
  templateName: 'topic',
  topicBinding: 'controller.model',
  userFiltersBinding: 'controller.userFilters',
  classNameBindings: ['controller.multiSelect:multi-select',
                      'topic.archetype',
                      'topic.category.read_restricted:read_restricted',
                      'topic.deleted:deleted-topic'],
  menuVisible: true,
  SHORT_POST: 1200,

  postStream: Em.computed.alias('controller.postStream'),

  updateBar: function() {
    Em.run.scheduleOnce('afterRender', this, 'updateProgressBar');
  }.observes('controller.streamPercentage'),

  updateProgressBar: function() {
    var $topicProgress = this.topicProgress;

    // cache lookup
    if (!$topicProgress) {
      $topicProgress = $('#topic-progress');
      if (!$topicProgress.length) {
        return;
      }
      this.topicProgress = $topicProgress;
    }

    // speeds up stuff, bypass jquery slowness and extra checks
    var totalWidth = $topicProgress[0].offsetWidth;
    var progressWidth = this.get('controller.streamPercentage') * totalWidth;

    $topicProgress.find('.bg')
                  .css("border-right-width", (progressWidth === totalWidth) ? "0px" : "1px")
                  .width(progressWidth);
  },

  updateTitle: function() {
    var title = this.get('topic.title');
    if (title) return Discourse.set('title', title);
  }.observes('topic.loaded', 'topic.title'),

  currentPostChanged: function() {
    var current = this.get('controller.currentPost');

    var topic = this.get('topic');
    if (!(current && topic)) return;

    if (current > (this.get('maxPost') || 0)) {
      this.set('maxPost', current);
    }

    var postUrl = topic.get('url');
    if (current > 1) { postUrl += "/" + current; }
    // TODO: @Robin, this should all be integrated into the router,
    //  the view should not be performing routing work
    //
    //  This workaround ensures the router is aware the route changed,
    //    without it, the up button was broken on long topics.
    //  To repro, go to a topic with 50 posts, go to first post,
    //    scroll to end, click up button ... nothing happens
    var handler =_.first(
          _.where(Discourse.URL.get("router.router.currentHandlerInfos"),
              function(o) {
                return o.name === "topic.fromParams";
              })
          );
    if(handler){
      handler.context = {nearPost: current};
    }
    Discourse.URL.replaceState(postUrl);
  }.observes('controller.currentPost', 'highest_post_number'),

  composeChanged: function() {
    var composerController = Discourse.get('router.composerController');
    composerController.clearState();
    composerController.set('topic', this.get('topic'));
  }.observes('composer'),

  enteredTopic: function() {
    if (this.present('controller.enteredAt')) {
      var topicView = this;
      Em.run.schedule('afterRender', function() {
        topicView.updatePosition();
      });
    }
  }.observes('controller.enteredAt'),

  didInsertElement: function(e) {
    this.bindScrolling({debounce: 0});

    var topicView = this;
    Em.run.schedule('afterRender', function () {
      $(window).resize('resize.discourse-on-scroll', function() {
        topicView.updatePosition();
      });
    });

    this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
      if ($(e.target).hasClass('mention')) { return false; }
      return Discourse.ClickTrack.trackClick(e);
    });
  },

  // This view is being removed. Shut down operations
  willDestroyElement: function() {

    this.unbindScrolling();
    $(window).unbind('resize.discourse-on-scroll');

    // Unbind link tracking
    this.$().off('mouseup.discourse-redirect', '.cooked a, a.track-link');

    this.resetExamineDockCache();

    // this happens after route exit, stuff could have trickled in
    this.set('controller.controllers.header.showExtraInfo', false);
  },

  debounceLoadSuggested: Discourse.debounce(function(){
    if (this.get('isDestroyed') || this.get('isDestroying')) { return; }

    var incoming = this.get('topicTrackingState.newIncoming');
    var suggested = this.get('topic.details.suggested_topics');
    var topicId = this.get('topic.id');

    if(suggested) {

      var existing = _.invoke(suggested, 'get', 'id');

      var lookup = _.chain(incoming)
        .last(5)
        .reverse()
        .union(existing)
        .uniq()
        .without(topicId)
        .first(5)
        .value();

      Discourse.TopicList.loadTopics(lookup, "").then(function(topics){
        suggested.clear();
        suggested.pushObjects(topics);
      });
    }
  }, 1000),

  hasNewSuggested: function(){
    this.debounceLoadSuggested();
  }.observes('topicTrackingState.incomingCount'),

  gotFocus: function(){
    if (Discourse.get('hasFocus')){
      this.scrolled();
    }
  }.observes("Discourse.hasFocus"),

  getPost: function($post){
    var post, postView;
    postView = Ember.View.views[$post.prop('id')];
    if (postView) {
      return postView.get('post');
    }
    return null;
  },

  // Called for every post seen, returns the post number
  postSeen: function($post) {
    var post = this.getPost($post);

    if (post) {
      var postNumber = post.get('post_number');
      if (postNumber > (this.get('controller.last_read_post_number') || 0)) {
        this.set('controller.last_read_post_number', postNumber);
      }
      if (!post.get('read')) {
        post.set('read', true);
      }
      return post.get('post_number');
    }
  },

  resetExamineDockCache: function() {
    this.set('docAt', false);
  },

  updateDock: function(postView) {
    if (!postView) return;
    var post = postView.get('post');
    if (!post) return;

    this.set('controller.progressPosition', this.get('postStream').indexOf(post) + 1);
  },

  throttledPositionUpdate: Discourse.debounce(function() {
    Discourse.ScreenTrack.current().scrolled();
    var model = this.get('controller.model');
    if (model && this.get('nextPositionUpdate')) {
      this.set('controller.currentPost', this.get('nextPositionUpdate'));
    }
  },500),

  scrolled: function(){
    this.updatePosition();
  },

  /**
    Process the posts the current user has seen in the topic.

    @private
    @method processSeenPosts
  **/
  processSeenPosts: function() {
    var rows = $('.topic-post.ready');
    if (!rows || rows.length === 0) { return; }

    // if we have no rows
    var info = Discourse.Eyeline.analyze(rows);
    if(!info) { return; }

    // are we scrolling upwards?
    if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
      var $body = $('body');
      var $elem = $(rows[0]);
      var distToElement = $body.scrollTop() - $elem.position().top;
      this.get('postStream').prependMore().then(function() {
        Em.run.next(function () {
          $('html, body').scrollTop($elem.position().top + distToElement);
        });
      });
    }

    // are we scrolling down?
    var currentPost;
    if(info.bottom === rows.length-1) {
      currentPost = this.postSeen($(rows[info.bottom]));
      this.get('postStream').appendMore();
    }

    // update dock
    this.updateDock(Ember.View.views[rows[info.bottom].id]);

    // mark everything on screen read
    var topicView = this;
    _.each(info.onScreen,function(item){
      var seen = topicView.postSeen($(rows[item]));
      currentPost = currentPost || seen;
    });

    var currentForPositionUpdate = currentPost;
    if (!currentForPositionUpdate) {
      var postView = this.getPost($(rows[info.bottom]));
      if (postView) { currentForPositionUpdate = postView.get('post_number'); }
    }

    if (currentForPositionUpdate) {
      this.set('nextPositionUpdate', currentPost || currentForPositionUpdate);
      this.throttledPositionUpdate();
    } else {
      console.error("can't update position ");
    }
  },

  /**
    The user has scrolled the window, or it is finished rendering and ready for processing.

    @method updatePosition
  **/
  updatePosition: function() {
    this.processSeenPosts();

    var offset = window.pageYOffset || $('html').scrollTop();
    if (!this.get('docAt')) {
      var title = $('#topic-title');
      if (title && title.length === 1) {
        this.set('docAt', title.offset().top);
      }
    }

    var headerController = this.get('controller.controllers.header'),
        topic = this.get('controller.model');
    if (this.get('docAt')) {
      headerController.set('showExtraInfo', offset >= this.get('docAt') || topic.get('postStream.firstPostNotLoaded'));
    } else {
      headerController.set('showExtraInfo', topic.get('postStream.firstPostNotLoaded'));
    }

    // Dock the counter if necessary
    var $lastPost = $('article[data-post-id=' + topic.get('postStream.lastPostId') + "]");
    var lastPostOffset = $lastPost.offset();
    if (!lastPostOffset) {
      this.set('controller.dockedCounter', false);
      return;
    }
    this.set('controller.dockedCounter', (offset >= (lastPostOffset.top + $lastPost.height()) - $(window).height()));
  },

  topicTrackingState: function() {
    return Discourse.TopicTrackingState.current();
  }.property(),

  browseMoreMessage: function() {
    var opts = {
      latestLink: "" + (I18n.t("topic.view_latest_topics")) + ""
    };

    var category = this.get('controller.content.category');
    if (category) {
      opts.catLink = Discourse.Utilities.categoryLink(category);
    } else {
      opts.catLink = "" + (I18n.t("topic.browse_all_categories")) + "";
    }

    var tracking = this.get('topicTrackingState');

    var unreadTopics = tracking.countUnread();
    var newTopics = tracking.countNew();

    if (newTopics + unreadTopics > 0) {
      var hasBoth = unreadTopics > 0 && newTopics > 0;

      return I18n.messageFormat("topic.read_more_MF", {
        "BOTH": hasBoth,
        "UNREAD": unreadTopics,
        "NEW": newTopics,
        "CATEGORY": category ? true : false,
        latestLink: opts.latestLink,
        catLink: opts.catLink
      });
    }
    else if (category) {
      return I18n.t("topic.read_more_in_category", opts);
    } else {
      return I18n.t("topic.read_more", opts);
    }
  }.property('topicTrackingState.messageCount')

});

Discourse.TopicView.reopenClass({

  // Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not.
  jumpToPost: function(topicId, postNumber, avoidScrollIfPossible) {
    Em.run.scheduleOnce('afterRender', function() {
      var rows = $('.topic-post.ready');

      // Make sure we're looking at the topic we want to scroll to
      if (topicId !== parseInt($('#topic').data('topic-id'), 10)) { return false; }

      var $post = $("#post_" + postNumber);
      if ($post.length) {

        var postTop = $post.offset().top;
        var highlight = true;

        var header = $('header');
        var title = $('#topic-title');
        var expectedOffset = title.height() - header.find('.contents').height();

        if (expectedOffset < 0) {
          expectedOffset = 0;
        }

        var offset = (header.outerHeight(true) + expectedOffset);
        var windowScrollTop = $('html, body').scrollTop();

        if (avoidScrollIfPossible && postTop > windowScrollTop + offset && postTop < windowScrollTop + $(window).height() + 100) {
          // in view
        } else {
          // not in view ... bring into view
          if (postNumber === 1) {
            $(window).scrollTop(0);
            highlight = false;
          } else {
            var desired = $post.offset().top - offset;
            $(window).scrollTop(desired);

            // TODO @Robin, I am seeing multiple events in chrome issued after
            // jumpToPost if I refresh a page, sometimes I see 2, sometimes 3
            //
            // 1. Where are they coming from?
            // 2. On refresh we should only issue a single scrollTop
            // 3. If you are scrolled down in BoingBoing desired sometimes is wrong
            //      due to vanishing header, we should not be rendering it imho until after
            //      we render the posts

            var first = true;
            var t = new Date();
            // console.log("DESIRED:" + desired);
            var enforceDesired = function(){
              if($(window).scrollTop() !== desired) {
                console.log("GOT EVENT " + $(window).scrollTop());
                console.log("Time " + (new Date() - t));
                console.trace();
                if(first) {
                  $(window).scrollTop(desired);
                  first = false;
                }
                // $(document).unbind("scroll", enforceDesired);
              }
            };

            // uncomment this line to help debug this issue.
            // $(document).scroll(enforceDesired);
          }
        }

        if(highlight) {
          var $contents = $('.topic-body .contents', $post);
          var origColor = $contents.data('orig-color') || $contents.css('backgroundColor');

          $contents.data("orig-color", origColor);
          $contents
            .addClass('highlighted')
            .stop()
            .animate({ backgroundColor: origColor }, 2500, 'swing', function(){
              $contents.removeClass('highlighted');
            });
        }
      }
    });
  }
});
<|endoftext|>