<commit_before>/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, {
/******/ 				configurable: false,
/******/ 				enumerable: true,
/******/ 				get: getter
/******/ 			});
/******/ 		}
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "/";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 9);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var _tag = function _tag(tagName, properties, children) {
    var element = document.createElementNS("http://www.w3.org/2000/svg", tagName);
    Object.keys(properties).forEach(function (name) {
        element.setAttribute(name, properties[name]);
    });
    [].concat(children || []).map(function (def) {
        return typeof def === "string" ? document.createTextNode(def) : def;
    }).forEach(function (node) {
        return element.appendChild(node);
    });
    return element;
},
    _svg = _tag.bind(null, "svg");

["circle", "text", "path", "defs", "linearGradient", "stop", "rect"].forEach(function (tag) {
    _svg[tag] = _tag.bind(null, tag);
});

module.exports = _svg;

/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

module.exports = {
    background: "#ccd0d3",
    circle: {
        light: "#dce0e1",
        dark: "#b3b7ba",
        background: "#a8acae"
    },
    progress: {
        total: "#def1f2",
        step: "#f3dfe6"
    },
    text: {
        time: "#ffffff",
        ms: "#ffffff",
        step: "#000000"
    }
};

/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var svg = __webpack_require__(0),
    colors = __webpack_require__(1);

module.exports = function () {
    return svg.defs({}, [svg.linearGradient({ id: "outerBorder", x1: 0.5, x2: 0, y1: 0.5, y2: 0 }, [svg.stop({ offset: "0%", "stop-color": colors.circle.light }), svg.stop({ offset: "100%", "stop-color": colors.circle.dark })]), svg.linearGradient({ id: "innerBorder", x1: 0.5, x2: 0, y1: 0.5, y2: 0 }, [svg.stop({ offset: "0%", "stop-color": colors.circle.dark }), svg.stop({ offset: "100%", "stop-color": colors.circle.light })])]);
};

/***/ }),
/* 3 */,
/* 4 */,
/* 5 */,
/* 6 */,
/* 7 */,
/* 8 */,
/* 9 */
/***/ (function(module, exports, __webpack_require__) {

module.exports = __webpack_require__(10);

/***/ }),
/* 10 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

/*eslint-disable no-alert*/

var svg = __webpack_require__(0),
    colors = __webpack_require__(1),
    gradients = __webpack_require__(2),
    digit = function digit(x, baseId) {
    return [svg.rect({ x: x - 0.1, y: -0.8, width: 0.2, height: 0.4,
        fill: colors.circle.background, stroke: "url(#outerBorder)", "stroke-width": 0.01 }), svg.text({ id: "inc" + baseId, x: x, y: -0.8, "font-family": "Arial", "font-size": 0.2, "text-anchor": "middle",
        fill: colors.text.step, stroke: "url(#innerBorder)", "stroke-opacity": 0.2, "stroke-width": 0.001 }, "⏶"), svg.text({ id: "dig" + baseId, x: x, y: -0.5, "font-family": "Arial", "font-size": 0.3, "text-anchor": "middle",
        fill: colors.text.step, stroke: "url(#innerBorder)", "stroke-opacity": 0.2, "stroke-width": 0.001 }, "0"), svg.text({ id: "dec" + baseId, x: x, y: -0.28, "font-family": "Arial", "font-size": 0.2, "text-anchor": "middle",
        fill: colors.text.step, stroke: "url(#innerBorder)", "stroke-opacity": 0.2, "stroke-width": 0.001 }, "⏷")];
},
    setup = function setup() {
    document.body.appendChild(svg({
        width: "100%",
        height: "100%",
        viewBox: "-1 -1 2 2",
        style: "background-color: " + colors.background + ";"
    }, [gradients()].concat(digit(-0.4, 0), digit(-0.15, 1), digit(0.15, 2), digit(0.4, 3))));
};

window.addEventListener("load", setup);

/***/ })
/******/ ]);<commit_msg>Latest<commit_after>/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, {
/******/ 				configurable: false,
/******/ 				enumerable: true,
/******/ 				get: getter
/******/ 			});
/******/ 		}
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "/";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 9);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var _tag = function _tag(tagName, properties, children) {
    var element = document.createElementNS("http://www.w3.org/2000/svg", tagName);
    Object.keys(properties).forEach(function (name) {
        element.setAttribute(name, properties[name]);
    });
    [].concat(children || []).map(function (def) {
        return typeof def === "string" ? document.createTextNode(def) : def;
    }).forEach(function (node) {
        return element.appendChild(node);
    });
    return element;
},
    _svg = _tag.bind(null, "svg");

["circle", "text", "path", "defs", "linearGradient", "stop", "rect", "g"].forEach(function (tag) {
    _svg[tag] = _tag.bind(null, tag);
});

module.exports = _svg;

/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

module.exports = {
    background: "#ccd0d3",
    circle: {
        light: "#dce0e1",
        dark: "#b3b7ba",
        background: "#a8acae"
    },
    progress: {
        total: "#def1f2",
        step: "#f3dfe6"
    },
    text: {
        time: "#ffffff",
        ms: "#ffffff",
        step: "#000000"
    }
};

/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var svg = __webpack_require__(0),
    colors = __webpack_require__(1);

module.exports = function () {
    return svg.defs({}, [svg.linearGradient({ id: "outerBorder", x1: 0.5, x2: 0, y1: 0.5, y2: 0 }, [svg.stop({ offset: "0%", "stop-color": colors.circle.light }), svg.stop({ offset: "100%", "stop-color": colors.circle.dark })]), svg.linearGradient({ id: "innerBorder", x1: 0.5, x2: 0, y1: 0.5, y2: 0 }, [svg.stop({ offset: "0%", "stop-color": colors.circle.dark }), svg.stop({ offset: "100%", "stop-color": colors.circle.light })])]);
};

/***/ }),
/* 3 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var _zero = function _zero(x) {
    var count = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;

    var result = [],
        max = Math.pow(10, count - 1);
    while (max > 1 && x < max) {
        result.push("0");
        max /= 10;
    }
    result.push(x);
    return result.join("");
};

module.exports = function (tick) {
    var ms = _zero(tick % 1000, 3),
        seconds = (tick - ms) / 1000,
        s = _zero(seconds % 60),
        m = _zero((seconds - s) / 60),
        time = m + ":" + s;
    return { time: time, ms: ms };
};

/***/ }),
/* 4 */,
/* 5 */,
/* 6 */,
/* 7 */,
/* 8 */,
/* 9 */
/***/ (function(module, exports, __webpack_require__) {

module.exports = __webpack_require__(10);

/***/ }),
/* 10 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

/*eslint-disable no-alert*/

var svg = __webpack_require__(0),
    colors = __webpack_require__(1),
    gradients = __webpack_require__(2),
    sequenceEditor = __webpack_require__(11).allocate(),
    createDigit = function createDigit(x, baseId) {
    var textProperties = {
        "font-family": "Arial", "font-size": 0.25, "text-anchor": "middle",
        fill: colors.text.step, stroke: "url(#innerBorder)", "stroke-opacity": 0.2, "stroke-width": 0.001
    };
    return [svg.rect({ x: x - 0.1, y: -0.8, width: 0.2, height: 0.4,
        fill: colors.circle.background, stroke: "url(#outerBorder)", "stroke-width": 0.01 }), svg.text(Object.assign({ id: "inc" + baseId, x: x, y: -0.8 }, textProperties), "⏶"), svg.text(Object.assign({ id: "dig" + baseId, x: x, y: -0.52 }, textProperties), ""), svg.text(Object.assign({ id: "dec" + baseId, x: x, y: -0.26 }, textProperties), "⏷")];
},
    createButton = function createButton(_ref) {
    var id = _ref.id,
        cx = _ref.cx,
        x = _ref.x,
        y = _ref.y,
        label = _ref.label;
    return [svg.g({ id: id }, [svg.circle({ r: 0.15, cx: cx, cy: 0.7,
        fill: colors.circle.light, stroke: "url(#innerBorder)", "stroke-width": 0.01 }), svg.text({ x: x, y: y, "font-family": "Arial", "font-size": 0.2, "text-anchor": "middle",
        fill: colors.text.step, stroke: "url(#outerBorder)", "stroke-opacity": 0.2, "stroke-width": 0.001 }, label)])];
},
    refresh = function refresh(sequence /*, lengthChanged*/) {
    var current = sequence[sequence.length - 1];
    [0, 1, 3, 4].forEach(function (pos, digit) {
        document.getElementById("dig" + digit).innerHTML = current.substr(pos, 1);
    });
    // alert(sequence, lengthChanged);
},
    setup = function setup() {
    document.body.appendChild(svg({
        width: "100%",
        height: "100%",
        viewBox: "-1 -1 2 2",
        style: "background-color: " + colors.background + ";"
    }, [gradients()].concat(createDigit(-0.4, 0), createDigit(-0.15, 1), createDigit(0.15, 2), createDigit(0.4, 3)).concat(createButton({ id: "remove", cx: -0.4, x: -0.4, y: 0.75, label: "⎌" }), createButton({ id: "add", cx: 0, x: 0, y: 0.77, label: "+" }), createButton({ id: "run", cx: 0.4, x: 0.42, y: 0.77, label: "▶" }))));
    sequenceEditor.on(refresh);
},
    noop = function noop() {},
    mapping = {
    inc0: function inc0() {
        return sequenceEditor.inc(600);
    },
    dec0: function dec0() {
        return sequenceEditor.dec(600);
    },
    inc1: function inc1() {
        return sequenceEditor.inc(60);
    },
    dec1: function dec1() {
        return sequenceEditor.dec(60);
    },
    inc2: function inc2() {
        return sequenceEditor.inc(10);
    },
    dec2: function dec2() {
        return sequenceEditor.dec(10);
    },
    inc3: function inc3() {
        return sequenceEditor.inc(1);
    },
    dec3: function dec3() {
        return sequenceEditor.dec(1);
    }
};

window.addEventListener("load", setup);

window.addEventListener("click", function (e) {
    var target = e.target,
        id = void 0;
    while (!id && target) {
        id = target.id;
        target = target.parentNode;
    }
    (mapping[id] || noop)();
});

