From 363c4e0b14adb5fa27f7ef274c7fe20c5ae7c10e Mon Sep 17 00:00:00 2001 From: electron128 Date: Sat, 2 Jan 2016 15:52:17 +0100 Subject: [PATCH 01/64] started mithril.js based webui --- libresapi/src/webui-src/.gitignore | 2 + libresapi/src/webui-src/app/_chat.sass | 71 + libresapi/src/webui-src/app/_reset.scss | 48 + libresapi/src/webui-src/app/assets/index.html | 21 + libresapi/src/webui-src/app/chat.js | 27 + libresapi/src/webui-src/app/main.sass | 9 + libresapi/src/webui-src/app/mithril.js | 1159 +++++++++++++++++ libresapi/src/webui-src/brunch-config.js | 12 + libresapi/src/webui-src/package.json | 11 + 9 files changed, 1360 insertions(+) create mode 100644 libresapi/src/webui-src/.gitignore create mode 100644 libresapi/src/webui-src/app/_chat.sass create mode 100644 libresapi/src/webui-src/app/_reset.scss create mode 100644 libresapi/src/webui-src/app/assets/index.html create mode 100644 libresapi/src/webui-src/app/chat.js create mode 100644 libresapi/src/webui-src/app/main.sass create mode 100644 libresapi/src/webui-src/app/mithril.js create mode 100644 libresapi/src/webui-src/brunch-config.js create mode 100644 libresapi/src/webui-src/package.json diff --git a/libresapi/src/webui-src/.gitignore b/libresapi/src/webui-src/.gitignore new file mode 100644 index 000000000..cff629a7a --- /dev/null +++ b/libresapi/src/webui-src/.gitignore @@ -0,0 +1,2 @@ +node_modules/* +public/* diff --git a/libresapi/src/webui-src/app/_chat.sass b/libresapi/src/webui-src/app/_chat.sass new file mode 100644 index 000000000..c0c46007a --- /dev/null +++ b/libresapi/src/webui-src/app/_chat.sass @@ -0,0 +1,71 @@ +.chat + $color: lightblue + $header_height: 50px + $left_width: 200px + $right_width: 200px + $input_height: 100px + padding: 15px + &.container + height: 100% + padding: 0px + position: relative + &.header + position: absolute + top: 0px + left: 0px + right: 0px + height: $header_height + background-color: $color + border-bottom: solid 1px gray + box-sizing: border-box + &.left + position: absolute + top: $header_height + bottom: 0px + left: 0px + width: $left_width + border-right: solid 1px gray + box-sizing: border-box + background-color: lightgray + &.right + position: absolute + top: $header_height + right: 0px + bottom: 0px + width: $right_width + box-sizing: border-box + border-left: solid 1px gray + &.middle + //background-color: blue + position: absolute + top: $header_height + left: $left_width + right: $right_width + box-sizing: border-box + padding-top: 0px + padding-left: 0px + padding-right: 0px + + &.msg + padding: 0px + $author_width: 100px + &.container + position: relative + border-bottom: solid 1px lightgray + padding: 10px + //background-color: lime + &.from + position: absolute + width: $author_width + top: 10px + left: 0px + color: gray + text-align: right + &.when + float: right + color: lightgray + margin-bottom: 10px + &.text + padding-left: $author_width + top: 0px + left: $author_width \ No newline at end of file diff --git a/libresapi/src/webui-src/app/_reset.scss b/libresapi/src/webui-src/app/_reset.scss new file mode 100644 index 000000000..af944401f --- /dev/null +++ b/libresapi/src/webui-src/app/_reset.scss @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/libresapi/src/webui-src/app/assets/index.html b/libresapi/src/webui-src/app/assets/index.html new file mode 100644 index 000000000..b914d9932 --- /dev/null +++ b/libresapi/src/webui-src/app/assets/index.html @@ -0,0 +1,21 @@ + + + + + rswebui5 + + + +
if app does not load, enable JavaScript!
+ + + + diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js new file mode 100644 index 000000000..f91ab536c --- /dev/null +++ b/libresapi/src/webui-src/app/chat.js @@ -0,0 +1,27 @@ +"use strict"; + +var m = require("mithril"); + +function msg(from, when, text){ + return m(".chat.msg.container",[ + m(".chat.msg.from", from), + m(".chat.msg.when", when), + m(".chat.msg.text", text), + ]); +} + +module.exports = { + view: function(){ + return m(".chat.container", [ + m(".chat.header", "headerbar"), + m(".chat.left", "left"), + m(".chat.right", "right"), + m(".chat.middle", [ + msg("Andi", "now", "Hallo"), + msg("Test", "now", "Hallo back"), + msg("Somebody", "now", "Hallo back, sfjhfu dsjkchsd wehfskf sdjksdf sjdnfkjsf sdjkfhjksdf jksdfjksdnf sjdefhsjkn cesjdfhsjk fskldcjhsklc ksdj"), + ]), + m(".chat.clear", ""), + ]); + } +} \ No newline at end of file diff --git a/libresapi/src/webui-src/app/main.sass b/libresapi/src/webui-src/app/main.sass new file mode 100644 index 000000000..1a7df2b49 --- /dev/null +++ b/libresapi/src/webui-src/app/main.sass @@ -0,0 +1,9 @@ +@import "reset" + +html, body, #main + height: 100% + +body + font-family: "Sans-serif" + +@import "chat" \ No newline at end of file diff --git a/libresapi/src/webui-src/app/mithril.js b/libresapi/src/webui-src/app/mithril.js new file mode 100644 index 000000000..318573d8c --- /dev/null +++ b/libresapi/src/webui-src/app/mithril.js @@ -0,0 +1,1159 @@ +var m = (function app(window, undefined) { + var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function"; + var type = {}.toString; + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; + var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; + var noop = function() {} + + // caching commonly used variables + var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; + + // self invoking function needed because of the way mocks work + function initialize(window){ + $document = window.document; + $location = window.location; + $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout; + $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout; + } + + initialize(window); + + + /** + * @typedef {String} Tag + * A string that looks like -> div.classname#id[param=one][param2=two] + * Which describes a DOM node + */ + + /** + * + * @param {Tag} The DOM node tag + * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs + * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional) + * + */ + function m() { + var args = [].slice.call(arguments); + var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1] || "view" in args[1]) && !("subtree" in args[1]); + var attrs = hasAttrs ? args[1] : {}; + var classAttrName = "class" in attrs ? "class" : "className"; + var cell = {tag: "div", attrs: {}}; + var match, classes = []; + if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string") + while (match = parser.exec(args[0])) { + if (match[1] === "" && match[2]) cell.tag = match[2]; + else if (match[1] === "#") cell.attrs.id = match[2]; + else if (match[1] === ".") classes.push(match[2]); + else if (match[3][0] === "[") { + var pair = attrParser.exec(match[3]); + cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) + } + } + + var children = hasAttrs ? args.slice(2) : args.slice(1); + if (children.length === 1 && type.call(children[0]) === ARRAY) { + cell.children = children[0] + } + else { + cell.children = children + } + + for (var attrName in attrs) { + if (attrs.hasOwnProperty(attrName)) { + if (attrName === classAttrName && attrs[attrName] != null && attrs[attrName] !== "") { + classes.push(attrs[attrName]) + cell.attrs[attrName] = "" //create key in correct iteration order + } + else cell.attrs[attrName] = attrs[attrName] + } + } + if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" "); + + return cell + } + function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { + //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached` + //the diff algorithm can be summarized as this: + //1 - compare `data` and `cached` + //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is + //3 - recursively apply this algorithm for every array and for the children of every virtual element + + //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions: + //- `cached` always has a property called `nodes`, which is a list of DOM elements that correspond to the data represented by the respective virtual element + //- in order to support attaching `nodes` as a property of `cached`, `cached` is *always* a non-primitive object, i.e. if the data was a string, then cached is a String instance. If data was `null` or `undefined`, cached is `new String("")` + //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context) + //- when `cached` is an Object, it represents a virtual element; when it's an Array, it represents a list of elements; when it's a String, Number or Boolean, it represents a text node + + //`parentElement` is a DOM element used for W3C DOM API calls + //`parentTag` is only used for handling a corner case for textarea values + //`parentCache` is used to remove nodes in some multi-node cases + //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable + //`data` and `cached` are, respectively, the new and old nodes being diffed + //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent) + //`editable` is a flag that indicates whether an ancestor is contenteditable + //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor + //`configs` is a list of config functions to run after the topmost `build` call finishes running + + //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings + //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")} + //- it simplifies diffing code + //data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version) + try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""} + if (data.subtree === "retain") return cached; + var cachedType = type.call(cached), dataType = type.call(data); + if (cached == null || cachedType !== dataType) { + if (cached != null) { + if (parentCache && parentCache.nodes) { + var offset = index - parentIndex; + var end = offset + (dataType === ARRAY ? data : cached.nodes).length; + clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) + } + else if (cached.nodes) clear(cached.nodes, cached) + } + cached = new data.constructor; + if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) + cached.nodes = [] + } + + if (dataType === ARRAY) { + //recursively flatten array + for (var i = 0, len = data.length; i < len; i++) { + if (type.call(data[i]) === ARRAY) { + data = data.concat.apply([], data); + i-- //check current index again and flatten until there are no more nested arrays at that index + len = data.length + } + } + + var nodes = [], intact = cached.length === data.length, subArrayCount = 0; + + //keys algorithm: sort elements without recreating them if keys are present + //1) create a map of all existing keys, and mark all for deletion + //2) add new keys to map and mark them for addition + //3) if key exists in new list, change action from deletion to a move + //4) for each key, handle its corresponding action as marked in previous steps + var DELETION = 1, INSERTION = 2 , MOVE = 3; + var existing = {}, shouldMaintainIdentities = false; + for (var i = 0; i < cached.length; i++) { + if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { + shouldMaintainIdentities = true; + existing[cached[i].attrs.key] = {action: DELETION, index: i} + } + } + + var guid = 0 + for (var i = 0, len = data.length; i < len; i++) { + if (data[i] && data[i].attrs && data[i].attrs.key != null) { + for (var j = 0, len = data.length; j < len; j++) { + if (data[j] && data[j].attrs && data[j].attrs.key == null) data[j].attrs.key = "__mithril__" + guid++ + } + break + } + } + + if (shouldMaintainIdentities) { + var keysDiffer = false + if (data.length != cached.length) keysDiffer = true + else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) { + if (cachedCell.attrs && dataCell.attrs && cachedCell.attrs.key != dataCell.attrs.key) { + keysDiffer = true + break + } + } + + if (keysDiffer) { + for (var i = 0, len = data.length; i < len; i++) { + if (data[i] && data[i].attrs) { + if (data[i].attrs.key != null) { + var key = data[i].attrs.key; + if (!existing[key]) existing[key] = {action: INSERTION, index: i}; + else existing[key] = { + action: MOVE, + index: i, + from: existing[key].index, + element: cached.nodes[existing[key].index] || $document.createElement("div") + } + } + } + } + var actions = [] + for (var prop in existing) actions.push(existing[prop]) + var changes = actions.sort(sortChanges); + var newCached = new Array(cached.length) + newCached.nodes = cached.nodes.slice() + + for (var i = 0, change; change = changes[i]; i++) { + if (change.action === DELETION) { + clear(cached[change.index].nodes, cached[change.index]); + newCached.splice(change.index, 1) + } + if (change.action === INSERTION) { + var dummy = $document.createElement("div"); + dummy.key = data[change.index].attrs.key; + parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); + newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) + newCached.nodes[change.index] = dummy + } + + if (change.action === MOVE) { + if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { + parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) + } + newCached[change.index] = cached[change.from] + newCached.nodes[change.index] = change.element + } + } + cached = newCached; + } + } + //end key algorithm + + for (var i = 0, cacheCount = 0, len = data.length; i < len; i++) { + //diff each item in the array + var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs); + if (item === undefined) continue; + if (!item.nodes.intact) intact = false; + if (item.$trusted) { + //fix offset of next element if item was a trusted string w/ more than one html element + //the first clause in the regexp matches elements + //the second clause (after the pipe) matches text nodes + subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length + } + else subArrayCount += type.call(item) === ARRAY ? item.length : 1; + cached[cacheCount++] = item + } + if (!intact) { + //diff the array itself + + //update the list of DOM nodes by collecting the nodes from each item + for (var i = 0, len = data.length; i < len; i++) { + if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) + } + //remove items from the end of the array if the new array is shorter than the old one + //if errors ever happen here, the issue is most likely a bug in the construction of the `cached` data structure somewhere earlier in the program + for (var i = 0, node; node = cached.nodes[i]; i++) { + if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]) + } + if (data.length < cached.length) cached.length = data.length; + cached.nodes = nodes + } + } + else if (data != null && dataType === OBJECT) { + var views = [], controllers = [] + while (data.view) { + var view = data.view.$original || data.view + var controllerIndex = m.redraw.strategy() == "diff" && cached.views ? cached.views.indexOf(view) : -1 + var controller = controllerIndex > -1 ? cached.controllers[controllerIndex] : new (data.controller || noop) + var key = data && data.attrs && data.attrs.key + data = pendingRequests == 0 || (cached && cached.controllers && cached.controllers.indexOf(controller) > -1) ? data.view(controller) : {tag: "placeholder"} + if (data.subtree === "retain") return cached; + if (key) { + if (!data.attrs) data.attrs = {} + data.attrs.key = key + } + if (controller.onunload) unloaders.push({controller: controller, handler: controller.onunload}) + views.push(view) + controllers.push(controller) + } + if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.") + if (!data.attrs) data.attrs = {}; + if (!cached.attrs) cached.attrs = {}; + + var dataAttrKeys = Object.keys(data.attrs) + var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) + //if an element is different enough from the one in cache, recreate it + if (data.tag != cached.tag || dataAttrKeys.sort().join() != Object.keys(cached.attrs).sort().join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (m.redraw.strategy() == "all" && (!cached.configContext || cached.configContext.retain !== true)) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) { + if (cached.nodes.length) clear(cached.nodes); + if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() + if (cached.controllers) { + for (var i = 0, controller; controller = cached.controllers[i]; i++) { + if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop}) + } + } + } + if (type.call(data.tag) != STRING) return; + + var node, isNew = cached.nodes.length === 0; + if (data.attrs.xmlns) namespace = data.attrs.xmlns; + else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; + else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"; + + if (isNew) { + if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); + else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag); + cached = { + tag: data.tag, + //set attributes first, then create children + attrs: hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs, + children: data.children != null && data.children.length > 0 ? + build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : + data.children, + nodes: [node] + }; + if (controllers.length) { + cached.views = views + cached.controllers = controllers + for (var i = 0, controller; controller = controllers[i]; i++) { + if (controller.onunload && controller.onunload.$old) controller.onunload = controller.onunload.$old + if (pendingRequests && controller.onunload) { + var onunload = controller.onunload + controller.onunload = noop + controller.onunload.$old = onunload + } + } + } + + if (cached.children && !cached.children.nodes) cached.children.nodes = []; + //edge case: setting value on
+ + + ); + if(this.state.page === "waiting") + return( +
+ waiting for response from server... +
+ ); + if(this.state.page === "peer") + return( +
+

Do you want to add {this.state.data.name} to your friendslist?

+ Allow direct downloads from this node
+ Auto download recommended files from this node
+
add to friendslist
+
+ ); + }, + + */ diff --git a/libresapi/src/webui-src/app/menu.js b/libresapi/src/webui-src/app/menu.js index 2faaa1205..73ea862b5 100644 --- a/libresapi/src/webui-src/app/menu.js +++ b/libresapi/src/webui-src/app/menu.js @@ -10,9 +10,12 @@ function goback(){ } function buildmenu(menu, tagname, runstate, ignore){ - if ((menu.runstate === undefined || runstate.match(menu.runstate)) - && (!ignore.match(menu.name)) - && (menu.path === undefined || !menu.path.contains(":"))) { + if ( + (menu.runstate === undefined || runstate.match(menu.runstate)) + && (!ignore.match(menu.name)) + && (menu.path === undefined || !menu.path.contains(":")) + && (menu.show === undefined || menu.show) + ) { if (menu.action === undefined) { return m(tagname , { onclick: function(){ diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index dba9208e0..67e064e00 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -12,6 +12,11 @@ module.exports = { nodes: [ name: "peers", runstate: "running_ok.*", }, + { + name: "addpeer", + runstate: "running_ok.*", + show: false, + }, { name:"searchresult", path: "/search/:id", diff --git a/libresapi/src/webui-src/app/peers.js b/libresapi/src/webui-src/app/peers.js index cf474971a..45a235f79 100644 --- a/libresapi/src/webui-src/app/peers.js +++ b/libresapi/src/webui-src/app/peers.js @@ -7,10 +7,35 @@ module.exports = {view: function(){ var peers = rs("peers"); console.log("peers:" + peers); if(peers === undefined){ - return m("div", "waiting_server"); - } - return m("div", peers.map(function(peer){ - return m("div",peer.name) - })); + return m("div",[ + m("h2","peers"), + m("h3","waiting_server"), + ]); + }; + var online = 0; + var peerlist = peers.map(function(peer){ + var isonline = false; + var loclist = peer.locations.map(function(location){ + if (location.is_online && ! isonline){ + online +=1; + isonline = true; + } + return m("li", + {style:"color:" + (location.is_online ? "lime": "grey")}, + location.location); + }); + return [ + m("h2", {style:"color:" + (isonline ? "lime": "grey")} ,peer.name), + m("ul", loclist ), + ]; + }); + + return m("div",[ + m("div.btn2",{onclick: function(){m.route("/addpeer")}},"add new friend"), + m("h2","peers ( online: " + online +" / " + peers.length + "):"), + m("table", [ + peerlist, + ]), + ]); } }; From 2e41c373b55affe5cc4dcaa6ade768050cbbedbe Mon Sep 17 00:00:00 2001 From: zeners Date: Tue, 12 Jan 2016 19:26:04 +0100 Subject: [PATCH 16/64] webui: add / remove peers --- libresapi/src/webui-src/app/addpeer.js | 84 ++++++++++++++++++++++- libresapi/src/webui-src/app/main.js | 40 +++++------ libresapi/src/webui-src/app/peers.js | 62 ++++++++++++++--- libresapi/src/webui-src/app/retroshare.js | 11 +++ 4 files changed, 165 insertions(+), 32 deletions(-) diff --git a/libresapi/src/webui-src/app/addpeer.js b/libresapi/src/webui-src/app/addpeer.js index 5d6ba7992..5f8806ccf 100644 --- a/libresapi/src/webui-src/app/addpeer.js +++ b/libresapi/src/webui-src/app/addpeer.js @@ -3,12 +3,92 @@ var m = require("mithril"); var rs = require("retroshare"); +var newkey = ""; +var remote = ""; module.exports = { view: function(){ - return m("h2","add new friend"); + var key = m.route.param("radix"); + var pgp = m.route.param("pgp_id"); + var peer_id =m.route.param("peer_id"); + + if (key===undefined && pgp === undefined) { + + var owncert = rs("peers/self/certificate"); + if (owncert === undefined ) { + owncert = {cert_string:"< waiting for server ... >"} + } + return m("div", [ + m("h2","add new friend (Step 1/3)"), + m("p","Your own key, give it to your friends"), + m("pre", owncert.cert_string), + m("p","paste your friends key below"), + m("textarea", { + ref:"cert", + cols:"70", + rows:"16", + onchange: m.withAttr("value", function(value){newkey=value;}) + }), + m("br"), + m("input.btn2",{ + type:"button", + value:"read", + onclick: function (){ + m.route("/addpeer",{radix:newkey}) + }, + }) + ]); + } else if (pgp === undefined) { + rs.request("peers/examine_cert",{cert_string:key}, + function(data,responsetoken){ + data.radix=key; + m.route("/addpeer", data); + } + ); + return m("div", [ + m("h2","add new friend (Step 2/3)"), + m("div", "analyse cert, please wait for server ...") + // { data: null, debug_msg: "failed to load certificate ", returncode: "fail" } + ]); + } else { + var result = { + cert_string:key , + flags:{ + allow_direct_download:false, + allow_push:false, + // set to false, until the webinterface supports managment of the blacklist/whitelist + require_whitelist: false, + } + }; + return m("div",[ + m("h2","add new friend (Step 2/3)"), + m("p","Do you want to add " + + m.route.param("name") + + " (" + m.route.param("location") + ")" + + " to your friendslist?"), + m("input.checkbox[type=checkbox]", { + onchange: m.withAttr("checked", function(checked){ + result.flags.allow_direct_download=checked; + }) + }), "Allow direct downloads from this node", + m("br"), + m("input.checkbox[type=checkbox]", { + onchange: m.withAttr("checked", function(checked){ + result.flags.allow_push=checked; + }) + }), "Auto download recommended files from this node", + m("div.btn2",{ + onclick: function(){ + rs.request("peers",result,function(data, responsetoken){ + m.route("/peers"); + }) + } + },"add to friendslist") + + ]) + } } -} +}; /* diff --git a/libresapi/src/webui-src/app/main.js b/libresapi/src/webui-src/app/main.js index a7f687ee3..32ea255a0 100644 --- a/libresapi/src/webui-src/app/main.js +++ b/libresapi/src/webui-src/app/main.js @@ -37,7 +37,7 @@ function Page(content, runst){ } else { if ("waiting_init|waiting_startup".match(runstate.runstate)) { return m("h2","server starting ...") - } else if("waiting_account_select|running_ok*".match(runstate.runstate)) { + } else if("waiting_account_select|running_ok.*".match(runstate.runstate)) { if (runst === undefined || runst.match(runstate.runstate)) { return m("div", [ m("div", menu.view()), @@ -61,26 +61,24 @@ function Page(content, runst){ }; -var me ={}; +module.exports = { + init:function(main){ + console.log("start init ..."); + var menudef = require("menudef"); + var maps = {}; + var m = require("mithril"); -me.init = function(main){ - console.log("start init ..."); - var menudef = require("menudef"); - var maps = {}; - var m = require("mithril"); - - menudef.nodes.map(function(menu){ - if (menu.action === undefined) { - var path = menu.path != undefined ? menu.path : ("/" + menu.name); - var module = menu.module != undefined ? menu.module : menu.name - console.log("adding route " + menu.name + " for " + path + " with module " + module); - maps[path] = new Page(require(module), menu.runstate); - } - }); - m.route.mode = "hash"; - m.route(main,"/",maps); - console.log("init done."); + menudef.nodes.map(function(menu){ + if (menu.action === undefined) { + var path = menu.path != undefined ? menu.path : ("/" + menu.name); + var module = menu.module != undefined ? menu.module : menu.name + console.log("adding route " + menu.name + " for " + path + " with module " + module); + maps[path] = new Page(require(module), menu.runstate); + } + }); + m.route.mode = "hash"; + m.route(main,"/",maps); + console.log("init done."); + } }; -module.exports = me; - diff --git a/libresapi/src/webui-src/app/peers.js b/libresapi/src/webui-src/app/peers.js index 45a235f79..763a3ca10 100644 --- a/libresapi/src/webui-src/app/peers.js +++ b/libresapi/src/webui-src/app/peers.js @@ -6,34 +6,78 @@ var rs = require("retroshare"); module.exports = {view: function(){ var peers = rs("peers"); console.log("peers:" + peers); + + //waiting for peerlist ... if(peers === undefined){ return m("div",[ m("h2","peers"), m("h3","waiting_server"), ]); }; + + //building peerlist (prebuild for counting) var online = 0; var peerlist = peers.map(function(peer){ var isonline = false; + var avatar_address =""; + + //building location list (prebuild for state + icon) var loclist = peer.locations.map(function(location){ if (location.is_online && ! isonline){ online +=1; isonline = true; } - return m("li", - {style:"color:" + (location.is_online ? "lime": "grey")}, - location.location); + if (location.avatar_address != "" && avatar_address =="") { + avatar_address=location.avatar_address; + } + return m("div",{ + style:"color:" + (location.is_online ? "lime": "grey") + + ";cursor:pointer", + + },location.location); }); - return [ - m("h2", {style:"color:" + (isonline ? "lime": "grey")} ,peer.name), - m("ul", loclist ), - ]; + + //return friend (peer + locations) + return m("div.flexbox[style=color:lime]",[ + // avatar-icon + m("div", [ + avatar_address == "" ? "" : ( + m("img",{ + src: rs.apiurl("peers" + avatar_address), + style:"border-radius:3mm;margin:2mm;", + }) + ) + ]), + //peername + locations + m("div.flexwidemember",[ + m("h1[style=margin-bottom:1mm;]", + {style:"color:" + (isonline ? "lime": "grey")} , + peer.name + ), + m("div", loclist ), + ]), + //remove-button + m("div", { + style: "color:red;" + + "font-size:1.5em;" + + "padding:0.2em;" + + "cursor:pointer", + onclick: function (){ + var yes = window.confirm( + "Remove " + peer.name + " from friendslist?"); + if(yes){ + rs.request("peers/" + peer.pgp_id +"/delete"); + } + } + }, "X") + ]); }); + // return add-peer-button + peerlist return m("div",[ m("div.btn2",{onclick: function(){m.route("/addpeer")}},"add new friend"), - m("h2","peers ( online: " + online +" / " + peers.length + "):"), - m("table", [ + m("h2","peers (online: " + online +" / " + peers.length + "):"), + m("div", [ peerlist, ]), ]); diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index e7e2521a3..4bedf306d 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -215,3 +215,14 @@ rs.forceUpdate = function(path){ cache[path].requested=false; } +//return api-path +rs.apiurl = function(path) { + if (path === undefined) { + path=""; + } + if (path.length > 0 && "^\\\\|\\/".match(path)) { + path=path.substr(1); + } + return api_url + path; +} + From b6bb69df7a1097ee7ec41633e3ee49992a447f89 Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 16 Jan 2016 17:25:12 +0100 Subject: [PATCH 17/64] webui: adding counting feature for menu --- libresapi/src/webui-src/app/menu.js | 11 +++++++++-- libresapi/src/webui-src/app/menudef.js | 20 ++++++++++++++++++++ libresapi/src/webui-src/app/peers.js | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/libresapi/src/webui-src/app/menu.js b/libresapi/src/webui-src/app/menu.js index 73ea862b5..e01ec10c4 100644 --- a/libresapi/src/webui-src/app/menu.js +++ b/libresapi/src/webui-src/app/menu.js @@ -16,6 +16,13 @@ function buildmenu(menu, tagname, runstate, ignore){ && (menu.path === undefined || !menu.path.contains(":")) && (menu.show === undefined || menu.show) ) { + var name = menu.name; + if (menu.counter != undefined && menu.counterfnkt != undefined) { + var data=rs(menu.counter); + if (data != undefined) { + name += " (" + menu.counterfnkt(data) + ")"; + } + } if (menu.action === undefined) { return m(tagname , { onclick: function(){ @@ -23,9 +30,9 @@ function buildmenu(menu, tagname, runstate, ignore){ menu.path != undefined ? menu.path : "/" + menu.name ) } - }, menu.name); + }, name); } else { - return m(tagname, {onclick: function(){menu.action(rs,m)}}, menu.name); + return m(tagname, {onclick: function(){menu.action(rs,m)}}, name); } } } diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index 67e064e00..9da066caa 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -11,6 +11,22 @@ module.exports = { nodes: [ { name: "peers", runstate: "running_ok.*", + counter: "peers", + counterfnkt: function(data){ + var onlinecount = 0; + data.map(function(peer) { + var is_online = false; + peer.locations.map(function (location){ + if (location.is_online) { + is_online=true; + } + }); + if (is_online) { + onlinecount +=1; + } + }); + return onlinecount + "/" + data.length; + } }, { name: "addpeer", @@ -29,6 +45,10 @@ module.exports = { nodes: [ { name: "downloads", runstate: "running_ok.*", + counter: "transfers/downloads", + counterfnkt: function(data){ + return data.length; + } }, { name: "chat", diff --git a/libresapi/src/webui-src/app/peers.js b/libresapi/src/webui-src/app/peers.js index 763a3ca10..f22e96e46 100644 --- a/libresapi/src/webui-src/app/peers.js +++ b/libresapi/src/webui-src/app/peers.js @@ -5,7 +5,7 @@ var rs = require("retroshare"); module.exports = {view: function(){ var peers = rs("peers"); - console.log("peers:" + peers); + //console.log("peers:" + peers); //waiting for peerlist ... if(peers === undefined){ From f4c68a1016d2b08aa6426e6f0faa1bc689497220 Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 16 Jan 2016 20:22:49 +0100 Subject: [PATCH 18/64] webgui: first steps in chat --- libresapi/src/webui-src/app/_chat.sass | 6 +-- libresapi/src/webui-src/app/chat.js | 58 ++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/libresapi/src/webui-src/app/_chat.sass b/libresapi/src/webui-src/app/_chat.sass index f541f2f45..deee9ffa5 100644 --- a/libresapi/src/webui-src/app/_chat.sass +++ b/libresapi/src/webui-src/app/_chat.sass @@ -1,5 +1,5 @@ .chat - $color: lightblue + $color: black $header_height: 50px $left_width: 200px $right_width: 200px @@ -26,7 +26,7 @@ width: $left_width border-right: solid 1px gray box-sizing: border-box - background-color: lightgray + background-color: black &.right position: absolute top: $header_height @@ -59,7 +59,7 @@ width: $author_width top: 10px left: 0px - color: gray + color: white text-align: right &.when float: right diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index f91ab536c..b272c3fdd 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -1,6 +1,7 @@ "use strict"; var m = require("mithril"); +var rs = require("retroshare"); function msg(from, when, text){ return m(".chat.msg.container",[ @@ -10,18 +11,59 @@ function msg(from, when, text){ ]); } +function lobbies(){ + var lobs = rs("chat/lobbies"); + if (lobs === undefined) { + return m("div","loading ...") + }; + var dta = lobs.map(function (lobby){ + return m("div.btn",{ + title: "topic: " + lobby.topic + "\n" + + "subscribed: " + lobby.subscribed, + onclick: function(){ + m.route("/chat?lobby=" + lobby.chat_id); + } + },lobby.name + (lobby.unread_msg_count > 0 ? ("(" + lobby.unread_msg_count + ")") : "")); + }); + return dta; + +} + +function lobby(lobbyid){ + var msgs = rs("chat/messages/" + lobbyid); + var info = rs("chat/info/" + lobbyid); + if (msgs === undefined || info === undefined) { + return m("div","waiting ..."); + } + return msgs.map(function(item){ + var d = new Date(new Number(item.send_time)*1000); + return msg(item.author_name, d.toLocaleDateString() + " " + d.toLocaleTimeString(),item.msg); + }); +} + module.exports = { - view: function(){ + frame: function(content, right){ return m(".chat.container", [ m(".chat.header", "headerbar"), - m(".chat.left", "left"), - m(".chat.right", "right"), - m(".chat.middle", [ - msg("Andi", "now", "Hallo"), - msg("Test", "now", "Hallo back"), - msg("Somebody", "now", "Hallo back, sfjhfu dsjkchsd wehfskf sdjksdf sjdnfkjsf sdjkfhjksdf jksdfjksdnf sjdefhsjkn cesjdfhsjk fskldcjhsklc ksdj"), + m(".chat.left", [ + m("div.chat.header","lobbies:"), + m("hr"), + lobbies(), ]), + m(".chat.right", right), + m(".chat.middle", content), m(".chat.clear", ""), ]); + }, + view: function(){ + var lobbyid = m.route.param("lobby"); + if (lobbyid != undefined ) { + return this.frame( + lobby(lobbyid),[] + ); + }; + return this.frame( + m("div", "please select lobby"), + m("div","right")); } -} \ No newline at end of file +} From e944ab6045523d0b8ae819ffe5fdbf6125347d66 Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 24 Jan 2016 18:03:48 +0100 Subject: [PATCH 19/64] webui: createlogin for existing pgp-key --- libresapi/src/webui-src/app/accountselect.js | 11 +- libresapi/src/webui-src/app/assets/index.html | 3 + libresapi/src/webui-src/app/createlogin.js | 110 ++++++++++++++++++ libresapi/src/webui-src/app/main.js | 19 ++- libresapi/src/webui-src/app/menudef.js | 6 + libresapi/src/webui-src/app/retroshare.js | 14 +++ 6 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 libresapi/src/webui-src/app/createlogin.js diff --git a/libresapi/src/webui-src/app/accountselect.js b/libresapi/src/webui-src/app/accountselect.js index 3def5da58..1b989579c 100644 --- a/libresapi/src/webui-src/app/accountselect.js +++ b/libresapi/src/webui-src/app/accountselect.js @@ -32,10 +32,15 @@ module.exports = {view: function(){ accounts.map(function(account){ accountMap.set(account.id,account); return [ - m("div.btn2", {onclick: m.withAttr("account", selAccount), account:account.id }, account.location + " (" + account.name + ")"), + m("div.btn2", { + onclick: m.withAttr("account", selAccount), + account:account.id + }, + account.location + " (" + account.name + ")"), m("br") - ] - })]); + ] + }) + ]); } else { return m("div", "logging in ..." ); } diff --git a/libresapi/src/webui-src/app/assets/index.html b/libresapi/src/webui-src/app/assets/index.html index f4aacba35..438ac876d 100644 --- a/libresapi/src/webui-src/app/assets/index.html +++ b/libresapi/src/webui-src/app/assets/index.html @@ -14,6 +14,9 @@ var ui = require("main"); var main = document.getElementById("main"); ui.init(main); + if (m.initControl != undefined) { + m.initControl.focus(); + } } diff --git a/libresapi/src/webui-src/app/createlogin.js b/libresapi/src/webui-src/app/createlogin.js new file mode 100644 index 000000000..1161b7656 --- /dev/null +++ b/libresapi/src/webui-src/app/createlogin.js @@ -0,0 +1,110 @@ +var m = require("mithril"); +var rs = require("retroshare"); + +var locationName = ""; +var password =""; + +function listprofiles(){ + var locations = rs("control/locations"); + var knownProfileIds = []; + var result = []; + if(locations === undefined || locations == null){ + return m("div", "profiles: waiting_server"); + } + locations.map(function(location) { + if (knownProfileIds.indexOf(location.pgp_id)<0){ + knownProfileIds.push(location.pgp_id); + result.push(m( + "div.btn2",{ + onclick: function(){ + m.route("/createlogin",{ + id: location.pgp_id, + name: location.name, + }) + } + }, + location.name + )); + } + }); + + return result; +} + +function setLocationName(location) { + locationName = location; +} + +function setPasswd(passwd) { + password = passwd; +} + + +function createLocation(loc) { + rs.request("control/create_location",loc,function(data){ + m.route("/accountselect", {}); + }); + m.route("/createlogin",{state:wait}); +} + + +module.exports = { + view: function(){ + var profile = m.route.param("id"); + var state = m.route.param("state"); + var profname = m.route.param("name"); + var hidden = m.route.param("hidden"); + if (state == "wait"){ + return m("div","waiting ..."); + } else if (profile === undefined) { + return m("div",[ + m("h2","create login - Step 1 / 2: select profile(PGP-ID)"), + m("hr"), + m("div.btn2",{} ,""), + m("div.btn2",{} ,""), + listprofiles() + ]); + }; + m.initControl = "txtpasswd"; + return m("div",[ + m("h2","create login - Step 2 / 2: create location"), + m("h3","- for " + profname + " (" +profile + ")"), + m("hr"), + m("h2","enter password:"), + m("input", { + id: "txtpasswd", + type:"password", + onchange: m.withAttr("value",setPasswd), + onkeydown: function(event){ + if (event.keyCode == 13){ + setPasswd(this.value); + document.getElementById("txtlocation").focus(); + } + } + }), + m("h2","location name:"), + m("input",{ + id: "txtlocation", + type:"text", + onchange:m.withAttr("value", setLocationName), + onkeydown: function(event){ + if (event.keyCode == 13){ + createLocation({ + ssl_name: this.value, + pgp_id: profile, + pgp_password: password, + }); + } + }, + }), + m("br"), + m("input",{ + type: "button", + onclick: function(){ + createLocation(locationName); + }, + value: "create location", + }), + ]); + } +} diff --git a/libresapi/src/webui-src/app/main.js b/libresapi/src/webui-src/app/main.js index 32ea255a0..5483609d0 100644 --- a/libresapi/src/webui-src/app/main.js +++ b/libresapi/src/webui-src/app/main.js @@ -27,12 +27,27 @@ function Page(content, runst){ } else if (runstate.runstate == null){ return m("h2", "server down"); } else if (needpasswd != undefined && needpasswd.want_password === true){ + m.initControl = "txtpasswd"; return m("div",[ m("h2","password required"), m("h3",needpasswd.key_name), - m("input",{type:"password", onchange:m.withAttr("value", setPasswd)}), + m("input",{ + id: "txtpasswd", + type:"password", + onchange:m.withAttr("value", setPasswd), + onkeydown: function(event){ + if (event.keyCode == 13){ + setPasswd(this.value); + sendPassword(needpasswd); + } + } + }), m("br"), - m("input[type=button][value=send password]",{onclick: function(){sendPassword(needpasswd);}}), + m("input[type=button][value=send password]",{ + onclick: function(){ + sendPassword(needpasswd); + } + }), ]); } else { if ("waiting_init|waiting_startup".match(runstate.runstate)) { diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index 9da066caa..cac6299a0 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -8,6 +8,12 @@ module.exports = { nodes: [ module: "accountselect", runstate: "waiting_account_select", }, + { + name: "create login", + path: "/createlogin", + module: "createlogin", + runstate: "waiting_account_select", + }, { name: "peers", runstate: "running_ok.*", diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index 4bedf306d..6c1763573 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -88,6 +88,7 @@ function check_for_changes(){ m.sync(requests).then(function trigger_render(){ m.startComputation(); m.endComputation(); + checkFocus(); setTimeout(check_for_changes, 500); }); } @@ -148,10 +149,23 @@ function schedule_request_missing(){ m.sync(requests).then(function trigger_render(){ m.startComputation(); m.endComputation(); + checkFocus(); }); }); } +function checkFocus(){ + if (m.initControl != undefined) { + var ctrl = document.getElementById(m.initControl); + if (ctrl!= null) { + ctrl.focus(); + m.initControl = undefined; + } else { + console.log("focus-control '" + m.initControl + "' not found!") + } + } +} + // called every time, rs or rs.request failed, only response or value is set function requestFail(path, response, value) { rs.error = "error on " + path; From 2fc35cf7074af9576d3e3ab907d152dd33160a1d Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 24 Jan 2016 19:32:15 +0100 Subject: [PATCH 20/64] webui: createlogin with new pgpid --- libresapi/src/webui-src/app/createlogin.js | 203 ++++++++++++++++----- 1 file changed, 158 insertions(+), 45 deletions(-) diff --git a/libresapi/src/webui-src/app/createlogin.js b/libresapi/src/webui-src/app/createlogin.js index 1161b7656..7e69871a7 100644 --- a/libresapi/src/webui-src/app/createlogin.js +++ b/libresapi/src/webui-src/app/createlogin.js @@ -3,6 +3,8 @@ var rs = require("retroshare"); var locationName = ""; var password =""; +var ssl_name = ""; +var newName = ""; function listprofiles(){ var locations = rs("control/locations"); @@ -39,8 +41,47 @@ function setPasswd(passwd) { password = passwd; } +function setSslName(ssl) { + ssl_name = ssl; +} -function createLocation(loc) { +function setNewName(name) { + newName = name; +} + +function checkpasswd(){ + var status = ""; + var color = "red"; + var lbl = document.getElementById("lblpwdinfo"); + var passwd2 = document.getElementById("txtpasswd2").value; + if (passwd2 == password && passwd2 != "") { + color = "lime"; + status = "password ok"; + } else if (passwd2 == "") { + color = "yellow"; + status = "password required"; + } else { + color = "red"; + status = "passwords don't match"; + } + lbl.textContent = status; + lbl.style.color=color; +} + + +function createLocation() { + var profile = m.route.param("id"); + var profname = m.route.param("name"); + + var loc ={ + ssl_name: document.getElementById("txtlocation").value, + pgp_password: password, + }; + if (profile != undefined) { + loc.pgp_id= profile; + } else { + loc.pgp_name = newName; + }; rs.request("control/create_location",loc,function(data){ m.route("/accountselect", {}); }); @@ -56,55 +97,127 @@ module.exports = { var hidden = m.route.param("hidden"); if (state == "wait"){ return m("div","waiting ..."); - } else if (profile === undefined) { + } if (state == "newid"){ + m.initControl = "txtnewname"; + return m("div",[ + m("h2","create login - Step 2 / 2: create location"), + m("h3","- for new profile "), + m("hr"), + m("h2","PGP-profile name:"), + m("input",{ + id: "txtnewname", + type:"text", + onchange:m.withAttr("value", setNewName), + onkeydown: function(event){ + if (event.keyCode == 13){ + document.getElementById("txtpasswd").focus(); + } + }, + }), + m("h2","enter password:"), + m("input", { + id: "txtpasswd", + type:"password", + onchange: m.withAttr("value",setPasswd), + onkeydown: function(event){ + if (event.keyCode == 13){ + setPasswd(this.value); + document.getElementById("txtpasswd2").focus(); + }; + checkpasswd; + } + }), + m("h2", "re-enter password:"), + m("input", { + id: "txtpasswd2", + type:"password", + onfocus: checkpasswd, + onchange: checkpasswd, + onkeyup: function(event){ + if (event.keyCode == 13){ + document.getElementById("txtlocation").focus(); + } + checkpasswd(); + } + }), + m("h3",{ + id: "lblpwdinfo", + style:"color:yellow", + }, "password required"), + m("h2","location name:"), + m("input",{ + id: "txtlocation", + type:"text", + onchange:m.withAttr("value", setLocationName), + onkeydown: function(event){ + if (event.keyCode == 13){ + setSslName(this.value); + createLocation(); + } + }, + }), + m("br"), + m("input",{ + type: "button", + onclick: function(){ + createLocation(); + }, + value: "create location", + }), + ]); + } else if (profile != undefined) { + m.initControl = "txtpasswd"; + return m("div",[ + m("h2","create login - Step 2 / 2: create location"), + m("h3","- for " + profname + " (" +profile + ")"), + m("hr"), + m("h2","enter password:"), + m("input", { + id: "txtpasswd", + type:"password", + onchange: m.withAttr("value",setPasswd), + onkeydown: function(event){ + if (event.keyCode == 13){ + setPasswd(this.value); + document.getElementById("txtlocation").focus(); + } + } + }), + m("h2","location name:"), + m("input",{ + id: "txtlocation", + type:"text", + onchange:m.withAttr("value", setLocationName), + onkeydown: function(event){ + if (event.keyCode == 13){ + setSslName(this.value); + createLocation(); + } + }, + }), + m("br"), + m("input",{ + type: "button", + onclick: function(){ + createLocation(); + }, + value: "create location", + }), + ]); + } else { return m("div",[ m("h2","create login - Step 1 / 2: select profile(PGP-ID)"), m("hr"), - m("div.btn2",{} ,""), - m("div.btn2",{} ,""), + m("div.btn2",{ + onclick: function(){ + m.route("/createlogin", {state: "newid"}); + }, + } ,""), + m("div.btn2",{ + } ,""), listprofiles() ]); }; - m.initControl = "txtpasswd"; - return m("div",[ - m("h2","create login - Step 2 / 2: create location"), - m("h3","- for " + profname + " (" +profile + ")"), - m("hr"), - m("h2","enter password:"), - m("input", { - id: "txtpasswd", - type:"password", - onchange: m.withAttr("value",setPasswd), - onkeydown: function(event){ - if (event.keyCode == 13){ - setPasswd(this.value); - document.getElementById("txtlocation").focus(); - } - } - }), - m("h2","location name:"), - m("input",{ - id: "txtlocation", - type:"text", - onchange:m.withAttr("value", setLocationName), - onkeydown: function(event){ - if (event.keyCode == 13){ - createLocation({ - ssl_name: this.value, - pgp_id: profile, - pgp_password: password, - }); - } - }, - }), - m("br"), - m("input",{ - type: "button", - onclick: function(){ - createLocation(locationName); - }, - value: "create location", - }), - ]); + } } From 2292b7c998b4eb6e63b7d58fe24844272d0e4c7e Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 24 Jan 2016 20:07:22 +0100 Subject: [PATCH 21/64] webui: createlogin with import pgp-key --- libresapi/src/webui-src/app/createlogin.js | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/libresapi/src/webui-src/app/createlogin.js b/libresapi/src/webui-src/app/createlogin.js index 7e69871a7..1cfc9bdda 100644 --- a/libresapi/src/webui-src/app/createlogin.js +++ b/libresapi/src/webui-src/app/createlogin.js @@ -88,6 +88,36 @@ function createLocation() { m.route("/createlogin",{state:wait}); } +function certDrop(event) +{ + console.log("onDrop()"); + console.log(event.dataTransfer.files); + event.preventDefault(); + + var reader = new FileReader(); + + var widget = this; + + reader.onload = function(evt) { + console.log("onDrop(): file loaded"); + rs.request( + "control/import_pgp",{ + key_string:evt.target.result, + }, importCallback); + }; + reader.readAsText(event.dataTransfer.files[0]); + m.route("/createlogin",{state:"waiting"}); +} + +function importCallback(resp) +{ + console.log("importCallback()" + resp); + m.route("/createlogin",{ + id:resp.pgp_id, + name:"", + }); +} + module.exports = { view: function(){ @@ -214,6 +244,11 @@ module.exports = { }, } ,""), m("div.btn2",{ + ondragover:function(event){ + /*important: block default event*/ + event.preventDefault(); + }, + ondrop: certDrop, } ,""), listprofiles() ]); From b2b96c097faa05c2b45849860b3938191bba9131 Mon Sep 17 00:00:00 2001 From: zeners Date: Tue, 26 Jan 2016 11:55:48 +0100 Subject: [PATCH 22/64] webui: small main reorganisation --- libresapi/src/webui-src/app/main.js | 16 +++++++++++----- libresapi/src/webui-src/app/retroshare.js | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/libresapi/src/webui-src/app/main.js b/libresapi/src/webui-src/app/main.js index 5483609d0..cb628f8fc 100644 --- a/libresapi/src/webui-src/app/main.js +++ b/libresapi/src/webui-src/app/main.js @@ -17,7 +17,14 @@ function sendPassword(data) { }); } -function Page(content, runst){ +function Page(menu){ + this.menu = menu; + this.module = (menu.module != undefined) ? menu.module : menu.name; + this.path = menu.path != undefined ? menu.path : ("/" + menu.name); + + var runst = menu.runstate; + var content = require(module); + this.view = function(){ var runstate = rs("control/runstate"); var needpasswd = rs("control/password"); @@ -85,10 +92,9 @@ module.exports = { menudef.nodes.map(function(menu){ if (menu.action === undefined) { - var path = menu.path != undefined ? menu.path : ("/" + menu.name); - var module = menu.module != undefined ? menu.module : menu.name - console.log("adding route " + menu.name + " for " + path + " with module " + module); - maps[path] = new Page(require(module), menu.runstate); + var p = new Page(menu) + console.log("adding route " + menu.name + " for " + p.path + " with module " + p.module); + maps[p.path] = p; } }); m.route.mode = "hash"; diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index 6c1763573..089d1f7ce 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -161,7 +161,8 @@ function checkFocus(){ ctrl.focus(); m.initControl = undefined; } else { - console.log("focus-control '" + m.initControl + "' not found!") + console.log("focus-control '" + m.initControl + "' not found!"); + m.initControl = undefined; } } } From 9e1fc59c6e868ca74f67753d63aa388f2908713d Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 31 Jan 2016 13:37:55 +0100 Subject: [PATCH 23/64] webgui: fixed menu-display --- libresapi/src/webui-src/app/main.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libresapi/src/webui-src/app/main.js b/libresapi/src/webui-src/app/main.js index cb628f8fc..6bee57a36 100644 --- a/libresapi/src/webui-src/app/main.js +++ b/libresapi/src/webui-src/app/main.js @@ -23,7 +23,8 @@ function Page(menu){ this.path = menu.path != undefined ? menu.path : ("/" + menu.name); var runst = menu.runstate; - var content = require(module); + var content = require(this.module); + var mm = require("menu"); this.view = function(){ var runstate = rs("control/runstate"); @@ -62,7 +63,7 @@ function Page(menu){ } else if("waiting_account_select|running_ok.*".match(runstate.runstate)) { if (runst === undefined || runst.match(runstate.runstate)) { return m("div", [ - m("div", menu.view()), + m("div", mm.view()), m("hr"), m("div", content) ]); @@ -70,7 +71,7 @@ function Page(menu){ // funktion currently not available m.route("/"); return m("div", [ - m("div", menu.view()), + m("div", mm.view()), m("hr"), m("div", require("home").view()) ]); From b649088b20a637216a7203797ef992c98f8f50ef Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 31 Jan 2016 13:24:47 +0100 Subject: [PATCH 24/64] webui: added todo --- libresapi/src/webui-src/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libresapi/src/webui-src/README.md b/libresapi/src/webui-src/README.md index 60934e235..824ce1814 100644 --- a/libresapi/src/webui-src/README.md +++ b/libresapi/src/webui-src/README.md @@ -47,4 +47,5 @@ should provide forward, backward and follow-list-end [ ] backend: chat typing notifications [ ] make routines to handle retroshare links [ ] backend: edit shared folders -[ ] backend: view shared files \ No newline at end of file +[ ] backend: view shared files +[ ] redirect if a url is not usable in the current runstate (e.g. redirect from login page to home page, after login) From 22afc8e67bade9d7abd6ff1bcf94db579ec18f26 Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 31 Jan 2016 13:25:28 +0100 Subject: [PATCH 25/64] libresapi: fix empty peers list --- libresapi/src/api/PeersHandler.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libresapi/src/api/PeersHandler.cpp b/libresapi/src/api/PeersHandler.cpp index 9cdf29897..9894836e8 100644 --- a/libresapi/src/api/PeersHandler.cpp +++ b/libresapi/src/api/PeersHandler.cpp @@ -193,6 +193,8 @@ void PeersHandler::handleWildcard(Request &req, Response &resp) ok &= mRsPeers->getPeerDetails(*lit, details); detailsVec.push_back(details); } + // mark response as list, in case it is empty + resp.mDataStream.getStreamToMember(); for(std::list::iterator lit = identities.begin(); lit != identities.end(); ++lit) { // if no own ssl id is known, then hide the own id from the friendslist From 284e5a9ed04cf75204c5194037496fa88b1b1468 Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 31 Jan 2016 15:58:20 +0100 Subject: [PATCH 26/64] webui: reorganize counterfnkt --- libresapi/src/webui-src/app/menu.js | 9 +++------ libresapi/src/webui-src/app/menudef.js | 13 ++++++------- libresapi/src/webui-src/app/retroshare.js | 10 ++++++++++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/libresapi/src/webui-src/app/menu.js b/libresapi/src/webui-src/app/menu.js index e01ec10c4..c4f5dec0c 100644 --- a/libresapi/src/webui-src/app/menu.js +++ b/libresapi/src/webui-src/app/menu.js @@ -17,11 +17,8 @@ function buildmenu(menu, tagname, runstate, ignore){ && (menu.show === undefined || menu.show) ) { var name = menu.name; - if (menu.counter != undefined && menu.counterfnkt != undefined) { - var data=rs(menu.counter); - if (data != undefined) { - name += " (" + menu.counterfnkt(data) + ")"; - } + if (menu.counter != undefined) { + name += menu.counter(); } if (menu.action === undefined) { return m(tagname , { @@ -32,7 +29,7 @@ function buildmenu(menu, tagname, runstate, ignore){ } }, name); } else { - return m(tagname, {onclick: function(){menu.action(rs,m)}}, name); + return m(tagname, {onclick: function(){menu.action(m)}}, name); } } } diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index cac6299a0..9ac012e62 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -1,3 +1,4 @@ +rs=require("retroshare"); module.exports = { nodes: [ { name: "home", @@ -17,8 +18,7 @@ module.exports = { nodes: [ { name: "peers", runstate: "running_ok.*", - counter: "peers", - counterfnkt: function(data){ + counter: rs.counting("peers", function(data){ var onlinecount = 0; data.map(function(peer) { var is_online = false; @@ -32,7 +32,7 @@ module.exports = { nodes: [ } }); return onlinecount + "/" + data.length; - } + }) }, { name: "addpeer", @@ -51,10 +51,9 @@ module.exports = { nodes: [ { name: "downloads", runstate: "running_ok.*", - counter: "transfers/downloads", - counterfnkt: function(data){ + counter: rs.counting("transfers/downloads", function(data) { return data.length; - } + }) }, { name: "chat", @@ -63,7 +62,7 @@ module.exports = { nodes: [ { name: "shutdown", runstate: "running_ok|waiting_account_select", - action: function(rs, m){ + action: function(m){ rs.request("control/shutdown",null,function(){ rs("control/runstate").runstate=null; rs.forceUpdate("control/runstate"); diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index 089d1f7ce..d5b71cd47 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -241,3 +241,13 @@ rs.apiurl = function(path) { return api_url + path; } +rs.counting = function(path, counterfnkt) { + return function () { + var data=rs(path); + if (data != undefined) { + return " (" + counterfnkt(data) + ")"; + } + return ""; + } +} + From 94b02943e6d5fa4bc783e9963e668a756169dde7 Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 31 Jan 2016 16:04:34 +0100 Subject: [PATCH 27/64] libresapi: added identity/create_identity --- libresapi/src/api/IdentityHandler.cpp | 58 +++++++++++++++++++++++++++ libresapi/src/api/IdentityHandler.h | 1 + 2 files changed, 59 insertions(+) diff --git a/libresapi/src/api/IdentityHandler.cpp b/libresapi/src/api/IdentityHandler.cpp index 26578380f..f5128561a 100644 --- a/libresapi/src/api/IdentityHandler.cpp +++ b/libresapi/src/api/IdentityHandler.cpp @@ -41,11 +41,64 @@ protected: }; +class CreateIdentityTask: public GxsResponseTask +{ +public: + CreateIdentityTask(RsIdentity* idservice): + GxsResponseTask(idservice, idservice->getTokenService()), mState(BEGIN), mToken(0), mRsIdentity(idservice){} +private: + enum State {BEGIN, WAIT_ACKN, WAIT_ID}; + State mState; + uint32_t mToken; + RsIdentity* mRsIdentity; + RsGxsId mId; +protected: + virtual void gxsDoWork(Request &req, Response &resp) + { + switch(mState) + { + case BEGIN:{ + RsIdentityParameters params; + req.mStream << makeKeyValueReference("name", params.nickname) << makeKeyValueReference("pgp_linked", params.isPgpLinked); + + if(params.nickname == "") + { + resp.setFail("name can't be empty"); + done(); + return; + } + mRsIdentity->createIdentity(mToken, params); + addWaitingToken(mToken); + mState = WAIT_ACKN; + break; + } + case WAIT_ACKN:{ + RsGxsGroupId grpId; + if(!mRsIdentity->acknowledgeGrp(mToken, grpId)) + { + resp.setFail("acknowledge of group id failed"); + done(); + return; + } + mId = RsGxsId(grpId); + requestGxsId(mId); + mState = WAIT_ID; + break; + } + case WAIT_ID: + streamGxsId(mId, resp.mDataStream); + resp.setOk(); + done(); + } + } +}; + IdentityHandler::IdentityHandler(RsIdentity *identity): mRsIdentity(identity) { addResourceHandler("*", this, &IdentityHandler::handleWildcard); addResourceHandler("own", this, &IdentityHandler::handleOwn); + addResourceHandler("create_identity", this, &IdentityHandler::handleCreateIdentity); } void IdentityHandler::handleWildcard(Request &req, Response &resp) @@ -134,4 +187,9 @@ ResponseTask* IdentityHandler::handleOwn(Request &req, Response &resp) return 0; } +ResponseTask* IdentityHandler::handleCreateIdentity(Request &req, Response &resp) +{ + return new CreateIdentityTask(mRsIdentity); +} + } // namespace resource_api diff --git a/libresapi/src/api/IdentityHandler.h b/libresapi/src/api/IdentityHandler.h index f858e9594..8d110727e 100644 --- a/libresapi/src/api/IdentityHandler.h +++ b/libresapi/src/api/IdentityHandler.h @@ -15,5 +15,6 @@ private: RsIdentity* mRsIdentity; void handleWildcard(Request& req, Response& resp); ResponseTask *handleOwn(Request& req, Response& resp); + ResponseTask *handleCreateIdentity(Request& req, Response& resp); }; } // namespace resource_api From 57d1d0868a12389f947579dc0382432f6ca8f27f Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 31 Jan 2016 18:16:26 +0100 Subject: [PATCH 28/64] webui: adding and listing identities --- libresapi/src/webui-src/app/addidentity.js | 51 ++++++++++++++++++++++ libresapi/src/webui-src/app/addpeer.js | 3 +- libresapi/src/webui-src/app/createlogin.js | 8 +--- libresapi/src/webui-src/app/identities.js | 21 +++++++++ libresapi/src/webui-src/app/menudef.js | 18 ++++++-- libresapi/src/webui-src/app/retroshare.js | 14 ++++++ libresapi/src/webui-src/app/waiting.js | 8 ++++ 7 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 libresapi/src/webui-src/app/addidentity.js create mode 100644 libresapi/src/webui-src/app/identities.js create mode 100644 libresapi/src/webui-src/app/waiting.js diff --git a/libresapi/src/webui-src/app/addidentity.js b/libresapi/src/webui-src/app/addidentity.js new file mode 100644 index 000000000..907209d0b --- /dev/null +++ b/libresapi/src/webui-src/app/addidentity.js @@ -0,0 +1,51 @@ +"use strict"; + +var m = require("mithril"); +var rs = require("retroshare"); + +function createidentity(){ + var data = { + name: document.getElementById("txtname").value, + pgp_linked: document.getElementById("chklinked").checked, + }; + m.route("/waiting"); + rs.request("identity/create_identity",data, function(){ + m.route("/identities"); + }) +} + +module.exports = {view: function(){ + m.initControl = "txtname"; + return m("div", + m("h2","create identity"), + m("hr"), + m("h3","name"), + m("input", { + type: "text", + id: "txtname", + /* + onkeydown: function(event){ + if (event.keyCode == 13){ + setPasswd(this.value); + sendPassword(needpasswd); + } + } + */ + }), + m("b","linked with pgp-id: "), + m("input", { + type: "checkbox", + id: "chklinked", + style: { + fontweight:"bold", + width: "0%", + } + }), + m("p"," "), + m("input.btn2", { + onclick: createidentity, + type: "button", + value: "create new identity", + }) + ) +}} diff --git a/libresapi/src/webui-src/app/addpeer.js b/libresapi/src/webui-src/app/addpeer.js index 5f8806ccf..80542f412 100644 --- a/libresapi/src/webui-src/app/addpeer.js +++ b/libresapi/src/webui-src/app/addpeer.js @@ -61,7 +61,7 @@ module.exports = { } }; return m("div",[ - m("h2","add new friend (Step 2/3)"), + m("h2","add new friend (Step 3/3)"), m("p","Do you want to add " + m.route.param("name") + " (" + m.route.param("location") + ")" @@ -79,6 +79,7 @@ module.exports = { }), "Auto download recommended files from this node", m("div.btn2",{ onclick: function(){ + m.route("/waiting"); rs.request("peers",result,function(data, responsetoken){ m.route("/peers"); }) diff --git a/libresapi/src/webui-src/app/createlogin.js b/libresapi/src/webui-src/app/createlogin.js index 1cfc9bdda..e1c147250 100644 --- a/libresapi/src/webui-src/app/createlogin.js +++ b/libresapi/src/webui-src/app/createlogin.js @@ -189,9 +189,7 @@ module.exports = { m("br"), m("input",{ type: "button", - onclick: function(){ - createLocation(); - }, + onclick: createLocation, value: "create location", }), ]); @@ -228,9 +226,7 @@ module.exports = { m("br"), m("input",{ type: "button", - onclick: function(){ - createLocation(); - }, + onclick: createLocation, value: "create location", }), ]); diff --git a/libresapi/src/webui-src/app/identities.js b/libresapi/src/webui-src/app/identities.js new file mode 100644 index 000000000..68f14c154 --- /dev/null +++ b/libresapi/src/webui-src/app/identities.js @@ -0,0 +1,21 @@ +"use strict"; + +var m = require("mithril"); +var rs = require("retroshare"); + +module.exports = {view: function(){ + return m("div",[ + m("h2","identities"), + m("hr"), + m("div.btn2", { + onclick: function (){ + m.route("/addidentity"); + } + },"< create new identity >"), + m("ul", + rs.list("identity/own", function(item){ + return m("li",[m("h2",item.name)]); + }) + ) + ]); +}} diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index 9ac012e62..e3cc893ea 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -39,6 +39,16 @@ module.exports = { nodes: [ runstate: "running_ok.*", show: false, }, + { + name: "identities", + runstate: "running_ok.*", + counter: rs.counting("identity/own"), + }, + { + name: "addidentity", + runstate: "running_ok.*", + show: false, + }, { name:"searchresult", path: "/search/:id", @@ -51,9 +61,7 @@ module.exports = { nodes: [ { name: "downloads", runstate: "running_ok.*", - counter: rs.counting("transfers/downloads", function(data) { - return data.length; - }) + counter: rs.counting("transfers/downloads") }, { name: "chat", @@ -70,5 +78,9 @@ module.exports = { nodes: [ }); } }, + { + name: "waiting", + show: false, + }, ] } diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index d5b71cd47..e955bbb13 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -203,6 +203,7 @@ function rs(path, args, callback, options){ module.exports = rs; +// single request for action rs.request=function(path, args, callback, options){ if (options === undefined) { options = {}; @@ -226,6 +227,7 @@ rs.request=function(path, args, callback, options){ return req; }; +// force reload for path rs.forceUpdate = function(path){ cache[path].requested=false; } @@ -241,13 +243,25 @@ rs.apiurl = function(path) { return api_url + path; } +// counting in menu rs.counting = function(path, counterfnkt) { return function () { var data=rs(path); if (data != undefined) { + if (counterfnkt === undefined) { + return " (" + data.length + ")"; + } return " (" + counterfnkt(data) + ")"; } return ""; } } +// listing data-elements +rs.list = function(path, buildfktn){ + var list = rs(path); + if (list === undefined) { + return "< waiting for server ... >" + } + return list.map(buildfktn); +} diff --git a/libresapi/src/webui-src/app/waiting.js b/libresapi/src/webui-src/app/waiting.js new file mode 100644 index 000000000..cd56348c9 --- /dev/null +++ b/libresapi/src/webui-src/app/waiting.js @@ -0,0 +1,8 @@ +"use strict"; + +var m = require("mithril"); +var rs = require("retroshare"); + +module.exports = {view: function(){ + return m("h2","please wait ..."); +}} From f4f37cc354c7f33bcd08d46d147057769dc27922 Mon Sep 17 00:00:00 2001 From: electron128 Date: Sat, 6 Feb 2016 10:42:17 +0100 Subject: [PATCH 29/64] webui: added todo --- libresapi/src/webui-src/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/libresapi/src/webui-src/README.md b/libresapi/src/webui-src/README.md index 824ce1814..90548880f 100644 --- a/libresapi/src/webui-src/README.md +++ b/libresapi/src/webui-src/README.md @@ -49,3 +49,4 @@ should provide forward, backward and follow-list-end [ ] backend: edit shared folders [ ] backend: view shared files [ ] redirect if a url is not usable in the current runstate (e.g. redirect from login page to home page, after login) +[ ] sort friendslist From 116513963f60d1a04c7c8e66b04322c2e178c237 Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 7 Feb 2016 11:11:45 +0100 Subject: [PATCH 30/64] libresapi: added statetoken to identity handler responses --- libresapi/src/api/ApiServer.cpp | 2 +- libresapi/src/api/GetPluginInterfaces.h | 1 + libresapi/src/api/IdentityHandler.cpp | 43 +++++++++++++++++++++---- libresapi/src/api/IdentityHandler.h | 22 +++++++++++-- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/libresapi/src/api/ApiServer.cpp b/libresapi/src/api/ApiServer.cpp index fccc3cc72..176e6290e 100644 --- a/libresapi/src/api/ApiServer.cpp +++ b/libresapi/src/api/ApiServer.cpp @@ -226,7 +226,7 @@ class ApiServerMainModules public: ApiServerMainModules(ResourceRouter& router, StateTokenServer* sts, const RsPlugInInterfaces &ifaces): mPeersHandler(sts, ifaces.mNotify, ifaces.mPeers, ifaces.mMsgs), - mIdentityHandler(ifaces.mIdentity), + mIdentityHandler(sts, ifaces.mNotify, ifaces.mIdentity), mForumHandler(ifaces.mGxsForums), mServiceControlHandler(ifaces.mServiceControl), mFileSearchHandler(sts, ifaces.mNotify, ifaces.mTurtle, ifaces.mFiles), diff --git a/libresapi/src/api/GetPluginInterfaces.h b/libresapi/src/api/GetPluginInterfaces.h index 053023144..73cca040a 100644 --- a/libresapi/src/api/GetPluginInterfaces.h +++ b/libresapi/src/api/GetPluginInterfaces.h @@ -4,6 +4,7 @@ class RsPlugInInterfaces; namespace resource_api{ +// populates the given RsPlugInInterfaces object with pointers from gloabl variables like rsPeers, rsMsgs, rsFiles... bool getPluginInterfaces(RsPlugInInterfaces& interfaces); } // namespace resource_api diff --git a/libresapi/src/api/IdentityHandler.cpp b/libresapi/src/api/IdentityHandler.cpp index f5128561a..1902df295 100644 --- a/libresapi/src/api/IdentityHandler.cpp +++ b/libresapi/src/api/IdentityHandler.cpp @@ -16,17 +16,18 @@ namespace resource_api class SendIdentitiesListTask: public GxsResponseTask { public: - SendIdentitiesListTask(RsIdentity* idservice, std::list ids): - GxsResponseTask(idservice, 0) + SendIdentitiesListTask(RsIdentity* idservice, std::list ids, StateToken state): + GxsResponseTask(idservice, 0), mStateToken(state) { for(std::list::iterator vit = ids.begin(); vit != ids.end(); ++vit) { requestGxsId(*vit); - mIds.push_back(*vit);// convert fro list to vector + mIds.push_back(*vit);// convert from list to vector } } private: std::vector mIds; + StateToken mStateToken; protected: virtual void gxsDoWork(Request &req, Response &resp) { @@ -35,6 +36,7 @@ protected: { streamGxsId(*vit, resp.mDataStream.getStreamToMember()); } + resp.mStateToken = mStateToken; resp.setOk(); done(); } @@ -93,20 +95,39 @@ protected: } }; -IdentityHandler::IdentityHandler(RsIdentity *identity): - mRsIdentity(identity) +IdentityHandler::IdentityHandler(StateTokenServer *sts, RsNotify *notify, RsIdentity *identity): + mStateTokenServer(sts), mNotify(notify), mRsIdentity(identity), + mMtx("IdentityHandler Mtx"), mStateToken(sts->getNewToken()) { + mNotify->registerNotifyClient(this); + addResourceHandler("*", this, &IdentityHandler::handleWildcard); addResourceHandler("own", this, &IdentityHandler::handleOwn); addResourceHandler("create_identity", this, &IdentityHandler::handleCreateIdentity); } +IdentityHandler::~IdentityHandler() +{ + mNotify->unregisterNotifyClient(this); +} + +void IdentityHandler::notifyGxsChange(const RsGxsChanges &changes) +{ + RS_STACK_MUTEX(mMtx); // ********** LOCKED ********** + // if changes come from identity service, invalidate own state token + if(changes.mService == mRsIdentity->getTokenService()) + { + mStateTokenServer->replaceToken(mStateToken); + } +} + void IdentityHandler::handleWildcard(Request &req, Response &resp) { bool ok = true; if(req.isPut()) { +#ifdef REMOVE RsIdentityParameters params; req.mStream << makeKeyValueReference("name", params.nickname); if(req.mStream.isOK()) @@ -120,9 +141,14 @@ void IdentityHandler::handleWildcard(Request &req, Response &resp) { ok = false; } +#endif } else { + { + RS_STACK_MUTEX(mMtx); // ********** LOCKED ********** + resp.mStateToken = mStateToken; + } RsTokReqOptions opts; opts.mReqType = GXS_REQUEST_TYPE_GROUP_DATA; uint32_t token; @@ -179,9 +205,14 @@ void IdentityHandler::handleWildcard(Request &req, Response &resp) ResponseTask* IdentityHandler::handleOwn(Request &req, Response &resp) { + StateToken state; + { + RS_STACK_MUTEX(mMtx); // ********** LOCKED ********** + state = mStateToken; + } std::list ids; if(mRsIdentity->getOwnIds(ids)) - return new SendIdentitiesListTask(mRsIdentity, ids); + return new SendIdentitiesListTask(mRsIdentity, ids, state); resp.mDataStream.getStreamToMember(); resp.setWarning("identities not loaded yet"); return 0; diff --git a/libresapi/src/api/IdentityHandler.h b/libresapi/src/api/IdentityHandler.h index 8d110727e..5e5d67f13 100644 --- a/libresapi/src/api/IdentityHandler.h +++ b/libresapi/src/api/IdentityHandler.h @@ -1,20 +1,36 @@ #pragma once +#include +#include + #include "ResourceRouter.h" +#include "StateTokenServer.h" class RsIdentity; namespace resource_api { -class IdentityHandler: public ResourceRouter +class IdentityHandler: public ResourceRouter, NotifyClient { public: - IdentityHandler(RsIdentity* identity); + IdentityHandler(StateTokenServer* sts, RsNotify* notify, RsIdentity* identity); + virtual ~IdentityHandler(); + + // from NotifyClient + // note: this may get called from foreign threads + virtual void notifyGxsChange(const RsGxsChanges &changes); + private: - RsIdentity* mRsIdentity; void handleWildcard(Request& req, Response& resp); ResponseTask *handleOwn(Request& req, Response& resp); ResponseTask *handleCreateIdentity(Request& req, Response& resp); + + StateTokenServer* mStateTokenServer; + RsNotify* mNotify; + RsIdentity* mRsIdentity; + + RsMutex mMtx; + StateToken mStateToken; // mutex protected }; } // namespace resource_api From 3a9ff8e1ea956a124d7626545e484b416918bc7b Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 7 Feb 2016 12:10:25 +0100 Subject: [PATCH 31/64] fix integer sizing issues in webserver --- libresapi/src/api/ApiServerMHD.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libresapi/src/api/ApiServerMHD.cpp b/libresapi/src/api/ApiServerMHD.cpp index 2feed2d42..55c055eb2 100644 --- a/libresapi/src/api/ApiServerMHD.cpp +++ b/libresapi/src/api/ApiServerMHD.cpp @@ -305,7 +305,7 @@ public: // get content-type from extension std::string ext = ""; - unsigned int i = info.fname.rfind('.'); + std::string::size_type i = info.fname.rfind('.'); if(i != std::string::npos) ext = info.fname.substr(i+1); MHD_add_response_header(resp, "Content-Type", ContentTypes::cTypeFromExt(ext).c_str()); From 2c2c7936e576c45d1f773576a69b1ba95d30325b Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 7 Feb 2016 14:09:33 +0100 Subject: [PATCH 32/64] libresapi: make list of chat lobby participants available at chat/lobby_participants/ --- libresapi/src/api/ChatHandler.cpp | 93 ++++++++++++++++++++++++++++++- libresapi/src/api/ChatHandler.h | 9 +++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/libresapi/src/api/ChatHandler.cpp b/libresapi/src/api/ChatHandler.cpp index 4a40bb38c..cadaae810 100644 --- a/libresapi/src/api/ChatHandler.cpp +++ b/libresapi/src/api/ChatHandler.cpp @@ -97,6 +97,39 @@ StreamBase& operator << (StreamBase& left, ChatHandler::ChatInfo& info) return left; } +class SendLobbyParticipantsTask: public GxsResponseTask +{ +public: + SendLobbyParticipantsTask(RsIdentity* idservice, ChatHandler::LobbyParticipantsInfo pi): + GxsResponseTask(idservice, 0), mParticipantsInfo(pi) + { + const std::map& map = mParticipantsInfo.participants; + for(std::map::const_iterator mit = map.begin(); mit != map.end(); ++mit) + { + requestGxsId(mit->first); + } + } +private: + ChatHandler::LobbyParticipantsInfo mParticipantsInfo; +protected: + virtual void gxsDoWork(Request &req, Response &resp) + { + resp.mDataStream.getStreamToMember(); + const std::map& map = mParticipantsInfo.participants; + for(std::map::const_iterator mit = map.begin(); mit != map.end(); ++mit) + { + StreamBase& stream = resp.mDataStream.getStreamToMember(); + double last_active = mit->second; + stream << makeKeyValueReference("last_active", last_active); + streamGxsId(mit->first, stream.getStreamToMember("identity")); + } + resp.mStateToken = mParticipantsInfo.state_token; + resp.setOk(); + done(); + } + +}; + ChatHandler::ChatHandler(StateTokenServer *sts, RsNotify *notify, RsMsgs *msgs, RsPeers* peers, RsIdentity* identity, UnreadMsgNotify* unread): mStateTokenServer(sts), mNotify(notify), mRsMsgs(msgs), mRsPeers(peers), mRsIdentity(identity), mUnreadMsgNotify(unread), mMtx("ChatHandler::mMtx") { @@ -111,6 +144,7 @@ ChatHandler::ChatHandler(StateTokenServer *sts, RsNotify *notify, RsMsgs *msgs, addResourceHandler("lobbies", this, &ChatHandler::handleLobbies); addResourceHandler("subscribe_lobby", this, &ChatHandler::handleSubscribeLobby); addResourceHandler("unsubscribe_lobby", this, &ChatHandler::handleUnsubscribeLobby); + addResourceHandler("lobby_participants", this, &ChatHandler::handleLobbyParticipants); addResourceHandler("messages", this, &ChatHandler::handleMessages); addResourceHandler("send_message", this, &ChatHandler::handleSendMessage); addResourceHandler("mark_chat_as_read", this, &ChatHandler::handleMarkChatAsRead); @@ -170,9 +204,46 @@ void ChatHandler::tick() l.is_broadcast = false; l.gxs_id = info.gxs_id; lobbies.push_back(l); + + // update the lobby participants list + // maybe it causes to much traffic to do this in every tick, + // because the client would get the whole list every time a message was received + // we could reduce the checking frequency + std::map::iterator mit = mLobbyParticipantsInfos.find(*lit); + if(mit == mLobbyParticipantsInfos.end()) + { + mLobbyParticipantsInfos[*lit].participants = info.gxs_ids; + mLobbyParticipantsInfos[*lit].state_token = mStateTokenServer->getNewToken(); + } + else + { + LobbyParticipantsInfo& pi = mit->second; + if(!std::equal(pi.participants.begin(), pi.participants.end(), info.gxs_ids.begin())) + { + pi.participants = info.gxs_ids; + mStateTokenServer->replaceToken(pi.state_token); + } + } } } + // remove participants info of old lobbies + std::vector participants_info_to_delete; + for(std::map::iterator mit = mLobbyParticipantsInfos.begin(); + mit != mLobbyParticipantsInfos.end(); ++mit) + { + if(std::find(subscribed_ids.begin(), subscribed_ids.end(), mit->first) == subscribed_ids.end()) + { + participants_info_to_delete.push_back(mit->first); + } + } + for(std::vector::iterator vit = participants_info_to_delete.begin(); vit != participants_info_to_delete.end(); ++vit) + { + LobbyParticipantsInfo& pi = mLobbyParticipantsInfos[*vit]; + mStateTokenServer->discardToken(pi.state_token); + mLobbyParticipantsInfos.erase(*vit); + } + { Lobby l; l.name = "BroadCast"; @@ -496,11 +567,31 @@ void ChatHandler::handleSubscribeLobby(Request &req, Response &resp) resp.setFail("lobby join failed. (See console for more info)"); } -void ChatHandler::handleUnsubscribeLobby(Request &req, Response &/*resp*/) +void ChatHandler::handleUnsubscribeLobby(Request &req, Response &resp) { ChatLobbyId id = 0; req.mStream << makeKeyValueReference("id", id); mRsMsgs->unsubscribeChatLobby(id); + resp.setOk(); +} + +ResponseTask* ChatHandler::handleLobbyParticipants(Request &req, Response &resp) +{ + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + + ChatId id(req.mPath.top()); + if(!id.isLobbyId()) + { + resp.setFail("Path element \""+req.mPath.top()+"\" is not a ChatLobbyId."); + return 0; + } + std::map::const_iterator mit = mLobbyParticipantsInfos.find(id.toLobbyId()); + if(mit == mLobbyParticipantsInfos.end()) + { + resp.setFail("lobby not found"); + return 0; + } + return new SendLobbyParticipantsTask(mRsIdentity, mit->second); } void ChatHandler::handleMessages(Request &req, Response &resp) diff --git a/libresapi/src/api/ChatHandler.h b/libresapi/src/api/ChatHandler.h index bb15cf2f1..931e10514 100644 --- a/libresapi/src/api/ChatHandler.h +++ b/libresapi/src/api/ChatHandler.h @@ -81,6 +81,12 @@ public: } }; + class LobbyParticipantsInfo{ + public: + StateToken state_token; + std::map participants; + }; + class ChatInfo{ public: bool is_broadcast; @@ -96,6 +102,7 @@ private: void handleLobbies(Request& req, Response& resp); void handleSubscribeLobby(Request& req, Response& resp); void handleUnsubscribeLobby(Request& req, Response& resp); + ResponseTask* handleLobbyParticipants(Request& req, Response& resp); void handleMessages(Request& req, Response& resp); void handleSendMessage(Request& req, Response& resp); void handleMarkChatAsRead(Request& req, Response& resp); @@ -124,6 +131,8 @@ private: StateToken mLobbiesStateToken; std::vector mLobbies; + std::map mLobbyParticipantsInfos; + StateToken mUnreadMsgsStateToken; }; From 3d814b7926776a1862654ebbf6fb73b2fe509269 Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 7 Feb 2016 14:23:01 +0100 Subject: [PATCH 33/64] libresapi: added chat/send_status usage: $ curl --data "{\"chat_id\":\"\",\"status\":\"Hi there\"}" http:///api/v2/chat/send_status --- libresapi/src/api/ChatHandler.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/libresapi/src/api/ChatHandler.cpp b/libresapi/src/api/ChatHandler.cpp index cadaae810..9d4062271 100644 --- a/libresapi/src/api/ChatHandler.cpp +++ b/libresapi/src/api/ChatHandler.cpp @@ -771,9 +771,20 @@ void ChatHandler::handleTypingLabel(Request &/*req*/, Response &/*resp*/) } -void ChatHandler::handleSendStatus(Request &/*req*/, Response &/*resp*/) +void ChatHandler::handleSendStatus(Request &req, Response &resp) { - + std::string chat_id; + std::string status; + req.mStream << makeKeyValueReference("chat_id", chat_id) + << makeKeyValueReference("status", status); + ChatId id(chat_id); + if(id.isNotSet()) + { + resp.setFail("chat_id is invalid"); + return; + } + mRsMsgs->sendStatusString(id, status); + resp.setOk(); } void ChatHandler::handleUnreadMsgs(Request &/*req*/, Response &resp) From 253182032a404e627feef889ccc952711350132b Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 13 Feb 2016 20:25:31 +0100 Subject: [PATCH 34/64] webui: chat-channel subscribe --- libresapi/src/webui-src/app/chat.js | 63 ++++++++++++++++++++--- libresapi/src/webui-src/app/retroshare.js | 10 ++-- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index b272c3fdd..d8bfdfdb8 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -20,6 +20,9 @@ function lobbies(){ return m("div.btn",{ title: "topic: " + lobby.topic + "\n" + "subscribed: " + lobby.subscribed, + style: { + backgroundColor: lobby.subscribed ? 'blue' : 'darkred', + }, onclick: function(){ m.route("/chat?lobby=" + lobby.chat_id); } @@ -29,16 +32,64 @@ function lobbies(){ } +function getLobbyDetails(lobbyid){ + var lobs = rs("chat/lobbies"); + if (lobs === undefined) { + return null; + }; + for(var i = 0, l = lobs.length; i < l; ++i) { + if (lobs[i].chat_id == lobbyid) { + return lobs[i]; + } + } + return null; +} + function lobby(lobbyid){ + var lobby = getLobbyDetails(lobbyid); + if (lobby == null) { + return m("div","waiting ..."); + } var msgs = rs("chat/messages/" + lobbyid); var info = rs("chat/info/" + lobbyid); if (msgs === undefined || info === undefined) { return m("div","waiting ..."); } - return msgs.map(function(item){ - var d = new Date(new Number(item.send_time)*1000); - return msg(item.author_name, d.toLocaleDateString() + " " + d.toLocaleTimeString(),item.msg); - }); + var intro = [ + m("h2",lobby.name), + m("p",lobby.topic), + m("hr") + ] + if (!lobby.subscribed) { + return [ + intro, + m("b","select subscribe identity:"), + m("p"), + rs.list("identity/own", function(item){ + return m("div.btn2",{ + onclick:function(){ + console.log("subscribe - id:" + lobby.id +", " + + "gxs_id:" + item.gxs_id) + rs.request("chat/subscribe_lobby",{ + id:lobby.id, + gxs_id:item.gxs_id + }) + } + },"subscribe as " + item.name); + }), + ]; + } + return [ + intro, + msgs.map(function(item){ + var d = new Date(new Number(item.send_time)*1000); + return msg( + item.author_name, + d.toLocaleDateString() + " " + d.toLocaleTimeString(), + item.msg + ); + }) + ]; } module.exports = { @@ -46,8 +97,8 @@ module.exports = { return m(".chat.container", [ m(".chat.header", "headerbar"), m(".chat.left", [ - m("div.chat.header","lobbies:"), - m("hr"), + m("div.chat.header[style=position:relative]","lobbies:"), + m("br"), lobbies(), ]), m(".chat.right", right), diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index e955bbb13..1f4b3674d 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -170,7 +170,11 @@ function checkFocus(){ // called every time, rs or rs.request failed, only response or value is set function requestFail(path, response, value) { rs.error = "error on " + path; - console.log("Error on " + path + (response == null ? ", value " + value : ", response " + response)); + console.log("Error on " + path + + (response == null ? ", value: " + value : (", response: " + + (response.debug_msg === undefined ? response : response.debug_msg) + )) + ); } function rs(path, args, callback, options){ @@ -260,8 +264,8 @@ rs.counting = function(path, counterfnkt) { // listing data-elements rs.list = function(path, buildfktn){ var list = rs(path); - if (list === undefined) { + if (list === undefined|| list == null) { return "< waiting for server ... >" - } + }; return list.map(buildfktn); } From fc5f8c3b8c74fdc8fb97197ff30a865aaea8f40e Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 14 Feb 2016 14:55:57 +0100 Subject: [PATCH 35/64] removed unused function declaration ChatId::toGxsId() --- libretroshare/src/retroshare/rsmsgs.h | 1 - 1 file changed, 1 deletion(-) diff --git a/libretroshare/src/retroshare/rsmsgs.h b/libretroshare/src/retroshare/rsmsgs.h index d60511f48..6e4ae8f42 100644 --- a/libretroshare/src/retroshare/rsmsgs.h +++ b/libretroshare/src/retroshare/rsmsgs.h @@ -307,7 +307,6 @@ public: bool isBroadcast() const; RsPeerId toPeerId() const; - RsGxsId toGxsId() const; ChatLobbyId toLobbyId() const; DistantChatPeerId toDistantChatId() const; From 2ab755bb5c3cc8355e23cecc94a01e27e78ed966 Mon Sep 17 00:00:00 2001 From: electron128 Date: Sun, 14 Feb 2016 14:57:41 +0100 Subject: [PATCH 36/64] libresapi: added chat/receive_status usage: $ curl http:///localhost:8080/api/v2/chat/receive_status/ --- libresapi/src/api/ChatHandler.cpp | 195 +++++++++++++------------- libresapi/src/api/ChatHandler.h | 22 ++- libresapi/src/api/GxsResponseTask.cpp | 1 + libresapi/src/api/GxsResponseTask.h | 2 +- 4 files changed, 121 insertions(+), 99 deletions(-) diff --git a/libresapi/src/api/ChatHandler.cpp b/libresapi/src/api/ChatHandler.cpp index 9d4062271..598db5c09 100644 --- a/libresapi/src/api/ChatHandler.cpp +++ b/libresapi/src/api/ChatHandler.cpp @@ -149,7 +149,7 @@ ChatHandler::ChatHandler(StateTokenServer *sts, RsNotify *notify, RsMsgs *msgs, addResourceHandler("send_message", this, &ChatHandler::handleSendMessage); addResourceHandler("mark_chat_as_read", this, &ChatHandler::handleMarkChatAsRead); addResourceHandler("info", this, &ChatHandler::handleInfo); - addResourceHandler("typing_label", this, &ChatHandler::handleTypingLabel); + addResourceHandler("receive_status", this, &ChatHandler::handleReceiveStatus); addResourceHandler("send_status", this, &ChatHandler::handleSendStatus); addResourceHandler("unread_msgs", this, &ChatHandler::handleUnreadMsgs); } @@ -166,20 +166,21 @@ void ChatHandler::notifyChatMessage(const ChatMessage &msg) mRawMsgs.push_back(msg); } -// to be removed -/* -ChatHandler::Lobby ChatHandler::getLobbyInfo(ChatLobbyId id) +void ChatHandler::notifyChatStatus(const ChatId &chat_id, const std::string &status) { - tick(); - - RS_STACK_MUTEX(mMtx); // ********* LOCKED ********** - for(std::vector::iterator vit = mLobbies.begin(); vit != mLobbies.end(); ++vit) - if(vit->id == id) - return *vit; - std::cerr << "ChatHandler::getLobbyInfo Error: Lobby not found" << std::endl; - return Lobby(); + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + locked_storeTypingInfo(chat_id, status); +} + +void ChatHandler::notifyChatLobbyEvent(uint64_t lobby_id, uint32_t event_type, + const RsGxsId &nickname, const std::string& any_string) +{ + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + if(event_type == RS_CHAT_LOBBY_EVENT_PEER_STATUS) + { + locked_storeTypingInfo(ChatId(lobby_id), any_string, nickname); + } } -*/ void ChatHandler::tick() { @@ -510,6 +511,15 @@ void ChatHandler::getPlainText(const std::string& in, std::string &out, std::vec } } +void ChatHandler::locked_storeTypingInfo(const ChatId &chat_id, std::string status, RsGxsId lobby_gxs_id) +{ + TypingLabelInfo& info = mTypingLabelInfo[chat_id]; + info.timestamp = time(0); + info.status = status; + mStateTokenServer->replaceToken(info.state_token); + info.author_id = lobby_gxs_id; +} + void ChatHandler::handleWildcard(Request &/*req*/, Response &resp) { RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ @@ -664,89 +674,6 @@ void ChatHandler::handleMarkChatAsRead(Request &req, Response &resp) mStateTokenServer->replaceToken(mUnreadMsgsStateToken); } -// to be removed -// we do now cache chat info, to be able to include it in new message notify easily -/* -class InfoResponseTask: public GxsResponseTask -{ -public: - InfoResponseTask(ChatHandler* ch, RsPeers* peers, RsIdentity* identity): GxsResponseTask(identity, 0), mChatHandler(ch), mRsPeers(peers), mState(BEGIN){} - - enum State {BEGIN, WAITING}; - ChatHandler* mChatHandler; - RsPeers* mRsPeers; - State mState; - bool is_broadcast; - bool is_gxs_id; - bool is_lobby; - bool is_peer; - std::string remote_author_id; - std::string remote_author_name; - virtual void gxsDoWork(Request& req, Response& resp) - { - ChatId id(req.mPath.top()); - if(id.isNotSet()) - { - resp.setFail("not a valid chat id"); - done(); - return; - } - if(mState == BEGIN) - { - is_broadcast = false; - is_gxs_id = false; - is_lobby = false; - is_peer = false; - if(id.isBroadcast()) - { - is_broadcast = true; - } - else if(id.isGxsId()) - { - is_gxs_id = true; - remote_author_id = id.toGxsId().toStdString(); - requestGxsId(id.toGxsId()); - } - else if(id.isLobbyId()) - { - is_lobby = true; - remote_author_id = ""; - remote_author_name = mChatHandler->getLobbyInfo(id.toLobbyId()).name; - } - else if(id.isPeerId()) - { - is_peer = true; - remote_author_id = id.toPeerId().toStdString(); - remote_author_name = mRsPeers->getPeerName(id.toPeerId()); - } - else - { - std::cerr << "Error in InfoResponseTask::gxsDoWork(): unhandled chat_id=" << id.toStdString() << std::endl; - } - mState = WAITING; - } - else - { - if(is_gxs_id) - remote_author_name = getName(id.toGxsId()); - resp.mDataStream << makeKeyValueReference("remote_author_id", remote_author_id) - << makeKeyValueReference("remote_author_name", remote_author_name) - << makeKeyValueReference("is_broadcast", is_broadcast) - << makeKeyValueReference("is_gxs_id", is_gxs_id) - << makeKeyValueReference("is_lobby", is_lobby) - << makeKeyValueReference("is_peer", is_peer); - resp.setOk(); - done(); - } - } -}; - -ResponseTask *ChatHandler::handleInfo(Request &req, Response &resp) -{ - return new InfoResponseTask(this, mRsPeers, mRsIdentity); -} -*/ - void ChatHandler::handleInfo(Request &req, Response &resp) { RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ @@ -766,9 +693,83 @@ void ChatHandler::handleInfo(Request &req, Response &resp) resp.setOk(); } -void ChatHandler::handleTypingLabel(Request &/*req*/, Response &/*resp*/) +class SendTypingLabelInfo: public GxsResponseTask { +public: + SendTypingLabelInfo(RsIdentity* identity, RsPeers* peers, ChatId id, const ChatHandler::TypingLabelInfo& info): + GxsResponseTask(identity), mState(BEGIN), mPeers(peers),mId(id), mInfo(info) {} +private: + enum State {BEGIN, WAITING_ID}; + State mState; + RsPeers* mPeers; + ChatId mId; + ChatHandler::TypingLabelInfo mInfo; +protected: + void gxsDoWork(Request& /*req*/, Response& resp) + { + if(mState == BEGIN) + { + // lobby and distant require to fetch a gxs_id + if(mId.isLobbyId()) + { + requestGxsId(mInfo.author_id); + } + else if(mId.isDistantChatId()) + { + DistantChatPeerInfo dcpinfo ; + rsMsgs->getDistantChatStatus(mId.toDistantChatId(), dcpinfo); + requestGxsId(dcpinfo.to_id); + } + mState = WAITING_ID; + } + else + { + std::string name = "BUG: case not handled in SendTypingLabelInfo"; + if(mId.isPeerId()) + { + name = mPeers->getPeerName(mId.toPeerId()); + } + else if(mId.isDistantChatId()) + { + DistantChatPeerInfo dcpinfo ; + rsMsgs->getDistantChatStatus(mId.toDistantChatId(), dcpinfo); + name = getName(dcpinfo.to_id); + } + else if(mId.isLobbyId()) + { + name = getName(mInfo.author_id); + } + else if(mId.isBroadcast()) + { + name = mPeers->getPeerName(mId.broadcast_status_peer_id); + } + uint32_t ts = mInfo.timestamp; + resp.mDataStream << makeKeyValueReference("author_name", name) + << makeKeyValueReference("timestamp", ts) + << makeKeyValueReference("status_string", mInfo.status); + resp.mStateToken = mInfo.state_token; + resp.setOk(); + done(); + } + } +}; +ResponseTask* ChatHandler::handleReceiveStatus(Request &req, Response &resp) +{ + RS_STACK_MUTEX(mMtx); /********** LOCKED **********/ + ChatId id(req.mPath.top()); + if(id.isNotSet()) + { + resp.setFail("\""+req.mPath.top()+"\" is not a valid chat id"); + return 0; + } + std::map::iterator mit = mTypingLabelInfo.find(id); + if(mit == mTypingLabelInfo.end()) + { + locked_storeTypingInfo(id, ""); + mit = mTypingLabelInfo.find(id); + } + return new SendTypingLabelInfo(mRsIdentity, mRsPeers, id, mit->second); } void ChatHandler::handleSendStatus(Request &req, Response &resp) diff --git a/libresapi/src/api/ChatHandler.h b/libresapi/src/api/ChatHandler.h index 931e10514..e16aa445b 100644 --- a/libresapi/src/api/ChatHandler.h +++ b/libresapi/src/api/ChatHandler.h @@ -26,6 +26,13 @@ public: // note: this may get called from the own and from foreign threads virtual void notifyChatMessage(const ChatMessage& msg); + // typing label for peer, broadcast and distant chat + virtual void notifyChatStatus (const ChatId& /* chat_id */, const std::string& /* status_string */); + + //typing label for lobby chat, peer join and leave messages + virtual void notifyChatLobbyEvent (uint64_t /* lobby id */, uint32_t /* event type */ , + const RsGxsId& /* nickname */,const std::string& /* any string */); + // from tickable virtual void tick(); @@ -97,6 +104,15 @@ public: std::string remote_author_name; }; + class TypingLabelInfo{ + public: + time_t timestamp; + std::string status; + StateToken state_token; + // only for lobbies + RsGxsId author_id; + }; + private: void handleWildcard(Request& req, Response& resp); void handleLobbies(Request& req, Response& resp); @@ -107,11 +123,13 @@ private: void handleSendMessage(Request& req, Response& resp); void handleMarkChatAsRead(Request& req, Response& resp); void handleInfo(Request& req, Response& resp); - void handleTypingLabel(Request& req, Response& resp); + ResponseTask *handleReceiveStatus(Request& req, Response& resp); void handleSendStatus(Request& req, Response& resp); void handleUnreadMsgs(Request& req, Response& resp); void getPlainText(const std::string& in, std::string &out, std::vector &links); + // last parameter is only used for lobbies! + void locked_storeTypingInfo(const ChatId& chat_id, std::string status, RsGxsId lobby_gxs_id = RsGxsId()); StateTokenServer* mStateTokenServer; RsNotify* mNotify; @@ -128,6 +146,8 @@ private: std::map mChatInfo; + std::map mTypingLabelInfo; + StateToken mLobbiesStateToken; std::vector mLobbies; diff --git a/libresapi/src/api/GxsResponseTask.cpp b/libresapi/src/api/GxsResponseTask.cpp index 581f38ba6..dcb4ebb51 100644 --- a/libresapi/src/api/GxsResponseTask.cpp +++ b/libresapi/src/api/GxsResponseTask.cpp @@ -62,6 +62,7 @@ bool GxsResponseTask::doWork(Request &req, Response &resp) { more = false; // pause when an id failed, to give the service time tim fetch the data ready = false; + // TODO: remove identities which failed many times from list, to avoid blocking when ids fail } } if(!ready) diff --git a/libresapi/src/api/GxsResponseTask.h b/libresapi/src/api/GxsResponseTask.h index 7e133cdb9..8200b3eee 100644 --- a/libresapi/src/api/GxsResponseTask.h +++ b/libresapi/src/api/GxsResponseTask.h @@ -15,7 +15,7 @@ class GxsResponseTask: public ResponseTask { public: // token service is allowed to be null if no token functions are wanted - GxsResponseTask(RsIdentity* id_service, RsTokenService* token_service); + GxsResponseTask(RsIdentity* id_service, RsTokenService* token_service = 0); virtual bool doWork(Request &req, Response& resp); protected: From 288e81c486467f97e77829c0a4a1224d25f4c608 Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 14 Feb 2016 19:44:00 +0100 Subject: [PATCH 37/64] webui: full chat display --- libresapi/src/webui-src/app/chat.js | 34 +++++++++-- libresapi/src/webui-src/app/retroshare.js | 71 +++++++++++++++++------ 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index d8bfdfdb8..59f39afbe 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -46,15 +46,37 @@ function getLobbyDetails(lobbyid){ } function lobby(lobbyid){ + var msgs; var lobby = getLobbyDetails(lobbyid); - if (lobby == null) { - return m("div","waiting ..."); - } - var msgs = rs("chat/messages/" + lobbyid); var info = rs("chat/info/" + lobbyid); - if (msgs === undefined || info === undefined) { + if (lobby == null || info === undefined) { return m("div","waiting ..."); } + + var mem = rs.memory("chat/info/" + lobbyid); + if (mem.msg === undefined) { + mem.msg = []; + }; + + var reqData = {}; + if (mem.lastKnownMsg != undefined) { + reqData.begin_after = mem.lastKnownMsg; + } + + rs.request("chat/messages/" + lobbyid, reqData, function (data) { + mem.msg = mem.msg.concat(data); + if (mem.msg.length > 0) { + mem.lastKnownMsg = mem.msg[mem.msg.length -1].id; + } + if (data.length > 0 ) { + rs.request("chat/mark_chat_as_read/" + lobbyid,{}, null, + {allow: "ok|not_set"}); + } + }, { + onmismatch: function (){}, + log:function(){} //no logging (pulling) + }); + var intro = [ m("h2",lobby.name), m("p",lobby.topic), @@ -81,7 +103,7 @@ function lobby(lobbyid){ } return [ intro, - msgs.map(function(item){ + mem.msg.map(function(item){ var d = new Date(new Number(item.send_time)*1000); return msg( item.author_name, diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index 1f4b3674d..95c4a61a8 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -179,25 +179,21 @@ function requestFail(path, response, value) { function rs(path, args, callback, options){ if(cache[path] === undefined){ - if (options === undefined){ - options = {}; - } + options=optionsPrep(options,path); var req = { data: args, statetoken: undefined, requested: false, - allow: options.allow === undefined ? "ok" : options.allow, + allow: options.allow, then: function(response){ - console.log(path + ": response: " + response.returncode); + options.log(path + ": response: " + response.returncode); if (!this.allow.match(response.returncode)) { - requestFail(path, response, null); + options.onmismatch(response); } else if (callback != undefined && callback != null) { callback(response.data, response.statetoken); } }, - errorCallback: function(value){ - requestFail(path, null, value); - } + errorCallback: options.onfail }; cache[path] = req; schedule_request_missing(); @@ -209,9 +205,7 @@ module.exports = rs; // single request for action rs.request=function(path, args, callback, options){ - if (options === undefined) { - options = {}; - } + options = optionsPrep(options, path); var req = m.request({ method: options.method === undefined ? "POST" : options.method, url: api_url + path, @@ -219,18 +213,45 @@ rs.request=function(path, args, callback, options){ background: true }); req.then(function checkResponseAndCallback(response){ - console.log(path + ": response: " + response.returncode); - if (response.returncode != "ok") { - requestFail(path, response, null); + options.log(path + ": response: " + response.returncode); + if (!options.allow.match(response.returncode)) { + options.onmismatch(response); } else if (callback != undefined && callback != null) { callback(response.data, response.statetoken); } - }, function errhandling(value){ - requestFail(path, null, value); - }); + }, options.onfail); return req; }; +//set default-values for shared options in rs() and rs.request() +function optionsPrep(options, path) { + if (options === undefined) { + options = {}; + } + + if (options.onfail === undefined) { + options.onfail = function errhandling(value){ + requestFail(path, null, value); + } + }; + if (options.onmismatch === undefined) { + options.onmismatch = function errhandling(response){ + requestFail(path, response,null); + } + }; + + if (options.log === undefined) { + options.log = function(message) { + console.log(message); + } + } + + if (options.allow === undefined) { + options.allow = "ok"; + }; + return options; +} + // force reload for path rs.forceUpdate = function(path){ cache[path].requested=false; @@ -269,3 +290,17 @@ rs.list = function(path, buildfktn){ }; return list.map(buildfktn); } + +//remember additional data (feature of last resort) +rs.memory = function(path, args){ + var item = cache[path]; + if (item === undefined) { + rs(path, args); + item = cache[path]; + } + if (item.memory === undefined) { + item.memory = {}; + } + return item.memory; +}; + From c5f127f3b14f6eafda09a6f716822cf5db795892 Mon Sep 17 00:00:00 2001 From: zeners Date: Fri, 19 Feb 2016 18:48:17 +0100 Subject: [PATCH 38/64] webui: contains => match --- libresapi/src/webui-src/app/menu.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libresapi/src/webui-src/app/menu.js b/libresapi/src/webui-src/app/menu.js index c4f5dec0c..9d00a3f40 100644 --- a/libresapi/src/webui-src/app/menu.js +++ b/libresapi/src/webui-src/app/menu.js @@ -11,9 +11,9 @@ function goback(){ function buildmenu(menu, tagname, runstate, ignore){ if ( - (menu.runstate === undefined || runstate.match(menu.runstate)) + (menu.runstate === undefined || runstate.match(menu.runstate)!=null) && (!ignore.match(menu.name)) - && (menu.path === undefined || !menu.path.contains(":")) + && (menu.path === undefined || menu.path.match(":")==null) && (menu.show === undefined || menu.show) ) { var name = menu.name; From 34957b857a34dad598f8591238a8c28b5b09c7b4 Mon Sep 17 00:00:00 2001 From: electron128 Date: Fri, 19 Feb 2016 19:22:51 +0100 Subject: [PATCH 39/64] libresapi: added channels/create_post group creation acknowledge not implemented yet usage: $ curl --data "{\"group_id\":\"\",\"subject\":\"just a test\",\"message\":\"test message\"}" http:///api/v2/channels/create_post parameter object: { group_id: required string, subject: required string, message: required string, thumbnail_base64_png: optional string, files: optional array of { name: required string, hash: required string, size: required number } } --- libresapi/src/api/ApiServer.cpp | 7 +- libresapi/src/api/ChannelsHandler.cpp | 112 ++++++++++++++++++++++++++ libresapi/src/api/ChannelsHandler.h | 21 +++++ libresapi/src/libresapi.pro | 6 +- 4 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 libresapi/src/api/ChannelsHandler.cpp create mode 100644 libresapi/src/api/ChannelsHandler.h diff --git a/libresapi/src/api/ApiServer.cpp b/libresapi/src/api/ApiServer.cpp index 176e6290e..e434fe391 100644 --- a/libresapi/src/api/ApiServer.cpp +++ b/libresapi/src/api/ApiServer.cpp @@ -14,6 +14,7 @@ #include "StateTokenServer.h" // for the state token serialisers #include "ApiPluginHandler.h" +#include "ChannelsHandler.h" /* data types in json http://json.org/ @@ -232,7 +233,8 @@ public: mFileSearchHandler(sts, ifaces.mNotify, ifaces.mTurtle, ifaces.mFiles), mTransfersHandler(sts, ifaces.mFiles), mChatHandler(sts, ifaces.mNotify, ifaces.mMsgs, ifaces.mPeers, ifaces.mIdentity, &mPeersHandler), - mApiPluginHandler(sts, ifaces) + mApiPluginHandler(sts, ifaces), + mChannelsHandler(ifaces.mGxsChannels) { // the dynamic cast is to not confuse the addResourceHandler template like this: // addResourceHandler(derived class, parent class) @@ -254,6 +256,8 @@ public: &ChatHandler::handleRequest); router.addResourceHandler("apiplugin", dynamic_cast(&mApiPluginHandler), &ChatHandler::handleRequest); + router.addResourceHandler("channels", dynamic_cast(&mChannelsHandler), + &ChannelsHandler::handleRequest); } PeersHandler mPeersHandler; @@ -264,6 +268,7 @@ public: TransfersHandler mTransfersHandler; ChatHandler mChatHandler; ApiPluginHandler mApiPluginHandler; + ChannelsHandler mChannelsHandler; }; ApiServer::ApiServer(): diff --git a/libresapi/src/api/ChannelsHandler.cpp b/libresapi/src/api/ChannelsHandler.cpp new file mode 100644 index 000000000..27cc02e56 --- /dev/null +++ b/libresapi/src/api/ChannelsHandler.cpp @@ -0,0 +1,112 @@ +#include "ChannelsHandler.h" + +#include +#include +#include +#include "Operators.h" + +namespace resource_api +{ + +StreamBase& operator << (StreamBase& left, RsGxsFile& file) +{ + left << makeKeyValueReference("name", file.mName) + << makeKeyValueReference("hash", file.mHash); + if(left.serialise()) + { + double size = file.mSize; + left << makeKeyValueReference("size", size); + } + else + { + double size = 0; + left << makeKeyValueReference("size", size); + file.mSize = size; + } + return left; +} + +ChannelsHandler::ChannelsHandler(RsGxsChannels *channels): + mChannels(channels) +{ + addResourceHandler("create_post", this, &ChannelsHandler::handleCreatePost); +} + +ResponseTask* ChannelsHandler::handleCreatePost(Request &req, Response &resp) +{ + RsGxsChannelPost post; + + req.mStream << makeKeyValueReference("group_id", post.mMeta.mGroupId); + req.mStream << makeKeyValueReference("subject", post.mMeta.mMsgName); + req.mStream << makeKeyValueReference("message", post.mMsg); + + StreamBase& file_array = req.mStream.getStreamToMember("files"); + while(file_array.hasMore()) + { + RsGxsFile file; + file_array.getStreamToMember() << file; + post.mFiles.push_back(file); + } + + std::string thumbnail_base64; + req.mStream << makeKeyValueReference("thumbnail_base64_png", thumbnail_base64); + + if(post.mMeta.mGroupId.isNull()) + { + resp.setFail("groupd_id is null"); + return 0; + } + if(post.mMeta.mMsgName.empty()) + { + resp.setFail("subject is empty"); + return 0; + } + if(post.mMsg.empty()) + { + resp.setFail("msg text is empty"); + return 0; + } + // empty file list is ok, but files have to be valid + for(std::list::iterator lit = post.mFiles.begin(); lit != post.mFiles.end(); ++lit) + { + if(lit->mHash.isNull()) + { + resp.setFail("at least one file hash is empty"); + return 0; + } + if(lit->mName.empty()) + { + resp.setFail("at leats one file name is empty"); + return 0; + } + if(lit->mSize == 0) + { + resp.setFail("at least one file size is empty"); + return 0; + } + } + + std::vector png_data = Radix64::decode(thumbnail_base64); + if(!png_data.empty()) + { + if(png_data.size() < 8) + { + resp.setFail("Decoded thumbnail_base64_png is smaller than 8 byte. This can't be a valid png file!"); + return 0; + } + uint8_t png_magic_number[] = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}; + if(!std::equal(&png_magic_number[0],&png_magic_number[8],png_data.begin())) + { + resp.setFail("Decoded thumbnail_base64_png does not seem to be a png file. (Header is missing magic number)"); + return 0; + } + post.mThumbnail.copy(png_data.data(), png_data.size()); + } + + uint32_t token; + mChannels->createPost(token, post); + // TODO: grp creation acknowledge + return 0; +} + +} // namespace resource_api diff --git a/libresapi/src/api/ChannelsHandler.h b/libresapi/src/api/ChannelsHandler.h new file mode 100644 index 000000000..1d8559f52 --- /dev/null +++ b/libresapi/src/api/ChannelsHandler.h @@ -0,0 +1,21 @@ +#pragma once + +#include "ResourceRouter.h" + +class RsGxsChannels; + +namespace resource_api +{ + +class ChannelsHandler : public ResourceRouter +{ +public: + ChannelsHandler(RsGxsChannels* channels); + +private: + ResponseTask* handleCreatePost(Request& req, Response& resp); + + RsGxsChannels* mChannels; +}; + +} // namespace resource_api diff --git a/libresapi/src/libresapi.pro b/libresapi/src/libresapi.pro index d012680d8..bcbf542ab 100644 --- a/libresapi/src/libresapi.pro +++ b/libresapi/src/libresapi.pro @@ -66,7 +66,8 @@ SOURCES += \ api/LivereloadHandler.cpp \ api/TmpBlobStore.cpp \ util/ContentTypes.cpp \ - api/ApiPluginHandler.cpp + api/ApiPluginHandler.cpp \ + api/ChannelsHandler.cpp HEADERS += \ api/ApiServer.h \ @@ -91,4 +92,5 @@ HEADERS += \ api/LivereloadHandler.h \ api/TmpBlobStore.h \ util/ContentTypes.h \ - api/ApiPluginHandler.h + api/ApiPluginHandler.h \ + api/ChannelsHandler.h From fdb93c3f8df1d2ddb8967101ab7fe172b94811a1 Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 20 Feb 2016 18:03:27 +0100 Subject: [PATCH 40/64] webui: List forums, sort friends --- libresapi/src/webui-src/README.md | 104 +++++++++++----------- libresapi/src/webui-src/app/chat.js | 14 ++- libresapi/src/webui-src/app/forums.js | 64 +++++++++++++ libresapi/src/webui-src/app/identities.js | 3 +- libresapi/src/webui-src/app/menudef.js | 4 + libresapi/src/webui-src/app/peers.js | 1 + libresapi/src/webui-src/app/retroshare.js | 28 +++++- 7 files changed, 160 insertions(+), 58 deletions(-) create mode 100644 libresapi/src/webui-src/app/forums.js diff --git a/libresapi/src/webui-src/README.md b/libresapi/src/webui-src/README.md index 90548880f..94f69d309 100644 --- a/libresapi/src/webui-src/README.md +++ b/libresapi/src/webui-src/README.md @@ -1,52 +1,52 @@ -A new approach to build a webinterface for RS -============================================= - -1. get JSON encoded data from the backend, data contains a state token -2. render data with mithril.js -3. ask the backend if the state token from step 1 expired. If yes, then start again with step 1. - -Steps 1. and 3. are common for most things, only Step 2. differs. This allows to re-use code for steps 1. and 3. - -BUILD / DEVELOPMENT ------------- - - - install tools - sudo apt-get install TODO (insert package names for nodejs, ruby, sass here) - - run this once in webui-src directory, to install more tools - npm install - - start build - npm run watch - - the build process watches files for changes, and rebuilds and reloads the page. Build output is in ./public - - use the --webinterface 9090 command line parameter to enable webui in retroshare-nogui - - set the --docroot parameter of retroshare-nogui to point to the "libresapi/src/webui-src/public" directory - (or symlink from /usr/share/RetroShare06/webui on Linux, ./webui on Windows) - - retroshare-gui does not have a --docroot parameter. Use symlinks then. - -CONTRIBUTE ----------- - - - if you are a web developer or want to become one - get in contact! - - lots of work to do, i need you! - -TODO ----- -[ ] make stylesheets or find reusable sass/css components -google material design has nice rules for color, spacing and everything: https://www.google.de/design/spec/material-design/introduction.html -[ ] find icons, maybe use google material design iconfont -[ ] use urls/mithril routing for the menu. urls could replace state stored in rs.content -[ ] drag and drop private key upload and import -[ ] link from peer location to chat (use urls and mithril routing) -[ ] add/remove friend, own cert -[ ] downloads, search -[ ] make reusable infinite list controller, the js part to load data from Pagination.h (tweak Pagination.h to make everything work) -should provide forward, backward and follow-list-end -[ ] backend: view/create identities -[ ] backend: chat lobby participants list -[ ] chat: send_message -[ ] backend: chat typing notifications -[ ] make routines to handle retroshare links -[ ] backend: edit shared folders -[ ] backend: view shared files -[ ] redirect if a url is not usable in the current runstate (e.g. redirect from login page to home page, after login) -[ ] sort friendslist +A new approach to build a webinterface for RS +============================================= + +1. get JSON encoded data from the backend, data contains a state token +2. render data with mithril.js +3. ask the backend if the state token from step 1 expired. If yes, then start again with step 1. + +Steps 1. and 3. are common for most things, only Step 2. differs. This allows to re-use code for steps 1. and 3. + +BUILD / DEVELOPMENT +------------ + + - install tools + sudo apt-get install TODO (insert package names for nodejs, ruby, sass here) + - run this once in webui-src directory, to install more tools + npm install + - start build + npm run watch + - the build process watches files for changes, and rebuilds and reloads the page. Build output is in ./public + - use the --webinterface 9090 command line parameter to enable webui in retroshare-nogui + - set the --docroot parameter of retroshare-nogui to point to the "libresapi/src/webui-src/public" directory + (or symlink from /usr/share/RetroShare06/webui on Linux, ./webui on Windows) + - retroshare-gui does not have a --docroot parameter. Use symlinks then. + +CONTRIBUTE +---------- + + - if you are a web developer or want to become one + get in contact! + - lots of work to do, i need you! + +TODO +---- +[ ] make stylesheets or find reusable sass/css components +google material design has nice rules for color, spacing and everything: https://www.google.de/design/spec/material-design/introduction.html +[ ] find icons, maybe use google material design iconfont +[X] use urls/mithril routing for the menu. urls could replace state stored in rs.content +[X] drag and drop private key upload and import +[ ] link from peer location to chat (use urls and mithril routing) +[X] add/remove friend, own cert +[X] downloads, search +[ ] make reusable infinite list controller, the js part to load data from Pagination.h (tweak Pagination.h to make everything work) +should provide forward, backward and follow-list-end +[ ] backend: view/create identities +[ ] backend: chat lobby participants list +[ ] chat: send_message +[ ] backend: chat typing notifications +[ ] make routines to handle retroshare links +[ ] backend: edit shared folders +[ ] backend: view shared files +[ ] redirect if a url is not usable in the current runstate (e.g. redirect from login page to home page, after login) +[X] sort friendslist diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index 59f39afbe..1bc4e8ca3 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -117,7 +117,13 @@ function lobby(lobbyid){ module.exports = { frame: function(content, right){ return m(".chat.container", [ - m(".chat.header", "headerbar"), + m(".chat.header", [ + m( + "h2", + {style:{margin:"0px"}}, + "chat" + ) + ]), m(".chat.left", [ m("div.chat.header[style=position:relative]","lobbies:"), m("br"), @@ -136,7 +142,11 @@ module.exports = { ); }; return this.frame( - m("div", "please select lobby"), + m( + "div", + {style: {margin:"10px"}}, + "please select lobby" + ), m("div","right")); } } diff --git a/libresapi/src/webui-src/app/forums.js b/libresapi/src/webui-src/app/forums.js new file mode 100644 index 000000000..289c87b22 --- /dev/null +++ b/libresapi/src/webui-src/app/forums.js @@ -0,0 +1,64 @@ +"use strict"; + +var m = require("mithril"); +var rs = require("retroshare"); + +module.exports = {view: function(){ + return m("div",[ + m("h2","forums"), + m("hr"), + /* + m("div.btn2", { + onclick: function (){ + m.route("/addforum"); + } + },"< create new forum >"), + */ + m("ul", + rs.list("forums", + function(item){ + return m("li",[ + m("h2",item.name), + m("div",{style:{margin:"10px"}}, + [ + item.description != "" + ? [ + m("span", "Description: " + + item.description), + m("br")] + : [], + m("span","messages visible: " + + item.visible_msg_count), + ] + ), + /* + item.subscribed + ? [ + m( + "span.btn2", + {style:{padding:"0px"}}, + "unsubscribe" + ), + " ", + m( + "span.btn2", + {style:{padding:"0px", margin:"10px"}}, + "enter" + ), + m("hr", {style: {color:"silver"}}), + ] + : [ + m( + "span.btn2", + {style:{padding:"0px", margin:"10px"}}, + "subscribe" + ), + ] + */ + ]); + }, + rs.sort("name") + ) + ) + ]); +}} diff --git a/libresapi/src/webui-src/app/identities.js b/libresapi/src/webui-src/app/identities.js index 68f14c154..06557da7b 100644 --- a/libresapi/src/webui-src/app/identities.js +++ b/libresapi/src/webui-src/app/identities.js @@ -15,7 +15,8 @@ module.exports = {view: function(){ m("ul", rs.list("identity/own", function(item){ return m("li",[m("h2",item.name)]); - }) + }, + rs.sort("name")) ) ]); }} diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index e3cc893ea..538b7cc75 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -63,6 +63,10 @@ module.exports = { nodes: [ runstate: "running_ok.*", counter: rs.counting("transfers/downloads") }, + { + name: "forums", + runstate: "running_ok.*", + }, { name: "chat", runstate: "running_ok.*", diff --git a/libresapi/src/webui-src/app/peers.js b/libresapi/src/webui-src/app/peers.js index f22e96e46..f58d9bbbc 100644 --- a/libresapi/src/webui-src/app/peers.js +++ b/libresapi/src/webui-src/app/peers.js @@ -14,6 +14,7 @@ module.exports = {view: function(){ m("h3","waiting_server"), ]); }; + peers = peers.sort(rs.sort("name")); //building peerlist (prebuild for counting) var online = 0; diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index 95c4a61a8..a7ba47198 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -280,16 +280,19 @@ rs.counting = function(path, counterfnkt) { } return ""; } -} +}; // listing data-elements -rs.list = function(path, buildfktn){ +rs.list = function(path, buildfktn, sortfktn){ var list = rs(path); if (list === undefined|| list == null) { return "< waiting for server ... >" }; + if (sortfktn != undefined && sortfktn != null) { + list=list.sort(sortfktn); + } return list.map(buildfktn); -} +}; //remember additional data (feature of last resort) rs.memory = function(path, args){ @@ -304,3 +307,22 @@ rs.memory = function(path, args){ return item.memory; }; +//return sorting-function for string, based on property name +//using: list.sort(rs.sort("name")); +// ----- +//innersort: cascading sorting - using: +//list.sort(rs.sort("type",rs.sort("name"))) +rs.sort = function(name, innersort){ + return function(a,b){ + if (a[name].toLowerCase() == b[name].toLowerCase()) { + if (innersort === undefined) { + return 0 + } + return innersort(a,b); + } else if (a[name].toLowerCase() < b[name].toLowerCase()) { + return -1 + } else { + return 1 + } + } +} From b8a8f34a48b63ed458793f772640cc4a722a391b Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 20 Feb 2016 19:37:31 +0100 Subject: [PATCH 41/64] webui: chat: sending messages --- libresapi/src/webui-src/README.md | 2 +- libresapi/src/webui-src/app/_chat.sass | 6 +- libresapi/src/webui-src/app/chat.js | 85 ++++++++++++++++++-------- 3 files changed, 67 insertions(+), 26 deletions(-) diff --git a/libresapi/src/webui-src/README.md b/libresapi/src/webui-src/README.md index 94f69d309..49f7d3bb0 100644 --- a/libresapi/src/webui-src/README.md +++ b/libresapi/src/webui-src/README.md @@ -43,7 +43,7 @@ google material design has nice rules for color, spacing and everything: https:/ should provide forward, backward and follow-list-end [ ] backend: view/create identities [ ] backend: chat lobby participants list -[ ] chat: send_message +[X] chat: send_message [ ] backend: chat typing notifications [ ] make routines to handle retroshare links [ ] backend: edit shared folders diff --git a/libresapi/src/webui-src/app/_chat.sass b/libresapi/src/webui-src/app/_chat.sass index deee9ffa5..f9864bce7 100644 --- a/libresapi/src/webui-src/app/_chat.sass +++ b/libresapi/src/webui-src/app/_chat.sass @@ -45,7 +45,11 @@ padding-top: 0px padding-left: 0px padding-right: 0px - + &.bottom + position: absolute + bottom: 0px + right: $right_width + left: $left_width &.msg padding: 0px $author_width: 100px diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index 1bc4e8ca3..227a26464 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -3,7 +3,9 @@ var m = require("mithril"); var rs = require("retroshare"); -function msg(from, when, text){ +var msg = null; + +function dspmsg(from, when, text){ return m(".chat.msg.container",[ m(".chat.msg.from", from), m(".chat.msg.when", when), @@ -45,6 +47,15 @@ function getLobbyDetails(lobbyid){ return null; } +function sendmsg(msgid){ + var txtmsg = document.getElementById("txtNewMsg"); + rs.request("chat/send_message", { + chat_id: msgid, + msg: txtmsg.value + }); + txtmsg.value=""; +} + function lobby(lobbyid){ var msgs; var lobby = getLobbyDetails(lobbyid); @@ -100,12 +111,30 @@ function lobby(lobbyid){ },"subscribe as " + item.name); }), ]; + } else { + msg = m(".chat.bottom",[ + m("div","enter new message:"), + m("input",{ + id:"txtNewMsg", + onkeydown: function(event){ + if (event.keyCode == 13){ + sendmsg(lobbyid); + } + } + }), + m("div.btn2", { + style: {textAlign:"center"}, + onclick: function(){ + sendmsg(lobbyid); + } + },"submit") + ]); } return [ intro, mem.msg.map(function(item){ var d = new Date(new Number(item.send_time)*1000); - return msg( + return dspmsg( item.author_name, d.toLocaleDateString() + " " + d.toLocaleTimeString(), item.msg @@ -116,37 +145,45 @@ function lobby(lobbyid){ module.exports = { frame: function(content, right){ - return m(".chat.container", [ - m(".chat.header", [ - m( - "h2", - {style:{margin:"0px"}}, - "chat" - ) + return m("div", [ + m(".chat.container", [ + m(".chat.header", [ + m( + "h2", + {style:{margin:"0px"}}, + "chat" + ) + ]), + m(".chat.left", [ + m("div.chat.header[style=position:relative]","lobbies:"), + m("br"), + lobbies(), + ]), + m(".chat.right", right), + m(".chat.middle", content), + m(".chat.clear", ""), ]), - m(".chat.left", [ - m("div.chat.header[style=position:relative]","lobbies:"), - m("br"), - lobbies(), - ]), - m(".chat.right", right), - m(".chat.middle", content), - m(".chat.clear", ""), + msg != null + ? msg + : [], ]); }, view: function(){ var lobbyid = m.route.param("lobby"); + msg = null; if (lobbyid != undefined ) { return this.frame( - lobby(lobbyid),[] + lobby(lobbyid), + [] ); }; return this.frame( - m( - "div", - {style: {margin:"10px"}}, - "please select lobby" - ), - m("div","right")); + m( + "div", + {style: {margin:"10px"}}, + "please select lobby" + ), + m("div","right") + ); } } From f29f1b21af87a5cfa7ce028bbc81d57d6ad3da79 Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 21 Feb 2016 09:14:07 +0100 Subject: [PATCH 42/64] webui: personal chat --- libresapi/src/webui-src/app/chat.js | 87 ++++++++++++++++------- libresapi/src/webui-src/app/retroshare.js | 16 +++++ 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index 227a26464..ab652cab9 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -14,24 +14,51 @@ function dspmsg(from, when, text){ } function lobbies(){ - var lobs = rs("chat/lobbies"); - if (lobs === undefined) { - return m("div","loading ...") - }; - var dta = lobs.map(function (lobby){ - return m("div.btn",{ - title: "topic: " + lobby.topic + "\n" - + "subscribed: " + lobby.subscribed, - style: { - backgroundColor: lobby.subscribed ? 'blue' : 'darkred', + return [ + rs.list("chat/lobbies",function(lobby){ + return m("div.btn",{ + title: "topic: " + lobby.topic + "\n" + + "subscribed: " + lobby.subscribed, + style: { + backgroundColor: lobby.subscribed ? 'blue' : 'darkred', + }, + onclick: function(){ + m.route("/chat?lobby=" + lobby.chat_id); + } }, - onclick: function(){ - m.route("/chat?lobby=" + lobby.chat_id); - } - },lobby.name + (lobby.unread_msg_count > 0 ? ("(" + lobby.unread_msg_count + ")") : "")); - }); - return dta; - + lobby.name + ( + lobby.unread_msg_count > 0 + ? ("(" + lobby.unread_msg_count + ")") + : "") + ); + }, + rs.sort.bool("is_broadcast", + rs.sort.bool("subscribed", + rs.sort("name"))) + ), + m("br"), + m("h3","peers:"), + rs.list("peers",function(peer){ + return peer.locations.map(function(loc){ + if (loc.location == "") { + return []; + }; + return m("div.btn",{ + style: { + backgroundColor: loc.is_online ? 'blue' : 'darkred', + }, + onclick: function(){ + m.route("/chat?lobby=" + loc.chat_id); + } + }, + peer.name + " / " + loc.location + ( + lobby.unread_msgs > 0 + ? ("(" + lobby.unread_msgs + ")") + : "") + ); + }) + }) + ]; } function getLobbyDetails(lobbyid){ @@ -44,6 +71,18 @@ function getLobbyDetails(lobbyid){ return lobs[i]; } } + var peers = rs("peers"); + if (peers === undefined) { + return null; + }; + for(var i = 0, l = peers.length; i < l; ++i) { + var peer = peers[i]; + for(var i1 = 0, l1 = peer.locations.length; i1 < l1; ++i1) { + if (peer.locations[i1].chat_id == lobbyid) { + return peer.locations[i1]; + } + } + } return null; } @@ -58,9 +97,9 @@ function sendmsg(msgid){ function lobby(lobbyid){ var msgs; - var lobby = getLobbyDetails(lobbyid); + var lobdt = getLobbyDetails(lobbyid); var info = rs("chat/info/" + lobbyid); - if (lobby == null || info === undefined) { + if (lobdt == null || info === undefined) { return m("div","waiting ..."); } @@ -89,11 +128,11 @@ function lobby(lobbyid){ }); var intro = [ - m("h2",lobby.name), - m("p",lobby.topic), + m("h2",lobdt.name), + m("p",lobdt.topic), m("hr") ] - if (!lobby.subscribed) { + if (lobdt.subscribed != undefined && !lobdt.subscribed) { return [ intro, m("b","select subscribe identity:"), @@ -101,10 +140,10 @@ function lobby(lobbyid){ rs.list("identity/own", function(item){ return m("div.btn2",{ onclick:function(){ - console.log("subscribe - id:" + lobby.id +", " + console.log("subscribe - id:" + lobbyid +", " + "gxs_id:" + item.gxs_id) rs.request("chat/subscribe_lobby",{ - id:lobby.id, + id:lobbyid, gxs_id:item.gxs_id }) } diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index a7ba47198..8cd34a3f3 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -326,3 +326,19 @@ rs.sort = function(name, innersort){ } } } + +//return sorting-function for boolean, based on property name +rs.sort.bool = function(name, innersort){ + return function(a,b){ + if (a[name] == b[name]) { + if (innersort === undefined) { + return 0 + } + return innersort(a,b); + } else if (a[name]) { + return -1 + } else { + return 1 + } + } +} From 7c121d77924496d08b1cd5e28b0246d3a005abb0 Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 21 Feb 2016 09:29:09 +0100 Subject: [PATCH 43/64] webui: chat: unread msg count for peer fixed --- libresapi/src/webui-src/app/chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index ab652cab9..a6392b175 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -52,8 +52,8 @@ function lobbies(){ } }, peer.name + " / " + loc.location + ( - lobby.unread_msgs > 0 - ? ("(" + lobby.unread_msgs + ")") + loc.unread_msgs > 0 + ? ("(" + loc.unread_msgs + ")") : "") ); }) From 888e8f5d3517ee3badb06a6ae92a58919881ff67 Mon Sep 17 00:00:00 2001 From: zeners Date: Sun, 21 Feb 2016 09:38:57 +0100 Subject: [PATCH 44/64] webui: lobby-chat subscribe fixed --- libresapi/src/webui-src/app/chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index a6392b175..8e42815af 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -140,10 +140,10 @@ function lobby(lobbyid){ rs.list("identity/own", function(item){ return m("div.btn2",{ onclick:function(){ - console.log("subscribe - id:" + lobbyid +", " + console.log("subscribe - id:" + lobdt.id +", " + "gxs_id:" + item.gxs_id) rs.request("chat/subscribe_lobby",{ - id:lobbyid, + id:lobdt.id, gxs_id:item.gxs_id }) } From b1286f06ed854f1085393c27c80e74a1341abb85 Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 12 Mar 2016 17:44:43 +0100 Subject: [PATCH 45/64] webui: route peer location to chat-lobby --- libresapi/src/webui-src/README.md | 2 +- libresapi/src/webui-src/app/peers.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libresapi/src/webui-src/README.md b/libresapi/src/webui-src/README.md index 49f7d3bb0..057fb560b 100644 --- a/libresapi/src/webui-src/README.md +++ b/libresapi/src/webui-src/README.md @@ -36,7 +36,7 @@ google material design has nice rules for color, spacing and everything: https:/ [ ] find icons, maybe use google material design iconfont [X] use urls/mithril routing for the menu. urls could replace state stored in rs.content [X] drag and drop private key upload and import -[ ] link from peer location to chat (use urls and mithril routing) +[X] link from peer location to chat (use urls and mithril routing) [X] add/remove friend, own cert [X] downloads, search [ ] make reusable infinite list controller, the js part to load data from Pagination.h (tweak Pagination.h to make everything work) diff --git a/libresapi/src/webui-src/app/peers.js b/libresapi/src/webui-src/app/peers.js index f58d9bbbc..76ff118a8 100644 --- a/libresapi/src/webui-src/app/peers.js +++ b/libresapi/src/webui-src/app/peers.js @@ -34,6 +34,9 @@ module.exports = {view: function(){ return m("div",{ style:"color:" + (location.is_online ? "lime": "grey") + ";cursor:pointer", + onclick: function(){ + m.route("/chat?lobby=" + location.chat_id) + } },location.location); }); From e9af1794dd9a074c9d16158b32d2b9599ee42fff Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 12 Mar 2016 19:17:37 +0100 Subject: [PATCH 46/64] webui: need for master --- libresapi/src/webui-src/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libresapi/src/webui-src/README.md b/libresapi/src/webui-src/README.md index 057fb560b..508886842 100644 --- a/libresapi/src/webui-src/README.md +++ b/libresapi/src/webui-src/README.md @@ -50,3 +50,9 @@ should provide forward, backward and follow-list-end [ ] backend: view shared files [ ] redirect if a url is not usable in the current runstate (e.g. redirect from login page to home page, after login) [X] sort friendslist + +need 4 master +------------- +[ ] unsubscribe lobby +[ ] unread chat message counter in menu +[ ] list chat-lobby participants From 0afa2e37260a6b028e8d239e81dd67ec5e114ad6 Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 12 Mar 2016 20:20:56 +0100 Subject: [PATCH 47/64] webui: unread chat message counter in menu --- libresapi/src/webui-src/README.md | 2 +- libresapi/src/webui-src/app/menudef.js | 18 ++++++++++++++++++ libresapi/src/webui-src/app/retroshare.js | 20 ++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/libresapi/src/webui-src/README.md b/libresapi/src/webui-src/README.md index 508886842..e4534fa26 100644 --- a/libresapi/src/webui-src/README.md +++ b/libresapi/src/webui-src/README.md @@ -54,5 +54,5 @@ should provide forward, backward and follow-list-end need 4 master ------------- [ ] unsubscribe lobby -[ ] unread chat message counter in menu +[X] unread chat message counter in menu [ ] list chat-lobby participants diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index 538b7cc75..16e9a4c51 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -70,6 +70,24 @@ module.exports = { nodes: [ { name: "chat", runstate: "running_ok.*", + counter: rs.counting2([ + { + path: "peers", + counter: function(peer) { + var sum = 0; + peer.locations.map(function (loc) { + sum += parseInt(loc.unread_msgs); + }); + return sum; + } + }, + { + path: "chat/lobbies", + counter: function(lobby) { + return lobby.unread_msg_count; + } + } + ]) }, { name: "shutdown", diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index 8cd34a3f3..9f9bb7ee0 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -282,6 +282,26 @@ rs.counting = function(path, counterfnkt) { } }; +// counting in menu +rs.counting2 = function(targets) { + return function () { + var sum = 0; + targets.map(function(target) { + var data=rs(target.path); + if (data != undefined) { + data.map(function(item){ + sum += parseInt(target.counter(item)); + }); + }; + return null; + }); + if (sum > 0) { + return " (" + sum + ")"; + } + return ""; + } +}; + // listing data-elements rs.list = function(path, buildfktn, sortfktn){ var list = rs(path); From 41a8e53abf959a9ae989f8b41c1d3a103482a3fd Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 12 Mar 2016 23:28:27 +0100 Subject: [PATCH 48/64] webui: chat-layout fixed --- libresapi/src/webui-src/app/_chat.sass | 18 ++++++---- libresapi/src/webui-src/app/chat.js | 32 +++++++++++++++-- libresapi/src/webui-src/app/green-black.scss | 1 + libresapi/src/webui-src/app/main.js | 16 +++++++-- libresapi/src/webui-src/app/menudef.js | 30 +++++++--------- libresapi/src/webui-src/app/retroshare.js | 38 +++++++++++--------- 6 files changed, 90 insertions(+), 45 deletions(-) diff --git a/libresapi/src/webui-src/app/_chat.sass b/libresapi/src/webui-src/app/_chat.sass index f9864bce7..4b798da17 100644 --- a/libresapi/src/webui-src/app/_chat.sass +++ b/libresapi/src/webui-src/app/_chat.sass @@ -9,6 +9,7 @@ height: 100% padding: 0px position: relative + box-sizing: border-box &.header position: absolute top: 0px @@ -24,7 +25,7 @@ bottom: 0px left: 0px width: $left_width - border-right: solid 1px gray + //border-right: solid 1px gray box-sizing: border-box background-color: black &.right @@ -34,22 +35,24 @@ bottom: 0px width: $right_width box-sizing: border-box - border-left: solid 1px gray + //border-left: solid 1px gray &.middle //background-color: blue position: absolute - top: $header_height + top: 0px + margin-top: $header_height left: $left_width right: $right_width box-sizing: border-box - padding-top: 0px - padding-left: 0px - padding-right: 0px + padding: 0px + height: 100% + overflow-y: scroll &.bottom position: absolute bottom: 0px right: $right_width left: $left_width + padding: 5px &.msg padding: 0px $author_width: 100px @@ -57,6 +60,7 @@ position: relative border-bottom: solid 1px lightgray padding: 10px + height: unset //background-color: lime &.from position: absolute @@ -73,3 +77,5 @@ padding-left: $author_width top: 0px left: $author_width + white-space: pre-wrap + height: initial diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index 8e42815af..a79999d88 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -4,6 +4,7 @@ var m = require("mithril"); var rs = require("retroshare"); var msg = null; +var particips = []; function dspmsg(from, when, text){ return m(".chat.msg.container",[ @@ -169,6 +170,24 @@ function lobby(lobbyid){ },"submit") ]); } + if (lobdt.subscribed != undefined + && lobdt.subscribed + && !lobdt.is_broadcast + ) { + //set participants + particips = [ + m("h3","participants:"), + rs.list( + "chat/lobby_participants/" + lobbyid, + function(item) { + return m("div",item.identity.name); + }, + function (a,b){ + return rs.stringSort(a.identity.name,b.identity.name); + } + ) + ] + } return [ intro, mem.msg.map(function(item){ @@ -184,7 +203,13 @@ function lobby(lobbyid){ module.exports = { frame: function(content, right){ - return m("div", [ + return m("div", { + style: { + "height": "100%", + "box-sizing": "border-box", + "padding-bottom": "170px", + } + },[ m(".chat.container", [ m(".chat.header", [ m( @@ -211,9 +236,10 @@ module.exports = { var lobbyid = m.route.param("lobby"); msg = null; if (lobbyid != undefined ) { + particips = []; return this.frame( lobby(lobbyid), - [] + particips ); }; return this.frame( @@ -222,7 +248,7 @@ module.exports = { {style: {margin:"10px"}}, "please select lobby" ), - m("div","right") + m("div","") ); } } diff --git a/libresapi/src/webui-src/app/green-black.scss b/libresapi/src/webui-src/app/green-black.scss index 313f7bde4..e49973e10 100644 --- a/libresapi/src/webui-src/app/green-black.scss +++ b/libresapi/src/webui-src/app/green-black.scss @@ -6,6 +6,7 @@ body { /*padding: 1.5em;*/ padding: 2mm; font-size: 1.1em; + box-sizing: border-box; } #overlay{ diff --git a/libresapi/src/webui-src/app/main.js b/libresapi/src/webui-src/app/main.js index 6bee57a36..f83a7ce90 100644 --- a/libresapi/src/webui-src/app/main.js +++ b/libresapi/src/webui-src/app/main.js @@ -62,10 +62,22 @@ function Page(menu){ return m("h2","server starting ...") } else if("waiting_account_select|running_ok.*".match(runstate.runstate)) { if (runst === undefined || runst.match(runstate.runstate)) { - return m("div", [ + return m("div", { + style: { + height: "100%", + "box-sizing": "border-box", + "padding-bottom": "40px" + } + }, [ m("div", mm.view()), m("hr"), - m("div", content) + m("div", { + style: { + height: "100%", + "box-sizing": "border-box", + "padding-bottom":"40px" + } + }, content) ]); } else { // funktion currently not available diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index 16e9a4c51..5a4f41640 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -70,24 +70,18 @@ module.exports = { nodes: [ { name: "chat", runstate: "running_ok.*", - counter: rs.counting2([ - { - path: "peers", - counter: function(peer) { - var sum = 0; - peer.locations.map(function (loc) { - sum += parseInt(loc.unread_msgs); - }); - return sum; - } - }, - { - path: "chat/lobbies", - counter: function(lobby) { - return lobby.unread_msg_count; - } - } - ]) + counter: rs.counting2({ + "peers": function(peer) { + var sum = 0; + peer.locations.map(function (loc) { + sum += parseInt(loc.unread_msgs); + }); + return sum; + }, + "chat/lobbies": function(lobby) { + return lobby.unread_msg_count; + } + }) }, { name: "shutdown", diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index 9f9bb7ee0..2c7f8e500 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -286,15 +286,14 @@ rs.counting = function(path, counterfnkt) { rs.counting2 = function(targets) { return function () { var sum = 0; - targets.map(function(target) { - var data=rs(target.path); + for (var path in targets) { + var data=rs(path); if (data != undefined) { data.map(function(item){ - sum += parseInt(target.counter(item)); + sum += parseInt(targets[path](item)); }); }; - return null; - }); + }; if (sum > 0) { return " (" + sum + ")"; } @@ -327,23 +326,30 @@ rs.memory = function(path, args){ return item.memory; }; +// Sortierfunktion für Texte von Objekten, +// falls einfache Namen nicht funktionieren +rs.stringSort = function(textA,textB, innersort, objectA, objectB){ + if (textA.toLowerCase() == textB.toLowerCase()) { + if (innersort === undefined) { + return 0 + } + return innersort(objectA,objectB); + } else if (textA.toLowerCase() < textB.toLowerCase()) { + return -1 + } else { + return 1 + } + } + + //return sorting-function for string, based on property name //using: list.sort(rs.sort("name")); // ----- //innersort: cascading sorting - using: //list.sort(rs.sort("type",rs.sort("name"))) rs.sort = function(name, innersort){ - return function(a,b){ - if (a[name].toLowerCase() == b[name].toLowerCase()) { - if (innersort === undefined) { - return 0 - } - return innersort(a,b); - } else if (a[name].toLowerCase() < b[name].toLowerCase()) { - return -1 - } else { - return 1 - } + return function(a,b) { + return rs.stringSort(a[name],b[name],innersort,a,b); } } From 1b72d2cec30fb11376cc9fa1210a10c2264c9f72 Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 12 Mar 2016 23:38:39 +0100 Subject: [PATCH 49/64] webui: unsubscribe chat-lobby --- libresapi/src/webui-src/README.md | 4 ++-- libresapi/src/webui-src/app/chat.js | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/libresapi/src/webui-src/README.md b/libresapi/src/webui-src/README.md index e4534fa26..3d2748d6c 100644 --- a/libresapi/src/webui-src/README.md +++ b/libresapi/src/webui-src/README.md @@ -53,6 +53,6 @@ should provide forward, backward and follow-list-end need 4 master ------------- -[ ] unsubscribe lobby +[X] unsubscribe lobby [X] unread chat message counter in menu -[ ] list chat-lobby participants +[X] list chat-lobby participants diff --git a/libresapi/src/webui-src/app/chat.js b/libresapi/src/webui-src/app/chat.js index a79999d88..e398d5c69 100644 --- a/libresapi/src/webui-src/app/chat.js +++ b/libresapi/src/webui-src/app/chat.js @@ -176,6 +176,17 @@ function lobby(lobbyid){ ) { //set participants particips = [ + m("div.btn", { + style: { + "text-align":"center" + }, + onclick: function (){ + rs.request("chat/unsubscribe_lobby",{ + id:lobdt.id, + }); + m.route("/chat"); + } + },"unsubscribe"), m("h3","participants:"), rs.list( "chat/lobby_participants/" + lobbyid, From 12c0efb0061a2ed90fd55560cc2c4014df046875 Mon Sep 17 00:00:00 2001 From: zeners Date: Sat, 19 Mar 2016 17:39:46 +0100 Subject: [PATCH 50/64] webui: pgp-linked identity disabled reason: password-request don't work --- libresapi/src/webui-src/app/addidentity.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libresapi/src/webui-src/app/addidentity.js b/libresapi/src/webui-src/app/addidentity.js index 907209d0b..8d7fb08f9 100644 --- a/libresapi/src/webui-src/app/addidentity.js +++ b/libresapi/src/webui-src/app/addidentity.js @@ -6,7 +6,8 @@ var rs = require("retroshare"); function createidentity(){ var data = { name: document.getElementById("txtname").value, - pgp_linked: document.getElementById("chklinked").checked, + pgp_linked: false, + //document.getElementById("chklinked").checked, }; m.route("/waiting"); rs.request("identity/create_identity",data, function(){ @@ -32,6 +33,7 @@ module.exports = {view: function(){ } */ }), + /* m("b","linked with pgp-id: "), m("input", { type: "checkbox", @@ -41,6 +43,7 @@ module.exports = {view: function(){ width: "0%", } }), + */ m("p"," "), m("input.btn2", { onclick: createidentity, From 51e0d83c47940b5ecd86276312033e272fca566e Mon Sep 17 00:00:00 2001 From: zeners Date: Mon, 21 Mar 2016 17:04:21 +0100 Subject: [PATCH 51/64] webui: options / rights (only defaults) --- libresapi/src/api/ServiceControlHandler.cpp | 37 +++++++++ libresapi/src/webui-src/app/forums.js | 1 + libresapi/src/webui-src/app/menudef.js | 10 +++ libresapi/src/webui-src/app/options.js | 19 +++++ libresapi/src/webui-src/app/retroshare.js | 8 +- libresapi/src/webui-src/app/servicecontrol.js | 83 +++++++++++++++++++ 6 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 libresapi/src/webui-src/app/options.js create mode 100644 libresapi/src/webui-src/app/servicecontrol.js diff --git a/libresapi/src/api/ServiceControlHandler.cpp b/libresapi/src/api/ServiceControlHandler.cpp index dc7a07093..84497f5e5 100644 --- a/libresapi/src/api/ServiceControlHandler.cpp +++ b/libresapi/src/api/ServiceControlHandler.cpp @@ -4,6 +4,8 @@ #include "Operators.h" +#include + namespace resource_api { // maybe move to another place later @@ -73,7 +75,42 @@ void ServiceControlHandler::handleWildcard(Request &req, Response &resp) } else if(req.isPut()) { + // change service default + std::string serviceidtext; + bool enabled; + + req.mStream << makeKeyValueReference("service_id", serviceidtext) + << makeKeyValueReference("default_allowed", enabled); + + RsServicePermissions serv_perms ; + //uint32_t serviceid = fromString(serviceidtext); + uint32_t serviceid = atoi(serviceidtext.c_str()); + if (serviceid == 0) { + resp.setFail("serviceid missed"); + return; + } + + if(!rsServiceControl->getServicePermissions(serviceid, serv_perms)){ + resp.setFail("service_id " + serviceidtext + " is invalid"); + return; + } + + serv_perms.mDefaultAllowed = enabled; + if(serv_perms.mDefaultAllowed) + { + serv_perms.mPeersDenied.clear() ; + } + else + { + serv_perms.mPeersAllowed.clear() ; + } + + ok = rsServiceControl->updateServicePermissions(serviceid,serv_perms); + if (!ok) { + resp.setFail("updateServicePermissions failed"); + return; + } } } if(ok) diff --git a/libresapi/src/webui-src/app/forums.js b/libresapi/src/webui-src/app/forums.js index 289c87b22..423c99262 100644 --- a/libresapi/src/webui-src/app/forums.js +++ b/libresapi/src/webui-src/app/forums.js @@ -6,6 +6,7 @@ var rs = require("retroshare"); module.exports = {view: function(){ return m("div",[ m("h2","forums"), + m("p","(work in progress, currently only listing)"), m("hr"), /* m("div.btn2", { diff --git a/libresapi/src/webui-src/app/menudef.js b/libresapi/src/webui-src/app/menudef.js index 5a4f41640..dd1d63bd4 100644 --- a/libresapi/src/webui-src/app/menudef.js +++ b/libresapi/src/webui-src/app/menudef.js @@ -83,6 +83,16 @@ module.exports = { nodes: [ } }) }, + { + name:"options", + runstate: "running_ok.*", + }, + { + name:"servicecontrol", + runstate: "running_ok.*", + path:"/options/servicecontrol", + show: false, + }, { name: "shutdown", runstate: "running_ok|waiting_account_select", diff --git a/libresapi/src/webui-src/app/options.js b/libresapi/src/webui-src/app/options.js new file mode 100644 index 000000000..2bb8e74c7 --- /dev/null +++ b/libresapi/src/webui-src/app/options.js @@ -0,0 +1,19 @@ +"use strict"; + +var m = require("mithril"); +var rs = require("retroshare"); + +module.exports = { + view: function(){ + return m("div", [ + m("h2","Options"), + m("hr"), + m("div.btn2",{ + onclick: function(){ + m.route("/options/servicecontrol"); + }, + }, "rights") + ]); + } +} + diff --git a/libresapi/src/webui-src/app/retroshare.js b/libresapi/src/webui-src/app/retroshare.js index 2c7f8e500..be1213412 100644 --- a/libresapi/src/webui-src/app/retroshare.js +++ b/libresapi/src/webui-src/app/retroshare.js @@ -253,8 +253,12 @@ function optionsPrep(options, path) { } // force reload for path -rs.forceUpdate = function(path){ - cache[path].requested=false; +rs.forceUpdate = function(path, removeCache){ + if (removeCache === undefined || !removeCache) { + cache[path].requested=false; + } else { + delete cache[path]; + } } //return api-path diff --git a/libresapi/src/webui-src/app/servicecontrol.js b/libresapi/src/webui-src/app/servicecontrol.js new file mode 100644 index 000000000..9d4b36321 --- /dev/null +++ b/libresapi/src/webui-src/app/servicecontrol.js @@ -0,0 +1,83 @@ +"use strict"; + +var m = require("mithril"); +var rs = require("retroshare"); + +function setOption(id,value) { + return function(){ + rs.request("servicecontrol", { + service_id: id, + default_allowed: value, + }); + rs.forceUpdate("servicecontrol", true); + } +} + +module.exports = { + view: function(){ + return m("div", [ + m("h2","Options / Rights"), + m("div.btn2",{ + onclick: function(){ + m.route("/options") + }, + },"back to options"), + m("hr"), + m("ul", rs.list("servicecontrol", function(item){ + //return m("li",item.service_name) + if (item.service_name.match("banlist")) { + console.log("banlist:" + item.default_allowed); + } + return m("li", { + style: { + margin: "2px", + color: item.default_allowed ? "lime" :"red", + } + }, [ + m("div", { + onclick: setOption( + item.service_id, + !item.default_allowed + ), + }, [ + m("div.menu", { + style: { + float:"left", + width:"30px", + color: "#303030", + textAlign: "center", + backgroundColor: !item.default_allowed + ? "black" + : "lime", + } + }, "ON"), + m("div.menu",{ + style: { + float:"left", + width:"30px", + textAlign: "center", + marginRight:"5px", + color: "#303030", + backgroundColor: item.default_allowed + ? "black" + : "lime", + } + }, "OFF"), + ]), + m("div", { + style: { + color: "lime", + } + }, item.service_name), + m("div", { + style: { + clear: "left" + } + }), + ]); + }) + ) + ]); + } +} + From 83ff00e77daf322c7ab8c3afece8745e24e5547e Mon Sep 17 00:00:00 2001 From: zeners Date: Tue, 22 Mar 2016 12:08:23 +0100 Subject: [PATCH 52/64] webui: updated mithril to v0.2.3 no idea what it was before --- libresapi/src/webui-src/app/mithril.js | 3300 +++++++++++------ libresapi/src/webui-src/app/mithril.min.js | 8 + .../src/webui-src/app/mithril.min.js.map | 1 + 3 files changed, 2150 insertions(+), 1159 deletions(-) create mode 100644 libresapi/src/webui-src/app/mithril.min.js create mode 100644 libresapi/src/webui-src/app/mithril.min.js.map diff --git a/libresapi/src/webui-src/app/mithril.js b/libresapi/src/webui-src/app/mithril.js index 318573d8c..d18f81898 100644 --- a/libresapi/src/webui-src/app/mithril.js +++ b/libresapi/src/webui-src/app/mithril.js @@ -1,1159 +1,2141 @@ -var m = (function app(window, undefined) { - var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function"; - var type = {}.toString; - var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; - var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; - var noop = function() {} - - // caching commonly used variables - var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; - - // self invoking function needed because of the way mocks work - function initialize(window){ - $document = window.document; - $location = window.location; - $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout; - $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout; - } - - initialize(window); - - - /** - * @typedef {String} Tag - * A string that looks like -> div.classname#id[param=one][param2=two] - * Which describes a DOM node - */ - - /** - * - * @param {Tag} The DOM node tag - * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs - * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional) - * - */ - function m() { - var args = [].slice.call(arguments); - var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1] || "view" in args[1]) && !("subtree" in args[1]); - var attrs = hasAttrs ? args[1] : {}; - var classAttrName = "class" in attrs ? "class" : "className"; - var cell = {tag: "div", attrs: {}}; - var match, classes = []; - if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string") - while (match = parser.exec(args[0])) { - if (match[1] === "" && match[2]) cell.tag = match[2]; - else if (match[1] === "#") cell.attrs.id = match[2]; - else if (match[1] === ".") classes.push(match[2]); - else if (match[3][0] === "[") { - var pair = attrParser.exec(match[3]); - cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) - } - } - - var children = hasAttrs ? args.slice(2) : args.slice(1); - if (children.length === 1 && type.call(children[0]) === ARRAY) { - cell.children = children[0] - } - else { - cell.children = children - } - - for (var attrName in attrs) { - if (attrs.hasOwnProperty(attrName)) { - if (attrName === classAttrName && attrs[attrName] != null && attrs[attrName] !== "") { - classes.push(attrs[attrName]) - cell.attrs[attrName] = "" //create key in correct iteration order - } - else cell.attrs[attrName] = attrs[attrName] - } - } - if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" "); - - return cell - } - function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { - //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached` - //the diff algorithm can be summarized as this: - //1 - compare `data` and `cached` - //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is - //3 - recursively apply this algorithm for every array and for the children of every virtual element - - //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions: - //- `cached` always has a property called `nodes`, which is a list of DOM elements that correspond to the data represented by the respective virtual element - //- in order to support attaching `nodes` as a property of `cached`, `cached` is *always* a non-primitive object, i.e. if the data was a string, then cached is a String instance. If data was `null` or `undefined`, cached is `new String("")` - //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context) - //- when `cached` is an Object, it represents a virtual element; when it's an Array, it represents a list of elements; when it's a String, Number or Boolean, it represents a text node - - //`parentElement` is a DOM element used for W3C DOM API calls - //`parentTag` is only used for handling a corner case for textarea values - //`parentCache` is used to remove nodes in some multi-node cases - //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable - //`data` and `cached` are, respectively, the new and old nodes being diffed - //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent) - //`editable` is a flag that indicates whether an ancestor is contenteditable - //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor - //`configs` is a list of config functions to run after the topmost `build` call finishes running - - //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings - //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")} - //- it simplifies diffing code - //data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version) - try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""} - if (data.subtree === "retain") return cached; - var cachedType = type.call(cached), dataType = type.call(data); - if (cached == null || cachedType !== dataType) { - if (cached != null) { - if (parentCache && parentCache.nodes) { - var offset = index - parentIndex; - var end = offset + (dataType === ARRAY ? data : cached.nodes).length; - clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) - } - else if (cached.nodes) clear(cached.nodes, cached) - } - cached = new data.constructor; - if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) - cached.nodes = [] - } - - if (dataType === ARRAY) { - //recursively flatten array - for (var i = 0, len = data.length; i < len; i++) { - if (type.call(data[i]) === ARRAY) { - data = data.concat.apply([], data); - i-- //check current index again and flatten until there are no more nested arrays at that index - len = data.length - } - } - - var nodes = [], intact = cached.length === data.length, subArrayCount = 0; - - //keys algorithm: sort elements without recreating them if keys are present - //1) create a map of all existing keys, and mark all for deletion - //2) add new keys to map and mark them for addition - //3) if key exists in new list, change action from deletion to a move - //4) for each key, handle its corresponding action as marked in previous steps - var DELETION = 1, INSERTION = 2 , MOVE = 3; - var existing = {}, shouldMaintainIdentities = false; - for (var i = 0; i < cached.length; i++) { - if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { - shouldMaintainIdentities = true; - existing[cached[i].attrs.key] = {action: DELETION, index: i} - } - } - - var guid = 0 - for (var i = 0, len = data.length; i < len; i++) { - if (data[i] && data[i].attrs && data[i].attrs.key != null) { - for (var j = 0, len = data.length; j < len; j++) { - if (data[j] && data[j].attrs && data[j].attrs.key == null) data[j].attrs.key = "__mithril__" + guid++ - } - break - } - } - - if (shouldMaintainIdentities) { - var keysDiffer = false - if (data.length != cached.length) keysDiffer = true - else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) { - if (cachedCell.attrs && dataCell.attrs && cachedCell.attrs.key != dataCell.attrs.key) { - keysDiffer = true - break - } - } - - if (keysDiffer) { - for (var i = 0, len = data.length; i < len; i++) { - if (data[i] && data[i].attrs) { - if (data[i].attrs.key != null) { - var key = data[i].attrs.key; - if (!existing[key]) existing[key] = {action: INSERTION, index: i}; - else existing[key] = { - action: MOVE, - index: i, - from: existing[key].index, - element: cached.nodes[existing[key].index] || $document.createElement("div") - } - } - } - } - var actions = [] - for (var prop in existing) actions.push(existing[prop]) - var changes = actions.sort(sortChanges); - var newCached = new Array(cached.length) - newCached.nodes = cached.nodes.slice() - - for (var i = 0, change; change = changes[i]; i++) { - if (change.action === DELETION) { - clear(cached[change.index].nodes, cached[change.index]); - newCached.splice(change.index, 1) - } - if (change.action === INSERTION) { - var dummy = $document.createElement("div"); - dummy.key = data[change.index].attrs.key; - parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); - newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) - newCached.nodes[change.index] = dummy - } - - if (change.action === MOVE) { - if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { - parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) - } - newCached[change.index] = cached[change.from] - newCached.nodes[change.index] = change.element - } - } - cached = newCached; - } - } - //end key algorithm - - for (var i = 0, cacheCount = 0, len = data.length; i < len; i++) { - //diff each item in the array - var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs); - if (item === undefined) continue; - if (!item.nodes.intact) intact = false; - if (item.$trusted) { - //fix offset of next element if item was a trusted string w/ more than one html element - //the first clause in the regexp matches elements - //the second clause (after the pipe) matches text nodes - subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length - } - else subArrayCount += type.call(item) === ARRAY ? item.length : 1; - cached[cacheCount++] = item - } - if (!intact) { - //diff the array itself - - //update the list of DOM nodes by collecting the nodes from each item - for (var i = 0, len = data.length; i < len; i++) { - if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) - } - //remove items from the end of the array if the new array is shorter than the old one - //if errors ever happen here, the issue is most likely a bug in the construction of the `cached` data structure somewhere earlier in the program - for (var i = 0, node; node = cached.nodes[i]; i++) { - if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]) - } - if (data.length < cached.length) cached.length = data.length; - cached.nodes = nodes - } - } - else if (data != null && dataType === OBJECT) { - var views = [], controllers = [] - while (data.view) { - var view = data.view.$original || data.view - var controllerIndex = m.redraw.strategy() == "diff" && cached.views ? cached.views.indexOf(view) : -1 - var controller = controllerIndex > -1 ? cached.controllers[controllerIndex] : new (data.controller || noop) - var key = data && data.attrs && data.attrs.key - data = pendingRequests == 0 || (cached && cached.controllers && cached.controllers.indexOf(controller) > -1) ? data.view(controller) : {tag: "placeholder"} - if (data.subtree === "retain") return cached; - if (key) { - if (!data.attrs) data.attrs = {} - data.attrs.key = key - } - if (controller.onunload) unloaders.push({controller: controller, handler: controller.onunload}) - views.push(view) - controllers.push(controller) - } - if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.") - if (!data.attrs) data.attrs = {}; - if (!cached.attrs) cached.attrs = {}; - - var dataAttrKeys = Object.keys(data.attrs) - var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) - //if an element is different enough from the one in cache, recreate it - if (data.tag != cached.tag || dataAttrKeys.sort().join() != Object.keys(cached.attrs).sort().join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (m.redraw.strategy() == "all" && (!cached.configContext || cached.configContext.retain !== true)) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) { - if (cached.nodes.length) clear(cached.nodes); - if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() - if (cached.controllers) { - for (var i = 0, controller; controller = cached.controllers[i]; i++) { - if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop}) - } - } - } - if (type.call(data.tag) != STRING) return; - - var node, isNew = cached.nodes.length === 0; - if (data.attrs.xmlns) namespace = data.attrs.xmlns; - else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; - else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"; - - if (isNew) { - if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); - else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag); - cached = { - tag: data.tag, - //set attributes first, then create children - attrs: hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs, - children: data.children != null && data.children.length > 0 ? - build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : - data.children, - nodes: [node] - }; - if (controllers.length) { - cached.views = views - cached.controllers = controllers - for (var i = 0, controller; controller = controllers[i]; i++) { - if (controller.onunload && controller.onunload.$old) controller.onunload = controller.onunload.$old - if (pendingRequests && controller.onunload) { - var onunload = controller.onunload - controller.onunload = noop - controller.onunload.$old = onunload - } - } - } - - if (cached.children && !cached.children.nodes) cached.children.nodes = []; - //edge case: setting value on
- - - ); - if(this.state.page === "waiting") - return( -
- waiting for response from server... -
- ); - if(this.state.page === "peer") - return( -
-

Do you want to add {this.state.data.name} to your friendslist?

- Allow direct downloads from this node
- Auto download recommended files from this node
-
add to friendslist
-
- ); - }, -}); - -var ProgressBar = React.createClass({ - render: function(){ - return( -
-
-
); - } -}); - -function makeFriendlyUnit(bytes) -{ - if(bytes < 1e3) - return bytes.toFixed(1) + "B"; - if(bytes < 1e6) - return (bytes/1e3).toFixed(1) + "kB"; - if(bytes < 1e9) - return (bytes/1e6).toFixed(1) + "MB"; - if(bytes < 1e12) - return (bytes/1e9).toFixed(1) + "GB"; - return (bytes/1e12).toFixed(1) + "TB"; -} - - -var DownloadsWidget = React.createClass({ - mixins: [AutoUpdateMixin, SignalSlotMixin], - getPath: function(){ return "transfers/downloads";}, - getInitialState: function(){ - return {data: []}; - }, - render: function(){ - var widget = this; - var DL = React.createClass({ - render: function() - { - var file = this.props.data; - var startFn = function(){ - RS.request({ - path: "transfers/control_download", - data: { - action: "start", - id: file.id, - } - }, function(){ - console.log("start dl callback"); - } - )}; - var pauseFn = function(){ - RS.request({ - path: "transfers/control_download", - data: { - action: "pause", - id: file.id, - } - }, function(){ - console.log("pause dl callback"); - } - )}; - var cancelFn = function(){ - RS.request({ - path: "transfers/control_download", - data: { - action: "cancel", - id: file.id, - } - }, function(){ - console.log("cancel dl callback"); - } - )}; - var playFn = function(){ - widget.emit("play_file", {name: file.name, hash: file.hash}) - }; - var playBtn =
; - var splits = file.name.split("."); - if(splits[splits.length-1].toLowerCase() in extensions) - playBtn =
play
; - - var ctrlBtn =
; - if(file.download_status==="paused") - { - ctrlBtn =
start
; - } - else - { - ctrlBtn =
pause
; - } - return( - {this.props.data.name} - {makeFriendlyUnit(this.props.data.size)} - - {makeFriendlyUnit(this.props.data.transfer_rate*1e3)}/s - {this.props.data.download_status} - {ctrlBtn}
cancel
{playBtn} - ); - } - }); - return ( - - - - - - - - - - {this.state.data.map(function(dl){ return
; })} -
namesizecompletedtransfer ratedownload statusactions
); - }, -}); - -var SearchWidget = React.createClass({ - getInitialState: function(){ - return {search_id: undefined}; - }, - handleSearch: function(){ - console.log("searching for: "+this.refs.searchbox.getDOMNode().value); - var search_string = this.refs.searchbox.getDOMNode().value; - RS.request({path: "filesearch/create_search", data:{distant: true, search_string: search_string}}, this.onCreateSearchResponse); - }, - onCreateSearchResponse: function(resp){ - if(this.isMounted()){ - this.setState({search_id: resp.data.search_id}); - } - }, - render: function(){ - var ResultList = React.createClass({ - mixins: [AutoUpdateMixin], - getPath: function(){ return "filesearch/"+this.props.id; }, - getInitialState: function(){ - return {data: []}; - }, - start_download: function(fileinfo){ - RS.request({ - path: "transfers/control_download", - data:{ - action: "begin", - name: fileinfo.name, - size: fileinfo.size, - hash: fileinfo.hash, - }, - }); - }, - render: function(){ - var c2 = this; - var File = React.createClass({ - render: function(){ - var file = this.props.data; - return( - - {file.name} - {file.size} - download - ); - }, - }); - return ( - - - {this.state.data.map( - function(file){ - return ; - } - )} -
namesize
); - } - }); - - var results =
; - if(this.state.search_id !== undefined) - { - results = ; - } - return( -
-

turtle file search

-
- - -
- {results} -
); - }, -}); - -var MediaPlayerWidget = React.createClass({ - mixins: [SignalSlotMixin], - getInitialState: function(){ - return {file: undefined}; - }, - componentWillMount: function(){ - this.connect("play_file", this.play_file); - }, - play_file: function(file){ - this.setState({file: file}); - }, - render: function(){ - if(this.state.file === undefined) - { - return(
); - } - else - { - var splits = this.state.file.name.split("."); - var mediatype = extensions[splits[splits.length - 1].toLowerCase()]; - if (mediatype == "audio") { - return ( -
-

{this.state.file.name}

- -
- ); - } else if (mediatype == "video") { - return( -
-

{this.state.file.name}

- -
- ); - } else { - return(
); - } - } - }, -}); - -var PasswordWidget = React.createClass({ - mixins: [AutoUpdateMixin], - getInitialState: function(){ - return {data: {want_password: false}}; - }, - getPath: function(){ - return "control/password"; - }, - sendPassword: function(){ - RS.request({path: "control/password", data:{password: this.refs.password.getDOMNode().value}}) - }, - render: function(){ - if(this.state.data.want_password === false) - { - return
; - //return(

PasswordWidget: nothing to do.

); - } - else - { - return( -
-

Enter password for key {this.state.data.key_name}

- - -
- ); - } - }, -}); - -var AccountSelectWidget = React.createClass({ - mixins: [AutoUpdateMixin], - getInitialState: function(){ - return {mode: "list", data: []}; - }, - getPath: function(){ - return "control/locations"; - }, - selectAccount: function(id){ - console.log("login with id="+id) - RS.request({path: "control/login", data:{id: id}}); - var state = this.state; - state.mode = "wait"; - this.setState(state); - }, - render: function(){ - var component = this; - if(this.state.mode === "wait") - return

waiting...

; - else - return( -
- { - this.state.data.map(function(location){ - return
login {location.name} ({location.location})
; - }) - } -
- ); - }, -}); - -var LoginWidget = React.createClass({ - mixins: [AutoUpdateMixin], - getInitialState: function(){ - return {data: {runstate: "waiting_init"}}; - }, - getPath: function(){ - return "control/runstate"; - }, - shutdown: function(){ - RS.request({path: "control/shutdown"}); - }, - render: function(){ - if(this.state.data.runstate === "waiting_init") - { - return(

Retroshare is initialising... please wait...

); - } - else if(this.state.data.runstate === "waiting_account_select") - { - return(); - } - else - { - return( -
-

runstate: {this.state.data.runstate}

-
shutdown Retroshare
-
- ); - } - }, -}); - -var LoginWidget2 = React.createClass({ - debug: function(msg){ - console.log(msg); - }, - getInitialState: function(){ - return {display_data:[], state: "waiting", hidden_node: false, error: ""}; - }, - componentDidMount: function() - { - this.update(); - // FOR TESTING - //this.setState({state: "create_location"}); - }, - // this component depends on three resources - // have to fecth all, before can display anything - data: {}, - clear_data: function() - { - this.debug("clear_data()"); - data = - { - have_runstate: false, - runstate: "", - have_identities: false, - identities: [], - have_locations: false, - locations: [], - - pgp_id: undefined, - pgp_name: undefined, - }; - }, - update: function() - { - this.debug("update()"); - var c = this; - c.clear_data(); - function check_data() - { - if(c.data.have_runstate && c.data.have_locations && c.data.have_identities) - { - c.debug("update(): check_data(): have all data, will change to display mode"); - var data = []; - var pgp_ids_with_loc = []; - function pgp_id_has_location(pgp_id) - { - for(var i in pgp_ids_with_loc) - { - if(pgp_id === pgp_ids_with_loc[i]) - return true; - } - return false; - } - // collect all pgp ids with location - // collect location data - for(var i in c.data.locations) - { - if(!pgp_id_has_location(c.data.locations[i].pgp_id)) - pgp_ids_with_loc.push(c.data.locations[i].pgp_id); - data.push(c.data.locations[i]); - } - // collect pgp data without location - for(var i in c.data.identities) - { - if(!pgp_id_has_location(c.data.identities[i].pgp_id)) - data.push(c.data.identities[i]); - } - c.setState({ - display_data: data, - state: "display", - }); - } - } - RS.request({path:"control/runstate"}, function(resp){ - c.debug("update(): got control/runstate"); - if(resp.returncode === "ok"){ - c.data.have_runstate = true; - c.data.runstate = resp.data.runstate; - check_data(); - } - }); - RS.request({path:"control/identities"}, function(resp){ - c.debug("update(): got control/identities"); - if(resp.returncode === "ok"){ - c.data.have_identities = true; - c.data.identities = resp.data; - check_data(); - } - }); - RS.request({path:"control/locations"}, function(resp){ - c.debug("update(): got control/locations"); - if(resp.returncode === "ok"){ - c.data.have_locations = true; - c.data.locations = resp.data; - check_data(); - } - }); - }, - shutdown: function(){ - RS.request({path: "control/shutdown"}); - }, - selectAccount: function(id){ - this.debug("login with id="+id); - RS.request({path: "control/login", data:{id: id}}); - var state = this.state; - state.mode = "wait"; - this.setState(state); - }, - onDrop: function(event) - { - this.debug("onDrop()"); - this.debug(event.dataTransfer.files); - event.preventDefault(); - - var reader = new FileReader(); - - var widget = this; - - var c = this; - reader.onload = function(evt) { - c.debug("onDrop(): file loaded"); - RS.request({path:"control/import_pgp", data:{key_string:evt.target.result}}, c.importCallback); - }; - reader.readAsText(event.dataTransfer.files[0]); - this.setState({state:"waiting"}); - }, - importCallback: function(resp) - { - this.debug("importCallback()"); - console.log(resp); - if(resp.returncode === "ok") - { - this.debug("import ok"); - this.setState({error: "import ok"}); - } - else - { - this.setState({error: "import error"}); - } - this.update(); - - }, - selectAccount: function(id){ - console.log("login with id="+id) - RS.request({path: "control/login", data:{id: id}}); - this.setState({state: "waiting"}); - }, - setupCreateLocation: function(pgp_id, pgp_name) - { - this.data.pgp_id = pgp_id; - this.data.pgp_name = pgp_name; - this.setState({state: "create_location"}); - }, - submitLoc: function() - { - var req = { - path: "control/create_location", - data: { - ssl_name: "nogui-webui" - } - }; - if(this.data.pgp_id !== undefined) - { - req.data["pgp_password"] = this.refs.pwd1.getDOMNode().value; - req.data["pgp_id"] = this.data.pgp_id; - } - else - { - var pgp_name = this.refs.pgp_name.getDOMNode().value - var pwd1 = this.refs.pwd1.getDOMNode().value - var pwd2 = this.refs.pwd2.getDOMNode().value - if(pgp_name === "") - { - this.setState({error:"please fill in a name"}); - return; - } - if(pwd1 === "") - { - this.setState({error:"please fill in a password"}); - return; - } - if(pwd1 !== pwd2) - { - this.setState({error:"passwords do not match"}); - return; - } - req.data["pgp_name"] = pgp_name; - req.data["pgp_password"] = pwd1; - } - if(this.refs.cb_hidden.getDOMNode().checked) - { - var addr = this.refs.tor_addr.getDOMNode().value - var port = this.refs.tor_port.getDOMNode().value - if(addr === "" || port === "") - { - this.setState({error:"please fill in hidden adress and hidden port"}); - return; - } - req.data["hidden_adress"] = addr; - req.data["hidden_port"] = port; - } - var c = this; - RS.request(req, function(resp){ - if(resp.returncode === "ok") - { - c.setState({state:"waiting_end", msg:"created account"}); - } - else - { - c.setState({state:"display", error:"failed to create account: "+resp.debug_msg}) - } - }); - this.setState({state:"waiting"}); - }, - render: function(){ - var c = this; - if(this.state.state === "waiting") - { - return(
-

please wait a second... (LoginWidget2)

-
); - //return(

Retroshare is initialising... please wait...

); - } - //else if(this.state.data.runstate === "waiting_account_select") - else if(this.state.state === "display") - { - if(this.data.runstate === "waiting_account_select") - { - return( -
-
{this.state.error}
- { - this.state.display_data.map(function(loc){ - if(loc.peer_id) - return
{loc.name} ({loc.location})
; - else - return
{loc.name}
; - }) - } -
drag and drop a profile file here
-
create new profile
-
shut down Retroshare
-
); - } - else - { -
-

This is the login page. It has no use at the moment, because Retroshare is aleady running.

-
- } - } - else if(this.state.state === "create_location") - { - return( -
-
Create a new node
- {function(){ - if(c.data.pgp_id !== undefined) - return
- Create new nodw with pgp_id {c.data.pgp_id} - -
- else - return
- - - -
; - }()} - TOR hidden node
- {function(){if(c.state.hidden_node) - return
- - -
;}() - } -
{this.state.error}
-
cancel
-
go
-
- ); - } - else - { - return( -
-
shutdown Retroshare
-
- ); - } - }, -}); - -var Menu = React.createClass({ - mixins: [SignalSlotMixin], - getInitialState: function(){ - return {}; - }, - componentWillMount: function() - { - }, - render: function(){ - var outer = this; - var shutdownbutton; - if(this.props.fullcontrol === true) - shutdownbutton =
shutdown Retroshare
; - else - shutdownbutton =
; - - return ( -
-
- Start -
- {/*function(){ - if(outer.props.fullcontrol) - return (
- Login -
); - else return
; - }()*/} -
- Friends -
-
- Downloads -
-
- Search -
-
- Chatlobbies (unfinished) -
- {shutdownbutton} - {/*
- TestWidget -
*/} -
- ); - }, -}); - -var global_author_identity = null; - -var IdentitySelectorWidget = React.createClass({ - mixins: [AutoUpdateMixin], - getInitialState: function(){ - return {data: []}; - }, - getPath: function(){ - return "identity/own"; - }, - render: function(){ - if(this.state.data.length !== 0) - { - global_author_identity = this.state.data[0].gxs_id; - return
using identity {this.state.data[0].name}
; - } - else - { - return
error: no identity found. create_new_identity not implemented. try a page reload.
; - } - var c = this; - return( -
- { - this.state.data.map(function(id){ - return
{id.name}
; - }) - } -
- ); - }, -}); - -var LobbyListWidget = React.createClass({ - mixins: [AutoUpdateMixin, SignalSlotMixin], - getInitialState: function(){ - return {data: []}; - }, - getPath: function(){ - return "chat/lobbies"; - }, - enterLobby: function(id, chat_id) - { - var c = this; - if(global_author_identity === null) - { - alert("no identity selected, can't join a lobby"); - return; - } - RS.request( - {path:"chat/subscribe_lobby", data:{id:id, gxs_id:global_author_identity}}, - function(){ - c.emit("change_url", {url: "chat/"+chat_id}); - } - ); - }, - render: function(){ - var c = this; - return( -
- { - this.state.data.map(function(lobby){ - return
{lobby.name}
{lobby.topic}
{lobby.unread_msg_count + " unread msgs"}
; - }) - } -
- ); - }, -}); - -// implements automatic update using the state token system -// components using this mixin should have a member "getPath()" to specify the resource -// this widget handles paginated resources -var ListAutoUpdateMixin = -{ - // react component lifecycle callbacks - componentDidMount: function() - { - this._aum_debug("ListAutoUpdateMixin did mount path="+this.getPath()); - this._aum_on_data_changed(); - }, - componentWillUnmount: function() - { - this._aum_debug("ListAutoUpdateMixin will unmount path="+this.getPath()); - RS.unregister_token_listener(this._aum_on_data_changed); - }, - - // private auto update mixin methods - _aum_debug: function(msg) - { - console.log(msg); - }, - _aum_on_data_changed: function() - { - if(this.state.data.length === 0) - { - this._aum_debug("ListAutoUpdateMixin: first request"); - RS.request({path: this.getPath()}, this._aum_response_callback); - } - else - { - this._aum_debug("ListAutoUpdateMixin: requesting elements after id="+this._aum_last_id); - RS.request({path: this.getPath(), data: {begin_after: this.state.data[this.state.data.length-1].id}}, this._aum_response_callback); - } - }, - _aum_response_callback: function(resp) - { - this._aum_debug("Mixin received data: "+JSON.stringify(resp)); - // it is impossible to update the state of an unmounted component - // but it may happen that the component is unmounted before a request finishes - // if response is too late, we drop it - if(!this.isMounted()) - { - this._aum_debug("ListAutoUpdateMixin: component not mounted. Discarding response. path="+this.getPath()); - return; - } - var state = this.state; - if(state.data.length === 0) - { - this._aum_debug("ListAutoUpdateMixin: received first response"); - state.data = resp.data; - } - else - { - this._aum_debug("ListAutoUpdateMixin: appending response to the end"); - state.data = state.data.concat(resp.data); - if(resp.data.length !== 0) - this._aum_last_id = resp.data[resp.data.length-1].id; - } - if(this.onDataUpdated) - this.onDataUpdated(); - this.setState(state); - // load mroe data until there is no more data left - if(resp.data.length === 0) - { - RS.unregister_token_listener(this._aum_on_data_changed); - RS.register_token_listener(this._aum_on_data_changed, resp.statetoken); - } - else - { - this._aum_on_data_changed(); - } - }, -}; - -function getChatTypeString(info) -{ - if(info.is_broadcast) - return "[broadcast]"; - if(info.is_gxs_id) - return "[distant]"; - if(info.is_lobby) - return "[lobby]"; - if(info.is_peer) - return "[friend]"; - return "[unknown chat type]"; -} - -var ChatInfoWidget = React.createClass({ - mixins: [OneTimeUpdateMixin], - getInitialState: function(){ - return {data: null}; - }, - getPath: function(){ - return "chat/info/"+this.props.id; - }, - render: function(){ - if(this.state.data === null) - return
- return( -
- {getChatTypeString(this.state.data)} {this.state.data.remote_author_name} -
-
- ); - }, -}); - -// ChatWidget sets this, and the unread msgs widget ignores unread smgs with this id -var global_current_chat_id = null; - -var UnreadChatMsgsCountWidget = React.createClass({ - mixins: [AutoUpdateMixin, SignalSlotMixin], - getInitialState: function(){ - return {data: []}; - }, - getPath: function(){ - return "chat/unread_msgs"; - }, - render: function(){ - var c = this; - var count = 0; - for(var i in this.state.data) - { - if(this.state.data[i].id !== global_current_chat_id) - count = count + parseInt(this.state.data[i].unread_count); - } - return( -
- {count !== 0? (count + " unread chat messages. click here to read them."): ""} -
- ); - }, -}); - -var UnreadChatMsgsListWidget = React.createClass({ - mixins: [AutoUpdateMixin, SignalSlotMixin], - getInitialState: function(){ - return {data: []}; - }, - getPath: function(){ - return "chat/unread_msgs"; - }, - render: function(){ - var c = this; - return( -
-
Unread Chats:
- {this.state.data.map(function(i){ - return
- {getChatTypeString(i)} {"["+i.unread_count+"]"} {i.remote_author_name} -
; - })} -
- ); - }, -}); - -var LinkWidget = React.createClass({ - getInitialState: function(){ - return {expanded: false}; - }, - render: function(){ - var c = this; - // setting href={something} is unsafe! - // only allow known link types - // we don't want javascript:alert(0) in a link - var http = "http://"; - var https = "https://"; - var retroshare = "retroshare://"; - if(this.props.url.substr(0, http.length) === http - || this.props.url.substr(0, https.lenth) === https - || this.props.url.substr(0, retroshare.length) === retroshare) - { - if(this.state.expanded){ - return
Really follow this link? {this.props.url} close
; - } - else{ - return {this.props.label}; - } - } - else - { - return {"[unsafe link type detected: \""+this.props.url+"\"] "+this.props.label}; - } - }, -}); - -// text: a string -// links: [{first:1, second:2, third:3}] -// text between first and second is the url of the link -// text between secon and third is the link label -function markup(text, links) -{ - function debug(stuff){console.log(stuff)} - var out = []; - var last_link = 0; - debug(text); - debug(links); - for(var i in links) - { - out.push(text.substr(last_link, links[i].first).replace(/ /g, " ")); - var url = text.substr(links[i].first, links[i].second); - url = url.replace(/&/g, "&"); - var label = text.substr(links[i].second, links[i].third); - label = label.replace(/ /g, " "); - last_link = links[i].third; - debug(links[i]); - debug(url); - debug(label); - out.push(); - } - out.push(text.substr(last_link).replace(/ /g, " ")); - debug(out); - return out; -} - -var ChatWidget = React.createClass({ - mixins: [ListAutoUpdateMixin], - getInitialState: function(){ - return {data: []}; - }, - getPath: function(){ - return "chat/messages/"+this.props.id; - }, - onDataUpdated: function() - { - RS.request({path:"chat/mark_chat_as_read/"+this.props.id}); - }, - // react component lifecycle callbacks - componentDidMount: function() - { - global_current_chat_id = this.props.id; - }, - componentWillUnmount: function() - { - global_current_chat_id = null; - }, - render: function(){ - var c = this; - return( -
- - { - this.state.data.map(function(msg){ - return
{new Date(msg.send_time*1000).toLocaleTimeString()} {msg.author_name}: {markup(msg.msg, msg.links)}
; - }) - } - -
- ); - }, -}); - -var TestWidget = React.createClass({ - mixins: [SignalSlotMixin], - getInitialState: function(){ - return {s:"one"}; - }, - componentWillMount: function() - { - }, - one: function(){ - this.setState({s:"one"}); - }, - two: function(){ - this.setState({s:"two"}); - }, - render: function(){ - var outer = this; - var outercontainerstyle = {borderStyle: "solid", borderColor: "darksalmon", overflow: "hidden", width: "100%"}; - var transx = "0px"; - if(this.state.s === "two") - transx = "-45%"; - var innercontainerstyle = {width: "200%", transform: "translatex("+transx+")", WebkitTransform: "translatex("+transx+")", transition: "all 0.5s ease-in-out", WebkitTransition: "all 0.5s ease-in-out"}; - var innerstyle = {float:"left", width: "45%"}; - var two =
; - if(this.state.s === "two") - two =
- two -
; - return ( -
-
-
- one -
- {two} -
-
- ); - }, -}); - -var FileUploadWidget = React.createClass({ - getInitialState: function(){ - // states: - // waiting_user waiting_upload upload_ok upload_failed - return {state: "waiting_user", file_name: "", file_id: 0}; - }, - onDrop: function(event) - { - console.log(event.dataTransfer.files); - event.preventDefault(); - - this.setState({state:"waiting_upload", file_name: event.dataTransfer.files[0].name}); - - var reader = new FileReader(); - var xhr = new XMLHttpRequest(); - - var self = this; - xhr.upload.addEventListener("progress", function(e) { - if (e.lengthComputable) { - var percentage = Math.round((e.loaded * 100) / e.total); - console.log("progress:"+percentage); - } - }, false); - - var widget = this; - xhr.onreadystatechange = function(){ - if (xhr.readyState === 4) { - if(xhr.status !== 200) - { - console.log("upload failed status="+xhr.status); - widget.setState({state: "upload_failed"}); - return; - } - //console.log("upload ok"); - //console.log(JSON.parse(xhr.responseText)); - var resp = JSON.parse(xhr.responseText); - if(resp.ok) - { - widget.setState({state:"upload_ok", file_id: resp.id}); - // tell parent about successful upload - if(widget.props.onUploadReady) - widget.props.onUploadReady(resp.id); - } - else - widget.setState({state:"upload_fail"}); - } - }; - xhr.open("POST", upload_url); - reader.onload = function(evt) { - xhr.send(evt.target.result); - }; - // must read as array buffer, to preserve binary data as it is - reader.readAsArrayBuffer(event.dataTransfer.files[0]); - }, - render: function(){ - var text = "bug-should not happen"; - if(this.state.state ==="waiting_user") - text = "drop a file"; - if(this.state.state ==="waiting_upload") - text = "File " + this.state.file_name + " is being uploaded"; - if(this.state.state ==="upload_ok") - text = "File " + this.state.file_name + " uploaded ok. file_id=" + this.state.file_id; - if(this.state.state ==="upload_fail") - text = "Could not upload file" + this.state.file_name; - - return ( -
- {text} -
- ); - }, -}); - -var MainWidget = React.createClass({ - mixins: [SignalSlotMixin, AutoUpdateMixin], - getPath: function(){ - return "control/runstate"; - }, - getInitialState: function(){ - // hack: get hash - var url = window.location.hash.slice(1); - if(url === "") - url = "menu"; - return {page: url, data: {runstate: "waiting_for_server"}}; - //return {page: "login"}; - }, - componentWillMount: function() - { - var outer = this; - this.connect("url_changed", - function(params) - { - console.log("MainWidget received url_changed. url="+params.url); - var url = params.url; - if(url.length === 0) - url = "menu"; - outer.setState({page: url}); - }); - }, - render: function(){ - console.log("MainWidget render()"); - console.log(this.state); - - var outer = this; - var mainpage =

page not implemented: {this.state.page}

; - - if(this.state.data.runstate === "waiting_for_server") - { - mainpage =

waiting for reply from server...
please wait...

; - } - if(this.state.data.runstate === "waiting_init" || this.state.data.runstate === "waiting_account_select") - { - //mainpage = ; - mainpage = ; - } - - if(this.state.page === "testing") - { - //mainpage = ; - //mainpage = ; - mainpage = ; - } - - if(this.state.data.runstate === "running_ok" || this.state.data.runstate ==="running_ok_no_full_control") - { - if(this.state.page === "main") - { - mainpage =

- A new webinterface for Retroshare. Build with react.js. - React allows to build a modern and user friendly single page application. - The component system makes this very simple. - Updating the GUI is also very simple: one React mixin can handle updating for all components. -

-
; - } - if(this.state.page === "friends") - { - mainpage = ; - } - if(this.state.page === "downloads") - { - mainpage = ; - } - if(this.state.page === "search") - { - mainpage = ; - } - if(this.state.page === "lobbies") - { - mainpage = ; - } - if(this.state.page.split("/")[0] === "chat") - { - mainpage = ; - } - if(this.state.page === "unread_chats") - { - mainpage = ; - } - if(this.state.page === "add_friend") - { - mainpage = ; - } - if(this.state.page === "menu") - { - mainpage = ; - } - mainpage =
- - - - {mainpage} -
; - } - - var menubutton =
<- menu
; - if(this.state.page === "menu") - menubutton =
Retroshare webinterface
; - return ( -
- {/*
test
*/} - - - {menubutton} - {mainpage} - {/**/} -
- ); - }, -}); - -React.render( - , - document.body -); \ No newline at end of file diff --git a/libresapi/src/webfiles/index.html b/libresapi/src/webfiles/index.html deleted file mode 100644 index c8413d5d2..000000000 --- a/libresapi/src/webfiles/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - New webinterface for Retroshare - - - - - - - - - - - - - - - - - -

