Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • cordeliaze/cesium
  • pfouque06/cesium
  • wellno1/cesium
  • 1000i100/cesium
  • vincentux/cesium
  • calbasi/cesium
  • thomasbromehead/cesium
  • matograine/cesium
  • clients/cesium-grp/cesium
  • cedricmenec/cesium
  • Pamplemousse/cesium
  • etienneleba/cesium
  • tnntwister/cesium
  • scanlegentil/cesium
  • morvanc/cesium
  • yyy/cesium
  • Axce/cesium
  • Bertrandbenj/cesium
  • Lupus/cesium
  • elmau/cesium
  • MartinDelille/cesium
  • tykayn/cesium
  • numeropi/cesium
  • Vivakvo/cesium
  • pokapow/cesium
  • pini-gh/cesium
  • anam/cesium
  • RavanH/cesium
  • bpresles/cesium
  • am97/cesium
  • tuxmain/cesium
  • jytou/cesium
  • oliviermaurice/cesium
  • 666titi999/cesium
  • Yvv/cesium
35 results
Select Git revision
Show changes
Showing
with 3552 additions and 1515 deletions
......@@ -77,6 +77,7 @@ angular.module('cesium.currency.controllers', ['ngFileSaver', 'cesium.services']
;
function CurrencyViewController($scope, $q, $timeout, $ionicPopover, Modals, BMA, UIUtils, csSettings, csCurrency, csNetwork, ModalUtils) {
'ngInject';
$scope.formData = {
useRelative: false, // Override in enter()
......@@ -172,8 +173,8 @@ function CurrencyViewController($scope, $q, $timeout, $ionicPopover, Modals, BMA
}
}),
// Get the current block informations
BMA.blockchain.current()
// Get the current block information
csCurrency.blockchain.current()
.then(function(block){
M = block.monetaryMass;
data.N = block.membersCount;
......@@ -234,7 +235,7 @@ function CurrencyViewController($scope, $q, $timeout, $ionicPopover, Modals, BMA
console.debug("[currency] Parameters loaded in " + (Date.now() - now) + 'ms' );
$scope.loading = false;
$scope.$broadcast('$$rebind::' + 'rebind'); // force bind of currency name
$scope.$broadcast('$$rebind::rebind'); // force bind of currency name
// Set Ink
UIUtils.ink();
......
angular.module('cesium.feed.controllers', ['cesium.services'])
.controller('FeedCtrl', FeedController)
;
function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHttp, csCache, csSettings) {
'ngInject';
$scope.search = {
loading: true,
maxCount: (csConfig.feed && csConfig.feed.maxCount) || 3, // 3 month by default
maxContentLength: (csConfig.feed && csConfig.feed.maxContentLength) || 1300,
maxAgeInMonths: (csConfig.feed && csConfig.feed.maxAgeInMonths) || 3, // 3 month by default
minTime: undefined, // unix time
loadCount: 0,
maxLoadCount: 2 // = 1 redirection max
};
$scope.enter = function(e, state) {
// Wait platform to be ready
csSettings.ready()
.then(function() {
return $scope.load();
})
.catch(function() {
// Continue
return null;
})
.then(function(feed) {
$scope.loading = false;
$scope.feed = feed;
var showFeed = feed && feed.items && feed.items.length > 0 || false;
$scope.$parent.toggleFeed(showFeed);
});
};
$scope.$on('$ionicParentView.enter', $scope.enter);
$scope.load = function() {
var feedUrl = csSettings.getFeedUrl();
if (!feedUrl || typeof feedUrl !== 'string') return; // Skip
// Min unix time, to exclude old topics
var maxAgeInMonths = $scope.search.maxAgeInMonths;
var minDate = maxAgeInMonths > 0 ? moment().subtract(maxAgeInMonths, 'month').startOf('day').utc() : undefined;
$scope.search.minTime = minDate && minDate.unix();
// Reset load counter
$scope.search.loadCount = 0;
var now = Date.now();
console.debug("[feed] Loading from {url: {0}, minTime: '{1}'}".format(feedUrl, minDate && minDate.toISOString() || 'all'));
return $scope.loadJsonFeed(feedUrl)
.then(function (feed) {
console.debug('[feed] {0} items loaded in {0}ms'.format(feed && feed.items && feed.items.length || 0, Date.now() - now));
if (!feed || !feed.items) return null; // skip
// Clean title (remove duplicated titles)
$scope.cleanDuplicatedTitles(feed);
return feed;
})
.catch(function(err) {
console.error('[feed] Failed to load.', err);
return null;
});
};
/**
* Load a JSON file, from the given URL, and convert into the JSON Feed format
* @param feedUrl
* @param maxCount
* @returns {*}
*/
$scope.loadJsonFeed = function(feedUrl, maxItemCount) {
var locale = $translate.use();
var minTime = $scope.search.minTime;
maxItemCount = maxItemCount || $scope.search.maxCount;
// Increment load counter (to avoid infinite loop)
$scope.search.loadCount++;
return $scope.getJson(feedUrl)
.then(function(json) {
// Parse JSON from discourse
if ($scope.isJsonDiscourse(json)) {
return $scope.parseJsonDiscourse(feedUrl, json);
}
// Return a copy (to avoid any change in cached data)
return angular.copy(json);
})
.then(function(feed) {
if (!feed || !feed.items && !feed.next_url) return null; // skip
// SKip if incompatible language
if (feed.language && !$scope.isCompatibleLanguage(locale, feed.language)) {
console.debug("[feed] Skip feed item '{0}' - Expected language: '{1}', actual: '{2}'".format(feed.title, locale, feed.language));
return null;
}
// Migrate from old version 1.0 to 1.1
if (feed.version === 'https://jsonfeed.org/version/1') {
if (feed.author && !feed.authors) {
feed.authors = [feed.author];
delete feed.author;
}
(feed.items || []).forEach(function (item) {
if (item.author && !item.authors) {
item.authors = [item.author];
delete item.author;
}
});
feed.version = 'https://jsonfeed.org/version/1.1';
}
feed.items = (feed.items || []).reduce(function (res, item) {
// Skip if empty (missing title and content)
if ($scope.isEmptyFeedItem(item)) return res;
item = $scope.prepareJsonFeedItem(item, feed);
// Skip if too old items
if (minTime > 0 && item.creationTime && (item.creationTime < minTime)) return res;
// Skip if not same language
if (item.language && !$scope.isCompatibleLanguage(locale, item.language)) {
console.debug("[feed] Feed item '{0}' EXCLUDED - expected locale: {1}, actual language: {2}".format(item.title || feed.title, locale, item.language));
return res;
}
return res.concat(item);
}, []);
return feed;
})
.then(function(feed) {
if (!feed) return null; // skip
feed.items = feed.items || [];
// Slice to keep last (more recent) items
if (feed.items.length > maxItemCount) {
feed.items = feed.items.slice(feed.items.length - maxItemCount);
return feed;
}
// Not enough items: try to fetch more
var canFetchMore = feed.next_url && feed.next_url !== feedUrl && $scope.search.loadCount < $scope.search.maxLoadCount;
if (canFetchMore && feed.items.length < maxItemCount) {
console.debug("[feed] Loading from {next_url: '{0}'}".format(feed.next_url));
// Fetch more
return $scope.loadJsonFeed(feed.next_url, maxItemCount - feed.items.length)
.then(function(moreFeed) {
// Append new items
if (moreFeed && moreFeed.items && moreFeed.items.length) {
feed.items = feed.items.concat(moreFeed.items.slice(0, maxItemCount - feed.items.length));
}
return feed;
});
}
return feed;
});
};
/**
* Fetch a JSON file, from a URL. Use a cache (of 1 hour) to avoid to many network request
* @param url
* @returns {*}
*/
$scope.getJson = function(url) {
return $q(function(resolve, reject) {
$http.get(url, {
timeout: csConfig.timeout,
responseType: 'json',
cache: csCache.get('csFeed-', csCache.constants.LONG)
})
.success(resolve)
.error(reject);
});
};
$scope.isEmptyFeedItem = function(item) {
return (!item || (!item.title && !item.content_text && !item.content_html));
};
/**
* Prepare a feed for the template :
* - set 'time' with a unix timestamp
* - set 'content' with the HTML or text content (truncated if too long)
* - fill authors if not exists, using feed authors
* @param item
* @param feed
* @returns {{content}|*}
*/
$scope.prepareJsonFeedItem = function(item, feed) {
if ($scope.isEmptyFeedItem(item)) throw Error('Empty feed item');
var maxContentLength = $scope.search.maxContentLength;
// Convert UTC time
if (item.date_published) {
item.creationTime = moment.utc(item.date_published).unix();
}
if (item.date_modified) {
item.time = moment.utc(item.date_modified).unix();
}
// Convert content to HTML
if (item.content_html) {
item.content = item.content_html;
}
else {
item.content = (item.content_text||'').replace(/\n/g, '<br/>');
}
// Trunc content, if need
if (maxContentLength > 0 && item.content && item.content.length > maxContentLength) {
var endIndex = Math.max(item.content.lastIndexOf(" ", maxContentLength), item.content.lastIndexOf("<", maxContentLength));
item.content = item.content.substring(0, endIndex) + ' (...)';
item.truncated = true;
}
// If author is missing, copy the main author
item.authors = item.authors || feed.authors;
return item;
};
/**
* Prepare feed (e.g. clean duplicated title, when feed URL is a discourse topic, all item will have the same title)
* @param feed
* @returns {*}
*/
$scope.cleanDuplicatedTitles = function(feed) {
if (!feed || !feed.items) return feed;
_.forEach(feed.items, function(item, index) {
if (item.title && index > 0 && (item.title === feed.items[0].title)) {
delete item.title;
}
});
return feed;
};
/**
* Detect this the given JSON is from Discourse
* @param json
* @returns {*}
*/
$scope.isJsonDiscourse = function(json) {
return $scope.isDiscourseCategory(json) || $scope.isDiscourseTopic(json);
};
/**
* Transform a JSON from Discourse into JSON feed
* @param feedUrl
* @param json
* @returns {*}
*/
$scope.parseJsonDiscourse = function(feedUrl, json) {
// Detect if category category
if ($scope.isDiscourseCategory(json)) {
// Convert category to feed
return $scope.parseDiscourseCategory(feedUrl, json);
}
// Convert topic to feed
return $scope.parseDiscourseTopic(feedUrl, json);
};
$scope.isDiscourseCategory = function(category) {
return category && category.topic_list && Array.isArray(category.topic_list.topics) &&
!!category.topic_list.more_topics_url || false;
};
$scope.parseDiscourseCategory = function(url, category, locale) {
// Check is a discourse category
if (!$scope.isDiscourseCategory(category)) throw new Error('Not a discourse category');
locale = locale || $translate.use();
var uri = csHttp.uri.parse(url);
var baseUrl = uri.protocol + '//' + uri.host + (uri.port != 443 && uri.port != 80 ? uri.port : '');
var pageUrl = baseUrl + category.topic_list.more_topics_url.replace(/\?page=[0-9]+/, '');
var feed = {
version: "https://jsonfeed.org/version/1.1", // fixed value
home_page_url: pageUrl,
feed_url: url,
title: 'HOME.FEEDS_TITLE'
};
return $q.all(
category.topic_list.topics.reduce(function(res, topic) {
if (!topic.pinned || !topic.visible) return res; // Skip not pinned topic
// Exclude category description (=tag 'about-category')
if (topic.tags && topic.tags.includes('about-category')) return res;
// Detect language, from the title. Then skip if not compatible with expected locale
var topicLanguage = $scope.getLanguageFromTitle(topic.title);
if (!$scope.isCompatibleLanguage(locale, topicLanguage)) return res;
// Compute the URL to load the topic
var topicUrl = [baseUrl, 't', topic.slug, topic.id].join('/') + '.json';
// Load topic JSON
return res.concat($scope.getJson(topicUrl)
.catch(function(err) {
console.error("[feed] Failed to load discourse topic from '{}'".format(topicUrl), err);
return null; // continue
})
);
}, []))
.then(function(topics) {
feed.items = topics.reduce(function(res, topic) {
if (!$scope.isDiscourseTopic(topic)) return res; // Not a topic: skip
var feedTopic = $scope.parseDiscourseTopic(baseUrl, topic, feed);
if (!feedTopic.items || !feedTopic.items.length) return res; // Topic is empty: skip
return res.concat(feedTopic.items[0]);
}, []);
return feed;
});
};
$scope.isDiscourseTopic = function(topic) {
return topic && topic.title && topic.post_stream && Array.isArray(topic.post_stream.posts);
};
$scope.parseDiscourseTopic = function(url, topic, feed) {
// Make sure this is a valid topic
if (!$scope.isDiscourseTopic(topic)) throw new Error('Not a discourse topic');
var uri = csHttp.uri.parse(url);
var baseUrl = uri.protocol + '//' + uri.host + (uri.port != 443 && uri.port != 80 ? uri.port : '');
// Clean title (e.g. remove '(fr)' or '(en)' or '(es)')
// Prefer unicode title (e.g. emoji are replaced)
var title = $scope.cleanTitle(topic.unicode_title || topic.title);
var language = $scope.getLanguageFromTitle(topic.title);
// Prepare root feed, if not yet exists
feed = feed || {
version: "https://jsonfeed.org/version/1.1", // fixed value
home_page_url: [baseUrl, 't', topic.slug, topic.id].join('/'),
feed_url: url,
title: 'HOME.FEEDS_TITLE',
language: language
};
feed.language = feed.language || language;
feed.items = topic.post_stream.posts.reduce(function(res, post) {
if (!post.cooked || post.cooked.trim() === '') return res; // Skip if empty
// SKip if hidden, or deleted post
if (post.hidden || post.deleted_at) return res;
var author = {
name: post.display_username,
url: [baseUrl, 'u', post.username].join('/'),
avatar: post.avatar_template ? (baseUrl + post.avatar_template.replace('{size}', '60')) : undefined
};
// Try to resolve author pubkey, to replace author url by a link to wot identity
var developer = _.find(csConfig.developers || [], function(developer) {
return developer.name && (
(post.display_username && developer.name.toLowerCase() === post.display_username.toLowerCase()) ||
(post.username && developer.name.toLowerCase() === post.username.toLowerCase())
);
});
if (developer && developer.pubkey) {
author.url = '@' + developer.pubkey;
}
// Fill parent feed defaults
feed.authors = feed.authors || [author];
return res.concat({
id: post.id,
url: [baseUrl, 't', post.topic_slug, post.topic_id, post.post_number].join('/'),
title: title,
date_published: post.created_at,
date_modified: post.updated_at,
content_html: post.cooked,
authors: [author],
language: language,
tags: post.tags || topic.tags
});
}, []);
return feed;
};
/**
* Clean a title : remove locale string at the end (e.g. '(fr)' or '(en)' or '(es)')
* @param title
*/
$scope.cleanTitle = function(title) {
if (!title) return undefined;
return title.replace(/\s\([a-z]{2}(:?-[A-Z]{2})?\)$/, '');
};
/**
* Clean a title : remove locale string at the end (e.g. '(fr)' or '(en)' or '(es)')
* @param title
*/
$scope.getLanguageFromTitle = function(title) {
if (!title) return undefined;
var matches = /\s\(([a-z]{2}(:?-[A-Z]{2})?)\)$/.exec(title);
return matches && matches[1];
};
$scope.isCompatibleLanguage = function(expectedLocale, language) {
if (!expectedLocale || !language || expectedLocale === language) return true;
// Extract the language from the locale, then compare
// E.g. 'fr-FR' => 'fr'
var expectedLanguage = expectedLocale.split('-', 2)[0];
return expectedLanguage.toLowerCase() === language.toLowerCase();
};
csSettings.api.locale.on.changed($scope, function() {
if ($scope.loading) return;
console.debug("[feed] Locale changed. Reload feed...");
$scope.enter();
});
}
......@@ -130,7 +130,7 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
return $scope.executeStep(partName, steps, index+1);
})
.catch(function(err) {
if (err && err.message == 'transition prevented') {
if (err && err.message === 'transition prevented') {
console.error('ERROR: in help tour [{0}], in step [{1}] -> use large if exists, to prevent [transition prevented] error'.format(partName, index));
}
else {
......@@ -161,117 +161,143 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
$scope.tour = true;
$scope.continue = true;
// Currency
return $scope.startCurrencyTour(0, true)
.then(function(endIndex){
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.currency=endIndex;
csSettings.store();
return $scope.continue;
})
console.debug("[help] Starting help tour... {demo: {0}, readonly: {1}, isLogin: {2}}".format(
csConfig.demo, csConfig.readonly, csWallet.isLogin()));
// Network
// Wallet (if NOT readonly and NOT login)
return ((!csConfig.readonly && csWallet.isLogin()) ? $scope.startWalletNoLoginTour(0, true) : $q.when(true))
// Wallet (if login)
.then(function(next){
if (!next) return false;
return $scope.startNetworkTour(0, true)
if (csConfig.readonly || !csWallet.isLogin()) return true; // not login or readonly: continue
return $scope.startWalletTour(0, true)
.then(function(endIndex){
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.network=endIndex;
if (!endIndex) return false;
csSettings.data.helptip.wallet=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Wot lookup
// Wallet certifications
.then(function(next){
if (!next) return false;
return $scope.startWotLookupTour(0, true)
if (csConfig.readonly || !csWallet.isLogin()) return true; // not login or readonly: continue
return $scope.startWalletCertTour(0, true)
.then(function(endIndex){
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.wotLookup=endIndex;
if (!endIndex) return false;
csSettings.data.helptip.walletCerts=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Wot identity
// My operations (if login)
.then(function(next){
if (!next) return false;
return $scope.startWotTour(0, true)
if (csConfig.readonly || !csWallet.isLogin()) return true; // not login or readonly: continue
return $scope.startTxTour(0, true)
.then(function(endIndex){
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.wot=endIndex;
if (!endIndex) return false;
csSettings.data.helptip.tx=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Identity certifications
// My wallets (if login)
.then(function(next){
if (!next) return false;
return $scope.startWotCertTour(0, true)
if (csConfig.readonly || !csWallet.isLogin()) return true; // not login or readonly: continue
return $scope.startWalletsTour(0, true)
.then(function(endIndex){
if (!endIndex) return false;
csSettings.data.helptip.wotCerts=endIndex;
csSettings.data.helptip.wallets=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Wallet (if NOT login)
// Header tour
.then(function(next){
if (!next) return false;
return $scope.startWalletNoLoginTour(0, true);
if (csConfig.readonly) return true; // readonly: continue
return $scope.startHeaderTour(0, true);
})
// Wallet (if login)
// Settings tour (if not readonly mode)
.then(function(next){
if (!next) return false;
if (!csWallet.isLogin()) return true; // not login: continue
return $scope.startWalletTour(0, true)
if (csConfig.readonly) return true; // Skip if readonly mode (will be play later)
return $scope.startSettingsTour(0, true);
})
// Wot lookup tour
.then(function(next){
if (!next) return false;
return $scope.startWotLookupTour(0, true)
.then(function(endIndex){
if (!endIndex) return false;
csSettings.data.helptip.wallet=endIndex;
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.wotLookup=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Wallet certifications
// Wot identity
.then(function(next){
if (!next) return false;
if (!csWallet.isLogin()) return true; // not login: continue
return $scope.startWalletCertTour(0, true)
return $scope.startWotTour(0, true)
.then(function(endIndex){
if (!endIndex) return false;
csSettings.data.helptip.walletCerts=endIndex;
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.wot=endIndex;
csSettings.store();
return $scope.continue;
});
})
// My operations (if login)
// Identity certifications
.then(function(next){
if (!next) return false;
if (!csWallet.isLogin()) return true; // not login: continue
return $scope.startTxTour(0, true)
return $scope.startWotCertTour(0, true)
.then(function(endIndex){
if (!endIndex) return false;
csSettings.data.helptip.tx=endIndex;
csSettings.data.helptip.wotCerts=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Header tour
// Currency tour
.then(function(next){
if (!next) return false;
return $scope.startHeaderTour(0, true);
return $scope.startCurrencyTour(0, true)
.then(function(endIndex){
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.currency=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Settings tour
// Network tour
.then(function(next){
if (!next) return false;
return $scope.startNetworkTour(0, true)
.then(function(endIndex){
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.network=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Settings tour (if readonly mode)
.then(function(next){
if (!next) return false;
if (!csConfig.readonly) return true; // Skip if NOT readonly
return $scope.startSettingsTour(0, true);
})
......@@ -304,7 +330,7 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
bindings: {
content: 'HELP.TIP.MENU_BTN_CURRENCY',
icon: {
position: 'left'
position: UIUtils.screen.isSmall() || csConfig.readonly ? 'left' : 'bottom-left'
}
}
});
......@@ -392,7 +418,8 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
},
hasNext: hasNext
},
timeout: 1200 // need for Firefox
timeout: 1200, // need for Firefox
retry: 2
});
}
];
......@@ -416,7 +443,7 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
// Select the second tabs
$timeout(function () {
var tabs = $window.document.querySelectorAll('ion-tabs .tabs a');
if (tabs && tabs.length == 3) {
if (tabs && tabs.length === 3) {
angular.element(tabs[2]).triggerHandler('click');
}
}, 100);
......@@ -434,7 +461,7 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
bindings: {
content: 'HELP.TIP.MENU_BTN_NETWORK',
icon: {
position: 'left'
position: UIUtils.screen.isSmall() || csConfig.readonly ? 'left' : 'bottom-left'
}
}
});
......@@ -529,7 +556,7 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
bindings: {
content: 'HELP.TIP.MENU_BTN_WOT',
icon: {
position: 'left'
position: UIUtils.screen.isSmall() || csConfig.readonly ? 'left' : 'bottom-left'
}
},
onError: 'continue'
......@@ -634,6 +661,8 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
* @returns {*}
*/
$scope.startWotCertTour = function(startIndex, hasNext) {
if (csConfig.readonly) return $q.when(true);
var steps = [
function() {
......@@ -693,6 +722,32 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
hasNext: hasNext
}
});
},
function () {
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-tx', {
bindings: {
content: 'HELP.TIP.MENU_BTN_TX',
icon: {
position: 'left'
},
hasNext: hasNext
}
});
},
function () {
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-wallets', {
bindings: {
content: 'HELP.TIP.MENU_BTN_WALLETS',
icon: {
position: 'left'
},
hasNext: hasNext
}
});
}
];
......@@ -736,7 +791,9 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
icon: {
position: UIUtils.screen.isSmall() ? 'right' : 'center'
}
}
},
timeout: UIUtils.screen.isSmall() ? 2000 : 1000,
retry: 10
});
});
},
......@@ -912,7 +969,7 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-tx', {
bindings: {
content: csWallet.data.isMember ? 'HELP.TIP.MENU_BTN_TX_MEMBER' : 'HELP.TIP.MENU_BTN_TX',
content: 'HELP.TIP.MENU_BTN_TX',
icon: {
position: 'left'
}
......@@ -965,12 +1022,36 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
});
};
/**
* Features tour on My wallets
* @returns {*}
*/
$scope.startWalletsTour = function(startIndex, hasNext) {
var steps = [
function () {
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-wallets', {
bindings: {
content: 'HELP.TIP.MENU_BTN_WALLETS',
icon: {
position: 'left'
},
hasNext: hasNext
}
});
}
];
return $scope.executeStep('my-wallets', steps, startIndex);
};
/**
* header tour
* @returns {*}
*/
$scope.startHeaderTour = function(startIndex, hasNext) {
if (UIUtils.screen.isSmall()) return $q.when(true);
if (UIUtils.screen.isSmall() || csConfig.readonly) return $q.when(true);
function _getProfilBtnElement() {
var elements = $window.document.querySelectorAll('#helptip-header-bar-btn-profile');
......@@ -984,11 +1065,14 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
if (UIUtils.screen.isSmall()) return true; // skip for small screen
var element = _getProfilBtnElement();
if (!element) return true;
return $scope.showHelpTip(element, {
bindings: {
content: 'HELP.TIP.HEADER_BAR_BTN_PROFILE',
icon: {
position: 'right'
position: 'right',
// If home; add offset because of locales button
style: $state.is('app.home') ? 'margin-right: 60px' : undefined
}
}
});
......@@ -1047,13 +1131,13 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
var steps = [
function () {
if (!UIUtils.screen.isSmall()) return true;
if (!UIUtils.screen.isSmall() && !csConfig.readonly) return true;
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-settings', {
return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'helptip-menu-btn-settings' : 'menu-btn-settings', {
bindings: {
content: 'HELP.TIP.MENU_BTN_SETTINGS',
icon: {
position: 'left'
position: UIUtils.screen.isSmall() ? 'left' : 'bottom-left'
}
},
timeout: 1000
......@@ -1072,10 +1156,10 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
bindings: {
content: 'HELP.TIP.SETTINGS_CHANGE_UNIT',
contentParams: contentParams,
icon: {
icon: UIUtils.screen.isSmall() ? {
position: 'right',
style: 'margin-right: 60px'
},
} : {position: 'center'},
hasNext: hasNext
},
timeout: 1000
......@@ -1105,11 +1189,12 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
if (csWallet.isLogin()) {
return $state.go('app.view_wallet')
.then(function(){
return $scope.showHelpTip('helptip-wallet-certifications', {
return $scope.showHelpTip('helptip-wallet-pubkey', {
bindings: {
content: 'HELP.TIP.END_LOGIN',
hasNext: false
}
},
timeout: 1200
});
});
}
......@@ -1128,7 +1213,7 @@ function HelpTipController($scope, $state, $window, $ionicSideMenuDelegate, $tim
.then(function(){
return $scope.showHelpTip('helptip-home-logo', {
bindings: {
content: 'HELP.TIP.END_NOT_LOGIN',
content: !csConfig.readonly ? 'HELP.TIP.END_NOT_LOGIN' : 'HELP.TIP.END_READONLY',
contentParams: contentParams,
hasNext: false
}
......
angular.module('cesium.home.controllers', ['cesium.platform', 'cesium.services'])
.config(function($stateProvider, $urlRouterProvider) {
'ngInject';
$stateProvider
.state('app.home', {
url: "/home?error&uri&login",
views: {
'menuContent': {
templateUrl: "templates/home/home.html",
controller: 'HomeCtrl'
}
}
});
// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/app/home');
})
.controller('HomeCtrl', HomeController)
;
function HomeController($scope, $state, $timeout, $interval, $ionicHistory, $translate, $http, $q, $location,
UIUtils, BMA, Device, csConfig, csHttp, csCache, csPlatform, csNetwork, csCurrency, csSettings, csWallet) {
'ngInject';
$scope.loading = true;
$scope.loadingPct = 0;
$scope.loadingMessage = '';
$scope.locales = angular.copy(csSettings.locales);
$scope.smallscreen = UIUtils.screen.isSmall();
$scope.showInstallHelp = false;
$scope.showFeed = false;
$scope.enter = function(e, state) {
if (ionic.Platform.isIOS() && window.StatusBar) {
// needed to fix Xcode 9 / iOS 11 issue with blank space at bottom of webview
// https://github.com/meteor/meteor/issues/9041
StatusBar.overlaysWebView(false);
StatusBar.overlaysWebView(true);
}
if (state && state.stateParams && state.stateParams.uri) {
return $scope.handleUri(state.stateParams.uri)
.then(function() {
$scope.loading = false;
});
}
else if (state && state.stateParams && state.stateParams.error) { // Error query parameter
$scope.error = state.stateParams.error;
$scope.node = csCurrency.data.node;
$scope.loading = false;
$scope.cleanLocationHref(state);
return $q.when();
}
else {
// Loading progress percent
var startTime = Date.now();
var interval = $interval(function(){
var duration = Date.now() - startTime;
var timeout = Math.max(csNetwork.data.timeout, duration);
// Waiting to start
if (!$scope.loadingMessage) {
$scope.loadingPct = Math.min($scope.loadingPct + 0.5, 98);
}
else if (duration < timeout) {
var loadingPct = Math.min(duration / timeout * 100, 98);
if (loadingPct > $scope.loadingPct) {
$scope.loadingPct = loadingPct;
}
}
$scope.$broadcast('$$rebind::loading'); // force rebind loading
}, 200);
var hasLoginParam = state && state.stateParams && state.stateParams.login || false;
// Wait platform to be ready
return csPlatform.ready()
.catch(function(err) {
$scope.node = csCurrency.data.node;
$scope.error = err;
})
.then(function() {
// Stop progression
$interval.cancel(interval);
// Mark as loaded
$scope.loading = false;
$scope.loadingMessage = '';
$scope.loadingPct = 100;
$scope.$broadcast('$$rebind::loading'); // force rebind loading
$scope.$broadcast('$$rebind::feed'); // force rebind feed
// Open the login modal
if (hasLoginParam && !csWallet.isLogin() && !$scope.error) {
$scope.cleanLocationHref(state);
return csWallet.login();
}
});
}
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.reload = function() {
$scope.loading = true;
$scope.loadingPct = 0;
$scope.loadingMessage = '';
delete $scope.error;
$timeout($scope.enter, 200);
};
/**
* Catch click for quick fix
* @param action
*/
$scope.doQuickFix = function(action) {
if (action === 'settings') {
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go('app.settings');
}
};
$scope.changeLanguage = function(langKey) {
$translate.use(langKey);
$scope.hideLocalesPopover();
csSettings.data.locale = _.findWhere($scope.locales, {id: langKey});
csSettings.store();
};
$scope.toggleFeed = function(show) {
$scope.showFeed = (show !== undefined) ? show : !$scope.showFeed;
if (!this.loading) {
$scope.$broadcast('$$rebind::feed'); // force rebind feed
}
};
/* -- show/hide locales popup -- */
$scope.showLocalesPopover = function(event) {
UIUtils.popover.show(event, {
templateUrl: 'templates/common/popover_locales.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.localesPopover = popover;
}
});
};
$scope.hideLocalesPopover = function() {
if ($scope.localesPopover) {
$scope.localesPopover.hide();
$scope.localesPopover = null;
}
};
// remove '?uri&error' from the location URI, and inside history
$scope.cleanLocationHref = function(state) {
state = state || {stateName: 'app.home'};
state.stateParams = state.stateParams || {};
if (state && state.stateParams) {
var stateParams = angular.copy(state.stateParams);
delete stateParams.uri;
delete stateParams.error;
delete stateParams.login;
$location.search(stateParams).replace();
// Update location href
$ionicHistory.nextViewOptions({
disableAnimate: true,
disableBack: false,
historyRoot: false
});
return $state.go(state.stateName, stateParams, {
reload: false,
inherit: false,
notify: false
});
}
};
// Listen platform messages
csPlatform.api.start.on.message($scope, function(message) {
$scope.loadingMessage = message;
});
// Listen network offline/online
Device.api.network.on.offline($scope, function() {
csPlatform.stop();
$scope.loadingMessage = '';
$scope.loading = false;
$scope.node = csCurrency.data.node;
$scope.error = true;
});
Device.api.network.on.online($scope, function() {
if (!$scope.loading && $scope.error) {
delete $scope.error;
$scope.reload();
}
});
// For DEV ONLY
/*$timeout(function() {
$scope.loginAndGo();
}, 500);*/
}
......@@ -40,7 +40,7 @@ function JoinController($scope, $timeout, $controller, Modals, csWallet) {
}
function JoinChooseAccountTypeModalController($scope, $state, Modals, UIUtils, csCurrency) {
function JoinChooseAccountTypeModalController($scope, $state, Modals, UIUtils, csConfig, csCurrency) {
'ngInject';
$scope.formData = {};
......@@ -86,6 +86,9 @@ function JoinChooseAccountTypeModalController($scope, $state, Modals, UIUtils, c
};
$scope.selectAccountTypeAndClose = function(type) {
if (csConfig.demo) {
return UIUtils.alert.demo();
}
$scope.formData.accountType = type;
$scope.closeModal($scope.formData);
};
......@@ -113,11 +116,12 @@ function JoinChooseAccountTypeModalController($scope, $state, Modals, UIUtils, c
}
function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtils, CryptoUtils, csSettings, Modals, csWallet, BMA, parameters) {
function JoinModalController($scope, $state, $interval, $q, $timeout, Device, UIUtils, CryptoUtils, csSettings, Modals, csWallet, BMA, parameters) {
'ngInject';
$scope.formData = {
pseudo: ''
pseudo: parameters.uid || '',
pubkey: parameters.pubkey || undefined
};
$scope.slides = {
slider: null,
......@@ -138,15 +142,26 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
$scope.formData.computing=false;
$scope.smallscreen = UIUtils.screen.isSmall();
$scope.userIdPattern = BMA.constants.regexp.USER_ID;
$scope.accountAvailable = !!parameters.pubkey;
// Read input parameters
$scope.currency = parameters.currency;
$scope.accountType = parameters.accountType || 'member';
var wallet;
$scope.load = function() {
if ($scope.loading) {
if ($scope.accountType == 'member') {
// Get the wallet
wallet = (parameters.walletId && csWallet.children.get(parameters.walletId)) ||
(parameters.pubkey && csWallet.children.getByPubkey(parameters.pubkey)) ||
((!parameters.pubkey || csWallet.isUserPubkey(parameters.pubkey)) && csWallet);
if (!wallet) throw new Error("Cannot found the corresponding wallet, from parameters.pubkey or parameters.walletId");
console.debug("[join] Starting join modal on wallet {0}".format(wallet.id));
if ($scope.accountType === 'member') {
$scope.licenseFileUrl = csSettings.getLicenseUrl();
if ($scope.licenseFileUrl) {
// Use HTML in iframe, when original file is markdown (fix #538)
......@@ -190,6 +205,12 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
};
$scope.showAccountPubkey = function() {
if (parameters.pubkey && parameters.pseudo === $scope.formData.pseudo) {
$scope.formData.pubkey = parameters.pubkey;
$scope.formData.computing = false;
return;
}
$scope.formData.computing=true;
CryptoUtils.scryptKeypair($scope.formData.username, $scope.formData.password)
......@@ -218,6 +239,7 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
var index = $scope.slides.slider.activeIndex;
if($scope.accountType === 'member') {
index += ($scope.licenseFileUrl ? 0 : 1); // skip index 0, when no license file
index += (parameters.pubkey && index >= 2 ? 2 : 0); // skip salt+pass, if already a pubkey
if (index === 0) return "licenseForm";
if (index === 1) return "pseudoForm";
if (index === 2) return "saltForm";
......@@ -235,14 +257,14 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
var formName = $scope.getCurrentFormName();
var behavior;
if (formName == "licenseForm") {
if (formName === "licenseForm") {
behavior = {
hasPreviousButton: false,
hasNextButton: false,
hasAcceptButton: true
};
}
else if (formName == "pseudoForm") {
else if (formName === "pseudoForm") {
behavior = {
helpAnchor: 'join-pseudo',
hasPreviousButton: $scope.licenseFileUrl && true,
......@@ -250,7 +272,7 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
focus: 'pseudo'
};
}
else if (formName == "saltForm") {
else if (formName === "saltForm") {
behavior = {
helpAnchor: 'join-salt',
hasPreviousButton: $scope.accountType === 'member',
......@@ -258,7 +280,7 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
focus: 'salt'
};
}
else if (formName == "passwordForm") {
else if (formName === "passwordForm") {
behavior = {
helpAnchor: 'join-password',
hasPreviousButton: true,
......@@ -266,7 +288,7 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
focus: 'password'
};
}
else if (formName == "confirmForm") {
else if (formName === "confirmForm") {
behavior = {
hasPreviousButton: true,
hasNextButton: false,
......@@ -342,16 +364,25 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
var onErrorLogout = function(message) {
return function(err) {
csWallet.logout()
if (parameter.uid) {
wallet.unauth()
.then(function(){
UIUtils.onError(message)(err);
});
}
else {
wallet.logout()
.then(function(){
UIUtils.onError(message)(err);
});
}
throw new Error('CANCELLED');
};
};
UIUtils.loading.show();
return csWallet.login({
return wallet.login({
auth: true,
isNew: true,
method: 'SCRYPT_DEFAULT',
......@@ -364,44 +395,74 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
csSettings.data.wallet = csSettings.data.wallet || {};
csSettings.data.wallet.alertIfUnusedWallet = false; // do not alert if empty
// Send self
return csWallet.self($scope.formData.pseudo, false/*do NOT load membership here*/)
var needSelf = angular.isUndefined(parameters.uid) || angular.isUndefined(parameters.blockUid) ||
(parameters.uid !== $scope.formData.pseudo);
if (!needSelf) {
wallet.setSelf(parameters.uid, parameters.blockUid);
}
// Self promise (if need)
var selfPromise = needSelf ?
wallet.self($scope.formData.pseudo, false/*do NOT load membership here*/)
.catch(onErrorLogout('ERROR.SEND_IDENTITY_FAILED')) :
$q.when();
return selfPromise
.then(function() {
// Send membership IN
csWallet.membership.inside()
return wallet.membership.inside()
.catch(function(err) {
if (err && err.ucode != BMA.errorCodes.MEMBERSHIP_ALREADY_SEND) return;
onErrorLogout('ERROR.SEND_MEMBERSHIP_IN_FAILED')(err);
});
})
.then(function() {
$scope.closeModal();
// Redirect to wallet
$state.go('app.view_wallet')
if (wallet.isDefault()) {
return $state.go('app.view_wallet');
} else {
return $state.go('app.view_wallet_by_id', {id: wallet.id});
}
})
.then(function() {
// Wait 12s (for wallet load)
// Wait 2s (for wallet load)
// then ask to download revocation file
return $timeout(
$scope.downloadRevocationRegistration,
return $timeout(function() {
// Hide the loading indicator, if wallet already loaded
if (wallet.isDataLoaded({requirements: true})) {
UIUtils.loading.hide();
}
return $scope.downloadRevocationRegistration();
},
2000);
});
})
.catch(function(err) {
if (err && err.ucode != BMA.errorCodes.MEMBERSHIP_ALREADY_SEND) return;
onErrorLogout('ERROR.SEND_MEMBERSHIP_IN_FAILED')(err);
});
})
.catch(onErrorLogout('ERROR.SEND_IDENTITY_FAILED'));
}
else{
$scope.closeModal();
// Redirect to wallet
if (wallet.isDefault()) {
$state.go('app.view_wallet');
}
else {
$state.go('app.view_wallet_by_id', {id: wallet.id});
}
}
})
.catch(function(err) {
UIUtils.loading.hide();
console.error('>>>>>>>' , err);
UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR');
if (err === 'CANCELLED') return;
if (err && err.ucode != BMA.errorCodes.MEMBERSHIP_ALREADY_SEND) {
console.error("[wallet] Node: already membership", err);
return; // OK
}
else {
UIUtils.alert.error('ERROR.UNKNOWN_ERROR');
}
});
};
......@@ -414,7 +475,7 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
})
.then(function(confirm) {
if (!confirm) return;
return csWallet.downloadRevocation();
return wallet.downloadRevocation();
});
};
......@@ -457,15 +518,22 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
return;
}
var uid = $scope.formData.pseudo.toUpperCase();
var uid = $scope.formData.pseudo;
$scope.formData.computing=true;
// Same has given uid + self block: skip control
if (parameters.uid && uid === parameters.uid) {
$scope.formData.computing=false;
$scope.uiAlreadyUsed = false;
return;
}
else {
// search on uid
BMA.wot.lookup({ search: uid })
.then(function(res) {
$scope.uiAlreadyUsed = (res.results || []).some(function(pub){
return (pub.uids || []).some(function(idty) {
return (idty.uid.toUpperCase() === uid); // same Uid
return (idty.uid === uid); // same Uid
});
});
$scope.formData.computing=false;
......@@ -475,10 +543,16 @@ function JoinModalController($scope, $state, $interval, $timeout, Device, UIUtil
$scope.formData.computing=false;
$scope.uiAlreadyUsed = false;
});
}
};
$scope.$watch('formData.pseudo', $scope.checkUid, true);
$scope.checkAccountAvailable = function() {
if (parameters.pubkey) {
$scope.accountAvailable = true;
return;
}
delete $scope.accountAvailable;
// Search for tx source, from pubkey
return BMA.tx.sources({ pubkey: $scope.formData.pubkey })
......
angular.module('cesium.login.controllers', ['cesium.services'])
.config(function($stateProvider) {
'ngInject';
$stateProvider
.state('app.login', {
url: "/login",
views: {
'menuContent': {
templateUrl: "templates/home/home.html",
controller: 'LoginCtrl'
}
}
})
;
})
.controller('LoginCtrl', LoginController)
.controller('LoginModalCtrl', LoginModalController)
.controller('AuthCtrl', AuthController)
......@@ -26,32 +8,22 @@ angular.module('cesium.login.controllers', ['cesium.services'])
;
function LoginController($scope, $timeout, $controller, csWallet) {
function LoginModalController($scope, $timeout, $q, $ionicPopover, $window, CryptoUtils, csCrypto, ionicReady,
UIUtils, BMA, Modals, csConfig, csSettings, Device, parameters) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('HomeCtrl', {$scope: $scope}));
$scope.showLoginModal = function() {
if ($scope.loading) return $timeout($scope.showLoginModal, 500); // recursive call
if (!csWallet.isLogin() && !$scope.error) {
return $timeout(csWallet.login, 300);
}
};
$scope.$on('$ionicView.enter', $scope.showLoginModal);
parameters = parameters || {};
// Demo mode: force PUBKEY method
if (csConfig.demo) {
parameters.method = 'PUBKEY';
}
function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils, csCrypto, ionicReady,
UIUtils, BMA, Modals, csSettings, Device, parameters) {
'ngInject';
parameters = parameters || {};
$scope.computing = false;
$scope.pubkey = null;
$scope.formData = {};
$scope.showSalt = false;
$scope.showPassword = false;
$scope.showPubkey = false;
$scope.showComputePubkeyButton = false;
$scope.autoComputePubkey = false;
......@@ -131,10 +103,13 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
if (!$scope.formData.username || !$scope.formData.password) return;
var scryptPrams = $scope.formData.scrypt && $scope.formData.scrypt.params;
UIUtils.loading.show();
promise = CryptoUtils.scryptKeypair($scope.formData.username, $scope.formData.password, scryptPrams)
promise = csCrypto.ready()
.then(function() {
return csCrypto.scrypt.keypair($scope.formData.username, $scope.formData.password, scryptPrams);
})
.then(function(keypair) {
if (!keypair) return UIUtils.loading.hide(10);
var pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
var pubkey = csCrypto.util.encode_base58(keypair.signPk);
// Check pubkey
if (parameters.expectedPubkey && parameters.expectedPubkey != pubkey) {
$scope.pubkey = pubkey;
......@@ -170,7 +145,7 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
})
.then(function(keypair) {
if (!keypair) return UIUtils.loading.hide(10);
var pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
var pubkey = csCrypto.util.encode_base58(keypair.signPk);
// Check pubkey
if (parameters.expectedPubkey && parameters.expectedPubkey != pubkey) {
......@@ -308,15 +283,18 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
var salt = $scope.formData.username;
var pwd = $scope.formData.password;
var scryptPrams = $scope.formData.scrypt && $scope.formData.scrypt.params;
return CryptoUtils.scryptSignPk(salt, pwd, scryptPrams)
.then(function (signPk) {
return csCrypto.ready()
.then(function() {
return csCrypto.scrypt.pubkey(salt, pwd, scryptPrams);
})
.then(function (pubkey) {
// If model has changed before the response, then retry
if (salt !== $scope.formData.username || pwd !== $scope.formData.password) {
return $scope.computePubkey();
}
$scope.pubkey = CryptoUtils.util.encode_base58(signPk);
$scope.pubkey = pubkey;
if ($scope.expectedPubkey && $scope.expectedPubkey != $scope.pubkey) {
$scope.pubkeyError = true;
}
......@@ -351,6 +329,11 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
return Modals.showHelp(parameters);
};
$scope.toggleShowSalt = function() {
console.debug('[login] Toggle showSalt');
$scope.showSalt = !$scope.showSalt;
};
$scope.doScan = function() {
if ($scope.computing) return;
......@@ -419,7 +402,11 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
$scope.changeMethod = function(method, params){
$scope.hideMethodsPopover();
if (!method || method == $scope.formData.method) return; // same method
if (method !== 'PUBKEY' && csConfig.demo) {
return UIUtils.alert.demo();
}
if (!method || method === $scope.formData.method) return; // same method
console.debug("[login] method is: " + method);
$scope.formData.method = method;
......@@ -431,7 +418,7 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
}
// Scrypt (advanced or not)
if (method == 'SCRYPT_DEFAULT' || method == 'SCRYPT_ADVANCED') {
if (method === 'SCRYPT_DEFAULT' || method === 'SCRYPT_ADVANCED') {
$scope.pubkey = null;
......@@ -451,9 +438,9 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
}
$scope.changeScrypt(scrypt);
$scope.autoComputePubkey = $scope.autoComputePubkey && (method == 'SCRYPT_DEFAULT');
$scope.autoComputePubkey = $scope.autoComputePubkey && (method === 'SCRYPT_DEFAULT');
}
else if (method == 'SCAN') {
else if (method === 'SCAN') {
return $scope.doScan();
}
else {
......@@ -506,14 +493,17 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
});
};
$scope.fileChanged = function(event) {
$scope.validatingFile = true;
$scope.formData.file = event && event.target && event.target.files && event.target.files.length && event.target.files[0];
if (!$scope.formData.file) {
$scope.onFileChanged = function(file) {
if (!file || !file.fileData) {
$scope.validatingFile = false;
return;
return; // Skip
}
$scope.formData.file = {
name: file.fileData.name,
size: file.fileData.size,
content: file.fileContent
};
$scope.validatingFile = true;
$timeout(function() {
console.debug("[login] key file changed: ", $scope.formData.file);
$scope.validatingFile = true;
......@@ -526,7 +516,7 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
}
else {
$scope.formData.file.pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
$scope.formData.file.valid = !$scope.expectedPubkey || $scope.expectedPubkey == $scope.formData.file.pubkey;
$scope.formData.file.valid = !$scope.expectedPubkey || $scope.expectedPubkey === $scope.formData.file.pubkey;
$scope.validatingFile = false;
}
......@@ -544,41 +534,6 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
});
};
/**
* On file drop
*/
$scope.onKeyFileDrop = function(file) {
if (!file || !file.fileData) return;
$scope.formData.file = {
name: file.fileData.name,
size: file.fileData.size,
content: file.fileContent
};
$scope.validatingFile = true;
$timeout(function() {
return $scope.readKeyFile($scope.formData.file, {withSecret: false})
.then(function (keypair) {
if (!keypair || !keypair.signPk) {
$scope.formData.file.valid = false;
$scope.formData.file.pubkey = undefined;
}
else {
$scope.formData.file.pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
$scope.formData.file.valid = !$scope.expectedPubkey || $scope.expectedPubkey == $scope.formData.file.pubkey;
$scope.validatingFile = false;
}
})
.catch(function (err) {
$scope.validatingFile = false;
$scope.formData.file.valid = false;
$scope.formData.file.pubkey = undefined;
UIUtils.onError('ERROR.AUTH_FILE_ERROR')(err);
});
});
};
$scope.removeKeyFile = function() {
$scope.formData.file = undefined;
};
......@@ -624,18 +579,25 @@ function LoginModalController($scope, $timeout, $q, $ionicPopover, CryptoUtils,
// TODO : for DEV only
/*$timeout(function() {
$scope.formData = {
username: 'benoit.lavenier@e-is.pro',
password: ''
method: 'SCRYPT_DEFAULT',
username: 'abc',
password: 'def'
};
//$scope.form = {$valid:true};
$scope.form = {$valid:true};
$timeout($scope.doLogin, 500);
}, 900); */
}
function AuthController($scope, $controller){
function AuthController($scope, $controller, csConfig){
var config = angular.copy(csConfig);
config.demo = false;
config.readonly = false;
// Initialize the super class and extend it.
angular.extend(this, $controller('LoginModalCtrl', {$scope: $scope, parameters: {auth: true}}));
angular.extend(this, $controller('LoginModalCtrl', {$scope: $scope, parameters: {auth: true}, csConfig: config}));
$scope.setForm = function(form) {
$scope.form = form;
......
......@@ -21,7 +21,7 @@ angular.module('cesium.network.controllers', ['cesium.services'])
})
.state('app.view_peer', {
url: "/network/peer/:server?ssl&tor",
url: "/network/peer/:server?ssl&tor&path",
cache: false,
views: {
'menuContent': {
......@@ -48,12 +48,13 @@ angular.module('cesium.network.controllers', ['cesium.services'])
;
function NetworkLookupController($scope, $state, $location, $ionicPopover, $window, $translate,
BMA, UIUtils, csConfig, csSettings, csCurrency, csNetwork, csWot) {
BMA, Device, UIUtils, csConfig, csSettings, csCurrency, csNetwork, csWot) {
'ngInject';
$scope.networkStarted = false;
$scope.ionItemClass = '';
$scope.expertMode = csSettings.data.expertMode && !UIUtils.screen.isSmall();
$scope.timeout = csSettings.data.timeout;
$scope.isHttps = ($window.location.protocol === 'https:');
$scope.search = {
text: '',
......@@ -70,7 +71,7 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
$scope.compactMode = true;
$scope.listeners = [];
$scope.helptipPrefix = 'helptip-network';
$scope.enableLocationHref = true; // can be overrided by sub-controler (e.g. popup)
$scope.enableLocationHref = true; // can be overrided by sub-controller (e.g. popup)
$scope.removeListeners = function() {
if ($scope.listeners.length) {
......@@ -92,8 +93,9 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
csCurrency.get()
.then(function (currency) {
if (currency) {
$scope.node = !BMA.node.same(currency.node.host, currency.node.port) ?
BMA.instance(currency.node.host, currency.node.port) : BMA;
var isDefaultNode = BMA.node.same(currency.node);
$scope.node = isDefaultNode ? BMA :
BMA.instance(currency.node.host, currency.node.port, currency.node.path || '');
if (state && state.stateParams) {
if (state.stateParams.type && ['mirror', 'member', 'offline'].indexOf(state.stateParams.type) != -1) {
$scope.search.type = state.stateParams.type;
......@@ -116,6 +118,10 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
* Leave the view
*/
$scope.leave = function() {
// Close node, if not the default BMA
if ($scope.node !== BMA) {
$scope.node.close();
}
if (!$scope.networkStarted) return;
$scope.removeListeners();
csNetwork.close();
......@@ -143,8 +149,8 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
asc : $scope.search.asc
},
expertMode: $scope.expertMode,
// larger timeout when on expert mode
timeout: csConfig.timeout && ($scope.expertMode ? (csConfig.timeout / 10) : (csConfig.timeout / 100))
timeout: angular.isDefined($scope.timeout) ? $scope.timeout : Device.network.timeout(),
withSandboxes: $scope.expertMode && Device.isDesktop() // SKip sandboxes if mobile
};
return options;
};
......@@ -152,44 +158,67 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
$scope.load = function() {
if ($scope.search.loading){
$scope.updating = false;
// Start network scan
csNetwork.start($scope.node, $scope.computeOptions());
csNetwork.start($scope.node, $scope.computeOptions())
.then(function() {
$scope.onDataChanged();
});
// Catch event on new peers
$scope.refreshing = false;
$scope.listeners.push(
csNetwork.api.data.on.changed($scope, function(data) {
if (!$scope.refreshing) {
$scope.refreshing = true;
$scope.onDataChanged(data);
}));
}
// Show help tip
$scope.showHelpTip();
};
$scope.onDataChanged = function(data) {
data = csNetwork.data || data;
if (!data || $scope.updating /*|| !$scope.networkStarted*/) return; // Skip if no data, or already updating
var now = Date.now();
console.debug("[peers] Fetching name + avatar, on {0} peers...".format(data.peers && data.peers.length || 0));
// Mark as updating
$scope.updating = true;
// Add name+avatar to peers
csWot.extendAll(data.peers)
.then(function() {
console.debug("[peers] Fetching name + avatar on peers [OK] in {0}ms".format(Date.now() - now));
// Avoid to refresh if view has been leaving
if ($scope.networkStarted) {
$scope.updateView(data);
}
$scope.refreshing = false;
})
.catch(function(err) {
console.error(err);
// Continue
})
.then(function() {
$scope.updating = false;
});
}
}));
}
// Show help tip
$scope.showHelpTip();
};
$scope.updateView = function(data) {
console.debug("[peers] Updating UI");
$scope.$broadcast('$$rebind::' + 'rebind'); // force data binding
$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 || csNetwork.isBusy();
if (!$scope.loading) {
$scope.$broadcast('$$rebind::rebind'); // force data binding
}
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() {
......@@ -200,7 +229,7 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
$scope.sort = function() {
$scope.search.loading = true;
$scope.refreshing = true;
$scope.updating = true;
csNetwork.sort($scope.computeOptions());
$scope.updateView(csNetwork.data);
};
......@@ -248,7 +277,7 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
$scope.toggleCompactMode = function() {
$scope.compactMode = !$scope.compactMode;
$scope.$broadcast('$$rebind::' + 'rebind'); // force data binding
$scope.$broadcast('$$rebind::rebind'); // force data binding
};
$scope.selectPeer = function(peer) {
......@@ -257,7 +286,7 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
return;
}
// Skipp offline or not a BMA peer
// Skip offline or not a BMA peer
if (!peer.online || !peer.hasBma()) return;
var stateParams = {server: peer.getServer()};
......@@ -267,6 +296,9 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win
if (peer.isTor()) {
stateParams.tor = true;
}
if (peer.getPath()) {
stateParams.path = peer.getPath();
}
$state.go('app.view_peer', stateParams);
};
......@@ -394,10 +426,13 @@ function NetworkLookupModalController($scope, $controller, parameters) {
$scope.search.ssl = angular.isDefined(parameters.ssl) ? parameters.ssl : $scope.search.ssl;
$scope.search.ws2p = angular.isDefined(parameters.ws2p) ? parameters.ws2p : $scope.search.ws2p;
$scope.expertMode = angular.isDefined(parameters.expertMode) ? parameters.expertMode : $scope.expertMode;
$scope.timeout = angular.isDefined(parameters.timeout) ? parameters.timeout : $scope.timeout;
$scope.ionItemClass = parameters.ionItemClass || 'item-border-large';
$scope.enableLocationHref = false;
$scope.helptipPrefix = '';
//$scope.compactMode = false; // Always false, because no toggle button in the modal
$scope.selectPeer = function(peer) {
$scope.closeModal(peer);
};
......@@ -452,6 +487,7 @@ function PeerInfoPopoverController($scope, $q, csSettings, csCurrency, csHttp, B
$scope.load = function() {
console.debug("[peer-popover] Loading peer info...");
$scope.loading = true;
$scope.formData = {};
......@@ -484,7 +520,7 @@ function PeerInfoPopoverController($scope, $q, csSettings, csCurrency, csHttp, B
// continue
}),
// Get duniter latest version
// Get latest version
BMA.version.latest()
.then(function(latestRelease){
$scope.formData.latestRelease = latestRelease;
......@@ -496,7 +532,7 @@ function PeerInfoPopoverController($scope, $q, csSettings, csCurrency, csHttp, B
])
.then(function() {
// Compare, to check if newer
if ($scope.formData.latestRelease && $scope.formData.software == 'duniter') {
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;
......@@ -506,7 +542,7 @@ function PeerInfoPopoverController($scope, $q, csSettings, csCurrency, csHttp, B
$scope.formData.hasNewRelease = false;
}
$scope.loading = false;
$scope.$broadcast('$$rebind::' + 'rebind'); // force data binding
$scope.$broadcast('$$rebind::rebind'); // force data binding
});
};
......@@ -544,10 +580,11 @@ function PeerViewController($scope, $q, $window, $state, UIUtils, csWot, BMA) {
$scope.$on('$ionicView.enter', function(e, state) {
var isDefaultNode = !state.stateParams || !state.stateParams.server;
var server = state.stateParams && state.stateParams.server || BMA.server;
var path = state.stateParams && state.stateParams.path || (isDefaultNode ? BMA.path : '');
var useSsl = state.stateParams && state.stateParams.ssl == "true" || (isDefaultNode ? BMA.useSsl : false);
var useTor = state.stateParams.tor == "true" || (isDefaultNode ? BMA.useTor : false);
return $scope.load(server, useSsl, useTor)
return $scope.load(server, path, useSsl, useTor)
.then(function() {
return $scope.$broadcast('$csExtension.enter', e, state);
})
......@@ -559,24 +596,31 @@ function PeerViewController($scope, $q, $window, $state, UIUtils, csWot, BMA) {
});
});
$scope.load = function(server, useSsl, useTor) {
$scope.load = function(server, path, useSsl, useTor) {
var port, host;
var serverParts = server.split(':', 2);
if (serverParts.length === 2) {
host = serverParts[0];
port = serverParts[1];
}
else {
host = server;
port = useSsl ? 443 : 80;
}
var node = {
server: server,
host: server,
host: host,
port: port,
path: path,
useSsl: useSsl,
useTor: useTor
};
var serverParts = server.split(':');
if (serverParts.length == 2) {
node.host = serverParts[0];
node.port = serverParts[1];
}
angular.merge($scope.node,
useTor ?
// For TOR, use a web2tor to access the endpoint
BMA.lightInstance(node.host + ".to", 443, true/*ssl*/, 60000 /*long timeout*/) :
BMA.lightInstance(node.host, node.port, node.useSsl),
BMA.lightInstance(node.host + ".to", 443, node.path, true/*ssl*/, 60000 /*long timeout*/) :
BMA.lightInstance(node.host, node.port, node.path, node.useSsl),
node);
$scope.isReachable = !$scope.isHttps || useSsl;
......@@ -587,10 +631,13 @@ function PeerViewController($scope, $q, $window, $state, UIUtils, csWot, BMA) {
// find the current peer
var peers = (res && res.peers || []).reduce(function(res, json) {
var peer = new Peer(json);
return (peer.getEndpoints('BASIC_MERKLED_API') || []).reduce(function(res, ep) {
return (peer.getEndpoints('BASIC_MERKLED_API') || [])
.concat((peer.getEndpoints('BMAS') || []))
.reduce(function(res, ep) {
var bma = BMA.node.parseEndPoint(ep);
if((bma.dns == node.host || bma.ipv4 == node.host || bma.ipv6 == node.host) && (
bma.port == node.port)) {
if ((bma.dns === node.host || bma.ipv4 === node.host || bma.ipv6 === node.host) &&
(bma.port == node.port) &&
(bma.path == node.path)) {
peer.bma = bma;
return res.concat(peer);
}
......@@ -676,6 +723,9 @@ function PeerViewController($scope, $q, $window, $state, UIUtils, csWot, BMA) {
if (peer.isTor()) {
stateParams.tor = true;
}
if (peer.getPath()) {
stateParams.path = peer.getPath();
}
$state.go('app.view_peer', stateParams);
};
......
......@@ -20,7 +20,7 @@ angular.module('cesium.settings.controllers', ['cesium.services', 'cesium.curren
.controller('SettingsCtrl', SettingsController)
;
function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $timeout, $translate, $ionicPopover,
function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $timeout, $translate, $ionicPopover, $ionicScrollDelegate,
UIUtils, Modals, BMA, csHttp, csConfig, csCurrency, csSettings, csPlatform) {
'ngInject';
......@@ -29,8 +29,12 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
$scope.loading = true;
$scope.nodePopup = {};
$scope.bma = BMA;
$scope.listeners = [];
$scope.platform = {
loading: !csPlatform.isStarted(),
loadingMessage: 'COMMON.LOADING'
};
$scope.timeouts = csSettings.timeouts;
$scope.keepAuthIdleLabels = {
/*0: {
labelKey: 'SETTINGS.KEEP_AUTH_OPTION.NEVER'
......@@ -80,29 +84,31 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
};
$scope.blockValidityWindows = _.keys($scope.blockValidityWindowLabels);
$scope.$on('$ionicView.enter', function() {
$scope.enter = function() {
$scope.addListeners();
$q.all([
csSettings.ready(),
csCurrency.parameters()
.then(function(parameters) {
return parameters && parameters.avgGenTime;
})
// Make sure to continue even if node is down - Fix #788
.catch(function(err) {
console.error("[settings] Could not not currency parameters. Using default 'avgGenTime' (300)", err);
return {avgGenTime: 300};
// Continue (will use default value)
// Make sure to continue even if node is down - Fix #788
})
.then(function(parameters) {
var avgGenTime = parameters && parameters.avgGenTime;
if (!avgGenTime || avgGenTime < 0) {
console.warn('[settings] Could not not currency parameters. Using default G1 \'avgGenTime\' (300s)');
avgGenTime = 300; /* = G1 value = 5min */
}
_.each($scope.blockValidityWindows, function(blockCount) {
if (blockCount > 0) {
$scope.blockValidityWindowLabels[blockCount].labelParams.time= parameters.avgGenTime * blockCount;
$scope.blockValidityWindowLabels[blockCount].labelParams.time = avgGenTime * blockCount;
}
});
})
])
.then($scope.load)
;
});
.then($scope.load);
};
$scope.setPopupForm = function(popupForm) {
$scope.popupForm = popupForm;
......@@ -111,9 +117,13 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
$scope.load = function() {
$scope.loading = true; // to avoid the call of csWallet.store()
$scope.platform.loading = !csPlatform.isStarted();
// Fill locales
$scope.locales = angular.copy(csSettings.locales);
// Apply settings
angular.merge($scope.formData, csSettings.data);
......@@ -130,6 +140,22 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
}, 100);
};
$scope.addListeners = function() {
$scope.listeners = [
// Listen platform start message
csPlatform.api.start.on.message($scope, function(message) {
$scope.platform.loading = !csPlatform.isStarted();
$scope.platform.loadingMessage = message;
})
];
};
$scope.leave = function() {
console.debug('[settings] Leaving page');
$scope.removeListeners();
};
$scope.reset = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
......@@ -148,39 +174,88 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
$translate.use(langKey);
};
$scope.changeExpertMode = function(expertMode) {
// Restart platform, to auto select node
if (!expertMode) {
csPlatform.restart();
}
};
// Change node
$scope.changeNode= function(node) {
$scope.changeNode = function(node, confirm) {
// If platform not stared yet: wait then loop
if (!csPlatform.isStarted()) {
UIUtils.loading.update({template: this.loadingMessage});
return csPlatform.ready()
.then(function() {
return $scope.changeNode(node, confirm); // Loop
});
}
// Ask user to confirm, before allow to change the node
if (!confirm && !$scope.formData.expertMode) {
return UIUtils.alert.confirm('CONFIRM.ENABLE_EXPERT_MODE_TO_CHANGE_NODE', 'CONFIRM.POPUP_WARNING_TITLE', {
cssClass: 'warning',
cancelText: 'COMMON.BTN_NO',
okText: 'COMMON.BTN_YES_CONTINUE',
okType: 'button-assertive'
})
.then(function(confirm) {
if (!confirm) return;
$scope.changeNode(node, true);
});
}
// If not given, get node from settings data
if (!node || !node.host) {
var host = $scope.formData.node.host;
if (!host) return; // Should never occur
var useSsl = angular.isDefined($scope.formData.node.useSsl) ?
$scope.formData.node.useSsl :
($scope.formData.node.port == 443);
var port = !!$scope.formData.node.port && $scope.formData.node.port != 80 && $scope.formData.node.port != 443 ? $scope.formData.node.port : undefined;
node = node || {
host: $scope.formData.node.host,
var path = $scope.formData.node.path || (host.indexOf('/') !== -1 ? host.substring(host.indexOf('/')) : '');
if (path.endsWith('/')) path = path.substring(0, path.length - 1); // Remove trailing slash
host = host.indexOf('/') !== -1 ? host.substring(0, host.indexOf('/')) : host; // Remove path from host
node = {
host: host,
port: port,
useSsl: angular.isDefined($scope.formData.node.useSsl) ?
$scope.formData.node.useSsl :
($scope.formData.node.port == 443)
path: path,
useSsl: useSsl
};
}
$scope.showNodePopup(node)
.then(function(newNode) {
if (newNode.host === $scope.formData.node.host &&
newNode.port === $scope.formData.node.port &&
newNode.useSsl === $scope.formData.node.useSsl && !$scope.formData.node.temporary) {
newNode.port == $scope.formData.node.port &&
newNode.path === $scope.formData.node.path &&
newNode.useSsl === $scope.formData.node.useSsl &&
!$scope.formData.node.temporary) {
return; // same node = nothing to do
}
// Change to expert mode
$scope.formData.expertMode = true;
UIUtils.loading.show();
var nodeBMA = BMA.instance(newNode.host, newNode.port, newNode.useSsl, true /*cache*/);
nodeBMA.isAlive()
BMA.isAlive(newNode)
.then(function(alive) {
if (!alive) {
UIUtils.loading.hide();
return UIUtils.alert.error('ERROR.INVALID_NODE_SUMMARY')
.then(function(){
$scope.changeNode(newNode); // loop
$scope.changeNode(newNode, true); // loop
});
}
UIUtils.loading.hide();
angular.merge($scope.formData.node, newNode);
delete $scope.formData.node.temporary;
BMA.copy(nodeBMA);
BMA.stop();
BMA.copy(newNode);
$scope.bma = BMA;
// Restart platform (or start if not already started)
......@@ -206,24 +281,31 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
.then(function (peer) {
if (peer) {
var bma = peer.getBMA();
var host = (bma.dns ? bma.dns :
(peer.hasValid4(bma) ? bma.ipv4 : bma.ipv6));
var useSsl = bma.useSsl || bma.port == 443;
var port = bma.port || (useSsl ? 443 : 80);
return {
host: (bma.dns ? bma.dns :
(peer.hasValid4(bma) ? bma.ipv4 : bma.ipv6)),
port: bma.port || 80,
useSsl: bma.useSsl || bma.port == 443
host: host,
port: port,
path: bma.path || '',
useSsl: useSsl
};
}
})
.then(function(newNode) {
$scope.changeNode(newNode);
$scope.changeNode(newNode, true);
});
};
// Show node popup
$scope.showNodePopup = function(node) {
return $q(function(resolve, reject) {
$scope.popupData.newNode = node.port ? [node.host, node.port].join(':') : node.host;
$scope.popupData.useSsl = node.useSsl;
var useSsl = node.useSsl || node.port == 443;
var host = (node.port && node.port != 80 && node.port != 443) ? [node.host, node.port].join(':') : node.host;
if (node.path && node.path.length && node.path !== '/') host += node.path;
$scope.popupData.newNode = host;
$scope.popupData.useSsl = useSsl;
if (!!$scope.popupForm) {
$scope.popupForm.$setPristine();
}
......@@ -246,7 +328,7 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
e.preventDefault();
} else {
return {
server: $scope.popupData.newNode,
host: $scope.popupData.newNode,
useSsl: $scope.popupData.useSsl
};
}
......@@ -255,16 +337,23 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
]
})
.then(function(res) {
if (!res) { // user cancel
if (!res || !res.host) { // user cancel
UIUtils.loading.hide();
reject('CANCELLED');
return;
}
var parts = res.server.split(':');
parts[1] = parts[1] ? parts[1] : 80;
var host = res.host;
var path = host.indexOf('/') !== -1 ? host.substring(host.indexOf('/')) : '';
host = host.indexOf('/') !== -1 ? host.substring(0, host.indexOf('/')) : host;
var parts = host.split(':', 2);
host = parts[0];
var port = parts[1] ? parts[1] : (res.useSsl ? 443 : 80);
var useSsl = res.useSsl || port == 443;
resolve({
host: parts[0],
port: parts[1],
useSsl: res.useSsl
host: host,
port: port,
path: path,
useSsl: useSsl
});
});
});
......@@ -283,14 +372,21 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
}
$scope.saving = true;
var now = Date.now();
// Async - to avoid UI lock
return $timeout(function() {
console.debug('[settings] Saving...');
// Make sure to format helptip
$scope.cleanupHelpTip();
// Applying
csSettings.apply($scope.formData);
// Applying UI effect
UIUtils.setEffects($scope.formData.uiEffects);
// Store
return csSettings.store();
......@@ -298,6 +394,7 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
.then(function() {
//return $timeout(function() {
$scope.saving = false;
console.debug('[settings] Saving [OK] in {0}ms'.format(Date.now() - now));
//}, 100);
});
};
......@@ -384,6 +481,7 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
var helptipScope = $scope.createHelptipScope(tour);
if (!helptipScope) return; // could be undefined, if a global tour already is already started
$ionicScrollDelegate.scrollTop(true);
return helptipScope.startSettingsTour(index, false)
.then(function(endIndex) {
helptipScope.$destroy();
......@@ -391,4 +489,18 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
csSettings.store();
});
};
$scope.removeListeners = function() {
if ($scope.listeners.length) {
console.debug('[settings] Closing listeners');
_.forEach($scope.listeners, function(remove){
remove();
});
$scope.listeners = [];
}
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.$on('$ionicView.beforeLeave', $scope.leave);
}
......@@ -129,16 +129,24 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
$scope.destPub = parameters.pubkey;
}
if (parameters.amount) {
$scope.formData.amount = parameters.amount;
var amount = Number(parameters.amount);
// Trunc at 2 decimals
$scope.formData.amount = !isNaN(amount) ? Math.trunc(parseFloat(parameters.amount) * 100) / 100 : null;
$scope.formData.useRelative=false;
}
else if (parameters.udAmount) {
$scope.formData.amount = parameters.udAmount;
var udAmount = Number(parameters.udAmount);
$scope.formData.amount = !isNaN(udAmount) ? udAmount : null;
$scope.formData.useRelative=true;
}
if (parameters.comment) {
var cleanComment = parameters.comment.trim()
.replaceAll(new RegExp(BMA.constants.regexp.INVALID_COMMENT_CHARS, 'g'), ' ')
.replaceAll(/\s+/g, ' ');
if (cleanComment.length > 0) {
$scope.formData.useComment = true;
$scope.formData.comment = parameters.comment;
$scope.formData.comment = cleanComment;
}
}
if (parameters.restPub || parameters.all) {
$scope.restUid = '';
......@@ -156,6 +164,7 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
$scope.formData.walletId = parameters.wallet;
}
};
// Read default parameters
$scope.setParameters(parameters);
......@@ -164,7 +173,7 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
wallet = $scope.enableSelectWallet && ($scope.formData.walletId ? csWallet.children.get($scope.formData.walletId) : csWallet) || csWallet;
if (!wallet.isDefault()) {
console.debug("[transfer] Using wallet {" + wallet.id + "}");
console.debug("[transfer] Using wallet {{0}}".format(wallet.id));
}
// Make to sure to load full wallet data (balance)
return wallet.login({sources: true, silent: true})
......@@ -226,6 +235,8 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
};
$scope.onAmountChanged = function() {
if (!$scope.form || !$scope.form.amount) return; // skip if modal has been destroyed
if ($scope.sending || !$scope.form.amount) return; // skip if sending TX
var amount = $scope.formData.amount;
if (amount && typeof amount === "string") {
......@@ -256,7 +267,7 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
}
$scope.form.$valid = valid;
$scope.form.amount.$invalid = !valid;
if ($scope.form.amount) $scope.form.amount.$invalid = !valid;
if (!valid || !$scope.formData.all || !amount) {
$scope.formData.restAmount = undefined;
......@@ -274,6 +285,8 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
};
$scope.doTransfer = function() {
if ($scope.loading) return; // Skip
$scope.form.$submitted=true;
if(!$scope.form.$valid || !$scope.formData.destPub || !$scope.formData.amount) {
......@@ -319,7 +332,7 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
amount = amount.toFixed(2) * 100; // remove 2 decimals on quantitative mode
}
// convert comment: trim, then null if empty
// Trim comment to null
var comment = $scope.formData.comment && $scope.formData.comment.trim();
if (comment && !comment.length) {
comment = null;
......@@ -333,12 +346,12 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
}
})
.then(function() {
$scope.sending = false;
UIUtils.loading.hide();
return $scope.closeModal(true);
})
.then(function(res) {
$timeout(function() {
$scope.sending = false;
UIUtils.toast.show('INFO.TRANSFER_SENT');
}, 500);
return res;
......@@ -392,12 +405,12 @@ function TransferModalController($scope, $q, $translate, $timeout, $filter, $foc
return Modals.showWotLookup({enableWallets: true})
.then(function(result){
if (result) {
if (formDataField == 'destPub') {
if (formDataField === 'destPub') {
$scope.destUid = result.uid;
$scope.destPub = result.uid ? '' : result.pubkey;
$scope.formData.destPub = result.pubkey;
}
else if (formDataField == 'restPub') {
else if (formDataField === 'restPub') {
$scope.restUid = result.uid;
$scope.restPub = result.uid ? '' : result.pubkey;
$scope.formData.restPub = result.pubkey;
......
......@@ -64,6 +64,11 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
$scope.loading = true;
$scope.settings = csSettings.data;
$scope.qrcodeId = 'qrcode-wallet-' + $scope.$id;
$scope.toggleQRCode = false;
$scope.likeData = {
likes: {},
abuses: {}
};
var wallet;
......@@ -86,7 +91,7 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
}
else {
// update view (to refresh avatar + plugin data, such as profile, subscriptions...)
UIUtils.loading.hide();
UIUtils.loading.hide(10);
$timeout($scope.updateView, 300);
}
};
......@@ -100,21 +105,25 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
$scope.formData = walletData;
$scope.loading = false; // very important, to avoid TX to be display before wallet.currentUd is loaded
$scope.updateView();
$scope.showQRCode();
$scope.showHelpTip();
$scope.addListeners();
UIUtils.loading.hide(); // loading could have be open (e.g. new account)
$scope.showQRCode();
if (wallet.isDefault()) $scope.showHelpTip();
UIUtils.loading.hide(10); // loading could have be open (e.g. new account)
})
.catch(function(err){
if (err === 'CANCELLED') {
$scope.showHome();
return;
}
UIUtils.onError('ERROR.LOAD_WALLET_DATA_ERROR')(err);
});
};
$scope.updateView = function() {
$scope.motion.show({selector: '#wallet .item'});
$scope.$broadcast('$$rebind::' + 'rebind'); // force rebind
$scope.$broadcast('$$rebind::rebind'); // force rebind
};
......@@ -186,6 +195,7 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
if (!uid) { // user cancel
delete $scope.formData.uid;
UIUtils.loading.hide();
reject('CANCELLED');
return;
}
resolve(uid);
......@@ -200,6 +210,8 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
return $scope.showUidPopup()
.then(function(uid) {
if (!uid) return;
UIUtils.loading.show();
return wallet.self(uid)
......@@ -247,35 +259,14 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
return UIUtils.alert.info("INFO.NOT_NEED_MEMBERSHIP");
}
// Select uid (or reuse it)
return ((keepSelf && !!$scope.formData.blockUid) ?
$q.when($scope.formData.uid) :
$scope.showUidPopup())
var uid = angular.isDefined($scope.formData.blockUid) && $scope.formData.uid || undefined;
// Ask user confirmation
.then(function(uid) {
return UIUtils.alert.confirm("CONFIRM.MEMBERSHIP")
.then(function(confirm) {
if (!confirm) throw 'CANCELLED';
return uid;
});
// Approve the license
return Modals.showJoinMember({
uid : uid,
blockUid: uid && $scope.formData.blockUid,
pubkey: $scope.formData.pubkey
})
// Send self (identity) - if need
.then(function (uid) {
UIUtils.loading.show();
// If uid changed, or self blockUid not retrieve : do self() first
if (!$scope.formData.blockUid || uid != $scope.formData.uid) {
$scope.formData.blockUid = null;
$scope.formData.uid = uid;
return wallet.self(uid, false/*do NOT load membership here*/);
}
})
// Send membership
.then($scope.doMembershipIn)
.catch(function(err) {
if (err === 'CANCELLED') return;
if (!wallet.data.uid) {
......@@ -447,16 +438,16 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
* @param fix
*/
$scope.doQuickFix = function(event) {
if (event == 'renew') {
if (event === 'renew') {
$scope.renewMembership();
}
else if (event == 'membership') {
else if (event === 'membership') {
$scope.membershipIn(true/*keep self*/);
}
else if (event == 'fixMembership') {
$scope.fixMembership();
else if (event === 'fixMembership') {
$scope.fixMembership(false);
}
else if (event == 'fixIdentity') {
else if (event === 'fixIdentity') {
$scope.fixIdentity();
}
};
......@@ -480,27 +471,26 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
};
$scope.showQRCode = function(timeout) {
if (!$scope.qrcode) {
$scope.qrcode = new QRCode(
$scope.qrcodeId,
{
text: $scope.formData.pubkey,
width: 200,
height: 200,
correctLevel: QRCode.CorrectLevel.L
});
UIUtils.motion.toggleOn({selector: '#'+$scope.qrcodeId}, timeout || 1100);
if (!wallet || !$scope.qrcodeId) return; // Skip
// Get the DIV element
var element = angular.element(document.querySelector('#' + $scope.qrcodeId + ' .content'));
if (!element) {
console.error("[wallet-controller] Cannot found div #{0} for the QRCode. Skipping.".format($scope.qrcodeId));
return;
}
else {
$scope.qrcode.clear();
$scope.qrcode.makeCode($scope.formData.pubkey);
wallet.loadQrCode()
.then(function(svg) {
element.html(svg);
UIUtils.motion.toggleOn({selector: '#'+$scope.qrcodeId}, timeout || 1100);
}
});
};
$scope.hideQRCode = function() {
if ($scope.qrcode) {
$scope.qrcode.clear();
if (!$scope.qrcodeId) return;
var element = angular.element(document.querySelector('#' + $scope.qrcodeId));
if (element) {
UIUtils.motion.toggleOff({selector: '#'+$scope.qrcodeId});
}
};
......@@ -513,7 +503,7 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
});
}
else {
$state.go(UIUtils.screen.isSmall() ? 'app.wallet_cert_by_id' : 'app.wallet_cert_by_id_lg', {
$state.go(UIUtils.screen.isSmall() ? 'app.wallet_cert_by_id' : 'app.wallet_cert_lg_by_id', {
id: $scope.walletId,
type: 'received'
});
......@@ -528,7 +518,7 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
});
}
else {
$state.go(UIUtils.screen.isSmall() ? 'app.wallet_cert_by_id' : 'app.wallet_cert_by_id_lg', {
$state.go(UIUtils.screen.isSmall() ? 'app.wallet_cert_by_id' : 'app.wallet_cert_lg_by_id', {
id: $scope.walletId,
type: 'given'
});
......@@ -558,7 +548,7 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
.then(function(done){
if (done) {
UIUtils.toast.show('INFO.TRANSFER_SENT');
$scope.$broadcast('$$rebind::' + 'balance'); // force rebind balance
$scope.$broadcast('$$rebind::balance'); // force rebind balance
$scope.motion.show({selector: '.item-pending'});
}
});
......@@ -600,9 +590,28 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
});
};
$scope.showSelectWalletModal = function() {
if (!csWallet.children.count()) return;
/* -- popovers -- */
return Modals.showSelectWallet({
parameters: {
showDefault: true,
showBalance: false,
excludedWalletId: $scope.walletId
}
})
.then(function(newWallet) {
if (!newWallet || wallet && newWallet.id === wallet.id) return;
$scope.removeListeners();
$scope.loading = true;
wallet = newWallet;
console.debug("[transfer] Using wallet {" + wallet.id + "}");
$scope.formData = {};
return $scope.load();
});
};
/* -- popovers -- */
$scope.showActionsPopover = function(event) {
UIUtils.popover.show(event, {
......@@ -645,11 +654,13 @@ function WalletController($scope, $rootScope, $q, $ionicPopup, $timeout, $state,
};
$scope.showSelectWalletPopover = function(event) {
return csPopovers.showSelectWallet(event, {
scope: $scope
})
parameters: {
excludedWalletId: $scope.walletId
}})
.then(function(newWallet) {
if (!newWallet || newWallet.id === wallet.id) return;
if (!newWallet || newWallet.id === $scope.walletId) return; // nothing changed
if (newWallet.isDefault()) {
return $state.go('app.view_wallet');
}
......@@ -676,8 +687,11 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
'ngInject';
$scope.loading = true;
$scope.loadingMore = false;
$scope.lastMoreTxTime = null;
$scope.settings = csSettings.data;
$scope.listeners = [];
$scope.qrcodeId = 'qrcode-wallet-tx-' + $scope.$id;
var wallet;
......@@ -692,6 +706,8 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
return $scope.showHome();
}
$scope.walletId = wallet.id;
$scope.cleanLocationHref(state);
return $scope.load();
......@@ -710,15 +726,17 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
$scope.$on('$ionicView.leave', $scope.leave);
$scope.load = function() {
if (!wallet) return;
if (!wallet) return $q.reject('Missing wallet');
var hasMinData = wallet.isDataLoaded({minData: true});
var fromTime = csHttp.date.now() - csSettings.data.walletHistoryTimeSecond;
var options = {
requirements: !hasMinData, // load requirements (=minData) once
minData: !hasMinData,
sources: true,
tx: {
enable: true
enable: true,
fromTime: fromTime
}
};
......@@ -727,15 +745,21 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
$scope.formData = walletData;
$scope.loading = false; // very important, to avoid TX to be display before wallet.currentUd is loaded
$scope.updateView();
$scope.showFab('fab-transfer');
$scope.showHelpTip();
$scope.addListeners();
UIUtils.loading.hide(); // loading could have be open (e.g. during login phase)
$scope.showFab('fab-transfer');
$scope.showQRCode();
if (wallet.isDefault()) $scope.showHelpTip();
return UIUtils.loading.hide(10); // loading could have be open (e.g. during login phase)
})
.catch(function(err){
if (err === 'CANCELLED') {
$scope.showHome();
return;
}
console.error(err);
UIUtils.onError('ERROR.LOAD_WALLET_DATA_ERROR')(err);
});
};
......@@ -753,9 +777,16 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
// Update view
$scope.updateView = function() {
if (!$scope.formData || $scope.loading) return;
$scope.$broadcast('$$rebind::' + 'balance'); // force rebind balance
$scope.$broadcast('$$rebind::' + 'rebind'); // force rebind
if (!$scope.formData || !$scope.formData.tx || $scope.loading) return;
$scope.minScrollMoreTime = moment().utc()
.subtract(csSettings.data.walletHistoryScrollMaxTimeSecond, 'second')
.startOf('day')
.unix();
$scope.canScrollMore = $scope.formData.tx.fromTime > $scope.minScrollMoreTime;
$scope.$broadcast('$$rebind::balance'); // force rebind balance
$scope.$broadcast('$$rebind::rebind'); // force rebind
$scope.motion.show({selector: '.view-wallet-tx .item', ink: false});
};
......@@ -763,7 +794,19 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
options = options || {};
options.fromTime = options.fromTime || -1; // default: full history
var pubkey = $scope.formData.pubkey;
csTx.downloadHistoryFile(pubkey, options);
$scope.hideActionsPopover();
UIUtils.toast.show('COMMON.DOWNLOADING_DOTS');
return csTx.downloadHistoryFile(pubkey, options)
.then(UIUtils.toast.hide)
.then(function() {
UIUtils.toast.show('INFO.FILE_DOWNLOADED', 1000);
})
.catch(function(err){
if (err && err === 'CANCELLED') return;
UIUtils.onError('ERROR.DOWNLOAD_TX_HISTORY_FAILED')(err);
});
};
// Updating wallet data
......@@ -778,6 +821,7 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
},
api: false
};
return (silent ?
// If silent: just refresh
wallet.refreshData(options) :
......@@ -792,6 +836,31 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
.catch(UIUtils.onError('ERROR.REFRESH_WALLET_DATA'));
};
$scope.showQRCode = function(timeout) {
if (!wallet || !$scope.qrcodeId) return; // Skip
// Get the DIV element
var element = angular.element(document.querySelector('#' + $scope.qrcodeId + ' .content'));
if (!element) {
console.error("[wallet-controller] Cannot found div #{0} for the QRCode. Skipping.".format($scope.qrcodeId));
return;
}
wallet.loadQrCode()
.then(function(svg) {
element.html(svg);
UIUtils.motion.toggleOn({selector: '#'+$scope.qrcodeId}, timeout || 1100);
});
};
$scope.hideQRCode = function() {
if (!$scope.qrcodeId) return;
var element = angular.element(document.querySelector('#' + $scope.qrcodeId));
if (element) {
UIUtils.motion.toggleOff({selector: '#'+$scope.qrcodeId});
}
};
/* -- add listeners -- */
$scope.addListeners = function() {
......@@ -841,7 +910,7 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
.then(function(done){
if (done) {
UIUtils.toast.show('INFO.TRANSFER_SENT');
$scope.$broadcast('$$rebind::' + 'balance'); // force rebind balance
$scope.$broadcast('$$rebind::balance'); // force rebind balance
$scope.motion.show({selector: '.item-pending'});
}
});
......@@ -859,27 +928,43 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
};
$scope.showMoreTx = function(fromTime) {
if ($scope.loadingMore) return; // Skip
// Add a delay if previous load has been done recently
var waitDelayMs = $scope.lastMoreTxTime ? Date.now() - $scope.lastMoreTxTime : BMA.constants.LIMIT_REQUEST_DELAY;
if (waitDelayMs > 0 && waitDelayMs < BMA.constants.LIMIT_REQUEST_DELAY) {
return $timeout(function() {
return $scope.showMoreTx(fromTime);
}, waitDelayMs);
}
$scope.loadingMore = true;
$scope.loadingMoreTime = Date.now();
fromTime = fromTime ||
($scope.formData.tx.fromTime - csSettings.data.walletHistoryTimeSecond) ||
(csHttp.date.now() - 2 * csSettings.data.walletHistoryTimeSecond);
UIUtils.loading.show();
return wallet.refreshData({tx: {enable: true, fromTime: fromTime}})
console.info('[wallet-tx] Fetching more TX, since: ' + fromTime);
return wallet.refreshData({sources: true, tx: {enable: true, fromTime: fromTime}})
.then(function() {
$scope.loadingMore = false;
$scope.updateView();
UIUtils.loading.hide();
$scope.$broadcast('scroll.infiniteScrollComplete');
})
.catch(function(err) {
// If http rest limitation: wait then retry
if (err.ucode == BMA.errorCodes.HTTP_LIMITATION) {
$timeout(function() {
return $scope.showMoreTx(fromTime);
if (err.ucode === BMA.errorCodes.HTTP_LIMITATION) {
return $timeout(function() {
return $scope.showMoreTx(fromTime); // Loop
}, 2000);
}
else {
$scope.loadingMore = false;
$scope.canScrollMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
UIUtils.onError('ERROR.REFRESH_WALLET_DATA')(err);
}
});
};
......@@ -887,8 +972,11 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
if (!csWallet.children.count()) return;
return Modals.showSelectWallet({
parameters: {
showDefault: true,
showBalance: false
showBalance: false,
excludedWalletId: $scope.walletId
}
})
.then(function(newWallet) {
if (!newWallet || wallet && newWallet.id === wallet.id) return;
......@@ -903,6 +991,24 @@ function WalletTxController($scope, $ionicPopover, $state, $timeout, $location,
/* -- popover -- */
$scope.showActionsPopover = function(event) {
UIUtils.popover.show(event, {
templateUrl: 'templates/wallet/popover_tx_actions.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.actionsPopover = popover;
}
});
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
$scope.actionsPopover = null;
}
};
var paddingIndent = 10;
$scope.toUnlockUIArray = function(unlockTreeItem, leftPadding, operator) {
......@@ -1061,7 +1167,7 @@ function WalletTxErrorController($scope, UIUtils, csSettings, csWallet) {
}
function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, parameters){
function WalletSecurityModalController($scope, UIUtils, csConfig, csWallet, $translate, parameters){
var wallet = parameters && parameters.wallet || csWallet;
......@@ -1103,6 +1209,10 @@ function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, pa
});
});
$scope.$on("$ionicSlides.sliderInitialized", function(event, data){
// Disable swipe
data.slider.lockSwipes();
});
$scope.slidePrev = function() {
$scope.slides.slider.unlockSwipes();
......@@ -1172,7 +1282,10 @@ function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, pa
}
};
$scope.selectOption = function(option){
$scope.selectOption = function(option, enableOnDemo){
if (!enableOnDemo && csConfig.demo) {
return UIUtils.alert.demo();
}
$scope.option = option;
$scope.slideNext();
};
......@@ -1219,13 +1332,12 @@ function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, pa
};
/**
* Recover Id
* Set the file content
*/
$scope.recoverContent = function(file) {
$scope.onFileChanged = function(file) {
$scope.hasContent = angular.isDefined(file) && file !== '';
$scope.fileData = file.fileData ? file.fileData : '';
$scope.isValidFile = $scope.fileData !== '' && $scope.fileData.type == 'text/plain';
$scope.isValidFile = $scope.fileData !== '' && $scope.fileData.type === 'text/plain';
if ($scope.isValidFile && $scope.option === 'recoverID') {
$scope.content = file.fileContent.split('\n');
......@@ -1277,7 +1389,8 @@ function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, pa
else {
UIUtils.alert.error('ERROR.RECOVER_ID_FAILED');
}
});
})
.catch(UIUtils.onError('ERROR.RECOVER_ID_FAILED'));
};
......@@ -1327,11 +1440,20 @@ function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, pa
return wallet.getCryptedId(record)
.then(function(record){
wallet.downloadSaveId(record);
$scope.closeModal();
return wallet.downloadSaveId(record)
.then(function() {
UIUtils.toast.show('INFO.FILE_DOWNLOADED', 1000);
})
.catch(function(err){
if (err && err === 'CANCELLED') return;
UIUtils.onError('ERROR.DOWNLOAD_SAVE_ID_FAILED')(err);
});
})
;
})
;
};
$scope.isRequired = function(){
......@@ -1362,15 +1484,15 @@ function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, pa
return wallet.downloadRevocation();
})
.then(UIUtils.loading.hide)
.then(function() {
UIUtils.loading.hide();
UIUtils.toast.show('INFO.FILE_DOWNLOADED', 1000);
})
.catch(function(err){
if (err && err === 'CANCELLED') return;
UIUtils.onError('ERROR.DOWNLOAD_REVOCATION_FAILED')(err);
})
;
});
};
......@@ -1434,7 +1556,16 @@ function WalletSecurityModalController($scope, UIUtils, csWallet, $translate, pa
$scope.closeModal();
return UIUtils.loading.hide();
})
.catch(UIUtils.onError('ERROR.REVOCATION_FAILED'));
.catch(function(err) {
if ($scope.revocation){
$scope.isValidFile = false;
$scope.revocationError = err && err.message || err || 'ERROR.REVOCATION_FAILED';
UIUtils.loading.hide(10);
}
else {
UIUtils.onError('ERROR.REVOCATION_FAILED')(err);
}
});
};
......
......@@ -9,7 +9,7 @@ angular.module('cesium.wallets.controllers', ['cesium.services', 'cesium.currenc
views: {
'menuContent': {
templateUrl: "templates/wallet/list/view_wallets.html",
controller: 'WalletListCtrl'
controller: 'WalletListViewCtrl'
}
},
data: {
......@@ -59,119 +59,220 @@ angular.module('cesium.wallets.controllers', ['cesium.services', 'cesium.currenc
})
;
})
.controller('WalletListAbstractCtrl', WalletListAbstractController)
.controller('WalletListCtrl', WalletListController)
.controller('WalletListViewCtrl', WalletListViewController)
.controller('WalletSelectModalCtrl', WalletSelectModalController)
.controller('WalletListImportModalCtrl', WalletListImportModalController)
.controller('PopoverWalletSelectModalCtrl', PopoverWalletSelectModalController)
.controller('WalletSelectPopoverCtrl', WalletSelectPopoverController)
;
function WalletListController($scope, $controller, $state, $timeout, $q, $translate, $ionicPopover, $ionicPopup,
ModalUtils, UIUtils, Modals, csCurrency, csSettings, csWallet){
function WalletListAbstractController($scope, $q, $timeout, UIUtils, filterTranslations, csSettings, csCurrency, csWallet) {
'ngInject';
$scope.settings = csSettings.data;
$scope.listeners = [];
$scope.loading = true;
$scope.wallets = null;
$scope.formData = {
useRelative: csSettings.data.useRelative,
showDefault: true,
showBalance: false,
balance: undefined,
updatingWalletId: undefined,
stopped: false,
minData: true
};
$scope.motion = null; // no animation
// Initialize the super class and extend it.
angular.extend(this, $controller('WalletSelectModalCtrl', {$scope: $scope, parameters: {}}));
$scope.setParameters = function(parameters) {
parameters = parameters || {};
// Override defaults
$scope.formData.name = undefined;
$scope.motion = UIUtils.motion.default;
$scope.entered = false;
$scope.formData.useRelative = angular.isDefined(parameters.useRelative) ? parameters.useRelative : $scope.formData.useRelative;
$scope.formData.showBalance = angular.isDefined(parameters.showBalance) ? parameters.showBalance : $scope.formData.showBalance;
$scope.formData.minData = angular.isDefined(parameters.minData) ? parameters.minData : $scope.formData.minData;
$scope.formData.excludedWalletId = parameters.excludedWalletId;
$scope.formData.showDefault = (angular.isDefined(parameters.showDefault) ? parameters.showDefault : $scope.formData.showDefault) &&
($scope.formData.excludedWalletId !== 'default');
$scope.enter = function(e, state) {
// First enter
if (!$scope.entered) {
$scope.entered = true;
$scope.setParameters({
showDefault: true,
showBalance: true,
minData: false
});
};
return $scope.load()
.then(function() {
$scope.load = function(options) {
options = options || {};
$scope.loading = (options.silent !== false);
$scope.formData.balance = undefined;
$scope.formData.updatingWalletId = undefined;
$scope.formData.stopped = false;
// Load currency, and filter translations (need by 'formatAmount' filter)
var jobs = [];
jobs.push(csCurrency.name()
.then(function(currency) {
$scope.currency = currency;
return filterTranslations.ready();
}));
// Get children wallets
$scope.defaultWallet = $scope.formData.showDefault ? csWallet : undefined;
if (!$scope.wallets) {
jobs.push(
csWallet.children.all()
.then(function(children) {
$scope.wallets=children;
UIUtils.loading.hide();
if (!$scope.wallets) return; // user cancel, or error
$scope.addListeners();
$scope.showFab('fab-add-wallet');
})
);
}
// Prepare load options
var walletLoadOptions = {
silent: true,
minData: $scope.formData.minData,
sources: $scope.formData.showBalance,
tx: {
enable: false
},
api: true
};
var hasLoadError = false;
var loadCounter = 0;
var now = Date.now();
var balance = 0;
return (jobs.length ? $q.all(jobs) : $q.when())
// Load wallet data (apply a timeout between each wallet)
.then(function() {
var wallets = $scope.formData.showDefault ? [csWallet].concat($scope.wallets) : $scope.wallets;
if (!wallets.length) return;
console.debug("[wallets] Loading {0} wallets...".format(wallets.length));
return wallets.reduce(function(res, wallet) {
var skip= !options.refresh && wallet.isDataLoaded(walletLoadOptions);
if (skip) {
console.debug("[wallets] Wallet #{0} already loaded. Skipping".format(wallet.id));
return res.then(function(){
balance += wallet.data.balance;
$scope.updateWalletView(wallet.id);
});
}
// If already enter
loadCounter++;
return res.then(function() {
if ($scope.formData.stopped) return; // skip if stopped
// Loading next wallet, after waiting some time
$scope.formData.updatingWalletId = wallet.id;
var loadPromise;
if (options.refresh && wallet.data.loaded) {
var refreshOptions = angular.merge({
// Refresh requirements if member account
requirements: (!wallet.data.requirements.loaded || wallet.data.requirements.isMember || wallet.data.requirements.wasMember || wallet.data.requirements.pendingMembership)
}, walletLoadOptions);
loadPromise = wallet.refreshData(refreshOptions);
}
else {
// Re-add listeners
$scope.addListeners();
if ($scope.formData.stopped) {
$scope.loading = false;
$scope.formData.stopped = false;
loadPromise = wallet.loadData(walletLoadOptions);
}
loadPromise.then(function(walletData) {
balance += walletData.balance;
$scope.updateWalletView(wallet.id);
})
.catch(function(err) {
console.error("[wallets] Error while loading data of wallet #{0}".format(wallet.id), err);
hasLoadError = true;
});
return loadPromise;
});
}, $q.when());
})
.then(function() {
if (hasLoadError) {
return UIUtils.alert.error('ERROR.LOAD_WALLET_LIST_FAILED')
.then(function() {
$scope.resetData();
$scope.cancel();
});
}
// Stop
if ($scope.formData.stopped) return;
if (loadCounter) {
console.debug("[wallets] Loaded data of {0} wallet(s) in {1}ms".format(loadCounter, (Date.now() - now)));
}
$scope.formData.balance = balance;
$scope.formData.updatingWalletId = undefined;
$scope.loading = false;
UIUtils.loading.hide();
$scope.updateView();
})
.catch(function(err) {
$scope.resetData();
if (err && err === 'CANCELLED') {
$scope.cancel();
throw err;
}
}
return UIUtils.onError('ERROR.LOAD_WALLET_LIST_FAILED')(err);
});
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.leave = function() {
$scope.formData.stopped = true;
$scope.formData.updatingWalletId = undefined;
$scope.loading = false;
$scope.removeListeners();
$scope.filterFn = function(parameters) {
return function(wallet) {
return !parameters || wallet.id !== parameters.excludedWalletId;
};
};
$scope.$on('$ionicView.leave', $scope.leave);
$scope.cancel = function() {
$scope.showHome();
// Clean controller data
$scope.resetData = function() {
console.debug("[wallets] Cleaning wallet list");
$scope.wallets = null;
$scope.loading = true;
$scope.entered = false;
$scope.formData.balance = undefined;
$scope.formData.updatingWalletId = undefined;
};
$scope.select = function(event, wallet) {
if (event.isDefaultPrevented() || !wallet || $scope.selectPrevented) return;
if (wallet.isDefault()) {
$state.go('app.view_wallet');
$scope.updateView = function(walletId) {
if (!$scope.wallets || !$scope.wallets.length) return;
var selectorSuffix = walletId && (' #wallet-' + walletId) || '';
if ($scope.motion) {
$scope.motion.show({selector: '.list .item.item-wallet' + selectorSuffix, ink: true});
}
else {
$state.go('app.view_wallet_by_id', {id: wallet.id});
UIUtils.ink({selector: '.list .item.item-wallet' + selectorSuffix});
}
event.preventDefault();
};
$scope.editWallet = function(event, wallet) {
event.preventDefault();
$scope.updateWalletView = function(walletId) {
if ($scope.motion) {
$scope.motion.show({selector: '.list #wallet-' + walletId, ink: true});
}
else {
UIUtils.ink({selector: '.list #wallet-' + walletId});
}
};
return $scope.showEditPopup(wallet)
.then(function(newName) {
if (!newName) return;
$scope.doUpdate = function(silent) {
if ($scope.loading || !$scope.wallets || !$scope.wallets.length || $scope.formData.updatingWalletId) return $q.when();
// Auth (if encryption is need)
return (csSettings.data.useLocalStorageEncryption ? csWallet.auth({minData: true}) : $q.when())
$scope.selectPrevented = true;
$timeout(function() {
$scope.selectPrevented = false;
}, 1000);
// Save changes
return $scope.load({silent: silent, refresh: true})
.then(function() {
wallet.data.localName = newName;
csWallet.storeData();
UIUtils.loading.hide();
$scope.updateView();
})
.catch(function(err) {
if (err === 'CANCELLED') {
return UIUtils.loading.hide();
$scope.loading = false;
$scope.selectPrevented = false;
if (silent) {
$scope.$broadcast('$$rebind::rebind'); // force rebind
}
UIUtils.onError('ERROR.SAVE_WALLET_LIST_FAILED')(err);
});
$scope.updateView();
});
};
$scope.downloadAsFile = function() {
if (!$scope.wallets) return; // user cancel
return csWallet.children.downloadFile();
};
$scope.addNewWallet = function(wallet) {
if (!wallet) return $q.reject("Missing 'wallet' argument");
......@@ -216,43 +317,179 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl
success: UIUtils.loading.show,
method: 'PUBKEY' // Default method - fix #767
})
.then(function(walletData) {
if (!walletData) { // User cancelled
.then(function(data) {
if (!data || !data.pubkey) { // User cancelled
UIUtils.loading.hide(100);
return;
}
// Avoid to add main wallet again
if (csWallet.isUserPubkey(walletData.pubkey)) {
UIUtils.loading.hide();
UIUtils.alert.error('ERROR.COULD_NOT_ADD_MAIN_WALLET');
return;
}
// Avoid to add main wallet again
if (csWallet.isUserPubkey(data.pubkey)) {
throw new Error('ERROR.COULD_NOT_ADD_MAIN_WALLET');
}
// Avoid to add exists wallet again
if (csWallet.children.hasPubkey(data.pubkey)) {
throw new Error('ERROR.COULD_NOT_ADD_EXISTING_WALLET');
}
console.debug("[wallet] Adding secondary wallet {{0}}".format(data.pubkey && data.pubkey.substring(0,8)));
// Add the child wallet
return $scope.addNewWallet(wallet)
.then(function() {
UIUtils.loading.hide();
$scope.updateView();
});
})
.catch(UIUtils.onError('ERROR.ADD_SECONDARY_WALLET_FAILED'));
};
/* -- Method to override by subclasses-- */
$scope.addListenersOnWallet = function(wallet) {
// Can be override by subclass
};
$scope.cancel = function() {
console.warn("cancel() must be implement by subclass");
};
$scope.select = function(event, wallet) {
console.warn("select() must be implement by subclass");
};
}
function WalletSelectModalController($scope, $controller, parameters) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('WalletListAbstractCtrl', {$scope: $scope}));
$scope.$on('modal.shown', function() {
$scope.setParameters(parameters);
$scope.load();
});
$scope.cancel = function() {
$scope.closeModal();
};
$scope.select = function(event, wallet) {
if (event.isDefaultPrevented() || !wallet || $scope.selectPrevented) return;
$scope.closeModal(wallet);
};
// Default actions
if (parameters) {
$scope.setParameters(parameters);
}
}
function WalletListViewController($scope, $controller, $state, $timeout, $q, $translate, $ionicPopover, $ionicPopup,
ModalUtils, UIUtils, Modals, csCurrency, csSettings, csWallet){
'ngInject';
$scope.settings = csSettings.data;
$scope.listeners = [];
// Initialize the super class and extend it.
angular.extend(this, $controller('WalletListAbstractCtrl', {$scope: $scope, parameters: {}}));
// Override defaults
$scope.formData.name = undefined;
$scope.motion = UIUtils.motion.default;
$scope.entered = false;
$scope.enter = function(e, state) {
// First enter
if (!$scope.entered) {
$scope.entered = true;
$scope.setParameters({
showDefault: true,
showBalance: true,
minData: false
});
return $scope.load()
.then(function() {
UIUtils.loading.hide();
if (!$scope.wallets) return; // user cancel, or error
$scope.addListeners();
$scope.showFab('fab-add-wallet');
});
}
// If already enter
else {
// Re-add listeners
$scope.addListeners();
if ($scope.formData.stopped) {
$scope.loading = false;
$scope.formData.stopped = false;
$scope.formData.updatingWalletId = undefined;
$scope.updateView();
}
}
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.leave = function() {
$scope.formData.stopped = true;
$scope.formData.updatingWalletId = undefined;
$scope.loading = false;
$scope.removeListeners();
};
$scope.$on('$ionicView.leave', $scope.leave);
$scope.cancel = function() {
$scope.showHome();
};
$scope.select = function(event, wallet) {
if (event.isDefaultPrevented() || !wallet || $scope.selectPrevented) return;
if (wallet.isDefault()) {
$state.go('app.view_wallet');
}
else {
$state.go('app.view_wallet_by_id', {id: wallet.id});
}
event.preventDefault();
};
$scope.editWallet = function(event, wallet) {
// Avoid to add exists wallet again
if (csWallet.children.hasPubkey(walletData.pubkey)) {
UIUtils.loading.hide();
UIUtils.alert.error('ERROR.COULD_NOT_ADD_EXISTING_WALLET');
return;
}
event.preventDefault();
return $scope.showEditPopup(wallet)
.then(function(newName) {
if (!newName) return;
console.debug("[wallet] Adding secondary wallet {"+walletData.pubkey.substring(0,8)+"}");
// Auth (if encryption is need)
return (csSettings.data.useLocalStorageEncryption ? csWallet.auth({minData: true}) : $q.when())
// Add the child wallet
return $scope.addNewWallet(wallet)
// Save changes
.then(function() {
wallet.data.localName = newName;
csWallet.storeData();
UIUtils.loading.hide();
$scope.updateView();
});
})
.catch(function(err) {
if (err === 'CANCELLED') {
// Silent
UIUtils.loading.hide();
return UIUtils.loading.hide();
}
UIUtils.onError('ERROR.SAVE_WALLET_LIST_FAILED')(err);
});
});
};
$scope.downloadAsFile = function() {
if (!$scope.wallets) return; // user cancel
return csWallet.children.downloadFile();
};
$scope.selectAndRemoveWallet = function() {
$scope.hideActionsPopover();
return Modals.showSelectWallet({
......@@ -260,23 +497,21 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl
showDefault: false
})
.then(function(wallet) {
if (!wallet || !wallet.id) return;
if (!wallet || !wallet.id) return; // User has cancelled
// Auth (if encryption is need))
return (csSettings.data.useLocalStorageEncryption ? csWallet.auth({minData: true}) : $q.when())
// Remove the wallet
.then(function() {
csWallet.children.remove(wallet.id);
return csWallet.children.remove(wallet.id);
})
// Refresh UI
.then(function() {
UIUtils.loading.hide();
$scope.updateView();
})
.catch(function(err) {
if (err === 'CANCELLED') {
return UIUtils.loading.hide();
}
UIUtils.onError('ERROR.ADD_SECONDARY_WALLET_FAILED')(err);
});
.catch(UIUtils.onError('ERROR.REMOVE_SECONDARY_WALLET_FAILED'));
});
};
......@@ -284,6 +519,7 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl
$scope.hideActionsPopover();
var loginAndAddWallet = function(authData) {
if (!authData || !authData.pubkey) return $q.reject('Invalid authentication data');
console.debug("[wallet] Adding secondary wallet {"+authData.pubkey.substring(0,8)+"}");
var wallet = csWallet.children.instance();
return wallet.login({
......@@ -305,6 +541,7 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl
'WalletListImportModalCtrl'
)
.then(function(items){
if (!items || !items.length) return; // User cancel
UIUtils.loading.show();
......@@ -342,7 +579,7 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl
return $q(function(resolve, reject) {
$translate(['ACCOUNT.WALLET_LIST.EDIT_POPOVER.TITLE', 'ACCOUNT.WALLET_LIST.EDIT_POPOVER.HELP', 'COMMON.BTN_OK', 'COMMON.BTN_CANCEL'])
.then(function (translations) {
$scope.formData.name = wallet.data.localName || wallet.data.name || wallet.data.uid || wallet.data.pubkey.substring(0, 8);
$scope.formData.name = wallet.data.localName || wallet.data.name || wallet.data.uid || (wallet.data.pubkey && wallet.data.pubkey.substring(0, 8)) || '';
// Choose UID popup
$ionicPopup.show({
......@@ -451,13 +688,13 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl
var inheritedUpdateView = $scope.updateView;
$scope.updateView = function() {
inheritedUpdateView();
$scope.$broadcast('$$rebind::' + 'rebind'); // force rebind
$scope.$broadcast('$$rebind::rebind'); // force rebind
};
var inheritedUpdateWalletView = $scope.updateWalletView;
$scope.updateWalletView = function(walletId) {
inheritedUpdateWalletView(walletId);
$scope.$broadcast('$$rebind::' + 'rebind'); // force rebind
$scope.$broadcast('$$rebind::rebind'); // force rebind
};
// Detect changes in settings useRelative
......@@ -468,233 +705,19 @@ function WalletListController($scope, $controller, $state, $timeout, $q, $transl
}, true);
}
function WalletSelectModalController($scope, $q, $timeout, UIUtils, filterTranslations, csSettings, csCurrency, csWallet, parameters){
'ngInject';
$scope.loading = true;
$scope.wallets = null;
$scope.formData = {
useRelative: csSettings.data.useRelative,
showDefault: true,
showBalance: false,
balance: undefined,
updatingWalletId: undefined,
stopped: false,
minData: true
};
$scope.motion = null; // no animation
$scope.setParameters = function(parameters) {
parameters = parameters || {};
$scope.formData.useRelative = angular.isDefined(parameters.useRelative) ? parameters.useRelative : $scope.formData.useRelative;
$scope.formData.showDefault = angular.isDefined(parameters.showDefault) ? parameters.showDefault : $scope.formData.showDefault;
$scope.formData.showBalance = angular.isDefined(parameters.showBalance) ? parameters.showBalance : $scope.formData.showBalance;
$scope.formData.minData = angular.isDefined(parameters.minData) ? parameters.minData : $scope.formData.minData;
};
$scope.load = function(options) {
options = options || {};
$scope.loading = angular.isUndefined(options.silent) || !options.silent;
$scope.formData.balance = undefined;
$scope.formData.updatingWalletId = undefined;
$scope.formData.stopped = false;
// Load currency, and filter translations (need by 'formatAmount' filter)
var jobs = [];
jobs.push(csCurrency.name()
.then(function(currency) {
$scope.currency = currency;
return filterTranslations.ready();
}));
// Get children wallets
$scope.defaultWallet = $scope.formData.showDefault ? csWallet : undefined;
if (!$scope.wallets) {
jobs.push(
csWallet.children.all()
.then(function(children) {
$scope.wallets=children;
UIUtils.loading.hide();
})
);
}
// Prepare load options
var walletLoadOptions = {
silent: true,
minData: $scope.formData.minData,
sources: $scope.formData.showBalance,
tx: {
enable: false
},
api: true
};
var hasLoadError = false;
var loadCounter = 0;
var now = Date.now();
var balance = 0;
return (jobs.length ? $q.all(jobs) : $q.when())
// Load wallet data (apply a timeout between each wallet)
.then(function() {
var wallets = $scope.formData.showDefault ? [csWallet].concat($scope.wallets) : $scope.wallets;
if (!wallets.length) return;
console.debug("[wallets] Loading {0} wallets...".format(wallets.length));
return wallets.reduce(function(res, wallet) {
var skip= !options.refresh && wallet.isDataLoaded(walletLoadOptions);
if (skip) {
console.debug("[wallets] Wallet #{0} already loaded. Skipping".format(wallet.id));
return res.then(function(){
balance += wallet.data.balance;
$scope.updateWalletView(wallet.id);
});
}
loadCounter++;
return res.then(function() {
if ($scope.formData.stopped) return; // skip if stopped
// Loading next wallet, after waiting some time
$scope.formData.updatingWalletId = wallet.id;
var loadPromise;
if (options.refresh && wallet.data.loaded) {
var refreshOptions = angular.merge({
// Refresh requirements if member account
requirements: (!wallet.data.requirements.loaded || wallet.data.requirements.isMember || wallet.data.requirements.wasMember || wallet.data.requirements.pendingMembership)
}, walletLoadOptions);
loadPromise = wallet.refreshData(refreshOptions);
}
else {
loadPromise = wallet.loadData(walletLoadOptions);
}
loadPromise.then(function(walletData) {
balance += walletData.balance;
$scope.updateWalletView(wallet.id);
})
.catch(function(err) {
console.error("[wallets] Error while loading data of wallet #{0}".format(wallet.id), err);
hasLoadError = true;
});
return loadPromise;
});
}, $q.when());
})
.then(function() {
if (hasLoadError) {
return UIUtils.alert.error('ERROR.LOAD_WALLET_LIST_FAILED')
.then(function() {
$scope.resetData();
$scope.cancel();
});
}
// Stop
if ($scope.formData.stopped) return;
if (loadCounter) {
console.debug("[wallets] Loaded data of {0} wallet(s) in {1}ms".format(loadCounter, (Date.now() - now)));
}
$scope.formData.balance = balance;
$scope.formData.updatingWalletId = undefined;
$scope.loading = false;
UIUtils.loading.hide();
$scope.updateView();
})
.catch(function(err) {
$scope.resetData();
if (err && err === 'CANCELLED') {
$scope.cancel();
throw err;
}
return UIUtils.onError('ERROR.LOAD_WALLET_LIST_FAILED')(err);
});
};
$scope.$on('modal.shown', function() {
$scope.load();
});
$scope.cancel = function() {
$scope.closeModal();
};
$scope.select = function(event, wallet) {
if (event.isDefaultPrevented() || !wallet || $scope.selectPrevented) return;
$scope.closeModal(wallet);
};
// Clean controller data
$scope.resetData = function() {
console.debug("[wallets] Cleaning wallet list");
$scope.wallets = null;
$scope.loading = true;
$scope.entered = false;
$scope.formData.balance = undefined;
$scope.formData.updatingWalletId = undefined;
};
$scope.updateView = function(walletId) {
if (!$scope.wallets || !$scope.wallets.length) return;
var selectorSuffix = walletId && (' #wallet-' + walletId) || '';
if ($scope.motion) {
$scope.motion.show({selector: '.list .item.item-wallet' + selectorSuffix, ink: true});
}
else {
UIUtils.ink({selector: '.list .item.item-wallet' + selectorSuffix});
}
};
$scope.updateWalletView = function(walletId) {
if ($scope.motion) {
$scope.motion.show({selector: '.list #wallet-' + walletId, ink: true});
}
else {
UIUtils.ink({selector: '.list #wallet-' + walletId});
}
};
$scope.doUpdate = function(silent, event) {
if ($scope.loading || !$scope.wallets || !$scope.wallets.length || $scope.formData.updatingWalletId) return $q.when();
$scope.selectPrevented = true;
$timeout(function() {
$scope.selectPrevented = false;
}, 1000);
return $scope.load({silent: silent, refresh: true})
.then(function() {
$scope.loading = false;
$scope.selectPrevented = false;
if (silent) {
$scope.$broadcast('$$rebind::' + 'rebind'); // force rebind
}
$scope.updateView();
});
};
// Default actions
$scope.setParameters(parameters);
}
function PopoverWalletSelectModalController($scope, $controller, UIUtils) {
function WalletSelectPopoverController($scope, $controller, UIUtils, parameters) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('WalletSelectModalCtrl', {$scope: $scope, parameters: {
showDefault: true,
showBalance: false
}}));
angular.extend(this, $controller('WalletListAbstractCtrl', {$scope: $scope}));
// Disable list motion
$scope.motion = null;
$scope.$on('popover.shown', function() {
if ($scope.loading) {
$scope.setParameters(parameters);
$scope.load();
}
});
......@@ -703,12 +726,14 @@ function PopoverWalletSelectModalController($scope, $controller, UIUtils) {
if (!$scope.wallets || !$scope.wallets.length) return;
UIUtils.ink({selector: '.popover-wallets .list .item'});
$scope.$broadcast('$$rebind::rebind'); // force rebind
};
$scope.select = function(event, wallet) {
if (event.isDefaultPrevented() || !wallet || $scope.selectPrevented) return; // no selection
$scope.closePopover(wallet);
};
}
function WalletListImportModalController($scope, $timeout, BMA, csWallet) {
......@@ -720,13 +745,13 @@ function WalletListImportModalController($scope, $timeout, BMA, csWallet) {
$scope.isValidFile = false;
$scope.validatingFile = false;
$scope.importFromFile = function(file) {
$scope.onFileChanged = function(file) {
$scope.validatingFile = true;
$scope.hasContent = angular.isDefined(file) && file !== '';
$scope.fileData = file.fileData ? file.fileData : '';
var isValidFile = $scope.fileData !== '' &&
($scope.fileData.type == 'text/csv' || $scope.fileData.type == 'text/plain' || 'application/vnd.ms-excel' /*fix issue #810*/);
($scope.fileData.type === 'text/csv' || $scope.fileData.type === 'text/plain' || 'application/vnd.ms-excel' /*fix issue #810*/);
// Bad file type: invalid file
if (!isValidFile) {
......
......@@ -43,7 +43,7 @@ angular.module('cesium.wot.controllers', ['cesium.services'])
})
.state('app.wot_identity', {
url: "/wot/:pubkey/:uid?action&block",
url: "/wot/:pubkey/:uid?action&block&amount&comment&udAmount&restPub&wallet",
views: {
'menuContent': {
templateUrl: "templates/wot/view_identity.html",
......@@ -133,7 +133,7 @@ angular.module('cesium.wot.controllers', ['cesium.services'])
}
})
.state('app.wallet_cert_by_id_lg', {
.state('app.wallet_cert_lg_by_id', {
url: "/wallets/:id/cert/lg",
views: {
'menuContent': {
......@@ -157,12 +157,14 @@ angular.module('cesium.wot.controllers', ['cesium.services'])
.controller('WotCertificationsViewCtrl', WotCertificationsViewController)
.controller('WotCertificationChecklistCtrl', WotCertificationChecklistController)
.controller('WotSelectPubkeyIdentityModalCtrl', WotSelectPubkeyIdentityModalController)
;
function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $ionicPopover, $ionicHistory,
UIUtils, csConfig, csCurrency, csSettings, Device, BMA, csWallet, csWot) {
UIUtils, csConfig, csCurrency, csSettings, Device, BMA, csWallet, csWot, csCrypto) {
'ngInject';
var defaultSearchLimit = 10;
......@@ -204,13 +206,13 @@ function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $i
$scope.doGetPending(0, undefined, true/*skipLocationUpdate*/);
}
// get new comers
else if (params.type == 'newcomers' || (!csConfig.initPhase && !params.type)) {
else if (params.type === 'newcomers' || (!csConfig.initPhase && !params.type)) {
$scope.doGetNewcomers(0, undefined, true/*skipLocationUpdate*/);
}
else if (params.type == 'pending') {
else if (params.type === 'pending') {
$scope.doGetPending(0, undefined, true/*skipLocationUpdate*/);
}
else if (params.type == 'wallets') {
else if (params.type === 'wallets') {
$scope.doGetWallets(0, undefined, true/*skipLocationUpdate*/);
}
......@@ -254,7 +256,7 @@ function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $i
type: undefined
};
if ($scope.search.type == 'text') {
if ($scope.search.type === 'text') {
var text = $scope.search.text.trim();
if (text.match(/^#[\wḡĞǦğàáâãäåçèéêëìíîïðòóôõöùúûüýÿ]+$/)) {
stateParams.hash = text.substr(1);
......@@ -263,7 +265,7 @@ function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $i
stateParams.q = text;
}
}
else if ($scope.search.type != 'last') {
else if ($scope.search.type !== 'last') {
stateParams.type = $scope.search.type;
}
......@@ -283,21 +285,41 @@ function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $i
};
$scope.doSearch = function() {
$scope.search.loading = true;
var text = $scope.search.text.trim();
if ((UIUtils.screen.isSmall() && text.length < 3) || !text.length) {
$scope.search.results = undefined;
$scope.search.loading = false;
$scope.search.type = 'none';
$scope.search.total = undefined;
return $q.when();
}
$scope.search.loading = true;
$scope.search.type = 'text';
// If checksum is correct, search on simple pubkey
var pubkeyWithCk;
if (BMA.regexp.PUBKEY_WITH_CHECKSUM.test(text)) {
console.debug("[wot] Validating pubkey checksum... ");
var matches = BMA.regexp.PUBKEY_WITH_CHECKSUM.exec(text);
// DEBUG only
//console.debug(matches)
pubkey = matches[1];
var checksum = matches[2];
var expectedChecksum = csCrypto.util.pkChecksum(pubkey);
if (checksum === expectedChecksum) {
console.debug("[wot] checksum {" + checksum + "} valid for pubkey {" + pubkey + "}");
text = pubkey;
pubkeyWithCk = pubkey + ':' + checksum;
}
}
return csWot.search(text)
.then(function(idties){
if ($scope.search.type != 'text') return; // could have change
if ($scope.search.text.trim() !== text) return; // search text has changed before received response
if ($scope.search.type !== 'text') return; // could have change
originText = $scope.search.text.trim();
if (originText !== text && originText !== pubkeyWithCk) return; // search text has changed before received response
if ((!idties || !idties.length) && (BMA.regexp.PUBKEY.test(text) || BMA.regexp.PUBKEY_WITH_CHECKSUM.test(text))) {
return BMA.uri.parse(text)
......@@ -334,7 +356,7 @@ function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $i
return csWot.newcomers(offset, size)
.then(function(res){
if ($scope.search.type != 'newcomers') return false; // could have change
if ($scope.search.type !== 'newcomers') return false; // could have change
$scope.doDisplayResult(res && res.hits, offset, size, res && res.total);
return true;
})
......@@ -367,7 +389,7 @@ function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $i
return searchFunction(offset, size)
.then(function(res){
if ($scope.search.type != 'pending') return false; // could have change
if ($scope.search.type !== 'pending') return false; // could have change
$scope.doDisplayResult(res && res.hits, offset, size, res && res.total);
// Always disable "more" on initphase
$scope.search.hasMore = !csCurrency.data.initPhase && $scope.search.hasMore;
......@@ -398,7 +420,7 @@ function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $i
return csWallet.children.all()
.then(function(children) {
if (!children || $scope.search.type != 'wallets') return false; // could have change
if (!children || $scope.search.type !== 'wallets') return false; // could have change
var res = [csWallet].concat(children).reduce(function(res, wallet, index) {
var item = {
id: index,
......@@ -422,7 +444,7 @@ function WotLookupController($scope, $state, $q, $timeout, $focus, $location, $i
var offset = $scope.search.results ? $scope.search.results.length : 0;
$scope.search.loadingMore = true;
var searchFunction = ($scope.search.type == 'newcomers') ?
var searchFunction = ($scope.search.type === 'newcomers') ?
$scope.doGetNewcomers :
$scope.doGetPending;
......@@ -672,11 +694,16 @@ function WotLookupModalController($scope, $controller, $focus, csWallet, paramet
/**
* Abtract controller that load identity, that expose some useful methods in $scope, like 'certify()'
* @param $scope
* @param $rootScope
* @param $state
* @param $timeout
* @param $translate
* @param $ionicHistory
* @param $q
* @param UIUtils
* @param Modals
* @param csConfig
* @param csSettings
* @param csCurrency
* @param csWot
* @param csWallet
* @constructor
......@@ -756,43 +783,14 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
return;
}
// Check identity not expired
if ($scope.formData.requirements.expired) {
UIUtils.alert.error('ERROR.IDENTITY_EXPIRED');
return;
}
// Check not already certified
var previousCert = _.find($scope.formData.received_cert, function(cert) {
return cert.pubkey === wallet.data.pubkey && cert.valid && cert.expiresIn > csSettings.data.timeWarningExpire;
});
if (previousCert) {
$translate('ERROR.IDENTITY_ALREADY_CERTIFY', previousCert)
.then(function(message) {
UIUtils.alert.error(message, 'ERROR.UNABLE_TO_CERTIFY_TITLE');
});
return;
}
// Check no pending certification
previousCert = _.findWhere($scope.formData.received_cert_pending, { pubkey: wallet.data.pubkey, valid: true});
if (previousCert) {
$translate('ERROR.IDENTITY_ALREADY_CERTIFY_PENDING', previousCert)
.then(function(message) {
UIUtils.alert.error(message, 'ERROR.UNABLE_TO_CERTIFY_TITLE');
});
if (!$scope.commonCertificationVerifications($scope.formData, wallet)) {
return;
}
UIUtils.alert.confirm('CONFIRM.CERTIFY_RULES', 'CONFIRM.POPUP_SECURITY_WARNING_TITLE', {
cssClass: 'warning',
okText: 'WOT.BTN_YES_CERTIFY',
okType: 'button-assertive'
return $scope.displayConfirmationModalOrLicenseQuestions($scope.formData, wallet);
})
.then(function(confirm){
if (!confirm) {
return;
}
if (!confirm) return;
UIUtils.loading.show();
wallet.certify($scope.formData.uid,
$scope.formData.pubkey,
......@@ -812,7 +810,6 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
}
})
.catch(UIUtils.onError('ERROR.SEND_CERTIFICATION_FAILED'));
});
})
.catch(function(err) {
if (err === 'CANCELLED') return;
......@@ -821,6 +818,89 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
});
};
$scope.commonCertificationVerifications = function(identity, wallet) {
// Check it is no self-certification
if (identity.pubkey === wallet.data.pubkey) {
UIUtils.alert.error('ERROR.SELF_CERTIFICATION');
return false;
}
// Check identity not expired
if (identity.requirements.expired) {
UIUtils.alert.error('ERROR.IDENTITY_EXPIRED');
return false;
}
// Check not already certified
var previousCert = _.find(identity.received_cert, function (cert) {
return cert.pubkey === wallet.data.pubkey && cert.valid && cert.expiresIn > csSettings.data.timeWarningExpire;
});
if (previousCert) {
$translate('ERROR.IDENTITY_ALREADY_CERTIFY', previousCert)
.then(function (message) {
UIUtils.alert.error(message, 'ERROR.UNABLE_TO_CERTIFY_TITLE');
});
return false;
}
// Check no pending certification
previousCert = _.findWhere(identity.received_cert_pending, { pubkey: wallet.data.pubkey, valid: true });
if (previousCert) {
$translate('ERROR.IDENTITY_ALREADY_CERTIFY_PENDING', previousCert)
.then(function (message) {
UIUtils.alert.error(message, 'ERROR.UNABLE_TO_CERTIFY_TITLE');
});
return false;
}
return true;
};
$scope.showLicenseReminder = function() {
return UIUtils.alert.confirm(
'ACCOUNT.CERTIFICATION_MODAL.SHORT_LICENSE_REMINDER',
'ACCOUNT.CERTIFICATION_MODAL.REMINDER_TITLE',
{
cssClass: 'confirm large',
okText: 'COMMON.BTN_OK',
okType: 'button-positive'
}
);
};
$scope.displayConfirmationModalOrLicenseQuestions = function(identity, wallet) {
// Renew: simple confirmation modal
if (isCertificationRenewal(identity.received_cert, wallet.data.pubkey)) {
return $scope.certRenewalConfirmationModal();
}
// New certification: show questions modal
return Modals.showCertificationCheckList({
identity: identity
})
.then(function(confirm) {
if (!confirm) return false;
// Show license reminder modal
return $scope.showLicenseReminder();
})
.catch(function(err) {
if (err === 'CANCELLED') return false;
UIUtils.onError('ACCOUNT.CERTIFICATION_MODAL.CHECKLIST_CONDITIONS_NOT_MET')(err);
return false;
});
};
$scope.certRenewalConfirmationModal = function() {
return UIUtils.alert.confirm('CONFIRM.CERTIFY_RULES', 'CONFIRM.CERTIFY_RULES_TITLE_UID', {
cssClass: 'warning',
okText: 'WOT.BTN_YES_CERTIFY',
okType: 'button-assertive'
});
};
// Select an identity and certify
$scope.selectAndCertify = function() {
......@@ -839,21 +919,19 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
// Open Wot lookup modal
return Modals.showWotLookup();
})
.then(function (idty) {
if (!idty || !idty.pubkey) {
return; // cancelled
}
if (!idty.uid) { // not a member
.then(function(identity) {
if (!identity || !identity.pubkey) return; // cancelled
if (!identity.uid) { // not a member
UIUtils.alert.error('ERROR.IDENTITY_TO_CERTIFY_HAS_NO_SELF');
return;
}
UIUtils.loading.show();
var options = {cache: false, blockUid: idty.blockUid};
var options = {cache: false, blockUid: identity.blockUid};
// load selected identity
return csWot.load(idty.pubkey, idty.uid, options);
return csWot.load(identity.pubkey, identity.uid, options);
})
.then(function (identity) {
......@@ -864,43 +942,20 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
return;
}
// Check identity not expired
if (identity.requirements.expired) {
UIUtils.alert.error('ERROR.IDENTITY_EXPIRED');
return;
}
// Check not already certified
var previousCert = _.findWhere(identity.received_cert, {pubkey: wallet.data.pubkey, valid: true});
if (previousCert) {
$translate('ERROR.IDENTITY_ALREADY_CERTIFY', previousCert)
.then(function (message) {
UIUtils.alert.error(message, 'ERROR.UNABLE_TO_CERTIFY_TITLE');
});
if (!$scope.commonCertificationVerifications(identity, wallet)) {
return;
}
// Check not pending certification
previousCert = _.findWhere(identity.received_cert_pending, {pubkey: wallet.data.pubkey, valid: true});
if (previousCert) {
$translate('ERROR.IDENTITY_ALREADY_CERTIFY_PENDING', previousCert)
.then(function (message) {
UIUtils.alert.error(message, 'ERROR.UNABLE_TO_CERTIFY_TITLE');
// Prepare actions after user confirmation
return $scope.displayConfirmationModalOrLicenseQuestions(identity, wallet)
.then(function(confirm) {
if (!confirm) return;
return identity;
});
return;
}
// Ask confirmation
$translate('CONFIRM.CERTIFY_RULES_TITLE_UID', {uid: identity.uid})
.then(function (confirmTitle) {
return UIUtils.alert.confirm('CONFIRM.CERTIFY_RULES', confirmTitle);
})
.then(function (confirm) {
if (!confirm) {
return;
}
.then(function(identity){
if (!identity) return;
UIUtils.loading.show();
// Send certification
wallet.certify(identity.uid,
identity.pubkey,
......@@ -925,7 +980,6 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
if (err === 'CANCELLED') return;
UIUtils.onError('ERROR.LOAD_IDENTITY_FAILED')(err);
});
});
};
// Add wallet's data to a new cert
......@@ -944,6 +998,8 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
// Reset action param
stateParams.action = null;
stateParams.amount = null;
stateParams.comment = null;
// Update location href
$ionicHistory.nextViewOptions({
......@@ -960,10 +1016,10 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
};
$scope.doAction = function(action, options) {
if (action == 'certify') {
if (action === 'certify') {
return $scope.certify();
}
if (action == 'transfer') {
if (action === 'transfer') {
$scope.showTransferModal(options);
}
};
......@@ -1032,24 +1088,47 @@ function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $
/**
* Identity view controller - should extend WotIdentityAbstractCtrl
*/
function WotIdentityViewController($scope, $rootScope, $controller, $timeout, $state, UIUtils, Modals, csWallet) {
function WotIdentityViewController($scope, $rootScope, $controller, $timeout, $state, UIUtils, Modals) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('WotIdentityAbstractCtrl', {$scope: $scope}));
$scope.motion = UIUtils.motion.fadeSlideInRight;
$scope.qrcodeId = 'qrcode-wot-' + $scope.$id;
// Init likes here, to be able to use in extension
$scope.options = $scope.options || {};
$scope.options.like = {
kinds: ['LIKE', 'ABUSE'],
index: 'user',
type: 'profile'
};
$scope.likeData = {
likes: {},
abuses: {}
};
$scope.$on('$ionicView.enter', function(e, state) {
var onLoadSuccess = function() {
$scope.doMotion();
var doAction = function() {
if (state.stateParams && state.stateParams.action) {
$timeout(function() {
$scope.doAction(state.stateParams.action.trim());
$scope.doAction(state.stateParams.action.trim(), state.stateParams);
}, 100);
}
$scope.removeActionParamInLocationHref(state);
}
};
var onLoadSuccess = function() {
$scope.doMotion();
doAction();
// Need by like controller
$scope.likeData.id = $scope.formData.pubkey;
$scope.showQRCode();
};
var options = {
cache: true,
......@@ -1059,10 +1138,16 @@ function WotIdentityViewController($scope, $rootScope, $controller, $timeout, $s
if (state.stateParams &&
state.stateParams.pubkey &&
state.stateParams.pubkey.trim().length > 0) {
if ($scope.loading) { // load once
// First time: load identity data
if ($scope.loading) {
return $scope.load(state.stateParams.pubkey.trim(), state.stateParams.uid, options)
.then(onLoadSuccess);
.then(onLoadSuccess)
.catch(UIUtils.onError("ERROR.LOAD_IDENTITY_FAILED"));
}
// Do action
else {
doAction();
}
}
......@@ -1073,14 +1158,8 @@ function WotIdentityViewController($scope, $rootScope, $controller, $timeout, $s
return $scope.load(null, state.stateParams.uid, options)
.then(onLoadSuccess);
}
}
// Load from wallet pubkey
else if (csWallet.isLogin()){
if ($scope.loading) {
return $scope.load(csWallet.data.pubkey, csWallet.data.uid, options)
.then(onLoadSuccess);
else {
doAction();
}
}
......@@ -1127,6 +1206,32 @@ function WotIdentityViewController($scope, $rootScope, $controller, $timeout, $s
});
});
};
$scope.showQRCode = function(timeout) {
if (!$scope.qrcodeId || !$scope.formData.pubkey) return; // Skip
// Get the DIV element
var element = angular.element(document.querySelector('#' + $scope.qrcodeId + ' .content'));
if (!element) {
console.error("[wot-controller] Cannot found div #{0} for the QRCode. Skipping.".format($scope.qrcodeId));
return;
}
console.debug("[wot-controller] Generating QR code for identity...");
$timeout(function() {
var svg = UIUtils.qrcode.svg($scope.formData.pubkey);
element.html(svg);
UIUtils.motion.toggleOn({selector: '#'+$scope.qrcodeId}, timeout || 1100);
});
};
$scope.hideQRCode = function() {
if (!$scope.qrcodeId) return;
var element = angular.element(document.querySelector('#' + $scope.qrcodeId));
if (element) {
UIUtils.motion.toggleOff({selector: '#'+$scope.qrcodeId});
}
};
}
/**
......@@ -1180,15 +1285,27 @@ function WotIdentityTxViewController($scope, $timeout, $q, BMA, csSettings, csWo
// Update view
$scope.updateView = function() {
$scope.$broadcast('$$rebind::' + 'balance'); // force rebind balance
$scope.$broadcast('$$rebind::' + 'rebind'); // force rebind
$scope.$broadcast('$$rebind::balance'); // force rebind balance
$scope.$broadcast('$$rebind::rebind'); // force rebind
$scope.motion.show();
};
$scope.downloadHistoryFile = function(options) {
options = options || {};
options.fromTime = options.fromTime || -1; // default: full history
csTx.downloadHistoryFile($scope.pubkey, options);
UIUtils.loading.show();
UIUtils.toast.show('INFO.DOWNLOADING_DOTS');
return csTx.downloadHistoryFile($scope.pubkey, options)
.then(UIUtils.toast.hide)
.then(function() {
UIUtils.toast.show('INFO.FILE_DOWNLOADED', 1000);
})
.catch(function(err){
if (err && err === 'CANCELLED') return;
UIUtils.onError('ERROR.DOWNLOAD_TX_HISTORY_FAILED')(err);
});
};
$scope.showMoreTx = function(fromTime) {
......@@ -1206,7 +1323,7 @@ function WotIdentityTxViewController($scope, $timeout, $q, BMA, csSettings, csWo
})
.catch(function(err) {
// If http rest limitation: wait then retry
if (err.ucode == BMA.errorCodes.HTTP_LIMITATION) {
if (err.ucode === BMA.errorCodes.HTTP_LIMITATION) {
$timeout(function() {
return $scope.showMoreTx(fromTime);
}, 2000);
......@@ -1217,6 +1334,25 @@ function WotIdentityTxViewController($scope, $timeout, $q, BMA, csSettings, csWo
});
};
/* -- popover -- */
$scope.showActionsPopover = function(event) {
UIUtils.popover.show(event, {
templateUrl: 'templates/wot/popover_tx_actions.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.actionsPopover = popover;
}
});
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
$scope.actionsPopover = null;
}
};
}
......@@ -1243,8 +1379,8 @@ function WotCertificationsViewController($scope, $rootScope, $controller, csSett
$scope.$on('$ionicView.enter', function(e, state) {
if (state.stateParams && state.stateParams.type) {
$scope.motions.receivedCertifications.enable = (state.stateParams.type != 'given');
$scope.motions.givenCertifications.enable = (state.stateParams.type == 'given');
$scope.motions.receivedCertifications.enable = (state.stateParams.type !== 'given');
$scope.motions.givenCertifications.enable = (state.stateParams.type === 'given');
$scope.motions.avatar.enable = false;
}
......@@ -1392,6 +1528,75 @@ function WotCertificationsViewController($scope, $rootScope, $controller, csSett
};
}
/**
* Certification checklist controller
* @param $scope
* @param $controller
* @param parameters
*/
function WotCertificationChecklistController($scope, $controller, parameters){
// allow to display license
$controller('CurrencyViewCtrl', {$scope: $scope});
$scope.identity = parameters.identity;
$scope.prepare_cert_checklist = function() {
var original_cert_checklist = [
{
question: 'ACCOUNT.CERTIFICATION_MODAL.QUESTIONS.WELL_KNOWN',
expected_answer: true,
answer: false
},
{
question: 'ACCOUNT.CERTIFICATION_MODAL.QUESTIONS.REVOCATION',
expected_answer: true,
answer: false
},
{
question: 'ACCOUNT.CERTIFICATION_MODAL.QUESTIONS.CONTACT',
expected_answer: true,
answer: false
},
{
question: 'ACCOUNT.CERTIFICATION_MODAL.QUESTIONS.MASTER_ACCOUNT',
expected_answer: true,
answer: false
},
{
question: 'ACCOUNT.CERTIFICATION_MODAL.QUESTIONS.LICENSE',
expected_answer: true,
answer: false
},
{
question: 'ACCOUNT.CERTIFICATION_MODAL.QUESTIONS.CREDENTIALS',
expected_answer: true,
answer: false
},
// questions with negative answers
{
question: 'ACCOUNT.CERTIFICATION_MODAL.QUESTIONS.DOUBLE_IDENTITY',
expected_answer: false,
answer: false
},
{
question: 'ACCOUNT.CERTIFICATION_MODAL.QUESTIONS.PUBLIC_KEY_DIFFERENT',
expected_answer: false,
answer: false
},
];
return shuffle(original_cert_checklist).slice(0, 5);
};
$scope.cert_checklist = $scope.prepare_cert_checklist();
$scope.verifyAnswers = function() {
var ok = _.every($scope.cert_checklist, function(question) {
return (question.answer === question.expected_answer);
});
$scope.closeModal(ok);
};
}
/**
* Select identities from a pubkey (useful when many self on the same pubkey)
......@@ -1436,3 +1641,19 @@ function WotSelectPubkeyIdentityModalController($scope, $q, csWot, parameters) {
};
$scope.$on('modal.shown', $scope.load);
}
function isCertificationRenewal(identity_current_certs, certifier_pubkey) {
return _.find(identity_current_certs, function(certification) {
return certification.pubkey === certifier_pubkey;
});
}
// Fisher-Yates shuffle
function shuffle(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i
var t = array[i]; array[i] = array[j]; array[j] = t;
}
return array;
}
......@@ -121,26 +121,40 @@ angular.module('cesium.directives', [])
link: function (scope, element, attrs) {
var showCopyPopover = function (event) {
var value = attrs.copyOnClick;
if (value && Device.clipboard.enable) {
// copy to clipboard
if (value === undefined || value === null) return; // Skip if no value
if (Device.clipboard.enable) {
// copy to clipboard, using cordova
Device.clipboard.copy(value)
.then(function(){
UIUtils.toast.show('INFO.COPY_TO_CLIPBOARD_DONE');
})
.catch(UIUtils.onError('ERROR.COPY_CLIPBOARD'));
}
else if (value) {
else {
var rows = value && value.indexOf('\n') >= 0 ? value.split('\n').length : 1;
UIUtils.popover.show(event, {
scope: scope,
templateUrl: 'templates/common/popover_copy.html',
bindings: {
value: attrs.copyOnClick,
rows: rows
rows: rows,
copied: false
},
autoselect: '.popover-copy ' + (rows <= 1 ? 'input' : 'textarea')
autoselect: '.popover-copy ' + (rows <= 1 ? 'input' : 'textarea'),
// After popover, try to copy the selection
afterShow: document.execCommand ? function(popover) {
try {
document.execCommand("copy");
UIUtils.toast.show('INFO.COPY_TO_CLIPBOARD_DONE', 1000);
} catch (err) {
console.error("[copy-on-click] Failed to copy using document.execCommand('copy')", err);
}
} : undefined
});
}
};
element.bind('click', showCopyPopover);
element.bind('hold', showCopyPopover);
......@@ -199,6 +213,7 @@ angular.module('cesium.directives', [])
// All this does is allow the message
// to be sent when you tap return
.directive('input', function($timeout) {
'ngInject';
return {
restrict: 'E',
scope: {
......@@ -236,7 +251,8 @@ angular.module('cesium.directives', [])
};
})
.directive('trustAsHtml', ['$sce', '$compile', '$parse', function($sce, $compile, $parse){
.directive('trustAsHtml', function($sce, $compile, $parse){
'ngInject';
return {
restrict: 'A',
compile: function (tElement, tAttrs) {
......@@ -258,12 +274,14 @@ angular.module('cesium.directives', [])
};
}
};
}])
})
/**
* Close the current modal
*/
.directive('modalClose', ['$ionicHistory', '$timeout', function($ionicHistory, $timeout) {
.directive('modalClose', function($ionicHistory, $timeout) {
'ngInject';
return {
restrict: 'AC',
link: function($scope, $element) {
......@@ -288,12 +306,14 @@ angular.module('cesium.directives', [])
});
}
};
}])
})
/**
* Plugin extension point (see services/plugin-services.js)
*/
.directive('csExtensionPoint', function ($state, $compile, $controller, $templateCache, PluginService) {
'ngInject';
var getTemplate = function(extensionPoint) {
var template = extensionPoint.templateUrl ? $templateCache.get(extensionPoint.templateUrl) : extensionPoint.template;
if (!template) {
......@@ -339,6 +359,8 @@ angular.module('cesium.directives', [])
})
.directive('onReadFile', function ($parse) {
'ngInject';
return {
restrict: 'A',
scope: false,
......@@ -368,12 +390,13 @@ angular.module('cesium.directives', [])
};
})
.directive("dropzone", function($parse) {
.directive("dropZone", function($parse) {
'ngInject';
return {
restrict: 'A',
scope: false,
link: function(scope, elem, attrs) {
var fn = $parse(attrs.dropzone);
var fn = $parse(attrs.dropZone);
elem.bind('dragover', function (e) {
e.stopPropagation();
e.preventDefault();
......@@ -389,10 +412,11 @@ angular.module('cesium.directives', [])
elem.bind('drop', function(e) {
e.stopPropagation();
e.preventDefault();
var file = e.dataTransfer.files[0];
var fileData = {
name: e.dataTransfer.files[0].name,
size: e.dataTransfer.files[0].size,
type: e.dataTransfer.files[0].type
name: file.name,
size: file.size,
type: file.type
};
var reader = new FileReader();
......@@ -400,6 +424,7 @@ angular.module('cesium.directives', [])
scope.$apply(function () {
fn(scope, {
file: {
file: file,
fileContent: onLoadEvent.target.result,
fileData : fileData}
});
......@@ -411,9 +436,65 @@ angular.module('cesium.directives', [])
};
})
// See http://embed.plnkr.co/2vgnFe/
.directive('fileSelect', function($parse) {
'ngInject';
return {
restrict: 'A',
scope: false,
template: '<input type="file" style="display: none;" />' +
'<ng-transclude></ng-transclude>',
transclude: true,
link: function (scope, element, attrs) {
var fn = $parse(attrs.fileSelect);
var fileInput = element.children('input[file]');
if (attrs.accept) {
fileInput[0].accept = attrs.accept;
}
fileInput.on('change', function (onChangeEvent) {
var reader = new FileReader();
var file = this.files[0];
var fileData = {
name: file.name,
size: file.size,
type: file.type
};
reader.onload = function(onLoadEvent) {
scope.$applyAsync(function() {
fn(scope, {
file: {
file: file,
fileContent: onLoadEvent.target.result,
fileData : fileData}
});
// Reset the input file
fileInput[0].value = '';
});
};
reader.readAsText((onChangeEvent.srcElement || onChangeEvent.target).files[0]);
});
element.on('click', function () {
fileInput[0].click();
});
}
};
})
// Un-authenticate when window closed
// see: https://stackoverflow.com/questions/28197316/javascript-or-angularjs-defer-browser-close-or-tab-close-between-refresh
.directive('windowExitUnauth', function($window, csSettings, csWallet) {
'ngInject';
return {
restrict: 'AE',
link: function(element, attrs){
......@@ -428,4 +509,33 @@ angular.module('cesium.directives', [])
}
};
})
;
// Jdenticon directive (to generate icon from a string)
// see:
.directive('jdenticon', function($parse) {
'ngInject';
return {
restrict: 'A',
scope: false,
template: '<canvas></canvas>',
transclude: false,
link: function (scope, element, attrs) {
var value = attrs.jdenticon;
var size = attrs.jdenticonSize || 54;
var canvas = element.children('canvas')[0];
if (!value) {
canvas.height = 0;
canvas.width = 0;
}
else {
canvas.height = size;
canvas.width = size;
var ctx = canvas.getContext("2d");
jdenticon.drawIcon(ctx, value, size);
}
}
};
});
......@@ -31,6 +31,8 @@ function Block(json, attributes) {
that.transactionsCount = that.transactions ? that.transactions.length : 0;
that.empty = that.isEmpty();
that.id = [that.number, that.hash].join('-');
}
Block.prototype.isEmpty = function(){
......
......@@ -16,9 +16,12 @@ Peer.prototype.regexp = {
BMA: /^BASIC_MERKLED_API[ ]?/,
BMAS: /^BMAS[ ]?/,
WS2P: /^WS2P[ ]?/,
BMA_REGEXP: /^BASIC_MERKLED_API([ ]+([a-z_][a-z0-9-_.ğĞ]*))?([ ]+([0-9.]+))?([ ]+([0-9a-f:]+))?([ ]+([0-9]+))$/,
BMAS_REGEXP: /^BMAS([ ]+([a-z_][a-z0-9-_.ğĞ]*))?([ ]+([0-9.]+))?([ ]+([0-9a-f:]+))?([ ]+([0-9]+))$/,
WS2P_REGEXP: /^WS2P[ ]+([a-z0-9]+)([ ]+([a-z_][a-z0-9-_.ğĞ]*))?([ ]+([0-9.]+))?([ ]+([0-9a-f:]+))?([ ]+([0-9]+))([ ]+([a-z0-9/.&#!]+))?$/,
GVA: /^GVA(:? S)?[ ]?/,
BMA_REGEXP: /^BASIC_MERKLED_API( ([a-z_][a-z0-9-_.ğĞ]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))( ([a-z0-9/.&#!]+))?$/,
BMAS_REGEXP: /^BMAS( ([a-z_][a-z0-9-_.ğĞ]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))( ([a-z0-9/.&#!]+))?$/,
GVA_REGEXP: /^GVA( ([a-z_][a-z0-9-_.ğĞ]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))( ([a-z0-9/.&#!]+))?$/,
GVAS_REGEXP: /^GVA S( ([a-z_][a-z0-9-_.ğĞ]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))( ([a-z0-9/.&#!]+))?$/,
WS2P_REGEXP: /^WS2P ([a-z0-9]+)( ([a-z_][a-z0-9-_.ğĞ]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))( ([a-z0-9/.&#!]+))?$/,
LOCAL_IP_ADDRESS: /^127[.]0[.]0.|192[.]168[.]|10[.]0[.]0[.]|172[.]16[.]/
};
Peer.prototype.regex = Peer.prototype.regexp; // for backward compat
......@@ -28,6 +31,9 @@ Peer.prototype.keyID = function () {
if (bma.useBma) {
return [this.pubkey || "Unknown", bma.dns, bma.ipv4, bma.ipv6, bma.port, bma.useSsl, bma.path].join('-');
}
if (bma.useGva) {
return [this.pubkey || "Unknown", bma.dns, bma.ipv4, bma.ipv6, bma.port, bma.useSsl, bma.path].join('-');
}
return [this.pubkey || "Unknown", bma.ws2pid, bma.path].join('-');
};
......@@ -59,27 +65,33 @@ Peer.prototype.json = function() {
Peer.prototype.getBMA = function() {
if (this.bma) return this.bma;
var bma = null;
var bmaRegex = this.regexp.BMA_REGEXP;
var bmasRegex = this.regexp.BMAS_REGEXP;
var path = null;
var that = this;
this.endpoints.forEach(function(ep){
var matches = !bma && bmaRegex.exec(ep);
var matches = !bma && that.regexp.BMA_REGEXP.exec(ep);
if (matches) {
path = matches[10];
if (path && !path.startsWith('/')) path = '/' + path; // Fix path (add starting slash)
bma = {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"useSsl": matches[8] == 443,
"path": path || '',
"useBma": true
};
}
matches = !bma && bmasRegex.exec(ep);
matches = !bma && that.regexp.BMAS_REGEXP.exec(ep);
if (matches) {
path = matches[10];
if (path && !path.startsWith('/')) path = '/' + path; // Fix path (add starting slash)
bma = {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"path": path || '',
"useSsl": true,
"useBma": true
};
......@@ -120,28 +132,42 @@ Peer.prototype.getIPv6 = function() {
Peer.prototype.getPort = function() {
var bma = this.bma || this.getBMA();
return bma.port ? bma.port : null;
return bma.port ? parseInt(bma.port) : null;
};
Peer.prototype.getHost = function(getHost) {
bma = getHost || this.bma || this.getBMA();
Peer.prototype.getHost = function(bma) {
bma = bma || this.bma || this.getBMA();
return ((bma.port == 443 || bma.useSsl) && bma.dns) ? bma.dns :
(this.hasValid4(bma) ? bma.ipv4 :
(bma.dns ? bma.dns :
(bma.ipv6 ? '[' + bma.ipv6 + ']' :'')));
};
Peer.prototype.getURL = function(bma) {
Peer.prototype.getPath = function(bma) {
bma = bma || this.bma || this.getBMA();
var path = bma.path || '';
if (!path || bma.path === '') return '';
// Add starting slash
if (!path.startsWith('/')) path = '/' + path;
// Remove trailing slash
if (path.endsWith('/')) path = path.substring(0, path.length - 1);
return path;
};
Peer.prototype.getUrl = function(bma) {
bma = bma || this.bma || this.getBMA();
var host = this.getHost(bma);
var protocol = (bma.port == 443 || bma.useSsl) ? 'https' : 'http';
return protocol + '://' + host + (bma.port ? (':' + bma.port) : '');
return protocol + '://' + this.getServer(bma) + this.getPath(bma);
};
Peer.prototype.getServer = function(bma) {
bma = bma || this.bma || this.getBMA();
var host = this.getHost(bma);
return host + (host && bma.port ? (':' + bma.port) : '');
// Remove port if 80 or 443
return !host ? null : (host + (bma.port && bma.port != 80 && bma.port != 443 ? (':' + bma.port) : ''));
};
Peer.prototype.hasValid4 = function(bma) {
......@@ -172,9 +198,25 @@ Peer.prototype.isWs2p = function() {
Peer.prototype.isBma = function() {
var bma = this.bma || this.getBMA();
return !bma.useWs2p && !bma.useTor;
return bma.useBma;
};
Peer.prototype.hasBma = function() {
return this.hasEndpoint('(BASIC_MERKLE_API|BMAS|BMATOR)');
return this.hasEndpoint('(BASIC_MERKLED_API|BMAS|BMATOR)');
};
Peer.prototype.isGva = function() {
var bma = this.bma || this.getBMA();
return bma.useGva;
};
Peer.prototype.toJSON = function() {
return {
host: this.getHost(),
port: this.getPort(),
path: this.getPath(),
useSsl: this.isSsl(),
url: this.getUrl(),
server: this.getServer()
};
};
......@@ -59,7 +59,7 @@ function Ws2pMessage(message) {
// For DEBUG only:
/*
console.log('[http] private {0}, public {1}'.format(
console.debug('[http] private {0}, public {1}'.format(
(that.private && (that.private.useTor ? 'TOR ' : '' ) + (that.private.mode || 'false')) || 'false',
that.public && ((that.public.useTor ? 'TOR ' : '' ) + (that.public.mode || 'false')) || 'false'
), prefix);*/
......
......@@ -8,35 +8,50 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
var
started = false,
startPromise,
defaults = {
MEDIAN_TIME_OFFSET: 3600 /*G1 default value*/,
DATE_PATTERN: 'YYYY-MM-DD HH:mm',
DATE_SHORT_PATTERN: 'YYYY-MM-DD',
DATE_MONTH_YEAR_PATTERN: 'MMM YY',
DAYS: 'days',
UD: 'UD'
},
that = this;
that.MEDIAN_TIME_OFFSET = 3600 /*G1 default value*/;
// Default values
angular.merge(that, defaults);
// Update some translations, when locale changed
function onLocaleChange() {
console.debug('[filter] Loading translations for locale [{0}]'.format($translate.use()));
// DEBUG
//console.debug('[filter] Loading translations for locale [{0}]'.format($translate.use()));
return $translate(['COMMON.DATE_PATTERN', 'COMMON.DATE_SHORT_PATTERN', 'COMMON.UD', 'COMMON.DAYS'])
.then(function(translations) {
that.DATE_PATTERN = translations['COMMON.DATE_PATTERN'];
if (that.DATE_PATTERN === 'COMMON.DATE_PATTERN') {
that.DATE_PATTERN = 'YYYY-MM-DD HH:mm';
that.DATE_PATTERN = defaults.DATE_PATTERN;
}
that.DATE_SHORT_PATTERN = translations['COMMON.DATE_SHORT_PATTERN'];
if (that.DATE_SHORT_PATTERN === 'COMMON.DATE_SHORT_PATTERN') {
that.DATE_SHORT_PATTERN = 'YYYY-MM-DD';
that.DATE_SHORT_PATTERN = defaults.DATE_SHORT_PATTERN;
}
that.DATE_MONTH_YEAR_PATTERN = translations['COMMON.DATE_MONTH_YEAR_PATTERN'];
if (that.DATE_MONTH_YEAR_PATTERN === 'COMMON.DATE_MONTH_YEAR_PATTERN') {
that.DATE_MONTH_YEAR_PATTERN = 'MMM YY';
that.DATE_MONTH_YEAR_PATTERN = defaults.DATE_MONTH_YEAR_PATTERN;
}
that.DAYS = translations['COMMON.DAYS'];
if (that.DAYS === 'COMMON.DAYS') {
that.DAYS = 'days';
that.DAYS = defaults.DAYS;
}
that.UD = translations['COMMON.UD'];
if (that.UD === 'COMMON.UD') {
that.UD = 'UD';
that.UD = defaults.UD;
}
})
.catch(function(err) {
console.error('[filter] Failed to load translations for locale [{0}]'.format($translate.use()), err);
angular.merge(that, defaults);
});
}
......@@ -80,13 +95,14 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
})
.filter('formatAmount', function(csConfig, csSettings, csCurrency, $filter) {
var pattern = '0,0.0' + Array(csConfig.decimalCount || 4).join('0');
'ngInject';
var pattern = '0,0.0' + Array(csConfig.decimalCount || 2).join('0');
var patternBigNumber = '0,0.000 a';
var currencySymbol = $filter('currencySymbol');
// Always add one decimal for relative unit
var patternRelative = pattern + '0';
var minValueRelative = 1 / Math.pow(10, (csConfig.decimalCount || 4) + 1 /*add one decimal in relative*/);
var minValueRelative = 1 / Math.pow(10, (csConfig.decimalCount || 2) + 1 /*add one decimal in relative*/);
function formatRelative(input, options) {
var currentUD = options && options.currentUD ? options.currentUD : csCurrency.data.currentUD;
......@@ -124,6 +140,7 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
})
.filter('formatAmountNoHtml', function(csConfig, csSettings, csCurrency, $filter) {
'ngInject';
var minValue = 1 / Math.pow(10, csConfig.decimalCount || 4);
var format = '0,0.0' + Array(csConfig.decimalCount || 4).join('0');
var currencySymbol = $filter('currencySymbolNoHtml');
......@@ -164,15 +181,19 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
})
.filter('currencySymbol', function(filterTranslations, $filter, csSettings) {
'ngInject';
return function(input, useRelative) {
if (!input) return '';
return (angular.isDefined(useRelative) ? useRelative : csSettings.data.useRelative) ?
(filterTranslations.UD + '<sub>' + $filter('abbreviate')(input) + '</sub>') :
$filter('abbreviate')(input);
var symbol = $filter('abbreviate')(input);
if (angular.isDefined(useRelative) ? useRelative : csSettings.data.useRelative) {
return filterTranslations.UD + '<sub>' + $filter('abbreviate')(input) + '</sub>';
}
return symbol;
};
})
.filter('currencySymbolNoHtml', function(filterTranslations, $filter, csSettings) {
'ngInject';
return function(input, useRelative) {
if (!input) return '';
return (angular.isDefined(useRelative) ? useRelative : csSettings.data.useRelative) ?
......@@ -182,6 +203,7 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
})
.filter('formatDecimal', function(csConfig, csCurrency) {
'ngInject';
var minValue = 1 / Math.pow(10, csConfig.decimalCount || 4);
var format = '0,0.0' + Array(csConfig.decimalCount || 4).join('0');
......@@ -209,24 +231,28 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
})
.filter('formatDate', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input)).local().format(filterTranslations.DATE_PATTERN || 'YYYY-MM-DD HH:mm') : '';
};
})
.filter('formatDateShort', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input)).local().format(filterTranslations.DATE_SHORT_PATTERN || 'YYYY-MM-DD') : '';
};
})
.filter('formatDateMonth', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input)).local().format(filterTranslations.DATE_MONTH_YEAR_PATTERN || 'MMM YY') : '';
};
})
.filter('formatDateForFile', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input)).local().format(filterTranslations.DATE_FILE_PATTERN || 'YYYY-MM-DD') : '';
};
......@@ -245,6 +271,7 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
})
.filter('formatFromNowAndDate', function(filterTranslations) {
'ngInject';
return function(input, options) {
var m = input && moment.unix(parseInt(input));
return m && (m.fromNow() + (options && options.separator || ' | ') + m.local().format(filterTranslations.DATE_PATTERN || 'YYYY-MM-DD HH:mm')) || '';
......@@ -265,6 +292,7 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
.filter('formatDurationTime', function(filterTranslations) {
'ngInject';
return function(input) {
if (!input) return '';
var sign = input && input < 0 ? '-' : '+';
......@@ -280,11 +308,32 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
// Display time in ms or seconds (see i18n label 'COMMON.EXECUTION_TIME')
.filter('formatDurationMs', function() {
return function(input) {
return input ? (
(input < 1000) ?
(input + 'ms') :
(input/1000 + 's')
) : '';
if (!input) return '';
if (input < 1000) {
return input + 'ms';
}
var result = '';
var hours = Math.floor(input / (1000 * 60 * 60));
var minutes = Math.floor((input % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((input % (1000 * 60)) / 1000);
var milliseconds = Math.floor(input % 1000);
if (hours > 0) {
result += hours + 'h ';
}
if (minutes > 0) {
result += minutes + 'min ';
}
if (seconds > 0) {
result += seconds + 's ';
}
if (milliseconds > 0) {
result += milliseconds + 'ms';
}
return result.trim();
};
})
......@@ -305,12 +354,14 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
/* -- median time (apply currency offset)-- */
.filter('medianDate', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input) + filterTranslations.MEDIAN_TIME_OFFSET).local().format(filterTranslations.DATE_PATTERN || 'YYYY-MM-DD HH:mm') : '';
};
})
.filter('medianDateShort', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input) + filterTranslations.MEDIAN_TIME_OFFSET).local().format(filterTranslations.DATE_SHORT_PATTERN || 'YYYY-MM-DD') : '';
};
......@@ -318,24 +369,28 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
.filter('medianTime', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input)+filterTranslations.MEDIAN_TIME_OFFSET).local().format('HH:mm') : '';
};
})
.filter('medianFromNow', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input) + filterTranslations.MEDIAN_TIME_OFFSET).fromNow() : '';
};
})
.filter('medianFromNowShort', function(filterTranslations) {
'ngInject';
return function(input) {
return input ? moment.unix(parseInt(input)+filterTranslations.MEDIAN_TIME_OFFSET).fromNow(true) : '';
};
})
.filter('medianFromNowAndDate', function(filterTranslations) {
'ngInject';
return function(input, options) {
var m = input && moment.unix(parseInt(input)+filterTranslations.MEDIAN_TIME_OFFSET);
return m && (m.fromNow() + (options && options.separator || ' | ') + m.local().format(filterTranslations.DATE_PATTERN || 'YYYY-MM-DD HH:mm')) || '';
......@@ -349,7 +404,7 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
return function(input) {
if (!input) return '';
input = input.toLowerCase();
return input.substring(0,1).toUpperCase()+input.substring(1);
return input.length > 1 ? (input.substring(0,1).toUpperCase()+input.substring(1)) : input;
};
})
......@@ -390,9 +445,33 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
};
})
.filter('formatPubkey', function() {
return function(input) {
return input ? input.substr(0,8) : '';
.filter('formatPubkey', function(csCrypto) {
'ngInject';
return function(input, opts) {
if (!input || input.length < 43) return '';
var result = (!opts || opts.full !== true) ?
// See RFC0016
(input.substr(0,4) + '' + input.substr(input.length - 4)) : input;
// If given (e.g. already computed) use the existing CHK
if (opts && opts.checksum) {
result += ':' + opts.checksum;
}
// Crypto libs can be not loaded yet
else if (csCrypto.isStarted()){
result += ':' + csCrypto.util.pkChecksum(input);
}
return result;
};
})
.filter('pkChecksum', function(csCrypto) {
'ngInject';
return function(input, opts) {
if (!input || input.length < 43) return '';
if (opts && opts.prefix) {
return ':' + csCrypto.util.pkChecksum(input);
}
return csCrypto.util.pkChecksum(input);
};
})
......@@ -444,6 +523,7 @@ angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalpre
})
.filter('trustAsHtml', function($sce) {
'ngInject';
return function(html) {
return $sce.trustAsHtml(html);
};
......
// Workaround to add "".startsWith() if not present
if (typeof String.prototype.startsWith !== 'function') {
console.debug("Adding String.prototype.startsWith() -> was missing on this platform");
String.prototype.startsWith = function(prefix, position) {
return this.indexOf(prefix, position) === 0;
};
}
// Workaround to add "".startsWith() if not present
if (typeof String.prototype.trim !== 'function') {
console.debug("Adding String.prototype.trim() -> was missing on this platform");
// Make sure we trim BOM and NBSP
var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
String.prototype.trim = function() {
return this.replace(rtrim, '');
};
}
// Workaround to add Math.trunc() if not present - fix #144
if (Math && typeof Math.trunc !== 'function') {
console.debug("Adding Math.trunc() -> was missing on this platform");
Math.trunc = function(number) {
return parseInt((number - 0.5).toFixed());
};
}
// Workaround to add "".format() if not present
if (typeof String.prototype.format !== 'function') {
console.debug("Adding String.prototype.format() -> was missing on this platform");
String.prototype.format = function() {
var args = arguments;
return this.replace(/{(\d+)}/g, function(match, number) {
return typeof args[number] != 'undefined' ? args[number] : match;
});
};
}
// Workaround to add "".endsWith() if not present
if (typeof String.prototype.startsWith !== 'function') {
console.debug("Adding String.prototype.endsWith() -> was missing on this platform");
String.prototype.startsWith = function() {
var args = arguments;
return this.indexOf(args[0]) === 0;
};
}
// Workaround to add "".endsWith() if not present
if (typeof String.prototype.endsWith !== 'function') {
console.debug("Adding String.prototype.endsWith() -> was missing on this platform");
String.prototype.endsWith = function() {
var args = arguments;
return this.lastIndexOf(args[0]) === this.length - 1;
};
}
......@@ -33,11 +33,14 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
// endRemoveIf(no-device)
})
.config(function($compileProvider, csConfig) {
'ngInject';
$compileProvider.debugInfoEnabled(!!csConfig.debug);
$compileProvider.debugInfoEnabled(csConfig.debug === true);
// Fix issue #893
// See https://stackoverflow.com/questions/31859257/firefox-addon-using-angularjs-ng-src-not-working
$compileProvider.imgSrcSanitizationWhitelist(/^\s*(filesystem:resource|resource|moz-extension|chrome-extension|file|data):/);
})
.config(function($animateProvider) {
......@@ -46,10 +49,20 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
$animateProvider.classNameFilter( /\banimate-/ );
})
// Configure cache (used by HTTP requests) default max age
// Configure cache (used by HTTP requests) default options
.config(function (CacheFactoryProvider, csConfig) {
'ngInject';
angular.extend(CacheFactoryProvider.defaults, { maxAge: csConfig.cacheTimeMs || 60 * 1000 /*1min*/});
angular.extend(CacheFactoryProvider.defaults, {
// Fixed options:
recycleFreq: 60 * 1000, // Scan expired items every 1min
storagePrefix: 'caches.', // Override storage key prefix
capacity: 100, // Force to use a LRU cache, to avoid size exceed max
// Options overwritten by the csCache service:
maxAge: csConfig.cacheTimeMs || 60 * 1000, // from config if exists, or 1min
storageMode: 'memory' // Do NOT use local Storage by default
});
})
// Configure screen size detection
......@@ -87,17 +100,23 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
})
.factory('csPlatform', function (ionicReady, $rootScope, $q, $state, $translate, $timeout, UIUtils,
BMA, Device, csHttp, csConfig, csSettings, csCurrency, csWallet) {
.factory('csPlatform', function (ionicReady, $rootScope, $q, $state, $translate, $timeout, $ionicHistory, $window,
UIUtils, Modals, BMA, Device, Api,
csHttp, csConfig, csCache, csSettings, csNetwork, csCurrency, csWallet) {
'ngInject';
var
fallbackNodeIndex = 0,
defaultSettingsNode,
checkBmaNodeAliveCounter = 0,
started = false,
startPromise,
listeners,
removeChangeStateListener;
listeners = [],
removeChangeStateListener,
api = new Api(this, 'csPlatform')
;
// Fix csConfig values
csConfig.demo = csConfig.demo === true || csConfig.demo === 'true' || false;
csConfig.readonly = csConfig.readonly === true || csConfig.readonly === 'true' || false;
function disableChangeState() {
if (removeChangeStateListener) return; // make sure to call this once
......@@ -109,8 +128,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
startPromise.then(function () {
$state.go(next.name, nextParams);
});
}
else {
} else {
UIUtils.loading.hide();
}
}
......@@ -125,64 +143,212 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
removeChangeStateListener = null;
}
// Alert user if node not reached - fix issue #
// Alert user if node not reached
function checkBmaNodeAlive(alive) {
if (alive) return true;
if (alive) return true; // Ok, current node is alive
// Remember the default node
defaultSettingsNode = defaultSettingsNode || csSettings.data.node;
var askUserConfirmation = checkBmaNodeAliveCounter === 0 && csSettings.data.expertMode;
checkBmaNodeAliveCounter++;
if (checkBmaNodeAliveCounter > 3) throw 'ERROR.CHECK_NETWORK_CONNECTION'; // Avoid infinite loop
var fallbackNode = csSettings.data.fallbackNodes && fallbackNodeIndex < csSettings.data.fallbackNodes.length && csSettings.data.fallbackNodes[fallbackNodeIndex++];
if (!fallbackNode) {
throw 'ERROR.CHECK_NETWORK_CONNECTION';
}
var newServer = fallbackNode.host + ((!fallbackNode.port && fallbackNode.port != 80 && fallbackNode.port != 443) ? (':' + fallbackNode.port) : '');
api.start.raise.message('NETWORK.INFO.CONNECTING_TO_NETWORK');
// Skip is same as actual node
if (BMA.node.same(fallbackNode.host, fallbackNode.port)) {
console.debug('[platform] Skipping fallback node [{0}]: same as actual node'.format(newServer));
return checkBmaNodeAlive(); // loop (= go to next node)
var timeout = csSettings.data.expertMode && csSettings.data.timeout > 0 ? csSettings.data.timeout : Device.network.timeout();
return BMA.filterAliveNodes(csSettings.data.fallbackNodes, timeout)
.then(function (fallbackNodes) {
if (!fallbackNodes.length) throw 'ERROR.CHECK_NETWORK_CONNECTION';
return _.sample(fallbackNodes); // Random select
})
.then(function(fallbackNode) {
// Ask user before using the fallback node
if (fallbackNode && askUserConfirmation) {
return askUseFallbackNode(fallbackNode);
}
// Try to get summary
return csHttp.get(fallbackNode.host, fallbackNode.port, '/node/summary', fallbackNode.port==443 || BMA.node.forceUseSsl)()
.catch(function(err) {
console.error('[platform] Could not reach fallback node [{0}]: skipping'.format(newServer));
// silent, but return no result (will loop to the next fallback node)
return fallbackNode;
})
.then(function(res) {
if (!res) return checkBmaNodeAlive(); // Loop
.then(function (fallbackNode) {
if (!fallbackNode) return; // Skip
console.info("[platform] Switching to fallback node: {0}".format(fallbackNode.server));
var node = {
host: fallbackNode.host,
port: fallbackNode.port,
path: fallbackNode.path,
useSsl: fallbackNode.useSsl
};
csSettings.data.node = node;
csSettings.data.node.temporary = true;
csHttp.cache.clear();
// Force to show port/ssl, if this is the only difference
var messageParam = {old: BMA.server, new: newServer};
if (messageParam.old === messageParam.new) {
if (BMA.port != fallbackNode.port) {
messageParam.new += ':' + fallbackNode.port;
// loop
return BMA.copy(fallbackNode)
.then(checkBmaNodeAlive);
});
}
else if (BMA.useSsl == false && (fallbackNode.useSsl || fallbackNode.port==443)) {
messageParam.new += ' (SSL)';
// Make sure the BMA node is synchronized (is on the main consensus block)
function checkBmaNodeSynchronized(alive) {
if (!alive) return false;
var now = Date.now();
console.info('[platform] Checking peer [{0}] is well synchronized...'.format(BMA.server));
api.start.raise.message('NETWORK.INFO.ANALYZING_NETWORK');
var askUserConfirmation = csSettings.data.expertMode;
var minConsensusPeerCount = csSettings.data.minConsensusPeerCount || -1;
return csNetwork.getSynchronizedBmaPeers(BMA, {autoRefresh: false})
.then(function(peers) {
var consensusBlockNumber = peers.length ? peers[0].currentNumber : undefined;
var consensusPeerCount = peers.length;
// Not enough peers on main consensus (e.g. an isolated peer). Should never occur.
if (!consensusPeerCount || (minConsensusPeerCount > 0 && consensusPeerCount < minConsensusPeerCount)) {
console.warn("[platform] Not enough BMA peers on the main consensus block: {0} found. Will peek another peer...".format(consensusPeerCount));
// Retry using another fallback peer
return checkBmaNodeAlive(false)
.then(checkBmaNodeSynchronized); // Loop
}
// Filter on compatible peers
peers = peers.reduce(function(res, peer) {
if (!peer.compatible) return res;
// Serialize to JSON, then append
return res.concat(peer.toJSON());
}, []);
console.info("[platform] Keep {0}/{1} BMA peers, synchronized and compatible, in {2}ms".format(peers.length, consensusPeerCount, Date.now() - now));
// Try to find the current peer in synchronized peers
var synchronized = false;
peers = _.filter(peers, function(peer) {
if (BMA.url !== peer.url) return true;
synchronized = true;
return false;
});
// Saving others peers to settings
csSettings.savePeers(peers);
// OK (current BMA node is sync and compatible): continue
if (synchronized) {
console.info("[platform] Default peer [{0}{1}] is eligible.".format(BMA.server, BMA.path));
return true;
}
// Peer is not well synchronized: checking its current block
console.warn("[platform] Default peer [{0}{1}] is NOT on the consensus block #{2}. Checking its current block...".format(
BMA.server,
BMA.path,
consensusBlockNumber));
return csCurrency.blockchain.current() // Use currency, to fill the cache
.catch(function() {
return {number: 0};
})
.then(function(block) {
// OK: only few blocks late, so we keep it
if (Math.abs(block.number - consensusBlockNumber) <= 2) {
console.info("[platform] Keep default peer [{0}{1}] anyway, because current block #{2} closed to the consensus block".format(
BMA.server,
BMA.path,
block.number));
return true;
}
// No eligible peer to peek
if (!peers.length) {
console.warn("[platform] Not enough BMA peers compatible with Cesium: {0} found. Will peek another peer...".format(peers.length));
// Retry using another fallback peer
return checkBmaNodeAlive(false)
.then(checkBmaNodeSynchronized); // Loop
}
// KO: peek another peer
var randomSynchronizedPeer = _.sample(peers);
// If Expert mode: ask user to select a node
if (askUserConfirmation) {
return askUseFallbackNode(randomSynchronizedPeer, 'CONFIRM.USE_SYNC_FALLBACK_NODE');
}
return $translate('CONFIRM.USE_FALLBACK_NODE', messageParam)
.then(function(msg) {
return UIUtils.alert.confirm(msg);
return randomSynchronizedPeer;
})
.then(function(node) {
if (node === true) return true;
if (!node) {
return selectBmaNode();
}
console.info("[platform] Switching to synchronized fallback peer [{0}:{1}]".format(node.host, node.port));
// Only change BMA node in settings
angular.merge(csSettings.data.node, {
host: node.host,
port: node.port,
path: node.path,
useSsl: node.useSsl,
temporary: askUserConfirmation ? true : undefined // Mark as temporary
});
return BMA.copy(node);
});
});
}
/**
* Ask user to confirm, before switching to fallback node
* @param fallbackNode
* @param messageKey
* @returns {*}
*/
function askUseFallbackNode(fallbackNode, messageKey) {
var newUrl = fallbackNode.url || csHttp.getUrl(fallbackNode.host, fallbackNode.port, fallbackNode.path, fallbackNode.useSsl);
var confirmMsgParams = {old: BMA.url, new: newUrl};
messageKey = messageKey || 'CONFIRM.USE_FALLBACK_NODE';
return $translate(messageKey, confirmMsgParams)
.then(UIUtils.alert.confirm)
.then(function (confirm) {
if (!confirm) return;
if (!confirm) return; // Stop
return fallbackNode;
});
}
// User can select a node
function selectBmaNode() {
var parameters = {
enableFilter: false,
type: 'all',
bma: true,
expertMode: true
};
if ($window.location.protocol === 'https:') {
parameters.ssl = true;
}
return Modals.showNetworkLookup(parameters)
.then(function(peer) {
if (!peer) return true; // User cancelled (= keep the default node)
var node = {
host: peer.getHost(),
port: peer.getPort(),
useSsl: peer.isSsl()
};
console.info("[platform] Selected peer:", node);
// Only change BMA node in settings
csSettings.data.node = fallbackNode;
csSettings.data.node = node;
// Add a marker, for UI
csSettings.data.node.temporary = true;
csHttp.cache.clear();
// loop
return BMA.copy(fallbackNode)
.then(checkBmaNodeAlive);
});
return BMA.copy(node);
});
}
......@@ -193,7 +359,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
function getLatestRelease() {
var latestRelease = csSettings.data.latestReleaseUrl && csHttp.uri.parse(csSettings.data.latestReleaseUrl);
if (latestRelease) {
return csHttp.get(latestRelease.host, latestRelease.protocol == 'https:' ? 443 : latestRelease.port, "/" + latestRelease.pathname)()
return csHttp.getWithCache(latestRelease.host, latestRelease.protocol === 'https:' ? 443 : latestRelease.port, "/" + latestRelease.pathname, undefined, csCache.constants.LONG)()
.then(function (json) {
if (json && json.name && json.tag_name && json.html_url) {
return {
......@@ -213,10 +379,10 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
}
function addListeners() {
listeners = [
// Listen if node changed
listeners.push(
BMA.api.node.on.restart($rootScope, restart, this)
];
);
}
function removeListeners() {
......@@ -231,11 +397,12 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
return startPromise || start();
}
function restart() {
console.debug('[platform] restarting csPlatform');
function restart(startDelayMs) {
console.debug('[platform] Restarting ...');
return stop()
.then(function () {
return $timeout(start, 200);
if (startDelayMs === 0) return start();
return $timeout(start, startDelayMs || 200);
});
}
......@@ -244,6 +411,8 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
// Avoid change state
disableChangeState();
api.start.raise.message('COMMON.LOADING');
// We use 'ionicReady()' instead of '$ionicPlatform.ready()', because this one is callable many times
startPromise = ionicReady()
......@@ -257,7 +426,10 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
// Load BMA
.then(function() {
return BMA.ready().then(checkBmaNodeAlive);
checkBmaNodeAliveCounter = 0;
return BMA.ready()
.then(checkBmaNodeAlive)
.then(checkBmaNodeSynchronized);
})
// Load currency
......@@ -271,10 +443,13 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
addListeners();
startPromise = null;
started = true;
api.start.raise.message(''); // Reset message
})
.catch(function(err) {
startPromise = null;
started = false;
api.start.raise.message(''); // Reset message
if ($state.current.name !== $rootScope.errorState) {
$state.go($rootScope.errorState, {error: 'peer'});
}
......@@ -285,20 +460,26 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
}
function stop() {
if (!started) return $q.when();
if (!started && !startPromise) return $q.when();
removeListeners();
csWallet.stop();
csCurrency.stop();
BMA.stop();
return $q.all([
csWallet.stop({emitEvent: false}),
csCurrency.stop({emitEvent: false}),
BMA.stop()
])
.then(function() {
return $timeout(function() {
enableChangeState();
started = false;
startPromise = null;
}, 500);
}, 200);
});
}
api.registerEvent('start', 'message');
return {
disableChangeState: disableChangeState,
isStarted: isStarted,
......@@ -308,7 +489,8 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
stop: stop,
version: {
latest: getLatestRelease
}
},
api: api
};
})
......@@ -326,13 +508,13 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
// Compute the root path
var hashIndex = $window.location.href.indexOf('#');
$rootScope.rootPath = (hashIndex != -1) ? $window.location.href.substr(0, hashIndex) : $window.location.href;
$rootScope.rootPath = (hashIndex !== -1) ? $window.location.href.substr(0, hashIndex) : $window.location.href;
console.debug('[app] Root path is [' + $rootScope.rootPath + ']');
// removeIf(device)
// -- Automatic redirection to HTTPS
if ((csConfig.httpsMode === true || csConfig.httpsMode == 'true' ||csConfig.httpsMode === 'force') &&
$window.location.protocol != 'https:') {
if ((csConfig.httpsMode === true || csConfig.httpsMode === 'true' ||csConfig.httpsMode === 'force') &&
$window.location.protocol !== 'https:') {
$rootScope.$on('$stateChangeStart', function (event, next, nextParams, fromState) {
var path = 'https' + $rootScope.rootPath.substr(4) + $state.href(next, nextParams);
if (csConfig.httpsModeDebug) {
......@@ -364,7 +546,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
// Ionic Platform Grade is not A, disabling views transitions
if (ionic.Platform.grade.toLowerCase() !== 'a') {
console.info('[app] Disabling UI effects, because plateform\'s grade is [' + ionic.Platform.grade + ']');
console.info('[app] Disabling UI effects, because platform\'s grade is {{0}}'.format(ionic.Platform.grade));
UIUtils.setEffects(false);
}
......@@ -390,6 +572,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
if ($ionicHistory.backView()) {
return $ionicHistory.goBack();
}
event.preventDefault();
return UIUtils.alert.confirm('CONFIRM.EXIT_APP')
.then(function (confirm) {
......@@ -397,49 +580,16 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
ionic.Platform.exitApp();
});
}, 100);
// Make sure platform is started
return csPlatform.ready();
});
})
;
// Workaround to add "".startsWith() if not present
if (typeof String.prototype.startsWith !== 'function') {
console.debug("Adding String.prototype.startsWith() -> was missing on this platform");
String.prototype.startsWith = function(prefix, position) {
return this.indexOf(prefix, position) === 0;
};
}
// Workaround to add "".startsWith() if not present
if (typeof String.prototype.trim !== 'function') {
console.debug("Adding String.prototype.trim() -> was missing on this platform");
// Make sure we trim BOM and NBSP
var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
String.prototype.trim = function() {
return this.replace(rtrim, '');
};
}
// Make sure platform is started
.then(csPlatform.ready)
// Workaround to add Math.trunc() if not present - fix #144
if (Math && typeof Math.trunc !== 'function') {
console.debug("Adding Math.trunc() -> was missing on this platform");
Math.trunc = function(number) {
return (number - 0.5).toFixed();
};
// Applying some settings
.then(function(){
// Applying UI effects, if now already disable (e.g. because of poor platform grade)
if (UIUtils.motion.enable) {
UIUtils.setEffects($rootScope.settings.uiEffects);
}
// Workaround to add "".format() if not present
if (typeof String.prototype.format !== 'function') {
console.debug("Adding String.prototype.format() -> was missing on this platform");
String.prototype.format = function() {
var args = arguments;
return this.replace(/{(\d+)}/g, function(match, number) {
return typeof args[number] != 'undefined' ? args[number] : match;
});
};
}
})
;
......@@ -2,15 +2,18 @@
angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.settings.services'])
.factory('BMA', function($q, $window, $rootScope, $timeout, csCrypto, Api, Device, UIUtils, csConfig, csSettings, csHttp) {
.factory('BMA', function($q, $window, $rootScope, $timeout, $http,
csCrypto, Api, Device, UIUtils, csConfig, csSettings, csCache, csHttp) {
'ngInject';
function BMA(host, port, useSsl, useCache) {
function BMA(host, port, path, useSsl, useCache, timeout) {
var pubkey = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}";
var
id = (!host ? 'default' : '{0}:{1}'.format(host, (port || (useSsl ? '443' : '80')))), // Unique id of this instance
cachePrefix = "BMA-",
pubkey = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}",
// TX output conditions
SIG = "SIG\\(([0-9a-zA-Z]{43,44})\\)",
SIG = "SIG\\((" + pubkey + ")\\)",
XHX = 'XHX\\(([A-F0-9]{1,64})\\)',
CSV = 'CSV\\(([0-9]{1,8})\\)',
CLTV = 'CLTV\\(([0-9]{1,10})\\)',
......@@ -23,22 +26,26 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
api = {
BMA: 'BASIC_MERKLED_API',
BMAS: 'BMAS',
GVA: 'GVA',
GVAS: 'GVA S',
WS2P: 'WS2P',
BMATOR: 'BMATOR',
WS2PTOR: 'WS2PTOR'
},
regexp = {
USER_ID: "[A-Za-z0-9_-]+",
CURRENCY: "[A-Za-z0-9_-]+",
USER_ID: "[0-9a-zA-Z-_]+",
CURRENCY: "[0-9a-zA-Z-_]+",
PUBKEY: pubkey,
PUBKEY_WITH_CHECKSUM: "(" + pubkey +"):([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{3})",
COMMENT: "[ a-zA-Z0-9-_:/;*\\[\\]()?!^\\+=@&~#{}|\\\\<>%.]*",
INVALID_COMMENT_CHARS: "[^ a-zA-Z0-9-_:/;*\\[\\]()?!^\\+=@&~#{}|\\\\<>%.]*",
INVALID_COMMENT_CHARS: "[^ a-zA-Z0-9-_:/;*\\[\\]()?!^\\+=@&~#{}|\\\\<>%.]+",
// duniter://[uid]:[pubkey]@[host]:[port]
URI_WITH_AT: "duniter://(?:([A-Za-z0-9_-]+):)?("+pubkey+"@([a-zA-Z0-9-.]+.[ a-zA-Z0-9-_:/;*?!^\\+=@&~#|<>%.]+)",
URI_WITH_PATH: "duniter://([a-zA-Z0-9-.]+.[a-zA-Z0-9-_:.]+)/("+pubkey+")(?:/([A-Za-z0-9_-]+))?",
BMA_ENDPOINT: "BASIC_MERKLED_API" + REGEX_ENDPOINT_PARAMS,
BMA_ENDPOINT: api.BMA + REGEX_ENDPOINT_PARAMS,
BMAS_ENDPOINT: api.BMAS + REGEX_ENDPOINT_PARAMS,
GVA_ENDPOINT: api.GVA + REGEX_ENDPOINT_PARAMS,
GVAS_ENDPOINT: api.GVAS + REGEX_ENDPOINT_PARAMS,
WS2P_ENDPOINT: api.WS2P + " ([a-f0-9]{8})" + REGEX_ENDPOINT_PARAMS,
BMATOR_ENDPOINT: api.BMATOR + " ([a-z0-9-_.]*|[0-9.]+|[0-9a-f:]+.onion)(?: ([0-9]+))?",
WS2PTOR_ENDPOINT: api.WS2PTOR + " ([a-f0-9]{8}) ([a-z0-9-_.]*|[0-9.]+|[0-9a-f:]+.onion)(?: ([0-9]+))?(?: (.+))?"
......@@ -47,6 +54,9 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
REVOCATION_ALREADY_REGISTERED: 1002,
HTTP_LIMITATION: 1006,
IDENTITY_SANDBOX_FULL: 1007,
CERTIFICATION_SANDBOX_FULL: 1008,
MEMBERSHIP_SANDBOX_FULL: 1009,
TRANSACTIONS_SANDBOX_FULL: 1010,
NO_MATCHING_IDENTITY: 2001,
UID_ALREADY_USED: 2003,
NO_MATCHING_MEMBER: 2004,
......@@ -64,14 +74,28 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
PROTOCOL_VERSION: 10,
ROOT_BLOCK_HASH: 'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855',
LIMIT_REQUEST_COUNT: 5, // simultaneous async request to a Duniter node
LIMIT_REQUEST_DELAY: 1000, // time (in second) to wait between to call of a rest request
LIMIT_REQUEST_DELAY: 1000, // time (in ms) to wait between to call of a rest request
TIMEOUT: {
NONE: -1,
SHORT: 1000, // 1s
MEDIUM: 5000, // 5s
LONG: 10000, // 10s
DEFAULT: csConfig.timeout,
VERY_LONG: 60000 * 2 // 2 min (.e.g need by /tx/sources for Duniter < v1.9
},
regexp: regexp,
api: api
},
listeners,
that = this;
that.api = new Api(this, 'BMA-' + that.server);
that.raw = {
getByPath: {},
getCountByPath: {},
postByPath: {},
wsByPath: {}
};
that.api = new Api(this, 'BMA-' + id);
that.started = false;
that.init = init;
......@@ -82,34 +106,35 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
console.debug('[BMA] Enable SSL (forced by config or detected in URL)');
}
if (host) {
init(host, port, useSsl, useCache);
}
that.useCache = useCache; // need here because used in get() function
if (host) init(host, port, path, useSsl, timeout);
that.useCache = angular.isDefined(useCache) ? useCache : true; // need here because used in get() function
function init(host, port, useSsl, useCache) {
function init(host, port, path, useSsl, timeout) {
if (that.started) that.stop();
that.alive = false;
that.cache = _emptyCache();
// Use settings as default, if exists
if (csSettings.data && csSettings.data.node) {
host = host || csSettings.data.node.host;
port = port || csSettings.data.node.port;
useSsl = angular.isDefined(useSsl) ? useSsl : (port == 443 || csSettings.data.node.useSsl || that.forceUseSsl);
useCache = angular.isDefined(useCache) ? useCache : true;
var node = csSettings.data && csSettings.data.node;
if (node) {
host = host || node.host;
port = port || node.port;
path = path || node.path;
useSsl = angular.isDefined(useSsl) ? useSsl : (port == 443 || node.useSsl || that.forceUseSsl);
}
if (!host) {
return; // could not init yet
}
if (!host) return; // could not init yet
path = path && path.length ? path : (host.indexOf('/') !== -1 ? host.substring(host.indexOf('/')) : '');
if (path.endsWith('/')) path = path.substring(0, path.length -1); // Remove trailing slash
host = host.indexOf('/') !== -1 ? host.substring(0, host.indexOf('/')) : host; // Remove path from host
that.host = host;
that.port = port || 80;
that.path = path || '';
that.useSsl = angular.isDefined(useSsl) ? useSsl : (that.port == 443 || that.forceUseSsl);
that.useCache = angular.isDefined(useCache) ? useCache : false;
that.server = csHttp.getServer(host, port);
that.url = csHttp.getUrl(host, port, ''/*path*/, useSsl);
that.server = csHttp.getServer(that.host, that.port);
that.url = csHttp.getUrl(that.host, that.port, that.path, that.useSsl);
that.timeout = timeout || (csSettings.data.expertMode && csSettings.data.timeout > 0 ? csSettings.data.timeout : Device.network.timeout());
}
function exact(regexpContent) {
......@@ -120,65 +145,99 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
return new RegExp(regexpContent);
}
function _emptyCache() {
return {
getByPath: {},
postByPath: {},
wsByPath: {}
};
}
function closeWs() {
if (!that.cache) return;
if (!that.raw) return;
console.warn('[BMA] Closing all websockets...');
_.keys(that.cache.wsByPath||{}).forEach(function(key) {
var sock = that.cache.wsByPath[key];
_.keys(that.raw.wsByPath||{}).forEach(function(key) {
var sock = that.raw.wsByPath[key];
sock.close();
});
that.cache.wsByPath = {};
that.raw.wsByPath = {};
}
that.cleanCache = function() {
console.debug('[BMA] Cleaning requests cache...');
closeWs();
that.cache = _emptyCache();
};
function cleanCache() {
console.debug("[BMA] Cleaning cache {prefix: '{0}'}...".format(cachePrefix));
csCache.clear(cachePrefix);
// Clean raw requests by path cache
cleanRawRequests();
}
/**
* Clean raw requests by path cache
*/
function cleanRawRequests() {
that.raw.getByPath = {};
that.raw.getCountByPath = {};
that.raw.postByPath = {};
that.raw.wsByPath = {};
}
function setTimeout(newTimeout) {
if (that.timeout === newTimeout && newTimeout > 0) return; // Skip if same, or invalid
console.debug('[BMA] Using timeout: {0}ms'.format(newTimeout));
that.timeout = newTimeout;
get = function (path, cacheTime) {
// Clean raw requests by path cache
cleanRawRequests();
}
function getCacheable(path, cacheTime, forcedTimeout) {
cacheTime = that.useCache && cacheTime;
cacheTime = that.useCache && cacheTime || 0 /* no cache*/ ;
forcedTimeout = forcedTimeout || that.timeout;
var cacheKey = path + (cacheTime ? ('#'+cacheTime) : '');
var getRequestFn = function(params) {
// Store requestFn into a variable a function, to be able to call it to loop
var wrappedRequest = function(params) {
if (!that.started) {
if (!that._startPromise) {
console.error('[BMA] Trying to get [{0}] before start()...'.format(path));
console.warn('[BMA] Trying to get [{0}] before start(). Waiting...'.format(path));
}
return that.ready().then(function() {
return getRequestFn(params);
return that.ready()
.then(function() {
return wrappedRequest(params);
});
}
var request = that.cache.getByPath[cacheKey];
// Create the request function, if not exists
var request = that.raw.getByPath[cacheKey];
if (!request) {
if (cacheTime) {
request = csHttp.getWithCache(that.host, that.port, path, that.useSsl, cacheTime);
request = csHttp.getWithCache(that.host, that.port, that.path + path, that.useSsl, cacheTime, null/*autoRefresh*/, forcedTimeout, cachePrefix);
}
else {
request = csHttp.get(that.host, that.port, path, that.useSsl);
request = csHttp.get(that.host, that.port, that.path + path, that.useSsl, forcedTimeout);
}
that.cache.getByPath[cacheKey] = request;
that.raw.getByPath[cacheKey] = request;
}
return request(params);
};
return wrappedRequest;
}
function get(path, cacheTime, forcedTimeout) {
var request = getCacheable(path, cacheTime, forcedTimeout);
var execCount = 1;
var wrappedRequest = function(params) {
return request(params)
.then(function(res) {
execCount--;
return res;
})
.catch(function(err){
// If node return too many requests error
if (err && err.ucode == exports.errorCodes.HTTP_LIMITATION) {
if (err && err.ucode === exports.errorCodes.HTTP_LIMITATION) {
// If max number of retry not reach
if (execCount <= exports.constants.LIMIT_REQUEST_COUNT) {
if (execCount == 1) {
if (execCount === 1) {
console.warn("[BMA] Too many HTTP requests: Will wait then retry...");
// Update the loading message (if exists)
UIUtils.loading.update({template: "COMMON.LOADING_WAIT"});
......@@ -186,18 +245,88 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
// Wait 1s then retry
return $timeout(function() {
execCount++;
return request(params);
return wrappedRequest(params); // Loop
}, exports.constants.LIMIT_REQUEST_DELAY);
}
}
throw err;
});
};
return wrappedRequest;
}
function incrementGetUsageCount(path, limitRequestCount) {
limitRequestCount = limitRequestCount || constants.LIMIT_REQUEST_COUNT;
// Wait if too many requests on this path
if (that.raw.getCountByPath[path] >= limitRequestCount) {
// DEBUG
//console.debug("[BMA] Delaying request '{0}' to avoid a quota error...".format(path));
return $timeout(function() {
return incrementGetUsageCount(path, limitRequestCount);
}, constants.LIMIT_REQUEST_DELAY);
}
that.raw.getCountByPath[path]++;
return $q.when();
}
function decrementGetPathCount(path, timeout) {
if (timeout > 0) {
$timeout(function() {
decrementGetPathCount(path);
}, timeout);
}
else {
that.raw.getCountByPath[path]--;
}
}
/**
* Allow to call GET requests, with a limited rate (in Duniter node). Parallel execution will be done,
* until the max limitRequestCount, then BMA will wait few times, and continue.
* @param path
* @param cacheTime
* @param forcedTimeout
* @param limitRequestCount
* @returns {function(*): *}
*/
function getHighUsage(path, cacheTime, forcedTimeout, limitRequestCount) {
limitRequestCount = limitRequestCount || constants.LIMIT_REQUEST_COUNT;
that.raw.getCountByPath[path] = that.raw.getCountByPath[path] || 0;
var request = getCacheable(path, cacheTime, forcedTimeout);
var wrappedRequest = function(params) {
var start = Date.now();
return incrementGetUsageCount(path, limitRequestCount)
.then(function() {
return request(params);
})
.then(function(res) {
decrementGetPathCount(path, constants.LIMIT_REQUEST_DELAY - (Date.now() - start));
return res;
})
.catch(function(err) {
decrementGetPathCount(path, constants.LIMIT_REQUEST_DELAY - (Date.now() - start));
// When too many request, retry in 3s
if (err && err.ucode === errorCodes.HTTP_LIMITATION) {
return getRequestFn;
// retry
return $timeout(function () {
return wrappedRequest(params);
}, constants.LIMIT_REQUEST_DELAY);
}
throw err;
});
};
post = function(path) {
return wrappedRequest;
}
function post(path) {
var postRequest = function(obj, params) {
if (!that.started) {
if (!that._startPromise) {
......@@ -208,58 +337,87 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
});
}
var request = that.cache.postByPath[path];
var request = that.raw.postByPath[path];
if (!request) {
request = csHttp.post(that.host, that.port, path, that.useSsl);
that.cache.postByPath[path] = request;
request = csHttp.post(that.host, that.port, that.path + path, that.useSsl);
that.raw.postByPath[path] = request;
}
return request(obj, params);
};
return postRequest;
};
}
ws = function(path) {
function ws(path) {
return function() {
var sock = that.cache.wsByPath[path];
var sock = that.raw.wsByPath[path];
if (!sock || sock.isClosed()) {
sock = csHttp.ws(that.host, that.port, path, that.useSsl);
sock = csHttp.ws(that.host, that.port, that.path + path, that.useSsl);
// When close, remove from cache
sock.onclose = function() {
delete that.cache.wsByPath[path];
delete that.raw.wsByPath[path];
};
that.cache.wsByPath[path] = sock;
that.raw.wsByPath[path] = sock;
}
return sock;
};
};
}
that.isAlive = function(node, forcedTimeout) {
node = node || that;
forcedTimeout = forcedTimeout || that.timeout;
that.isAlive = function() {
return csHttp.get(that.host, that.port, '/node/summary', that.useSsl)()
// WARN:
// - Cannot use previous get() function, because
// node can be !=that, or not be started yet
// - Do NOT use cache here
return csHttp.get(node.host, node.port, (node.path || '') + '/node/summary', node.useSsl || that.forceUseSsl, forcedTimeout)()
.then(function(json) {
var software = json && json.duniter && json.duniter.software;
var isCompatible = true;
// Check duniter min version
if (software === 'duniter' && json.duniter.version && true) {
isCompatible = csHttp.version.isCompatible(csSettings.data.minVersion, json.duniter.version);
if (software === 'duniter' && json.duniter.version) {
isCompatible = csHttp.version.isCompatible(csSettings.data.minVersion, json.duniter.version) &&
// version < 1.8.7 (no storage) OR transaction storage enabled
(!json.duniter.storage || json.duniter.storage.transactions === true);
if (!isCompatible) {
console.error('[BMA] Incompatible Duniter peer [{0}{1}] (actual version {2}): min expected version is {3} with transactions storage enabled'.format(
csHttp.getServer(node.host, node.port),
node.path ||'',
json.duniter.version || '?', csSettings.data.minVersion));
}
// TODO: check other software (DURS, Juniter, etc.)
else {
console.debug('[BMA] Unknown node software [{0} v{1}]: could not check compatibility.'.format(software || '?', json.duniter.version || '?'));
}
if (!isCompatible) {
console.error('[BMA] Incompatible node [{0} v{1}]: expected at least v{2}'.format(software, json.duniter.version, csSettings.data.minVersion));
else {
console.warn('[BMA] Unknown software [{0}] found in peer [{1}{2}] (version {3}): could not check compatibility.'.format(
software || '?',
csHttp.getServer(node.host, node.port),
node.path ||'',
json.duniter.version || '?'));
}
return isCompatible;
})
.catch(function() {
return false;
return false; // Unreachable
});
};
function isSameNode(node2) {
node2 = node2 || {};
var useSsl = angular.isDefined(node2.useSsl) ? node2.useSsl : (node2.port && node2.port == 443);
var port = node2.port || (useSsl ? 443 : 80);
// Same host
return that.host === node2.host &&
// Same path
((!that.path && !node2.path) || (that.path == node2.path||'')) &&
// Same port
((!that.port && !node2.port) || (that.port == port)) &&
// Same useSsl
(that.useSsl === useSsl);
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
......@@ -270,17 +428,22 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
function addListeners() {
listeners = [
// Listen if node changed
csSettings.api.data.on.changed($rootScope, onSettingsChanged, this)
csSettings.api.data.on.changed($rootScope, onSettingsChanged, this),
Device.api.network.on.changed($rootScope, onNetworkChanged, this)
];
}
function onSettingsChanged(settings) {
// Wait 1s (because settings controller can have restart the service), then copy the settings node
$timeout(function() {
exports.copy(settings.node);
}, 1000);
}
var server = csHttp.getUrl(settings.node.host, settings.node.port, ''/*path*/, settings.node.useSsl);
var hasChanged = (server != that.url);
if (hasChanged) {
init(settings.node.host, settings.node.port, settings.node.useSsl, that.useCache);
that.restart();
function onNetworkChanged(connectionType) {
connectionType = connectionType || Device.network.connectionType();
if (connectionType !== 'none') {
setTimeout(Device.network.timeout());
}
}
......@@ -289,15 +452,16 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
};
that.ready = function() {
if (that.started) return $q.when(true);
return that._startPromise || that.start();
if (that.started) return $q.when(that.alive);
return (that._startPromise || that.start());
};
that.start = function() {
if (that._startPromise) return that._startPromise;
if (that.started) return $q.when(that.alive);
if (!that.host) {
// Load without argument: wait settings, then init using setting's data
if (!that.host || !csSettings.isStarted()) {
return csSettings.ready()
.then(function() {
that.init();
......@@ -309,56 +473,58 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
});
}
if (that.useSsl) {
console.debug('[BMA] Starting [{0}] (SSL on)...'.format(that.server));
}
else {
console.debug('[BMA] Starting [{0}]...'.format(that.server));
}
console.debug("[BMA] Starting from [{0}{1}] {ssl: {2})...".format(that.server, that.path, that.useSsl));
var now = Date.now();
that._startPromise = $q.all([
csSettings.ready,
that.isAlive()
])
.then(function(res) {
that.alive = res[1];
that._startPromise = that.isAlive()
.then(function(alive) {
that.alive = alive;
if (!that.alive) {
console.error('[BMA] Could not start [{0}]: node unreachable'.format(that.server));
console.error("[BMA] Could not start using peer [{0}{1}]: unreachable or incompatible".format(that.server, that.path));
that.started = true;
delete that._startPromise;
return false;
return false; // Not alive
}
// Add listeners
if (!listeners || listeners.length === 0) {
if (!listeners || !listeners.length) {
addListeners();
}
console.debug('[BMA] Started in '+(Date.now()-now)+'ms');
console.debug('[BMA] Started in {0}ms'.format(Date.now()-now));
that.api.node.raise.start();
that.started = true;
delete that._startPromise;
return true;
return true; // Alive
});
return that._startPromise;
};
that.stop = function() {
if (!that.started && !that._startPromise) return $q.when(); // Skip multiple call
console.debug('[BMA] Stopping...');
removeListeners();
csHttp.cache.clear();
that.cleanCache();
delete that._startPromise;
if (that.alive) {
closeWs();
cleanCache();
that.alive = false;
that.started = false;
delete that._startPromise;
that.api.node.raise.stop();
}
else {
that.started = false;
}
return $q.when();
};
that.restart = function() {
that.stop();
return $timeout(that.start, 200)
return that.stop()
.then(function() {
return $timeout(that.start, 200);
})
.then(function(alive) {
if (alive) {
that.api.node.raise.restart();
......@@ -367,6 +533,43 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
});
};
that.filterAliveNodes = function(fallbackNodes, forcedTimeout) {
forcedTimeout = forcedTimeout || csSettings.data.timeout > 0 ? csSettings.data.timeout : that.timeout;
// Filter to exclude the current BMA node
fallbackNodes = _.filter(fallbackNodes || [], function(node) {
node.server = node.server || node.host + ((!node.port && node.port != 80 && node.port != 443) ? (':' + node.port) : '');
var same = that.node.same(node);
if (same) console.debug('[BMA] Skipping fallback node [{0}]: same as current BMA node'.format(node.server));
return !same;
});
console.debug('[BMA] Getting alive fallback nodes... {timeout: {0}}'.format(forcedTimeout));
var aliveNodes = [];
return $q.all(_.map(fallbackNodes, function(node) {
return that.isAlive(node, timeout)
.then(function(alive) {
if (alive) {
node.url = csHttp.getUrl(node);
node.server = csHttp.getUrl(node);
aliveNodes.push({
host: node.host,
port: node.port,
useSsl: node.useSsl || node.port == 443,
path: node.path
});
}
else {
console.error('[BMA] Unreachable (or not compatible) fallback node [{0}]: skipping'.format(node.server));
}
});
}))
.then(function() {
return aliveNodes;
});
};
that.api.registerEvent('node', 'start');
that.api.registerEvent('node', 'stop');
that.api.registerEvent('node', 'restart');
......@@ -377,6 +580,7 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
regexp: {
USER_ID: exact(regexp.USER_ID),
COMMENT: exact(regexp.COMMENT),
INVALID_COMMENT_CHARS: test(regexp.INVALID_COMMENT_CHARS),
PUBKEY: exact(regexp.PUBKEY),
PUBKEY_WITH_CHECKSUM: exact(regexp.PUBKEY_WITH_CHECKSUM),
CURRENCY: exact(regexp.CURRENCY),
......@@ -384,6 +588,8 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
BMA_ENDPOINT: exact(regexp.BMA_ENDPOINT),
BMAS_ENDPOINT: exact(regexp.BMAS_ENDPOINT),
WS2P_ENDPOINT: exact(regexp.WS2P_ENDPOINT),
GVA_ENDPOINT: exact(regexp.GVA_ENDPOINT),
GVAS_ENDPOINT: exact(regexp.GVAS_ENDPOINT),
BMATOR_ENDPOINT: exact(regexp.BMATOR_ENDPOINT),
WS2PTOR_ENDPOINT: exact(regexp.WS2PTOR_ENDPOINT),
// TX output conditions
......@@ -397,16 +603,15 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
TX_OUTPUT_FUNCTIONS: test(OUTPUT_FUNCTIONS)
},
node: {
summary: get('/node/summary', csHttp.cache.LONG),
same: function(host2, port2) {
return host2 == that.host && ((!that.port && !port2) || (that.port == port2||80)) && (that.useSsl == (port2 && port2 === 443));
},
summary: get('/node/summary', csCache.constants.MEDIUM),
sandbox: get('/node/sandbox'),
same: isSameNode,
forceUseSsl: that.forceUseSsl
},
network: {
peering: {
self: get('/network/peering'),
peers: get('/network/peering/peers')
peers: getHighUsage('/network/peering/peers', null, null, 10)
},
peers: get('/network/peers'),
ws2p: {
......@@ -416,50 +621,103 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
},
wot: {
lookup: get('/wot/lookup/:search'),
certifiedBy: get('/wot/certified-by/:pubkey'),
certifiersOf: get('/wot/certifiers-of/:pubkey'),
certifiedBy: get('/wot/certified-by/:pubkey?pubkey=true', csCache.constants.SHORT),
certifiersOf: get('/wot/certifiers-of/:pubkey?pubkey=true', csCache.constants.SHORT),
member: {
all: get('/wot/members', csHttp.cache.LONG),
pending: get('/wot/pending', csHttp.cache.SHORT)
all: get('/wot/members', csCache.constants.LONG),
pending: get('/wot/pending', csCache.constants.SHORT)
},
requirements: function(params, cache) {
// No cache by default
if (cache !== true) return exports.raw.wot.requirements(params);
return exports.raw.wot.requirementsWithCache(params);
},
requirements: get('/wot/requirements/:pubkey'),
add: post('/wot/add'),
certify: post('/wot/certify'),
revoke: post('/wot/revoke')
},
blockchain: {
parameters: get('/blockchain/parameters', csHttp.cache.LONG),
block: get('/blockchain/block/:block', csHttp.cache.SHORT),
parameters: get('/blockchain/parameters', csCache.constants.VERY_LONG),
block: get('/blockchain/block/:block', csCache.constants.SHORT),
blocksSlice: get('/blockchain/blocks/:count/:from'),
current: get('/blockchain/current'),
current: function(cache) {
// No cache by default
return (cache !== true) ? exports.raw.blockchain.current() : exports.raw.blockchain.currentWithCache();
},
membership: post('/blockchain/membership'),
stats: {
ud: get('/blockchain/with/ud', csHttp.cache.SHORT),
ud: get('/blockchain/with/ud', csCache.constants.MEDIUM),
tx: get('/blockchain/with/tx'),
newcomers: get('/blockchain/with/newcomers'),
newcomers: get('/blockchain/with/newcomers', csCache.constants.MEDIUM),
hardship: get('/blockchain/hardship/:pubkey'),
difficulties: get('/blockchain/difficulties')
}
},
tx: {
sources: get('/tx/sources/:pubkey'),
sources: get('/tx/sources/:pubkey', csCache.constants.SHORT),
process: post('/tx/process'),
history: {
all: get('/tx/history/:pubkey'),
times: get('/tx/history/:pubkey/times/:from/:to', csHttp.cache.LONG),
timesNoCache: get('/tx/history/:pubkey/times/:from/:to'),
blocks: get('/tx/history/:pubkey/blocks/:from/:to', csHttp.cache.LONG),
pending: get('/tx/history/:pubkey/pending')
all: function(params) {
return exports.raw.tx.history.all(params)
.then(function(res) {
res.history = res.history || {};
// Clean sending and pendings, because already returned by tx/history/:pubkey/pending
res.history.sending = [];
res.history.pendings = [];
return res;
});
},
times: function(params, cache) {
// No cache by default
return ((cache !== true) ? exports.raw.tx.history.times(params) : exports.raw.tx.history.timesWithCache(params))
.then(function(res) {
res.history = res.history || {};
// Clean sending and pendings, because already returned by tx/history/:pubkey/pending
res.history.sending = [];
res.history.pendings = [];
return res;
});
},
/*blocks: get('/tx/history/:pubkey/blocks/:from/:to', csCache.constants.LONG),*/
pending: getHighUsage('/tx/history/:pubkey/pending')
}
},
ud: {
history: get('/ud/history/:pubkey')
history: {
all: get('/ud/history/:pubkey'),
times: function(params, cache) {
// No cache by default
return ((cache !== true) ? exports.raw.ud.history.times(params) : exports.raw.ud.history.timesWithCache(params));
},
/*blocks: get('/ud/history/:pubkey/blocks/:from/:to', csCache.constants.LONG),*/
}
},
uri: {},
version: {},
raw: {}
raw: {
blockchain: {
currentWithCache: get('/blockchain/current', csCache.constants.SHORT),
current: get('/blockchain/current')
},
wot: {
requirementsWithCache: get('/wot/requirements/:pubkey?pubkey=true', csCache.constants.LONG),
requirements: get('/wot/requirements/:pubkey?pubkey=true')
},
tx: {
history: {
timesWithCache: getHighUsage('/tx/history/:pubkey/times/:from/:to', csCache.constants.LONG),
times: getHighUsage('/tx/history/:pubkey/times/:from/:to'),
all: get('/tx/history/:pubkey')
}
},
ud: {
history: {
timesWithCache: get('/ud/history/:pubkey/times/:from/:to', csCache.constants.LONG),
times: get('/ud/history/:pubkey/times/:from/:to')
}
}
}
};
exports.regex = exports.regexp; // deprecated
exports.tx.parseUnlockCondition = function(unlockCondition) {
......@@ -557,29 +815,35 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
};
exports.node.parseEndPoint = function(endpoint, epPrefix) {
var path = null;
// Try BMA
var matches = exports.regexp.BMA_ENDPOINT.exec(endpoint);
if (matches) {
path = matches[10];
if (path && !path.startsWith('/')) path = '/' + path; // Fix path
return {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"useSsl": matches[8] && matches[8] == 443,
"path": matches[10],
"path": path || '',
"useBma": true
};
}
// Try BMAS
matches = exports.regexp.BMAS_ENDPOINT.exec(endpoint);
if (matches) {
path = matches[10];
if (path && !path.startsWith('/')) path = '/' + path; // Fix path
return {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"useSsl": true,
"path": matches[10],
"path": path || '',
"useBma": true
};
}
......@@ -591,12 +855,51 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
"port": matches[2] || 80,
"useSsl": false,
"useTor": true,
"useBma": true
"useBma": true,
"useWs2p": false,
"useGva": false
};
}
// Try GVA
matches = exports.regexp.GVA_ENDPOINT.exec(endpoint);
if (matches) {
path = matches[10];
if (path && !path.startsWith('/')) path = '/' + path; // Add starting slash
return {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"useSsl": matches[8] && matches[8] == 443,
"path": path || '',
"useBma": false,
"useWs2p": false,
"useGva": true,
};
}
// Try GVAS
matches = exports.regexp.GVAS_ENDPOINT.exec(endpoint);
if (matches) {
path = matches[10];
if (!path.startsWith('/')) path = '/' + path; // Fix GVA path
return {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 443,
"useSsl": true,
"path": path || '',
"useBma": false,
"useWs2p": false,
"useGva": true,
};
}
// Try WS2P
matches = exports.regexp.WS2P_ENDPOINT.exec(endpoint);
if (matches) {
path = matches[11];
if (path && !path.startsWith('/')) path = '/' + path;
return {
"ws2pid": matches[1] || '',
"dns": matches[3] || '',
......@@ -604,21 +907,25 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
"ipv6": matches[7] || '',
"port": matches[9] || 80,
"useSsl": matches[9] && matches[9] == 443,
"path": matches[11] || '',
"useWs2p": true
"path": path || '',
"useWs2p": true,
"useBma": false
};
}
// Try WS2PTOR
matches = exports.regexp.WS2PTOR_ENDPOINT.exec(endpoint);
if (matches) {
path = matches[4];
if (path && !path.startsWith('/')) path = '/' + path;
return {
"ws2pid": matches[1] || '',
"dns": matches[2] || '',
"port": matches[3] || 80,
"path": matches[4] || '',
"path": path || '',
"useSsl": false,
"useTor": true,
"useWs2p": true
"useWs2p": true,
"useBma": false
};
}
......@@ -626,13 +933,15 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
if (epPrefix) {
matches = exact(epPrefix + REGEX_ENDPOINT_PARAMS).exec(endpoint);
if (matches) {
path = matches[10];
if (path && !path.startsWith('/')) path = '/' + path;
return {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"useSsl": matches[8] && matches[8] == 443,
"path": matches[10],
"path": path || '',
"useBma": false
};
}
......@@ -641,12 +950,25 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
};
exports.copy = function(otherNode) {
var url = csHttp.getUrl(otherNode.host, otherNode.port, otherNode.path || '', otherNode.useSsl);
var hasChanged = (url !== that.url);
if (hasChanged) {
var wasStarted = that.started;
// Stop, if need
if (wasStarted) that.stop();
that.init(otherNode.host, otherNode.port, otherNode.useSsl, that.useCache/*keep original value*/);
// Restart (only if was already started)
return wasStarted ? that.start() : $q.when();
that.init(otherNode.host, otherNode.port, otherNode.path || '', otherNode.useSsl);
if (wasStarted) {
return $timeout(function () {
return that.start()
.then(function (alive) {
if (alive) {
that.api.node.raise.restart();
}
return alive;
});
}, 200); // Wait stop finished
}
}
};
exports.wot.member.uids = function() {
......@@ -679,83 +1001,24 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
/**
* Return all expected blocks
* @param blockNumbers a rray of block number
* @param blockNumbers an array of block number
*/
exports.blockchain.blocks = function(blockNumbers){
return exports.raw.getHttpRecursive(exports.blockchain.block, 'block', blockNumbers);
return $q.all(blockNumbers.map(function(block) {
return exports.blockchain.block({block: block});
}));
};
/**
* Return all expected blocks
* @param blockNumbers a rray of block number
* @param leaves
*/
exports.network.peering.peersByLeaves = function(leaves){
return exports.raw.getHttpRecursive(exports.network.peering.peers, 'leaf', leaves, 0, 10);
};
exports.raw.getHttpRecursive = function(httpGetRequest, paramName, paramValues, offset, size) {
offset = angular.isDefined(offset) ? offset : 0;
size = size || exports.constants.LIMIT_REQUEST_COUNT;
return $q(function(resolve, reject) {
var result = [];
var jobs = [];
_.each(paramValues.slice(offset, offset+size), function(paramValue) {
var requestParams = {};
requestParams[paramName] = paramValue;
jobs.push(
httpGetRequest(requestParams)
.then(function(res){
if (!res) return;
result.push(res);
})
);
});
$q.all(jobs)
.then(function() {
if (offset < paramValues.length - 1) {
$timeout(function() {
exports.raw.getHttpRecursive(httpGetRequest, paramName, paramValues, offset+size, size)
.then(function(res) {
if (!res || !res.length) {
resolve(result);
return;
}
resolve(result.concat(res));
})
.catch(function(err) {
reject(err);
});
}, exports.constants.LIMIT_REQUEST_DELAY);
}
else {
resolve(result);
}
})
.catch(function(err){
if (err && err.ucode === exports.errorCodes.HTTP_LIMITATION) {
resolve(result);
}
else {
reject(err);
}
});
});
return $q.all(leaves.map(function(leaf) {
return exports.network.peering.peers({leaf: leaf});
}));
};
exports.raw.getHttpWithRetryIfLimitation = function(exec) {
return exec()
.catch(function(err){
// When too many request, retry in 3s
if (err && err.ucode == exports.errorCodes.HTTP_LIMITATION) {
return $timeout(function() {
// retry
return exports.raw.getHttpWithRetryIfLimitation(exec);
}, exports.constants.LIMIT_REQUEST_DELAY);
}
});
};
exports.blockchain.lastUd = function() {
return exports.blockchain.stats.ud()
......@@ -772,115 +1035,76 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
};
exports.uri.parse = function(uri) {
return $q(function(resolve, reject) {
var pubkey;
if (!uri) return $q.reject("Missing required argument 'uri'");
// If pubkey: not need to parse
if (exact(regexp.PUBKEY).test(uri)) {
return $q(function(resolve, reject) {
// Pubkey or pubkey+checksum
if (exports.regexp.PUBKEY.test(uri) || exports.regexp.PUBKEY_WITH_CHECKSUM.test(uri)) {
resolve({
pubkey: uri
});
}
// If pubkey+checksum
else if (exact(regexp.PUBKEY_WITH_CHECKSUM).test(uri)) {
console.debug("[BMA.parse] Detecting a pubkey with checksum: " + uri);
var matches = exports.regexp.PUBKEY_WITH_CHECKSUM.exec(uri);
pubkey = matches[1];
var checksum = matches[2];
console.debug("[BMA.parse] Detecting a pubkey {"+pubkey+"} with checksum {" + checksum + "}");
var expectedChecksum = csCrypto.util.pkChecksum(pubkey);
console.debug("[BMA.parse] Expecting checksum for pubkey is {" + expectedChecksum + "}");
if (checksum != expectedChecksum) {
reject( {message: 'ERROR.PUBKEY_INVALID_CHECKSUM'});
}
else {
// Uid
else if (uri.startsWith('@') && exports.regexp.USER_ID.test(uid.substring(1))) {
resolve({
pubkey: pubkey
uid: uid.substring(1)
});
}
// G1 protocols
else if (uri.startsWith('june:') || uri.startsWith('web+june:')) {
var parser = csHttp.uri.parse(uri);
// Pubkey (explicit path)
var pubkey;
if (parser.hostname === 'wallet' || parser.hostname === 'pubkey') {
pubkey = parser.pathSegments[0];
if (!exports.regexp.PUBKEY.test(pubkey) && !exports.regexp.PUBKEY_WITH_CHECKSUM.test(pubkey)) {
reject({message: 'ERROR.INVALID_PUBKEY'});
return;
}
else if(uri.startsWith('duniter://')) {
var parser = csHttp.uri.parse(uri),
uid,
currency = parser.host.indexOf('.') === -1 ? parser.host : null,
host = parser.host.indexOf('.') !== -1 ? parser.host : null;
if (parser.username) {
if (parser.password) {
uid = parser.username;
pubkey = parser.password;
}
else {
pubkey = parser.username;
}
}
if (parser.pathname) {
var paths = parser.pathname.split('/');
var pathCount = !paths ? 0 : paths.length;
var index = 0;
if (!currency && pathCount > index) {
currency = paths[index++];
}
if (!pubkey && pathCount > index) {
pubkey = paths[index++];
}
if (!uid && pathCount > index) {
uid = paths[index++];
}
if (pathCount > index) {
reject( {message: 'Bad Duniter URI format. Invalid path (incomplete or redundant): '+ parser.pathname}); return;
parser.pathSegments = parser.pathSegments.slice(1);
}
else if (parser.hostname &&
(exports.regexp.PUBKEY.test(parser.hostname) || exports.regexp.PUBKEY_WITH_CHECKSUM.test(parser.hostname))) {
pubkey = parser.hostname;
}
if (!currency){
if (host) {
csHttp.get(host + '/blockchain/parameters')()
.then(function(parameters){
if (pubkey) {
resolve({
uid: uid,
pubkey: pubkey,
host: host,
currency: parameters.currency
pathSegments: parser.pathSegments,
params: parser.searchParams
});
})
.catch(function(err) {
console.error(err);
reject({message: 'Could not get node parameter. Currency could not be retrieve'});
});
}
else {
reject({message: 'Bad Duniter URI format. Missing currency name (or node address).'}); return;
}
}
else {
if (!host) {
// UID
else if (parser.hostname && parser.hostname.startsWith('@') && exports.regexp.USER_ID.test(parser.hostname.substring(1))) {
resolve({
uid: uid,
pubkey: pubkey,
currency: currency
uid: parser.hostname.substring(1),
pathSegments: parser.pathSegments,
params: parser.searchParams
});
}
// Check if currency are the same (between node and uri)
return csHttp.get(host + '/blockchain/parameters')()
.then(function(parameters){
if (parameters.currency !== currency) {
reject( {message: "Node's currency ["+parameters.currency+"] does not matched URI's currency ["+currency+"]."}); return;
}
// Block
else if (parser.hostname === 'block') {
resolve({
uid: uid,
pubkey: pubkey,
host: host,
currency: currency
});
})
.catch(function(err) {
console.error(err);
reject({message: 'Could not get node parameter. Currency could not be retrieve'});
block: {number: parser.pathSegments[0]},
pathSegments: parser.pathSegments.slice(1),
params: parser.searchParams
});
}
// Other case
else {
console.debug("[BMA.parse] Unknown URI format: " + uri);
reject({message: 'ERROR.UNKNOWN_URI_FORMAT'});
}
}
else {
console.debug("[BMA.parse] Could not parse URI: " + uri);
console.debug("[BMA.parse] Unknown URI format: " + uri);
reject({message: 'ERROR.UNKNOWN_URI_FORMAT'});
}
})
......@@ -888,37 +1112,70 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
// Check values against regex
.then(function(result) {
if (!result) return;
if (result.pubkey && !(exact(regexp.PUBKEY).test(result.pubkey))) {
throw {message: "Invalid pubkey format [" + result.pubkey + "]"};
}
if (result.uid && !(exact(regexp.USER_ID).test(result.uid))) {
throw {message: "Invalid uid format [" + result.uid + "]"};
}
if (result.currency && !(exact(regexp.CURRENCY).test(result.currency))) {
throw {message: "Invalid currency format ["+result.currency+"]"};
// Validate checksum
if (result.pubkey && exports.regexp.PUBKEY_WITH_CHECKSUM.test(result.pubkey)) {
console.debug("[BMA.parse] Validating pubkey checksum... ");
var matches = exports.regexp.PUBKEY_WITH_CHECKSUM.exec(result.pubkey);
pubkey = matches[1];
var checksum = matches[2];
return csCrypto.ready()
.then(function() {
return csCrypto.util.pkChecksum(pubkey);
})
.then(function(expectedChecksum){
if (checksum !== expectedChecksum) {
console.warn("[BMA.parse] Detecting a pubkey {{0}} with checksum {{1}}, but expecting checksum is {{2}}".format(
pubkey, checksum, expectedChecksum
));
throw {message: 'ERROR.PUBKEY_INVALID_CHECKSUM'};
}
result.pubkey = pubkey;
result.pubkeyChecksum = checksum;
return result;
});
}
return result;
});
};
// Define get latest release (or fake function is no URL defined)
var duniterLatestReleaseUrl = csSettings.data.duniterLatestReleaseUrl && csHttp.uri.parse(csSettings.data.duniterLatestReleaseUrl);
exports.raw.getLatestRelease = duniterLatestReleaseUrl ?
csHttp.getWithCache(duniterLatestReleaseUrl.host,
duniterLatestReleaseUrl.port,
"/" + duniterLatestReleaseUrl.pathname,
/*useSsl*/ (duniterLatestReleaseUrl.port == 443 || duniterLatestReleaseUrl.protocol == 'https:' || that.forceUseSsl),
csHttp.cache.LONG
) :
if (csSettings.data.duniterLatestReleaseUrl) {
var releaseUri = csHttp.uri.parse(csSettings.data.duniterLatestReleaseUrl);
var releaseUriUseSsl = releaseUri.port == 443 || releaseUri.protocol === 'https:' || that.forceUseSsl;
exports.raw.getLatestRelease = csHttp.getWithCache(releaseUri.host, releaseUri.port, "/" + releaseUri.pathname, releaseUriUseSsl,
csCache.constants.LONG);
}
// No URL define: use a fake function
function() {
else {
exports.raw.getLatestRelease = function() {
return $q.when();
};
}
exports.version.latest = function() {
return exports.raw.getLatestRelease()
.then(function (json) {
if (!json) return;
// Gitlab
if (Array.isArray(json)) {
var releaseVersion = _.find(json, function(res) {
return res.tag && res.description && res.description.contains(':white_check_mark: Release\n');
});
if (releaseVersion) {
var version = releaseVersion.tag.startsWith('v') ? releaseVersion.tag.substring(1) : releaseVersion.tag;
var url = (csSettings.data.duniterLatestReleaseUrl.endsWith('.json') ?
csSettings.data.duniterLatestReleaseUrl.substring(0, csSettings.data.duniterLatestReleaseUrl.length - 4) :
csSettings.data.duniterLatestReleaseUrl) + '/' + releaseVersion.tag;
return {
version: version,
url: url
};
}
}
// Github
if (json.name && json.html_url) {
return {
version: json.name,
......@@ -947,34 +1204,46 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
angular.merge(that, exports);
}
var service = new BMA(undefined, undefined, undefined, true);
var service = new BMA();
service.instance = function(host, port, useSsl, useCache) {
return new BMA(host, port, useSsl, useCache);
service.instance = function(host, port, path, useSsl, useCache, timeout) {
useCache = angular.isDefined(useCache) ? useCache : false; // No cache by default
return new BMA(host, port, path, useSsl, useCache, timeout);
};
service.lightInstance = function(host, port, useSsl, timeout) {
service.lightInstance = function(host, port, path, useSsl, timeout) {
port = port || 80;
useSsl = angular.isDefined(useSsl) ? useSsl : (port == 443);
timeout = timeout || (csSettings.data.expertMode && csSettings.data.timeout > 0 ? csSettings.data.timeout : Device.network.timeout());
path = path || (host.indexOf('/') !== -1 ? host.substring(host.indexOf('/')) : '');
if (!path.startsWith('/')) path = '/' + path; // Add starting slash
if (path.endsWith('/')) path = path.substring(0, path.length - 1); // Remove trailing slash
host = host.indexOf('/') !== -1 ? host.substring(0, host.indexOf('/')) : host;
return {
host: host,
port: port,
path: path,
useSsl: useSsl,
url: csHttp.getUrl(host, port, ''/*path*/, useSsl),
server: csHttp.getServer(host, port),
url: csHttp.getUrl(host, port, path, useSsl),
node: {
summary: csHttp.getWithCache(host, port, '/node/summary', useSsl, csHttp.cache.LONG, false, timeout)
summary: csHttp.getWithCache(host, port, path + '/node/summary', useSsl, csCache.constants.MEDIUM, false/*autoRefresh*/, timeout),
sandboxes: csHttp.get(host, port, path + '/node/sandboxes', useSsl, timeout ? Math.max(timeout * 2, 3000) : undefined), // /!\ sandboxes request can be long
},
network: {
peering: {
self: csHttp.get(host, port, '/network/peering', useSsl, timeout)
self: csHttp.get(host, port, path + '/network/peering', useSsl, timeout)
},
peers: csHttp.get(host, port, '/network/peers', useSsl, timeout)
peers: csHttp.get(host, port, path + '/network/peers', useSsl, timeout)
},
blockchain: {
current: csHttp.get(host, port, '/blockchain/current', useSsl, timeout),
current: csHttp.get(host, port, path + '/blockchain/current', useSsl, timeout),
stats: {
hardship: csHttp.get(host, port, '/blockchain/hardship/:pubkey', useSsl, timeout)
hardship: csHttp.get(host, port, path + '/blockchain/hardship/:pubkey', useSsl, timeout)
}
},
tx: {
process: csHttp.post(host, port, path + '/tx/process', useSsl, timeout)
}
};
};
......