/***/ }),
/* 11 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var tickFormatter = __webpack_require__(3),
    _allocate = function _allocate() {
    return {
        callback: function callback() {},
        sequence: [0]
    };
},
    _notify = function _notify(editor, lengthChanged) {
    editor.callback(editor.sequence.map(function (tick) {
        return tickFormatter(tick).time;
    }), lengthChanged || false);
},
    _attach = function _attach(editor, callback) {
    editor.callback = callback;
    _notify(editor);
},
    _inc = function _inc(editor, sec) {
    var top = editor.sequence.pop() + sec * 1000;
    if (top < 0) {
        top = 0;
    }
    editor.sequence.push(top);
    _notify(editor);
},
    _add = function _add(editor) {
    editor.sequence.push(0);
    _notify(editor, true);
},
    _remove = function _remove(editor) {
    var sequence = editor.sequence,
        hasRemainingItems = sequence.length > 1;
    if (hasRemainingItems) {
        sequence.pop();
    } else {
        sequence[0] = 0;
    }
    _notify(editor, hasRemainingItems);
},
    _get = function _get(editor) {
    return editor.sequence;
};

module.exports = {

    allocate: function allocate() {
        var editor = _allocate();
        return {
            on: function on(callback) {
                return _attach(editor, callback);
            },
            inc: function inc(sec) {
                return _inc(editor, sec);
            },
            dec: function dec(sec) {
                return _inc(editor, -sec);
            },
            add: function add() {
                return _add(editor);
            },
            remove: function remove() {
                return _remove(editor);
            },
            get: function get() {
                return _get(editor);
            }
        };
    }

};

/***/ })
/******/ ]);<|endoftext|>


<commit_before>"use strict";!function(n){var e=function(n,e){function t(n){var t=document.querySelectorAll(n);if(!t.length)return!1;var i=[];return r(t,function(n){i.push({parent:n})}),o(i,function(n){n.image=n.parent.querySelector(e)}),i}function i(){return"function"==typeof requestAnimationFrame&&!/iP(ad|hone|od).*OS\s7.*/.test(navigator.userAgent)}function o(n,e){for(var t=0;t<n.length;t++)e(n[t])}function r(n,e){return n.length?void o(n,e):void e(n)}function a(){function n(n){n.style.display="block"}o(d,function(e){e.image&&n(e.image)})}function c(){u(),window.addEventListener("resize",function(){return u()})}function f(n){return n.getBoundingClientRect().height}function u(){var n=window.innerHeight;d.forEach(function(e){if(e.image){var t=f(e.parent),i=e.parent.offsetTop;e.distanceToVisible=i-n,e.height=t,e.offset=e.parent.offsetTop,e.parallaxSpace=f(e.image)-t,e.scrollSpace=t+n}})}function s(n,e){var t=e>=n.distanceToVisible,i=e>=n.height+n.offset;if(t&&!i){var o=e-n.distanceToVisible,r=o-n.scrollSpace/2,a=n.parallaxSpace/n.scrollSpace,c=parseFloat((r*a).toFixed(1));n.image.style[g]="translateY("+c*-1+"px)"}}function l(){requestAnimationFrame(function(){var n=window.pageYOffset;d.forEach(function(e){return s(e,n)}),l()})}function p(){l()}if(n&&e){var d=t(n),g="webkitTransform"in document.body.style?"webkitTransform":"transform";if(d)return a(),i()&&(c(),p()),{recalculate:function(){u()}}}};"object"==typeof module?module.exports=e:n.Prlx=e}(this);<commit_msg>:package: Compile<commit_after>"use strict";!function(n){var prlx=function(n,e){function t(n){var t=document.querySelectorAll(n);if(!t.length)return!1;var i=[];return r(t,function(n){i.push({parent:n})}),o(i,function(n){n.image=n.parent.querySelector(e)}),i}function i(){return"function"==typeof requestAnimationFrame&&!/iP(ad|hone|od).*OS\s7.*/.test(navigator.userAgent)}function o(n,e){for(var t=0;t<n.length;t++)e(n[t])}function r(n,e){return n.length?void o(n,e):void e(n)}function a(){function n(n){n.style.display="block"}o(d,function(e){e.image&&n(e.image)})}function c(){u(),window.addEventListener("resize",function(){return u()})}function f(n){return n.getBoundingClientRect().height}function u(){var n=window.innerHeight;d.forEach(function(e){if(e.image){var t=f(e.parent),i=e.parent.offsetTop;e.distanceToVisible=i-n,e.height=t,e.offset=e.parent.offsetTop,e.parallaxSpace=f(e.image)-t,e.scrollSpace=t+n}})}function s(n,e){var t=e>=n.distanceToVisible,i=e>=n.height+n.offset;if(t&&!i){var o=e-n.distanceToVisible,r=o-n.scrollSpace/2,a=n.parallaxSpace/n.scrollSpace,c=parseFloat((r*a).toFixed(1));n.image.style[g]="translateY("+c*-1+"px)"}}function l(){requestAnimationFrame(function(){var n=window.pageYOffset;d.forEach(function(e){return s(e,n)}),l()})}function p(){l()}if(!n||!e)return!1;var d=t(n),g="webkitTransform"in document.body.style?"webkitTransform":"transform";return!!d&&(a(),i()&&(c(),p()),{recalculate:function(){u()}})};"object"==typeof module?module.exports=prlx:n.prlx=prlx}(this);<|endoftext|>


<commit_before>'use strict';

angular.module('app.services').factory('OrganizationServices', ['$http', '$window', function ($http, $window) {

    var organization = {};

    var baseRemoteUrl = "http://minitrelloapiajdm.apphb.com";
    var baseLocalUrl = "http://localhost:1416";
    var baseUrl = baseRemoteUrl;

    organization.getOrganizationsForLoggedUser = function () {
        return $http.get(baseUrl + '/organizations/' + $window.sessionStorage.token);
    };

    organization.createNewOrganizationForLoggedUser = function (model) {
        console.log(baseUrl + '/organizations/create/' + $window.sessionStorage.token);
        console.log("model:" + model);
        return $http.post(baseUrl + '/organizations/create/' + $window.sessionStorage.token, model);
    };

    organization.archiveOrganizationForLoggedUser = function (idOrganization) {
        return $http.delete(baseUrl + '/organization/delete/' + idOrganization + '/' + $window.sessionStorage.token);
    };
    return organization;

}]);
<commit_msg>deleteOrga<commit_after>'use strict';

angular.module('app.services').factory('OrganizationServices', ['$http', '$window', function ($http, $window) {

    var organization = {};

    var baseRemoteUrl = "http://minitrelloapiajdm.apphb.com";
    var baseLocalUrl = "http://localhost:1416";
    var baseUrl = baseRemoteUrl;

    organization.getOrganizationsForLoggedUser = function () {
        return $http.get(baseUrl + '/organizations/' + $window.sessionStorage.token);
    };

    organization.createNewOrganizationForLoggedUser = function (model) {
        console.log(baseUrl + '/organizations/create/' + $window.sessionStorage.token);
        console.log("model:" + model);
        return $http.post(baseUrl + '/organizations/create/' + $window.sessionStorage.token, model);
    };

    organization.archiveOrganizationForLoggedUser = function (idOrganization) {
        console.log("ParamOrgaDele");
        console.log(idOrganization);
        return $http.delete(baseUrl + '/organization/delete/' + idOrganization + '/' + $window.sessionStorage.token);
    };
    return organization;

}]);
<|endoftext|>


<commit_before>import { combineResolvers } from 'graphql-resolvers';
import { isAdmin, isAuthor } from './authorization';
import { isRobotViewing } from '../../../util/crawler';
import nanoid from 'nanoid';
import Sequelize from 'sequelize';
import socialize from '../../../api/social_butterfly/socialize';

const ATTRIBUTES_NAVIGATION = [
  'username',
  'section',
  'album',
  'name',
  'title',
  'thumb',
  'hidden',
  'template',
  'style',
  'code',
];

