diff --git a/.gitignore b/.gitignore index ec5565e0bb29bc5054a4a7595c4c110efd23506c..ab49affc19fa4254f91b2b25cbb3da5d776133da 100644 --- a/.gitignore +++ b/.gitignore @@ -17,14 +17,10 @@ /package-lock.json /yarn-error.log +/dist/web +/dist/android -/dist/* -!/dist/desktop - -/hooks/playstore-config.json -/hooks/minify-conf.json /hooks/uglify-config.json -/hooks/after_prepare/ionic-minify.js /hooks/after_prepare/uglify.js /www/js/config.js diff --git a/doc/development_guide.md b/doc/development_guide.md index c4df1378a48760f2a710e5446d9cd524a93efc2f..6f53040be77c209bb75ba4b0a0a68a83b3dbab3d 100644 --- a/doc/development_guide.md +++ b/doc/development_guide.md @@ -6,7 +6,7 @@ To build Cesium, you will have to: - Installing build tools: ``` - sudo apt-get install build-essential + sudo apt-get install git wget curl unzip build-essential software-properties-common ruby ruby-dev ruby-ffi gcc make ``` - Installing [nvm](https://github.com/nvm-sh/nvm) @@ -18,14 +18,14 @@ If you are using fish shell, there is a [dedicated plugin](https://github.com/jo > Then reload your terminal, for instance by executing the commande `bash` - - Configure NodeJS to use a version 6: (**WARNING**: upper version will NOT work !) + - Configure NodeJS to use a version 10: (**WARNING**: upper version will NOT work !) ``` - nvm install 6 + nvm install 10 ``` - - Installing node.js build tools: + - Installing node.js build tools, as global dependencies: ``` - npm install -g yarn gulp cordova@9.0.0 ionic@1.7.16 + npm install -g yarn gulp cordova ionic ``` ## Get the source code and dependencies @@ -40,7 +40,6 @@ If you are using fish shell, there is a [dedicated plugin](https://github.com/jo - Installing Cordova plugins (need for platforms specific builds) ``` ionic state restore - ionic browser add crosswalk@12.41.296.5 ``` - This should create a new directory `platforms/android` @@ -66,10 +65,10 @@ If you are using fish shell, there is a [dedicated plugin](https://github.com/jo - Compiling and running Cesium: ``` - ionic serve + npm start ``` -> or alternative: `ionic serve` +> or alternative: `yarn run start` or `ionic serve` - Open a web browser at address: [localhost:8100](http://localhost:8100). The application should be running. diff --git a/package.json b/package.json index fd56f6320d0a41224595f0965482a6636b9c1332..61ff9d11524f689cf785664b546cfef525bc128d 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "url": "git@git.duniter.org:clients/cesium/cesium.git" }, "scripts": { - "clean": "trash www/dist/** platforms/web platforms/**/*.apk platforms/desktop/**/*.deb", - "postinstall": "node -e \"try { require('fs').symlinkSync(require('path').resolve('node_modules/@bower_components'), 'www/lib', 'junction') } catch (e) { }\" && rm -f hooks/minify-conf.json hooks/uglify-config.json hooks/after_prepare/ionic-minify.js hooks/after_prepare/uglify.js", + "clean": "trash www/dist/** dist/web/* dist/desktop/**/*.deb platforms/android/**/*.apk", + "postinstall": "node -e \"try { require('fs').symlinkSync(require('path').resolve('node_modules/@bower_components'), 'www/lib', 'junction') } catch (e) { }\" && rm -f hooks/uglify-config.json hooks/after_prepare/uglify.js", "install-platforms": "ionic cordova prepare", "start": "ionic serve", "docker:build": "sudo docker build . -t cesium/release", @@ -182,7 +182,9 @@ ], "cordova": { "plugins": { - "cordova-plugin-camera": {}, + "cordova-plugin-camera": { + "CAMERA_USAGE_DESCRIPTION": "Add picture to the user profile", + "PHOTOLIBRARY_USAGE_DESCRIPTION": "Take a picture for the user profile"}, "cordova-plugin-console": {}, "cordova-plugin-device": {}, "cordova-plugin-dialogs": {}, @@ -200,7 +202,9 @@ }, "ionic-plugin-keyboard": {}, "cordova-clipboard": {}, - "cordova-plugin-ionic-webview": {} + "cordova-plugin-ionic-webview": { + "ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+" + } }, "platforms": [ "ios", @@ -211,4 +215,4 @@ "engines": { "yarn": ">= 1.0.0" } -} \ No newline at end of file +} diff --git a/scss/ionic.app.scss b/scss/ionic.app.scss index 015adfcadaa93c97ee01e4b05e8973482533d91c..2b4b46d1c280a3d1c788e0e0af0c609fbd036cc1 100644 --- a/scss/ionic.app.scss +++ b/scss/ionic.app.scss @@ -138,10 +138,10 @@ $screen-lg: 1200px; padding: 16px !important; } .no-padding-xs { - padding: 0px !important; + padding: 0 !important; } .no-margin-xs { - margin: 0px !important; + margin: 0 !important; } } @@ -169,10 +169,10 @@ $screen-lg: 1200px; padding: 16px !important; } .no-padding-sm { - padding: 0px !important; + padding: 0 !important; } .no-margin-sm { - margin: 0px !important; + margin: 0 !important; } } @@ -345,7 +345,7 @@ $screen-lg: 1200px; @media screen and (max-width: $screen-xs-max) { .no-margin-xs { - margin: 0px !important; + margin: 0 !important; } } @@ -482,7 +482,7 @@ html, body { height: 20px; line-height: 19px; max-width: 260px; - margin: 0px 5px; + margin: 0 5px; text-align: left; } @@ -493,7 +493,7 @@ html, body { position: absolute; right: 0; top: 0; - margin: 0px 5px; + margin: 0 5px; display: block; } } @@ -559,7 +559,7 @@ html, body { .title { color: black; text-align: left; - left: 0px !important; + left: 0 !important; font-size: 14px; line-height: 30px; } @@ -620,7 +620,7 @@ html, body { .button.button-small { height: 30px; font-size: 12px; - padding: 0px 5px !important; + padding: 0 5px !important; line-height: 30px; } } @@ -701,7 +701,7 @@ html, body { // Avoid to have not align button on bar-header .bar .buttons.pull-right, .bar .title + .button:last-child, .bar .title + .buttons, .bar > .button + .button:last-child, .bar > .button.pull-right { - top: 0px !important; + top: 0 !important; } // Avoid different between home view and other view (space between last button and left border) @@ -709,7 +709,7 @@ html, body { padding-right: 5px !important; .buttons-right span { - margin-left: 0px !important; + margin-left: 0 !important; } } @@ -861,7 +861,7 @@ html, body { } .no-padding { - padding: 0px !important; + padding: 0 !important; } .avatar-member { @@ -1266,7 +1266,7 @@ body { .card .card-header { padding-top: 5px !important; - padding-bottom: 0px !important; + padding-bottom: 0 !important; min-height: 25px; } @@ -1277,7 +1277,7 @@ body { .card .card-avatar, .card.card-avatar { .avatar { - box-shadow: 0px 3px 4px 0 rgba(0, 0, 0, 0.26); + box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.26); top: 7px; background-color: #D9D9D9; } @@ -1340,7 +1340,7 @@ a.underline:hover, .avatar, .item-avatar .avatar { - box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.26); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.26); height: 30px !important; width: 30px !important; left: 5px !important; @@ -1352,7 +1352,7 @@ a.underline:hover, } .card-footer { - padding-top: 0px; + padding-top: 0; padding-left: 42px !important; } } @@ -1557,7 +1557,7 @@ a.underline:hover, .list.item-border-large { .item { border-bottom: solid 1px #ccc !important; - margin: 0px 0px 1px; + margin: 0 0px 1px; } .item-divider { @@ -1573,11 +1573,11 @@ a.underline:hover, .list .item.item-small-height { padding-top: 2px; - padding-bottom: 0px; + padding-bottom: 0; min-height: 24px; .badge { - padding-top: 0px !important; + padding-top: 0 !important; top: inherit; } @@ -1606,12 +1606,12 @@ a.underline:hover, } .form-error { - padding: 0px 16px; + padding: 0 16px; font-size: 12px; color: red; vertical-align: middle; text-align: end; - top: 0px; + top: 0; position: relative; } @@ -1747,7 +1747,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; **********/ .modal.modal-full-height { - bottom: 0px; + bottom: 0; } // Force modal as fullscreen, on xs screen @@ -1755,7 +1755,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; .modal { top: 0 !important; left: 0 !important; - bottom: 0px; + bottom: 0; min-height: 100% !important; width: 100% !important; } @@ -1763,7 +1763,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; @media screen and (max-width: $screen-sm-max) { .modal { - bottom: 0px; + bottom: 0; // Hide swiper on tablet .swiper-pagination { @@ -1789,7 +1789,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; .modal.about .bar.bar-header .button + .title { - left: 0px !important; /* avoid title offset on large screens, if button are 'visible-xs')'*/ + left: 0 !important; /* avoid title offset on large screens, if button are 'visible-xs')'*/ } /********** @@ -1934,8 +1934,8 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; .item.item-divider { min-height: 2px; height: 2px; - padding-top: 0px; - padding-bottom: 0px; + padding-top: 0; + padding-bottom: 0; } .item, @@ -1953,7 +1953,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; } .item.item-complex { - padding-top: 0px; + padding-top: 0; } .item.item-button-right .button { @@ -1965,14 +1965,14 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; // Hide footer on small screen ion-content.has-footer { - bottom: 0px !important; /*ignore footer*/ + bottom: 0 !important; /*ignore footer*/ } .bar-header { background-color: $positive-900-bg; color: #fff; height: 150px; - padding-right: 0px !important; + padding-right: 0 !important; .platform-ios.platform-cordova & { height: calc(constant(safe-area-inset-top) + 150px); @@ -1984,7 +1984,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; box-shadow: none; // not need (define in bar-header) .content { - bottom: 0px; + bottom: 0; } } } @@ -2169,7 +2169,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; text-align: center; position: relative; top: 8px; - height: 0px; + height: 0; } .icon.icon-bottom-right { @@ -2195,7 +2195,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; width: 100%; bottom: 8px; text-align: center; - height: 0px; + height: 0; } } @@ -2223,7 +2223,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; .col .button { max-width: inherit; width: 100%; - padding: 5px 0px; + padding: 5px 0; margin: 0; } } @@ -2242,7 +2242,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; height: 35px; width: 35px; position: relative; - left: 0px; + left: 0; top: 4px; border: solid 1px #D9D9D9; } @@ -2255,7 +2255,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; height: 31px; width: 31px; position: relative; - left: 0px; + left: 0; top: 6px; } } @@ -2321,7 +2321,7 @@ $ionicon-var-badge-editable: $ionicon-var-edit + "\00a0"; .row-header { border-bottom: solid 1px #ccc !important; - margin: 0px; + margin: 0; min-height: 28px !important; } diff --git a/www/index.html b/www/index.html index 7f34f0bba13ac33d175d30243ac64f2ab3b41487..d742b7bd3be4718e4c75f6cc93113ac917d2ec92 100644 --- a/www/index.html +++ b/www/index.html @@ -26,7 +26,6 @@ <link rel="stylesheet" type="text/css" href="lib/chart.js/dist/Chart.min.css"> <link rel="stylesheet" type="text/css" href="dist/dist_css/plugins/es/css/style.css"> <link rel="stylesheet" type="text/css" href="dist/dist_css/plugins/graph/css/style.css"> - <link rel="stylesheet" type="text/css" href="dist/dist_css/plugins/graph/css/style.css"> <link rel="stylesheet" type="text/css" href="dist/dist_css/plugins/map/css/style.css"> <!--endRemoveIf(no-plugin)--> <!-- endbuild --> @@ -48,6 +47,7 @@ <meta property="og:locale:alternate" content="fr_FR" /> <meta property="og:locale:alternate" content="it_IT" /> <meta property="og:locale:alternate" content="nl_NL" /> + <meta property="og:locale:alternate" content="eo_EO" /> <!--endRemoveIf(device)--> @@ -72,11 +72,9 @@ <script src="lib/numeral/languages/es.js"></script> <script src="lib/numeral/languages/it.js"></script> <script src="js/vendor/numeral.eo.js"></script> - <script src="lib/socket.io-client/dist/socket.io.min.js"></script> <script src="lib/underscore/underscore-min.js"></script> <script src="lib/qrcode.js/qrcode.js"></script> - <script src="lib/aes-js/index.js"></script> <script src="lib/chart.js/dist/Chart.min.js"></script> @@ -191,6 +189,7 @@ <script src="dist/dist_js/plugins/es/js/entities/notification.js"></script> <script src="dist/dist_js/plugins/es/js/entities/comment.js"></script> <script src="dist/dist_js/plugins/es/js/entities/invitation.js"></script> + <script src="dist/dist_js/plugins/es/js/entities/peer.js"></script> <script src="dist/dist_js/plugins/es/js/services.js"></script> <script src="dist/dist_js/plugins/es/js/services/comment-services.js"></script> <script src="dist/dist_js/plugins/es/js/services/http-services.js"></script> @@ -211,6 +210,7 @@ <script src="dist/dist_js/plugins/es/js/services/tx-services.js"></script> <script src="dist/dist_js/plugins/es/js/services/geo-services.js"></script> <script src="dist/dist_js/plugins/es/js/services/document-services.js"></script> + <script src="dist/dist_js/plugins/es/js/services/network-services.js"></script> <script src="dist/dist_js/plugins/es/js/controllers/common-controllers.js"></script> <script src="dist/dist_js/plugins/es/js/controllers/app-controllers.js"></script> <script src="dist/dist_js/plugins/es/js/controllers/settings-controllers.js"></script> @@ -229,7 +229,6 @@ <script src="dist/dist_js/plugins/es/js/controllers/document-controllers.js"></script> <!-- Graph plugin --> - <!--removeIf(ubuntu)--> <!-- FIXME: issue #463 --> <script src="dist/dist_js/plugins/graph/js/plugin.js"></script> <script src="dist/dist_js/plugins/graph/js/services.js"></script> <script src="dist/dist_js/plugins/graph/js/services/data-services.js"></script> @@ -241,7 +240,6 @@ <script src="dist/dist_js/plugins/graph/js/controllers/account-controllers.js"></script> <script src="dist/dist_js/plugins/graph/js/controllers/docstats-controllers.js"></script> <script src="dist/dist_js/plugins/graph/js/controllers/synchro-controllers.js"></script> - <!--endRemoveIf(ubuntu)--> <!-- Map plugin --> <script src="dist/dist_js/plugins/map/js/plugin.js"></script> diff --git a/www/js/config.js b/www/js/config.js index c719769101888e1e11a346c363108466f76b9944..ca63850959a113c6af62116c41e156c6278a7e1f 100644 --- a/www/js/config.js +++ b/www/js/config.js @@ -94,7 +94,7 @@ angular.module("cesium.config", []) } }, "version": "1.4.18", - "build": "2019-12-28T12:21:59.344Z", + "build": "2019-12-31T11:21:28.655Z", "newIssueUrl": "https://git.duniter.org/clients/cesium-grp/cesium/issues/new" }) diff --git a/www/js/controllers/app-controllers.js b/www/js/controllers/app-controllers.js index c2e765ce192f42574ed5c979a603fd8b709374b0..90963f2bd437bb3a248f1a6a0932a94fb23dedea 100644 --- a/www/js/controllers/app-controllers.js +++ b/www/js/controllers/app-controllers.js @@ -559,8 +559,8 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, UIU * Catch click for quick fix * @param event */ - $scope.doQuickFix = function(event) { - if (event == 'settings') { + $scope.doQuickFix = function(action) { + if (action === 'settings') { $ionicHistory.nextViewOptions({ historyRoot: true }); diff --git a/www/js/services/http-services.js b/www/js/services/http-services.js index 94e57bfc2ef75b81a3063c1dca0002bb3d55c101..b332d6353219cdb7cce2aa9a7d27576c960047ec 100644 --- a/www/js/services/http-services.js +++ b/www/js/services/http-services.js @@ -158,7 +158,9 @@ angular.module('cesium.http.services', ['cesium.cache.services']) timeout = timeout || csSettings.data.timeout; function _waitOpen(self) { - if (!self.delegate) throw new Error('Websocket not opened'); + if (!self.delegate) { + throw new Error('Websocket {0} was closed!'.format(uri)); + } if (self.delegate.readyState == 1) { return $q.when(self.delegate); } diff --git a/www/plugins/es/css/style.css b/www/plugins/es/css/style.css index a127910219f4efcfc4d6672368765ed3cd5297a2..2ab89803bc0acc95ae10208ece9a11ea7e24bbf2 100644 --- a/www/plugins/es/css/style.css +++ b/www/plugins/es/css/style.css @@ -81,22 +81,22 @@ } .row-record .col-text-wrap { - padding: 0; - margin: 0; + padding: 0; + margin: 0; } .row-record .col .text-wrap { - height: 70px; - white-space: normal; - position: relative; - word-wrap: break-word !important; - overflow: hidden !important; - text-overflow: ellipsis; - -o-text-overflow: ellipsis; - -webkit-hyphens: auto; - -moz-hyphens: auto; - -ms-hyphens: auto; - -o-hyphens: auto; - hyphens: auto; + height: 70px; + white-space: normal; + position: relative; + word-wrap: break-word !important; + overflow: hidden !important; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + -webkit-hyphens: auto; + -moz-hyphens: auto; + -ms-hyphens: auto; + -o-hyphens: auto; + hyphens: auto; } /* Source: See doc: http://stackoverflow.com/questions/15814346 */ @@ -243,6 +243,20 @@ } +/********** + Document +**********/ + +.item-document.compacted { + min-height: 16px !important; + max-height: 16px !important; + border-bottom: 0 !important; +} + +.item-document.compacted .col{ + padding-top: 1px; +} + /********** Add specific Icons **********/ diff --git a/www/plugins/es/i18n/locale-en-GB.json b/www/plugins/es/i18n/locale-en-GB.json index 4ab28fa5cfa4deb5b1a08e0bbaa76cd60f803f14..e45f63b40b76e89b8817b7bc69e1cdd2c0136cb3 100644 --- a/www/plugins/es/i18n/locale-en-GB.json +++ b/www/plugins/es/i18n/locale-en-GB.json @@ -413,6 +413,7 @@ "TITLE": "Document search", "BTN_ACTIONS": "Actions", "SEARCH_HELP": "issuer:AAA*, time:1508406169", + "LAST_DOCUMENTS_DOTS": "Last documents:", "LAST_DOCUMENTS": "Last documents", "SHOW_QUERY": "Show query", "HIDE_QUERY": "Hide query", @@ -421,6 +422,8 @@ "HEADER_RECIPIENT": "Recipient", "READ": "Read", "BTN_REMOVE": "Delete this document", + "BTN_COMPACT": "Compact", + "HAS_REGISTERED": "create or edit his profile", "POPOVER_ACTIONS": { "TITLE": "Actions", "REMOVE_ALL": "Delete these documents..." @@ -470,6 +473,13 @@ "ES_WALLET": { "ERROR": { "RECIPIENT_IS_MANDATORY": "A recipient is required for encryption." + }, + "ES_PEER": { + "NAME": "Name", + "DOCUMENTS": "Documents", + "SOFTWARE": "Software", + "DOCUMENT_COUNT": "Number of documents", + "EMAIL_SUBSCRIPTION_COUNT": "{{emailSubscription}} subscribers to email notification" } }, "EVENT": { diff --git a/www/plugins/es/i18n/locale-en.json b/www/plugins/es/i18n/locale-en.json index 4ab28fa5cfa4deb5b1a08e0bbaa76cd60f803f14..e45f63b40b76e89b8817b7bc69e1cdd2c0136cb3 100644 --- a/www/plugins/es/i18n/locale-en.json +++ b/www/plugins/es/i18n/locale-en.json @@ -413,6 +413,7 @@ "TITLE": "Document search", "BTN_ACTIONS": "Actions", "SEARCH_HELP": "issuer:AAA*, time:1508406169", + "LAST_DOCUMENTS_DOTS": "Last documents:", "LAST_DOCUMENTS": "Last documents", "SHOW_QUERY": "Show query", "HIDE_QUERY": "Hide query", @@ -421,6 +422,8 @@ "HEADER_RECIPIENT": "Recipient", "READ": "Read", "BTN_REMOVE": "Delete this document", + "BTN_COMPACT": "Compact", + "HAS_REGISTERED": "create or edit his profile", "POPOVER_ACTIONS": { "TITLE": "Actions", "REMOVE_ALL": "Delete these documents..." @@ -470,6 +473,13 @@ "ES_WALLET": { "ERROR": { "RECIPIENT_IS_MANDATORY": "A recipient is required for encryption." + }, + "ES_PEER": { + "NAME": "Name", + "DOCUMENTS": "Documents", + "SOFTWARE": "Software", + "DOCUMENT_COUNT": "Number of documents", + "EMAIL_SUBSCRIPTION_COUNT": "{{emailSubscription}} subscribers to email notification" } }, "EVENT": { diff --git a/www/plugins/es/i18n/locale-fr-FR.json b/www/plugins/es/i18n/locale-fr-FR.json index c5c84bf3e2d57b466824a14c12da2383fcdcc451..cdf39e9c0555e8005a99f1394d9b8e7e587a1a67 100644 --- a/www/plugins/es/i18n/locale-fr-FR.json +++ b/www/plugins/es/i18n/locale-fr-FR.json @@ -463,7 +463,8 @@ "LOOKUP": { "TITLE": "Recherche de documents", "BTN_ACTIONS": "Actions", - "SEARCH_HELP": "Emetteur:AAA*, temps:1508406169", + "SEARCH_HELP": "issuer:AAA*, time:1508406169", + "LAST_DOCUMENTS_DOTS": "Derniers documents :", "LAST_DOCUMENTS": "Derniers documents", "SHOW_QUERY": "Voir la requête", "HIDE_QUERY": "Masquer la requête", @@ -472,6 +473,8 @@ "HEADER_RECIPIENT": "Destinataire", "READ": "Lu", "BTN_REMOVE": "Supprimer ce document", + "BTN_COMPACT": "Compacter", + "HAS_REGISTERED": "a créé ou modifié son profil", "POPOVER_ACTIONS": { "TITLE": "Actions", "REMOVE_ALL": "Supprimer ces documents..." @@ -523,6 +526,13 @@ "RECIPIENT_IS_MANDATORY": "Un destinataire est obligatoire pour le chiffrement." } }, + "ES_PEER": { + "NAME": "Nom", + "DOCUMENTS": "Documents", + "SOFTWARE": "Logiciel", + "DOCUMENT_COUNT": "Nombre de documents", + "EMAIL_SUBSCRIPTION_COUNT": "{{emailSubscription}} abonnés aux notifications par email" + }, "EVENT": { "NODE_STARTED": "Votre noeud ES API <b>{{params[0]}}</b> est démarré", "NODE_BMA_DOWN": "Le noeud <b>{{params[0]}}:{{params[1]}}</b> (utilisé par votre noeud ES API) est <b>injoignable</b>.", diff --git a/www/plugins/es/js/controllers/document-controllers.js b/www/plugins/es/js/controllers/document-controllers.js index 3e9f5212675fed9c93f2436c9ee621c0a54d2a5b..7d5d335c691849bfdd0e13ee38bf6c8650107499 100644 --- a/www/plugins/es/js/controllers/document-controllers.js +++ b/www/plugins/es/js/controllers/document-controllers.js @@ -22,45 +22,52 @@ angular.module('cesium.es.document.controllers', ['cesium.es.services']) .controller('ESDocumentLookupCtrl', ESDocumentLookupController) + .controller('ESLastDocumentsCtrl', ESLastDocumentsController) ; function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, csSettings, csWallet, UIUtils, esHttp, esDocument) { 'ngInject'; - $scope.search = { + $scope.search = $scope.search || { loading: true, - hasMore: true, + hasMore: false, text: undefined, index: 'invitation', type: 'certification', - results: undefined, + results: [], sort: 'time', - asc: false + asc: false, + loadingMore: false }; $scope.entered = false; $scope.searchTextId = 'documentSearchText'; $scope.ionItemClass = 'item-border-large'; - $scope.defaultSizeLimit = UIUtils.screen.isSmall() ? 50 : 100; + $scope.defaultSizeLimit = $scope.defaultSizeLimit || (UIUtils.screen.isSmall() ? 50 : 100); $scope.helptipPrefix = 'helptip-document'; - - $scope.$on('$ionicView.enter', function(e, state) { - + $scope.compactMode = angular.isDefined($scope.compactMode) ? $scope.compactMode : true; + $scope._source = $scope._source || ["issuer", "hash", "time", "creationTime", "title", "message"]; + $scope.showHeaders = angular.isDefined($scope.showHeaders) ? $scope.showHeaders : true; + + /** + * Enter into the view + * @param e + * @param state + */ + $scope.enter = function(e, state) { if (!$scope.entered) { $scope.entered = true; $scope.search.index = state.stateParams && state.stateParams.index || $scope.search.index; $scope.search.type = state.stateParams && state.stateParams.type || $scope.search.type; $scope.search.text = state.stateParams && state.stateParams.q || $scope.search.text; - $scope.search.last = !$scope.search.text; $scope.load(); } $scope.expertMode = angular.isDefined($scope.expertMode) ? $scope.expertMode : !UIUtils.screen.isSmall() && csSettings.data.expertMode; - }); - - $scope.load = function(size, offset) { - if ($scope.search.error) return; + }; + $scope.$on('$ionicView.enter', $scope.enter); + $scope.computeOptions = function(offset, size) { var options = { index: $scope.search.index, type: $scope.search.type, @@ -77,34 +84,44 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, options.sort = {time:'desc'}; } - $scope.search.loading = true; + // Included fields + options._source = options._source || $scope._source; + + return options; + }; + + $scope.load = function(offset, size, silent) { + if ($scope.search.error) return; + + var options = $scope.computeOptions(offset, size); + + $scope.search.loading = !silent; var searchFn = $scope.search.last ? esDocument.search(options) : esDocument.searchText($scope.search.text||'', options); return searchFn .then(function(res) { - $scope.search.results = res.hits; + if (!offset) { + $scope.search.results = res.hits; + $scope.search.took = res.took; + } + else { + $scope.search.results = $scope.search.results.concat(res.hits); + } $scope.search.total = res.total; - $scope.search.took = res.took; UIUtils.loading.hide(); $scope.search.loading = false; + $scope.search.hasMore = res.hits && res.hits.length > 0 && res.total > $scope.search.results.length; - if (res.hits && res.hits.length > 0) { - $scope.motion.show({selector: '.list .item.item-document'}); - $scope.search.hasMore = res.total > $scope.search.results.length; - } - else { - $scope.search.hasMore = false; - } - - $scope.$broadcast('$$rebind::rebind'); // notify binder + $scope.updateView(); }) .catch(function(err) { $scope.search.results = []; $scope.search.loading = false; $scope.search.error = true; + $scope.search.hasMore = false; UIUtils.onError('DOCUMENT.ERROR.LOAD_DOCUMENTS_FAILED')(err) .then(function() { $scope.search.error = false; @@ -112,6 +129,13 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, }); }; + $scope.updateView = function() { + if ($scope.motion && $scope.search.results && $scope.search.results.length) { + $scope.motion.show({selector: '.list .item.item-document'}); + } + $scope.$broadcast('$$rebind::rebind'); // notify binder + }; + $scope.doSearchText = function() { $scope.search.last = $scope.search.text ? false : true; return $scope.load() @@ -147,9 +171,10 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, }); }; - $scope.remove = function(index, options) { + $scope.remove = function($event, index) { var doc = $scope.search.results[index]; - if (!doc) return; + if (!doc || $event.defaultPrevented) return; + $event.stopPropagation(); UIUtils.alert.confirm('DOCUMENT.CONFIRM.REMOVE') .then(function(confirm) { @@ -171,6 +196,18 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, return $scope.openLink(event, url); }; + $scope.toggleCompactMode = function() { + $scope.compactMode = !$scope.compactMode; + $scope.updateView(); + + // Workaround to re-initialized the <ion-infinite-loop> + if (!$scope.search.hasMore && $scope.search.results.length && $scope.search.type == 'last') { + $timeout(function() { + $scope.search.hasMore = true; + }, 500); + } + }; + $scope.toggleSort = function(sort){ if ($scope.search.sort === sort && !$scope.search.asc) { $scope.search.asc = undefined; @@ -183,6 +220,71 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, $scope.load(); }; + $scope.showMore = function() { + if ($scope.search.loading) return; + $scope.search.loadingMore = true; + $scope.load( + $scope.search.results.length, // from + $scope.defaultSizeLimit, + true/*silent*/) + .then(function() { + $scope.search.loadingMore = false; + $scope.$broadcast('scroll.infiniteScrollComplete'); + }); + }; + + $scope.startListenChanges = function() { + var now = Date.now(); + var source = $scope.search.index + '/' + $scope.search.type; + var wsChanges = esHttp.websocket.changes(source); + return wsChanges.open() + .then(function(){ + console.debug("[ES] [document] Websocket opened in {0} ms".format(Date.now()- now)); + wsChanges.on(function(change) { + if (!$scope.search.last || !change) return; // ignore + esDocument.fromHit(change) + .then(function(doc) { + if (change._operation === 'DELETE') { + $scope.onDeleteDocument(doc); + } + else { + $scope.onNewDocument(doc); + } + }); + }); + }); + }; + + $scope.onNewDocument = function(document) { + if (!$scope.search.last || $scope.search.loading) return; // skip + console.debug("[ES] [document] Detected new document: ", document); + var index = _.findIndex($scope.search.results, {id: document.id, index: document.index, type: document.type}); + if (index < 0) { + $scope.search.total++; + $scope.search.results.splice(0, 0, document); + } + else { + document.updated = true; + $timeout(function() { + document.updated = false; + }, 2000); + $scope.search.results.splice(index, 1, document); + } + $scope.updateView(); + }; + + $scope.onDeleteDocument = function(document) { + if (!$scope.search.last || $scope.search.loading) return; // skip + $timeout(function() { + var index = _.findIndex($scope.search.results, {id: document.id, index: document.index, type: document.type}); + if (index < 0) return; // skip if not found + console.debug("[ES] [document] Detected document deletion: ", document); + $scope.search.results.splice(index, 1); + $scope.search.total--; + $scope.updateView(); + }, 750); + }; + /* -- Modals -- */ /* -- Popover -- */ @@ -207,14 +309,19 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, /* -- watch events -- */ - // Watch unauth - $scope.onUnauth = function() { + $scope.resetData = function() { + if ($scope.search.loading) return; + console.debug("[ES] [document] Resetting data (settings or account may have changed)"); // Reset all data - $scope.search.results = undefined; + $scope.search.results = []; $scope.search.loading = false; + $scope.search.total = undefined; + $scope.search.loadingMore = false; $scope.entered = false; + delete $scope.search.limit; }; - csWallet.api.data.on.unauth($scope, $scope.onUnauth); + + csWallet.api.data.on.unauth($scope, $scope.resetData); // for DEV only /*$timeout(function() { @@ -222,3 +329,71 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout, }, 900); */ } + + +function ESLastDocumentsController($scope, $controller, $timeout, $state) { + 'ngInject'; + + $scope.search = { + loading: true, + hasMore: true, + text: undefined, + index: 'user,page,group', type: 'profile,record,comment', + results: undefined, + sort: 'time', + asc: false + }; + $scope.expertMode = false; + $scope.defaultSizeLimit = 20; + $scope._source = ["issuer", "hash", "time", "creationTime", "title", "avatar._content_type", "city", "message", "record"]; + $scope.showHeaders = false; + + // Initialize the super class and extend it. + angular.extend(this, $controller('ESDocumentLookupCtrl', {$scope: $scope})); + $scope.$on('$ionicParentView.enter', $scope.enter); + + $scope.selectDocument = function(event, doc) { + if (!doc || !event || event.defaultPrevented) return; + event.stopPropagation(); + + if (doc.index === "user" && doc.type === "profile") { + $state.go('app.wot_identity', {pubkey: doc.pubkey, uid: doc.name}); + } + else if (doc.index === "page" && doc.type === "record") { + $state.go('app.view_page', {title: doc.title, id: doc.id}); + return + } + else if (doc.index === "group" && doc.type === "record") { + $state.go('app.view_group', {title: doc.title, id: doc.id}); + return + } + console.warn("Click on this kind of document not implement yet!", doc) + }; + + // Override parent function computeOptions + var inheritedComputeOptions = $scope.computeOptions; + $scope.computeOptions = function(offset, size){ + // Cal inherited function + var options = inheritedComputeOptions(offset, size); + + if (!options.sort || options.sort.time) { + var side = options.sort && options.sort.time || side; + options.sort = [ + //{'creationTime': side}, + {'time': side} + ]; + } + + options._source = options._source || $scope._source; + options.getTimeFunction = function(doc) { + doc.time = doc.creationTime || doc.time; + return doc.time; + }; + return options; + }; + + // Listen for changes + $timeout(function() { + $scope.startListenChanges(); + }, 1000); +} diff --git a/www/plugins/es/js/controllers/network-controllers.js b/www/plugins/es/js/controllers/network-controllers.js index cbf2346249dfb63197ca2bf71d1968068ec6677f..ebaf126bfe0a71658896f8243d50305938ca9f65 100644 --- a/www/plugins/es/js/controllers/network-controllers.js +++ b/www/plugins/es/js/controllers/network-controllers.js @@ -1,20 +1,688 @@ angular.module('cesium.es.network.controllers', ['cesium.es.services']) - .config(function(PluginServiceProvider, csConfig) { - 'ngInject'; - - var enable = csConfig.plugins && csConfig.plugins.es; - if (enable) { - PluginServiceProvider.extendState('app.network', { - points: { - 'buttons': { - templateUrl: "plugins/es/templates/network/view_network_extend.html", - controller: 'ESExtensionCtrl' +.config(function(PluginServiceProvider, csConfig) { + 'ngInject'; + + var enable = csConfig.plugins && csConfig.plugins.es; + if (enable) { + PluginServiceProvider.extendState('app.network', { + points: { + 'buttons': { + templateUrl: "plugins/es/templates/network/view_network_extend.html", + controller: 'ESExtensionCtrl' + } + } + }) + ; + } +}) + +.config(function($stateProvider) { + 'ngInject'; + + $stateProvider + + .state('app.es_network', { + url: "/network/data?online&expert", + cache: false, // fix #766 + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/network/view_network.html", + controller: 'ESNetworkLookupCtrl' + } + }, + data: { + silentLocationChange: true + } + }) + + .state('app.view_es_peer', { + url: "/data/network/peer/:server?ssl&tor", + cache: false, + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/network/view_peer.html", + controller: 'ESPeerViewCtrl' + } + }, + data: { + preferHttp: true // avoid HTTPS if config has httpsMode=clever + } + }); +}) + + .controller('ESNetworkLookupCtrl', ESNetworkLookupController) + + .controller('ESPeerViewCtrl', ESPeerViewController) + + .controller('ESNetworkLookupModalCtrl', ESNetworkLookupModalController) + + .controller('ESNetworkLookupPopoverCtrl', ESNetworkLookupPopoverController) + + .controller('ESPeerInfoPopoverCtrl', ESPeerInfoPopoverController) + +; + +function ESNetworkLookupController($scope, $state, $location, $ionicPopover, $window, $translate, + esHttp, UIUtils, csConfig, csSettings, csCurrency, esNetwork, csWot) { + 'ngInject'; + + $scope.networkStarted = false; + $scope.ionItemClass = ''; + $scope.expertMode = csSettings.data.expertMode && !UIUtils.screen.isSmall(); + $scope.isHttps = ($window.location.protocol === 'https:'); + $scope.search = { + text: '', + loading: true, + online: true, + results: [], + endpointFilter: esHttp.constants.ES_USER_API, + sort : undefined, + asc: true + }; + $scope.listeners = []; + $scope.helptipPrefix = 'helptip-network'; + $scope.enableLocationHref = true; // can be overrided by sub-controller (e.g. popup) + + $scope.removeListeners = function() { + if ($scope.listeners.length) { + console.debug("[ES] [network] Closing listeners"); + _.forEach($scope.listeners, function(remove){ + remove(); + }); + $scope.listeners = []; + } + }; + + /** + * Enter in view + */ + $scope.enter = function(e, state) { + if ($scope.networkStarted) return; + $scope.networkStarted = true; + $scope.search.loading = true; + csCurrency.get() + .then(function (currency) { + if (currency) { + $scope.node = !esHttp.node.same(currency.node.host, currency.node.port) ? + esHttp.instance(currency.node.host, currency.node.port) : esHttp; + if (state && state.stateParams) { + if (state.stateParams.online == 'true') { + $scope.search.online = true; + } + if (state.stateParams.expert) { + $scope.expertMode = (state.stateParams.expert == 'true'); + } } + $scope.load(); } }) - ; + .catch(function(err) { + UIUtils.onError('ERROR.GET_CURRENCY_FAILED')(err); + $scope.networkStarted = false; + }); + }; + $scope.$on('$ionicParentView.enter', $scope.enter); + + /** + * Leave the view + */ + $scope.leave = function() { + if (!$scope.networkStarted) return; + $scope.removeListeners(); + esNetwork.close(); + $scope.networkStarted = false; + $scope.search.loading = true; + }; + $scope.$on('$ionicView.beforeLeave', $scope.leave); + $scope.$on('$ionicParentView.beforeLeave', $scope.leave); + $scope.$on('$destroy', $scope.leave); + + + $scope.computeOptions = function() { + var options = { + filter: { + member: (!$scope.search.type || $scope.search.type === 'member'), + mirror: (!$scope.search.type || $scope.search.type === 'mirror'), + endpointFilter : (angular.isDefined($scope.search.endpointFilter) ? $scope.search.endpointFilter : null), + online: $scope.search.online && true + }, + sort: { + type : $scope.search.sort, + asc : $scope.search.asc + }, + expertMode: $scope.expertMode, + // larger timeout when on expert mode + timeout: csConfig.timeout && ($scope.expertMode ? (csConfig.timeout / 10) : (csConfig.timeout / 100)) + }; + return options; + }; + + $scope.load = function() { + + if ($scope.search.loading){ + esNetwork.start($scope.node, $scope.computeOptions()); + + // Catch event on new peers + $scope.refreshing = false; + $scope.listeners.push( + esNetwork.api.data.on.changed($scope, function(data){ + if (!$scope.refreshing) { + $scope.refreshing = true; + csWot.extendAll(data.peers) + .then(function() { + // Avoid to refresh if view has been leaving + if ($scope.networkStarted) { + $scope.updateView(data); + } + $scope.refreshing = false; + }); + } + })); + } + + // Show help tip + $scope.showHelpTip(); + }; + + $scope.updateView = function(data) { + console.debug("[peers] Updating UI"); + $scope.$broadcast('$$rebind::' + 'rebind'); // force data binding + $scope.search.results = data.peers; + $scope.search.memberPeersCount = data.memberPeersCount; + // Always tru if network not started (e.g. after leave+renter the view) + $scope.search.loading = !$scope.networkStarted || esNetwork.isBusy(); + if ($scope.motion && $scope.search.results && $scope.search.results.length > 0) { + $scope.motion.show({selector: '.item-peer'}); + } + if (!$scope.loading) { + $scope.$broadcast('$$rebind::' + 'rebind'); // force data binding + } + }; + + $scope.refresh = function() { + // Network + $scope.search.loading = true; + esNetwork.loadPeers(); + }; + + $scope.sort = function() { + $scope.search.loading = true; + $scope.refreshing = true; + esNetwork.sort($scope.computeOptions()); + $scope.updateView(esNetwork.data); + }; + + $scope.toggleOnline = function(online){ + $scope.hideActionsPopover(); + $scope.search.online = (online !== false); + esNetwork.close(); + $scope.search.loading = true; + $scope.load(); + + // Update location href + if ($scope.enableLocationHref) { + $location.search({online: $scope.search.online}).replace(); + } + }; + + $scope.toggleSearchEndpoint = function(endpoint){ + $scope.hideActionsPopover(); + if ($scope.search.endpointFilter === endpoint || endpoint === null) { + $scope.search.endpointFilter = null; + } + else { + $scope.search.endpointFilter = endpoint; + } + $scope.sort(); + }; + + $scope.toggleSort = function(sort){ + if ($scope.search.sort === sort && !$scope.search.asc) { + $scope.search.asc = undefined; + $scope.search.sort = undefined; + } + else { + $scope.search.asc = ($scope.search.sort === sort) ? !$scope.search.asc : true; + $scope.search.sort = sort; + } + $scope.sort(); + }; + + $scope.selectPeer = function(peer) { + // Skip offline + if (!peer.online ) return; + + var stateParams = {server: peer.getServer()}; + if (peer.isSsl()) { + stateParams.ssl = true; + } + if (peer.isTor()) { + stateParams.tor = true; + } + $state.go('app.view_es_peer', stateParams); + }; + + $scope.$on('csView.action.refresh', function(event, context) { + if (context === 'peers') { + $scope.refresh(); + } + }); + + $scope.$on('csView.action.showActionsPopover', function(event, clickEvent) { + $scope.showActionsPopover(clickEvent); + }); + + /* -- popover -- */ + + $scope.showActionsPopover = function(event) { + if (!$scope.actionsPopover) { + $ionicPopover.fromTemplateUrl('templates/network/lookup_popover_actions.html', { + scope: $scope + }).then(function(popover) { + $scope.actionsPopover = popover; + //Cleanup the popover when we're done with it! + $scope.$on('$destroy', function() { + $scope.actionsPopover.remove(); + }); + $scope.actionsPopover.show(event); + }); + } + else { + $scope.actionsPopover.show(event); + } + }; + + $scope.hideActionsPopover = function() { + if ($scope.actionsPopover) { + $scope.actionsPopover.hide(); + } + }; + + $scope.showEndpointsPopover = function($event, peer, endpointFilter) { + var endpoints = peer.getEndpoints(endpointFilter); + endpoints = (endpoints||[]).reduce(function(res, ep) { + var bma = esHttp.node.parseEndPoint(ep); + return res.concat({ + label: 'NETWORK.VIEW.NODE_ADDRESS', + value: peer.getServer() + (bma.path||'') + }); + }, []); + if (!endpoints.length) return; + + UIUtils.popover.show($event, { + templateUrl: 'templates/network/popover_endpoints.html', + bindings: { + titleKey: 'NETWORK.VIEW.ENDPOINTS.' + endpointFilter, + items: endpoints + } + }); + $event.stopPropagation(); + }; + + $scope.showWs2pPopover = function($event, peer) { + $event.stopPropagation(); + + return $translate('NETWORK.VIEW.PRIVATE_ACCESS') + .then(function(privateAccessMessage) { + UIUtils.popover.show($event, { + templateUrl: 'templates/network/popover_endpoints.html', + bindings: { + titleKey: 'NETWORK.VIEW.ENDPOINTS.WS2P', + valueKey: 'NETWORK.VIEW.NODE_ADDRESS', + items: [ + { + label: 'NETWORK.VIEW.NODE_ADDRESS', + value: !peer.bma.private ? (peer.getServer() + (peer.bma.path||'')) : privateAccessMessage + }, + { + label: 'NETWORK.VIEW.WS2PID', + value: peer.bma.ws2pid + }, + { + label: 'NETWORK.VIEW.POW_PREFIX', + value: peer.powPrefix + }] + } + }); + }); + }; + + + + /* -- help tip -- */ + + // Show help tip + $scope.showHelpTip = function(index, isTour) { + index = angular.isDefined(index) ? index : csSettings.data.helptip.network; + isTour = angular.isDefined(isTour) ? isTour : false; + if (index < 0) return; + + // Create a new scope for the tour controller + var helptipScope = $scope.createHelptipScope(); + if (!helptipScope) return; // could be undefined, if a global tour already is already started + helptipScope.tour = isTour; + + return helptipScope.startNetworkTour(index, false) + .then(function(endIndex) { + helptipScope.$destroy(); + if (!isTour) { + csSettings.data.helptip.network = endIndex; + csSettings.store(); + } + }); + }; +} + + +function ESNetworkLookupModalController($scope, $controller, parameters) { + 'ngInject'; + + // Initialize the super class and extend it. + angular.extend(this, $controller('NetworkLookupCtrl', {$scope: $scope})); + + // Read parameters + parameters = parameters || {}; + $scope.enableFilter = angular.isDefined(parameters.enableFilter) ? parameters.enableFilter : true; + $scope.search.type = angular.isDefined(parameters.type) ? parameters.type : $scope.search.type; + $scope.search.endpointFilter = angular.isDefined(parameters.endpointFilter) ? parameters.endpointFilter : $scope.search.endpointFilter; + $scope.expertMode = angular.isDefined(parameters.expertMode) ? parameters.expertMode : $scope.expertMode; + $scope.ionItemClass = parameters.ionItemClass || 'item-border-large'; + $scope.enableLocationHref = false; + $scope.helptipPrefix = ''; + + $scope.selectPeer = function(peer) { + $scope.closeModal(peer); + }; + + $scope.$on('modal.hidden', function(){ + $scope.leave(); + }); + + // Disable this unsed method - called by load() + $scope.showHelpTip = function() {}; + + // Enter the modal + $scope.enter(); +} + + +function ESNetworkLookupPopoverController($scope, $controller) { + 'ngInject'; + + // Initialize the super class and extend it. + angular.extend(this, $controller('NetworkLookupCtrl', {$scope: $scope})); + + // Read parameters + var parameters = parameters || {}; + $scope.enableFilter = angular.isDefined(parameters.enableFilter) ? parameters.enableFilter : true; + $scope.search.type = angular.isDefined(parameters.type) ? parameters.type : $scope.search.type; + $scope.search.endpointFilter = angular.isDefined(parameters.endpointFilter) ? parameters.endpointFilter : $scope.search.endpointFilter; + $scope.expertMode = angular.isDefined(parameters.expertMode) ? parameters.expertMode : $scope.expertMode; + $scope.ionItemClass = parameters.ionItemClass || 'item-border-large'; + $scope.helptipPrefix = ''; + + $scope.selectPeer = function(peer) { + $scope.closePopover(peer); + }; + + $scope.$on('popover.hidden', function(){ + $scope.leave(); + }); + + // Disable this unsed method - called by load() + $scope.showHelpTip = function() {}; + + // Enter the popover + $scope.enter(); +} + +function ESPeerInfoPopoverController($scope, $q, csSettings, csCurrency, csHttp, esHttp) { + 'ngInject'; + + $scope.loading = true; + $scope.formData = {}; + + $scope.load = function() { + + $scope.loading = true; + $scope.formData = {}; + + return $q.all([ + // get current block + csCurrency.blockchain.current() + .then(function(block) { + $scope.formData.number = block.number; + $scope.formData.medianTime = block.medianTime; + $scope.formData.powMin = block.powMin; + $scope.formData.useSsl = esHttp.useSsl; + }) + .catch(function() { + delete $scope.formData.number; + delete $scope.formData.medianTime; + delete $scope.formData.powMin; + delete $scope.formData.useSsl; + // continue + }), + + // Get node current version + esHttp.node.summary() + .then(function(res){ + $scope.formData.version = res && res.duniter && res.duniter.version; + $scope.formData.software = res && res.duniter && res.duniter.software; + }) + .catch(function() { + delete $scope.formData.version; + delete $scope.formData.software; + // continue + }), + + // Get duniter latest version + esHttp.version.latest() + .then(function(latestRelease){ + $scope.formData.latestRelease = latestRelease; + }) + .catch(function() { + delete $scope.formData.latestRelease; + // continue + }) + ]) + .then(function() { + // Compare, to check if newer + if ($scope.formData.latestRelease && $scope.formData.software == 'duniter') { + var compare = csHttp.version.compare($scope.formData.version, $scope.formData.latestRelease.version); + $scope.formData.isPreRelease = compare > 0; + $scope.formData.hasNewRelease = compare < 0; + } + else { + $scope.formData.isPreRelease = false; + $scope.formData.hasNewRelease = false; + } + $scope.loading = false; + $scope.$broadcast('$$rebind::' + 'rebind'); // force data binding + }); + }; + + // Update UI on new block + csCurrency.api.data.on.newBlock($scope, function(block) { + if ($scope.loading) return; + console.debug("[peer info] Received new block. Reload content"); + $scope.load(); + }); + + // Update UI on settings changed + csSettings.api.data.on.changed($scope, function(data) { + if ($scope.loading) return; + console.debug("[peer info] Peer settings changed. Reload content"); + $scope.load(); + }); + + // Load data when enter + $scope.load(); +} + +function ESPeerViewController($scope, $q, $window, $state, UIUtils, csWot, esHttp, csHttp, csSettings) { + 'ngInject'; + + $scope.node = {}; + $scope.loading = true; + $scope.isHttps = ($window.location.protocol === 'https:'); + $scope.isReachable = true; + $scope.options = { + document: { + index : csSettings.data.plugins.es && csSettings.data.plugins.es.document && csSettings.data.plugins.es.document.index || 'user', + type: csSettings.data.plugins.es && csSettings.data.plugins.es.document && csSettings.data.plugins.es.document.type || 'profile' } + }; + + $scope.$on('$ionicView.beforeEnter', function (event, viewData) { + // Enable back button (workaround need for navigation outside tabs - https://stackoverflow.com/a/35064602) + viewData.enableBack = UIUtils.screen.isSmall() ? true : viewData.enableBack; }); + $scope.$on('$ionicView.enter', function(e, state) { + var isDefaultNode = !state.stateParams || !state.stateParams.server; + var server = state.stateParams && state.stateParams.server || esHttp.server; + var useSsl = state.stateParams && state.stateParams.ssl == "true" || (isDefaultNode ? esHttp.useSsl : false); + var useTor = state.stateParams.tor == "true" || (isDefaultNode ? esHttp.useTor : false); + + return $scope.load(server, useSsl, useTor) + .then(function() { + return $scope.$broadcast('$csExtension.enter', e, state); + }) + .then(function(){ + $scope.loading = false; + }); + }); + + $scope.load = function(server, useSsl, useTor) { + var node = { + server: server, + host: server, + useSsl: useSsl, + useTor: useTor + }; + var serverParts = server.split(':'); + if (serverParts.length == 2) { + node.host = serverParts[0]; + node.port = serverParts[1]; + } + node.url = csHttp.getUrl(node.host, node.port, undefined/*path*/, node.useSsl); + + angular.merge($scope.node, + useTor ? + // For TOR, use a web2tor to access the endpoint + esHttp.lightInstance(node.host + ".to", 443, 443, true/*ssl*/, 60000 /*long timeout*/) : + esHttp.lightInstance(node.host, node.port, node.useSsl), + node); + + $scope.isReachable = !$scope.isHttps || useSsl; + if (!$scope.isReachable) { + // Get node from the default node + return esHttp.network.peers() + .then(function(res) { + // find the current peer + var peers = (res && res.peers || []).reduce(function(res, json) { + var peer = new EsPeer(json); + if (!peer.hasEndpoint('GCHANGE_API')) return res; + var ep = esHttp.node.parseEndPoint(peer.getEndpoints('GCHANGE_API')[0]); + if((ep.dns == node.host || ep.ipv4 == node.host || ep.ipv6 == node.host) && ( + ep.port == node.port)) { + peer.ep = ep; + return res.concat(peer); + } + return res; + }, []); + var peer = peers.length && peers[0]; + + // Current node found + if (peer) { + $scope.node.pubkey = peer.pubkey; + $scope.node.currency = peer.currency; + return csWot.extend($scope.node); + } + else { + console.warn('Could not get peer from /network/peers'); + } + }); + } + + return $q.all([ + + // Get node peer info + $scope.node.network.peering.self() + .then(function(json) { + $scope.node.pubkey = json.pubkey; + $scope.node.currency = json.currency; + }), + + // Get node doc count + $scope.node.record.count($scope.options.document.index, $scope.options.document.type) + .then(function(count) { + $scope.node.docCount = count; + }), + + // Get known peers + $scope.node.network.peers() + .then(function(json) { + var peers = json.peers.reduce(function (res, p) { + var peer = new EsPeer(p); + if (!peer.hasEndpoint('GCHANGE_API')) return res; + peer.online = p.status === 'UP'; + peer.blockNumber = peer.block.replace(/-.+$/, ''); + peer.ep = esHttp.node.parseEndPoint(peer.getEndpoints('GCHANGE_API')[0]); + peer.dns = peer.getDns(); + peer.id = peer.keyID(); + peer.server = peer.getServer(); + return res.concat(peer); + }, []); + + // Extend (add uid+name+avatar) + return csWot.extendAll([$scope.node].concat(peers)) + .then(function() { + // Final sort + $scope.peers = _.sortBy(peers, function(p) { + var score = 1; + score += 10000 * (p.online ? 1 : 0); + score += 1000 * (p.hasMainConsensusBlock ? 1 : 0); + score += 100 * (p.name ? 1 : 0); + return -score; + }); + $scope.motion.show({selector: '.item-peer'}); + }); + }), + + // Get current block + $scope.node.blockchain.current() + .then(function(json) { + $scope.current = json; + }) + ]) + .catch(UIUtils.onError(useTor ? "PEER.VIEW.ERROR.LOADING_TOR_NODE_ERROR" : "PEER.VIEW.ERROR.LOADING_NODE_ERROR")); + }; + + $scope.selectPeer = function(peer) { + // Skip offline + if (!peer.online ) return; + + var stateParams = {server: peer.getServer()}; + if (peer.isSsl()) { + stateParams.ssl = true; + } + if (peer.isTor()) { + stateParams.tor = true; + } + $state.go('app.view_es_peer', stateParams); + }; + + /* -- manage link to raw document -- */ + + $scope.openRawPeering = function(event) { + return $scope.openLink(event, $scope.node.url + '/network/peering?pretty'); + }; + + $scope.openRawCurrentBlock = function(event) { + return $scope.openLink(event, $scope.node.url + '/network/peering?pretty'); + }; +} diff --git a/www/plugins/es/js/controllers/subscription-controllers.js b/www/plugins/es/js/controllers/subscription-controllers.js index d73f7736112f42d44862b77c9b827f59790629f7..8ad1106dadc76b103dfaa289858b67c73aa58442 100644 --- a/www/plugins/es/js/controllers/subscription-controllers.js +++ b/www/plugins/es/js/controllers/subscription-controllers.js @@ -35,9 +35,9 @@ angular.module('cesium.es.subscription.controllers', ['cesium.es.services']) }) - .controller('ViewSubscriptionsCtrl', ViewSubscriptionsController) + .controller('ViewSubscriptionsCtrl', ViewSubscriptionsController) - .controller('ModalEmailSubscriptionsCtrl', ModalEmailSubscriptionsController) + .controller('ModalEmailSubscriptionsCtrl', ModalEmailSubscriptionsController) ; @@ -260,7 +260,7 @@ function ViewSubscriptionsController($scope, $q, $ionicHistory, csWot, csWallet, }) .then(function(cat){ if (cat && cat.parent) { - return cat; + return cat; } }); }; @@ -272,7 +272,7 @@ function ViewSubscriptionsController($scope, $q, $ionicHistory, csWot, csWallet, } -function ModalEmailSubscriptionsController($scope, Modals, csSettings, esHttp, csWot, parameters) { +function ModalEmailSubscriptionsController($scope, Modals, csSettings, esHttp, csWot, esModals, parameters) { 'ngInject'; $scope.frequencies = [ @@ -290,6 +290,15 @@ function ModalEmailSubscriptionsController($scope, Modals, csSettings, esHttp, c $scope.recipient = {pubkey: $scope.formData.recipient}; return csWot.extendAll([$scope.recipient]); } + else { + return esHttp.network.peering.self() + .then(function(res){ + if (!res) return; + $scope.formData.recipient = res.pubkey; + $scope.recipient = {pubkey: $scope.formData.recipient}; + return csWot.extendAll([$scope.recipient]); + }); + } }); // Submit @@ -318,10 +327,9 @@ function ModalEmailSubscriptionsController($scope, Modals, csSettings, esHttp, c } $scope.showNetworkLookup = function() { - return Modals.showNetworkLookup({ + return esModals.showNetworkLookup({ enableFilter: true, - endpoint: esHttp.constants.ES_USER_API_ENDPOINT, - bma: false + endpointFilter: esHttp.constants.ES_SUBSCRIPTION_API }) .then(function (peer) { if (peer) { diff --git a/www/plugins/es/js/entities/peer.js b/www/plugins/es/js/entities/peer.js new file mode 100644 index 0000000000000000000000000000000000000000..0c0af2bc6090024dde520b9b5ffa75e8cf85186a --- /dev/null +++ b/www/plugins/es/js/entities/peer.js @@ -0,0 +1,163 @@ + + +function EsPeer(json) { + + var that = this; + + Object.keys(json).forEach(function (key) { + that[key] = json[key]; + }); + + that.endpoints = that.endpoints || []; +} + + +EsPeer.prototype.regexp = { + API_REGEXP: /^([A-Z_]+)(?:[ ]+([a-z_][a-z0-9-_.ÄŸÄž]*))?(?:[ ]+([0-9.]+))?(?:[ ]+([0-9a-f:]+))?(?:[ ]+([0-9]+))(?:\/[^\/]+)?$/, + LOCAL_IP_ADDRESS: /^127[.]0[.]0.|192[.]168[.]|10[.]0[.]0[.]|172[.]16[.]/ +}; + +EsPeer.prototype.keyID = function () { + var ep = this.ep || this.getEP(); + if (ep.useBma) { + return [this.pubkey || "Unknown", ep.dns, ep.ipv4, ep.ipv6, ep.port, ep.useSsl, ep.path].join('-'); + } + return [this.pubkey || "Unknown", ep.ws2pid, ep.path].join('-'); +}; + +EsPeer.prototype.copyValues = function(to) { + var obj = this; + ["version", "currency", "pub", "endpoints", "hash", "status", "block", "signature"].forEach(function (key) { + to[key] = obj[key]; + }); +}; + +EsPeer.prototype.copyValuesFrom = function(from) { + var obj = this; + ["version", "currency", "pub", "endpoints", "block", "signature"].forEach(function (key) { + obj[key] = from[key]; + }); +}; + +EsPeer.prototype.json = function() { + var obj = this; + var json = {}; + ["version", "currency", "endpoints", "status", "block", "signature"].forEach(function (key) { + json[key] = obj[key]; + }); + json.raw = this.raw && this.getRaw(); + json.pubkey = this.pubkey; + return json; +}; + +EsPeer.prototype.getEP = function() { + if (this.ep) return this.ep; + var ep = null; + var epRegex = this.regexp.API_REGEXP; + this.endpoints.forEach(function(epStr){ + var matches = !ep && epRegex.exec(epStr); + if (matches) { + ep = { + "api": matches[1] || '', + "dns": matches[2] || '', + "ipv4": matches[3] || '', + "ipv6": matches[4] || '', + "port": matches[5] || 80, + "path": matches[6] || '', + "useSsl": matches[5] == 443 + }; + } + }); + return ep || {}; +}; + +EsPeer.prototype.getEndpoints = function(regexp) { + if (!regexp) return this.endpoints; + if (typeof regexp === 'string') regexp = new RegExp('^' + regexp); + return this.endpoints.reduce(function(res, ep){ + return ep.match(regexp) ? res.concat(ep) : res; + }, []); +}; + +EsPeer.prototype.hasEndpoint = function(endpoint){ + var regExp = this.regexp[endpoint] || new RegExp('^' + endpoint); + var endpoints = this.getEndpoints(regExp); + return endpoints && endpoints.length > 0; +}; + +EsPeer.prototype.hasEsEndpoint = function() { + var endpoints = this.getEsEndpoints(); + return endpoints && endpoints.length > 0; +}; + +EsPeer.prototype.getEsEndpoints = function() { + return this.getEndpoints(/^(ES_CORE_API|ES_USER_API|ES_SUBSCRIPTION_API|GCHANGE_API)/); +}; + +EsPeer.prototype.getDns = function() { + var ep = this.ep || this.getEP(); + return ep.dns ? ep.dns : null; +}; + +EsPeer.prototype.getIPv4 = function() { + var ep = this.ep || this.getEP(); + return ep.ipv4 ? ep.ipv4 : null; +}; + +EsPeer.prototype.getIPv6 = function() { + var ep = this.ep || this.getEP(); + return ep.ipv6 ? ep.ipv6 : null; +}; + +EsPeer.prototype.getPort = function() { + var ep = this.ep || this.getEP(); + return ep.port ? ep.port : null; +}; + +EsPeer.prototype.getHost = function() { + var ep = this.ep || this.getEP(); + return ((ep.port == 443 || ep.useSsl) && ep.dns) ? ep.dns : + (this.hasValid4(ep) ? ep.ipv4 : + (ep.dns ? ep.dns : + (ep.ipv6 ? '[' + ep.ipv6 + ']' :''))); +}; + +EsPeer.prototype.getURL = function() { + var ep = this.ep || this.getEP(); + var host = this.getHost(); + var protocol = (ep.port == 443 || ep.useSsl) ? 'https' : 'http'; + return protocol + '://' + host + (ep.port ? (':' + ep.port) : ''); +}; + +EsPeer.prototype.getServer = function() { + var ep = this.ep || this.getEP(); + var host = this.getHost(); + return host + (host && ep.port ? (':' + ep.port) : ''); +}; + +EsPeer.prototype.hasValid4 = function(ep) { + return ep.ipv4 && + /* exclude private address - see https://fr.wikipedia.org/wiki/Adresse_IP */ + !ep.ipv4.match(this.regexp.LOCAL_IP_ADDRESS) ? + true : false; +}; + +EsPeer.prototype.isReachable = function () { + return !!this.getServer(); +}; + +EsPeer.prototype.isSsl = function() { + var ep = this.ep || this.getEP(); + return ep.useSsl; +}; + +EsPeer.prototype.isTor = function() { + var ep = this.ep || this.getEP(); + return ep.useTor; +}; + +EsPeer.prototype.isHttp = function() { + var ep = this.ep || this.getEP(); + return !bma.useTor; +}; + diff --git a/www/plugins/es/js/services.js b/www/plugins/es/js/services.js index a4a8f888d6b3b0a0c43316b03689261f3b42739b..0bb32bb2e7c0a718b8ca65a31deffb44e2a8be2e 100644 --- a/www/plugins/es/js/services.js +++ b/www/plugins/es/js/services.js @@ -19,6 +19,7 @@ angular.module('cesium.es.services', [ 'cesium.es.wot.services', 'cesium.es.tx.services', 'cesium.es.geo.services', - 'cesium.es.document.services' + 'cesium.es.document.services', + 'cesium.es.network.services' ]) ; diff --git a/www/plugins/es/js/services/comment-services.js b/www/plugins/es/js/services/comment-services.js index 374752bbec7a371e6b57cd0fd67e7998140b66f6..5044924bbcb0dd285ad80ff54954b2ca53fbc0b1 100644 --- a/www/plugins/es/js/services/comment-services.js +++ b/www/plugins/es/js/services/comment-services.js @@ -101,7 +101,7 @@ angular.module('cesium.es.comment.services', ['ngResource', 'cesium.services', options.size = options.size || DEFAULT_SIZE; options.loadAvatar = angular.isDefined(options.loadAvatar) ? options.loadAvatar : true; options.loadAvatarAllParent = angular.isDefined(options.loadAvatarAllParent) ? (options.loadAvatar && options.loadAvatarAllParent) : false; - if (options.size < 0) options.size = DEFAULT_SIZE; + if (options.size < 0) options.size = 1000; // all comments var request = { query : { @@ -199,19 +199,14 @@ angular.module('cesium.es.comment.services', ['ngResource', 'cesium.services', }); // Open websocket - var time = Date.now(); + var now = Date.now(); console.info("[ES] [comment] Starting websocket to listen comments on [{0}/record/{1}]".format(index, recordId.substr(0,8))); - var wsChanges = exports.raw.wsChanges(); + var wsChanges = esHttp.websocket.changes(index + '/comment'); return wsChanges.open() - // Define source filter - .then(function(sock) { - return sock.send(index + '/comment'); - }) - // Listen changes .then(function(){ - console.debug("[ES] [comment] Websocket opened in {0} ms".format(Date.now() - time)); + console.debug("[ES] [comment] Websocket opened in {0} ms".format(Date.now() - now)); wsChanges.on(function(change) { if (!change) return; scope.$applyAsync(function() { @@ -237,7 +232,7 @@ angular.module('cesium.es.comment.services', ['ngResource', 'cesium.services', // fill map by id data.mapById[change._id] = comment; exports.raw.refreshTreeLinks(data) - // fill avatars (and uid) + // fill avatars (and uid) .then(function() { return csWot.extend(comment, 'issuer'); }) diff --git a/www/plugins/es/js/services/document-services.js b/www/plugins/es/js/services/document-services.js index 41567a05474b7178d224b9c6b9b352ad35faf4f0..7e987962021037ff34a1766f2484f5fd2513bd2a 100644 --- a/www/plugins/es/js/services/document-services.js +++ b/www/plugins/es/js/services/document-services.js @@ -11,7 +11,7 @@ angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', }) .factory('esDocument', function($q, $rootScope, $timeout, UIUtils, Api, CryptoUtils, - csPlatform, csConfig, csSettings, csWot, csWallet, esHttp) { + csPlatform, csConfig, csSettings, csWot, csWallet, esHttp) { 'ngInject'; var @@ -25,9 +25,44 @@ angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', }, raw = { search: esHttp.post('/:index/:type/_search'), - searchText: esHttp.get('/:index/:type/_search?q=:text') + searchText: esHttp.get('/:index/:type/_search?q=:text&_source=:source') }; + function _initOptions(options) { + if (!options || !options.index || !options.type) throw new Error('Missing mandatory options [index, type]'); + + var side = 'desc'; + if (options.type == 'peer') { + if (!options.sort || options.sort.time) { + side = options.sort && options.sort.time || side; + options.sort = { + 'stats.medianTime': { + nested_path: 'stats', + order: side + } + }; + } + options._source = fields.peer; + options.getTimeFunction = function(doc) { + doc.time = doc.stats && doc.stats.medianTime; + return doc.time; + }; + } + else if (options.type == 'movement') { + if (!options.sort || options.sort.time) { + side = options.sort && options.sort.time || side; + options.sort = {'medianTime': side}; + } + options._source = options._source || fields.movement; + options.getTimeFunction = function(doc) { + doc.time = doc.medianTime; + return doc.time; + }; + } + + return options; + } + function _readSearchHits(res, options) { options.issuerField = options.issuerField || 'pubkey'; @@ -36,8 +71,9 @@ angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', doc.index = hit._index; doc.type = hit._type; doc.id = hit._id; - doc.pubkey = doc.issuer || options.issuerField && doc[options.issuerField]; // need to call csWot.extendAll() - doc.time = doc.time || options.getTimeFunction && options.getTimeFunction(doc); + doc.pubkey = doc.issuer || options.issuerField && doc[options.issuerField] || doc.pubkey; // need to call csWot.extendAll() + doc.time = options.getTimeFunction && options.getTimeFunction(doc) || doc.time; + doc.thumbnail = esHttp.image.fromHit(hit, 'thumbnail'); return res.concat(doc); }, []); @@ -62,54 +98,25 @@ angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', }); } - function search(options) { - options = options || {}; + function readSearchHit(hit) { + var options = _initOptions({ + index: hit._index, + type: hit._type + }); - var sortParts, side; - if (options.type == 'movement') { - if (!options.sort) { - options.sort = 'stats.medianTime:desc'; - } - else { - sortParts = options.sort.split(':'); - side = sortParts.length > 1 ? sortParts[1] : 'desc'; - - options.sort = [ - {'stats.medianTime': { - nested_path : 'stats', - order: side - }} - ]; - options._source = fields.peer; - options.getTimeFunction = function(doc) { - doc.time = doc.stats && doc.stats.medianTime; - return doc.time; - }; + return _readSearchHits({ + hits: { + hits: [hit] } - } - else if (options.type == 'movement') { - if (!options.sort) { - options.sort = 'medianTime:desc'; - } - else { - sortParts = options.sort.split(':'); - side = sortParts.length > 1 ? sortParts[1] : 'desc'; - - options.sort = [ - {'medianTime': { - order: side - }} - ]; - options._source = fields.movement; - options.getTimeFunction = function(doc) { - doc.time = doc.medianTime; - return doc.time; - }; - } - } + }, options) + .then(function(res) { + return res.hits[0]; + }); + } + function search(options) { + options = _initOptions(options); - if (!options || !options.index || !options.type) throw new Error('Missing mandatory options [index, type]'); var request = { from: options.from || 0, size: options.size || constants.DEFAULT_LOAD_SIZE, @@ -121,9 +128,9 @@ angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', } return raw.search(request, { - index: options.index, - type: options.type - }) + index: options.index, + type: options.type + }) .then(function(res) { return _readSearchHits(res, options); }); @@ -140,7 +147,7 @@ angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', from: options.from || 0, size: options.size || constants.DEFAULT_LOAD_SIZE, sort: options.sort || 'time:desc', - _source: options._source || fields.commons.join(',') + source: options._source && options._source.join(',') || fields.commons.join(',') }; console.debug('[ES] [wallet] [document] [{0}/{1}] Loading documents...'.format( @@ -166,6 +173,7 @@ angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', function remove(document, options) { if (!document || !document.index || !document.type || !document.id) return $q.reject('Could not remove document: missing mandatory fields'); + return esHttp.record.remove(document.index, document.type)(document.id, options); } @@ -190,7 +198,8 @@ angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', removeAll: removeAll, fields: { commons: fields.commons - } + }, + fromHit: readSearchHit }; }) ; diff --git a/www/plugins/es/js/services/http-services.js b/www/plugins/es/js/services/http-services.js index bc1b070799232d93e40f113f2a56e540b715fcbc..1aa5d2dfe172d4b8367ba3a347f729696ce622d1 100644 --- a/www/plugins/es/js/services/http-services.js +++ b/www/plugins/es/js/services/http-services.js @@ -20,15 +20,19 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic that = this, cachePrefix = 'esHttp-', constants = { + ES_USER_API: 'ES_USER_API', + ES_SUBSCRIPTION_API: 'ES_SUBSCRIPTION_API', ES_USER_API_ENDPOINT: 'ES_USER_API( ([a-z_][a-z0-9-_.]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))', + ANY_API_ENDPOINT: '([A-Z_]+)(?:[ ]+([a-z_][a-z0-9-_.ÄŸÄž]*))?(?:[ ]+([0-9.]+))?(?:[ ]+([0-9a-f:]+))?(?:[ ]+([0-9]+))(?:\\/[^\\/]+)?', MAX_UPLOAD_BODY_SIZE: csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.maxUploadBodySize || 2097152 /*=2M*/ }, regexp = { IMAGE_SRC: exact('data:([A-Za-z//]+);base64,(.+)'), URL: match('(www\\.|https?:\/\/(www\\.)?)[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)'), - HASH_TAG: match('(?:^|[\t\n\r\s ])#([\\wḡĞǦğà áâãäåçèéêëìÃîïðòóôõöùúûüýÿ]+)'), + HASH_TAG: match('(?:^|[\t\n\r\s ])#([0-9_-\\wḡĞǦğà áâãäåçèéêëìÃîïðòóôõöùúûüýÿ]+)'), USER_TAG: match('(?:^|[\t\n\r\s ])@('+BMA.constants.regexp.USER_ID+')'), - ES_USER_API_ENDPOINT: exact(constants.ES_USER_API_ENDPOINT) + ES_USER_API_ENDPOINT: exact(constants.ES_USER_API_ENDPOINT), + API_ENDPOINT: exact(constants.ANY_API_ENDPOINT), }, fallbackNodeIndex = 0, listeners, @@ -74,8 +78,8 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic } function isSameNode(host, port, useSsl) { - return (that.host == host) && - (that.port == port) && + return (that.host === host) && + (that.port === port) && (angular.isUndefined(useSsl) || useSsl == that.useSsl); } @@ -140,6 +144,9 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic return that.start(true /*skipInit*/); }; + // Get node time (UTC) FIXME: get it from the node + that.date = { now : csHttp.date.now }; + that.byteCount = function (s) { s = (typeof s == 'string') ? s : JSON.stringify(s); return encodeURI(s).split(/%(?:u[0-9A-F]{2})?[0-9A-F]{2}|./).length - 1; @@ -157,7 +164,7 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic var getRequestFn = function(params) { if (!that.started) { if (!that._startPromise) { - console.error('[ES] [http] Trying to get [{0}] before start(). Waiting...'.format(path)); + console.warn('[ES] [http] Trying to get [{0}] before start(). Waiting...'.format(path)); } return that.ready().then(function(start) { if (!start) return $q.reject('ERROR.ES_CONNECTION_ERROR'); @@ -214,6 +221,23 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic }; }; + that.wsChanges = function(source) { + var wsChanges = that.ws('/ws/_changes')(); + if (!source) return wsChanges; + // var oldOpen = wsChanges.open; + // wsChanges.open = function() { + // return oldOpen.call(wsChanges).then(function(sock) { + // if(sock) { + // sock.send(source); + // } + // else { + // console.warn('Trying to access ws changes, but no sock anymore... already open ?'); + // } + // }); + // }; + return wsChanges; + }; + that.isAlive = function() { return csHttp.get(that.host, that.port, '/node/summary', that.useSsl)() .then(function(json) { @@ -378,6 +402,28 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic return urls; } + function parseMarkdownTitlesFromText(value, prefix, suffix) { + prefix = prefix || '##'; + var reg = match('(?:^|[\\r\\s])('+prefix+'([^#></]+)' + (suffix||'') + ')'); + var matches = value && reg.exec(value); + var lines = matches && []; + var res = matches && []; + while(matches) { + var line = matches[1]; + if (!_.contains(lines, line)) { + lines.push(line); + res.push({ + line: line, + title: matches[2] + }); + } + value = value.substr(matches.index + matches[1].length + 1); + matches = value.length > 0 && reg.exec(value); + } + return res; + } + + function escape(text) { if (!text) return text; return text.replace(/</g, '<').replace(/>/g, '>'); @@ -417,6 +463,13 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic var link = '<a ui-sref=\"{0}({uid: \'{1}\'})\">@{2}</a>'.format(options.uidState, tag, tag); content = content.replace('@'+tag, link); }); + + // Replace markdown titles + var titles = parseMarkdownTitlesFromText(content, '#+[ ]*', '<br>'); + _.forEach(titles, function(matches){ + var size = matches.line.lastIndexOf('#', 5)+1; + content = content.replace(matches.line, '<h{0}>{1}</h{2}>'.format(size, matches.title, size)); + }); } return content; } @@ -492,36 +545,43 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic fillRecordTags(obj, options.tagFields); } - var str = JSON.stringify(obj); - - return CryptoUtils.util.hash(str) - .then(function(hash) { - return CryptoUtils.sign(hash, keypair) - .then(function(signature) { - // Prepend hash+signature - str = '{"hash":"{0}","signature":"{1}",'.format(hash, signature) + str.substring(1); - // Send data - return postRequest(str, params) - .then(function (id){ - - // Clear cache - csCache.clear(cachePrefix); - - return id; - }) - .catch(function(err) { - var bodyLength = that.byteCount(obj); - if (bodyLength > constants.MAX_UPLOAD_BODY_SIZE) { - throw {message: 'ERROR.ES_MAX_UPLOAD_BODY_SIZE', length: bodyLength}; - } - throw err; - }); - }); - }); - }); + var str = JSON.stringify(obj); + + return CryptoUtils.util.hash(str) + .then(function(hash) { + return CryptoUtils.sign(hash, keypair) + .then(function(signature) { + // Prepend hash+signature + str = '{"hash":"{0}","signature":"{1}",'.format(hash, signature) + str.substring(1); + // Send data + return postRequest(str, params) + .then(function (id){ + + // Clear cache + csCache.clear(cachePrefix); + + return id; + }) + .catch(function(err) { + var bodyLength = that.byteCount(obj); + if (bodyLength > constants.MAX_UPLOAD_BODY_SIZE) { + throw {message: 'ERROR.ES_MAX_UPLOAD_BODY_SIZE', length: bodyLength}; + } + throw err; + }); + }); + }); + }); }; } + function countRecord(index, type) { + return that.get("/{0}/{1}/_search?size=0".format(index, type)) + .then(function(res) { + return res && res.hits && res.hits.total; + }); + } + function removeRecord(index, type) { return function(id, options) { options = options || {}; @@ -624,25 +684,26 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic }; function parseEndPoint(endpoint) { - var matches = regexp.ES_USER_API_ENDPOINT.exec(endpoint); + var matches = regexp.API_ENDPOINT.exec(endpoint); if (!matches) return; - var port = matches[8] || 80; return { + "api": matches[1] || '', "dns": matches[2] || '', - "ipv4": matches[4] || '', - "ipv6": matches[6] || '', - "port": port, - "useSsl": port == 80 ? false : (port == 443) + "ipv4": matches[3] || '', + "ipv6": matches[4] || '', + "port": matches[5] || 80, + "path": matches[6] || '', + "useSsl": matches[5] == 443 }; } function emptyHit() { return { - _id: null, - _index: null, - _type: null, - _version: null, - _source: {} + _id: null, + _index: null, + _type: null, + _version: null, + _source: {} }; } @@ -673,13 +734,26 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic sameAsSettings: isSameNodeAsSettings, isFallback: isFallbackNode }, + websocket: { + changes: that.wsChanges, + block: that.ws('/ws/block'), + peer: that.ws('/ws/peer') + }, + wot: { + member: { + uids : that.get('/wot/members') + } + }, network: { - peering: that.get('/network/peering'), + peering: { + self: that.get('/network/peering') + }, peers: that.get('/network/peers') }, record: { post: postRecord, - remove: removeRecord + remove: removeRecord, + count : countRecord }, image: { fromAttachment: imageFromAttachment, @@ -707,6 +781,53 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic return new EsHttp(host, port, useSsl, useCache); }; + service.lightInstance = function(host, port, useSsl, timeout) { + port = port || 80; + useSsl = angular.isDefined(useSsl) ? useSsl : (port == 443); + + function countHits(path, params) { + return csHttp.get(host, port, path)(params) + .then(function(res) { + return res && res.hits && res.hits.total; + }); + } + + function countRecords(index, type) { + return countHits("/{0}/{1}/_search?size=0".format(index, type)); + } + + function countSubscriptions(params) { + var queryString = _.keys(params||{}).reduce(function(res, key) { + return (res && (res + " AND ") || "") + key + ":" + params[key]; + }, ''); + return countHits("/subscription/record/_search?size=0&q=" + queryString); + } + + return { + host: host, + port: port, + useSsl: useSsl, + node: { + summary: csHttp.getWithCache(host, port, '/node/summary', useSsl, csHttp.cache.LONG, false, timeout) + }, + network: { + peering: { + self: csHttp.get(host, port, '/network/peering', useSsl, timeout) + }, + peers: csHttp.get(host, port, '/network/peers', useSsl, timeout) + }, + blockchain: { + current: csHttp.get(host, port, '/blockchain/current?_source=number,hash,medianTime', useSsl, timeout) + }, + record: { + count: countRecords + }, + subscription: { + count: countSubscriptions + } + }; + }; + return service; }) ; diff --git a/www/plugins/es/js/services/modal-services.js b/www/plugins/es/js/services/modal-services.js index 06a11add94f65b63b4354b30633e9f5d0ae4eba1..b1186405753a15e220e139414db0656d3a7b5807 100644 --- a/www/plugins/es/js/services/modal-services.js +++ b/www/plugins/es/js/services/modal-services.js @@ -1,65 +1,84 @@ angular.module('cesium.es.modal.services', ['cesium.modal.services', 'cesium.es.message.services']) -.factory('esModals', function($state, ModalUtils, UIUtils, csWallet) { - 'ngInject'; + .factory('esModals', function($state, ModalUtils, UIUtils, csWallet) { + 'ngInject'; - function showMessageCompose(parameters) { - return ModalUtils.show('plugins/es/templates/message/modal_compose.html','ESMessageComposeModalCtrl', - parameters, {focusFirstInput: true}); - } + function showMessageCompose(parameters) { + return ModalUtils.show('plugins/es/templates/message/modal_compose.html','ESMessageComposeModalCtrl', + parameters, {focusFirstInput: true}); + } - function showNotificationsPopover(scope, event) { - return UIUtils.popover.show(event, { - templateUrl :'plugins/es/templates/common/popover_notification.html', - scope: scope, - autoremove: false, // reuse popover - afterHidden: scope.resetUnreadCount - }) - .then(function(notification) { - if (!notification) return; // no selection - if (notification.onRead && typeof notification.onRead == 'function') notification.onRead(); - if (notification.state) { - $state.go(notification.state, notification.stateParams); + function updateNotificationCountAndReadTime() { + csWallet.data.notifications.unreadCount = 0; + if (csWallet.data.notifications && csWallet.data.notifications.history.length) { + var lastNotification = csWallet.data.notifications.history[0]; + var readTime = lastNotification ? lastNotification.time : 0; + csSettings.data.wallet = csSettings.data.wallet || {}; + if (readTime && csSettings.data.wallet.notificationReadTime != readTime) { + csSettings.data.wallet.notificationReadTime = readTime; + csSettings.store(); } - }); - } + } + } - function showNewInvitation(parameters) { - return csWallet.auth({minData: true}) - .then(function(walletData) { - UIUtils.loading.hide(); + function showNotificationsPopover(scope, event) { + return UIUtils.popover.show(event, { + templateUrl :'plugins/es/templates/common/popover_notification.html', + scope: scope, + autoremove: false, // reuse popover + afterHidden: updateNotificationCountAndReadTime + }) + .then(function(notification) { + if (!notification) return; // no selection + if (notification.onRead && typeof notification.onRead == 'function') notification.onRead(); + if (notification.state) { + $state.go(notification.state, notification.stateParams); + } + }); + } - // Not allow for non-member - issue #561 - if (!walletData.isMember) { - return UIUtils.alert.error('ERROR.ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION'); - } - return ModalUtils.show('plugins/es/templates/invitation/modal_new_invitation.html', 'ESNewInvitationModalCtrl', - parameters); - }); - } + function showNewInvitation(parameters) { + return csWallet.auth({minData: true}) + .then(function(walletData) { + UIUtils.loading.hide(); + + // Not allow for non-member - issue #561 + if (!walletData.isMember) { + return UIUtils.alert.error('ERROR.ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION'); + } + return ModalUtils.show('plugins/es/templates/invitation/modal_new_invitation.html', 'ESNewInvitationModalCtrl', + parameters); + }); + } + + function showNewPage(options) { + var wallet = options && options.wallet || csWallet; + return wallet.auth({minData: true}) + .then(function() { + UIUtils.loading.hide(); - function showNewPage(options) { - var wallet = options && options.wallet || csWallet; - return wallet.auth({minData: true}) - .then(function() { - UIUtils.loading.hide(); + return ModalUtils.show('plugins/es/templates/registry/modal_record_type.html', undefined, { + title: 'REGISTRY.EDIT.TITLE_NEW' + }) + .then(function(type){ + if (type) { + $state.go('app.registry_add_record', {type: type, wallet: wallet.id}); + } + }); + }); + } - return ModalUtils.show('plugins/es/templates/registry/modal_record_type.html', undefined, { - title: 'REGISTRY.EDIT.TITLE_NEW' - }) - .then(function(type){ - if (type) { - $state.go('app.registry_add_record', {type: type, wallet: wallet.id}); - } - }); - }); - } + function showNetworkLookup(parameters) { + return ModalUtils.show('plugins/es/templates/network/modal_network.html', 'NetworkLookupModalCtrl', + parameters, {focusFirstInput: true}); + } - return { - showMessageCompose: showMessageCompose, - showNotifications: showNotificationsPopover, - showNewInvitation: showNewInvitation, - showNewPage: showNewPage - }; + return { + showMessageCompose: showMessageCompose, + showNotifications: showNotificationsPopover, + showNewInvitation: showNewInvitation, + showNewPage: showNewPage, + showNetworkLookup: showNetworkLookup + }; -}); + }); diff --git a/www/plugins/es/js/services/network-services.js b/www/plugins/es/js/services/network-services.js new file mode 100644 index 0000000000000000000000000000000000000000..d9589f548cfc07bb3ea8f7c7dcccbff10ccda537 --- /dev/null +++ b/www/plugins/es/js/services/network-services.js @@ -0,0 +1,645 @@ + +angular.module('cesium.es.network.services', ['ngApi', 'cesium.es.http.services']) + +.factory('esNetwork', function($rootScope, $q, $interval, $timeout, $window, csSettings, csConfig, esHttp, Api, BMA) { + 'ngInject'; + + factory = function(id) { + + var + interval, + constants = { + UNKNOWN_BUID: -1 + }, + isHttpsMode = $window.location.protocol === 'https:', + api = new Api(this, "csNetwork-" + id), + + data = { + pod: null, + listeners: [], + loading: true, + peers: [], + filter: { + endpointFilter: null, + online: true, + ssl: undefined, + tor: undefined + }, + sort:{ + type: null, + asc: true + }, + expertMode: false, + knownBlocks: [], + mainBlock: null, + searchingPeersOnNetwork: false, + timeout: csConfig.timeout + }, + + // Return the block uid + buid = function(block) { + return block && [block.number, block.hash].join('-'); + }, + + resetData = function() { + data.pod = null; + data.listeners = []; + data.peers.splice(0); + data.filter = { + endpointFilter: null, + online: true + }; + data.sort = { + type: null, + asc: true + }; + data.expertMode = false; + data.knownBlocks = []; + data.mainBlock = null; + data.loading = true; + data.searchingPeersOnNetwork = false; + data.timeout = csConfig.timeout; + + data.document = { + index : csSettings.data.plugins.es && csSettings.data.plugins.es.document && csSettings.data.plugins.es.document.index || 'user', + type: csSettings.data.plugins.es && csSettings.data.plugins.es.document && csSettings.data.plugins.es.document.type || 'profile' + }; + }, + + hasPeers = function() { + return data.peers && data.peers.length > 0; + }, + + getPeers = function() { + return data.peers; + }, + + isBusy = function() { + return data.loading; + }, + + getKnownBlocks = function() { + return data.knownBlocks; + }, + + loadPeers = function() { + data.peers = []; + data.searchingPeersOnNetwork = true; + data.loading = true; + data.pod = data.pod || esHttp; + var newPeers = []; + + if (interval) { + $interval.cancel(interval); + } + + interval = $interval(function() { + // not same job instance + if (newPeers.length) { + flushNewPeersAndSort(newPeers); + } + else if (data.loading && !data.searchingPeersOnNetwork) { + data.loading = false; + $interval.cancel(interval); + // The peer lookup end, we can make a clean final report + sortPeers(true/*update main buid*/); + + console.debug('[network] Finish: {0} peers found.'.format(data.peers.length)); + } + }, 1000); + + return $q.when() + .then(function(){ + // online nodes + if (data.filter.online) { + return data.pod.network.peers() + .then(function(res){ + var jobs = []; + _.forEach(res.peers, function(json) { + if (json.status == 'UP') { + jobs.push(addOrRefreshPeerFromJson(json, newPeers)); + } + }); + + if (jobs.length) return $q.all(jobs); + }) + .catch(function(err) { + // Log and continue + console.error(err); + }); + } + + // offline nodes + return data.pod.network.peers() + .then(function(res){ + var jobs = []; + _.forEach(res.peers, function(json) { + if (json.status !== 'UP') { + jobs.push(addOrRefreshPeerFromJson(json, newPeers)); + } + }); + if (jobs.length) return $q.all(jobs); + }); + }) + + .then(function(){ + data.searchingPeersOnNetwork = false; + }) + .catch(function(err){ + console.error(err); + data.searchingPeersOnNetwork = false; + }); + }, + + /** + * Apply filter on a peer. (peer uid should have been filled BEFORE) + */ + applyPeerFilter = function(peer) { + // no filter + if (!data.filter) return true; + + // Filter on endpoints + if (data.filter.endpointFilter && + (peer.ep && peer.ep.api && peer.ep.api !== data.filter.endpointFilter || !peer.hasEndpoint(data.filter.endpointFilter))) { + return false; + } + + // Filter on status + if (!data.filter.online && peer.status == 'UP') { + return false; + } + + // Filter on ssl + if (angular.isDefined(data.filter.ssl) && peer.isSsl() != data.filter.ssl) { + return false; + } + + // Filter on tor + if (angular.isDefined(data.filter.tor) && peer.isTor() != data.filter.tor) { + return false; + } + + return true; + }, + + addOrRefreshPeerFromJson = function(json, list) { + list = list || data.newPeers; + + var peers = createPeerEntities(json); + var hasUpdates = false; + + var jobs = peers.reduce(function(jobs, peer) { + var existingPeer = _.findWhere(data.peers, {id: peer.id}); + var existingMainBuid = existingPeer ? existingPeer.buid : null; + var existingOnline = existingPeer ? existingPeer.online : false; + + return jobs.concat( + refreshPeer(peer) + .then(function (refreshedPeer) { + if (existingPeer) { + // remove existing peers, when reject or offline + if (!refreshedPeer || (refreshedPeer.online !== data.filter.online && data.filter.online !== 'all')) { + console.debug('[network] Peer [{0}] removed (cause: {1})'.format(peer.server, !refreshedPeer ? 'filtered' : (refreshedPeer.online ? 'UP': 'DOWN'))); + data.peers.splice(data.peers.indexOf(existingPeer), 1); + hasUpdates = true; + } + else if (refreshedPeer.buid !== existingMainBuid){ + console.debug('[network] {0} endpoint [{1}] new current block'.format( + refreshedPeer.ep && (refreshedPeer.ep.useBma ? 'BMA' : 'WS2P') || 'null', + refreshedPeer.server)); + hasUpdates = true; + } + else if (existingOnline !== refreshedPeer.online){ + console.debug('[network] {0} endpoint [{1}] is now {2}'.format( + refreshedPeer.ep && (refreshedPeer.ep.useBma ? 'BMA' : 'WS2P') || 'null', + refreshedPeer.server, + refreshedPeer.online ? 'UP' : 'DOWN')); + hasUpdates = true; + } + else { + console.debug("[network] {0} endpoint [{1}] unchanged".format( + refreshedPeer.ep && (refreshedPeer.ep.useBma ? 'BMA' : 'WS2P') || 'null', + refreshedPeer.server)); + } + } + else if (refreshedPeer && (refreshedPeer.online === data.filter.online || data.filter.online === 'all')) { + console.debug("[network] {0} endpoint [{1}] is {2}".format( + refreshedPeer.ep && refreshedPeer.ep.api || '', + refreshedPeer.server, + refreshedPeer.online ? 'UP' : 'DOWN' + )); + list.push(refreshedPeer); + hasUpdates = true; + } + }) + ); + }, []); + return (jobs.length === 1 ? jobs[0] : $q.all(jobs)) + .then(function() { + return hasUpdates; + }); + }, + + createPeerEntities = function(json, ep) { + if (!json) return []; + var peer = new EsPeer(json); + + // Read endpoints + if (!ep) { + var endpointsAsString = peer.getEndpoints(); + if (!endpointsAsString) return []; // no BMA + + var endpoints = endpointsAsString.reduce(function (res, epStr) { + var ep = esHttp.node.parseEndPoint(epStr); + return ep ? res.concat(ep) : res; + }, []); + + // recursive call, on each endpoint + if (endpoints.length > 1) { + return endpoints.reduce(function (res, ep) { + return res.concat(createPeerEntities(json, ep)); + }, []); + } + else { + // if only one endpoint: use it and continue + ep = endpoints[0]; + } + } + peer.ep = ep; + peer.server = peer.getServer(); + peer.dns = peer.getDns(); + peer.blockNumber = peer.block && peer.block.replace(/-.+$/, ''); + peer.id = peer.keyID(); + return [peer]; + }, + + refreshPeer = function(peer) { + + // Apply filter + if (!applyPeerFilter(peer)) return $q.when(); + + if (!data.filter.online || (data.filter.online === 'all' && peer.status === 'DOWN') || !peer.getHost() /*fix #537*/) { + peer.online = false; + return $q.when(peer); + } + + // App running in SSL: Do not try to access not SSL node, + if (isHttpsMode && !peer.isSsl()) { + peer.online = (peer.status === 'UP'); + peer.buid = constants.UNKNOWN_BUID; + delete peer.version; + + return $q.when(peer); + } + + // Do not try to access TOR or WS2P endpoints + if (peer.ep.useTor) { + peer.online = (peer.status == 'UP'); + peer.buid = constants.UNKNOWN_BUID; + delete peer.software; + delete peer.version; + return $q.when(peer); + } + + peer.api = peer.api || esHttp.lightInstance(peer.getHost(), peer.getPort(), peer.isSsl(), data.timeout); + + // Get current block + return peer.api.blockchain.current() + .then(function(block) { + peer.currentNumber = block.number; + peer.online = true; + peer.buid = buid(block); + peer.medianTime = block.medianTime; + if (data.knownBlocks.indexOf(peer.buid) === -1) { + data.knownBlocks.push(peer.buid); + } + return peer; + }) + .catch(function(err) { + // Special case for currency init (root block not exists): use fixed values + if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) { + peer.online = true; + peer.buid = buid({number:0, hash: BMA.constants.ROOT_BLOCK_HASH}); + peer.difficulty = 0; + return peer; + } + if (!peer.secondTry) { + var ep = peer.ep || peer.getEP(); + if (ep.dns && peer.server.indexOf(ep.dns) == -1) { + // try again, using DNS instead of IPv4 / IPV6 + peer.secondTry = true; + peer.api = esHttp.lightInstance(ep.dns, ep.port, ep.useSsl); + return refreshPeer(peer); // recursive call + } + } + + peer.online=false; + peer.currentNumber = null; + peer.buid = null; + return peer; + }) + .then(function(peer) { + // Exit if offline + if (!data.filter.online || !peer || !peer.online) return peer; + + peer.docCount = {}; + + return $q.all([ + // Get summary (software and version) - expert mode only + !data.expertMode ? $q.when() : peer.api.node.summary() + .then(function(res){ + peer.software = res && res.duniter && res.duniter.software || undefined; + peer.version = res && res.duniter && res.duniter.version || '?'; + }) + .catch(function() { + peer.software = undefined; + peer.version = '?'; + }), + + // Count documents + peer.api.record.count(data.document.index,data.document.type) + .then(function(count){ + peer.docCount.record = count; + }) + .catch(function() { + peer.docCount.record = undefined; + }), + + // Count email subscription + peer.api.subscription.count({recipient: peer.pubkey, type: 'email'}) + .then(function(res){ + peer.docCount.emailSubscription = res; + }) + .catch(function() { + peer.docCount.emailSubscription = undefined; // continue + }) + ]); + + }) + .then(function() { + // Clean the instance + delete peer.api; + return peer; + }); + }, + + flushNewPeersAndSort = function(newPeers, updateMainBuid) { + newPeers = newPeers || data.newPeers; + if (!newPeers.length) return; + var ids = _.map(data.peers, function(peer){ + return peer.id; + }); + var hasUpdates = false; + var newPeersAdded = 0; + _.forEach(newPeers.splice(0), function(peer) { + if (!ids[peer.id]) { + data.peers.push(peer); + ids[peer.id] = peer; + hasUpdates = true; + newPeersAdded++; + } + }); + if (hasUpdates) { + console.debug('[network] Flushing {0} new peers...'.format(newPeersAdded)); + sortPeers(updateMainBuid); + } + }, + + computeScoreAlphaValue = function(value, nbChars, asc) { + if (!value) return 0; + var score = 0; + value = value.toLowerCase(); + if (nbChars > value.length) { + nbChars = value.length; + } + score += value.charCodeAt(0); + for (var i=1; i < nbChars; i++) { + score += Math.pow(0.001, i) * value.charCodeAt(i); + } + return asc ? (1000 - score) : score; + }, + + sortPeers = function(updateMainBuid) { + // Construct a map of buid, with peer count and medianTime + var buids = {}; + _.forEach(data.peers, function(peer){ + if (peer.buid) { + var buid = buids[peer.buid]; + if (!buid || !buid.medianTime) { + buid = { + buid: peer.buid, + count: 0, + medianTime: peer.medianTime + }; + buids[peer.buid] = buid; + } + // If not already done, try to fill medianTime (need to compute consensusBlockDelta) + else if (!buid.medianTime && peer.medianTime) { + buid.medianTime = peer.medianTime; + } + if (buid.buid != constants.UNKNOWN_BUID) { + buid.count++; + } + } + }); + // Compute pct of use, per buid + _.forEach(_.values(buids), function(buid) { + buid.pct = buid.count * 100 / data.peers.length; + }); + var mainBlock = _.max(buids, function(obj) { + return obj.count; + }); + _.forEach(data.peers, function(peer){ + peer.hasMainConsensusBlock = peer.buid == mainBlock.buid; + peer.hasConsensusBlock = peer.buid && !peer.hasMainConsensusBlock && buids[peer.buid].count > 1; + if (peer.hasConsensusBlock) { + peer.consensusBlockDelta = buids[peer.buid].medianTime - mainBlock.medianTime; + } + }); + data.peers = _.uniq(data.peers, false, function(peer) { + return peer.id; + }); + data.peers = _.sortBy(data.peers, function(peer) { + var score = 0; + if (data.sort.type) { + var sortScore = 0; + sortScore += (data.sort.type == 'name' ? computeScoreAlphaValue(peer.name, 10, data.sort.asc) : 0); + sortScore += (data.sort.type == 'software' ? computeScoreAlphaValue(peer.software, 10, data.sort.asc) : 0); + sortScore += (data.sort.type == 'api') && + ((peer.hasEndpoint('ES_SUBSCRIPTION_API') && (data.sort.asc ? 1 : -1) || 0) + + (peer.hasEndpoint('ES_USER_API') && (data.sort.asc ? 0.01 : -0.01) || 0) + + (peer.isSsl() && (data.sort.asc ? 0.75 : -0.75) || 0)) || 0; + sortScore += (data.sort.type == 'doc_count' ? (peer.docCount ? (data.sort.asc ? (1000000000 - peer.docCount) : peer.docCount) : 0) : 0); + score += (10000000000 * sortScore); + } + score += (1000000000 * (peer.online ? 1 : 0)); + score += (100000000 * (peer.hasMainConsensusBlock ? 1 : 0)); + score += (1000000 * (peer.hasConsensusBlock ? buids[peer.buid].pct : 0)); + if (data.expertMode) { + score += (100 * (peer.difficulty ? (10000-peer.difficulty) : 0)); + score += (1 * (peer.uid ? computeScoreAlphaValue(peer.uid, 2, true) : 0)); + } + else { + score += (100 * (peer.uid ? computeScoreAlphaValue(peer.uid, 2, true) : 0)); + score += (1 * (!peer.uid ? computeScoreAlphaValue(peer.pubkey, 2, true) : 0)); + } + return -score; + }); + + // Raise event on new main block + if (updateMainBuid && mainBlock.buid && (!data.mainBlock || data.mainBlock.buid !== mainBlock.buid)) { + data.mainBlock = mainBlock; + api.data.raise.mainBlockChanged(mainBlock); + } + + // Raise event when changed + api.data.raise.changed(data); // raise event + }, + + removeListeners = function() { + _.forEach(data.listeners, function(remove){ + remove(); + }); + data.listeners = []; + }, + + addListeners = function() { + data.listeners = [ + + // Listen for new block + data.pod.websocket.block().onListener(function(block) { + if (!block || data.loading) return; + var buid = [block.number, block.hash].join('-'); + if (data.knownBlocks.indexOf(buid) === -1) { + console.debug('[network] Receiving block: ' + buid.substring(0, 20)); + data.knownBlocks.push(buid); + // If first block: do NOT refresh peers (will be done in start() method) + var skipRefreshPeers = data.knownBlocks.length === 1; + if (!skipRefreshPeers) { + data.loading = true; + // We wait 2s when a new block is received, just to wait for network propagation + $timeout(function() { + console.debug('[network] new block received by WS: will refresh peers'); + loadPeers(); + }, 2000, false /*invokeApply*/); + } + } + }), + + // Listen for new peer + data.pod.websocket.peer().onListener(function(json) { + if (!json || data.loading) return; + var newPeers = []; + addOrRefreshPeerFromJson(json, newPeers) + .then(function(hasUpdates) { + if (!hasUpdates) return; + if (newPeers.length>0) { + flushNewPeersAndSort(newPeers, true); + } + else { + console.debug('[network] [ws] Peers updated received'); + sortPeers(true); + } + }); + }) + ]; + }, + + sort = function(options) { + options = options || {}; + data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter; + data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort; + sortPeers(false); + }, + + start = function(pod, options) { + options = options || {}; + return esHttp.ready() + .then(function() { + close(); + data.pod = pod || esHttp; + data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter; + data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort; + data.expertMode = angular.isDefined(options.expertMode) ? options.expertMode : data.expertMode; + data.timeout = angular.isDefined(options.timeout) ? options.timeout : csConfig.timeout; + console.info('[network] Starting network from [{0}]'.format(data.pod.server)); + var now = Date.now(); + + addListeners(); + + return loadPeers() + .then(function(peers){ + console.debug('[network] Started in '+(Date.now() - now)+'ms'); + return peers; + }); + }); + }, + + close = function() { + if (data.pod) { + console.info('[network-service] Stopping...'); + removeListeners(); + } + resetData(); + }, + + isStarted = function() { + return !data.pod; + }, + + $q_started = function(callback) { + if (!isStarted()) { // start first + return start() + .then(function() { + return $q(callback); + }); + } + else { + return $q(callback); + } + }, + + getMainBlockUid = function() { + return $q_started(function(resolve, reject){ + resolve (data.mainBuid); + }); + }, + + // Get peers on the main consensus blocks + getTrustedPeers = function() { + return $q_started(function(resolve, reject){ + resolve(data.peers.reduce(function(res, peer){ + return (peer.hasMainConsensusBlock && peer.uid) ? res.concat(peer) : res; + }, [])); + }); + } + ; + + // Register extension points + api.registerEvent('data', 'changed'); + api.registerEvent('data', 'mainBlockChanged'); + api.registerEvent('data', 'rollback'); + + return { + id: id, + data: data, + start: start, + close: close, + hasPeers: hasPeers, + getPeers: getPeers, + sort: sort, + getTrustedPeers: getTrustedPeers, + getKnownBlocks: getKnownBlocks, + getMainBlockUid: getMainBlockUid, + loadPeers: loadPeers, + isBusy: isBusy, + // api extension + api: api + }; + }; + + var service = factory('default'); + + service.instance = factory; + return service; +}); diff --git a/www/plugins/es/templates/document/item_document_comment.html b/www/plugins/es/templates/document/item_document_comment.html new file mode 100644 index 0000000000000000000000000000000000000000..48a46ae3f1af5fda51c82c7e88d37b8af4edbe9e --- /dev/null +++ b/www/plugins/es/templates/document/item_document_comment.html @@ -0,0 +1,44 @@ +<ion-item id="doc-{{::doc.id}}" + class="item item-document item-document-comment item-icon-left ink {{::ionItemClass}} no-padding-top no-padding-bottom" + ng-class="{'compacted': compactMode}" + ng-click="selectDocument($event, doc)"> + + <i ng-show=":rebind:!compactMode" ng-if=":rebind:!doc.avatar" class="icon ion-ios-chatbubble-outline stable" ></i> + <i ng-show=":rebind:!compactMode" ng-if=":rebind:doc.avatar" class="avatar" style="background-image: url('{{:rebind:doc.avatar.src}}')"></i> + + <div class="row no-padding"> + <div class="col"> + <h4 > + <i class="ion-ios-chatbubble-outline dark"></i> + <span class="gray" ng-if=":rebind:doc.name"> + <i class="ion-person" ng-show=":rebind:!compactMode"></i> + {{:rebind:doc.name}}: + </span> + <span class="dark"> + <i class="ion-quote" ng-if=":rebind:!compactMode"></i> + {{:rebind:doc.message|truncText:50}} + </span> + </h4> + <h4 class="gray"> <i class="ion-clock"></i> {{:rebind:doc.time|formatDate}}</h4> + </div> + + <div class="col"> + <h3> + <a ui-sref="app.wot_identity({pubkey: doc.pubkey, uid: doc.name})"> + + </a> + </h3> + </div> + + <div class="col" ng-if=":rebind:!compactMode"> + <a + ng-if=":rebind:login && doc.pubkey==walletData.pubkey" + ng-click="remove($event, $index)" + class="gray pull-right hidden-xs hidden-sm" + title="{{'DOCUMENT.LOOKUP.BTN_REMOVE'|translate}}"> + <i class="ion-trash-a"></i> + </a> + </div> + + </div> +</ion-item> diff --git a/www/plugins/es/templates/document/item_document_profile.html b/www/plugins/es/templates/document/item_document_profile.html new file mode 100644 index 0000000000000000000000000000000000000000..9416881469bf6f655e142762a62f29a5f683a1e3 --- /dev/null +++ b/www/plugins/es/templates/document/item_document_profile.html @@ -0,0 +1,41 @@ +<ion-item id="doc-{{::doc.id}}" + class="item item-document item-icon-left ink {{::ionItemClass}} no-padding-top no-padding-bottom" + ng-class="{'compacted': compactMode}" + ng-click="selectDocument($event, doc)"> + + <i ng-show=":rebind:!compactMode" ng-if=":rebind:doc.avatar" class="avatar" style="background-image: url({{:rebind:doc.avatar.src}})"></i> + <i ng-show=":rebind:!compactMode" ng-if=":rebind:!doc.avatar" class="icon ion-person stable"></i> + + <div class="row no-padding"> + <div class="col"> + <h4 ng-if=":rebind:doc.title"> + <i class="ion-person gray"></i> + <span class="dark"> + {{:rebind:doc.title}} + </span> + <span class="gray" > + {{:rebind:'DOCUMENT.LOOKUP.HAS_REGISTERED'|translate}} + </span> + </h4> + <h4> + <span class="dark" ng-if=":rebind:doc.city"> + <i class="ion-location"></i> {{:rebind:doc.city}} + </span> + <span class="gray"> + <i class="ion-clock"></i> {{:rebind:doc.time|formatDate}} + </span> + </h4> + </div> + + <div class="col" ng-if=":rebind:!compactMode"> + <a + ng-if=":rebind:login && doc.pubkey==walletData.pubkey" + ng-click="remove($event, $index)" + class="gray pull-right" + title="{{'DOCUMENT.LOOKUP.BTN_REMOVE'|translate}}"> + <i class="ion-trash-a"></i> + </a> + </div> + + </div> +</ion-item> diff --git a/www/plugins/es/templates/document/items_documents.html b/www/plugins/es/templates/document/items_documents.html index 6306e779a66007c9f22db73d29ebda6714b21d39..b72f39ca728e0c755cdf6457ec46d91160792d58 100644 --- a/www/plugins/es/templates/document/items_documents.html +++ b/www/plugins/es/templates/document/items_documents.html @@ -1,5 +1,5 @@ -<div class="item row row-header done in hidden-xs hidden-sm"> +<div class="item row row-header done in hidden-xs hidden-sm" ng-if="showHeaders"> <a class="no-padding dark col col-header" ng-if=":rebind:expertMode" @@ -27,5 +27,14 @@ <!-- for each doc --> <ng-repeat ng-repeat="doc in :rebind:search.results track by doc.id" - ng-include="'plugins/es/templates/document/item_document.html'"> + ng-switch on="doc.type"> + <div ng-switch-when="comment"> + <ng-include src="::'plugins/es/templates/document/item_document_comment.html'"></ng-include> + </div> + <div ng-switch-when="profile"> + <ng-include src="::'plugins/es/templates/document/item_document_profile.html'"></ng-include> + </div> + <div ng-switch-default> + <ng-include src="::'plugins/es/templates/document/item_document.html'"></ng-include> + </div> </ng-repeat> diff --git a/www/plugins/es/templates/document/list_documents.html b/www/plugins/es/templates/document/list_documents.html new file mode 100644 index 0000000000000000000000000000000000000000..a161798401396a50ee1e05116f83399196040255 --- /dev/null +++ b/www/plugins/es/templates/document/list_documents.html @@ -0,0 +1,13 @@ + +<ion-list class="list" ng-class="::motion.ionListClass"> + + <ng-include src="'plugins/es/templates/document/items_documents.html'"></ng-include> + +</ion-list> + +<ion-infinite-scroll + ng-if="!search.loading && search.hasMore" + spinner="android" + on-infinite="showMore()" + distance="1%"> +</ion-infinite-scroll> diff --git a/www/plugins/es/templates/network/item_content_peer.html b/www/plugins/es/templates/network/item_content_peer.html new file mode 100644 index 0000000000000000000000000000000000000000..efbad7b3920cda9343dd16cb7643dc1bdcb2252a --- /dev/null +++ b/www/plugins/es/templates/network/item_content_peer.html @@ -0,0 +1,69 @@ + + <i class="icon ion-android-desktop" + ng-class=":rebind:{'balanced': peer.online && peer.hasMainConsensusBlock, 'energized': peer.online && peer.hasConsensusBlock, 'gray': peer.online && !peer.hasConsensusBlock && !peer.hasMainConsensusBlock, 'stable': !peer.online}" + ng-if=":rebind:!peer.avatar"></i> + <b class="icon-secondary ion-person" ng-if=":rebind:!peer.avatar" + ng-class=":rebind:{'balanced': peer.online && peer.hasMainConsensusBlock, 'energized': peer.online && peer.hasConsensusBlock, 'gray': peer.online && !peer.hasConsensusBlock && !peer.hasMainConsensusBlock, 'stable': !peer.online}" + style="left: 26px; top: -3px;"></b> + <i class="avatar" ng-if=":rebind:peer.avatar" style="background-image: url('{{:rebind:peer.avatar.src}}')"></i> + <b class="icon-secondary assertive ion-close-circled" ng-if=":rebind:!peer.online" style="left: 37px; top: -10px;"></b> + + <div class="row no-padding"> + <div class="col no-padding"> + <h3 class="dark">{{:rebind:peer.dns || peer.server}}</h3> + <h4> + <span class="gray" ng-if=":rebind:!peer.name"> + <i class="ion-key"></i> {{:rebind:peer.pubkey|formatPubkey}} + </span> + <span class="positive" ng-if=":rebind:peer.name"> + <i class="ion-person"></i> {{:rebind:peer.name}} + </span> + <span class="gray">{{:rebind:peer.dns && (' | ' + peer.server) + (peer.ep.path||'') }}</span> + </h4> + </div> + <div class="col col-15 no-padding text-center hidden-xs hidden-sm" ng-if="::expertMode"> + <div style="min-width: 50px; padding-top: 5px;" > + <span ng-if=":rebind:peer.isSsl()" title="SSL"> + <i class="ion-locked"></i><small class="hidden-md"> SSL</small> + </span> + <span ng-if=":rebind:peer.hasEndpoint('ES_SUBSCRIPTION_API')" + title="{{'ES_PEER.EMAIL_SUBSCRIPTION_COUNT'|translate: peer.docCount }}"> + <i class="ion-email"></i> {{:rebind:peer.docCount.emailSubscription || '?'}} + </span> + </div> + <div ng-if=":rebind:peer.isTor()"> + <i class="ion-bma-tor-api"></i> + </div> + <div ng-if=":rebind:peer.isWs2p()&&peer.isTor()" ng-click="showWs2pPopover($event, peer)"> + <i class="ion-bma-tor-api"></i> + </div> + </div> + <div class="col col-20 no-padding text-center" ng-if="::!expertMode && search.type != 'offline'"> + <div style="min-width: 50px; padding-top: 5px;" ng-if=":rebind:peer.docCount.emailSubscription!==undefined"> + <span ng-if=":rebind:peer.hasEndpoint('ES_SUBSCRIPTION_API')" + title="{{'ES_PEER.EMAIL_SUBSCRIPTION_COUNT'|translate: peer.docCount }}"> + <i class="ion-email"></i> {{:rebind:peer.docCount.emailSubscription || '?'}} + </span> + </div> + </div> + <div class="col col-20 no-padding text-center" ng-if="::expertMode && search.type != 'offline'"> + <h4 class="hidden-sm hidden-xs gray"> + {{:rebind:peer.software||'?'}} + </h4> + <h4 class="hidden-sm hidden-xs gray">{{:rebind: peer.version ? ('v'+peer.version) : ''}}</h4> + </div> + <div class="col col-20 no-padding text-center" id="{{$index === 0 ? helptipPrefix + '-peer-0-block' : ''}}"> + <span class="badge badge-stable"> + {{:rebind:peer.docCount.record !== undefined ? (peer.docCount.record|formatInteger) : '?'}} + <span ng-if=":rebind:!expertMode && peer.docCount.record!==undefined"> + {{::'ES_PEER.DOCUMENTS'|translate|lowercase }} + </span> + </span> + <span class="badge badge-secondary" + ng-class=":rebind:{'balanced': peer.hasMainConsensusBlock, 'energized': peer.hasConsensusBlock, 'ng-hide': !peer.currentNumber }" + ng-if="::expertMode"> + {{:rebind:'BLOCKCHAIN.VIEW.TITLE'|translate: {number:peer.currentNumber} }} + </span> + + </div> + </div> diff --git a/www/plugins/es/templates/network/items_peers.html b/www/plugins/es/templates/network/items_peers.html new file mode 100644 index 0000000000000000000000000000000000000000..d2c6b6225dde9c29affb2a1fb9bed9626b7b4633 --- /dev/null +++ b/www/plugins/es/templates/network/items_peers.html @@ -0,0 +1,36 @@ +<div ng-class="::motion.ionListClass" class="no-padding"> + + <div class="item item-text-wrap no-border done in gray no-padding-top no-padding-bottom inline text-italic" ng-if="::isHttps && expertMode"> + <small><i class="icon ion-alert-circled"></i> {{::'NETWORK.INFO.ONLY_SSL_PEERS'|translate}}</small> + </div> + + <div class="item row row-header hidden-xs hidden-sm done in" ng-if="::expertMode"> + <a class="col col-header no-padding dark" ng-click="toggleSort('name')"> + <cs-sort-icon asc="search.asc" sort="search.sort" toggle="'name'"></cs-sort-icon> + {{::'ES_PEER.NAME' | translate}} / {{::'COMMON.PUBKEY' | translate}} + </a> + <a class="no-padding dark hidden-md col col-15 col-header" + ng-click="toggleSort('api')"> + <cs-sort-icon asc="search.asc" sort="search.sort" toggle="'api'"></cs-sort-icon> + {{::'PEER.API' | translate}} + </a> + <a class="no-padding dark col col-20 col-header" + ng-click="toggleSort('difficulty')"> + <cs-sort-icon asc="search.asc" sort="search.sort" toggle="'software'"></cs-sort-icon> + {{::'ES_PEER.SOFTWARE' | translate}} + </a> + <a class="col col-20 col-header no-padding dark" ng-click="toggleSort('doc_count')"> + <cs-sort-icon asc="search.asc" sort="search.sort" toggle="'doc_count'"></cs-sort-icon> + {{::'ES_PEER.DOCUMENTS' | translate}} + </a> + </div> + + <div ng-repeat="peer in :rebind:search.results track by peer.id" + class="item item-peer item-icon-left ink" + ng-class="::ionItemClass" + id="{{helptipPrefix}}-peer-{{$index}}" + ng-click="selectPeer(peer)" + ng-include="'plugins/es/templates/network/item_content_peer.html'"> + </div> + +</div> diff --git a/www/plugins/es/templates/network/lookup_popover_actions.html b/www/plugins/es/templates/network/lookup_popover_actions.html new file mode 100644 index 0000000000000000000000000000000000000000..ac30de8e11325b3d163ec319efbbaa3e557402d2 --- /dev/null +++ b/www/plugins/es/templates/network/lookup_popover_actions.html @@ -0,0 +1,31 @@ +<ion-popover-view class="fit has-header"> + <ion-header-bar> + <h1 class="title" translate>PEER.POPOVER_FILTER_TITLE</h1> + </ion-header-bar> + <ion-content scroll="false"> + <div class="list item-text-wrap"> + + <a class="item item-icon-left item-icon-right ink" + ng-click="toggleSearchType('member')"> + <i class="icon ion-person"></i> + {{'PEER.MEMBERS' | translate}} + <i class="icon ion-ios-checkmark-empty" ng-show="search.type=='member'"></i> + </a> + + <a class="item item-icon-left item-icon-right ink" + ng-click="toggleSearchType('mirror')"> + <i class="icon ion-radio-waves"></i> + {{'PEER.MIRRORS' | translate}} + <i class="icon ion-ios-checkmark-empty" ng-show="search.type=='mirror'"></i> + </a> + + <a class="item item-icon-left item-icon-right ink" + ng-click="toggleSearchType('offline')"> + <i class="icon ion-eye-disabled"></i> + {{'PEER.OFFLINE' | translate}} + <i class="icon ion-ios-checkmark-empty" ng-show="search.type=='offline'"></i> + </a> + + </div> + </ion-content> +</ion-popover-view> diff --git a/www/plugins/es/templates/network/modal_network.html b/www/plugins/es/templates/network/modal_network.html new file mode 100644 index 0000000000000000000000000000000000000000..efe1c1dd3a806631782b8b2767af09a44eff7c03 --- /dev/null +++ b/www/plugins/es/templates/network/modal_network.html @@ -0,0 +1,36 @@ +<ion-modal-view id="nodes" class="modal-full-height" cache-view="false"> + <ion-header-bar class="bar-positive"> + <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button> + <h1 class="title" translate>PEER.PEER_LIST</h1> + <div class="buttons buttons-right header-item"> + <span class="secondary"> + <button class="button button-clear icon ion-loop button-clear" ng-click="refresh()"> + + </button> + <button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" + ng-click="showActionsPopover($event)"> + </button> + </span> + </div> + </ion-header-bar> + + <ion-content> + <div class="list"> + <div class="padding padding-xs" style="display: block; height: 60px;"> + + <div class="pull-left"> + <h4 ng-if="!enableFilter || !search.type"> + {{'PEER.ALL_PEERS' | translate}} <span ng-if="!search.loading">({{search.results.length}})</span> + </h4> + </div> + + <div class="pull-right"> + <ion-spinner class="icon" icon="android" ng-if="search.loading"></ion-spinner> + </div> + </div> + + <ng-include src="'plugins/es/templates/network/items_peers.html'"></ng-include> + + </div> + </ion-content> +</ion-modal-view> diff --git a/www/plugins/es/templates/network/popover_endpoints.html b/www/plugins/es/templates/network/popover_endpoints.html new file mode 100644 index 0000000000000000000000000000000000000000..75338af01396ddeb3b6f0739b39a8aa70d42c2c3 --- /dev/null +++ b/www/plugins/es/templates/network/popover_endpoints.html @@ -0,0 +1,15 @@ +<ion-popover-view class="popover-endpoints popover-light" style="height: {{(titleKey?30:0)+((!items || items.length <= 1) ? 55 : 3+items.length*52)}}px"> + <ion-header-bar class="bar bar-header stable-bg" ng-if="titleKey"> + <div class="title"> + {{titleKey | translate:titleValues }} + </div> + </ion-header-bar> + <ion-content scroll="false"> + <div class="list" ng-class="{'has-header': titleKey}"> + <div class="item item-text-wrap" ng-repeat="item in items"> + <div class="item-label" ng-if="item.label">{{item.label | translate}}</div> + <div id="endpoint_{{$index}}" class="badge item-note dark">{{item.value}}</span> + </div> + </div> + </ion-content> +</ion-popover-view> diff --git a/www/plugins/es/templates/network/popover_network.html b/www/plugins/es/templates/network/popover_network.html new file mode 100644 index 0000000000000000000000000000000000000000..ce40b91754b9baec9110db07cac15eebfeccdc5b --- /dev/null +++ b/www/plugins/es/templates/network/popover_network.html @@ -0,0 +1,38 @@ +<ion-popover-view class="fit hidden-xs hidden-sm popover-notification popover-network" + ng-controller="NetworkLookupPopoverCtrl"> + <ion-header-bar class="stable-bg block"> + <div class="title"> + {{'MENU.NETWORK'|translate}} + <ion-spinner class="ion-spinner-small" icon="android" ng-if="search.loading"></ion-spinner> + </div> + + <div class="pull-right"> + <a ng-class="{'positive': search.type=='member', 'dark': search.type!=='member'}" + ng-click="toggleSearchType('member')" + translate>PEER.MEMBERS</a> + </div> + </ion-header-bar> + <ion-content scroll="true"> + <div class="list no-padding"> + <ng-include src="'plugins/es/templates/network/items_peers.html'"></ng-include> + </div> + </ion-content> + + <ion-footer-bar class="stable-bg block"> + <!-- settings --> + <div class="pull-left"> + <a class="positive" + ui-sref="app.settings" + ng-click="closePopover()" + translate>COMMON.NOTIFICATIONS.SETTINGS</a> + </div> + + <!-- show all --> + <div class="pull-right"> + <a class="positive" + ui-sref="app.es_network" + ng-click="closePopover()" + translate>COMMON.NOTIFICATIONS.SHOW_ALL</a> + </div> + </ion-footer-bar> +</ion-popover-view> diff --git a/www/plugins/es/templates/network/popover_peer_info.html b/www/plugins/es/templates/network/popover_peer_info.html new file mode 100644 index 0000000000000000000000000000000000000000..a01e83fd910cdaabb7ddcc7374d5806b3fa28611 --- /dev/null +++ b/www/plugins/es/templates/network/popover_peer_info.html @@ -0,0 +1,86 @@ +<ion-popover-view class="fit hidden-xs hidden-sm popover-notification popover-peer-info" + ng-controller="PeerInfoPopoverCtrl"> + <ion-header-bar class="stable-bg block"> + <div class="title"> + {{'PEER.VIEW.TITLE'|translate}} + </div> + </ion-header-bar> + <ion-content scroll="true" > + <div class="center padding" ng-if="loading"> + <ion-spinner icon="android"></ion-spinner> + </div> + + <div class="list no-padding" ng-if="!loading"> + + <div class="item" ng-if=":rebind:formData.software"> + <i class="ion-outlet"></i> + {{'NETWORK.VIEW.SOFTWARE'|translate}} + <div class="badge" + ng-class=":rebind:{'badge-energized': formData.isPreRelease, 'badge-assertive': formData.hasNewRelease }"> + {{formData.software}} v{{:rebind:formData.version}} + </div> + <div class="gray badge badge-secondary" ng-if="formData.isPreRelease"> + <i class="ion-alert-circled"></i> + <span ng-bind-html="'NETWORK.VIEW.WARN_PRE_RELEASE'|translate: formData.latestRelease"></span> + </div> + <div class="gray badge badge-secondary" ng-if="formData.hasNewRelease"> + <i class="ion-alert-circled"></i> + <span ng-bind-html="'NETWORK.VIEW.WARN_NEW_RELEASE'|translate: formData.latestRelease"></span> + </div> + </div> + + <div class="item"> + <i class="ion-locked"></i> + {{'NETWORK.VIEW.ENDPOINTS.BMAS'|translate}} + <div class="badge badge-balanced" ng-if=":rebind:formData.useSsl" translate>COMMON.BTN_YES</div> + <div class="badge badge-assertive" ng-if=":rebind:!formData.useSsl" translate>COMMON.BTN_NO</div> + </div> + + <div class="item"> + <i class="ion-cube"></i> + {{'BLOCKCHAIN.VIEW.TITLE_CURRENT'|translate}} + <div class="badge badge-balanced"> + {{:rebind:formData.number | formatInteger}} + </div> + </div> + + <div class="item"> + <i class="ion-clock"></i> + {{'CURRENCY.VIEW.MEDIAN_TIME'|translate}} + <div class="badge dark"> + {{:rebind:formData.medianTime | medianDate}} + </div> + </div> + + <div class="item"> + <i class="ion-lock-combination"></i> + {{'CURRENCY.VIEW.POW_MIN'|translate}} + <div class="badge dark"> + {{:rebind:formData.powMin | formatInteger}} + </div> + </div> + + <!-- Allow extension here --> + <cs-extension-point name="default"></cs-extension-point> + + </div> + </ion-content> + + <ion-footer-bar class="stable-bg block"> + <!-- settings --> + <div class="pull-left"> + <a class="positive" + ui-sref="app.settings" + ng-click="closePopover()" + translate>MENU.SETTINGS</a> + </div> + + <!-- show all --> + <div class="pull-right"> + <a class="positive" + ui-sref="app.view_es_peer" + ng-click="closePopover()" + translate>PEER.BTN_SHOW_PEER</a> + </div> + </ion-footer-bar> +</ion-popover-view> diff --git a/www/plugins/es/templates/network/view_network.html b/www/plugins/es/templates/network/view_network.html new file mode 100644 index 0000000000000000000000000000000000000000..d2c29d8c1ae0a65b2aed875cf89383dce2487135 --- /dev/null +++ b/www/plugins/es/templates/network/view_network.html @@ -0,0 +1,79 @@ +<ion-view> + <ion-nav-title> + <span translate>MENU.NETWORK</span> + </ion-nav-title> + + <ion-nav-buttons side="secondary"> + <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="refresh()"> + </button> + </ion-nav-buttons> + + + <ion-content scroll="true" ng-init="enableFilter=true; ionItemClass='item-border-large';"> + + <div class="row responsive-sm responsive-md responsive-lg"> + <div class="col list col-border-right"> + <div class="padding padding-xs" style="display: block; height: 60px;"> + <div class="pull-left"> + <h4> + <span ng-if="enableFilter && !search.online" translate>PEER.OFFLINE_PEERS</span> + <span ng-if="!enableFilter || search.online" translate>PEER.ALL_PEERS</span> + <span ng-if="search.results.length">({{search.results.length}})</span> + <ion-spinner ng-if="search.loading" class="icon ion-spinner-small" icon="android"></ion-spinner> + </h4> + </div> + + <div class="pull-right"> + + <div class="pull-right" ng-if="enableFilter"> + + <a class="button button-text button-small hidden-xs hidden-sm ink" + ng-class="{'button-text-positive': !search.online, 'button-text-stable': search.online}" + ng-click="toggleOnline(!search.online)" > + <i class="icon ion-close-circled light-gray"></i> + <span>{{'PEER.OFFLINE'|translate}}</span> + </a> + + <!-- Allow extension here --> + <cs-extension-point name="filter-buttons"></cs-extension-point> + </div> + </div> + </div> + + <div id="helptip-network-peers" style="display: block"></div> + + <ng-include src="'plugins/es/templates/network/items_peers.html'"></ng-include> + </div> + + <div class="col col-33 " ng-controller="ESLastDocumentsCtrl"> + <div class="padding padding-xs" style="display: block;"> + <h4 translate>DOCUMENT.LOOKUP.LAST_DOCUMENTS_DOTS</h4> + + <div class="pull-right hidden-xs hidden-sm"> + <a class="button button-text button-small ink" + ng-class="{'button-text-positive': compactMode, 'button-text-stable': !compactMode}" + ng-click="toggleCompactMode()" > + <i class="icon ion-navicon"></i> + <b class="icon-secondary ion-arrow-down-b" style="top: -8px; left: 5px; font-size: 8px;"></b> + <b class="icon-secondary ion-arrow-up-b" style="top: 4px; left: 5px; font-size: 8px;"></b> + <span>{{'DOCUMENT.LOOKUP.BTN_COMPACT'|translate}}</span> + </a> + + <!-- Allow extension here --> + <cs-extension-point name="buttons"></cs-extension-point> + + <a class="button button-text button-small ink" + ui-sref="app.document_search({index: search.index, type: search.type})" > + <i class="icon ion-android-search"></i> + <span>{{'COMMON.BTN_SEARCH'|translate}}</span> + </a> + + </div> + </div> + + <ng-include src="'plugins/es/templates/document/list_documents.html'"></ng-include> + + </div> + </div> + </ion-content> +</ion-view> diff --git a/www/plugins/es/templates/network/view_peer.html b/www/plugins/es/templates/network/view_peer.html new file mode 100644 index 0000000000000000000000000000000000000000..8d102bf95d0c2b60ce0367313c6a3348bf9f192f --- /dev/null +++ b/www/plugins/es/templates/network/view_peer.html @@ -0,0 +1,137 @@ +<ion-view> + <ion-nav-title> + <span translate>PEER.VIEW.TITLE</span> + </ion-nav-title> + + <ion-content class="has-header" scroll="true"> + + <div class="row no-padding"> + <div class="col col-20 hidden-xs hidden-sm"> + </div> + + <div class="col list"> + + <ion-item> + <h1> + <span translate>PEER.VIEW.TITLE</span> + <span class="gray"> + {{node.host}} + </span> + </h1> + <h2 class="gray"> + <i class="gray icon ion-android-globe"></i> + {{node.ep.dns || node.server}} + <span class="gray" ng-if="!loading && node.useSsl"> + <i class="gray ion-locked"></i> <small>SSL</small> + </span> + <span class="gray" ng-if="!loading && node.useTor"> + <i class="gray ion-bma-tor-api"></i> + </span> + </h2> + + <!-- node owner --> + <h3> + <span class="dark"> + <i class="icon ion-android-desktop"></i> + {{'PEER.VIEW.OWNER'|translate}} + </span> + <a class="positive" + ng-if="node.name" + ui-sref="app.wot_identity({pubkey: node.pubkey, uid: node.name})"> + <i class="ion-person"></i> {{node.name}} + </a> + <span ng-if="!loading && !node.name"> + <a class="gray" + ui-sref="app.wot_identity({pubkey: node.pubkey})"> + <i class="ion-key"></i> + {{node.pubkey|formatPubkey}} + </a> + </span> + </h3> + + <h3> + <a ng-click="openRawPeering($event)"> + <i class="icon ion-share"></i> {{'PEER.VIEW.SHOW_RAW_PEERING'|translate}} + </a> + + <span class="gray" ng-if="!isReachable"> | </span> + <a ng-if="!isReachable" + ng-click="openRawCurrentBlock($event)"> + <i class="icon ion-share"></i> <span translate>PEER.VIEW.SHOW_RAW_CURRENT_BLOCK</span> + </a> + </h3> + </ion-item> + + + <div class="item item-divider" translate> + PEER.VIEW.GENERAL_DIVIDER + </div> + + <ion-item class="item-icon-left item-text-wrap ink" + copy-on-click="{{node.pubkey}}"> + <i class="icon ion-key"></i> + <span translate>COMMON.PUBKEY</span> + <h4 class="dark text-left">{{node.pubkey}}</h4> + </ion-item> + + <ion-item class="item item-icon-left item-text-wrap" + ng-if="isReachable"> + <i class="icon ion-document"></i> + <span translate>ES_PEER.DOCUMENT_COUNT</span> + <div class="badge badge-stable" ng-if="!loading"> + {{node.docCount|formatInteger}} + </div> + </ion-item> + + <!--<a class="item item-icon-left item-icon-right item-text-wrap ink"--> + <!--ng-if="isReachable"--> + <!--ui-sref="app.document_search(options.document)">--> + <!--<i class="icon ion-document" style="font-size: 25px;"></i>--> + <!--<i class="icon-secondary ion-clock" style="font-size: 18px; left: 33px; top: -12px;"></i>--> + <!--<span translate>DOCUMENT.LOOKUP.LAST_DOCUMENTS</span>--> + <!--<i class="gray icon ion-ios-arrow-right"></i>--> + <!--</a>--> + + <ion-item class="item item-icon-left item-text-wrap ink" + ng-if="isReachable"> + <i class="icon ion-cube"></i> + <span translate>BLOCKCHAIN.VIEW.TITLE_CURRENT</span> + <div class="badge badge-calm" ng-if="!loading"> + {{current.number|formatInteger}} + </div> + </ion-item> + + <!-- Allow extension here --> + <cs-extension-point name="general"></cs-extension-point> + + <div class="item item-divider" ng-hide="loading || !isReachable" translate> + PEER.VIEW.KNOWN_PEERS + </div> + + <ion-item class="item item-text-wrap no-border done in gray no-padding-top no-padding-bottom inline text-italic" + ng-show="!loading && !isReachable"> + <small><i class="icon ion-alert-circled"></i> {{'NETWORK.INFO.ONLY_SSL_PEERS'|translate}}</small> + </ion-item> + + <div class="item center" ng-if="loading"> + <ion-spinner class="icon" icon="android"></ion-spinner> + </div> + + <div class="list no-padding {{::motion.ionListClass}}" ng-if="isReachable"> + + <div ng-repeat="peer in :rebind:peers track by peer.id" + class="item item-peer item-icon-left ink" + ng-class="::ionItemClass" + ng-click="selectPeer(peer)" + ng-include="'plugins/es/templates/network/item_content_peer.html'"> + </div> + + </div> + </div> + + <div class="col col-20 hidden-xs hidden-sm"> + </div> + </div> + + </ion-content> +</ion-view> diff --git a/www/plugins/graph/js/controllers/docstats-controllers.js b/www/plugins/graph/js/controllers/docstats-controllers.js index 2052e4b3734686ee9a9ba608a283bf1da12afbcc..055e2e863ef056e1bbabd8172ec380fef4f81770 100644 --- a/www/plugins/graph/js/controllers/docstats-controllers.js +++ b/www/plugins/graph/js/controllers/docstats-controllers.js @@ -31,6 +31,8 @@ function GpDocStatsController($scope, $state, $controller, $q, $translate, gpCol // Initialize the super class and extend it. angular.extend(this, $controller('GpCurrencyAbstractCtrl', {$scope: $scope})); + $scope.formData.rangeDuration = 'month'; + $scope.displayRightAxis = true; $scope.hiddenDatasets = []; @@ -353,7 +355,7 @@ function GpDocStatsController($scope, $state, $controller, $q, $translate, gpCol if (!values) return undefined; var previousValue; return _.map(values, function(value) { - var newValue = (value !== undefined) && (value - (previousValue || value)) || undefined; + var newValue = (value !== undefined && previousValue !== undefined) ? (value - (previousValue || value)) : undefined; previousValue = value; return newValue; }); diff --git a/www/plugins/graph/js/controllers/network-controllers.js b/www/plugins/graph/js/controllers/network-controllers.js index 603a22a39e9b978671c5f063b9d6a0c8fe265209..5ba4e0086839c5a626edb2e03d4669ed69184a1d 100644 --- a/www/plugins/graph/js/controllers/network-controllers.js +++ b/www/plugins/graph/js/controllers/network-controllers.js @@ -25,6 +25,15 @@ angular.module('cesium.graph.network.controllers', ['chart.js', 'cesium.graph.se } } }) + + .extendState('app.es_network', { + points: { + 'buttons': { + templateUrl: "plugins/graph/templates/network/view_es_network_extend.html", + controller: 'GpNetworkViewExtendCtrl' + } + } + }) ; $stateProvider diff --git a/www/plugins/graph/templates/network/view_es_network_extend.html b/www/plugins/graph/templates/network/view_es_network_extend.html new file mode 100644 index 0000000000000000000000000000000000000000..e2e0544a736d242a35e6c042af2ba12256ee9855 --- /dev/null +++ b/www/plugins/graph/templates/network/view_es_network_extend.html @@ -0,0 +1,8 @@ +<!-- Buttons section --> +<ng-if ng-if=":state:enable && extensionPoint === 'buttons'"> + <a class="button button-text button-small ink" + ui-sref="app.doc_stats_lg" > + <i class="icon ion-stats-bars"></i> + <span>{{'NETWORK.VIEW.BTN_GRAPH'|translate}}</span> + </a> +</ng-if> diff --git a/www/plugins/graph/templates/network/view_network_extend.html b/www/plugins/graph/templates/network/view_network_extend.html index 60ecb297c5808645048990ec9c18ffd8124b388a..8b387b41d5c2998a05c96de70722e9c4a7651f06 100644 --- a/www/plugins/graph/templates/network/view_network_extend.html +++ b/www/plugins/graph/templates/network/view_network_extend.html @@ -1,5 +1,5 @@ <!-- Buttons section --> -<ng-if ng-if="enable && extensionPoint === 'buttons'"> +<ng-if ng-if=":state:enable && extensionPoint === 'buttons'"> <a class="button button-text button-small ink" ui-sref="app.blockchain_stats" > <i class="icon ion-stats-bars"></i> diff --git a/www/templates/menu.html b/www/templates/menu.html index 1be68ccd107e09c81c51697b10313bb3cb4eeccf..65e5e325ce4593ee31eed8cd23d8915879bb7f1e 100644 --- a/www/templates/menu.html +++ b/www/templates/menu.html @@ -82,8 +82,8 @@ <i class="icon light ion-android-exit"></i> </a> - <!-- Fullscreen button --> <!-- removeIf(device) --> + <!-- Fullscreen button --> <a ng-if="::$root.device.isWeb()" ng-click="toggleFullscreen()" class="button-icon" @@ -144,7 +144,7 @@ <i class="icon-secondary ion-card" style="top: 22px; left: 19px; font-size: 20px; background-color: white; width:17px; height: 14px;"></i> {{:locale:'MENU.WALLETS'|translate}} </a> - + <!-- MAIN Section --> <div class="item item-divider"></div> @@ -162,7 +162,7 @@ <!-- POWER USER Section --> <div class="item item-divider"></div> - + <a menu-close class="item item-icon-left" active-link="active" @@ -185,8 +185,8 @@ <!-- Allow extension here --> <cs-extension-point name="menu-discover"></cs-extension-point> - - + + <div class="item item-divider visible-xs visible-sm"></div> diff --git a/yarn.lock b/yarn.lock index 3f409800c5b444c28b6f70caeba6a2e353db209f..c122feb9b6bf11dbbda94f4547c31def6237731e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,10 @@ version "0.1.2" resolved "https://codeload.github.com/VivekKhandre/Leaflet.FeatureGroup.SubGroup/tar.gz/2ec699f11e1b6a8fa2596a1bb2b7a144d162c6d6" +"@bower_components/Leaflet.awesome-markers@lvoogdt/Leaflet.awesome-markers#2.0.2": + version "0.0.0" + resolved "https://codeload.github.com/lvoogdt/Leaflet.awesome-markers/tar.gz/bfac1eb6f7896072d690bde57c1fb5002961a99a" + "@bower_components/aes-js@ricmoo/aes-js#3.1.2": version "3.1.2" resolved "https://codeload.github.com/ricmoo/aes-js/tar.gz/7c9fad4add4b349dcb89a4e2125f37defaef3bc8" @@ -151,10 +155,6 @@ dependencies: leaflet "*" -"@bower_components/leaflet.awesome-markers@lvoogdt/Leaflet.awesome-markers#2.0.2": - version "0.0.0" - resolved "https://codeload.github.com/lvoogdt/Leaflet.awesome-markers/tar.gz/bfac1eb6f7896072d690bde57c1fb5002961a99a" - "@bower_components/leaflet.loading@ebrelsford/Leaflet.loading#^0.1.24": version "0.1.24" resolved "https://codeload.github.com/ebrelsford/Leaflet.loading/tar.gz/a1329ac02d709e9b63416b5cb09cea861a37fb4a"