- - - diff --git a/libresapi/src/webfiles/react.js b/libresapi/src/webfiles/react.js deleted file mode 100644 index 2b767c8d6..000000000 --- a/libresapi/src/webfiles/react.js +++ /dev/null @@ -1,19541 +0,0 @@ -/** - * React v0.13.1 - */ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1) { - if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') { - console.debug( - 'Download the React DevTools for a better development experience: ' + - 'http://fb.me/react-devtools' - ); - } - } - - var expectedFeatures = [ - // shims - Array.isArray, - Array.prototype.every, - Array.prototype.forEach, - Array.prototype.indexOf, - Array.prototype.map, - Date.now, - Function.prototype.bind, - Object.keys, - String.prototype.split, - String.prototype.trim, - - // shams - Object.create, - Object.freeze - ]; - - for (var i = 0; i < expectedFeatures.length; i++) { - if (!expectedFeatures[i]) { - console.error( - 'One or more ES5 shim/shams expected by React are not available: ' + - 'http://fb.me/react-warning-polyfills' - ); - break; - } - } - } -} - -React.version = '0.13.1'; - -module.exports = React; - -},{"117":117,"144":144,"19":19,"21":21,"27":27,"32":32,"33":33,"34":34,"38":38,"39":39,"40":40,"51":51,"54":54,"57":57,"58":58,"66":66,"70":70,"75":75,"78":78,"81":81,"84":84}],2:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule AutoFocusMixin - * @typechecks static-only - */ - -'use strict'; - -var focusNode = _dereq_(119); - -var AutoFocusMixin = { - componentDidMount: function() { - if (this.props.autoFocus) { - focusNode(this.getDOMNode()); - } - } -}; - -module.exports = AutoFocusMixin; - -},{"119":119}],3:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015 Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule BeforeInputEventPlugin - * @typechecks static-only - */ - -'use strict'; - -var EventConstants = _dereq_(15); -var EventPropagators = _dereq_(20); -var ExecutionEnvironment = _dereq_(21); -var FallbackCompositionState = _dereq_(22); -var SyntheticCompositionEvent = _dereq_(93); -var SyntheticInputEvent = _dereq_(97); - -var keyOf = _dereq_(141); - -var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space -var START_KEYCODE = 229; - -var canUseCompositionEvent = ( - ExecutionEnvironment.canUseDOM && - 'CompositionEvent' in window -); - -var documentMode = null; -if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) { - documentMode = document.documentMode; -} - -// Webkit offers a very useful `textInput` event that can be used to -// directly represent `beforeInput`. The IE `textinput` event is not as -// useful, so we don't use it. -var canUseTextInputEvent = ( - ExecutionEnvironment.canUseDOM && - 'TextEvent' in window && - !documentMode && - !isPresto() -); - -// In IE9+, we have access to composition events, but the data supplied -// by the native compositionend event may be incorrect. Japanese ideographic -// spaces, for instance (\u3000) are not recorded correctly. -var useFallbackCompositionData = ( - ExecutionEnvironment.canUseDOM && - ( - (!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11) - ) -); - -/** - * Opera <= 12 includes TextEvent in window, but does not fire - * text input events. Rely on keypress instead. - */ -function isPresto() { - var opera = window.opera; - return ( - typeof opera === 'object' && - typeof opera.version === 'function' && - parseInt(opera.version(), 10) <= 12 - ); -} - -var SPACEBAR_CODE = 32; -var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE); - -var topLevelTypes = EventConstants.topLevelTypes; - -// Events and their corresponding property names. -var eventTypes = { - beforeInput: { - phasedRegistrationNames: { - bubbled: keyOf({onBeforeInput: null}), - captured: keyOf({onBeforeInputCapture: null}) - }, - dependencies: [ - topLevelTypes.topCompositionEnd, - topLevelTypes.topKeyPress, - topLevelTypes.topTextInput, - topLevelTypes.topPaste - ] - }, - compositionEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionEnd: null}), - captured: keyOf({onCompositionEndCapture: null}) - }, - dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionEnd, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown - ] - }, - compositionStart: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionStart: null}), - captured: keyOf({onCompositionStartCapture: null}) - }, - dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionStart, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown - ] - }, - compositionUpdate: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionUpdate: null}), - captured: keyOf({onCompositionUpdateCapture: null}) - }, - dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionUpdate, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown - ] - } -}; - -// Track whether we've ever handled a keypress on the space key. -var hasSpaceKeypress = false; - -/** - * Return whether a native keypress event is assumed to be a command. - * This is required because Firefox fires `keypress` events for key commands - * (cut, copy, select-all, etc.) even though no character is inserted. - */ -function isKeypressCommand(nativeEvent) { - return ( - (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) && - // ctrlKey && altKey is equivalent to AltGr, and is not a command. - !(nativeEvent.ctrlKey && nativeEvent.altKey) - ); -} - - -/** - * Translate native top level events into event types. - * - * @param {string} topLevelType - * @return {object} - */ -function getCompositionEventType(topLevelType) { - switch (topLevelType) { - case topLevelTypes.topCompositionStart: - return eventTypes.compositionStart; - case topLevelTypes.topCompositionEnd: - return eventTypes.compositionEnd; - case topLevelTypes.topCompositionUpdate: - return eventTypes.compositionUpdate; - } -} - -/** - * Does our fallback best-guess model think this event signifies that - * composition has begun? - * - * @param {string} topLevelType - * @param {object} nativeEvent - * @return {boolean} - */ -function isFallbackCompositionStart(topLevelType, nativeEvent) { - return ( - topLevelType === topLevelTypes.topKeyDown && - nativeEvent.keyCode === START_KEYCODE - ); -} - -/** - * Does our fallback mode think that this event is the end of composition? - * - * @param {string} topLevelType - * @param {object} nativeEvent - * @return {boolean} - */ -function isFallbackCompositionEnd(topLevelType, nativeEvent) { - switch (topLevelType) { - case topLevelTypes.topKeyUp: - // Command keys insert or clear IME input. - return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1); - case topLevelTypes.topKeyDown: - // Expect IME keyCode on each keydown. If we get any other - // code we must have exited earlier. - return (nativeEvent.keyCode !== START_KEYCODE); - case topLevelTypes.topKeyPress: - case topLevelTypes.topMouseDown: - case topLevelTypes.topBlur: - // Events are not possible without cancelling IME. - return true; - default: - return false; - } -} - -/** - * Google Input Tools provides composition data via a CustomEvent, - * with the `data` property populated in the `detail` object. If this - * is available on the event object, use it. If not, this is a plain - * composition event and we have nothing special to extract. - * - * @param {object} nativeEvent - * @return {?string} - */ -function getDataFromCustomEvent(nativeEvent) { - var detail = nativeEvent.detail; - if (typeof detail === 'object' && 'data' in detail) { - return detail.data; - } - return null; -} - -// Track the current IME composition fallback object, if any. -var currentComposition = null; - -/** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {?object} A SyntheticCompositionEvent. - */ -function extractCompositionEvent( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent -) { - var eventType; - var fallbackData; - - if (canUseCompositionEvent) { - eventType = getCompositionEventType(topLevelType); - } else if (!currentComposition) { - if (isFallbackCompositionStart(topLevelType, nativeEvent)) { - eventType = eventTypes.compositionStart; - } - } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) { - eventType = eventTypes.compositionEnd; - } - - if (!eventType) { - return null; - } - - if (useFallbackCompositionData) { - // The current composition is stored statically and must not be - // overwritten while composition continues. - if (!currentComposition && eventType === eventTypes.compositionStart) { - currentComposition = FallbackCompositionState.getPooled(topLevelTarget); - } else if (eventType === eventTypes.compositionEnd) { - if (currentComposition) { - fallbackData = currentComposition.getData(); - } - } - } - - var event = SyntheticCompositionEvent.getPooled( - eventType, - topLevelTargetID, - nativeEvent - ); - - if (fallbackData) { - // Inject data generated from fallback path into the synthetic event. - // This matches the property of native CompositionEventInterface. - event.data = fallbackData; - } else { - var customData = getDataFromCustomEvent(nativeEvent); - if (customData !== null) { - event.data = customData; - } - } - - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; -} - -/** - * @param {string} topLevelType Record from `EventConstants`. - * @param {object} nativeEvent Native browser event. - * @return {?string} The string corresponding to this `beforeInput` event. - */ -function getNativeBeforeInputChars(topLevelType, nativeEvent) { - switch (topLevelType) { - case topLevelTypes.topCompositionEnd: - return getDataFromCustomEvent(nativeEvent); - case topLevelTypes.topKeyPress: - /** - * If native `textInput` events are available, our goal is to make - * use of them. However, there is a special case: the spacebar key. - * In Webkit, preventing default on a spacebar `textInput` event - * cancels character insertion, but it *also* causes the browser - * to fall back to its default spacebar behavior of scrolling the - * page. - * - * Tracking at: - * https://code.google.com/p/chromium/issues/detail?id=355103 - * - * To avoid this issue, use the keypress event as if no `textInput` - * event is available. - */ - var which = nativeEvent.which; - if (which !== SPACEBAR_CODE) { - return null; - } - - hasSpaceKeypress = true; - return SPACEBAR_CHAR; - - case topLevelTypes.topTextInput: - // Record the characters to be added to the DOM. - var chars = nativeEvent.data; - - // If it's a spacebar character, assume that we have already handled - // it at the keypress level and bail immediately. Android Chrome - // doesn't give us keycodes, so we need to blacklist it. - if (chars === SPACEBAR_CHAR && hasSpaceKeypress) { - return null; - } - - return chars; - - default: - // For other native event types, do nothing. - return null; - } -} - -/** - * For browsers that do not provide the `textInput` event, extract the - * appropriate string to use for SyntheticInputEvent. - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {object} nativeEvent Native browser event. - * @return {?string} The fallback string for this `beforeInput` event. - */ -function getFallbackBeforeInputChars(topLevelType, nativeEvent) { - // If we are currently composing (IME) and using a fallback to do so, - // try to extract the composed characters from the fallback object. - if (currentComposition) { - if ( - topLevelType === topLevelTypes.topCompositionEnd || - isFallbackCompositionEnd(topLevelType, nativeEvent) - ) { - var chars = currentComposition.getData(); - FallbackCompositionState.release(currentComposition); - currentComposition = null; - return chars; - } - return null; - } - - switch (topLevelType) { - case topLevelTypes.topPaste: - // If a paste event occurs after a keypress, throw out the input - // chars. Paste events should not lead to BeforeInput events. - return null; - case topLevelTypes.topKeyPress: - /** - * As of v27, Firefox may fire keypress events even when no character - * will be inserted. A few possibilities: - * - * - `which` is `0`. Arrow keys, Esc key, etc. - * - * - `which` is the pressed key code, but no char is available. - * Ex: 'AltGr + d` in Polish. There is no modified character for - * this key combination and no character is inserted into the - * document, but FF fires the keypress for char code `100` anyway. - * No `input` event will occur. - * - * - `which` is the pressed key code, but a command combination is - * being used. Ex: `Cmd+C`. No character is inserted, and no - * `input` event will occur. - */ - if (nativeEvent.which && !isKeypressCommand(nativeEvent)) { - return String.fromCharCode(nativeEvent.which); - } - return null; - case topLevelTypes.topCompositionEnd: - return useFallbackCompositionData ? null : nativeEvent.data; - default: - return null; - } -} - -/** - * Extract a SyntheticInputEvent for `beforeInput`, based on either native - * `textInput` or fallback behavior. - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {?object} A SyntheticInputEvent. - */ -function extractBeforeInputEvent( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent -) { - var chars; - - if (canUseTextInputEvent) { - chars = getNativeBeforeInputChars(topLevelType, nativeEvent); - } else { - chars = getFallbackBeforeInputChars(topLevelType, nativeEvent); - } - - // If no characters are being inserted, no BeforeInput event should - // be fired. - if (!chars) { - return null; - } - - var event = SyntheticInputEvent.getPooled( - eventTypes.beforeInput, - topLevelTargetID, - nativeEvent - ); - - event.data = chars; - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; -} - -/** - * Create an `onBeforeInput` event to match - * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents. - * - * This event plugin is based on the native `textInput` event - * available in Chrome, Safari, Opera, and IE. This event fires after - * `onKeyPress` and `onCompositionEnd`, but before `onInput`. - * - * `beforeInput` is spec'd but not implemented in any browsers, and - * the `input` event does not provide any useful information about what has - * actually been added, contrary to the spec. Thus, `textInput` is the best - * available event to identify the characters that have actually been inserted - * into the target node. - * - * This plugin is also responsible for emitting `composition` events, thus - * allowing us to share composition fallback code for both `beforeInput` and - * `composition` event types. - */ -var BeforeInputEventPlugin = { - - eventTypes: eventTypes, - - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ - extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent - ) { - return [ - extractCompositionEvent( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent - ), - extractBeforeInputEvent( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent - ) - ]; - } -}; - -module.exports = BeforeInputEventPlugin; - -},{"141":141,"15":15,"20":20,"21":21,"22":22,"93":93,"97":97}],4:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule CSSProperty - */ - -'use strict'; - -/** - * CSS properties which accept numbers but are not in units of "px". - */ -var isUnitlessNumber = { - boxFlex: true, - boxFlexGroup: true, - columnCount: true, - flex: true, - flexGrow: true, - flexShrink: true, - fontWeight: true, - lineClamp: true, - lineHeight: true, - opacity: true, - order: true, - orphans: true, - widows: true, - zIndex: true, - zoom: true, - - // SVG-related properties - fillOpacity: true, - strokeOpacity: true -}; - -/** - * @param {string} prefix vendor-specific prefix, eg: Webkit - * @param {string} key style name, eg: transitionDuration - * @return {string} style name prefixed with `prefix`, properly camelCased, eg: - * WebkitTransitionDuration - */ -function prefixKey(prefix, key) { - return prefix + key.charAt(0).toUpperCase() + key.substring(1); -} - -/** - * Support style names that may come passed in prefixed by adding permutations - * of vendor prefixes. - */ -var prefixes = ['Webkit', 'ms', 'Moz', 'O']; - -// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an -// infinite loop, because it iterates over the newly added props too. -Object.keys(isUnitlessNumber).forEach(function(prop) { - prefixes.forEach(function(prefix) { - isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]; - }); -}); - -/** - * Most style properties can be unset by doing .style[prop] = '' but IE8 - * doesn't like doing that with shorthand properties so for the properties that - * IE8 breaks on, which are listed here, we instead unset each of the - * individual properties. See http://bugs.jquery.com/ticket/12385. - * The 4-value 'clock' properties like margin, padding, border-width seem to - * behave without any problems. Curiously, list-style works too without any - * special prodding. - */ -var shorthandPropertyExpansions = { - background: { - backgroundImage: true, - backgroundPosition: true, - backgroundRepeat: true, - backgroundColor: true - }, - border: { - borderWidth: true, - borderStyle: true, - borderColor: true - }, - borderBottom: { - borderBottomWidth: true, - borderBottomStyle: true, - borderBottomColor: true - }, - borderLeft: { - borderLeftWidth: true, - borderLeftStyle: true, - borderLeftColor: true - }, - borderRight: { - borderRightWidth: true, - borderRightStyle: true, - borderRightColor: true - }, - borderTop: { - borderTopWidth: true, - borderTopStyle: true, - borderTopColor: true - }, - font: { - fontStyle: true, - fontVariant: true, - fontWeight: true, - fontSize: true, - lineHeight: true, - fontFamily: true - } -}; - -var CSSProperty = { - isUnitlessNumber: isUnitlessNumber, - shorthandPropertyExpansions: shorthandPropertyExpansions -}; - -module.exports = CSSProperty; - -},{}],5:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule CSSPropertyOperations - * @typechecks static-only - */ - -'use strict'; - -var CSSProperty = _dereq_(4); -var ExecutionEnvironment = _dereq_(21); - -var camelizeStyleName = _dereq_(108); -var dangerousStyleValue = _dereq_(113); -var hyphenateStyleName = _dereq_(133); -var memoizeStringOnly = _dereq_(143); -var warning = _dereq_(154); - -var processStyleName = memoizeStringOnly(function(styleName) { - return hyphenateStyleName(styleName); -}); - -var styleFloatAccessor = 'cssFloat'; -if (ExecutionEnvironment.canUseDOM) { - // IE8 only supports accessing cssFloat (standard) as styleFloat - if (document.documentElement.style.cssFloat === undefined) { - styleFloatAccessor = 'styleFloat'; - } -} - -if ("production" !== "development") { - // 'msTransform' is correct, but the other prefixes should be capitalized - var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/; - - // style values shouldn't contain a semicolon - var badStyleValueWithSemicolonPattern = /;\s*$/; - - var warnedStyleNames = {}; - var warnedStyleValues = {}; - - var warnHyphenatedStyleName = function(name) { - if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { - return; - } - - warnedStyleNames[name] = true; - ("production" !== "development" ? warning( - false, - 'Unsupported style property %s. Did you mean %s?', - name, - camelizeStyleName(name) - ) : null); - }; - - var warnBadVendoredStyleName = function(name) { - if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { - return; - } - - warnedStyleNames[name] = true; - ("production" !== "development" ? warning( - false, - 'Unsupported vendor-prefixed style property %s. Did you mean %s?', - name, - name.charAt(0).toUpperCase() + name.slice(1) - ) : null); - }; - - var warnStyleValueWithSemicolon = function(name, value) { - if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) { - return; - } - - warnedStyleValues[value] = true; - ("production" !== "development" ? warning( - false, - 'Style property values shouldn\'t contain a semicolon. ' + - 'Try "%s: %s" instead.', - name, - value.replace(badStyleValueWithSemicolonPattern, '') - ) : null); - }; - - /** - * @param {string} name - * @param {*} value - */ - var warnValidStyle = function(name, value) { - if (name.indexOf('-') > -1) { - warnHyphenatedStyleName(name); - } else if (badVendoredStyleNamePattern.test(name)) { - warnBadVendoredStyleName(name); - } else if (badStyleValueWithSemicolonPattern.test(value)) { - warnStyleValueWithSemicolon(name, value); - } - }; -} - -/** - * Operations for dealing with CSS properties. - */ -var CSSPropertyOperations = { - - /** - * Serializes a mapping of style properties for use as inline styles: - * - * > createMarkupForStyles({width: '200px', height: 0}) - * "width:200px;height:0;" - * - * Undefined values are ignored so that declarative programming is easier. - * The result should be HTML-escaped before insertion into the DOM. - * - * @param {object} styles - * @return {?string} - */ - createMarkupForStyles: function(styles) { - var serialized = ''; - for (var styleName in styles) { - if (!styles.hasOwnProperty(styleName)) { - continue; - } - var styleValue = styles[styleName]; - if ("production" !== "development") { - warnValidStyle(styleName, styleValue); - } - if (styleValue != null) { - serialized += processStyleName(styleName) + ':'; - serialized += dangerousStyleValue(styleName, styleValue) + ';'; - } - } - return serialized || null; - }, - - /** - * Sets the value for multiple styles on a node. If a value is specified as - * '' (empty string), the corresponding style property will be unset. - * - * @param {DOMElement} node - * @param {object} styles - */ - setValueForStyles: function(node, styles) { - var style = node.style; - for (var styleName in styles) { - if (!styles.hasOwnProperty(styleName)) { - continue; - } - if ("production" !== "development") { - warnValidStyle(styleName, styles[styleName]); - } - var styleValue = dangerousStyleValue(styleName, styles[styleName]); - if (styleName === 'float') { - styleName = styleFloatAccessor; - } - if (styleValue) { - style[styleName] = styleValue; - } else { - var expansion = CSSProperty.shorthandPropertyExpansions[styleName]; - if (expansion) { - // Shorthand property that IE8 won't like unsetting, so unset each - // component to placate it - for (var individualStyleName in expansion) { - style[individualStyleName] = ''; - } - } else { - style[styleName] = ''; - } - } - } - } - -}; - -module.exports = CSSPropertyOperations; - -},{"108":108,"113":113,"133":133,"143":143,"154":154,"21":21,"4":4}],6:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule CallbackQueue - */ - -'use strict'; - -var PooledClass = _dereq_(28); - -var assign = _dereq_(27); -var invariant = _dereq_(135); - -/** - * A specialized pseudo-event module to help keep track of components waiting to - * be notified when their DOM representations are available for use. - * - * This implements `PooledClass`, so you should never need to instantiate this. - * Instead, use `CallbackQueue.getPooled()`. - * - * @class ReactMountReady - * @implements PooledClass - * @internal - */ -function CallbackQueue() { - this._callbacks = null; - this._contexts = null; -} - -assign(CallbackQueue.prototype, { - - /** - * Enqueues a callback to be invoked when `notifyAll` is invoked. - * - * @param {function} callback Invoked when `notifyAll` is invoked. - * @param {?object} context Context to call `callback` with. - * @internal - */ - enqueue: function(callback, context) { - this._callbacks = this._callbacks || []; - this._contexts = this._contexts || []; - this._callbacks.push(callback); - this._contexts.push(context); - }, - - /** - * Invokes all enqueued callbacks and clears the queue. This is invoked after - * the DOM representation of a component has been created or updated. - * - * @internal - */ - notifyAll: function() { - var callbacks = this._callbacks; - var contexts = this._contexts; - if (callbacks) { - ("production" !== "development" ? invariant( - callbacks.length === contexts.length, - 'Mismatched list of contexts in callback queue' - ) : invariant(callbacks.length === contexts.length)); - this._callbacks = null; - this._contexts = null; - for (var i = 0, l = callbacks.length; i < l; i++) { - callbacks[i].call(contexts[i]); - } - callbacks.length = 0; - contexts.length = 0; - } - }, - - /** - * Resets the internal queue. - * - * @internal - */ - reset: function() { - this._callbacks = null; - this._contexts = null; - }, - - /** - * `PooledClass` looks for this. - */ - destructor: function() { - this.reset(); - } - -}); - -PooledClass.addPoolingTo(CallbackQueue); - -module.exports = CallbackQueue; - -},{"135":135,"27":27,"28":28}],7:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ChangeEventPlugin - */ - -'use strict'; - -var EventConstants = _dereq_(15); -var EventPluginHub = _dereq_(17); -var EventPropagators = _dereq_(20); -var ExecutionEnvironment = _dereq_(21); -var ReactUpdates = _dereq_(87); -var SyntheticEvent = _dereq_(95); - -var isEventSupported = _dereq_(136); -var isTextInputElement = _dereq_(138); -var keyOf = _dereq_(141); - -var topLevelTypes = EventConstants.topLevelTypes; - -var eventTypes = { - change: { - phasedRegistrationNames: { - bubbled: keyOf({onChange: null}), - captured: keyOf({onChangeCapture: null}) - }, - dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topChange, - topLevelTypes.topClick, - topLevelTypes.topFocus, - topLevelTypes.topInput, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyUp, - topLevelTypes.topSelectionChange - ] - } -}; - -/** - * For IE shims - */ -var activeElement = null; -var activeElementID = null; -var activeElementValue = null; -var activeElementValueProp = null; - -/** - * SECTION: handle `change` event - */ -function shouldUseChangeEvent(elem) { - return ( - elem.nodeName === 'SELECT' || - (elem.nodeName === 'INPUT' && elem.type === 'file') - ); -} - -var doesChangeEventBubble = false; -if (ExecutionEnvironment.canUseDOM) { - // See `handleChange` comment below - doesChangeEventBubble = isEventSupported('change') && ( - (!('documentMode' in document) || document.documentMode > 8) - ); -} - -function manualDispatchChangeEvent(nativeEvent) { - var event = SyntheticEvent.getPooled( - eventTypes.change, - activeElementID, - nativeEvent - ); - EventPropagators.accumulateTwoPhaseDispatches(event); - - // If change and propertychange bubbled, we'd just bind to it like all the - // other events and have it go through ReactBrowserEventEmitter. Since it - // doesn't, we manually listen for the events and so we have to enqueue and - // process the abstract event manually. - // - // Batching is necessary here in order to ensure that all event handlers run - // before the next rerender (including event handlers attached to ancestor - // elements instead of directly on the input). Without this, controlled - // components don't work properly in conjunction with event bubbling because - // the component is rerendered and the value reverted before all the event - // handlers can run. See https://github.com/facebook/react/issues/708. - ReactUpdates.batchedUpdates(runEventInBatch, event); -} - -function runEventInBatch(event) { - EventPluginHub.enqueueEvents(event); - EventPluginHub.processEventQueue(); -} - -function startWatchingForChangeEventIE8(target, targetID) { - activeElement = target; - activeElementID = targetID; - activeElement.attachEvent('onchange', manualDispatchChangeEvent); -} - -function stopWatchingForChangeEventIE8() { - if (!activeElement) { - return; - } - activeElement.detachEvent('onchange', manualDispatchChangeEvent); - activeElement = null; - activeElementID = null; -} - -function getTargetIDForChangeEvent( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topChange) { - return topLevelTargetID; - } -} -function handleEventsForChangeEventIE8( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topFocus) { - // stopWatching() should be a noop here but we call it just in case we - // missed a blur event somehow. - stopWatchingForChangeEventIE8(); - startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID); - } else if (topLevelType === topLevelTypes.topBlur) { - stopWatchingForChangeEventIE8(); - } -} - - -/** - * SECTION: handle `input` event - */ -var isInputEventSupported = false; -if (ExecutionEnvironment.canUseDOM) { - // IE9 claims to support the input event but fails to trigger it when - // deleting text, so we ignore its input events - isInputEventSupported = isEventSupported('input') && ( - (!('documentMode' in document) || document.documentMode > 9) - ); -} - -/** - * (For old IE.) Replacement getter/setter for the `value` property that gets - * set on the active element. - */ -var newValueProp = { - get: function() { - return activeElementValueProp.get.call(this); - }, - set: function(val) { - // Cast to a string so we can do equality checks. - activeElementValue = '' + val; - activeElementValueProp.set.call(this, val); - } -}; - -/** - * (For old IE.) Starts tracking propertychange events on the passed-in element - * and override the value property so that we can distinguish user events from - * value changes in JS. - */ -function startWatchingForValueChange(target, targetID) { - activeElement = target; - activeElementID = targetID; - activeElementValue = target.value; - activeElementValueProp = Object.getOwnPropertyDescriptor( - target.constructor.prototype, - 'value' - ); - - Object.defineProperty(activeElement, 'value', newValueProp); - activeElement.attachEvent('onpropertychange', handlePropertyChange); -} - -/** - * (For old IE.) Removes the event listeners from the currently-tracked element, - * if any exists. - */ -function stopWatchingForValueChange() { - if (!activeElement) { - return; - } - - // delete restores the original property definition - delete activeElement.value; - activeElement.detachEvent('onpropertychange', handlePropertyChange); - - activeElement = null; - activeElementID = null; - activeElementValue = null; - activeElementValueProp = null; -} - -/** - * (For old IE.) Handles a propertychange event, sending a `change` event if - * the value of the active element has changed. - */ -function handlePropertyChange(nativeEvent) { - if (nativeEvent.propertyName !== 'value') { - return; - } - var value = nativeEvent.srcElement.value; - if (value === activeElementValue) { - return; - } - activeElementValue = value; - - manualDispatchChangeEvent(nativeEvent); -} - -/** - * If a `change` event should be fired, returns the target's ID. - */ -function getTargetIDForInputEvent( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topInput) { - // In modern browsers (i.e., not IE8 or IE9), the input event is exactly - // what we want so fall through here and trigger an abstract event - return topLevelTargetID; - } -} - -// For IE8 and IE9. -function handleEventsForInputEventIE( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topFocus) { - // In IE8, we can capture almost all .value changes by adding a - // propertychange handler and looking for events with propertyName - // equal to 'value' - // In IE9, propertychange fires for most input events but is buggy and - // doesn't fire when text is deleted, but conveniently, selectionchange - // appears to fire in all of the remaining cases so we catch those and - // forward the event if the value has changed - // In either case, we don't want to call the event handler if the value - // is changed from JS so we redefine a setter for `.value` that updates - // our activeElementValue variable, allowing us to ignore those changes - // - // stopWatching() should be a noop here but we call it just in case we - // missed a blur event somehow. - stopWatchingForValueChange(); - startWatchingForValueChange(topLevelTarget, topLevelTargetID); - } else if (topLevelType === topLevelTypes.topBlur) { - stopWatchingForValueChange(); - } -} - -// For IE8 and IE9. -function getTargetIDForInputEventIE( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topSelectionChange || - topLevelType === topLevelTypes.topKeyUp || - topLevelType === topLevelTypes.topKeyDown) { - // On the selectionchange event, the target is just document which isn't - // helpful for us so just check activeElement instead. - // - // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire - // propertychange on the first input event after setting `value` from a - // script and fires only keydown, keypress, keyup. Catching keyup usually - // gets it and catching keydown lets us fire an event for the first - // keystroke if user does a key repeat (it'll be a little delayed: right - // before the second keystroke). Other input methods (e.g., paste) seem to - // fire selectionchange normally. - if (activeElement && activeElement.value !== activeElementValue) { - activeElementValue = activeElement.value; - return activeElementID; - } - } -} - - -/** - * SECTION: handle `click` event - */ -function shouldUseClickEvent(elem) { - // Use the `click` event to detect changes to checkbox and radio inputs. - // This approach works across all browsers, whereas `change` does not fire - // until `blur` in IE8. - return ( - elem.nodeName === 'INPUT' && - (elem.type === 'checkbox' || elem.type === 'radio') - ); -} - -function getTargetIDForClickEvent( - topLevelType, - topLevelTarget, - topLevelTargetID) { - if (topLevelType === topLevelTypes.topClick) { - return topLevelTargetID; - } -} - -/** - * This plugin creates an `onChange` event that normalizes change events - * across form elements. This event fires at a time when it's possible to - * change the element's value without seeing a flicker. - * - * Supported elements are: - * - input (see `isTextInputElement`) - * - textarea - * - select - */ -var ChangeEventPlugin = { - - eventTypes: eventTypes, - - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ - extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent) { - - var getTargetIDFunc, handleEventFunc; - if (shouldUseChangeEvent(topLevelTarget)) { - if (doesChangeEventBubble) { - getTargetIDFunc = getTargetIDForChangeEvent; - } else { - handleEventFunc = handleEventsForChangeEventIE8; - } - } else if (isTextInputElement(topLevelTarget)) { - if (isInputEventSupported) { - getTargetIDFunc = getTargetIDForInputEvent; - } else { - getTargetIDFunc = getTargetIDForInputEventIE; - handleEventFunc = handleEventsForInputEventIE; - } - } else if (shouldUseClickEvent(topLevelTarget)) { - getTargetIDFunc = getTargetIDForClickEvent; - } - - if (getTargetIDFunc) { - var targetID = getTargetIDFunc( - topLevelType, - topLevelTarget, - topLevelTargetID - ); - if (targetID) { - var event = SyntheticEvent.getPooled( - eventTypes.change, - targetID, - nativeEvent - ); - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; - } - } - - if (handleEventFunc) { - handleEventFunc( - topLevelType, - topLevelTarget, - topLevelTargetID - ); - } - } - -}; - -module.exports = ChangeEventPlugin; - -},{"136":136,"138":138,"141":141,"15":15,"17":17,"20":20,"21":21,"87":87,"95":95}],8:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ClientReactRootIndex - * @typechecks - */ - -'use strict'; - -var nextReactRootIndex = 0; - -var ClientReactRootIndex = { - createReactRootIndex: function() { - return nextReactRootIndex++; - } -}; - -module.exports = ClientReactRootIndex; - -},{}],9:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule DOMChildrenOperations - * @typechecks static-only - */ - -'use strict'; - -var Danger = _dereq_(12); -var ReactMultiChildUpdateTypes = _dereq_(72); - -var setTextContent = _dereq_(149); -var invariant = _dereq_(135); - -/** - * Inserts `childNode` as a child of `parentNode` at the `index`. - * - * @param {DOMElement} parentNode Parent node in which to insert. - * @param {DOMElement} childNode Child node to insert. - * @param {number} index Index at which to insert the child. - * @internal - */ -function insertChildAt(parentNode, childNode, index) { - // By exploiting arrays returning `undefined` for an undefined index, we can - // rely exclusively on `insertBefore(node, null)` instead of also using - // `appendChild(node)`. However, using `undefined` is not allowed by all - // browsers so we must replace it with `null`. - parentNode.insertBefore( - childNode, - parentNode.childNodes[index] || null - ); -} - -/** - * Operations for updating with DOM children. - */ -var DOMChildrenOperations = { - - dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup, - - updateTextContent: setTextContent, - - /** - * Updates a component's children by processing a series of updates. The - * update configurations are each expected to have a `parentNode` property. - * - * @param {array} updates List of update configurations. - * @param {array} markupList List of markup strings. - * @internal - */ - processUpdates: function(updates, markupList) { - var update; - // Mapping from parent IDs to initial child orderings. - var initialChildren = null; - // List of children that will be moved or removed. - var updatedChildren = null; - - for (var i = 0; i < updates.length; i++) { - update = updates[i]; - if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || - update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) { - var updatedIndex = update.fromIndex; - var updatedChild = update.parentNode.childNodes[updatedIndex]; - var parentID = update.parentID; - - ("production" !== "development" ? invariant( - updatedChild, - 'processUpdates(): Unable to find child %s of element. This ' + - 'probably means the DOM was unexpectedly mutated (e.g., by the ' + - 'browser), usually due to forgetting a when using tables, ' + - 'nesting tags like
,

, or , or using non-SVG elements ' + - 'in an parent. Try inspecting the child nodes of the element ' + - 'with React ID `%s`.', - updatedIndex, - parentID - ) : invariant(updatedChild)); - - initialChildren = initialChildren || {}; - initialChildren[parentID] = initialChildren[parentID] || []; - initialChildren[parentID][updatedIndex] = updatedChild; - - updatedChildren = updatedChildren || []; - updatedChildren.push(updatedChild); - } - } - - var renderedMarkup = Danger.dangerouslyRenderMarkup(markupList); - - // Remove updated children first so that `toIndex` is consistent. - if (updatedChildren) { - for (var j = 0; j < updatedChildren.length; j++) { - updatedChildren[j].parentNode.removeChild(updatedChildren[j]); - } - } - - for (var k = 0; k < updates.length; k++) { - update = updates[k]; - switch (update.type) { - case ReactMultiChildUpdateTypes.INSERT_MARKUP: - insertChildAt( - update.parentNode, - renderedMarkup[update.markupIndex], - update.toIndex - ); - break; - case ReactMultiChildUpdateTypes.MOVE_EXISTING: - insertChildAt( - update.parentNode, - initialChildren[update.parentID][update.fromIndex], - update.toIndex - ); - break; - case ReactMultiChildUpdateTypes.TEXT_CONTENT: - setTextContent( - update.parentNode, - update.textContent - ); - break; - case ReactMultiChildUpdateTypes.REMOVE_NODE: - // Already removed by the for-loop above. - break; - } - } - } - -}; - -module.exports = DOMChildrenOperations; - -},{"12":12,"135":135,"149":149,"72":72}],10:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule DOMProperty - * @typechecks static-only - */ - -/*jslint bitwise: true */ - -'use strict'; - -var invariant = _dereq_(135); - -function checkMask(value, bitmask) { - return (value & bitmask) === bitmask; -} - -var DOMPropertyInjection = { - /** - * Mapping from normalized, camelcased property names to a configuration that - * specifies how the associated DOM property should be accessed or rendered. - */ - MUST_USE_ATTRIBUTE: 0x1, - MUST_USE_PROPERTY: 0x2, - HAS_SIDE_EFFECTS: 0x4, - HAS_BOOLEAN_VALUE: 0x8, - HAS_NUMERIC_VALUE: 0x10, - HAS_POSITIVE_NUMERIC_VALUE: 0x20 | 0x10, - HAS_OVERLOADED_BOOLEAN_VALUE: 0x40, - - /** - * Inject some specialized knowledge about the DOM. This takes a config object - * with the following properties: - * - * isCustomAttribute: function that given an attribute name will return true - * if it can be inserted into the DOM verbatim. Useful for data-* or aria-* - * attributes where it's impossible to enumerate all of the possible - * attribute names, - * - * Properties: object mapping DOM property name to one of the - * DOMPropertyInjection constants or null. If your attribute isn't in here, - * it won't get written to the DOM. - * - * DOMAttributeNames: object mapping React attribute name to the DOM - * attribute name. Attribute names not specified use the **lowercase** - * normalized name. - * - * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties. - * Property names not specified use the normalized name. - * - * DOMMutationMethods: Properties that require special mutation methods. If - * `value` is undefined, the mutation method should unset the property. - * - * @param {object} domPropertyConfig the config as described above. - */ - injectDOMPropertyConfig: function(domPropertyConfig) { - var Properties = domPropertyConfig.Properties || {}; - var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {}; - var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {}; - var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {}; - - if (domPropertyConfig.isCustomAttribute) { - DOMProperty._isCustomAttributeFunctions.push( - domPropertyConfig.isCustomAttribute - ); - } - - for (var propName in Properties) { - ("production" !== "development" ? invariant( - !DOMProperty.isStandardName.hasOwnProperty(propName), - 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' + - '\'%s\' which has already been injected. You may be accidentally ' + - 'injecting the same DOM property config twice, or you may be ' + - 'injecting two configs that have conflicting property names.', - propName - ) : invariant(!DOMProperty.isStandardName.hasOwnProperty(propName))); - - DOMProperty.isStandardName[propName] = true; - - var lowerCased = propName.toLowerCase(); - DOMProperty.getPossibleStandardName[lowerCased] = propName; - - if (DOMAttributeNames.hasOwnProperty(propName)) { - var attributeName = DOMAttributeNames[propName]; - DOMProperty.getPossibleStandardName[attributeName] = propName; - DOMProperty.getAttributeName[propName] = attributeName; - } else { - DOMProperty.getAttributeName[propName] = lowerCased; - } - - DOMProperty.getPropertyName[propName] = - DOMPropertyNames.hasOwnProperty(propName) ? - DOMPropertyNames[propName] : - propName; - - if (DOMMutationMethods.hasOwnProperty(propName)) { - DOMProperty.getMutationMethod[propName] = DOMMutationMethods[propName]; - } else { - DOMProperty.getMutationMethod[propName] = null; - } - - var propConfig = Properties[propName]; - DOMProperty.mustUseAttribute[propName] = - checkMask(propConfig, DOMPropertyInjection.MUST_USE_ATTRIBUTE); - DOMProperty.mustUseProperty[propName] = - checkMask(propConfig, DOMPropertyInjection.MUST_USE_PROPERTY); - DOMProperty.hasSideEffects[propName] = - checkMask(propConfig, DOMPropertyInjection.HAS_SIDE_EFFECTS); - DOMProperty.hasBooleanValue[propName] = - checkMask(propConfig, DOMPropertyInjection.HAS_BOOLEAN_VALUE); - DOMProperty.hasNumericValue[propName] = - checkMask(propConfig, DOMPropertyInjection.HAS_NUMERIC_VALUE); - DOMProperty.hasPositiveNumericValue[propName] = - checkMask(propConfig, DOMPropertyInjection.HAS_POSITIVE_NUMERIC_VALUE); - DOMProperty.hasOverloadedBooleanValue[propName] = - checkMask(propConfig, DOMPropertyInjection.HAS_OVERLOADED_BOOLEAN_VALUE); - - ("production" !== "development" ? invariant( - !DOMProperty.mustUseAttribute[propName] || - !DOMProperty.mustUseProperty[propName], - 'DOMProperty: Cannot require using both attribute and property: %s', - propName - ) : invariant(!DOMProperty.mustUseAttribute[propName] || - !DOMProperty.mustUseProperty[propName])); - ("production" !== "development" ? invariant( - DOMProperty.mustUseProperty[propName] || - !DOMProperty.hasSideEffects[propName], - 'DOMProperty: Properties that have side effects must use property: %s', - propName - ) : invariant(DOMProperty.mustUseProperty[propName] || - !DOMProperty.hasSideEffects[propName])); - ("production" !== "development" ? invariant( - !!DOMProperty.hasBooleanValue[propName] + - !!DOMProperty.hasNumericValue[propName] + - !!DOMProperty.hasOverloadedBooleanValue[propName] <= 1, - 'DOMProperty: Value can be one of boolean, overloaded boolean, or ' + - 'numeric value, but not a combination: %s', - propName - ) : invariant(!!DOMProperty.hasBooleanValue[propName] + - !!DOMProperty.hasNumericValue[propName] + - !!DOMProperty.hasOverloadedBooleanValue[propName] <= 1)); - } - } -}; -var defaultValueCache = {}; - -/** - * DOMProperty exports lookup objects that can be used like functions: - * - * > DOMProperty.isValid['id'] - * true - * > DOMProperty.isValid['foobar'] - * undefined - * - * Although this may be confusing, it performs better in general. - * - * @see http://jsperf.com/key-exists - * @see http://jsperf.com/key-missing - */ -var DOMProperty = { - - ID_ATTRIBUTE_NAME: 'data-reactid', - - /** - * Checks whether a property name is a standard property. - * @type {Object} - */ - isStandardName: {}, - - /** - * Mapping from lowercase property names to the properly cased version, used - * to warn in the case of missing properties. - * @type {Object} - */ - getPossibleStandardName: {}, - - /** - * Mapping from normalized names to attribute names that differ. Attribute - * names are used when rendering markup or with `*Attribute()`. - * @type {Object} - */ - getAttributeName: {}, - - /** - * Mapping from normalized names to properties on DOM node instances. - * (This includes properties that mutate due to external factors.) - * @type {Object} - */ - getPropertyName: {}, - - /** - * Mapping from normalized names to mutation methods. This will only exist if - * mutation cannot be set simply by the property or `setAttribute()`. - * @type {Object} - */ - getMutationMethod: {}, - - /** - * Whether the property must be accessed and mutated as an object property. - * @type {Object} - */ - mustUseAttribute: {}, - - /** - * Whether the property must be accessed and mutated using `*Attribute()`. - * (This includes anything that fails ` in `.) - * @type {Object} - */ - mustUseProperty: {}, - - /** - * Whether or not setting a value causes side effects such as triggering - * resources to be loaded or text selection changes. We must ensure that - * the value is only set if it has changed. - * @type {Object} - */ - hasSideEffects: {}, - - /** - * Whether the property should be removed when set to a falsey value. - * @type {Object} - */ - hasBooleanValue: {}, - - /** - * Whether the property must be numeric or parse as a - * numeric and should be removed when set to a falsey value. - * @type {Object} - */ - hasNumericValue: {}, - - /** - * Whether the property must be positive numeric or parse as a positive - * numeric and should be removed when set to a falsey value. - * @type {Object} - */ - hasPositiveNumericValue: {}, - - /** - * Whether the property can be used as a flag as well as with a value. Removed - * when strictly equal to false; present without a value when strictly equal - * to true; present with a value otherwise. - * @type {Object} - */ - hasOverloadedBooleanValue: {}, - - /** - * All of the isCustomAttribute() functions that have been injected. - */ - _isCustomAttributeFunctions: [], - - /** - * Checks whether a property name is a custom attribute. - * @method - */ - isCustomAttribute: function(attributeName) { - for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) { - var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i]; - if (isCustomAttributeFn(attributeName)) { - return true; - } - } - return false; - }, - - /** - * Returns the default property value for a DOM property (i.e., not an - * attribute). Most default values are '' or false, but not all. Worse yet, - * some (in particular, `type`) vary depending on the type of element. - * - * TODO: Is it better to grab all the possible properties when creating an - * element to avoid having to create the same element twice? - */ - getDefaultValueForProperty: function(nodeName, prop) { - var nodeDefaults = defaultValueCache[nodeName]; - var testElement; - if (!nodeDefaults) { - defaultValueCache[nodeName] = nodeDefaults = {}; - } - if (!(prop in nodeDefaults)) { - testElement = document.createElement(nodeName); - nodeDefaults[prop] = testElement[prop]; - } - return nodeDefaults[prop]; - }, - - injection: DOMPropertyInjection -}; - -module.exports = DOMProperty; - -},{"135":135}],11:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule DOMPropertyOperations - * @typechecks static-only - */ - -'use strict'; - -var DOMProperty = _dereq_(10); - -var quoteAttributeValueForBrowser = _dereq_(147); -var warning = _dereq_(154); - -function shouldIgnoreValue(name, value) { - return value == null || - (DOMProperty.hasBooleanValue[name] && !value) || - (DOMProperty.hasNumericValue[name] && isNaN(value)) || - (DOMProperty.hasPositiveNumericValue[name] && (value < 1)) || - (DOMProperty.hasOverloadedBooleanValue[name] && value === false); -} - -if ("production" !== "development") { - var reactProps = { - children: true, - dangerouslySetInnerHTML: true, - key: true, - ref: true - }; - var warnedProperties = {}; - - var warnUnknownProperty = function(name) { - if (reactProps.hasOwnProperty(name) && reactProps[name] || - warnedProperties.hasOwnProperty(name) && warnedProperties[name]) { - return; - } - - warnedProperties[name] = true; - var lowerCasedName = name.toLowerCase(); - - // data-* attributes should be lowercase; suggest the lowercase version - var standardName = ( - DOMProperty.isCustomAttribute(lowerCasedName) ? - lowerCasedName : - DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) ? - DOMProperty.getPossibleStandardName[lowerCasedName] : - null - ); - - // For now, only warn when we have a suggested correction. This prevents - // logging too much when using transferPropsTo. - ("production" !== "development" ? warning( - standardName == null, - 'Unknown DOM property %s. Did you mean %s?', - name, - standardName - ) : null); - - }; -} - -/** - * Operations for dealing with DOM properties. - */ -var DOMPropertyOperations = { - - /** - * Creates markup for the ID property. - * - * @param {string} id Unescaped ID. - * @return {string} Markup string. - */ - createMarkupForID: function(id) { - return DOMProperty.ID_ATTRIBUTE_NAME + '=' + - quoteAttributeValueForBrowser(id); - }, - - /** - * Creates markup for a property. - * - * @param {string} name - * @param {*} value - * @return {?string} Markup string, or null if the property was invalid. - */ - createMarkupForProperty: function(name, value) { - if (DOMProperty.isStandardName.hasOwnProperty(name) && - DOMProperty.isStandardName[name]) { - if (shouldIgnoreValue(name, value)) { - return ''; - } - var attributeName = DOMProperty.getAttributeName[name]; - if (DOMProperty.hasBooleanValue[name] || - (DOMProperty.hasOverloadedBooleanValue[name] && value === true)) { - return attributeName; - } - return attributeName + '=' + quoteAttributeValueForBrowser(value); - } else if (DOMProperty.isCustomAttribute(name)) { - if (value == null) { - return ''; - } - return name + '=' + quoteAttributeValueForBrowser(value); - } else if ("production" !== "development") { - warnUnknownProperty(name); - } - return null; - }, - - /** - * Sets the value for a property on a node. - * - * @param {DOMElement} node - * @param {string} name - * @param {*} value - */ - setValueForProperty: function(node, name, value) { - if (DOMProperty.isStandardName.hasOwnProperty(name) && - DOMProperty.isStandardName[name]) { - var mutationMethod = DOMProperty.getMutationMethod[name]; - if (mutationMethod) { - mutationMethod(node, value); - } else if (shouldIgnoreValue(name, value)) { - this.deleteValueForProperty(node, name); - } else if (DOMProperty.mustUseAttribute[name]) { - // `setAttribute` with objects becomes only `[object]` in IE8/9, - // ('' + value) makes it output the correct toString()-value. - node.setAttribute(DOMProperty.getAttributeName[name], '' + value); - } else { - var propName = DOMProperty.getPropertyName[name]; - // Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the - // property type before comparing; only `value` does and is string. - if (!DOMProperty.hasSideEffects[name] || - ('' + node[propName]) !== ('' + value)) { - // Contrary to `setAttribute`, object properties are properly - // `toString`ed by IE8/9. - node[propName] = value; - } - } - } else if (DOMProperty.isCustomAttribute(name)) { - if (value == null) { - node.removeAttribute(name); - } else { - node.setAttribute(name, '' + value); - } - } else if ("production" !== "development") { - warnUnknownProperty(name); - } - }, - - /** - * Deletes the value for a property on a node. - * - * @param {DOMElement} node - * @param {string} name - */ - deleteValueForProperty: function(node, name) { - if (DOMProperty.isStandardName.hasOwnProperty(name) && - DOMProperty.isStandardName[name]) { - var mutationMethod = DOMProperty.getMutationMethod[name]; - if (mutationMethod) { - mutationMethod(node, undefined); - } else if (DOMProperty.mustUseAttribute[name]) { - node.removeAttribute(DOMProperty.getAttributeName[name]); - } else { - var propName = DOMProperty.getPropertyName[name]; - var defaultValue = DOMProperty.getDefaultValueForProperty( - node.nodeName, - propName - ); - if (!DOMProperty.hasSideEffects[name] || - ('' + node[propName]) !== defaultValue) { - node[propName] = defaultValue; - } - } - } else if (DOMProperty.isCustomAttribute(name)) { - node.removeAttribute(name); - } else if ("production" !== "development") { - warnUnknownProperty(name); - } - } - -}; - -module.exports = DOMPropertyOperations; - -},{"10":10,"147":147,"154":154}],12:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule Danger - * @typechecks static-only - */ - -/*jslint evil: true, sub: true */ - -'use strict'; - -var ExecutionEnvironment = _dereq_(21); - -var createNodesFromMarkup = _dereq_(112); -var emptyFunction = _dereq_(114); -var getMarkupWrap = _dereq_(127); -var invariant = _dereq_(135); - -var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/; -var RESULT_INDEX_ATTR = 'data-danger-index'; - -/** - * Extracts the `nodeName` from a string of markup. - * - * NOTE: Extracting the `nodeName` does not require a regular expression match - * because we make assumptions about React-generated markup (i.e. there are no - * spaces surrounding the opening tag and there is at least one attribute). - * - * @param {string} markup String of markup. - * @return {string} Node name of the supplied markup. - * @see http://jsperf.com/extract-nodename - */ -function getNodeName(markup) { - return markup.substring(1, markup.indexOf(' ')); -} - -var Danger = { - - /** - * Renders markup into an array of nodes. The markup is expected to render - * into a list of root nodes. Also, the length of `resultList` and - * `markupList` should be the same. - * - * @param {array} markupList List of markup strings to render. - * @return {array} List of rendered nodes. - * @internal - */ - dangerouslyRenderMarkup: function(markupList) { - ("production" !== "development" ? invariant( - ExecutionEnvironment.canUseDOM, - 'dangerouslyRenderMarkup(...): Cannot render markup in a worker ' + - 'thread. Make sure `window` and `document` are available globally ' + - 'before requiring React when unit testing or use ' + - 'React.renderToString for server rendering.' - ) : invariant(ExecutionEnvironment.canUseDOM)); - var nodeName; - var markupByNodeName = {}; - // Group markup by `nodeName` if a wrap is necessary, else by '*'. - for (var i = 0; i < markupList.length; i++) { - ("production" !== "development" ? invariant( - markupList[i], - 'dangerouslyRenderMarkup(...): Missing markup.' - ) : invariant(markupList[i])); - nodeName = getNodeName(markupList[i]); - nodeName = getMarkupWrap(nodeName) ? nodeName : '*'; - markupByNodeName[nodeName] = markupByNodeName[nodeName] || []; - markupByNodeName[nodeName][i] = markupList[i]; - } - var resultList = []; - var resultListAssignmentCount = 0; - for (nodeName in markupByNodeName) { - if (!markupByNodeName.hasOwnProperty(nodeName)) { - continue; - } - var markupListByNodeName = markupByNodeName[nodeName]; - - // This for-in loop skips the holes of the sparse array. The order of - // iteration should follow the order of assignment, which happens to match - // numerical index order, but we don't rely on that. - var resultIndex; - for (resultIndex in markupListByNodeName) { - if (markupListByNodeName.hasOwnProperty(resultIndex)) { - var markup = markupListByNodeName[resultIndex]; - - // Push the requested markup with an additional RESULT_INDEX_ATTR - // attribute. If the markup does not start with a < character, it - // will be discarded below (with an appropriate console.error). - markupListByNodeName[resultIndex] = markup.replace( - OPEN_TAG_NAME_EXP, - // This index will be parsed back out below. - '$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" ' - ); - } - } - - // Render each group of markup with similar wrapping `nodeName`. - var renderNodes = createNodesFromMarkup( - markupListByNodeName.join(''), - emptyFunction // Do nothing special with

; - * } - * }); - * - * The class specification supports a specific protocol of methods that have - * special meaning (e.g. `render`). See `ReactClassInterface` for - * more the comprehensive protocol. Any other properties and methods in the - * class specification will available on the prototype. - * - * @interface ReactClassInterface - * @internal - */ -var ReactClassInterface = { - - /** - * An array of Mixin objects to include when defining your component. - * - * @type {array} - * @optional - */ - mixins: SpecPolicy.DEFINE_MANY, - - /** - * An object containing properties and methods that should be defined on - * the component's constructor instead of its prototype (static methods). - * - * @type {object} - * @optional - */ - statics: SpecPolicy.DEFINE_MANY, - - /** - * Definition of prop types for this component. - * - * @type {object} - * @optional - */ - propTypes: SpecPolicy.DEFINE_MANY, - - /** - * Definition of context types for this component. - * - * @type {object} - * @optional - */ - contextTypes: SpecPolicy.DEFINE_MANY, - - /** - * Definition of context types this component sets for its children. - * - * @type {object} - * @optional - */ - childContextTypes: SpecPolicy.DEFINE_MANY, - - // ==== Definition methods ==== - - /** - * Invoked when the component is mounted. Values in the mapping will be set on - * `this.props` if that prop is not specified (i.e. using an `in` check). - * - * This method is invoked before `getInitialState` and therefore cannot rely - * on `this.state` or use `this.setState`. - * - * @return {object} - * @optional - */ - getDefaultProps: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * Invoked once before the component is mounted. The return value will be used - * as the initial value of `this.state`. - * - * getInitialState: function() { - * return { - * isOn: false, - * fooBaz: new BazFoo() - * } - * } - * - * @return {object} - * @optional - */ - getInitialState: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * @return {object} - * @optional - */ - getChildContext: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * Uses props from `this.props` and state from `this.state` to render the - * structure of the component. - * - * No guarantees are made about when or how often this method is invoked, so - * it must not have side effects. - * - * render: function() { - * var name = this.props.name; - * return
Hello, {name}!
; - * } - * - * @return {ReactComponent} - * @nosideeffects - * @required - */ - render: SpecPolicy.DEFINE_ONCE, - - - - // ==== Delegate methods ==== - - /** - * Invoked when the component is initially created and about to be mounted. - * This may have side effects, but any external subscriptions or data created - * by this method must be cleaned up in `componentWillUnmount`. - * - * @optional - */ - componentWillMount: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component has been mounted and has a DOM representation. - * However, there is no guarantee that the DOM node is in the document. - * - * Use this as an opportunity to operate on the DOM when the component has - * been mounted (initialized and rendered) for the first time. - * - * @param {DOMElement} rootNode DOM element representing the component. - * @optional - */ - componentDidMount: SpecPolicy.DEFINE_MANY, - - /** - * Invoked before the component receives new props. - * - * Use this as an opportunity to react to a prop transition by updating the - * state using `this.setState`. Current props are accessed via `this.props`. - * - * componentWillReceiveProps: function(nextProps, nextContext) { - * this.setState({ - * likesIncreasing: nextProps.likeCount > this.props.likeCount - * }); - * } - * - * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop - * transition may cause a state change, but the opposite is not true. If you - * need it, you are probably looking for `componentWillUpdate`. - * - * @param {object} nextProps - * @optional - */ - componentWillReceiveProps: SpecPolicy.DEFINE_MANY, - - /** - * Invoked while deciding if the component should be updated as a result of - * receiving new props, state and/or context. - * - * Use this as an opportunity to `return false` when you're certain that the - * transition to the new props/state/context will not require a component - * update. - * - * shouldComponentUpdate: function(nextProps, nextState, nextContext) { - * return !equal(nextProps, this.props) || - * !equal(nextState, this.state) || - * !equal(nextContext, this.context); - * } - * - * @param {object} nextProps - * @param {?object} nextState - * @param {?object} nextContext - * @return {boolean} True if the component should update. - * @optional - */ - shouldComponentUpdate: SpecPolicy.DEFINE_ONCE, - - /** - * Invoked when the component is about to update due to a transition from - * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState` - * and `nextContext`. - * - * Use this as an opportunity to perform preparation before an update occurs. - * - * NOTE: You **cannot** use `this.setState()` in this method. - * - * @param {object} nextProps - * @param {?object} nextState - * @param {?object} nextContext - * @param {ReactReconcileTransaction} transaction - * @optional - */ - componentWillUpdate: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component's DOM representation has been updated. - * - * Use this as an opportunity to operate on the DOM when the component has - * been updated. - * - * @param {object} prevProps - * @param {?object} prevState - * @param {?object} prevContext - * @param {DOMElement} rootNode DOM element representing the component. - * @optional - */ - componentDidUpdate: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component is about to be removed from its parent and have - * its DOM representation destroyed. - * - * Use this as an opportunity to deallocate any external resources. - * - * NOTE: There is no `componentDidUnmount` since your component will have been - * destroyed by that point. - * - * @optional - */ - componentWillUnmount: SpecPolicy.DEFINE_MANY, - - - - // ==== Advanced methods ==== - - /** - * Updates the component's currently mounted DOM representation. - * - * By default, this implements React's rendering and reconciliation algorithm. - * Sophisticated clients may wish to override this. - * - * @param {ReactReconcileTransaction} transaction - * @internal - * @overridable - */ - updateComponent: SpecPolicy.OVERRIDE_BASE - -}; - -/** - * Mapping from class specification keys to special processing functions. - * - * Although these are declared like instance properties in the specification - * when defining classes using `React.createClass`, they are actually static - * and are accessible on the constructor instead of the prototype. Despite - * being static, they must be defined outside of the "statics" key under - * which all other static methods are defined. - */ -var RESERVED_SPEC_KEYS = { - displayName: function(Constructor, displayName) { - Constructor.displayName = displayName; - }, - mixins: function(Constructor, mixins) { - if (mixins) { - for (var i = 0; i < mixins.length; i++) { - mixSpecIntoComponent(Constructor, mixins[i]); - } - } - }, - childContextTypes: function(Constructor, childContextTypes) { - if ("production" !== "development") { - validateTypeDef( - Constructor, - childContextTypes, - ReactPropTypeLocations.childContext - ); - } - Constructor.childContextTypes = assign( - {}, - Constructor.childContextTypes, - childContextTypes - ); - }, - contextTypes: function(Constructor, contextTypes) { - if ("production" !== "development") { - validateTypeDef( - Constructor, - contextTypes, - ReactPropTypeLocations.context - ); - } - Constructor.contextTypes = assign( - {}, - Constructor.contextTypes, - contextTypes - ); - }, - /** - * Special case getDefaultProps which should move into statics but requires - * automatic merging. - */ - getDefaultProps: function(Constructor, getDefaultProps) { - if (Constructor.getDefaultProps) { - Constructor.getDefaultProps = createMergedResultFunction( - Constructor.getDefaultProps, - getDefaultProps - ); - } else { - Constructor.getDefaultProps = getDefaultProps; - } - }, - propTypes: function(Constructor, propTypes) { - if ("production" !== "development") { - validateTypeDef( - Constructor, - propTypes, - ReactPropTypeLocations.prop - ); - } - Constructor.propTypes = assign( - {}, - Constructor.propTypes, - propTypes - ); - }, - statics: function(Constructor, statics) { - mixStaticSpecIntoComponent(Constructor, statics); - } -}; - -function validateTypeDef(Constructor, typeDef, location) { - for (var propName in typeDef) { - if (typeDef.hasOwnProperty(propName)) { - // use a warning instead of an invariant so components - // don't show up in prod but not in __DEV__ - ("production" !== "development" ? warning( - typeof typeDef[propName] === 'function', - '%s: %s type `%s` is invalid; it must be a function, usually from ' + - 'React.PropTypes.', - Constructor.displayName || 'ReactClass', - ReactPropTypeLocationNames[location], - propName - ) : null); - } - } -} - -function validateMethodOverride(proto, name) { - var specPolicy = ReactClassInterface.hasOwnProperty(name) ? - ReactClassInterface[name] : - null; - - // Disallow overriding of base class methods unless explicitly allowed. - if (ReactClassMixin.hasOwnProperty(name)) { - ("production" !== "development" ? invariant( - specPolicy === SpecPolicy.OVERRIDE_BASE, - 'ReactClassInterface: You are attempting to override ' + - '`%s` from your class specification. Ensure that your method names ' + - 'do not overlap with React methods.', - name - ) : invariant(specPolicy === SpecPolicy.OVERRIDE_BASE)); - } - - // Disallow defining methods more than once unless explicitly allowed. - if (proto.hasOwnProperty(name)) { - ("production" !== "development" ? invariant( - specPolicy === SpecPolicy.DEFINE_MANY || - specPolicy === SpecPolicy.DEFINE_MANY_MERGED, - 'ReactClassInterface: You are attempting to define ' + - '`%s` on your component more than once. This conflict may be due ' + - 'to a mixin.', - name - ) : invariant(specPolicy === SpecPolicy.DEFINE_MANY || - specPolicy === SpecPolicy.DEFINE_MANY_MERGED)); - } -} - -/** - * Mixin helper which handles policy validation and reserved - * specification keys when building React classses. - */ -function mixSpecIntoComponent(Constructor, spec) { - if (!spec) { - return; - } - - ("production" !== "development" ? invariant( - typeof spec !== 'function', - 'ReactClass: You\'re attempting to ' + - 'use a component class as a mixin. Instead, just use a regular object.' - ) : invariant(typeof spec !== 'function')); - ("production" !== "development" ? invariant( - !ReactElement.isValidElement(spec), - 'ReactClass: You\'re attempting to ' + - 'use a component as a mixin. Instead, just use a regular object.' - ) : invariant(!ReactElement.isValidElement(spec))); - - var proto = Constructor.prototype; - - // By handling mixins before any other properties, we ensure the same - // chaining order is applied to methods with DEFINE_MANY policy, whether - // mixins are listed before or after these methods in the spec. - if (spec.hasOwnProperty(MIXINS_KEY)) { - RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins); - } - - for (var name in spec) { - if (!spec.hasOwnProperty(name)) { - continue; - } - - if (name === MIXINS_KEY) { - // We have already handled mixins in a special case above - continue; - } - - var property = spec[name]; - validateMethodOverride(proto, name); - - if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { - RESERVED_SPEC_KEYS[name](Constructor, property); - } else { - // Setup methods on prototype: - // The following member methods should not be automatically bound: - // 1. Expected ReactClass methods (in the "interface"). - // 2. Overridden methods (that were mixed in). - var isReactClassMethod = - ReactClassInterface.hasOwnProperty(name); - var isAlreadyDefined = proto.hasOwnProperty(name); - var markedDontBind = property && property.__reactDontBind; - var isFunction = typeof property === 'function'; - var shouldAutoBind = - isFunction && - !isReactClassMethod && - !isAlreadyDefined && - !markedDontBind; - - if (shouldAutoBind) { - if (!proto.__reactAutoBindMap) { - proto.__reactAutoBindMap = {}; - } - proto.__reactAutoBindMap[name] = property; - proto[name] = property; - } else { - if (isAlreadyDefined) { - var specPolicy = ReactClassInterface[name]; - - // These cases should already be caught by validateMethodOverride - ("production" !== "development" ? invariant( - isReactClassMethod && ( - (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY) - ), - 'ReactClass: Unexpected spec policy %s for key %s ' + - 'when mixing in component specs.', - specPolicy, - name - ) : invariant(isReactClassMethod && ( - (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY) - ))); - - // For methods which are defined more than once, call the existing - // methods before calling the new property, merging if appropriate. - if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) { - proto[name] = createMergedResultFunction(proto[name], property); - } else if (specPolicy === SpecPolicy.DEFINE_MANY) { - proto[name] = createChainedFunction(proto[name], property); - } - } else { - proto[name] = property; - if ("production" !== "development") { - // Add verbose displayName to the function, which helps when looking - // at profiling tools. - if (typeof property === 'function' && spec.displayName) { - proto[name].displayName = spec.displayName + '_' + name; - } - } - } - } - } - } -} - -function mixStaticSpecIntoComponent(Constructor, statics) { - if (!statics) { - return; - } - for (var name in statics) { - var property = statics[name]; - if (!statics.hasOwnProperty(name)) { - continue; - } - - var isReserved = name in RESERVED_SPEC_KEYS; - ("production" !== "development" ? invariant( - !isReserved, - 'ReactClass: You are attempting to define a reserved ' + - 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + - 'as an instance property instead; it will still be accessible on the ' + - 'constructor.', - name - ) : invariant(!isReserved)); - - var isInherited = name in Constructor; - ("production" !== "development" ? invariant( - !isInherited, - 'ReactClass: You are attempting to define ' + - '`%s` on your component more than once. This conflict may be ' + - 'due to a mixin.', - name - ) : invariant(!isInherited)); - Constructor[name] = property; - } -} - -/** - * Merge two objects, but throw if both contain the same key. - * - * @param {object} one The first object, which is mutated. - * @param {object} two The second object - * @return {object} one after it has been mutated to contain everything in two. - */ -function mergeIntoWithNoDuplicateKeys(one, two) { - ("production" !== "development" ? invariant( - one && two && typeof one === 'object' && typeof two === 'object', - 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.' - ) : invariant(one && two && typeof one === 'object' && typeof two === 'object')); - - for (var key in two) { - if (two.hasOwnProperty(key)) { - ("production" !== "development" ? invariant( - one[key] === undefined, - 'mergeIntoWithNoDuplicateKeys(): ' + - 'Tried to merge two objects with the same key: `%s`. This conflict ' + - 'may be due to a mixin; in particular, this may be caused by two ' + - 'getInitialState() or getDefaultProps() methods returning objects ' + - 'with clashing keys.', - key - ) : invariant(one[key] === undefined)); - one[key] = two[key]; - } - } - return one; -} - -/** - * Creates a function that invokes two functions and merges their return values. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createMergedResultFunction(one, two) { - return function mergedResult() { - var a = one.apply(this, arguments); - var b = two.apply(this, arguments); - if (a == null) { - return b; - } else if (b == null) { - return a; - } - var c = {}; - mergeIntoWithNoDuplicateKeys(c, a); - mergeIntoWithNoDuplicateKeys(c, b); - return c; - }; -} - -/** - * Creates a function that invokes two functions and ignores their return vales. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createChainedFunction(one, two) { - return function chainedFunction() { - one.apply(this, arguments); - two.apply(this, arguments); - }; -} - -/** - * Binds a method to the component. - * - * @param {object} component Component whose method is going to be bound. - * @param {function} method Method to be bound. - * @return {function} The bound method. - */ -function bindAutoBindMethod(component, method) { - var boundMethod = method.bind(component); - if ("production" !== "development") { - boundMethod.__reactBoundContext = component; - boundMethod.__reactBoundMethod = method; - boundMethod.__reactBoundArguments = null; - var componentName = component.constructor.displayName; - var _bind = boundMethod.bind; - /* eslint-disable block-scoped-var, no-undef */ - boundMethod.bind = function(newThis ) {for (var args=[],$__0=1,$__1=arguments.length;$__0<$__1;$__0++) args.push(arguments[$__0]); - // User is trying to bind() an autobound method; we effectively will - // ignore the value of "this" that the user is trying to use, so - // let's warn. - if (newThis !== component && newThis !== null) { - ("production" !== "development" ? warning( - false, - 'bind(): React component methods may only be bound to the ' + - 'component instance. See %s', - componentName - ) : null); - } else if (!args.length) { - ("production" !== "development" ? warning( - false, - 'bind(): You are binding a component method to the component. ' + - 'React does this for you automatically in a high-performance ' + - 'way, so you can safely remove this call. See %s', - componentName - ) : null); - return boundMethod; - } - var reboundMethod = _bind.apply(boundMethod, arguments); - reboundMethod.__reactBoundContext = component; - reboundMethod.__reactBoundMethod = method; - reboundMethod.__reactBoundArguments = args; - return reboundMethod; - /* eslint-enable */ - }; - } - return boundMethod; -} - -/** - * Binds all auto-bound methods in a component. - * - * @param {object} component Component whose method is going to be bound. - */ -function bindAutoBindMethods(component) { - for (var autoBindKey in component.__reactAutoBindMap) { - if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) { - var method = component.__reactAutoBindMap[autoBindKey]; - component[autoBindKey] = bindAutoBindMethod( - component, - ReactErrorUtils.guard( - method, - component.constructor.displayName + '.' + autoBindKey - ) - ); - } - } -} - -var typeDeprecationDescriptor = { - enumerable: false, - get: function() { - var displayName = this.displayName || this.name || 'Component'; - ("production" !== "development" ? warning( - false, - '%s.type is deprecated. Use %s directly to access the class.', - displayName, - displayName - ) : null); - Object.defineProperty(this, 'type', { - value: this - }); - return this; - } -}; - -/** - * Add more to the ReactClass base class. These are all legacy features and - * therefore not already part of the modern ReactComponent. - */ -var ReactClassMixin = { - - /** - * TODO: This will be deprecated because state should always keep a consistent - * type signature and the only use case for this, is to avoid that. - */ - replaceState: function(newState, callback) { - ReactUpdateQueue.enqueueReplaceState(this, newState); - if (callback) { - ReactUpdateQueue.enqueueCallback(this, callback); - } - }, - - /** - * Checks whether or not this composite component is mounted. - * @return {boolean} True if mounted, false otherwise. - * @protected - * @final - */ - isMounted: function() { - if ("production" !== "development") { - var owner = ReactCurrentOwner.current; - if (owner !== null) { - ("production" !== "development" ? warning( - owner._warnedAboutRefsInRender, - '%s is accessing isMounted inside its render() function. ' + - 'render() should be a pure function of props and state. It should ' + - 'never access something that requires stale data from the previous ' + - 'render, such as refs. Move this logic to componentDidMount and ' + - 'componentDidUpdate instead.', - owner.getName() || 'A component' - ) : null); - owner._warnedAboutRefsInRender = true; - } - } - var internalInstance = ReactInstanceMap.get(this); - return ( - internalInstance && - internalInstance !== ReactLifeCycle.currentlyMountingInstance - ); - }, - - /** - * Sets a subset of the props. - * - * @param {object} partialProps Subset of the next props. - * @param {?function} callback Called after props are updated. - * @final - * @public - * @deprecated - */ - setProps: function(partialProps, callback) { - ReactUpdateQueue.enqueueSetProps(this, partialProps); - if (callback) { - ReactUpdateQueue.enqueueCallback(this, callback); - } - }, - - /** - * Replace all the props. - * - * @param {object} newProps Subset of the next props. - * @param {?function} callback Called after props are updated. - * @final - * @public - * @deprecated - */ - replaceProps: function(newProps, callback) { - ReactUpdateQueue.enqueueReplaceProps(this, newProps); - if (callback) { - ReactUpdateQueue.enqueueCallback(this, callback); - } - } -}; - -var ReactClassComponent = function() {}; -assign( - ReactClassComponent.prototype, - ReactComponent.prototype, - ReactClassMixin -); - -/** - * Module for creating composite components. - * - * @class ReactClass - */ -var ReactClass = { - - /** - * Creates a composite component class given a class specification. - * - * @param {object} spec Class specification (which must define `render`). - * @return {function} Component constructor function. - * @public - */ - createClass: function(spec) { - var Constructor = function(props, context) { - // This constructor is overridden by mocks. The argument is used - // by mocks to assert on what gets mounted. - - if ("production" !== "development") { - ("production" !== "development" ? warning( - this instanceof Constructor, - 'Something is calling a React component directly. Use a factory or ' + - 'JSX instead. See: http://fb.me/react-legacyfactory' - ) : null); - } - - // Wire up auto-binding - if (this.__reactAutoBindMap) { - bindAutoBindMethods(this); - } - - this.props = props; - this.context = context; - this.state = null; - - // ReactClasses doesn't have constructors. Instead, they use the - // getInitialState and componentWillMount methods for initialization. - - var initialState = this.getInitialState ? this.getInitialState() : null; - if ("production" !== "development") { - // We allow auto-mocks to proceed as if they're returning null. - if (typeof initialState === 'undefined' && - this.getInitialState._isMockFunction) { - // This is probably bad practice. Consider warning here and - // deprecating this convenience. - initialState = null; - } - } - ("production" !== "development" ? invariant( - typeof initialState === 'object' && !Array.isArray(initialState), - '%s.getInitialState(): must return an object or null', - Constructor.displayName || 'ReactCompositeComponent' - ) : invariant(typeof initialState === 'object' && !Array.isArray(initialState))); - - this.state = initialState; - }; - Constructor.prototype = new ReactClassComponent(); - Constructor.prototype.constructor = Constructor; - - injectedMixins.forEach( - mixSpecIntoComponent.bind(null, Constructor) - ); - - mixSpecIntoComponent(Constructor, spec); - - // Initialize the defaultProps property after all mixins have been merged - if (Constructor.getDefaultProps) { - Constructor.defaultProps = Constructor.getDefaultProps(); - } - - if ("production" !== "development") { - // This is a tag to indicate that the use of these method names is ok, - // since it's used with createClass. If it's not, then it's likely a - // mistake so we'll warn you to use the static property, property - // initializer or constructor respectively. - if (Constructor.getDefaultProps) { - Constructor.getDefaultProps.isReactClassApproved = {}; - } - if (Constructor.prototype.getInitialState) { - Constructor.prototype.getInitialState.isReactClassApproved = {}; - } - } - - ("production" !== "development" ? invariant( - Constructor.prototype.render, - 'createClass(...): Class specification must implement a `render` method.' - ) : invariant(Constructor.prototype.render)); - - if ("production" !== "development") { - ("production" !== "development" ? warning( - !Constructor.prototype.componentShouldUpdate, - '%s has a method called ' + - 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + - 'The name is phrased as a question because the function is ' + - 'expected to return a value.', - spec.displayName || 'A component' - ) : null); - } - - // Reduce time spent doing lookups by setting these on the prototype. - for (var methodName in ReactClassInterface) { - if (!Constructor.prototype[methodName]) { - Constructor.prototype[methodName] = null; - } - } - - // Legacy hook - Constructor.type = Constructor; - if ("production" !== "development") { - try { - Object.defineProperty(Constructor, 'type', typeDeprecationDescriptor); - } catch (x) { - // IE will fail on defineProperty (es5-shim/sham too) - } - } - - return Constructor; - }, - - injection: { - injectMixin: function(mixin) { - injectedMixins.push(mixin); - } - } - -}; - -module.exports = ReactClass; - -},{"135":135,"140":140,"141":141,"154":154,"27":27,"34":34,"39":39,"57":57,"60":60,"67":67,"68":68,"76":76,"77":77,"86":86}],34:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactComponent - */ - -'use strict'; - -var ReactUpdateQueue = _dereq_(86); - -var invariant = _dereq_(135); -var warning = _dereq_(154); - -/** - * Base class helpers for the updating state of a component. - */ -function ReactComponent(props, context) { - this.props = props; - this.context = context; -} - -/** - * Sets a subset of the state. Always use this to mutate - * state. You should treat `this.state` as immutable. - * - * There is no guarantee that `this.state` will be immediately updated, so - * accessing `this.state` after calling this method may return the old value. - * - * There is no guarantee that calls to `setState` will run synchronously, - * as they may eventually be batched together. You can provide an optional - * callback that will be executed when the call to setState is actually - * completed. - * - * When a function is provided to setState, it will be called at some point in - * the future (not synchronously). It will be called with the up to date - * component arguments (state, props, context). These values can be different - * from this.* because your function may be called after receiveProps but before - * shouldComponentUpdate, and this new state, props, and context will not yet be - * assigned to this. - * - * @param {object|function} partialState Next partial state or function to - * produce next partial state to be merged with current state. - * @param {?function} callback Called after state is updated. - * @final - * @protected - */ -ReactComponent.prototype.setState = function(partialState, callback) { - ("production" !== "development" ? invariant( - typeof partialState === 'object' || - typeof partialState === 'function' || - partialState == null, - 'setState(...): takes an object of state variables to update or a ' + - 'function which returns an object of state variables.' - ) : invariant(typeof partialState === 'object' || - typeof partialState === 'function' || - partialState == null)); - if ("production" !== "development") { - ("production" !== "development" ? warning( - partialState != null, - 'setState(...): You passed an undefined or null state object; ' + - 'instead, use forceUpdate().' - ) : null); - } - ReactUpdateQueue.enqueueSetState(this, partialState); - if (callback) { - ReactUpdateQueue.enqueueCallback(this, callback); - } -}; - -/** - * Forces an update. This should only be invoked when it is known with - * certainty that we are **not** in a DOM transaction. - * - * You may want to call this when you know that some deeper aspect of the - * component's state has changed but `setState` was not called. - * - * This will not invoke `shouldComponentUpdate`, but it will invoke - * `componentWillUpdate` and `componentDidUpdate`. - * - * @param {?function} callback Called after update is complete. - * @final - * @protected - */ -ReactComponent.prototype.forceUpdate = function(callback) { - ReactUpdateQueue.enqueueForceUpdate(this); - if (callback) { - ReactUpdateQueue.enqueueCallback(this, callback); - } -}; - -/** - * Deprecated APIs. These APIs used to exist on classic React classes but since - * we would like to deprecate them, we're not going to move them over to this - * modern base class. Instead, we define a getter that warns if it's accessed. - */ -if ("production" !== "development") { - var deprecatedAPIs = { - getDOMNode: 'getDOMNode', - isMounted: 'isMounted', - replaceProps: 'replaceProps', - replaceState: 'replaceState', - setProps: 'setProps' - }; - var defineDeprecationWarning = function(methodName, displayName) { - try { - Object.defineProperty(ReactComponent.prototype, methodName, { - get: function() { - ("production" !== "development" ? warning( - false, - '%s(...) is deprecated in plain JavaScript React classes.', - displayName - ) : null); - return undefined; - } - }); - } catch (x) { - // IE will fail on defineProperty (es5-shim/sham too) - } - }; - for (var fnName in deprecatedAPIs) { - if (deprecatedAPIs.hasOwnProperty(fnName)) { - defineDeprecationWarning(fnName, deprecatedAPIs[fnName]); - } - } -} - -module.exports = ReactComponent; - -},{"135":135,"154":154,"86":86}],35:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactComponentBrowserEnvironment - */ - -/*jslint evil: true */ - -'use strict'; - -var ReactDOMIDOperations = _dereq_(44); -var ReactMount = _dereq_(70); - -/** - * Abstracts away all functionality of the reconciler that requires knowledge of - * the browser context. TODO: These callers should be refactored to avoid the - * need for this injection. - */ -var ReactComponentBrowserEnvironment = { - - processChildrenUpdates: - ReactDOMIDOperations.dangerouslyProcessChildrenUpdates, - - replaceNodeWithMarkupByID: - ReactDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID, - - /** - * If a particular environment requires that some resources be cleaned up, - * specify this in the injected Mixin. In the DOM, we would likely want to - * purge any cached node ID lookups. - * - * @private - */ - unmountIDFromEnvironment: function(rootNodeID) { - ReactMount.purgeID(rootNodeID); - } - -}; - -module.exports = ReactComponentBrowserEnvironment; - -},{"44":44,"70":70}],36:[function(_dereq_,module,exports){ -/** - * Copyright 2014-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactComponentEnvironment - */ - -'use strict'; - -var invariant = _dereq_(135); - -var injected = false; - -var ReactComponentEnvironment = { - - /** - * Optionally injectable environment dependent cleanup hook. (server vs. - * browser etc). Example: A browser system caches DOM nodes based on component - * ID and must remove that cache entry when this instance is unmounted. - */ - unmountIDFromEnvironment: null, - - /** - * Optionally injectable hook for swapping out mount images in the middle of - * the tree. - */ - replaceNodeWithMarkupByID: null, - - /** - * Optionally injectable hook for processing a queue of child updates. Will - * later move into MultiChildComponents. - */ - processChildrenUpdates: null, - - injection: { - injectEnvironment: function(environment) { - ("production" !== "development" ? invariant( - !injected, - 'ReactCompositeComponent: injectEnvironment() can only be called once.' - ) : invariant(!injected)); - ReactComponentEnvironment.unmountIDFromEnvironment = - environment.unmountIDFromEnvironment; - ReactComponentEnvironment.replaceNodeWithMarkupByID = - environment.replaceNodeWithMarkupByID; - ReactComponentEnvironment.processChildrenUpdates = - environment.processChildrenUpdates; - injected = true; - } - } - -}; - -module.exports = ReactComponentEnvironment; - -},{"135":135}],37:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactCompositeComponent - */ - -'use strict'; - -var ReactComponentEnvironment = _dereq_(36); -var ReactContext = _dereq_(38); -var ReactCurrentOwner = _dereq_(39); -var ReactElement = _dereq_(57); -var ReactElementValidator = _dereq_(58); -var ReactInstanceMap = _dereq_(67); -var ReactLifeCycle = _dereq_(68); -var ReactNativeComponent = _dereq_(73); -var ReactPerf = _dereq_(75); -var ReactPropTypeLocations = _dereq_(77); -var ReactPropTypeLocationNames = _dereq_(76); -var ReactReconciler = _dereq_(81); -var ReactUpdates = _dereq_(87); - -var assign = _dereq_(27); -var emptyObject = _dereq_(115); -var invariant = _dereq_(135); -var shouldUpdateReactComponent = _dereq_(151); -var warning = _dereq_(154); - -function getDeclarationErrorAddendum(component) { - var owner = component._currentElement._owner || null; - if (owner) { - var name = owner.getName(); - if (name) { - return ' Check the render method of `' + name + '`.'; - } - } - return ''; -} - -/** - * ------------------ The Life-Cycle of a Composite Component ------------------ - * - * - constructor: Initialization of state. The instance is now retained. - * - componentWillMount - * - render - * - [children's constructors] - * - [children's componentWillMount and render] - * - [children's componentDidMount] - * - componentDidMount - * - * Update Phases: - * - componentWillReceiveProps (only called if parent updated) - * - shouldComponentUpdate - * - componentWillUpdate - * - render - * - [children's constructors or receive props phases] - * - componentDidUpdate - * - * - componentWillUnmount - * - [children's componentWillUnmount] - * - [children destroyed] - * - (destroyed): The instance is now blank, released by React and ready for GC. - * - * ----------------------------------------------------------------------------- - */ - -/** - * An incrementing ID assigned to each component when it is mounted. This is - * used to enforce the order in which `ReactUpdates` updates dirty components. - * - * @private - */ -var nextMountID = 1; - -/** - * @lends {ReactCompositeComponent.prototype} - */ -var ReactCompositeComponentMixin = { - - /** - * Base constructor for all composite component. - * - * @param {ReactElement} element - * @final - * @internal - */ - construct: function(element) { - this._currentElement = element; - this._rootNodeID = null; - this._instance = null; - - // See ReactUpdateQueue - this._pendingElement = null; - this._pendingStateQueue = null; - this._pendingReplaceState = false; - this._pendingForceUpdate = false; - - this._renderedComponent = null; - - this._context = null; - this._mountOrder = 0; - this._isTopLevel = false; - - // See ReactUpdates and ReactUpdateQueue. - this._pendingCallbacks = null; - }, - - /** - * Initializes the component, renders markup, and registers event listeners. - * - * @param {string} rootID DOM ID of the root node. - * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction - * @return {?string} Rendered markup to be inserted into the DOM. - * @final - * @internal - */ - mountComponent: function(rootID, transaction, context) { - this._context = context; - this._mountOrder = nextMountID++; - this._rootNodeID = rootID; - - var publicProps = this._processProps(this._currentElement.props); - var publicContext = this._processContext(this._currentElement._context); - - var Component = ReactNativeComponent.getComponentClassForElement( - this._currentElement - ); - - // Initialize the public class - var inst = new Component(publicProps, publicContext); - - if ("production" !== "development") { - // This will throw later in _renderValidatedComponent, but add an early - // warning now to help debugging - ("production" !== "development" ? warning( - inst.render != null, - '%s(...): No `render` method found on the returned component ' + - 'instance: you may have forgotten to define `render` in your ' + - 'component or you may have accidentally tried to render an element ' + - 'whose type is a function that isn\'t a React component.', - Component.displayName || Component.name || 'Component' - ) : null); - } - - // These should be set up in the constructor, but as a convenience for - // simpler class abstractions, we set them up after the fact. - inst.props = publicProps; - inst.context = publicContext; - inst.refs = emptyObject; - - this._instance = inst; - - // Store a reference from the instance back to the internal representation - ReactInstanceMap.set(inst, this); - - if ("production" !== "development") { - this._warnIfContextsDiffer(this._currentElement._context, context); - } - - if ("production" !== "development") { - // Since plain JS classes are defined without any special initialization - // logic, we can not catch common errors early. Therefore, we have to - // catch them here, at initialization time, instead. - ("production" !== "development" ? warning( - !inst.getInitialState || - inst.getInitialState.isReactClassApproved, - 'getInitialState was defined on %s, a plain JavaScript class. ' + - 'This is only supported for classes created using React.createClass. ' + - 'Did you mean to define a state property instead?', - this.getName() || 'a component' - ) : null); - ("production" !== "development" ? warning( - !inst.propTypes, - 'propTypes was defined as an instance property on %s. Use a static ' + - 'property to define propTypes instead.', - this.getName() || 'a component' - ) : null); - ("production" !== "development" ? warning( - !inst.contextTypes, - 'contextTypes was defined as an instance property on %s. Use a ' + - 'static property to define contextTypes instead.', - this.getName() || 'a component' - ) : null); - ("production" !== "development" ? warning( - typeof inst.componentShouldUpdate !== 'function', - '%s has a method called ' + - 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + - 'The name is phrased as a question because the function is ' + - 'expected to return a value.', - (this.getName() || 'A component') - ) : null); - } - - var initialState = inst.state; - if (initialState === undefined) { - inst.state = initialState = null; - } - ("production" !== "development" ? invariant( - typeof initialState === 'object' && !Array.isArray(initialState), - '%s.state: must be set to an object or null', - this.getName() || 'ReactCompositeComponent' - ) : invariant(typeof initialState === 'object' && !Array.isArray(initialState))); - - this._pendingStateQueue = null; - this._pendingReplaceState = false; - this._pendingForceUpdate = false; - - var renderedElement; - - var previouslyMounting = ReactLifeCycle.currentlyMountingInstance; - ReactLifeCycle.currentlyMountingInstance = this; - try { - if (inst.componentWillMount) { - inst.componentWillMount(); - // When mounting, calls to `setState` by `componentWillMount` will set - // `this._pendingStateQueue` without triggering a re-render. - if (this._pendingStateQueue) { - inst.state = this._processPendingState(inst.props, inst.context); - } - } - - renderedElement = this._renderValidatedComponent(); - } finally { - ReactLifeCycle.currentlyMountingInstance = previouslyMounting; - } - - this._renderedComponent = this._instantiateReactComponent( - renderedElement, - this._currentElement.type // The wrapping type - ); - - var markup = ReactReconciler.mountComponent( - this._renderedComponent, - rootID, - transaction, - this._processChildContext(context) - ); - if (inst.componentDidMount) { - transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); - } - - return markup; - }, - - /** - * Releases any resources allocated by `mountComponent`. - * - * @final - * @internal - */ - unmountComponent: function() { - var inst = this._instance; - - if (inst.componentWillUnmount) { - var previouslyUnmounting = ReactLifeCycle.currentlyUnmountingInstance; - ReactLifeCycle.currentlyUnmountingInstance = this; - try { - inst.componentWillUnmount(); - } finally { - ReactLifeCycle.currentlyUnmountingInstance = previouslyUnmounting; - } - } - - ReactReconciler.unmountComponent(this._renderedComponent); - this._renderedComponent = null; - - // Reset pending fields - this._pendingStateQueue = null; - this._pendingReplaceState = false; - this._pendingForceUpdate = false; - this._pendingCallbacks = null; - this._pendingElement = null; - - // These fields do not really need to be reset since this object is no - // longer accessible. - this._context = null; - this._rootNodeID = null; - - // Delete the reference from the instance to this internal representation - // which allow the internals to be properly cleaned up even if the user - // leaks a reference to the public instance. - ReactInstanceMap.remove(inst); - - // Some existing components rely on inst.props even after they've been - // destroyed (in event handlers). - // TODO: inst.props = null; - // TODO: inst.state = null; - // TODO: inst.context = null; - }, - - /** - * Schedule a partial update to the props. Only used for internal testing. - * - * @param {object} partialProps Subset of the next props. - * @param {?function} callback Called after props are updated. - * @final - * @internal - */ - _setPropsInternal: function(partialProps, callback) { - // This is a deoptimized path. We optimize for always having an element. - // This creates an extra internal element. - var element = this._pendingElement || this._currentElement; - this._pendingElement = ReactElement.cloneAndReplaceProps( - element, - assign({}, element.props, partialProps) - ); - ReactUpdates.enqueueUpdate(this, callback); - }, - - /** - * Filters the context object to only contain keys specified in - * `contextTypes` - * - * @param {object} context - * @return {?object} - * @private - */ - _maskContext: function(context) { - var maskedContext = null; - // This really should be getting the component class for the element, - // but we know that we're not going to need it for built-ins. - if (typeof this._currentElement.type === 'string') { - return emptyObject; - } - var contextTypes = this._currentElement.type.contextTypes; - if (!contextTypes) { - return emptyObject; - } - maskedContext = {}; - for (var contextName in contextTypes) { - maskedContext[contextName] = context[contextName]; - } - return maskedContext; - }, - - /** - * Filters the context object to only contain keys specified in - * `contextTypes`, and asserts that they are valid. - * - * @param {object} context - * @return {?object} - * @private - */ - _processContext: function(context) { - var maskedContext = this._maskContext(context); - if ("production" !== "development") { - var Component = ReactNativeComponent.getComponentClassForElement( - this._currentElement - ); - if (Component.contextTypes) { - this._checkPropTypes( - Component.contextTypes, - maskedContext, - ReactPropTypeLocations.context - ); - } - } - return maskedContext; - }, - - /** - * @param {object} currentContext - * @return {object} - * @private - */ - _processChildContext: function(currentContext) { - var inst = this._instance; - var childContext = inst.getChildContext && inst.getChildContext(); - if (childContext) { - ("production" !== "development" ? invariant( - typeof inst.constructor.childContextTypes === 'object', - '%s.getChildContext(): childContextTypes must be defined in order to ' + - 'use getChildContext().', - this.getName() || 'ReactCompositeComponent' - ) : invariant(typeof inst.constructor.childContextTypes === 'object')); - if ("production" !== "development") { - this._checkPropTypes( - inst.constructor.childContextTypes, - childContext, - ReactPropTypeLocations.childContext - ); - } - for (var name in childContext) { - ("production" !== "development" ? invariant( - name in inst.constructor.childContextTypes, - '%s.getChildContext(): key "%s" is not defined in childContextTypes.', - this.getName() || 'ReactCompositeComponent', - name - ) : invariant(name in inst.constructor.childContextTypes)); - } - return assign({}, currentContext, childContext); - } - return currentContext; - }, - - /** - * Processes props by setting default values for unspecified props and - * asserting that the props are valid. Does not mutate its argument; returns - * a new props object with defaults merged in. - * - * @param {object} newProps - * @return {object} - * @private - */ - _processProps: function(newProps) { - if ("production" !== "development") { - var Component = ReactNativeComponent.getComponentClassForElement( - this._currentElement - ); - if (Component.propTypes) { - this._checkPropTypes( - Component.propTypes, - newProps, - ReactPropTypeLocations.prop - ); - } - } - return newProps; - }, - - /** - * Assert that the props are valid - * - * @param {object} propTypes Map of prop name to a ReactPropType - * @param {object} props - * @param {string} location e.g. "prop", "context", "child context" - * @private - */ - _checkPropTypes: function(propTypes, props, location) { - // TODO: Stop validating prop types here and only use the element - // validation. - var componentName = this.getName(); - for (var propName in propTypes) { - if (propTypes.hasOwnProperty(propName)) { - var error; - try { - // This is intentionally an invariant that gets caught. It's the same - // behavior as without this statement except with a better message. - ("production" !== "development" ? invariant( - typeof propTypes[propName] === 'function', - '%s: %s type `%s` is invalid; it must be a function, usually ' + - 'from React.PropTypes.', - componentName || 'React class', - ReactPropTypeLocationNames[location], - propName - ) : invariant(typeof propTypes[propName] === 'function')); - error = propTypes[propName](props, propName, componentName, location); - } catch (ex) { - error = ex; - } - if (error instanceof Error) { - // We may want to extend this logic for similar errors in - // React.render calls, so I'm abstracting it away into - // a function to minimize refactoring in the future - var addendum = getDeclarationErrorAddendum(this); - - if (location === ReactPropTypeLocations.prop) { - // Preface gives us something to blacklist in warning module - ("production" !== "development" ? warning( - false, - 'Failed Composite propType: %s%s', - error.message, - addendum - ) : null); - } else { - ("production" !== "development" ? warning( - false, - 'Failed Context Types: %s%s', - error.message, - addendum - ) : null); - } - } - } - } - }, - - receiveComponent: function(nextElement, transaction, nextContext) { - var prevElement = this._currentElement; - var prevContext = this._context; - - this._pendingElement = null; - - this.updateComponent( - transaction, - prevElement, - nextElement, - prevContext, - nextContext - ); - }, - - /** - * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate` - * is set, update the component. - * - * @param {ReactReconcileTransaction} transaction - * @internal - */ - performUpdateIfNecessary: function(transaction) { - if (this._pendingElement != null) { - ReactReconciler.receiveComponent( - this, - this._pendingElement || this._currentElement, - transaction, - this._context - ); - } - - if (this._pendingStateQueue !== null || this._pendingForceUpdate) { - if ("production" !== "development") { - ReactElementValidator.checkAndWarnForMutatedProps( - this._currentElement - ); - } - - this.updateComponent( - transaction, - this._currentElement, - this._currentElement, - this._context, - this._context - ); - } - }, - - /** - * Compare two contexts, warning if they are different - * TODO: Remove this check when owner-context is removed - */ - _warnIfContextsDiffer: function(ownerBasedContext, parentBasedContext) { - ownerBasedContext = this._maskContext(ownerBasedContext); - parentBasedContext = this._maskContext(parentBasedContext); - var parentKeys = Object.keys(parentBasedContext).sort(); - var displayName = this.getName() || 'ReactCompositeComponent'; - for (var i = 0; i < parentKeys.length; i++) { - var key = parentKeys[i]; - ("production" !== "development" ? warning( - ownerBasedContext[key] === parentBasedContext[key], - 'owner-based and parent-based contexts differ ' + - '(values: `%s` vs `%s`) for key (%s) while mounting %s ' + - '(see: http://fb.me/react-context-by-parent)', - ownerBasedContext[key], - parentBasedContext[key], - key, - displayName - ) : null); - } - }, - - /** - * Perform an update to a mounted component. The componentWillReceiveProps and - * shouldComponentUpdate methods are called, then (assuming the update isn't - * skipped) the remaining update lifecycle methods are called and the DOM - * representation is updated. - * - * By default, this implements React's rendering and reconciliation algorithm. - * Sophisticated clients may wish to override this. - * - * @param {ReactReconcileTransaction} transaction - * @param {ReactElement} prevParentElement - * @param {ReactElement} nextParentElement - * @internal - * @overridable - */ - updateComponent: function( - transaction, - prevParentElement, - nextParentElement, - prevUnmaskedContext, - nextUnmaskedContext - ) { - var inst = this._instance; - - var nextContext = inst.context; - var nextProps = inst.props; - - // Distinguish between a props update versus a simple state update - if (prevParentElement !== nextParentElement) { - nextContext = this._processContext(nextParentElement._context); - nextProps = this._processProps(nextParentElement.props); - - if ("production" !== "development") { - if (nextUnmaskedContext != null) { - this._warnIfContextsDiffer( - nextParentElement._context, - nextUnmaskedContext - ); - } - } - - // An update here will schedule an update but immediately set - // _pendingStateQueue which will ensure that any state updates gets - // immediately reconciled instead of waiting for the next batch. - - if (inst.componentWillReceiveProps) { - inst.componentWillReceiveProps(nextProps, nextContext); - } - } - - var nextState = this._processPendingState(nextProps, nextContext); - - var shouldUpdate = - this._pendingForceUpdate || - !inst.shouldComponentUpdate || - inst.shouldComponentUpdate(nextProps, nextState, nextContext); - - if ("production" !== "development") { - ("production" !== "development" ? warning( - typeof shouldUpdate !== 'undefined', - '%s.shouldComponentUpdate(): Returned undefined instead of a ' + - 'boolean value. Make sure to return true or false.', - this.getName() || 'ReactCompositeComponent' - ) : null); - } - - if (shouldUpdate) { - this._pendingForceUpdate = false; - // Will set `this.props`, `this.state` and `this.context`. - this._performComponentUpdate( - nextParentElement, - nextProps, - nextState, - nextContext, - transaction, - nextUnmaskedContext - ); - } else { - // If it's determined that a component should not update, we still want - // to set props and state but we shortcut the rest of the update. - this._currentElement = nextParentElement; - this._context = nextUnmaskedContext; - inst.props = nextProps; - inst.state = nextState; - inst.context = nextContext; - } - }, - - _processPendingState: function(props, context) { - var inst = this._instance; - var queue = this._pendingStateQueue; - var replace = this._pendingReplaceState; - this._pendingReplaceState = false; - this._pendingStateQueue = null; - - if (!queue) { - return inst.state; - } - - var nextState = assign({}, replace ? queue[0] : inst.state); - for (var i = replace ? 1 : 0; i < queue.length; i++) { - var partial = queue[i]; - assign( - nextState, - typeof partial === 'function' ? - partial.call(inst, nextState, props, context) : - partial - ); - } - - return nextState; - }, - - /** - * Merges new props and state, notifies delegate methods of update and - * performs update. - * - * @param {ReactElement} nextElement Next element - * @param {object} nextProps Next public object to set as properties. - * @param {?object} nextState Next object to set as state. - * @param {?object} nextContext Next public object to set as context. - * @param {ReactReconcileTransaction} transaction - * @param {?object} unmaskedContext - * @private - */ - _performComponentUpdate: function( - nextElement, - nextProps, - nextState, - nextContext, - transaction, - unmaskedContext - ) { - var inst = this._instance; - - var prevProps = inst.props; - var prevState = inst.state; - var prevContext = inst.context; - - if (inst.componentWillUpdate) { - inst.componentWillUpdate(nextProps, nextState, nextContext); - } - - this._currentElement = nextElement; - this._context = unmaskedContext; - inst.props = nextProps; - inst.state = nextState; - inst.context = nextContext; - - this._updateRenderedComponent(transaction, unmaskedContext); - - if (inst.componentDidUpdate) { - transaction.getReactMountReady().enqueue( - inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), - inst - ); - } - }, - - /** - * Call the component's `render` method and update the DOM accordingly. - * - * @param {ReactReconcileTransaction} transaction - * @internal - */ - _updateRenderedComponent: function(transaction, context) { - var prevComponentInstance = this._renderedComponent; - var prevRenderedElement = prevComponentInstance._currentElement; - var nextRenderedElement = this._renderValidatedComponent(); - if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { - ReactReconciler.receiveComponent( - prevComponentInstance, - nextRenderedElement, - transaction, - this._processChildContext(context) - ); - } else { - // These two IDs are actually the same! But nothing should rely on that. - var thisID = this._rootNodeID; - var prevComponentID = prevComponentInstance._rootNodeID; - ReactReconciler.unmountComponent(prevComponentInstance); - - this._renderedComponent = this._instantiateReactComponent( - nextRenderedElement, - this._currentElement.type - ); - var nextMarkup = ReactReconciler.mountComponent( - this._renderedComponent, - thisID, - transaction, - context - ); - this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup); - } - }, - - /** - * @protected - */ - _replaceNodeWithMarkupByID: function(prevComponentID, nextMarkup) { - ReactComponentEnvironment.replaceNodeWithMarkupByID( - prevComponentID, - nextMarkup - ); - }, - - /** - * @protected - */ - _renderValidatedComponentWithoutOwnerOrContext: function() { - var inst = this._instance; - var renderedComponent = inst.render(); - if ("production" !== "development") { - // We allow auto-mocks to proceed as if they're returning null. - if (typeof renderedComponent === 'undefined' && - inst.render._isMockFunction) { - // This is probably bad practice. Consider warning here and - // deprecating this convenience. - renderedComponent = null; - } - } - - return renderedComponent; - }, - - /** - * @private - */ - _renderValidatedComponent: function() { - var renderedComponent; - var previousContext = ReactContext.current; - ReactContext.current = this._processChildContext( - this._currentElement._context - ); - ReactCurrentOwner.current = this; - try { - renderedComponent = - this._renderValidatedComponentWithoutOwnerOrContext(); - } finally { - ReactContext.current = previousContext; - ReactCurrentOwner.current = null; - } - ("production" !== "development" ? invariant( - // TODO: An `isValidNode` function would probably be more appropriate - renderedComponent === null || renderedComponent === false || - ReactElement.isValidElement(renderedComponent), - '%s.render(): A valid ReactComponent must be returned. You may have ' + - 'returned undefined, an array or some other invalid object.', - this.getName() || 'ReactCompositeComponent' - ) : invariant(// TODO: An `isValidNode` function would probably be more appropriate - renderedComponent === null || renderedComponent === false || - ReactElement.isValidElement(renderedComponent))); - return renderedComponent; - }, - - /** - * Lazily allocates the refs object and stores `component` as `ref`. - * - * @param {string} ref Reference name. - * @param {component} component Component to store as `ref`. - * @final - * @private - */ - attachRef: function(ref, component) { - var inst = this.getPublicInstance(); - var refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs; - refs[ref] = component.getPublicInstance(); - }, - - /** - * Detaches a reference name. - * - * @param {string} ref Name to dereference. - * @final - * @private - */ - detachRef: function(ref) { - var refs = this.getPublicInstance().refs; - delete refs[ref]; - }, - - /** - * Get a text description of the component that can be used to identify it - * in error messages. - * @return {string} The name or null. - * @internal - */ - getName: function() { - var type = this._currentElement.type; - var constructor = this._instance && this._instance.constructor; - return ( - type.displayName || (constructor && constructor.displayName) || - type.name || (constructor && constructor.name) || - null - ); - }, - - /** - * Get the publicly accessible representation of this component - i.e. what - * is exposed by refs and returned by React.render. Can be null for stateless - * components. - * - * @return {ReactComponent} the public component instance. - * @internal - */ - getPublicInstance: function() { - return this._instance; - }, - - // Stub - _instantiateReactComponent: null - -}; - -ReactPerf.measureMethods( - ReactCompositeComponentMixin, - 'ReactCompositeComponent', - { - mountComponent: 'mountComponent', - updateComponent: 'updateComponent', - _renderValidatedComponent: '_renderValidatedComponent' - } -); - -var ReactCompositeComponent = { - - Mixin: ReactCompositeComponentMixin - -}; - -module.exports = ReactCompositeComponent; - -},{"115":115,"135":135,"151":151,"154":154,"27":27,"36":36,"38":38,"39":39,"57":57,"58":58,"67":67,"68":68,"73":73,"75":75,"76":76,"77":77,"81":81,"87":87}],38:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactContext - */ - -'use strict'; - -var assign = _dereq_(27); -var emptyObject = _dereq_(115); -var warning = _dereq_(154); - -var didWarn = false; - -/** - * Keeps track of the current context. - * - * The context is automatically passed down the component ownership hierarchy - * and is accessible via `this.context` on ReactCompositeComponents. - */ -var ReactContext = { - - /** - * @internal - * @type {object} - */ - current: emptyObject, - - /** - * Temporarily extends the current context while executing scopedCallback. - * - * A typical use case might look like - * - * render: function() { - * var children = ReactContext.withContext({foo: 'foo'}, () => ( - * - * )); - * return
{children}
; - * } - * - * @param {object} newContext New context to merge into the existing context - * @param {function} scopedCallback Callback to run with the new context - * @return {ReactComponent|array} - */ - withContext: function(newContext, scopedCallback) { - if ("production" !== "development") { - ("production" !== "development" ? warning( - didWarn, - 'withContext is deprecated and will be removed in a future version. ' + - 'Use a wrapper component with getChildContext instead.' - ) : null); - - didWarn = true; - } - - var result; - var previousContext = ReactContext.current; - ReactContext.current = assign({}, previousContext, newContext); - try { - result = scopedCallback(); - } finally { - ReactContext.current = previousContext; - } - return result; - } - -}; - -module.exports = ReactContext; - -},{"115":115,"154":154,"27":27}],39:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactCurrentOwner - */ - -'use strict'; - -/** - * Keeps track of the current owner. - * - * The current owner is the component who should own any components that are - * currently being constructed. - * - * The depth indicate how many composite components are above this render level. - */ -var ReactCurrentOwner = { - - /** - * @internal - * @type {ReactComponent} - */ - current: null - -}; - -module.exports = ReactCurrentOwner; - -},{}],40:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactDOM - * @typechecks static-only - */ - -'use strict'; - -var ReactElement = _dereq_(57); -var ReactElementValidator = _dereq_(58); - -var mapObject = _dereq_(142); - -/** - * Create a factory that creates HTML tag elements. - * - * @param {string} tag Tag name (e.g. `div`). - * @private - */ -function createDOMFactory(tag) { - if ("production" !== "development") { - return ReactElementValidator.createFactory(tag); - } - return ReactElement.createFactory(tag); -} - -/** - * Creates a mapping from supported HTML tags to `ReactDOMComponent` classes. - * This is also accessible via `React.DOM`. - * - * @public - */ -var ReactDOM = mapObject({ - a: 'a', - abbr: 'abbr', - address: 'address', - area: 'area', - article: 'article', - aside: 'aside', - audio: 'audio', - b: 'b', - base: 'base', - bdi: 'bdi', - bdo: 'bdo', - big: 'big', - blockquote: 'blockquote', - body: 'body', - br: 'br', - button: 'button', - canvas: 'canvas', - caption: 'caption', - cite: 'cite', - code: 'code', - col: 'col', - colgroup: 'colgroup', - data: 'data', - datalist: 'datalist', - dd: 'dd', - del: 'del', - details: 'details', - dfn: 'dfn', - dialog: 'dialog', - div: 'div', - dl: 'dl', - dt: 'dt', - em: 'em', - embed: 'embed', - fieldset: 'fieldset', - figcaption: 'figcaption', - figure: 'figure', - footer: 'footer', - form: 'form', - h1: 'h1', - h2: 'h2', - h3: 'h3', - h4: 'h4', - h5: 'h5', - h6: 'h6', - head: 'head', - header: 'header', - hr: 'hr', - html: 'html', - i: 'i', - iframe: 'iframe', - img: 'img', - input: 'input', - ins: 'ins', - kbd: 'kbd', - keygen: 'keygen', - label: 'label', - legend: 'legend', - li: 'li', - link: 'link', - main: 'main', - map: 'map', - mark: 'mark', - menu: 'menu', - menuitem: 'menuitem', - meta: 'meta', - meter: 'meter', - nav: 'nav', - noscript: 'noscript', - object: 'object', - ol: 'ol', - optgroup: 'optgroup', - option: 'option', - output: 'output', - p: 'p', - param: 'param', - picture: 'picture', - pre: 'pre', - progress: 'progress', - q: 'q', - rp: 'rp', - rt: 'rt', - ruby: 'ruby', - s: 's', - samp: 'samp', - script: 'script', - section: 'section', - select: 'select', - small: 'small', - source: 'source', - span: 'span', - strong: 'strong', - style: 'style', - sub: 'sub', - summary: 'summary', - sup: 'sup', - table: 'table', - tbody: 'tbody', - td: 'td', - textarea: 'textarea', - tfoot: 'tfoot', - th: 'th', - thead: 'thead', - time: 'time', - title: 'title', - tr: 'tr', - track: 'track', - u: 'u', - ul: 'ul', - 'var': 'var', - video: 'video', - wbr: 'wbr', - - // SVG - circle: 'circle', - defs: 'defs', - ellipse: 'ellipse', - g: 'g', - line: 'line', - linearGradient: 'linearGradient', - mask: 'mask', - path: 'path', - pattern: 'pattern', - polygon: 'polygon', - polyline: 'polyline', - radialGradient: 'radialGradient', - rect: 'rect', - stop: 'stop', - svg: 'svg', - text: 'text', - tspan: 'tspan' - -}, createDOMFactory); - -module.exports = ReactDOM; - -},{"142":142,"57":57,"58":58}],41:[function(_dereq_,module,exports){ -/** - * Copyright 2013-2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactDOMButton - */ - -'use strict'; - -var AutoFocusMixin = _dereq_(2); -var ReactBrowserComponentMixin = _dereq_(29); -var ReactClass = _dereq_(33); -var ReactElement = _dereq_(57); - -var keyMirror = _dereq_(140); - -var button = ReactElement.createFactory('button'); - -var mouseListenerNames = keyMirror({ - onClick: true, - onDoubleClick: true, - onMouseDown: true, - onMouseMove: true, - onMouseUp: true, - onClickCapture: true, - onDoubleClickCapture: true, - onMouseDownCapture: true, - onMouseMoveCapture: true, - onMouseUpCapture: true -}); - -/** - * Implements a