const Content = {
  Query: {
    allContent: combineResolvers(isAdmin, async (parent, args, { models }) => {
      return await models.Content.findAll();
    }),

    async fetchContent(parent, { username, name }, { currentUser, hostname, models, req }) {
      if (!username) {
        if (hostname) {
          const hostnameUserData = await models.User.findOne({ attributes: ['username'], where: { hostname } });
          if (hostnameUserData) {
            username = hostnameUserData.username;
            name = name || 'home';
          }
        }
        if (!username) {
          username = (await models.User.findOne({ attributes: ['username'], where: { id: 1 } })).username;
          name = name || 'main';
        }
      }

      const content = await models.Content.findOne({ where: { username, name } });

      // Inherit style, code, template from the album.
      if (content?.album && content.album !== 'main') {
        const albumContent = await models.Content.findOne({
          where: { username, section: content.section, album: 'main', name: content.album },
        });
        if (albumContent.style) {
          content.style = albumContent.style + content.style;
        }
        if (albumContent.code) {
          content.code = albumContent.code + content.code;
        }
      }

      // Inherit style, code, template from section.
      if (content && content?.section !== 'main') {
        const sectionContent = await models.Content.findOne({
          where: { username, section: 'main', name: content.section },
        });
        if (sectionContent.style) {
          content.style = sectionContent.style + content.style;
        }
        if (sectionContent.code) {
          content.code = sectionContent.code + content.code;
        }
        if (!content.template && content.album === 'main') {
          content.template = sectionContent.template;
        }
      }
      if (content && !content.template && content.album === 'main') {
        // This is when we're visiting a url of the form: /profile/section/album
        const parentContent = await models.Content.findOne({
          where: { username, section: 'main', name: content.section },
        });
        content.template = parentContent.template;
      }

      // Update count
      // lack of 'req' is a dumb check for !fetchContentHead :-/
      const isOwnerViewing = currentUser?.model?.username === username;
      if (req && content && !isOwnerViewing) {
        const attributes = isRobotViewing(req)
          ? { count_robot: content.count_robot + 1 }
          : { count: content.count + 1 };
        await models.Content.update(attributes, {
          where: {
            username,
            name: content.name,
          },
        });
      }

      return decorateWithRefreshFlag(content);
    },

    // XXX(mime): see HTMLHead.js :(
    async fetchContentHead(parent, { username, name }, { hostname, models }) {
      return await Content.Query.fetchContent(parent, { username, name }, { hostname, models });
    },

    async fetchContentNeighbors(parent, { username, section, album, name }, { currentUser, models }) {
      const ATTRIBUTES_NAVIGATION_WITH_VIEW = ATTRIBUTES_NAVIGATION.concat(['view']);

      if (!username) {
        username = (await models.User.findOne({ attributes: ['username'], where: { id: 1 } })).username;
      }
      name = name || 'main';

      const sectionContent = await models.Content.findOne({
        where: { username, section: 'main', name: section === 'main' ? name : section },
      });
      const order = [['order'], getSQLSortType(sectionContent.sort_type)];

      const isOwnerViewing = currentUser?.model?.username === username;

      const constraints = {
        redirect: false,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      const collectionConstraints = {
        username,
        section,
        album,
      };
      const collection = await models.Content.findAll({
        attributes: ATTRIBUTES_NAVIGATION_WITH_VIEW,
        where: Object.assign({}, constraints, collectionConstraints),
        order,
      });

      const contentIndex = collection.findIndex(i => i.name === name);

      const collectionItemConstraints = {
        username,
        section: !album ? 'main' : section,
        album: album ? 'main' : '',
        name: album ? album : section,
      };
      const collectionItem = await models.Content.findOne({
        attributes: ATTRIBUTES_NAVIGATION_WITH_VIEW,
        where: collectionItemConstraints,
      });

      return {
        first: decorateWithRefreshFlag(collection[collection.length - 1]),
        prev: decoratePrefetchImages(decorateWithRefreshFlag(collection[contentIndex + 1])),
        top: decorateWithRefreshFlag(collectionItem),
        next: decoratePrefetchImages(decorateWithRefreshFlag(collection[contentIndex - 1])),
        last: decorateWithRefreshFlag(collection[0]),
      };
    },

    async fetchCollection(parent, { username, section, album, name }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;

      const sectionContent = await models.Content.findOne({
        where: { username, section: !album ? 'main' : section, name },
      });
      const order = [['order'], getSQLSortType(sectionContent.sort_type)];

      const constraints = {
        redirect: false,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      let collection = [];
      // Check first to see if this is an album and grab items within it.
      if (section !== 'main') {
        const contentConstraints = { username, section, album: name };
        collection = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, contentConstraints),
          order,
        });
      }

      // If we couldn't find an album then, look for the section instead and find the albums within it.
      if (!collection.length) {
        const contentConstraints = { username, section: name, album: 'main' };
        collection = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, contentConstraints),
          order,
        });

        const albumConstraints = Object.assign({}, constraints, contentConstraints);
        for (const content of collection) {
          albumConstraints['album'] = content.name;
          const albumFirstContent = await models.Content.findOne({
            attributes: ['thumb'],
            where: Object.assign({}, constraints, albumConstraints),
            order,
          });
          if (albumFirstContent) {
            content.thumb = albumFirstContent.thumb;
          }
        }

        // Then, it combines with top-level items in the section (not in an album).
        const topLevelContentConstraints = { username, section: name, album: '' };
        const topLevelItems = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, topLevelContentConstraints),
          order,
        });
        collection = collection.concat(topLevelItems);
      }

      // This is if the top-level user page is an album. (I'm pretty sure at least :-/)
      if (!collection.length && section !== 'main') {
        const contentConstraints = { username, section };
        collection = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, contentConstraints),
          order,
        });
      }

      return decorateArrayWithRefreshFlag(collection);
    },

    async fetchCollectionPaginated(parent, { username, section, name, offset }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;
      const limit = 20;

      const constraints = {
        redirect: false,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      const notEqualToMain = { [Sequelize.Op.ne]: 'main' };
      const contentConstraints = {
        username,
        section: section !== 'main' ? section : name !== 'home' ? name : notEqualToMain,
        album: section !== 'main' ? name : notEqualToMain,
      };
      return await models.Content.findAll({
        where: Object.assign({}, constraints, contentConstraints),
        order: [['order'], ['createdAt', 'DESC']],
        limit,
        offset: offset * limit,
      });
    },

    async fetchFeed(parent, { username }, { models }) {
      const notEqualToMain = { [Sequelize.Op.ne]: 'main' };
      const contentConstraints = {
        username,
        section: notEqualToMain,
        album: notEqualToMain,
        hidden: false,
        redirect: false,
      };
      return await models.Content.findAll({
        where: contentConstraints,
        order: [['createdAt', 'DESC']],
        limit: 50,
      });
    },

    async fetchCollectionLatest(parent, { username, section, name }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;

      const constraints = {
        redirect: false,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      const contentConstraints = { username, section: name };
      return await models.Content.findOne({
        where: Object.assign({}, constraints, contentConstraints),
        order: [['order'], ['createdAt', 'DESC']],
      });
    },

    async fetchSiteMap(parent, { username }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;
      const constraints = {};
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      const contentConstraints = {
        username,
        section: 'main',
        name: { [Sequelize.Op.notIn]: ['main', 'home', 'comments'] },
        redirect: 0,
      };

      const sections = await models.Content.findAll({
        attributes: ATTRIBUTES_NAVIGATION,
        where: Object.assign({}, constraints, contentConstraints),
        order: [['order'], ['createdAt']],
      });

      let siteMap = [];
      for (const section of sections) {
        const albumConstraints = {
          username,
          section: section.name,
          album: 'main',
        };

        const albums = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, albumConstraints),
          order: [['order'], ['createdAt']],
        });

        siteMap.push(section);
        if (albums.length) {
          siteMap = siteMap.concat(albums);
        }
      }

      return decorateArrayWithRefreshFlag(siteMap);
    },

    async searchContent(parent, { username, query }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;

      const constraints = {
        redirect: false,
        username,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      let collection = await models.Content.findAll({
        attributes: ATTRIBUTES_NAVIGATION.concat(['view']),
        where: [constraints, Sequelize.literal('match (title, view) against (:query)')],
        replacements: {
          query,
        },
      });

      for (const item of collection) {
        const HTML_REGEX = /<[^>]+>/g;
        item.preview = ellipsize(item.view.replace(HTML_REGEX, '').trim(), 130);
      }

      return decorateArrayWithRefreshFlag(collection);
    },
  },

  Mutation: {
    saveContent: combineResolvers(
      isAuthor,
      async (parent, { name, hidden, style, code, content }, { currentUser, models, req }) => {
        const username = currentUser.model.username;
        await models.Content.update(
          {
            hidden,
            style,
            code,
            content,
          },
          {
            where: {
              username,
              name,
            },
          }
        );

        const updatedContent = await models.Content.findOne({ where: { username, name } });

        if (!hidden) {
          await socialize(req, currentUser.model, updatedContent);
        }

        return { username: currentUser.model.username, name, style, code, content };
      }
    ),

    postContent: combineResolvers(
      isAuthor,
      async (
        parent,
        { section, album, name, title, hidden, thumb, style, code, content },
        { currentUser, models, req }
      ) => {
        name = (name || 'untitled') + '-' + nanoid(10);
        name = name.replace(/[^A-Za-z0-9-]/, '-');

        const createdContent = await models.Content.create({
          username: currentUser.model.username,
          section,
          album,
          name,
          title,
          thumb,
          hidden,
          style,
          code,
          content,
        });

        if (!hidden) {
          await socialize(req, currentUser.model, createdContent);
        }

        return {
          username: currentUser.model.username,
          section,
          album,
          name,
          title,
          hidden,
          thumb,
          style,
          code,
          content,
        };
      }
    ),
  },
};

export default Content;

function getSQLSortType(sortType) {
  if (sortType === 'oldest') {
    return ['createdAt'];
  }
  if (sortType === 'alphabetical') {
    return ['title'];
  }
  return ['createdAt', 'DESC'];
}

function decorateArrayWithRefreshFlag(list) {
  for (const item of list) {
    decorateWithRefreshFlag(item);
  }

  return list;
}

function decorateWithRefreshFlag(item) {
  if (item) {
    item.forceRefresh = !!(item.style || item.code);
  }

  return item;
}

function decoratePrefetchImages(item) {
  if (item) {
    item.prefetchImages = (item.view.match(/src=['"][^'"]+['"]/g) || []).map(i => i.slice(5, -1));
  }

  return item;
}

function ellipsize(str, len) {
  if (str.length <= len) {
    return str;
  }

  return str.slice(0, len) + '…';
}
<commit_msg>content: save html view so that we can render RSS feed properly<commit_after>import { combineResolvers } from 'graphql-resolvers';
import { convertFromRaw } from 'draft-js';
import { convertToHTML } from 'draft-convert';
import draftJSExtendPlugins from '../../../../client/editor/plugins';
import { isAdmin, isAuthor } from './authorization';
import { isRobotViewing } from '../../../util/crawler';
import nanoid from 'nanoid';
import Sequelize from 'sequelize';
import socialize from '../../../api/social_butterfly/socialize';

const ATTRIBUTES_NAVIGATION = [
  'username',
  'section',
  'album',
  'name',
  'title',
  'thumb',
  'hidden',
  'template',
  'style',
  'code',
];

const Content = {
  Query: {
    allContent: combineResolvers(isAdmin, async (parent, args, { models }) => {
      return await models.Content.findAll();
    }),

    async fetchContent(parent, { username, name }, { currentUser, hostname, models, req }) {
      if (!username) {
        if (hostname) {
          const hostnameUserData = await models.User.findOne({ attributes: ['username'], where: { hostname } });
          if (hostnameUserData) {
            username = hostnameUserData.username;
            name = name || 'home';
          }
        }
        if (!username) {
          username = (await models.User.findOne({ attributes: ['username'], where: { id: 1 } })).username;
          name = name || 'main';
        }
      }

      const content = await models.Content.findOne({ where: { username, name } });

      // Inherit style, code, template from the album.
      if (content?.album && content.album !== 'main') {
        const albumContent = await models.Content.findOne({
          where: { username, section: content.section, album: 'main', name: content.album },
        });
        if (albumContent.style) {
          content.style = albumContent.style + content.style;
        }
        if (albumContent.code) {
          content.code = albumContent.code + content.code;
        }
      }

      // Inherit style, code, template from section.
      if (content && content?.section !== 'main') {
        const sectionContent = await models.Content.findOne({
          where: { username, section: 'main', name: content.section },
        });
        if (sectionContent.style) {
          content.style = sectionContent.style + content.style;
        }
        if (sectionContent.code) {
          content.code = sectionContent.code + content.code;
        }
        if (!content.template && content.album === 'main') {
          content.template = sectionContent.template;
        }
      }
      if (content && !content.template && content.album === 'main') {
        // This is when we're visiting a url of the form: /profile/section/album
        const parentContent = await models.Content.findOne({
          where: { username, section: 'main', name: content.section },
        });
        content.template = parentContent.template;
      }

      // Update count
      // lack of 'req' is a dumb check for !fetchContentHead :-/
      const isOwnerViewing = currentUser?.model?.username === username;
      if (req && content && !isOwnerViewing) {
        const attributes = isRobotViewing(req)
          ? { count_robot: content.count_robot + 1 }
          : { count: content.count + 1 };
        await models.Content.update(attributes, {
          where: {
            username,
            name: content.name,
          },
        });
      }

      return decorateWithRefreshFlag(content);
    },

    // XXX(mime): see HTMLHead.js :(
    async fetchContentHead(parent, { username, name }, { hostname, models }) {
      return await Content.Query.fetchContent(parent, { username, name }, { hostname, models });
    },

    async fetchContentNeighbors(parent, { username, section, album, name }, { currentUser, models }) {
      const ATTRIBUTES_NAVIGATION_WITH_VIEW = ATTRIBUTES_NAVIGATION.concat(['view']);

      if (!username) {
        username = (await models.User.findOne({ attributes: ['username'], where: { id: 1 } })).username;
      }
      name = name || 'main';

      const sectionContent = await models.Content.findOne({
        where: { username, section: 'main', name: section === 'main' ? name : section },
      });
      const order = [['order'], getSQLSortType(sectionContent.sort_type)];

      const isOwnerViewing = currentUser?.model?.username === username;

      const constraints = {
        redirect: false,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      const collectionConstraints = {
        username,
        section,
        album,
      };
      const collection = await models.Content.findAll({
        attributes: ATTRIBUTES_NAVIGATION_WITH_VIEW,
        where: Object.assign({}, constraints, collectionConstraints),
        order,
      });

      const contentIndex = collection.findIndex(i => i.name === name);

      const collectionItemConstraints = {
        username,
        section: !album ? 'main' : section,
        album: album ? 'main' : '',
        name: album ? album : section,
      };
      const collectionItem = await models.Content.findOne({
        attributes: ATTRIBUTES_NAVIGATION_WITH_VIEW,
        where: collectionItemConstraints,
      });

      return {
        first: decorateWithRefreshFlag(collection[collection.length - 1]),
        prev: decoratePrefetchImages(decorateWithRefreshFlag(collection[contentIndex + 1])),
        top: decorateWithRefreshFlag(collectionItem),
        next: decoratePrefetchImages(decorateWithRefreshFlag(collection[contentIndex - 1])),
        last: decorateWithRefreshFlag(collection[0]),
      };
    },

    async fetchCollection(parent, { username, section, album, name }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;

      const sectionContent = await models.Content.findOne({
        where: { username, section: !album ? 'main' : section, name },
      });
      const order = [['order'], getSQLSortType(sectionContent.sort_type)];

      const constraints = {
        redirect: false,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      let collection = [];
      // Check first to see if this is an album and grab items within it.
      if (section !== 'main') {
        const contentConstraints = { username, section, album: name };
        collection = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, contentConstraints),
          order,
        });
      }

      // If we couldn't find an album then, look for the section instead and find the albums within it.
      if (!collection.length) {
        const contentConstraints = { username, section: name, album: 'main' };
        collection = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, contentConstraints),
          order,
        });

        const albumConstraints = Object.assign({}, constraints, contentConstraints);
        for (const content of collection) {
          albumConstraints['album'] = content.name;
          const albumFirstContent = await models.Content.findOne({
            attributes: ['thumb'],
            where: Object.assign({}, constraints, albumConstraints),
            order,
          });
          if (albumFirstContent) {
            content.thumb = albumFirstContent.thumb;
          }
        }

        // Then, it combines with top-level items in the section (not in an album).
        const topLevelContentConstraints = { username, section: name, album: '' };
        const topLevelItems = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, topLevelContentConstraints),
          order,
        });
        collection = collection.concat(topLevelItems);
      }

      // This is if the top-level user page is an album. (I'm pretty sure at least :-/)
      if (!collection.length && section !== 'main') {
        const contentConstraints = { username, section };
        collection = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, contentConstraints),
          order,
        });
      }

      return decorateArrayWithRefreshFlag(collection);
    },

    async fetchCollectionPaginated(parent, { username, section, name, offset }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;
      const limit = 20;

      const constraints = {
        redirect: false,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      const notEqualToMain = { [Sequelize.Op.ne]: 'main' };
      const contentConstraints = {
        username,
        section: section !== 'main' ? section : name !== 'home' ? name : notEqualToMain,
        album: section !== 'main' ? name : notEqualToMain,
      };
      return await models.Content.findAll({
        where: Object.assign({}, constraints, contentConstraints),
        order: [['order'], ['createdAt', 'DESC']],
        limit,
        offset: offset * limit,
      });
    },

    async fetchFeed(parent, { username }, { models }) {
      const notEqualToMain = { [Sequelize.Op.ne]: 'main' };
      const contentConstraints = {
        username,
        section: notEqualToMain,
        album: notEqualToMain,
        hidden: false,
        redirect: false,
      };
      return await models.Content.findAll({
        where: contentConstraints,
        order: [['createdAt', 'DESC']],
        limit: 50,
      });
    },

    async fetchCollectionLatest(parent, { username, section, name }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;

      const constraints = {
        redirect: false,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      const contentConstraints = { username, section: name };
      return await models.Content.findOne({
        where: Object.assign({}, constraints, contentConstraints),
        order: [['order'], ['createdAt', 'DESC']],
      });
    },

    async fetchSiteMap(parent, { username }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;
      const constraints = {};
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      const contentConstraints = {
        username,
        section: 'main',
        name: { [Sequelize.Op.notIn]: ['main', 'home', 'comments'] },
        redirect: 0,
      };

      const sections = await models.Content.findAll({
        attributes: ATTRIBUTES_NAVIGATION,
        where: Object.assign({}, constraints, contentConstraints),
        order: [['order'], ['createdAt']],
      });

      let siteMap = [];
      for (const section of sections) {
        const albumConstraints = {
          username,
          section: section.name,
          album: 'main',
        };

        const albums = await models.Content.findAll({
          attributes: ATTRIBUTES_NAVIGATION,
          where: Object.assign({}, constraints, albumConstraints),
          order: [['order'], ['createdAt']],
        });

        siteMap.push(section);
        if (albums.length) {
          siteMap = siteMap.concat(albums);
        }
      }

      return decorateArrayWithRefreshFlag(siteMap);
    },

    async searchContent(parent, { username, query }, { currentUser, models }) {
      const isOwnerViewing = currentUser?.model?.username === username;

      const constraints = {
        redirect: false,
        username,
      };
      if (!isOwnerViewing) {
        constraints['hidden'] = false;
      }

      let collection = await models.Content.findAll({
        attributes: ATTRIBUTES_NAVIGATION.concat(['view']),
        where: [constraints, Sequelize.literal('match (title, view) against (:query)')],
        replacements: {
          query,
        },
      });

      for (const item of collection) {
        const HTML_REGEX = /<[^>]+>/g;
        item.preview = ellipsize(item.view.replace(HTML_REGEX, '').trim(), 130);
      }

      return decorateArrayWithRefreshFlag(collection);
    },
  },

  Mutation: {
    saveContent: combineResolvers(
      isAuthor,
      async (parent, { name, hidden, style, code, content }, { currentUser, models, req }) => {
        const username = currentUser.model.username;
        await models.Content.update(
          {
            hidden,
            style,
            code,
            content,
            view: toHTML(content),
          },
          {
            where: {
              username,
              name,
            },
          }
        );

        const updatedContent = await models.Content.findOne({ where: { username, name } });

        if (!hidden) {
          await socialize(req, currentUser.model, updatedContent);
        }

        return { username: currentUser.model.username, name, hidden, style, code, content };
      }
    ),

    postContent: combineResolvers(
      isAuthor,
      async (
        parent,
        { section, album, name, title, hidden, thumb, style, code, content },
        { currentUser, models, req }
      ) => {
        name = (name || 'untitled') + '-' + nanoid(10);
        name = name.replace(/[^A-Za-z0-9-]/, '-');

        const createdContent = await models.Content.create({
          username: currentUser.model.username,
          section,
          album,
          name,
          title,
          thumb,
          hidden,
          style,
          code,
          content,
          view: toHTML(content),
        });

        if (!hidden) {
          await socialize(req, currentUser.model, createdContent);
        }

        return {
          username: currentUser.model.username,
          section,
          album,
          name,
          title,
          hidden,
          thumb,
          style,
          code,
          content,
        };
      }
    ),
  },
};

export default Content;

function toHTML(content) {
  return draftJSExtendPlugins(convertToHTML)(convertFromRaw(JSON.parse(content)));
}

function getSQLSortType(sortType) {
  if (sortType === 'oldest') {
    return ['createdAt'];
  }
  if (sortType === 'alphabetical') {
    return ['title'];
  }
  return ['createdAt', 'DESC'];
}

function decorateArrayWithRefreshFlag(list) {
  for (const item of list) {
    decorateWithRefreshFlag(item);
  }

  return list;
}

function decorateWithRefreshFlag(item) {
  if (item) {
    item.forceRefresh = !!(item.style || item.code);
  }

  return item;
}

function decoratePrefetchImages(item) {
  if (item) {
    item.prefetchImages = (item.view.match(/src=['"][^'"]+['"]/g) || []).map(i => i.slice(5, -1));
  }

  return item;
}

function ellipsize(str, len) {
  if (str.length <= len) {
    return str;
  }

  return str.slice(0, len) + '…';
}
<|endoftext|>


<commit_before>Meteor.publish('allUsers', function() {
	return Meteor.users.find({}, {
		fields: {
			'profile.firstName': 1,
			'profile.lastName': 1,
			'profile.points': 1
		}
	});
});
<commit_msg>Update userPublication<commit_after>Meteor.publish('allUsers', function() {
	return Meteor.users.find({}, {
		fields: {
			'profile.firstName': 1,
			'profile.lastName': 1,
			'profile.points': 1,
			'_id': 1
		}
	});
});
<|endoftext|>


<commit_before>// Service that propagates business process flow changes

'use strict';

var aFrom           = require('es5-ext/array/from')
  , ensureIterable  = require('es5-ext/iterable/validate-object')
  , ensureCallable  = require('es5-ext/object/valid-callable')
  , Set             = require('es6-set')
  , ensureType      = require('dbjs/valid-dbjs-type')
  , debug           = require('debug-ext')('business-process-flow')
  , resolveStepPath = require('../../utils/resolve-processing-step-full-path')
  , setupTriggers   = require('../_setup-triggers');

module.exports = function (BusinessProcessType, stepShortPaths/*, options*/) {
	var businessProcesses = ensureType(BusinessProcessType).instances
		.filterByKey('isFromEregistrations', true).filterByKey('isDemo', false);
	var options = Object(arguments[2]), customStepReturnHandler, onStepRedelegate, onStepStatus
	  , onUserProcessingEnd;
	if (options.customStepReturnHandler != null) {
		customStepReturnHandler = ensureCallable(options.customStepReturnHandler);
	}
	if (options.onStepRedelegate != null) onStepRedelegate = ensureCallable(options.onStepRedelegate);
	if (options.onStepStatus != null) onStepStatus = ensureCallable(options.onStepStatus);
	if (options.onUserProcessingEnd != null) {
		onUserProcessingEnd = ensureCallable(options.onUserProcessingEnd);
	}

	var stepPaths = aFrom(ensureIterable(stepShortPaths)).map(function (shortPath) {
		return 'processingSteps/map/' + resolveStepPath(shortPath);
	});

	// Business process: isSubmitted
	setupTriggers({
		trigger: businessProcesses.filterByKey('isSubmittedReady', true)
			.filterByKey('isSubmitted', false)
	}, function (businessProcess) {
		debug('%s submitted', businessProcess.__id__);
		businessProcess.isSubmitted = true;
	});

	// Below two triggers are needed just for migration phase
	// After that there's no scenario when they may trigger, therefore should be removed
	setupTriggers({
		trigger: businessProcesses.filterByKey('isSentBack', true)
			.filterByKey('isSubmitted', false)
	}, function (businessProcess) {
		debug('%s submitted (through sent back)', businessProcess.__id__);
		businessProcess.isSubmitted = true;
	});
	setupTriggers({
		trigger: businessProcesses.filterByKey('isUserProcessing', true)
			.filterByKey('isSubmitted', false)
	}, function (businessProcess) {
		debug('%s submitted (through user processing)', businessProcess.__id__);
		businessProcess.isSubmitted = true;
	});

	var businessProcessesSubmitted = businessProcesses.filterByKey('isSubmitted', true);

	// Business process: sentBack finalization
	setupTriggers({
		trigger: businessProcessesSubmitted.filterByKey('isSubmittedReady', true)
			.filterByKey('isSentBack', true)
	}, function (businessProcess) {
		debug('%s finalize sentBack', businessProcess.__id__);
		businessProcess.delete('isSentBack');
	});

	// Business process: isUserProcessing initialization
	setupTriggers({
		trigger: businessProcessesSubmitted.filterByKey('isUserProcessingReady', true)
			.filterByKey('isUserProcessing', false)
	}, function (businessProcess) {
		debug('%s initialize user processing', businessProcess.__id__);
		businessProcess.submissionForms.delete('isAffidavitSigned');
		businessProcess.isUserProcessing = true;
	});

	// Business process: isUserProcessing finalization
	setupTriggers({
		trigger: businessProcessesSubmitted.filterByKey('isSubmittedReady', true)
			.filterByKey('isUserProcessing', true)
	}, function (businessProcess) {
		debug('%s finalize user processing', businessProcess.__id__);
		if (onUserProcessingEnd) onUserProcessingEnd(businessProcess);
		businessProcess.delete('isUserProcessing');
	});

	// Processing steps:
	stepPaths.forEach(function (stepPath, index) {
		// status
		setupTriggers({
			trigger: businessProcessesSubmitted.filterByKeyPath(stepPath + '/status', function (value) {
				return value && (value !== 'pending');
			})
		}, function (businessProcess) {
			var step = businessProcess.getBySKeyPath(stepPath);
			if (step.getOwnDescriptor('status').hasOwnProperty('_value_')) return; // Already shadowed
			debug('%s %s step %s', businessProcess.__id__,
				step.shortPath, step.status);
			if (onStepStatus) onStepStatus(step);
			step.set('status', step.status);

			if (step.status === 'sentBack') {
				if (businessProcess.isSentBack) return;

				// Sent back initialization
				debug('%s sent back by %s step', businessProcess.__id__, step.shortPath);
				businessProcess.submissionForms.delete('isAffidavitSigned');
				businessProcess.isSentBack = true;

			} else if (step.status === 'redelegated') {

				// Redelegation initialization
				debug('%s redelegated to %s from %s', businessProcess.__id__,
					step.redelegatedTo.shortPath, step.shortPath);
				if (onStepRedelegate) onStepRedelegate(step);
				step.redelegatedTo.delete('revisionOfficialStatus');
				step.redelegatedTo.delete('officialStatus');
				step.redelegatedTo.delete('status');
				step.redelegatedTo.delete('isSatisfied');

			} else if (step.status === 'rejected') {

				// Business process rejection
				debug('%s rejected at %s', businessProcess.__id__, step.shortPath);
				businessProcess.isRejected = true;
			}
		});

		// isSatisfied
		setupTriggers({
			trigger: businessProcessesSubmitted.filterByKeyPath(stepPath + '/isSatisfiedReady', true)
				.filterByKeyPath(stepPath + '/isSatisfied', false)
		}, function (businessProcess) {
			var step = businessProcess.getBySKeyPath(stepPath);
			debug('%s %s step satisfied', businessProcess.__id__,
				step.shortPath);
			step.isSatisfied = true;
		});

		// Valid returns
		var returnStatuses = new Set(['sentBack', 'redelegated']);
		setupTriggers({
			preTrigger: businessProcessesSubmitted
				.filterByKeyPath(stepPath + '/isPreviousStepsSatisfiedDeep', false),
			trigger: businessProcessesSubmitted
				.filterByKeyPath(stepPath + '/isPreviousStepsSatisfiedDeep', true)
		}, function (businessProcess) {
			var step = businessProcess.getBySKeyPath(stepPath);
			if (!step.isApplicable) return;
			if (!step.status || (step.status === 'pending')) return;
			if (customStepReturnHandler) customStepReturnHandler(step);
			if (step.statusComputed && (step.statusComputed !== 'pending') &&
					!returnStatuses.has(step.status)) {
				return;
			}
			debug('%s %s step reset from %s to pending state', businessProcess.__id__,
				step.shortPath, step.status);
			step.delete('revisionOfficialStatus');
			step.delete('officialStatus');
			step.delete('status');
			step.delete('isSatisfied');
		});
	});
};
<commit_msg>Shadow isReady when status is resolved<commit_after>// Service that propagates business process flow changes

'use strict';

var aFrom           = require('es5-ext/array/from')
  , ensureIterable  = require('es5-ext/iterable/validate-object')
  , ensureCallable  = require('es5-ext/object/valid-callable')
  , Set             = require('es6-set')
  , ensureType      = require('dbjs/valid-dbjs-type')
  , debug           = require('debug-ext')('business-process-flow')
  , resolveStepPath = require('../../utils/resolve-processing-step-full-path')
  , setupTriggers   = require('../_setup-triggers');

module.exports = function (BusinessProcessType, stepShortPaths/*, options*/) {
	var businessProcesses = ensureType(BusinessProcessType).instances
		.filterByKey('isFromEregistrations', true).filterByKey('isDemo', false);
	var options = Object(arguments[2]), customStepReturnHandler, onStepRedelegate, onStepStatus
	  , onUserProcessingEnd;
	if (options.customStepReturnHandler != null) {
		customStepReturnHandler = ensureCallable(options.customStepReturnHandler);
	}
	if (options.onStepRedelegate != null) onStepRedelegate = ensureCallable(options.onStepRedelegate);
	if (options.onStepStatus != null) onStepStatus = ensureCallable(options.onStepStatus);
	if (options.onUserProcessingEnd != null) {
		onUserProcessingEnd = ensureCallable(options.onUserProcessingEnd);
	}

	var stepPaths = aFrom(ensureIterable(stepShortPaths)).map(function (shortPath) {
		return 'processingSteps/map/' + resolveStepPath(shortPath);
	});

	// Business process: isSubmitted
	setupTriggers({
		trigger: businessProcesses.filterByKey('isSubmittedReady', true)
			.filterByKey('isSubmitted', false)
	}, function (businessProcess) {
		debug('%s submitted', businessProcess.__id__);
		businessProcess.isSubmitted = true;
	});

	// Below two triggers are needed just for migration phase
	// After that there's no scenario when they may trigger, therefore should be removed
	setupTriggers({
		trigger: businessProcesses.filterByKey('isSentBack', true)
			.filterByKey('isSubmitted', false)
	}, function (businessProcess) {
		debug('%s submitted (through sent back)', businessProcess.__id__);
		businessProcess.isSubmitted = true;
	});
	setupTriggers({
		trigger: businessProcesses.filterByKey('isUserProcessing', true)
			.filterByKey('isSubmitted', false)
	}, function (businessProcess) {
		debug('%s submitted (through user processing)', businessProcess.__id__);
		businessProcess.isSubmitted = true;
	});

	var businessProcessesSubmitted = businessProcesses.filterByKey('isSubmitted', true);

	// Business process: sentBack finalization
	setupTriggers({
		trigger: businessProcessesSubmitted.filterByKey('isSubmittedReady', true)
			.filterByKey('isSentBack', true)
	}, function (businessProcess) {
		debug('%s finalize sentBack', businessProcess.__id__);
		businessProcess.delete('isSentBack');
	});

	// Business process: isUserProcessing initialization
	setupTriggers({
		trigger: businessProcessesSubmitted.filterByKey('isUserProcessingReady', true)
			.filterByKey('isUserProcessing', false)
	}, function (businessProcess) {
		debug('%s initialize user processing', businessProcess.__id__);
		businessProcess.submissionForms.delete('isAffidavitSigned');
		businessProcess.isUserProcessing = true;
	});

	// Business process: isUserProcessing finalization
	setupTriggers({
		trigger: businessProcessesSubmitted.filterByKey('isSubmittedReady', true)
			.filterByKey('isUserProcessing', true)
	}, function (businessProcess) {
		debug('%s finalize user processing', businessProcess.__id__);
		if (onUserProcessingEnd) onUserProcessingEnd(businessProcess);
		businessProcess.delete('isUserProcessing');
	});

	// Processing steps:
	stepPaths.forEach(function (stepPath, index) {
		// status
		setupTriggers({
			trigger: businessProcessesSubmitted.filterByKeyPath(stepPath + '/status', function (value) {
				return value && (value !== 'pending');
			})
		}, function (businessProcess) {
			var step = businessProcess.getBySKeyPath(stepPath);
			if (step.getOwnDescriptor('status').hasOwnProperty('_value_')) return; // Already shadowed
			debug('%s %s step %s', businessProcess.__id__,
				step.shortPath, step.status);
			if (onStepStatus) onStepStatus(step);
			step.set('isReady', true);
			step.set('status', step.status);

			if (step.status === 'sentBack') {
				if (businessProcess.isSentBack) return;

				// Sent back initialization
				debug('%s sent back by %s step', businessProcess.__id__, step.shortPath);
				businessProcess.submissionForms.delete('isAffidavitSigned');
				businessProcess.isSentBack = true;

			} else if (step.status === 'redelegated') {

				// Redelegation initialization
				debug('%s redelegated to %s from %s', businessProcess.__id__,
					step.redelegatedTo.shortPath, step.shortPath);
				if (onStepRedelegate) onStepRedelegate(step);
				step.redelegatedTo.delete('revisionOfficialStatus');
				step.redelegatedTo.delete('officialStatus');
				step.redelegatedTo.delete('status');
				step.redelegatedTo.delete('isSatisfied');

			} else if (step.status === 'rejected') {

				// Business process rejection
				debug('%s rejected at %s', businessProcess.__id__, step.shortPath);
				businessProcess.isRejected = true;
			}
		});

		// isSatisfied
		setupTriggers({
			trigger: businessProcessesSubmitted.filterByKeyPath(stepPath + '/isSatisfiedReady', true)
				.filterByKeyPath(stepPath + '/isSatisfied', false)
		}, function (businessProcess) {
			var step = businessProcess.getBySKeyPath(stepPath);
			debug('%s %s step satisfied', businessProcess.__id__,
				step.shortPath);
			step.isSatisfied = true;
		});

		// Valid returns
		var returnStatuses = new Set(['sentBack', 'redelegated']);
		setupTriggers({
			preTrigger: businessProcessesSubmitted
				.filterByKeyPath(stepPath + '/isPreviousStepsSatisfiedDeep', false),
			trigger: businessProcessesSubmitted
				.filterByKeyPath(stepPath + '/isPreviousStepsSatisfiedDeep', true)
		}, function (businessProcess) {
			var step = businessProcess.getBySKeyPath(stepPath);
			if (!step.isApplicable) return;
			if (!step.status || (step.status === 'pending')) return;
			if (customStepReturnHandler) customStepReturnHandler(step);
			if (step.statusComputed && (step.statusComputed !== 'pending') &&
					!returnStatuses.has(step.status)) {
				return;
			}
			debug('%s %s step reset from %s to pending state', businessProcess.__id__,
				step.shortPath, step.status);
			step.delete('revisionOfficialStatus');
			step.delete('officialStatus');
			step.delete('status');
			step.delete('isSatisfied');
		});
	});
};
<|endoftext|>


<commit_before>// @flow

import type { Viewer } from '../session/viewer';
import type {
  UpdateActivityResult,
  UpdateActivityRequest,
  SetThreadUnreadStatusRequest,
  SetThreadUnreadStatusResult,
} from 'lib/types/activity-types';
import { messageTypes } from 'lib/types/message-types';
import { threadPermissions } from 'lib/types/thread-types';
import { updateTypes } from 'lib/types/update-types';

import invariant from 'invariant';
import _difference from 'lodash/fp/difference';
import _max from 'lodash/fp/max';

import { ServerError } from 'lib/utils/errors';

import { dbQuery, SQL, mergeOrConditions } from '../database/database';
import { rescindPushNotifs } from '../push/rescind';
import { createUpdates } from '../creators/update-creator';
import { deleteActivityForViewerSession } from '../deleters/activity-deleters';
import { earliestFocusedTimeConsideredCurrent } from '../shared/focused-times';
import {
  checkThread,
  checkThreads,
} from '../fetchers/thread-permission-fetchers';

async function activityUpdater(
  viewer: Viewer,
  request: UpdateActivityRequest,
): Promise<UpdateActivityResult> {
  if (!viewer.loggedIn) {
    throw new ServerError('not_logged_in');
  }

  const focusUpdatesByThreadID = new Map();
  for (const activityUpdate of request.updates) {
    const threadID = activityUpdate.threadID;
    const updatesForThreadID = focusUpdatesByThreadID.get(threadID) ?? [];
    if (!focusUpdatesByThreadID.has(threadID)) {
      focusUpdatesByThreadID.set(threadID, updatesForThreadID);
    }
    updatesForThreadID.push(activityUpdate);
  }

  const unverifiedThreadIDs: $ReadOnlySet<string> = new Set(
    request.updates.map(update => update.threadID),
  );
  const viewerMemberThreads = await checkThreads(
    viewer,
    [...unverifiedThreadIDs],
    [
      {
        check: 'is_member',
      },
      {
        check: 'permission',
        permission: threadPermissions.VISIBLE,
      },
    ],
  );

  if (viewerMemberThreads.size === 0) {
    return { unfocusedToUnread: [] };
  }

  const currentlyFocused = [];
  const unfocusedLatestMessages = new Map<string, number>();
  const latestMessages = new Map<string, number>();
  const rescindConditions = [];
  for (const threadID of viewerMemberThreads) {
    const focusUpdates = focusUpdatesByThreadID.get(threadID);
    invariant(focusUpdates, `no focusUpdate for thread ID ${threadID}`);

    const latestMessage = _max(
      focusUpdates
        .filter(
          update =>
            update.latestMessage && !update.latestMessage.startsWith('local'),
        )
        .map(update => parseInt(update.latestMessage)),
    );
    if (latestMessage) {
      latestMessages.set(threadID, latestMessage);
    }

    const focusEnded = focusUpdates.some(update => !update.focus);

    if (!focusEnded) {
      currentlyFocused.push(threadID);
      rescindConditions.push(SQL`n.thread = ${threadID}`);
    } else {
      unfocusedLatestMessages.set(threadID, latestMessage ?? 0);
      if (latestMessage) {
        rescindConditions.push(
          SQL`(n.thread = ${threadID} AND n.message <= ${latestMessage})`,
        );
      }
    }
  }

  const focusUpdatePromise = updateFocusedRows(viewer, currentlyFocused);

  const outdatedUnfocused = await checkUnfocusedLatestMessage(
    viewer,
    unfocusedLatestMessages,
  );

  const setToRead = [...currentlyFocused];
  const setToUnread = [];
  for (const [threadID] of unfocusedLatestMessages) {
    if (!outdatedUnfocused.includes(threadID)) {
      setToRead.push(threadID);
    } else {
      setToUnread.push(threadID);
    }
  }

  const time = Date.now();
  const updateDatas = [];
  const appendUpdateDatas = (
    threadIDs: $ReadOnlyArray<string>,
    unread: boolean,
  ) => {
    for (const threadID of threadIDs) {
      updateDatas.push({
        type: updateTypes.UPDATE_THREAD_READ_STATUS,
        userID: viewer.userID,
        time,
        threadID,
        unread,
      });
    }
  };

  appendUpdateDatas(setToRead, false);
  appendUpdateDatas(setToUnread, true);

  const updateLatestReadMessagePromise = updateLastReadMessage(
    viewer,
    latestMessages,
  );
  const updatesPromise = createUpdates(updateDatas, {
    viewer,
    updatesForCurrentSession: 'ignore',
  });

  await Promise.all([
    focusUpdatePromise,
    updateLatestReadMessagePromise,
    updatesPromise,
  ]);

  // We do this afterwards so the badge count is correct
  const rescindCondition = SQL`n.user = ${viewer.userID} AND `;
  rescindCondition.append(mergeOrConditions(rescindConditions));
  await rescindPushNotifs(rescindCondition);

  return { unfocusedToUnread: outdatedUnfocused };
}

async function updateFocusedRows(
  viewer: Viewer,
  threadIDs: $ReadOnlyArray<string>,
): Promise<void> {
  const time = Date.now();
  if (threadIDs.length > 0) {
    const focusedInsertRows = threadIDs.map(threadID => [
      viewer.userID,
      viewer.session,
      threadID,
      time,
    ]);
    await dbQuery(SQL`
      INSERT INTO focused (user, session, thread, time)
      VALUES ${focusedInsertRows}
      ON DUPLICATE KEY UPDATE time = VALUES(time)
    `);
  }
  await deleteActivityForViewerSession(viewer, time);
}

// To protect against a possible race condition, we reset the thread to unread
// if the latest message ID on the client at the time that focus was dropped
// is no longer the latest message ID.
// Returns the set of unfocused threads that should be set to unread on
// the client because a new message arrived since they were unfocused.
async function checkUnfocusedLatestMessage(
  viewer: Viewer,
  unfocusedLatestMessages: Map<string, number>,
): Promise<string[]> {
  if (unfocusedLatestMessages.size === 0 || !viewer.loggedIn) {
    return [];
  }

  const unfocusedThreadIDs = [...unfocusedLatestMessages.keys()];
  const focusedElsewhereThreadIDs = await checkThreadsFocused(
    viewer,
    unfocusedThreadIDs,
  );
  const unreadCandidates = _difference(unfocusedThreadIDs)(
    focusedElsewhereThreadIDs,
  );
  if (unreadCandidates.length === 0) {
    return [];
  }

  const knowOfExtractString = `$.${threadPermissions.KNOW_OF}.value`;
  const query = SQL`
    SELECT m.thread, MAX(m.id) AS latest_message
    FROM messages m
    LEFT JOIN memberships stm ON m.type = ${messageTypes.CREATE_SUB_THREAD}
      AND stm.thread = m.content AND stm.user = ${viewer.userID}
    WHERE m.thread IN (${unreadCandidates}) AND
      m.user != ${viewer.userID} AND
      (
        m.type != ${messageTypes.CREATE_SUB_THREAD} OR
        JSON_EXTRACT(stm.permissions, ${knowOfExtractString}) IS TRUE
      )
    GROUP BY m.thread
  `;
  const [result] = await dbQuery(query);

  const resetToUnread = [];
  for (const row of result) {
    const threadID = row.thread.toString();
    const serverLatestMessage = row.latest_message;
    const clientLatestMessage = unfocusedLatestMessages.get(threadID);
    invariant(
      clientLatestMessage,
      'latest message should be set for all provided threads',
    );
    if (clientLatestMessage < serverLatestMessage) {
      resetToUnread.push(threadID);
    }
  }
  return resetToUnread;
}

async function checkThreadsFocused(
  viewer: Viewer,
  threadIDs: $ReadOnlyArray<string>,
): Promise<string[]> {
  const time = earliestFocusedTimeConsideredCurrent();
  const query = SQL`
    SELECT thread
    FROM focused
    WHERE time > ${time}
      AND user = ${viewer.userID}
      AND thread IN (${threadIDs})
    GROUP BY thread
  `;
  const [result] = await dbQuery(query);

  const focusedThreadIDs = [];
  for (const row of result) {
    focusedThreadIDs.push(row.thread.toString());
  }
  return focusedThreadIDs;
}

async function updateLastReadMessage(
  viewer: Viewer,
  lastReadMessages: $ReadOnlyMap<string, number>,
) {
  if (lastReadMessages.size === 0) {
    return;
  }

  const query = SQL`
    UPDATE memberships
    SET last_read_message = GREATEST(last_read_message,
      CASE
  `;
  lastReadMessages.forEach((lastMessage, threadID) => {
    query.append(SQL`
        WHEN thread = ${threadID} THEN ${lastMessage} `);
  });
  query.append(SQL`
        ELSE last_read_message
      END)
    WHERE thread IN (${[...lastReadMessages.keys()]})
      AND user = ${viewer.userID}
  `);

  return await dbQuery(query);
}

async function setThreadUnreadStatus(
  viewer: Viewer,
  request: SetThreadUnreadStatusRequest,
): Promise<SetThreadUnreadStatusResult> {
  if (!viewer.loggedIn) {
    throw new ServerError('not_logged_in');
  }

  const isMemberAndCanViewThread = await checkThread(viewer, request.threadID, [
    {
      check: 'is_member',
    },
    {
      check: 'permission',
      permission: threadPermissions.VISIBLE,
    },
  ]);
  if (!isMemberAndCanViewThread) {
    throw new ServerError('invalid_parameters');
  }

  const resetThreadToUnread = await shouldResetThreadToUnread(viewer, request);
  if (!resetThreadToUnread) {
    const lastReadMessage = request.unread
      ? SQL`0`
      : SQL`GREATEST(m.last_read_message, ${request.latestMessage ?? 0})`;
    const update = SQL`
      UPDATE memberships m
      SET m.last_read_message =
    `;
    update.append(lastReadMessage);
    update.append(SQL`
      WHERE m.thread = ${request.threadID} AND m.user = ${viewer.userID}
    `);
    const queryPromise = dbQuery(update);

    const time = Date.now();
    const updatesPromise = createUpdates(
      [
        {
          type: updateTypes.UPDATE_THREAD_READ_STATUS,
          userID: viewer.userID,
          time: time,
          threadID: request.threadID,
          unread: request.unread,
        },
      ],
      { viewer, updatesForCurrentSession: 'ignore' },
    );

    await Promise.all([updatesPromise, queryPromise]);
  }

  if (!request.unread) {
    const rescindCondition = SQL`
      n.user = ${viewer.userID} AND
      n.thread = ${request.threadID} AND
      n.message <= ${request.latestMessage}
    `;
    await rescindPushNotifs(rescindCondition);
  }

  return {
    resetToUnread: resetThreadToUnread,
  };
}

async function shouldResetThreadToUnread(
  viewer: Viewer,
  request: SetThreadUnreadStatusRequest,
): Promise<boolean> {
  if (request.unread) {
    return false;
  }

  const resetToUnread = await checkUnfocusedLatestMessage(
    viewer,
    new Map([[request.threadID, parseInt(request.latestMessage) || 0]]),
  );

  return resetToUnread.some(threadID => threadID === request.threadID);
}

// The `focused` table tracks which chat threads are currently in view for a
// given cookie. We track this so that if a user is currently viewing a thread's
// messages, then notifications on that thread are not sent. This function does
// not add new rows to the `focused` table, but instead extends currently active
// rows for the current cookie.
async function updateActivityTime(viewer: Viewer): Promise<void> {
  if (!viewer.loggedIn) {
    return;
  }
  const time = Date.now();
  const focusedQuery = SQL`
    UPDATE focused
    SET time = ${time}
    WHERE user = ${viewer.userID} AND session = ${viewer.session}
  `;
  await dbQuery(focusedQuery);
}

export { activityUpdater, updateActivityTime, setThreadUnreadStatus };
<commit_msg>[server] Fix activityUpdater to handle threads without messages on the client<commit_after>// @flow

import type { Viewer } from '../session/viewer';
import type {
  UpdateActivityResult,
  UpdateActivityRequest,
  SetThreadUnreadStatusRequest,
  SetThreadUnreadStatusResult,
} from 'lib/types/activity-types';
import { messageTypes } from 'lib/types/message-types';
import { threadPermissions } from 'lib/types/thread-types';
import { updateTypes } from 'lib/types/update-types';

import invariant from 'invariant';
import _difference from 'lodash/fp/difference';
import _max from 'lodash/fp/max';

import { ServerError } from 'lib/utils/errors';

import { dbQuery, SQL, mergeOrConditions } from '../database/database';
import { rescindPushNotifs } from '../push/rescind';
import { createUpdates } from '../creators/update-creator';
import { deleteActivityForViewerSession } from '../deleters/activity-deleters';
import { earliestFocusedTimeConsideredCurrent } from '../shared/focused-times';
import {
  checkThread,
  checkThreads,
} from '../fetchers/thread-permission-fetchers';

async function activityUpdater(
  viewer: Viewer,
  request: UpdateActivityRequest,
): Promise<UpdateActivityResult> {
  if (!viewer.loggedIn) {
    throw new ServerError('not_logged_in');
  }

  const focusUpdatesByThreadID = new Map();
  for (const activityUpdate of request.updates) {
    const threadID = activityUpdate.threadID;
    const updatesForThreadID = focusUpdatesByThreadID.get(threadID) ?? [];
    if (!focusUpdatesByThreadID.has(threadID)) {
      focusUpdatesByThreadID.set(threadID, updatesForThreadID);
    }
    updatesForThreadID.push(activityUpdate);
  }

  const unverifiedThreadIDs: $ReadOnlySet<string> = new Set(
    request.updates.map(update => update.threadID),
  );
  const viewerMemberThreads = await checkThreads(
    viewer,
    [...unverifiedThreadIDs],
    [
      {
        check: 'is_member',
      },
      {
        check: 'permission',
        permission: threadPermissions.VISIBLE,
      },
    ],
  );

  if (viewerMemberThreads.size === 0) {
    return { unfocusedToUnread: [] };
  }

  const currentlyFocused = [];
  const unfocusedLatestMessages = new Map<string, number>();
  const latestMessages = new Map<string, number>();
  const rescindConditions = [];
  for (const threadID of viewerMemberThreads) {
    const focusUpdates = focusUpdatesByThreadID.get(threadID);
    invariant(focusUpdates, `no focusUpdate for thread ID ${threadID}`);

    const latestMessage = _max(
      focusUpdates
        .filter(
          update =>
            update.latestMessage && !update.latestMessage.startsWith('local'),
        )
        .map(update => parseInt(update.latestMessage)),
    );
    if (latestMessage) {
      latestMessages.set(threadID, latestMessage);
    }

    const focusEnded = focusUpdates.some(update => !update.focus);

    if (!focusEnded) {
      currentlyFocused.push(threadID);
      rescindConditions.push(SQL`n.thread = ${threadID}`);
    } else {
      unfocusedLatestMessages.set(threadID, latestMessage ?? 0);
      if (latestMessage) {
        rescindConditions.push(
          SQL`(n.thread = ${threadID} AND n.message <= ${latestMessage})`,
        );
      }
    }
  }

  const focusUpdatePromise = updateFocusedRows(viewer, currentlyFocused);

  const outdatedUnfocused = await checkUnfocusedLatestMessage(
    viewer,
    unfocusedLatestMessages,
  );

  const setToRead = [...currentlyFocused];
  const setToUnread = [];
  for (const [threadID] of unfocusedLatestMessages) {
    if (!outdatedUnfocused.includes(threadID)) {
      setToRead.push(threadID);
    } else {
      setToUnread.push(threadID);
    }
  }

  const time = Date.now();
  const updateDatas = [];
  const appendUpdateDatas = (
    threadIDs: $ReadOnlyArray<string>,
    unread: boolean,
  ) => {
    for (const threadID of threadIDs) {
      updateDatas.push({
        type: updateTypes.UPDATE_THREAD_READ_STATUS,
        userID: viewer.userID,
        time,
        threadID,
        unread,
      });
    }
  };

  appendUpdateDatas(setToRead, false);
  appendUpdateDatas(setToUnread, true);

  const updateLatestReadMessagePromise = updateLastReadMessage(
    viewer,
    latestMessages,
  );
  const updatesPromise = createUpdates(updateDatas, {
    viewer,
    updatesForCurrentSession: 'ignore',
  });

  await Promise.all([
    focusUpdatePromise,
    updateLatestReadMessagePromise,
    updatesPromise,
  ]);

  // We do this afterwards so the badge count is correct
  if (rescindConditions.length > 0) {
    const rescindCondition = SQL`n.user = ${viewer.userID} AND `;
    rescindCondition.append(mergeOrConditions(rescindConditions));
    await rescindPushNotifs(rescindCondition);
  }

  return { unfocusedToUnread: outdatedUnfocused };
}

async function updateFocusedRows(
  viewer: Viewer,
  threadIDs: $ReadOnlyArray<string>,
): Promise<void> {
  const time = Date.now();
  if (threadIDs.length > 0) {
    const focusedInsertRows = threadIDs.map(threadID => [
      viewer.userID,
      viewer.session,
      threadID,
      time,
    ]);
    await dbQuery(SQL`
      INSERT INTO focused (user, session, thread, time)
      VALUES ${focusedInsertRows}
      ON DUPLICATE KEY UPDATE time = VALUES(time)
    `);
  }
  await deleteActivityForViewerSession(viewer, time);
}

// To protect against a possible race condition, we reset the thread to unread
// if the latest message ID on the client at the time that focus was dropped
// is no longer the latest message ID.
// Returns the set of unfocused threads that should be set to unread on
// the client because a new message arrived since they were unfocused.
async function checkUnfocusedLatestMessage(
  viewer: Viewer,
  unfocusedLatestMessages: Map<string, number>,
): Promise<string[]> {
  if (unfocusedLatestMessages.size === 0 || !viewer.loggedIn) {
    return [];
  }

  const unfocusedThreadIDs = [...unfocusedLatestMessages.keys()];
  const focusedElsewhereThreadIDs = await checkThreadsFocused(
    viewer,
    unfocusedThreadIDs,
  );
  const unreadCandidates = _difference(unfocusedThreadIDs)(
    focusedElsewhereThreadIDs,
  );
  if (unreadCandidates.length === 0) {
    return [];
  }

  const knowOfExtractString = `$.${threadPermissions.KNOW_OF}.value`;
  const query = SQL`
    SELECT m.thread, MAX(m.id) AS latest_message
    FROM messages m
    LEFT JOIN memberships stm ON m.type = ${messageTypes.CREATE_SUB_THREAD}
      AND stm.thread = m.content AND stm.user = ${viewer.userID}
    WHERE m.thread IN (${unreadCandidates}) AND
      m.user != ${viewer.userID} AND
      (
        m.type != ${messageTypes.CREATE_SUB_THREAD} OR
        JSON_EXTRACT(stm.permissions, ${knowOfExtractString}) IS TRUE
      )
    GROUP BY m.thread
  `;
  const [result] = await dbQuery(query);

  const resetToUnread = [];
  for (const row of result) {
    const threadID = row.thread.toString();
    const serverLatestMessage = row.latest_message;
    const clientLatestMessage = unfocusedLatestMessages.get(threadID);
    if (clientLatestMessage < serverLatestMessage) {
      resetToUnread.push(threadID);
    }
  }
  return resetToUnread;
}

async function checkThreadsFocused(
  viewer: Viewer,
  threadIDs: $ReadOnlyArray<string>,
): Promise<string[]> {
  const time = earliestFocusedTimeConsideredCurrent();
  const query = SQL`
    SELECT thread
    FROM focused
    WHERE time > ${time}
      AND user = ${viewer.userID}
      AND thread IN (${threadIDs})
    GROUP BY thread
  `;
  const [result] = await dbQuery(query);

  const focusedThreadIDs = [];
  for (const row of result) {
    focusedThreadIDs.push(row.thread.toString());
  }
  return focusedThreadIDs;
}

async function updateLastReadMessage(
  viewer: Viewer,
  lastReadMessages: $ReadOnlyMap<string, number>,
) {
  if (lastReadMessages.size === 0) {
    return;
  }

  const query = SQL`
    UPDATE memberships
    SET last_read_message = GREATEST(last_read_message,
      CASE
  `;
  lastReadMessages.forEach((lastMessage, threadID) => {
    query.append(SQL`
        WHEN thread = ${threadID} THEN ${lastMessage} `);
  });
  query.append(SQL`
        ELSE last_read_message
      END)
    WHERE thread IN (${[...lastReadMessages.keys()]})
      AND user = ${viewer.userID}
  `);

  return await dbQuery(query);
}

async function setThreadUnreadStatus(
  viewer: Viewer,
  request: SetThreadUnreadStatusRequest,
): Promise<SetThreadUnreadStatusResult> {
  if (!viewer.loggedIn) {
    throw new ServerError('not_logged_in');
  }

  const isMemberAndCanViewThread = await checkThread(viewer, request.threadID, [
    {
      check: 'is_member',
    },
    {
      check: 'permission',
      permission: threadPermissions.VISIBLE,
    },
  ]);
  if (!isMemberAndCanViewThread) {
    throw new ServerError('invalid_parameters');
  }

  const resetThreadToUnread = await shouldResetThreadToUnread(viewer, request);
  if (!resetThreadToUnread) {
    const lastReadMessage = request.unread
      ? SQL`0`
      : SQL`GREATEST(m.last_read_message, ${request.latestMessage ?? 0})`;
    const update = SQL`
      UPDATE memberships m
      SET m.last_read_message =
    `;
    update.append(lastReadMessage);
    update.append(SQL`
      WHERE m.thread = ${request.threadID} AND m.user = ${viewer.userID}
    `);
    const queryPromise = dbQuery(update);

    const time = Date.now();
    const updatesPromise = createUpdates(
      [
        {
          type: updateTypes.UPDATE_THREAD_READ_STATUS,
          userID: viewer.userID,
          time: time,
          threadID: request.threadID,
          unread: request.unread,
        },
      ],
      { viewer, updatesForCurrentSession: 'ignore' },
    );

    await Promise.all([updatesPromise, queryPromise]);
  }

  if (!request.unread) {
    const rescindCondition = SQL`
      n.user = ${viewer.userID} AND
      n.thread = ${request.threadID} AND
      n.message <= ${request.latestMessage}
    `;
    await rescindPushNotifs(rescindCondition);
  }

  return {
    resetToUnread: resetThreadToUnread,
  };
}

async function shouldResetThreadToUnread(
  viewer: Viewer,
  request: SetThreadUnreadStatusRequest,
): Promise<boolean> {
  if (request.unread) {
    return false;
  }

  const resetToUnread = await checkUnfocusedLatestMessage(
    viewer,
    new Map([[request.threadID, parseInt(request.latestMessage) || 0]]),
  );

  return resetToUnread.some(threadID => threadID === request.threadID);
}

// The `focused` table tracks which chat threads are currently in view for a
// given cookie. We track this so that if a user is currently viewing a thread's
// messages, then notifications on that thread are not sent. This function does
// not add new rows to the `focused` table, but instead extends currently active
// rows for the current cookie.
async function updateActivityTime(viewer: Viewer): Promise<void> {
  if (!viewer.loggedIn) {
    return;
  }
  const time = Date.now();
  const focusedQuery = SQL`
    UPDATE focused
    SET time = ${time}
    WHERE user = ${viewer.userID} AND session = ${viewer.session}
  `;
  await dbQuery(focusedQuery);
}

export { activityUpdater, updateActivityTime, setThreadUnreadStatus };
<|endoftext|>


<commit_before>const YouTube = require('simple-youtube-api');
const youtube = new YouTube('  Y  o  u  r     A  p  i     K  e  y  ');

youtube.videoById('https://www.youtube.com/watch?v=3odIdmuFfEY')
    .then(video => {
        console.log(`The video's title is ${video[0].title}`);
    })
    .catch(console.log);
<commit_msg>Update video.js example (#41)<commit_after>const YouTube = require('simple-youtube-api');
const youtube = new YouTube('  Y  o  u  r     A  p  i     K  e  y  ');

youtube.getVideo('https://www.youtube.com/watch?v=3odIdmuFfEY')
    .then(video => {
        console.log(`The video's title is ${video.title}`);
    })
    .catch(console.log);
<|endoftext|>


<commit_before>var jsPlugin = require("bit-loader-js");
var babelPlugin = require("bit-loader-babel");
var splitBundle = require("bit-bundler-splitter");
var Bitbundler = require("bit-bundler");
var buildstatsLogger = require("bit-bundler/loggers/buildstats");
var warningsLogger = require("bit-bundler/loggers/warnings");
var watchLogger = require("bit-bundler/loggers/watch");

var logger = watchLogger();
logger.pipe(buildstatsLogger()).pipe(warningsLogger());

var bitbundler = new Bitbundler({
  log: logger,
  loader: {
    plugins: [
      jsPlugin(),
      babelPlugin()
    ]
  },
  bundler: {
    plugins: [
      splitBundle("dest/watch-renderer.js", { match: { path: /src\/renderer/ } }),
      splitBundle("dest/watch-other.js", { match: { fileName: "other.js" } })
    ]
  },
  watch: true
});

bitbundler
  .bundle({
    src: "src/main.js",
    dest: "dest/watch-main.js"
  })
  .then(function() {
    console.log("watch bundle complete.");
  });
<commit_msg>removed extra log message that's no longer needed in the example because that's what buildstat is now logging<commit_after>var jsPlugin = require("bit-loader-js");
var babelPlugin = require("bit-loader-babel");
var splitBundle = require("bit-bundler-splitter");
var Bitbundler = require("bit-bundler");
var buildstatsLogger = require("bit-bundler/loggers/buildstats");
var warningsLogger = require("bit-bundler/loggers/warnings");
var watchLogger = require("bit-bundler/loggers/watch");

var logger = watchLogger();
logger.pipe(buildstatsLogger()).pipe(warningsLogger());

var bitbundler = new Bitbundler({
  log: logger,
  loader: {
    plugins: [
      jsPlugin(),
      babelPlugin()
    ]
  },
  bundler: {
    plugins: [
      splitBundle("dest/watch-renderer.js", { match: { path: /src\/renderer/ } }),
      splitBundle("dest/watch-other.js", { match: { fileName: "other.js" } })
    ]
  },
  watch: true
});

bitbundler
  .bundle({
    src: "src/main.js",
    dest: "dest/watch-main.js"
  });
<|endoftext|>


<commit_before>var hasfocusUpdatingProperty = '__ko_hasfocusUpdating';
var hasfocusLastValue = '__ko_hasfocusLastValue';
ko.bindingHandlers['hasfocus'] = {
    'init': function(element, valueAccessor, allBindings) {
        var handleElementFocusChange = function(isFocused) {
            // Where possible, ignore which event was raised and determine focus state using activeElement,
            // as this avoids phantom focus/blur events raised when changing tabs in modern browsers.
            // However, not all KO-targeted browsers (Firefox 2) support activeElement. For those browsers,
            // prevent a loss of focus when changing tabs/windows by setting a flag that prevents hasfocus
            // from calling 'blur()' on the element when it loses focus.
            // Discussion at https://github.com/SteveSanderson/knockout/pull/352
            element[hasfocusUpdatingProperty] = true;
            var ownerDoc = element.ownerDocument;
            if ("activeElement" in ownerDoc) {
                var active;
                try {
                    active = ownerDoc.activeElement;
                } catch(e) {
                    // IE9 throws if you access activeElement during page load (see issue #703)
                    active = ownerDoc.body;
                }
                isFocused = (active === element);
            }
            var modelValue = valueAccessor();
            ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'hasfocus', isFocused, true);

            //cache the latest value, so we can avoid unnecessarily calling focus/blur in the update function
            element[hasfocusLastValue] = isFocused;
            element[hasfocusUpdatingProperty] = false;
        };
        var handleElementFocusIn = handleElementFocusChange.bind(null, true);
        var handleElementFocusOut = handleElementFocusChange.bind(null, false);

        ko.utils.registerEventHandler(element, "focus", handleElementFocusIn);
        ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE
        ko.utils.registerEventHandler(element, "blur",  handleElementFocusOut);
        ko.utils.registerEventHandler(element, "focusout",  handleElementFocusOut); // For IE
    },
    'update': function(element, valueAccessor) {
        var value = !!ko.utils.unwrapObservable(valueAccessor()); //force boolean to compare with last value
        if (!element[hasfocusUpdatingProperty] && element[hasfocusLastValue] !== value) {
            value ? element.focus() : element.blur();
            ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, value ? "focusin" : "focusout"]); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
        }
    }
};
ko.expressionRewriting.twoWayBindings['hasfocus'] = true;

ko.bindingHandlers['hasFocus'] = ko.bindingHandlers['hasfocus']; // Make "hasFocus" an alias
ko.expressionRewriting.twoWayBindings['hasFocus'] = true;
<commit_msg>In IE, the blur method doesn't always cause the element to lose focus (for example, if the window is not in focus). Use another method to blur the element that does seem to be reliable in IE.<commit_after>var hasfocusUpdatingProperty = '__ko_hasfocusUpdating';
var hasfocusLastValue = '__ko_hasfocusLastValue';
ko.bindingHandlers['hasfocus'] = {
    'init': function(element, valueAccessor, allBindings) {
        var handleElementFocusChange = function(isFocused) {
            // Where possible, ignore which event was raised and determine focus state using activeElement,
            // as this avoids phantom focus/blur events raised when changing tabs in modern browsers.
            // However, not all KO-targeted browsers (Firefox 2) support activeElement. For those browsers,
            // prevent a loss of focus when changing tabs/windows by setting a flag that prevents hasfocus
            // from calling 'blur()' on the element when it loses focus.
            // Discussion at https://github.com/SteveSanderson/knockout/pull/352
            element[hasfocusUpdatingProperty] = true;
            var ownerDoc = element.ownerDocument;
            if ("activeElement" in ownerDoc) {
                var active;
                try {
                    active = ownerDoc.activeElement;
                } catch(e) {
                    // IE9 throws if you access activeElement during page load (see issue #703)
                    active = ownerDoc.body;
                }
                isFocused = (active === element);
            }
            var modelValue = valueAccessor();
            ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'hasfocus', isFocused, true);

            //cache the latest value, so we can avoid unnecessarily calling focus/blur in the update function
            element[hasfocusLastValue] = isFocused;
            element[hasfocusUpdatingProperty] = false;
        };
        var handleElementFocusIn = handleElementFocusChange.bind(null, true);
        var handleElementFocusOut = handleElementFocusChange.bind(null, false);

        ko.utils.registerEventHandler(element, "focus", handleElementFocusIn);
        ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE
        ko.utils.registerEventHandler(element, "blur",  handleElementFocusOut);
        ko.utils.registerEventHandler(element, "focusout",  handleElementFocusOut); // For IE
    },
    'update': function(element, valueAccessor) {
        var value = !!ko.utils.unwrapObservable(valueAccessor()); //force boolean to compare with last value
        if (!element[hasfocusUpdatingProperty] && element[hasfocusLastValue] !== value) {
            value ? element.focus() : (element.blur(), element.ownerDocument.body.focus());
            ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, value ? "focusin" : "focusout"]); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
        }
    }
};
ko.expressionRewriting.twoWayBindings['hasfocus'] = true;

ko.bindingHandlers['hasFocus'] = ko.bindingHandlers['hasfocus']; // Make "hasFocus" an alias
ko.expressionRewriting.twoWayBindings['hasFocus'] = true;
<|endoftext|>