astroport/www/gchange/dist_js/gchange.js

38046 lines
1.7 MiB
JavaScript
Raw Normal View History

2020-03-29 14:59:00 +02:00
angular.module('cesium.settings.services', ['ngApi', 'cesium.config'])
.factory('csSettings', ['$rootScope', '$q', '$window', 'Api', 'localStorage', '$translate', 'csConfig', function($rootScope, $q, $window, Api, localStorage, $translate, csConfig) {
'ngInject';
// Define app locales
var locales = [
{id:'en', label:'English', country: 'us'},
{id:'en-GB', label:'English (UK)', country: 'gb'},
{id:'eo-EO', label:'Esperanto'},
{id:'fr-FR', label:'Français', country: 'fr'}
];
var fallbackLocale = csConfig.fallbackLanguage ? fixLocale(csConfig.fallbackLanguage) : 'en';
// Convert browser locale to app locale (fix #140)
function fixLocale (locale) {
if (!locale) return fallbackLocale;
// exists in app locales: use it
if (_.findWhere(locales, {id: locale})) return locale;
// not exists: reiterate with the root (e.g. 'fr-XX' -> 'fr')
var localeParts = locale.split('-');
if (localeParts.length > 1) {
return fixLocale(localeParts[0]);
}
// If another locale exists with the same root: use it
var similarLocale = _.find(locales, function(l) {
return String.prototype.startsWith.call(l.id, locale);
});
if (similarLocale) return similarLocale.id;
return fallbackLocale;
}
// Convert browser locale to app locale (fix #140)
function fixLocaleWithLog (locale) {
var fixedLocale = fixLocale(locale);
if (locale !== fixedLocale) {
console.debug('[settings] Fix locale [{0}] -> [{1}]'.format(locale, fixedLocale));
}
return fixedLocale;
}
var
constants = {
STORAGE_KEY: 'GCHANGE_SETTINGS',
KEEP_AUTH_IDLE_SESSION: 9999
},
// Settings that user cannot change himself (only config can override this values)
fixedSettings = {
timeout : 4000,
cacheTimeMs: 60000, /*1 min*/
timeWarningExpireMembership: 2592000 * 2 /*=2 mois*/,
timeWarningExpire: 2592000 * 3 /*=3 mois*/,
minVersion: '1.7.0', // min duniter version
newIssueUrl: "https://github.com/duniter-gchange/gchange-client/issues/new?labels=bug",
//userForumUrl: "https://forum.gchange.fr",
userForumUrl: "https://forum.monnaie-libre.fr",
latestReleaseUrl: "https://api.github.com/repos/duniter-gchange/gchange-client/releases/latest",
httpsMode: false
},
defaultSettings = angular.merge({
useRelative: false,
useLocalStorage: !!$window.localStorage, // override to false if no device
useLocalStorageEncryption: false,
walletHistoryTimeSecond: 30 * 24 * 60 * 60 /*30 days*/,
walletHistorySliceSecond: 5 * 24 * 60 * 60 /*download using 5 days slice*/,
walletHistoryAutoRefresh: true, // override to false if device
rememberMe: true,
keepAuthIdle: 10 * 60,
showUDHistory: true,
showLoginSalt: false,
expertMode: false,
decimalCount: 2,
uiEffects: true,
blockValidityWindow: 6,
helptip: {
enable: false,
installDocUrl: "https://github.com/duniter-gchange/gchange-client/blob/master/README.md",
currency: 0,
network: 0,
wotLookup: 0,
wot: 0,
wotCerts: 0,
wallet: 0,
walletCerts: 0,
header: 0,
settings: 0
},
currency: {
allRules: false,
allWotRules: false
},
wallet: {
showPubkey: true,
alertIfUnusedWallet: true,
notificationReadTime: 0
},
locale: {
id: fixLocaleWithLog(csConfig.defaultLanguage || $translate.use()) // use config locale if set, or browser default
}
},
fixedSettings,
csConfig),
data = {},
previousData,
started = false,
startPromise,
api = new Api(this, "csSettings");
var
reset = function() {
_.keys(data).forEach(function(key){
delete data[key];
});
applyData(defaultSettings);
return api.data.raisePromise.reset(data)
.then(store);
},
getByPath = function(path, defaultValue) {
var obj = data;
_.each(path.split('.'), function(key) {
obj = obj[key];
if (angular.isUndefined(obj)) {
obj = defaultValue;
return; // stop
}
});
return obj;
},
emitChangedEvent = function() {
var hasChanged = angular.isUndefined(previousData) || !angular.equals(previousData, data);
if (hasChanged) {
previousData = angular.copy(data);
return api.data.raise.changed(data);
}
},
store = function() {
if (!started) {
console.debug('[setting] Waiting start finished...');
return (startPromise || start()).then(store);
}
var promise;
if (data.useLocalStorage) {
// When node is temporary (fallback node): keep previous node address - issue #476
if (data.node.temporary === true) {
promise = localStorage.getObject(constants.STORAGE_KEY)
.then(function(previousSettings) {
var savedData = angular.copy(data);
savedData.node = previousSettings && previousSettings.node || {};
delete savedData.temporary; // never store temporary flag
return localStorage.setObject(constants.STORAGE_KEY, savedData);
});
}
else {
promise = localStorage.setObject(constants.STORAGE_KEY, data);
}
}
else {
promise = localStorage.setObject(constants.STORAGE_KEY, null);
}
return promise
.then(function() {
if (data.useLocalStorage) {
console.debug('[setting] Saved locally');
}
// Emit event on store
return api.data.raisePromise.store(data);
})
// Emit event on store
.then(emitChangedEvent);
},
/**
* Apply new settings (can be partial)
* @param newData
*/
applyData = function(newData) {
if (!newData) return; // skip empty
var localeChanged = false;
if (newData.locale && newData.locale.id) {
// Fix previously stored locale (could use bad format)
var localeId = fixLocale(newData.locale.id);
newData.locale = _.findWhere(locales, {id: localeId});
localeChanged = !data.locale || newData.locale.id !== data.locale.id || newData.locale.id !== $translate.use();
}
// Force some fixed settings, before merging
_.keys(fixedSettings).forEach(function(key) {
newData[key] = defaultSettings[key]; // This will apply fixed value (override by config.js file)
});
// Apply new settings
angular.merge(data, newData);
// Delete temporary properties, if false
if (newData && newData.node && !newData.node.temporary || !data.node.temporary) delete data.node.temporary;
// Gchange workaround: Replace OLD default duniter node, by gchange pod
if ((data.plugins && data.plugins.es.host && data.plugins.es.port) &&
(!data.node || (data.node.host !== data.plugins.es.host))) {
var oldBmaNode = data.node.host;
var newBmaNode = data.plugins.es.host;
console.warn("[settings] Replacing duniter node {{0}} with gchange pod {{1}}".format(oldBmaNode, newBmaNode));
data.node = {
host: newBmaNode,
port: data.plugins.es.port,
useSsl: data.plugins.es.useSsl
};
}
// Apply the new locale (only if need)
// will produce an event cached by onLocaleChange();
if (localeChanged) {
$translate.use(data.locale.id);
}
},
restore = function() {
var now = Date.now();
return localStorage.getObject(constants.STORAGE_KEY)
.then(function(storedData) {
// No settings stored
if (!storedData) {
console.debug("[settings] No settings in local storage. Using defaults.");
applyData(defaultSettings);
emitChangedEvent();
return;
}
// Apply stored data
applyData(storedData);
console.debug('[settings] Loaded from local storage in '+(Date.now()-now)+'ms');
emitChangedEvent();
});
},
// Detect locale successful changes, then apply to vendor libs
onLocaleChange = function() {
var locale = $translate.use();
console.debug('[settings] Locale ['+locale+']');
// config moment lib
try {
moment.locale(locale.toLowerCase());
}
catch(err) {
try {
moment.locale(locale.substr(0,2));
}
catch(err) {
moment.locale('en-gb');
console.warn('[settings] Unknown local for moment lib. Using default [en]');
}
}
// config numeral lib
try {
numeral.language(locale.toLowerCase());
}
catch(err) {
try {
numeral.language(locale.substring(0, 2));
}
catch(err) {
numeral.language('en-gb');
console.warn('[settings] Unknown local for numeral lib. Using default [en]');
}
}
// Emit event
api.locale.raise.changed(locale);
},
ready = function() {
if (started) return $q.when();
return startPromise || start();
},
start = function() {
console.debug('[settings] Starting...');
startPromise = localStorage.ready()
// Restore
.then(restore)
// Emit ready event
.then(function() {
console.debug('[settings] Started');
started = true;
startPromise = null;
// Emit event (used by plugins)
api.data.raise.ready(data);
});
return startPromise;
};
$rootScope.$on('$translateChangeSuccess', onLocaleChange);
api.registerEvent('data', 'reset');
api.registerEvent('data', 'changed');
api.registerEvent('data', 'store');
api.registerEvent('data', 'ready');
api.registerEvent('locale', 'changed');
// Apply default settings. This is required on some browser (web or mobile - see #361)
applyData(defaultSettings);
// Default action
//start();
return {
ready: ready,
start: start,
data: data,
apply: applyData,
getByPath: getByPath,
reset: reset,
store: store,
restore: restore,
defaultSettings: defaultSettings,
// api extension
api: api,
locales: locales,
constants: constants
};
}]);
//var Base58, Base64, scrypt_module_factory = null, nacl_factory = null;
angular.module('cesium.crypto.services', ['cesium.utils.services'])
.factory('CryptoUtils', ['$q', '$timeout', 'ionicReady', function($q, $timeout, ionicReady) {
'ngInject';
/**
* CryptoAbstract, abstract class with useful methods
* @type {number}
*/
function CryptoAbstractService() {
this.loaded = false;
var that = this;
this.copy = function(source) {
_.forEach(_.keys(source), function(key) {
that[key] = source[key];
});
};
this.isLoaded = function() {
return this.loaded;
};
this.util = this.util || {};
/**
* Converts an array buffer to a string
*
* @private
* @param {ArrayBuffer} buf The buffer to convert
* @param {Function} callback The function to call when conversion is complete
*/
this.util.array_to_string = function(buf, callback) {
var bb = new Blob([new Uint8Array(buf)]);
var f = new FileReader();
f.onload = function(e) {
callback(e.target.result);
};
f.readAsText(bb);
};
}
CryptoAbstractService.prototype.constants = {
crypto_sign_BYTES: 64,
crypto_secretbox_NONCEBYTES: 24,
crypto_box_MACBYTES: 16,
SEED_LENGTH: 32, // Length of the key
SCRYPT_PARAMS:{
SIMPLE: {
N: 2048,
r: 8,
p: 1,
memory: -1 // default
},
DEFAULT: {
N: 4096,
r: 16,
p: 1,
memory: -1 // default
},
}
};
CryptoAbstractService.prototype.async_load_base58 = function(on_ready) {
var that = this;
if (Base58 !== null){return on_ready(Base58);}
else {$timeout(function(){that.async_load_base58(on_ready);}, 100);}
};
CryptoAbstractService.prototype.async_load_scrypt = function(on_ready, options) {
var that = this;
if (scrypt_module_factory !== null){scrypt_module_factory(on_ready, options);}
else {$timeout(function(){that.async_load_scrypt(on_ready, options);}, 100);}
};
CryptoAbstractService.prototype.async_load_nacl_js = function(on_ready, options) {
var that = this;
if (nacl_factory !== null) {nacl_factory.instantiate(on_ready, options);}
else {$timeout(function(){that.async_load_nacl_js(on_ready, options);}, 100);}
};
CryptoAbstractService.prototype.async_load_base64 = function(on_ready) {
var that = this;
if (Base64 !== null) {on_ready(Base64);}
else {$timeout(function(){that.async_load_base64(on_ready);}, 100);}
};
CryptoAbstractService.prototype.async_load_sha256 = function(on_ready) {
var that = this;
if (sha256 !== null){return on_ready(sha256);}
else {$timeout(function(){that.async_load_sha256(on_ready);}, 100);}
};
CryptoAbstractService.prototype.seed_from_signSk = function(signSk) {
var seed = new Uint8Array(that.constants.SEED_LENGTH);
for (var i = 0; i < seed.length; i++) seed[i] = signSk[i];
return seed;
};
CryptoAbstractService.prototype.seed_from_signSk = function(signSk) {
var seed = new Uint8Array(that.constants.SEED_LENGTH);
for (var i = 0; i < seed.length; i++) seed[i] = signSk[i];
return seed;
};
// Web crypto API - see https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API
var crypto = window.crypto || window.msCrypto || window.Crypto;
if (crypto && crypto.getRandomValues) {
CryptoAbstractService.prototype.crypto = crypto;
CryptoAbstractService.prototype.util = {};
CryptoAbstractService.prototype.util.random_nonce = function() {
var nonce = new Uint8Array(crypto_secretbox_NONCEBYTES);
this.crypto.getRandomValues(nonce);
return $q.when(nonce);
};
}
function FullJSServiceFactory() {
this.id = 'FullJS';
// libraries handlers
this.scrypt = null;
this.nacl = null;
this.base58 = null;
this.base64 = null;
var that = this;
this.util = this.util || {};
this.util.decode_utf8 = function (s) {
var i, d = unescape(encodeURIComponent(s)), b = new Uint8Array(d.length);
for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i);
return b;
};
this.util.encode_utf8 = function (s) {
return that.nacl.encode_utf8(s);
};
this.util.encode_base58 = function (a) { // TODO : move to abstract factory
return that.base58.encode(a);
};
this.util.decode_base58 = function (a) { // TODO : move to abstract factory
var i;
var d = that.base58.decode(a);
var b = new Uint8Array(d.length);
for (i = 0; i < d.length; i++) b[i] = d[i];
return b;
};
this.util.decode_base64 = function (a) {
return that.base64.decode(a);
};
this.util.encode_base64 = function (b) {
return that.base64.encode(b);
};
this.util.hash_sha256 = function (message) {
return $q(function (resolve) {
var msg = that.util.decode_utf8(message);
var hash = that.nacl.to_hex(that.nacl.crypto_hash_sha256(msg));
resolve(hash.toUpperCase());
});
};
this.util.random_nonce = function () {
if (that.crypto && that.crypto.getRandomValues) {
var nonce = new Uint8Array(that.constants.crypto_secretbox_NONCEBYTES);
that.crypto.getRandomValues(nonce);
return $q.when(nonce);
}
else {
return $q.when(that.nacl.crypto_box_random_nonce());
}
};
this.util.crypto_hash_sha256 = function(msg_int8) {
return that.nacl.crypto_hash_sha256(msg_int8);
};
this.util.crypto_scrypt = function(password, salt, N, r, p, seedLength) {
return $q(function(resolve, reject) {
try {
var seed = that.scrypt.crypto_scrypt(
password,
salt,
N,
r,
p,
seedLength);
resolve(seed);
}
catch(err) {
reject(err);
}
});
};
/**
* Compute the box key pair, from a sign key pair
*/
this.box_keypair_from_sign = function (signKeyPair) {
if (signKeyPair.boxSk && signKeyPair.boxPk) return $q.when(signKeyPair);
return $q(function (resolve, reject) {
try {
// TODO: waiting for a new version of js-nacl, with missing functions expose
//resolve(that.nacl.crypto_box_keypair_from_sign_sk(signKeyPair.signPk);
resolve(crypto_box_keypair_from_sign_sk(signKeyPair.signSk));
}
catch(err) {
reject(err);
}
});
};
/**
* Compute the box public key, from a sign public key
*/
this.box_pk_from_sign = function (signPk) {
return $q(function(resolve, reject) {
try {
// TODO: waiting for a new version of js-nacl, with missing functions expose
//resolve(that.nacl.crypto_box_pk_from_sign_pk(signPk));
resolve(crypto_box_pk_from_sign_pk(signPk));
}
catch(err) {
reject(err);
}
});
};
this.box_sk_from_sign = function (signSk) {
return $q(function(resolve, reject) {
try {
// TODO: waiting for a new version of js-nacl, with missing functions expose
//resolve(that.nacl.crypto_box_sk_from_sign_sk(signSk));
resolve(crypto_box_sk_from_sign_sk(signSk));
}
catch(err) {
reject(err);
}
});
};
/**
* Encrypt a message, from a key pair
*/
this.box = function(message, nonce, recipientPk, senderSk) {
return $q(function (resolve, reject) {
if (!message) {
resolve(message);
return;
}
var messageBin = that.nacl.encode_utf8(message);
if (typeof recipientPk === "string") {
recipientPk = that.util.decode_base58(recipientPk);
}
try {
var ciphertextBin = that.nacl.crypto_box(messageBin, nonce, recipientPk, senderSk);
var ciphertext = that.util.encode_base64(ciphertextBin);
resolve(ciphertext);
}
catch (err) {
reject(err);
}
});
};
/**
* Decrypt a message, from a key pair
*/
this.box_open = function (cypherText, nonce, senderPk, recipientSk) {
return $q(function (resolve, reject) {
if (!cypherText) {
resolve(cypherText);
return;
}
var ciphertextBin = that.util.decode_base64(cypherText);
if (typeof senderPk === "string") {
senderPk = that.util.decode_base58(senderPk);
}
try {
var message = that.nacl.crypto_box_open(ciphertextBin, nonce, senderPk, recipientSk);
resolve(that.nacl.decode_utf8(message));
}
catch (err) {
reject(err);
}
});
};
/**
* Create key pairs (sign and box), from salt+password (Scrypt auth)
*/
this.scryptKeypair = function(salt, password, scryptParams) {
return that.util.crypto_scrypt(
that.util.encode_utf8(password),
that.util.encode_utf8(salt),
scryptParams && scryptParams.N || that.constants.SCRYPT_PARAMS.DEFAULT.N,
scryptParams && scryptParams.r || that.constants.SCRYPT_PARAMS.DEFAULT.r,
scryptParams && scryptParams.p || that.constants.SCRYPT_PARAMS.DEFAULT.p,
that.constants.SEED_LENGTH)
.then(function(seed){
var signKeypair = that.nacl.crypto_sign_seed_keypair(seed);
var boxKeypair = that.nacl.crypto_box_seed_keypair(seed);
return {
signPk: signKeypair.signPk,
signSk: signKeypair.signSk,
boxPk: boxKeypair.boxPk,
boxSk: boxKeypair.boxSk
};
});
};
/**
* Create key pairs from a seed
*/
this.seedKeypair = function(seed) {
return $q(function(resolve, reject) {
var signKeypair = that.nacl.crypto_sign_seed_keypair(seed);
var boxKeypair = that.nacl.crypto_box_seed_keypair(seed);
resolve({
signPk: signKeypair.signPk,
signSk: signKeypair.signSk,
boxPk: boxKeypair.boxPk,
boxSk: boxKeypair.boxSk
});
});
};
/**
* Get sign pk from salt+password (scrypt auth)
*/
this.scryptSignPk = function(salt, password, scryptParams) {
return $q(function(resolve, reject) {
try {
var seed = that.scrypt.crypto_scrypt(
that.util.encode_utf8(password),
that.util.encode_utf8(salt),
scryptParams && scryptParams.N || that.constants.SCRYPT_PARAMS.DEFAULT.N,
scryptParams && scryptParams.r || that.constants.SCRYPT_PARAMS.DEFAULT.r,
scryptParams && scryptParams.p || that.constants.SCRYPT_PARAMS.DEFAULT.p,
that.constants.SEED_LENGTH);
var signKeypair = that.nacl.crypto_sign_seed_keypair(seed);
resolve(signKeypair.signPk);
}
catch(err) {
reject(err);
}
});
};
/**
* Verify a signature of a message, for a pubkey
*/
this.verify = function (message, signature, pubkey) {
return $q(function(resolve, reject) {
var msg = that.util.decode_utf8(message);
var sig = that.util.decode_base64(signature);
var pub = that.util.decode_base58(pubkey);
var sm = new Uint8Array(that.constants.crypto_sign_BYTES + msg.length);
var i;
for (i = 0; i < that.constants.crypto_sign_BYTES; i++) sm[i] = sig[i];
for (i = 0; i < msg.length; i++) sm[i+that.constants.crypto_sign_BYTES] = msg[i];
// Call to verification lib...
var verified = that.nacl.crypto_sign_open(sm, pub) !== null;
resolve(verified);
});
};
/**
* Sign a message, from a key pair
*/
this.sign = function(message, keypair) {
return $q(function(resolve, reject) {
var m = that.util.decode_utf8(message);
var sk = keypair.signSk;
var signedMsg = that.nacl.crypto_sign(m, sk);
var sig = new Uint8Array(that.constants.crypto_sign_BYTES);
for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i];
var signature = that.base64.encode(sig);
resolve(signature);
});
};
this.load = function() {
var deferred = $q.defer();
var naclOptions = {};
var scryptOptions = {};
if (ionic.Platform.grade.toLowerCase()!='a') {
console.info('Reduce NaCl memory to 16mb, because plateform grade is not [a] but [{0}]'.format(ionic.Platform.grade));
naclOptions.requested_total_memory = 16 * 1048576; // 16 Mo
}
var loadedLib = 0;
var checkAllLibLoaded = function() {
loadedLib++;
if (loadedLib === 4) {
that.loaded = true;
deferred.resolve();
}
};
this.async_load_nacl_js(function(lib) {
that.nacl = lib;
checkAllLibLoaded();
}, naclOptions);
this.async_load_scrypt(function(lib) {
that.scrypt = lib;
that.scrypt.requested_total_memory = scryptOptions.requested_total_memory;
checkAllLibLoaded();
}, scryptOptions);
this.async_load_base58(function(lib) {
that.base58 = lib;
checkAllLibLoaded();
});
that.async_load_base64(function(lib) {
that.base64 = lib;
checkAllLibLoaded();
});
return deferred.promise;
};
// Shortcuts
this.util.hash = that.util.hash_sha256;
this.box = {
keypair: {
fromSignKeypair: that.box_keypair_from_sign,
skFromSignSk: that.box_sk_from_sign,
pkFromSignPk: that.box_pk_from_sign
},
pack: that.box,
open: that.box_open
};
/*--
start WORKAROUND - Publish missing functions (see PR js-nacl: https://github.com/tonyg/js-nacl/pull/54)
-- */
function crypto_box_keypair_from_sign_sk(sk) {
var ska = check_injectBytes("crypto_box_keypair_from_sign_sk", "sk", sk,
that.nacl.nacl_raw._crypto_sign_secretkeybytes());
var skb = new Target(that.nacl.nacl_raw._crypto_box_secretkeybytes());
check("_crypto_sign_ed25519_sk_to_curve25519",
that.nacl.nacl_raw._crypto_sign_ed25519_sk_to_curve25519(skb.address, ska));
FREE(ska);
return that.nacl.crypto_box_keypair_from_raw_sk(skb.extractBytes());
}
function crypto_box_pk_from_sign_pk(pk) {
var pka = check_injectBytes("crypto_box_pk_from_sign_pk", "pk", pk,
that.nacl.nacl_raw._crypto_sign_publickeybytes());
var pkb = new Target(that.nacl.nacl_raw._crypto_box_publickeybytes());
check("_crypto_sign_ed25519_pk_to_curve25519",
that.nacl.nacl_raw._crypto_sign_ed25519_pk_to_curve25519(pkb.address, pka));
FREE(pka);
return pkb.extractBytes();
}
function crypto_box_sk_from_sign_sk(sk) {
var ska = check_injectBytes("crypto_box_sk_from_sign_sk", "sk", sk,
that.nacl.nacl_raw._crypto_sign_secretkeybytes());
var skb = new Target(that.nacl.nacl_raw._crypto_box_secretkeybytes());
check("_crypto_sign_ed25519_sk_to_curve25519",
that.nacl.nacl_raw._crypto_sign_ed25519_sk_to_curve25519(skb.address, ska));
FREE(ska);
return skb.extractBytes();
}
function check_length(function_name, what, thing, expected_length) {
if (thing.length !== expected_length) {
throw {message: "nacl." + function_name + " expected " +
expected_length + "-byte " + what + " but got length " + thing.length};
}
}
function check(function_name, result) {
if (result !== 0) {
throw {message: "nacl_raw." + function_name + " signalled an error"};
}
}
function check_injectBytes(function_name, what, thing, expected_length, leftPadding) {
check_length(function_name, what, thing, expected_length);
return injectBytes(thing, leftPadding);
}
function injectBytes(bs, leftPadding) {
var p = leftPadding || 0;
var address = MALLOC(bs.length + p);
that.nacl.nacl_raw.HEAPU8.set(bs, address + p);
for (var i = address; i < address + p; i++) {
that.nacl.nacl_raw.HEAPU8[i] = 0;
}
return address;
}
function MALLOC(nbytes) {
var result = that.nacl.nacl_raw._malloc(nbytes);
if (result === 0) {
throw {message: "malloc() failed", nbytes: nbytes};
}
return result;
}
function FREE(pointer) {
that.nacl.nacl_raw._free(pointer);
}
function free_all(addresses) {
for (var i = 0; i < addresses.length; i++) {
FREE(addresses[i]);
}
}
function extractBytes(address, length) {
var result = new Uint8Array(length);
result.set(that.nacl.nacl_raw.HEAPU8.subarray(address, address + length));
return result;
}
function Target(length) {
this.length = length;
this.address = MALLOC(length);
}
Target.prototype.extractBytes = function (offset) {
var result = extractBytes(this.address + (offset || 0), this.length - (offset || 0));
FREE(this.address);
this.address = null;
return result;
};
/*--
end of WORKAROUND
-- */
}
FullJSServiceFactory.prototype = new CryptoAbstractService();
/* -----------------------------------------------------------------------------------------------------------------
* Service that use Cordova MiniSodium plugin
* ----------------------------------------------------------------------------------------------------------------*/
/***
* Factory for crypto, using Cordova plugin
*/
function CordovaServiceFactory() {
this.id = 'MiniSodium';
// libraries handlers
this.nacl = null; // the cordova plugin
this.base58= null;
this.sha256= null;
var that = this;
// functions
this.util = this.util || {};
this.util.decode_utf8 = function(s) {
return that.nacl.to_string(s);
};
this.util.encode_utf8 = function(s) {
return that.nacl.from_string(s);
};
this.util.encode_base58 = function(a) {
return that.base58.encode(a);
};
this.util.decode_base58 = function(a) {
var i;
var d = that.base58.decode(a);
var b = new Uint8Array(d.length);
for (i = 0; i < d.length; i++) b[i] = d[i];
return b;
};
this.util.decode_base64 = function (a) {
return that.nacl.from_base64(a);
};
this.util.encode_base64 = function (b) {
return that.nacl.to_base64(b);
};
this.util.hash_sha256 = function(message) {
return $q.when(that.sha256(message).toUpperCase());
};
this.util.random_nonce = function() {
var nonce = new Uint8Array(that.constants.crypto_secretbox_NONCEBYTES);
that.crypto.getRandomValues(nonce);
return $q.when(nonce);
};
this.util.crypto_hash_sha256 = function (message) {
return that.nacl.from_hex(that.sha256(message));
};
this.util.crypto_scrypt = function(password, salt, N, r, p, seedLength) {
var deferred = $q.defer();
that.nacl.crypto_pwhash_scryptsalsa208sha256_ll(
password,
salt,
N,
r,
p,
seedLength,
function (err, seed) {
if (err) { deferred.reject(err); return;}
deferred.resolve(seed);
}
);
return deferred.promise;
};
/**
* Create key pairs (sign and box), from salt+password (Scrypt), using cordova
*/
this.scryptKeypair = function(salt, password, scryptParams) {
var deferred = $q.defer();
that.nacl.crypto_pwhash_scryptsalsa208sha256_ll(
that.nacl.from_string(password),
that.nacl.from_string(salt),
scryptParams && scryptParams.N || that.constants.SCRYPT_PARAMS.DEFAULT.N,
scryptParams && scryptParams.r || that.constants.SCRYPT_PARAMS.DEFAULT.r,
scryptParams && scryptParams.p || that.constants.SCRYPT_PARAMS.DEFAULT.p,
that.constants.SEED_LENGTH,
function (err, seed) {
if (err) { deferred.reject(err); return;}
that.nacl.crypto_sign_seed_keypair(seed, function (err, signKeypair) {
if (err) { deferred.reject(err); return;}
var result = {
signPk: signKeypair.pk,
signSk: signKeypair.sk
};
that.box_keypair_from_sign(result)
.then(function(boxKeypair) {
result.boxPk = boxKeypair.pk;
result.boxSk = boxKeypair.sk;
deferred.resolve(result);
})
.catch(function(err) {
deferred.reject(err);
});
});
}
);
return deferred.promise;
};
/**
* Create key pairs from a seed
*/
this.seedKeypair = function(seed) {
var deferred = $q.defer();
that.nacl.crypto_sign_seed_keypair(seed, function (err, signKeypair) {
if (err) { deferred.reject(err); return;}
deferred.resolve({
signPk: signKeypair.pk,
signSk: signKeypair.sk
});
});
return deferred.promise;
};
/**
* Get sign PK from salt+password (Scrypt), using cordova
*/
this.scryptSignPk = function(salt, password, scryptParams) {
var deferred = $q.defer();
that.nacl.crypto_pwhash_scryptsalsa208sha256_ll(
that.nacl.from_string(password),
that.nacl.from_string(salt),
scryptParams && scryptParams.N || that.constants.SCRYPT_PARAMS.DEFAULT.N,
scryptParams && scryptParams.r || that.constants.SCRYPT_PARAMS.DEFAULT.r,
scryptParams && scryptParams.p || that.constants.SCRYPT_PARAMS.DEFAULT.p,
that.constants.SEED_LENGTH,
function (err, seed) {
if (err) { deferred.reject(err); return;}
that.nacl.crypto_sign_seed_keypair(seed, function (err, signKeypair) {
if (err) { deferred.reject(err); return;}
deferred.resolve(signKeypair.pk);
});
}
);
return deferred.promise;
};
/**
* Verify a signature of a message, for a pubkey
*/
this.verify = function (message, signature, pubkey) {
var deferred = $q.defer();
that.nacl.crypto_sign_verify_detached(
that.nacl.from_base64(signature),
that.nacl.from_string(message),
that.nacl.from_base64(pubkey),
function(err, verified) {
if (err) { deferred.reject(err); return;}
deferred.resolve(verified);
});
return deferred.promise;
};
/**
* Sign a message, from a key pair
*/
this.sign = function(message, keypair) {
var deferred = $q.defer();
that.nacl.crypto_sign(
that.nacl.from_string(message), // message
keypair.signSk, // sk
function(err, signedMsg) {
if (err) { deferred.reject(err); return;}
var sig;
if (signedMsg.length > that.constants.crypto_sign_BYTES) {
sig = new Uint8Array(that.constants.crypto_sign_BYTES);
for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i];
}
else {
sig = signedMsg;
}
var signature = that.nacl.to_base64(sig);
deferred.resolve(signature);
});
return deferred.promise;
};
/**
* Compute the box key pair, from a sign key pair
*/
this.box_keypair_from_sign = function(signKeyPair) {
if (signKeyPair.boxSk && signKeyPair.boxPk) return $q.when(signKeyPair);
var deferred = $q.defer();
var result = {};
that.nacl.crypto_sign_ed25519_pk_to_curve25519(signKeyPair.signPk, function(err, boxPk) {
if (err) { deferred.reject(err); return;}
result.boxPk = boxPk;
if (result.boxSk) deferred.resolve(result);
});
that.nacl.crypto_sign_ed25519_sk_to_curve25519(signKeyPair.signSk, function(err, boxSk) {
if (err) { deferred.reject(err); return;}
result.boxSk = boxSk;
if (result.boxPk) deferred.resolve(result);
});
return deferred.promise;
};
/**
* Compute the box public key, from a sign public key
*/
this.box_pk_from_sign = function(signPk) {
var deferred = $q.defer();
that.nacl.crypto_sign_ed25519_pk_to_curve25519(signPk, function(err, boxPk) {
if (err) { deferred.reject(err); return;}
deferred.resolve(boxPk);
});
return deferred.promise;
};
/**
* Compute the box secret key, from a sign secret key
*/
this.box_sk_from_sign = function(signSk) {
var deferred = $q.defer();
that.nacl.crypto_sign_ed25519_sk_to_curve25519(signSk, function(err, boxSk) {
if (err) { deferred.reject(err); return;}
deferred.resolve(boxSk);
});
return deferred.promise;
};
/**
* Encrypt a message, from a key pair
*/
this.box = function(message, nonce, recipientPk, senderSk) {
if (!message) {
return $q.reject('No message');
}
var deferred = $q.defer();
var messageBin = that.nacl.from_string(message);
if (typeof recipientPk === "string") {
recipientPk = that.util.decode_base58(recipientPk);
}
that.nacl.crypto_box_easy(messageBin, nonce, recipientPk, senderSk, function(err, ciphertextBin) {
if (err) { deferred.reject(err); return;}
var ciphertext = that.util.encode_base64(ciphertextBin);
//console.debug('Encrypted message: ' + ciphertext);
deferred.resolve(ciphertext);
});
return deferred.promise;
};
/**
* Decrypt a message, from a key pair
*/
this.box_open = function(cypherText, nonce, senderPk, recipientSk) {
if (!cypherText) {
return $q.reject('No cypherText');
}
var deferred = $q.defer();
var ciphertextBin = that.nacl.from_base64(cypherText);
if (typeof senderPk === "string") {
senderPk = that.util.decode_base58(senderPk);
}
// Avoid crash if content has not the minimal length - Fix #346
if (ciphertextBin.length < that.constants.crypto_box_MACBYTES) {
deferred.reject('Invalid cypher content length');
return;
}
that.nacl.crypto_box_open_easy(ciphertextBin, nonce, senderPk, recipientSk, function(err, message) {
if (err) { deferred.reject(err); return;}
that.util.array_to_string(message, function(result) {
//console.debug('Decrypted text: ' + result);
deferred.resolve(result);
});
});
return deferred.promise;
};
this.load = function() {
var deferred = $q.defer();
if (!window.plugins || !window.plugins.MiniSodium) {
deferred.reject("Cordova plugin 'MiniSodium' not found. Please load Full JS implementation instead.");
}
else {
that.nacl = window.plugins.MiniSodium;
var loadedLib = 0;
var checkAllLibLoaded = function() {
loadedLib++;
if (loadedLib == 2) {
that.loaded = true;
deferred.resolve();
}
};
that.async_load_base58(function(lib) {
that.base58 = lib;
checkAllLibLoaded();
});
that.async_load_sha256(function(lib) {
that.sha256 = lib;
checkAllLibLoaded();
});
}
return deferred.promise;
};
// Shortcuts
this.util.hash = that.util.hash_sha256;
this.box = {
keypair: {
fromSignKeypair: that.box_keypair_from_sign,
skFromSignSk: that.box_sk_from_sign,
pkFromSignPk: that.box_pk_from_sign
},
pack: that.box,
open: that.box_open
};
}
CordovaServiceFactory.prototype = new CryptoAbstractService();
/* -----------------------------------------------------------------------------------------------------------------
* Create service instance
* ----------------------------------------------------------------------------------------------------------------*/
var service = new CryptoAbstractService();
var isDevice = true;
// removeIf(android)
// removeIf(ios)
isDevice = false;
// endRemoveIf(ios)
// endRemoveIf(android)
//console.debug("[crypto] Created CryptotUtils service. device=" + isDevice);
ionicReady().then(function() {
console.debug('[crypto] Starting...');
var now = Date.now();
var serviceImpl;
// Use Cordova plugin implementation, when exists
if (isDevice && window.plugins && window.plugins.MiniSodium && crypto && crypto.getRandomValues) {
console.debug('[crypto] Loading \'MiniSodium\' implementation...');
serviceImpl = new CordovaServiceFactory();
}
else {
console.debug('[crypto] Loading \'FullJS\' implementation...');
serviceImpl = new FullJSServiceFactory();
}
// Load (async lib)
serviceImpl.load()
.catch(function(err) {
console.error(err);
throw err;
})
.then(function() {
service.copy(serviceImpl);
console.debug('[crypto] Loaded \'{0}\' implementation in {1}ms'.format(service.id, Date.now() - now));
});
});
return service;
}])
/* -----------------------------
Crypto advanced service for Cesium
*/
.factory('csCrypto', ['$q', '$rootScope', '$timeout', 'CryptoUtils', 'UIUtils', 'Modals', function($q, $rootScope, $timeout, CryptoUtils, UIUtils, Modals) {
'ngInject';
function test(regexpContent) {
return new RegExp(regexpContent);
}
function xor(a, b) {
var length = Math.max(a.length, b.length);
var buffer = new Uint8Array(length);
for (var i = 0; i < length; ++i) {
buffer[i] = a[i] ^ b[i];
}
return buffer;
}
function concat_Uint8Array( buffer1, buffer2 ) {
var tmp = new Uint8Array( buffer1.byteLength + buffer2.byteLength );
tmp.set( new Uint8Array( buffer1 ), 0 );
tmp.set( new Uint8Array( buffer2 ), buffer1.byteLength );
return tmp;
}
var constants = {
WIF: {
DATA_LENGTH: 35
},
EWIF: {
SALT_LENGTH: 4,
DERIVED_HALF_LENGTH: 16,
DATA_LENGTH: 39,
SCRYPT_PARAMS: {
N: 16384,
r: 8,
p: 8
}
},
REGEXP: {
PUBKEY: '[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}',
SECKEY: '[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{86,88}',
FILE: {
TYPE_LINE: '^Type: ([a-zA-Z0-9]+)\n',
VERSION: 'Version: ([0-9]+)\n',
PUB: '[Pp]ub: ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44})\n',
SEC: '[Ss]ec: ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{86,88})(\n|$)',
DATA: '[Dd]ata: ([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)(\n|$)'
}
}
},
regexp = {
FILE: {
TYPE_LINE: test(constants.REGEXP.FILE.TYPE_LINE),
VERSION: test(constants.REGEXP.FILE.VERSION),
PUB: test(constants.REGEXP.FILE.PUB),
SEC: test(constants.REGEXP.FILE.SEC),
DATA: test(constants.REGEXP.FILE.DATA)
}
},
errorCodes = {
BAD_PASSWORD: 3001,
BAD_CHECKSUM: 3002
};
/* -- keyfile -- */
function readKeyFile(file, options) {
if (file && file.content) {
return parseKeyFileContent(file.content, options);
}
return $q(function(resolve, reject) {
if (!file) {
return reject('Argument [file] is missing');
}
//console.debug('[crypto] [keypair] reading file: ', file);
var reader = new FileReader();
reader.onload = function (event) {
parseKeyFileContent(event.target.result, options)
.then(resolve)
.catch(reject);
};
reader.readAsText(file, 'utf8');
});
}
function parseKeyFileContent(content, options) {
if (!content) return $q.reject('Argument [content] is missing');
options = options || {};
options.withSecret = angular.isDefined(options.withSecret) ? options.withSecret : false;
options.defaultType = options.defaultType || 'PubSec';
var matches;
var typeMatch = regexp.FILE.TYPE_LINE.exec(content);
// If no Type field: add default type
var type = typeMatch && typeMatch[1];
if (!type && options.defaultType) {
return parseKeyFileContent('Type: {0}\n{1}'.format(options.defaultType, content), options);
}
// Type: PubSec
if (type == 'PubSec') {
// Read Pub field
matches = regexp.FILE.PUB.exec(content);
if (!matches) return $q.reject('Missing [pub] field in file, or invalid public key value');
var signKeypair = {
signPk: CryptoUtils.base58.decode(matches[1])
};
if (!options.withSecret) return $q.resolve(signKeypair);
// Read Sec field
matches= regexp.FILE.SEC.exec(content);
if (!matches) return $q.reject('Missing [sec] field in file, or invalid secret key value');
signKeypair.signSk = CryptoUtils.base58.decode(matches[1]);
return $q.resolve(signKeypair);
}
// Type: WIF or EWIF
else if (type == 'WIF' || type == 'EWIF') {
matches = regexp.FILE.DATA.exec(content);
if (!matches) {
return $q.reject('Missing [Data] field in file. This is required for WIF or EWIF format');
}
return parseWIF_or_EWIF(matches[1], {
type: type,
password: options.password
})
.then(function(signKeypair) {
return signKeypair && !options.withSecret ? {signPk: signKeypair.signPk} : signKeypair;
});
}
// Type: unknown
if (options.defaultType) {
return $q.reject('Bad file format: missing Type field');
}
else {
return $q.reject('Bad file format, unknown type [' + type + ']');
}
}
/**
*
* @param data_base58
* @param options
* @returns {*}
*/
function parseWIF_or_EWIF(data_base58, options) {
options = options || {};
var data_int8 = data_base58 && CryptoUtils.base58.decode(data_base58);
if (!data_int8 || data_int8.length != constants.EWIF.DATA_LENGTH && data_int8.length != constants.WIF.DATA_LENGTH) {
return $q.reject('Invalid WIF or EWIF format (invalid bytes count).');
}
// Detect the type from the first byte
options.type = options.type || (data_int8[0] == 1 && 'WIF') || (data_int8[0] == 2 && 'EWIF');
// Type: WIF
if (options.type == 'WIF') {
return parseWIF_v1(data_base58);
}
// Type: EWIF
if (options.type == 'EWIF') {
// If not set, resolve password using the given callback
if (typeof options.password == "function") {
//console.debug("[crypto] [EWIF] Executing 'options.password()' to resolve the password...");
options.password = options.password();
if (!options.password) {
return $q.reject({message: "Invalid callback result for 'options.password()': must return a promise or a string."});
}
}
// If password is a promise, get the result then read data
if (typeof options.password === "object" && options.password.then) {
return options.password.then(function(password) {
if (!password) throw 'CANCELLED';
return parseEWIF_v1(data_base58, password);
});
}
// If password is a valid string, read data
if (typeof options.password == "string") {
return parseEWIF_v1(data_base58, options.password);
}
return $q.reject({message: 'Invalid EWIF options.password. Waiting a callback function, a promise or a string.'});
}
// Unknown type
return $q.reject({message: 'Invalid WIF or EWIF format: unknown first byte identifier.'});
}
function parseWIF_v1(wif_base58) {
var wif_int8 = CryptoUtils.util.decode_base58(wif_base58);
// Check identifier byte = 0x01
if (wif_int8[0] != 1) {
return $q.reject({message: 'Invalid WIF v1 format: expected [0x01] as first byte'});
}
// Check length
if (wif_int8.length != constants.WIF.DATA_LENGTH) {
return $q.reject({message: 'Invalid WIF v1 format: Data must be a '+constants.WIF.DATA_LENGTH+' bytes array, encoded in base 58.'});
}
var wif_int8_no_checksum = wif_int8.slice(0, -2),
seed = wif_int8.slice(1, -2),
checksum = wif_int8.slice(-2);
// Compute expected checksum
var expectedChecksum = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(wif_int8_no_checksum)).slice(0,2);
if (CryptoUtils.util.encode_base58(checksum) != CryptoUtils.util.encode_base58(expectedChecksum)) {
$q.reject({message: 'Invalid WIF format: bad checksum'});
}
// Generate keypair from seed
return CryptoUtils.seedKeypair(seed);
}
function parseEWIF_v1(ewif_base58, password) {
var ewif_int8 = CryptoUtils.util.decode_base58(ewif_base58);
// Check identifier byte = 0x02
if (ewif_int8[0] != 2) {
return $q.reject({message: 'Invalid EWIF v1 format: Expected [0x02] as first byte'});
}
// Check length
if (ewif_int8.length != constants.EWIF.DATA_LENGTH) {
return $q.reject({message: 'Invalid EWIF v1 format: Expected {0} bytes, encoded in base 58.'.format(constants.EWIF.DATA_LENGTH)});
}
var ewif_int8_no_checksum = ewif_int8.slice(0,-2);
var checksum = ewif_int8.slice(-2);
var salt = ewif_int8.slice(1,5);
var encryptedhalf1 = ewif_int8.slice(5,21);
var encryptedhalf2 = ewif_int8.slice(21,37);
// Retrieve the scrypt_seed
return CryptoUtils.util.crypto_scrypt(
CryptoUtils.util.encode_utf8(password),
salt,
constants.EWIF.SCRYPT_PARAMS.N,
constants.EWIF.SCRYPT_PARAMS.r,
constants.EWIF.SCRYPT_PARAMS.p,
64)
// Compute the final seed
.then(function(scrypt_seed) {
var derivedhalf1 = scrypt_seed.slice(0, 32);
var derivedhalf2 = scrypt_seed.slice(32, 64);
//AES
var aesEcb = new aesjs.ModeOfOperation.ecb(derivedhalf2);
var decryptedhalf1 = aesEcb.decrypt(encryptedhalf1);
var decryptedhalf2 = aesEcb.decrypt(encryptedhalf2);
decryptedhalf1 = new Uint8Array(decryptedhalf1);
decryptedhalf2 = new Uint8Array(decryptedhalf2);
//xor
var seed1 = xor(decryptedhalf1, derivedhalf1.slice(0, 16));
var seed2 = xor(decryptedhalf2, derivedhalf1.slice(16, 32));
var seed = concat_Uint8Array(seed1, seed2);
return seed;
})
// Get the keypair, from the seed
.then(CryptoUtils.seedKeypair)
// Do some controls
.then(function(keypair) {
// Check salt
var expectedSalt = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(keypair.signPk)).slice(0,4);
if(CryptoUtils.util.encode_base58(salt) !== CryptoUtils.util.encode_base58(expectedSalt)) {
throw {ucode: errorCodes.BAD_PASSWORD, message: 'ACCOUNT.SECURITY.KEYFILE.ERROR.BAD_PASSWORD'};
}
// Check checksum
var expectedChecksum = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(ewif_int8_no_checksum)).slice(0,2);
if (CryptoUtils.util.encode_base58(checksum) != CryptoUtils.util.encode_base58(expectedChecksum)) {
throw {ucode: errorCodes.BAD_CHECKSUM, message: 'ACCOUNT.SECURITY.KEYFILE.ERROR.BAD_CHECKSUM'};
}
return keypair;
});
}
function wif_v1_from_keypair(keypair) {
var seed = CryptoUtils.seed_from_signSk(keypair.signSk);
if (!seed || seed.byteLength !== CryptoUtils.constants.SEED_LENGTH)
throw "Bad see format. Expected {0} bytes".format(CryptoUtils.constants.SEED_LENGTH);
var fi = new Uint8Array(1);
fi[0] = 0x01;
var seed_fi = concat_Uint8Array(fi, seed);
// checksum
var checksum = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(seed_fi)).slice(0,2);
var wif_int8 = concat_Uint8Array(seed_fi, checksum);
return $q.when(CryptoUtils.util.encode_base58(wif_int8));
}
function ewif_v1_from_keypair(keypair, password) {
var seed = CryptoUtils.seed_from_signSk(keypair.signSk);
if (!seed || seed.byteLength !== CryptoUtils.constants.SEED_LENGTH)
return $q.reject({message: "Bad see format. Expected {0} bytes".format(CryptoUtils.constants.SEED_LENGTH)});
// salt
var salt = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(keypair.signPk)).slice(0,4);
// scrypt_seed
return CryptoUtils.util.crypto_scrypt(
CryptoUtils.util.encode_utf8(password),
salt,
constants.EWIF.SCRYPT_PARAMS.N,
constants.EWIF.SCRYPT_PARAMS.r,
constants.EWIF.SCRYPT_PARAMS.p,
64)
.then(function(scrypt_seed) {
var derivedhalf1 = scrypt_seed.slice(0,32);
var derivedhalf2 = scrypt_seed.slice(32,64);
//XOR & AES
var seed1_xor_derivedhalf1_1 = xor(seed.slice(0,16), derivedhalf1.slice(0,16));
var seed2_xor_derivedhalf1_2 = xor(seed.slice(16,32), derivedhalf1.slice(16,32));
var aesEcb = new aesjs.ModeOfOperation.ecb(derivedhalf2);
var encryptedhalf1 = aesEcb.encrypt(seed1_xor_derivedhalf1_1);
var encryptedhalf2 = aesEcb.encrypt(seed2_xor_derivedhalf1_2);
encryptedhalf1 = new Uint8Array(encryptedhalf1);
encryptedhalf2 = new Uint8Array(encryptedhalf2);
// concatenate ewif
var ewif_int8 = new Uint8Array(1);
ewif_int8[0] = 0x02;
ewif_int8 = concat_Uint8Array(ewif_int8,salt);
ewif_int8 = concat_Uint8Array(ewif_int8,encryptedhalf1);
ewif_int8 = concat_Uint8Array(ewif_int8,encryptedhalf2);
var checksum = CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(ewif_int8)).slice(0,2);
ewif_int8 = concat_Uint8Array(ewif_int8,checksum);
return CryptoUtils.util.encode_base58(ewif_int8);
});
}
function generateKeyFileContent(keypair, options) {
options = options || {};
options.type = options.type || "PubSec";
switch(options.type) {
// PubSec
case "PubSec" :
return $q.resolve(
"Type: PubSec\n" +
"Version: 1\n" +
"pub: " + CryptoUtils.base58.encode(keypair.signPk) + "\n" +
"sec: " + CryptoUtils.base58.encode(keypair.signSk) + "\n");
// WIF - v1
case "WIF" :
return wif_v1_from_keypair(keypair)
.then(function(data) {
return "Type: WIF\n" +
"Version: 1\n" +
"Data: " + data + "\n";
});
// EWIF - v1
case "EWIF" :
if (!options.password) return $q.reject({message: 'Missing EWIF options.password.'});
// If not set, resolve password using the given callback
if (options.password && typeof options.password == "function") {
console.debug("[crypto] [EWIF] Executing 'options.password()' to resolve the password...");
options.password = options.password();
if (!options.password) {
return $q.reject({message: "Invalid callback result for 'options.password()': must return a promise or a string."});
}
}
// If password is a promise, get the result then read data
if (options.password && typeof options.password == "object" && options.password.then) {
return options.password.then(function(password) {
if (!password) throw 'CANCELLED';
// Recursive call, with the string password in options
return generateKeyFileContent(keypair, angular.merge({}, options, {password: password}));
});
}
// If password is a valid string, read data
if (options.password && typeof options.password == "string") {
return ewif_v1_from_keypair(keypair, options.password)
.then(function(data) {
return "Type: EWIF\n" +
"Version: 1\n" +
"Data: " + data + "\n";
});
}
return $q.reject({message: 'Invalid EWIF options.password. Waiting a callback function, a promise or a string.'});
default:
return $q.reject({message: "Unknown keyfile format: " + options.type});
}
}
/* -- usefull methods -- */
function pkChecksum(pubkey) {
var signPk_int8 = CryptoUtils.util.decode_base58(pubkey);
return CryptoUtils.util.encode_base58(CryptoUtils.util.crypto_hash_sha256(CryptoUtils.util.crypto_hash_sha256(signPk_int8))).substring(0,3);
}
/* -- box (pack/unpack a record) -- */
function getBoxKeypair(keypair) {
if (!keypair) {
throw new Error('Missing keypair');
}
if (keypair.boxPk && keypair.boxSk) {
return $q.when(keypair);
}
return $q.all([
CryptoUtils.box.keypair.skFromSignSk(keypair.signSk),
CryptoUtils.box.keypair.pkFromSignPk(keypair.signPk)
])
.then(function(res) {
return {
boxSk: res[0],
boxPk: res[1]
};
});
}
function packRecordFields(record, keypair, recipientFieldName, cypherFieldNames, nonce) {
recipientFieldName = recipientFieldName || 'recipient';
if (!record[recipientFieldName]) {
return $q.reject({message:'ES_WALLET.ERROR.RECIPIENT_IS_MANDATORY'});
}
cypherFieldNames = cypherFieldNames || 'content';
if (typeof cypherFieldNames == 'string') {
cypherFieldNames = [cypherFieldNames];
}
// Work on a copy, to keep the original record (as it could be use again - fix #382)
record = angular.copy(record);
// Get recipient
var recipientPk = CryptoUtils.util.decode_base58(record[recipientFieldName]);
return $q.all([
getBoxKeypair(keypair),
CryptoUtils.box.keypair.pkFromSignPk(recipientPk),
nonce ? $q.when(nonce) : CryptoUtils.util.random_nonce()
])
.then(function(res) {
//var senderSk = res[0];
var boxKeypair = res[0];
var senderSk = boxKeypair.boxSk;
var boxRecipientPk = res[1];
var nonce = res[2];
return $q.all(
cypherFieldNames.reduce(function(res, fieldName) {
if (!record[fieldName]) return res; // skip undefined fields
return res.concat(
CryptoUtils.box.pack(record[fieldName], nonce, boxRecipientPk, senderSk)
);
}, []))
.then(function(cypherTexts){
// Replace field values with cypher texts
var i = 0;
_.forEach(cypherFieldNames, function(cypherFieldName) {
if (!record[cypherFieldName]) {
// Force undefined fields to be present in object
// This is better for ES storage, that always works on lazy update mode
record[cypherFieldName] = null;
}
else {
record[cypherFieldName] = cypherTexts[i++];
}
});
// Set nonce
record.nonce = CryptoUtils.util.encode_base58(nonce);
return record;
});
});
}
function openRecordFields(records, keypair, issuerFieldName, cypherFieldNames) {
issuerFieldName = issuerFieldName || 'issuer';
cypherFieldNames = cypherFieldNames || 'content';
if (typeof cypherFieldNames == 'string') {
cypherFieldNames = [cypherFieldNames];
}
var now = Date.now();
var issuerBoxPks = {}; // a map used as cache
var jobs = [getBoxKeypair(keypair)];
return $q.all(records.reduce(function(jobs, message) {
var issuer = message[issuerFieldName];
if (!issuer) {throw 'Record has no ' + issuerFieldName;}
if (issuerBoxPks[issuer]) return res;
return jobs.concat(
CryptoUtils.box.keypair.pkFromSignPk(CryptoUtils.util.decode_base58(issuer))
.then(function(issuerBoxPk) {
issuerBoxPks[issuer] = issuerBoxPk; // fill box pk cache
}));
}, jobs))
.then(function(res){
var boxKeypair = res[0];
return $q.all(records.reduce(function(jobs, record) {
var issuerBoxPk = issuerBoxPks[record[issuerFieldName]];
var nonce = CryptoUtils.util.decode_base58(record.nonce);
record.valid = true;
return jobs.concat(
cypherFieldNames.reduce(function(res, cypherFieldName) {
if (!record[cypherFieldName]) return res;
return res.concat(CryptoUtils.box.open(record[cypherFieldName], nonce, issuerBoxPk, boxKeypair.boxSk)
.then(function(text) {
record[cypherFieldName] = text;
})
.catch(function(err){
console.error(err);
console.warn('[ES] [crypto] a record may have invalid cypher ' + cypherFieldName);
record.valid = false;
}));
}, []));
}, []));
})
.then(function() {
console.debug('[ES] [crypto] All record decrypted in ' + (Date.now() - now) + 'ms');
return records;
});
}
function parseKeyFileData(data, options){
options = options || {};
options.withSecret = angular.isDefined(options.withSecret) ? options.withSecret : true;
options.silent = angular.isDefined(options.withSecret) ? options.silent : false;
options.password = function() {
return UIUtils.loading.hide(100)
.then(function() {
return Modals.showPassword({
title: 'ACCOUNT.SECURITY.KEYFILE.PASSWORD_POPUP.TITLE',
subTitle: 'ACCOUNT.SECURITY.KEYFILE.PASSWORD_POPUP.HELP',
error: options.error,
scope: options.scope
});
})
.then(function(password) {
// Timeout is need to force popup to be hide
return $timeout(function() {
if (password) UIUtils.loading.show();
return password;
}, 150);
});
};
if (!options.silent) {
UIUtils.loading.show();
}
return parseWIF_or_EWIF(data, options)
.then(function(res){
return res;
})
.catch(function(err) {
if (err && err === 'CANCELLED') return;
if (err && err.ucode == errorCodes.BAD_PASSWORD) {
// recursive call
return parseKeyFileData(data, {withSecret: options.withSecret, error: 'ACCOUNT.SECURITY.KEYFILE.ERROR.BAD_PASSWORD'});
}
console.error("[crypto] Unable to parse as WIF or EWIF format: " + (err && err.message || err));
throw err; // rethrow
});
}
// exports
return {
errorCodes: errorCodes,
constants: constants,
// copy CryptoUtils
util: angular.extend({
pkChecksum: pkChecksum
}, CryptoUtils.util),
keyfile: {
read: readKeyFile,
parseData: parseKeyFileData,
generateContent: generateKeyFileContent
},
box: {
getKeypair: getBoxKeypair,
pack: packRecordFields,
open: openRecordFields
}
};
}])
;
angular.module('cesium.utils.services', [])
// Replace the '$ionicPlatform.ready()', to enable multiple calls
// See http://stealthcode.co/multiple-calls-to-ionicplatform-ready/
.factory('ionicReady', ['$ionicPlatform', function($ionicPlatform) {
'ngInject';
var readyPromise;
return function () {
if (!readyPromise) {
readyPromise = $ionicPlatform.ready();
}
return readyPromise;
};
}])
.factory('UIUtils', ['$ionicLoading', '$ionicPopup', '$ionicConfig', '$ionicHistory', '$translate', '$q', 'ionicMaterialInk', 'ionicMaterialMotion', '$window', '$timeout', 'Fullscreen', '$ionicPopover', '$state', '$rootScope', 'screenmatch', function($ionicLoading, $ionicPopup, $ionicConfig, $ionicHistory, $translate, $q,
ionicMaterialInk, ionicMaterialMotion, $window, $timeout, Fullscreen,
$ionicPopover, $state, $rootScope, screenmatch) {
'ngInject';
var
loadingTextCache=null,
CONST = {
MAX_HEIGHT: 480,
MAX_WIDTH: 640,
THUMB_MAX_HEIGHT: 200,
THUMB_MAX_WIDTH: 200
},
data = {
smallscreen: screenmatch.bind('xs, sm', $rootScope)
},
exports,
raw = {}
;
function alertError(err, subtitle) {
if (!err) {
return $q.when();
}
return $q(function(resolve) {
$translate([err, subtitle, 'ERROR.POPUP_TITLE', 'ERROR.UNKNOWN_ERROR', 'COMMON.BTN_OK'])
.then(function (translations) {
var message = err.message || translations[err];
return $ionicPopup.show({
template: '<p>' + (message || translations['ERROR.UNKNOWN_ERROR']) + '</p>',
title: translations['ERROR.POPUP_TITLE'],
subTitle: translations[subtitle],
buttons: [
{
text: '<b>'+translations['COMMON.BTN_OK']+'</b>',
type: 'button-assertive',
onTap: function(e) {
resolve(e);
}
}
]
});
});
});
}
function alertInfo(message, subtitle) {
return $q(function(resolve) {
$translate([message, subtitle, 'INFO.POPUP_TITLE', 'COMMON.BTN_OK'])
.then(function (translations) {
$ionicPopup.show({
template: '<p>' + translations[message] + '</p>',
title: translations['INFO.POPUP_TITLE'],
subTitle: translations[subtitle],
buttons: [
{
text: translations['COMMON.BTN_OK'],
type: 'button-positive',
onTap: function(e) {
resolve(e);
}
}
]
});
});
});
}
function alertNotImplemented() {
return alertInfo('INFO.FEATURES_NOT_IMPLEMENTED');
}
function askConfirm(message, title, options) {
title = title || 'CONFIRM.POPUP_TITLE';
options = options || {};
options.cssClass = options.cssClass || 'confirm';
options.okText = options.okText || 'COMMON.BTN_OK';
options.cancelText = options.cancelText || 'COMMON.BTN_CANCEL';
return $translate([message, title, options.cancelText, options.okText])
.then(function (translations) {
return $ionicPopup.confirm({
template: translations[message],
cssClass: options.cssClass,
title: translations[title],
cancelText: translations[options.cancelText],
cancelType: options.cancelType,
okText: translations[options.okText],
okType: options.okType
});
});
}
function hideLoading(timeout){
if (timeout) {
return $timeout(function(){
return $ionicLoading.hide();
}, timeout);
}
else {
return $ionicLoading.hide();
}
}
function showLoading(options) {
if (!loadingTextCache) {
return $translate('COMMON.LOADING')
.then(function(translation){
loadingTextCache = translation;
return showLoading(options);
});
}
options = options || {};
options.template = options.template||loadingTextCache;
return $ionicLoading.show(options);
}
function updateLoading(options) {
return $ionicLoading._getLoader().then(function(loader) {
if (!loader || !loader.isShown) return;
// Translate template (if exists)
if (options && options.template) {
return $translate(options && options.template)
.then(function(template) {
options.template = template;
return loader;
});
}
})
.then(function(loader) {
if (loader && loader.isShown) return showLoading(options);
});
}
function showToast(message, duration, position) {
duration = duration || 'short';
position = position || 'bottom';
return $translate([message])
.then(function(translations){
// removeIf(device)
// Use the $ionicLoading toast.
// First, make sure to convert duration in number
if (typeof duration == 'string') {
if (duration == 'short') {
duration = 2000;
}
else {
duration = 5000;
}
}
return $ionicLoading.show({ template: translations[message], noBackdrop: true, duration: duration });
// endRemoveIf(device)
});
}
function onError(msg, reject/*optional*/) {
return function(err) {
var fullMsg = msg;
var subtitle;
if (!!err && !!err.message) {
fullMsg = err.message;
subtitle = msg;
}
else if (!msg){
fullMsg = err;
}
// If reject has been given, use it
if (!!reject) {
reject(fullMsg);
}
// If just a user cancellation: silent
else if (fullMsg == 'CANCELLED') {
return hideLoading(10); // timeout, to avoid bug on transfer (when error on reference)
}
// Otherwise, log to console and display error
else {
hideLoading(10); // timeout, to avoid bug on transfer (when error on reference)
return alertError(fullMsg, subtitle);
}
};
}
function isSmallScreen() {
return data.smallscreen.active;
}
function selectElementText(el) {
if (el.value || el.type == "text" || el.type == "textarea") {
// Source: http://stackoverflow.com/questions/14995884/select-text-on-input-focus
if ($window.getSelection && !$window.getSelection().toString()) {
el.setSelectionRange(0, el.value.length);
}
}
else {
if (el.childNodes && el.childNodes.length > 0) {
selectElementText(el.childNodes[0]);
}
else {
// See http://www.javascriptkit.com/javatutors/copytoclipboard.shtml
var range = $window.document.createRange(); // create new range object
range.selectNodeContents(el); // set range to encompass desired element text
var selection = $window.getSelection(); // get Selection object from currently user selected text
selection.removeAllRanges(); // unselect any user selected text (if any)
selection.addRange(range); // add range to Selection object to select it
}
}
}
function getSelectionText(){
var selectedText = "";
if (window.getSelection){ // all modern browsers and IE9+
selectedText = $window.getSelection().toString();
}
return selectedText;
}
function imageOnLoadResize(resolve, reject, thumbnail) {
return function(event) {
var width = event.target.width,
height = event.target.height,
maxWidth = (thumbnail ? CONST.THUMB_MAX_WIDTH : CONST.MAX_WIDTH),
maxHeight = (thumbnail ? CONST.THUMB_MAX_HEIGHT : CONST.MAX_HEIGHT)
;
var canvas = document.createElement("canvas");
var ctx;
// Thumbnail: resize and crop (to the expected size)
if (thumbnail) {
// landscape
if (width > height) {
width *= maxHeight / height;
height = maxHeight;
}
// portrait
else {
height *= maxWidth / width;
width = maxWidth;
}
canvas.width = maxWidth;
canvas.height = maxHeight;
ctx = canvas.getContext("2d");
var xoffset = Math.trunc((maxWidth - width) / 2 + 0.5);
var yoffset = Math.trunc((maxHeight - height) / 2 + 0.5);
ctx.drawImage(event.target,
xoffset, // x1
yoffset, // y1
maxWidth + -2 * xoffset, // x2
maxHeight + -2 * yoffset // y2
);
}
// Resize, but keep the full image
else {
// landscape
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
}
// portrait
else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
ctx = canvas.getContext("2d");
// Resize the whole image
ctx.drawImage(event.target, 0, 0, canvas.width, canvas.height);
}
var dataurl = canvas.toDataURL();
canvas.remove();
resolve(dataurl);
};
}
function resizeImageFromFile(file, thumbnail) {
var img = document.createElement("img");
return $q(function(resolve, reject) {
if (file) {
var reader = new FileReader();
reader.onload = function(event){
img.onload = imageOnLoadResize(resolve, reject, thumbnail);
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
else {
reject('no file to resize');
}
})
.then(function(dataurl) {
img.remove();
return dataurl;
})
;
}
function resizeImageFromSrc(imageSrc, thumbnail) {
var img = document.createElement("img");
return $q(function(resolve, reject) {
img.onload = imageOnLoadResize(resolve, reject, thumbnail);
img.src = imageSrc;
})
.then(function(data){
img.remove();
return data;
});
}
function imageOnLoadRotate(resolve, reject) {
var deg = Math.PI / 180;
var angle = 90 * deg;
return function(event) {
var width = event.target.width;
var height = event.target.height;
var maxWidth = CONST.MAX_WIDTH;
var maxHeight = CONST.MAX_HEIGHT;
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
var canvas = document.createElement("canvas");
canvas.width = height;
canvas.height = width;
var ctx = canvas.getContext("2d");
ctx.rotate(angle);
ctx.drawImage(event.target, 0, (-1) * canvas.width);
var dataurl = canvas.toDataURL();
canvas.remove();
resolve(dataurl);
};
}
function rotateFromSrc(imageSrc, angle) {
var img = document.createElement("img");
return $q(function(resolve, reject) {
img.onload = imageOnLoadRotate(resolve, reject, angle);
img.src = imageSrc;
})
.then(function(data){
img.remove();
return data;
});
}
function showPopover(event, options) {
var deferred = $q.defer();
options = options || {};
options.templateUrl = options.templateUrl ? options.templateUrl : 'templates/common/popover_copy.html';
options.scope = options.scope || $rootScope;
options.scope.popovers = options.scope.popovers || {};
options.autoselect = options.autoselect || false;
options.autoremove = angular.isDefined(options.autoremove) ? options.autoremove : true;
options.backdropClickToClose = angular.isDefined(options.backdropClickToClose) ? options.backdropClickToClose : true;
options.focusFirstInput = angular.isDefined(options.focusFirstInput) ? options.focusFirstInput : false;
var _show = function(popover) {
popover = popover || options.scope.popovers[options.templateUrl];
popover.isResolved=false;
popover.deferred=deferred;
popover.options=options;
// Fill the popover scope
if (options.bindings) {
angular.merge(popover.scope, options.bindings);
}
$timeout(function() { // This is need for Firefox
popover.show(event)
.then(function() {
var element;
// Auto select text
if (options.autoselect) {
element = document.querySelectorAll(options.autoselect)[0];
if (element) {
if ($window.getSelection && !$window.getSelection().toString()) {
element.setSelectionRange(0, element.value.length);
element.focus();
}
else {
element.focus();
}
}
}
else {
// Auto focus on a element
if (options.autofocus) {
element = document.querySelectorAll(options.autofocus)[0];
if (element) element.focus();
}
}
popover.scope.$parent.$emit('popover.shown');
// Callback 'afterShow'
if (options.afterShow) options.afterShow(popover);
});
});
};
var _cleanup = function(popover) {
popover = popover || options.scope.popovers[options.templateUrl];
if (popover) {
delete options.scope.popovers[options.templateUrl];
// Remove the popover
popover.remove()
// Workaround for issue #244
// See also https://github.com/driftyco/ionic-v1/issues/71
// and https://github.com/driftyco/ionic/issues/9069
.then(function() {
var bodyEl = angular.element($window.document.querySelectorAll('body')[0]);
bodyEl.removeClass('popover-open');
});
}
};
var popover = options.scope.popovers[options.templateUrl];
if (!popover) {
$ionicPopover.fromTemplateUrl(options.templateUrl, {
scope: options.scope,
backdropClickToClose: options.backdropClickToClose
})
.then(function (popover) {
popover.isResolved = false;
popover.scope.closePopover = function(result) {
var autoremove = popover.options && popover.options.autoremove;
if (popover.options) delete popover.options.autoremove; // remove to avoid to trigger 'popover.hidden'
popover.hide()
.then(function() {
if (autoremove) {
return _cleanup(popover);
}
})
.then(function() {
if (popover.deferred) {
popover.deferred.resolve(result);
}
delete popover.deferred;
delete popover.options;
});
};
// Execute action on hidden popover
popover.scope.$on('popover.hidden', function() {
if (popover.options && popover.options.afterHidden) {
popover.options.afterHidden();
}
if (popover.options && popover.options.autoremove) {
_cleanup(popover);
}
});
// Cleanup the popover when hidden
options.scope.$on('$remove', function() {
if (popover.deferred) {
popover.deferred.resolve();
}
_cleanup();
});
options.scope.popovers[options.templateUrl] = popover;
_show(popover);
});
}
else {
_show(popover);
}
return deferred.promise;
}
function showCopyPopover(event, value) {
var rows = value && value.indexOf('\n') >= 0 ? value.split('\n').length : 1;
return showPopover(event, {
templateUrl: 'templates/common/popover_copy.html',
bindings: {
value: value,
rows: rows
},
autoselect: '.popover-copy ' + (rows <= 1 ? 'input' : 'textarea')
});
}
function showSharePopover(event, options) {
options = options || {};
options.templateUrl = options.templateUrl ? options.templateUrl : 'templates/common/popover_share.html';
options.autoselect = options.autoselect || '.popover-share input';
options.bindings = options.bindings || {};
options.bindings.value = options.bindings.value || options.bindings.url ||
$state.href($state.current, $state.params, {absolute: true});
options.bindings.postUrl = options.bindings.postUrl || options.bindings.value;
options.bindings.postMessage = options.bindings.postMessage || '';
options.bindings.titleKey = options.bindings.titleKey || 'COMMON.POPOVER_SHARE.TITLE';
return showPopover(event, options);
}
function showHelptip(id, options) {
var element = (typeof id == 'string') && id ? $window.document.getElementById(id) : id;
if (!id && !element && options.selector) {
element = $window.document.querySelector(options.selector);
}
options = options || {};
var deferred = options.deferred || $q.defer();
if(element && !options.timeout) {
if (options.preAction) {
element[options.preAction]();
}
options.templateUrl = options.templateUrl ? options.templateUrl : 'templates/common/popover_helptip.html';
options.autofocus = options.autofocus || '#helptip-btn-ok';
options.bindings = options.bindings || {};
options.bindings.icon = options.bindings.icon || {};
options.bindings.icon.position = options.bindings.icon.position || false;
options.bindings.icon.glyph = options.bindings.icon.glyph ||
(options.bindings.icon.position && options.bindings.icon.position.startsWith('bottom-') ? 'ion-arrow-down-c' :'ion-arrow-up-c');
options.bindings.icon.class = options.bindings.icon.class || 'calm icon ' + options.bindings.icon.glyph;
options.bindings.tour = angular.isDefined(options.bindings.tour) ? options.bindings.tour : false;
showPopover(element, options)
.then(function(result){
if (options.postAction) {
element[options.postAction]();
}
deferred.resolve(result);
})
.catch(function(err){
if (options.postAction) {
element[options.postAction]();
}
deferred.reject(err);
});
}
else {
// Do timeout if ask
if (options.timeout) {
var timeout = options.timeout;
options.retryTimeout = options.retryTimeout || timeout;
delete options.timeout;
options.deferred = deferred;
$timeout(function () {
showHelptip(id, options);
}, timeout);
}
// No element: reject
else if (angular.isDefined(options.retry) && !options.retry) {
if (options.onError === 'continue') {
$timeout(function () {
deferred.resolve(true);
});
}
else {
$timeout(function () {
deferred.reject("[helptip] element now found: " + id);
});
}
}
// Retry until element appears
else {
options.retry = angular.isUndefined(options.retry) ? 2 : (options.retry-1);
options.deferred = deferred;
$timeout(function() {
showHelptip(id, options);
}, options.timeout || options.retryTimeout || 100);
}
}
return deferred.promise;
}
function showFab(id, timeout) {
if (!timeout) {
timeout = 900;
}
$timeout(function () {
// Could not use 'getElementById', because it return only once element,
// but many fabs can have the same id (many view could be loaded at the same time)
var fabs = document.getElementsByClassName('button-fab');
_.forEach(fabs, function(fab){
if (fab.id == id) {
fab.classList.toggle('on', true);
}
});
}, timeout);
}
function hideFab(id, timeout) {
if (!timeout) {
timeout = 10;
}
$timeout(function () {
// Could not use 'getElementById', because it return only once element,
// but many fabs can have the same id (many view could be loaded at the same time)
var fabs = document.getElementsByClassName('button-fab');
_.forEach(fabs, function(fab){
if (fab.id == id) {
fab.classList.toggle('on', false);
}
});
}, timeout);
}
function motionDelegate(delegate, ionListClass) {
var motionTimeout = isSmallScreen() ? 100 : 10;
var defaultSelector = '.list.{0} .item, .list .{0} .item'.format(ionListClass, ionListClass);
return {
ionListClass: ionListClass,
show: function(options) {
options = options || {};
options.selector = options.selector || defaultSelector;
options.ink = angular.isDefined(options.ink) ? options.ink : true;
options.startVelocity = options.startVelocity || (isSmallScreen() ? 1100 : 3000);
return $timeout(function(){
// Display ink effect (no selector need)
if (options.ink) exports.ink();
// Display the delegated motion effect
delegate(options);
}, options.timeout || motionTimeout);
}
};
}
function setEffects(enable) {
if (exports.motion.enable === enable) return; // same
console.debug('[UI] [effects] ' + (enable ? 'Enable' : 'Disable'));
exports.motion.enable = enable;
if (enable) {
$ionicConfig.views.transition('platform');
angular.merge(exports.motion, raw.motion);
}
else {
$ionicConfig.views.transition('none');
var nothing = {
class: undefined,
show: function(){}
};
angular.merge(exports.motion, {
enable : false,
default: nothing,
fadeSlideIn: nothing,
fadeSlideInRight: nothing,
panInLeft: nothing,
pushDown: nothing,
ripple: nothing,
slideUp: nothing,
fadeIn: nothing,
toggleOn: toggleOn,
toggleOff: toggleOff
});
$rootScope.motion = nothing;
}
$ionicHistory.clearCache();
}
raw.motion = {
enable: true,
default: motionDelegate(ionicMaterialMotion.ripple, 'animate-ripple'),
blinds: motionDelegate(ionicMaterialMotion.blinds, 'animate-blinds'),
fadeSlideIn: motionDelegate(ionicMaterialMotion.fadeSlideIn, 'animate-fade-slide-in'),
fadeSlideInRight: motionDelegate(ionicMaterialMotion.fadeSlideInRight, 'animate-fade-slide-in-right'),
panInLeft: motionDelegate(ionicMaterialMotion.panInLeft, 'animate-pan-in-left'),
pushDown: motionDelegate(ionicMaterialMotion.pushDown, 'push-down'),
ripple: motionDelegate(ionicMaterialMotion.ripple, 'animate-ripple'),
slideUp: motionDelegate(ionicMaterialMotion.slideUp, 'slide-up'),
fadeIn: motionDelegate(function(options) {
toggleOn(options);
}, 'fade-in'),
toggleOn: toggleOn,
toggleOff: toggleOff
};
function toggleOn(options, timeout) {
// We have a single option, so it may be passed as a string or property
if (typeof options === 'string') {
options = {
selector: options
};
}
// Fail early & silently log
var isInvalidSelector = typeof options.selector === 'undefined' || options.selector === '';
if (isInvalidSelector) {
console.error('invalid toggleOn selector');
return false;
}
$timeout(function () {
var elements = document.querySelectorAll(options.selector);
if (elements) _.forEach(elements, function(element){
element.classList.toggle('on', true);
});
}, timeout || 100);
}
function toggleOff(options, timeout) {
// We have a single option, so it may be passed as a string or property
if (typeof options === 'string') {
options = {
selector: options
};
}
// Fail early & silently log
var isInvalidSelector = typeof options.selector === 'undefined' || options.selector === '';
if (isInvalidSelector) {
console.error('invalid toggleOff selector');
return false;
}
$timeout(function () {
var elements = document.querySelectorAll(options.selector);
if (elements) _.forEach(elements, function(element){
element.classList.toggle('on', false);
});
}, timeout || 900);
}
exports = {
alert: {
error: alertError,
info: alertInfo,
confirm: askConfirm,
notImplemented: alertNotImplemented
},
loading: {
show: showLoading,
hide: hideLoading,
update: updateLoading
},
toast: {
show: showToast
},
onError: onError,
screen: {
isSmall: isSmallScreen,
fullscreen: Fullscreen
},
ink: ionicMaterialInk.displayEffect,
motion: raw.motion,
setEffects: setEffects,
fab: {
show: showFab,
hide: hideFab
},
popover: {
show: showPopover,
copy: showCopyPopover,
share: showSharePopover,
helptip: showHelptip
},
selection: {
select: selectElementText,
get: getSelectionText
},
image: {
resizeFile: resizeImageFromFile,
resizeSrc: resizeImageFromSrc,
rotateSrc: rotateFromSrc
},
raw: raw
};
return exports;
}])
// See http://plnkr.co/edit/vJQXtsZiX4EJ6Uvw9xtG?p=preview
.factory('$focus', ['$timeout', '$window', function($timeout, $window) {
'ngInject';
return function(id) {
// timeout makes sure that it is invoked after any other event has been triggered.
// e.g. click events that need to run before the focus or
// inputs elements that are in a disabled state but are enabled when those events
// are triggered.
$timeout(function() {
var element = $window.document.getElementById(id);
if(element)
element.focus();
});
};
}])
;
angular.module('cesium.cache.services', ['ngResource', 'angular-cache'])
.factory('csCache', ['$http', 'csSettings', 'CacheFactory', function($http, csSettings, CacheFactory) {
'ngInject';
var
constants = {
LONG: 1 * 60 * 60 * 1000 /*5 min*/,
SHORT: csSettings.defaultSettings.cacheTimeMs
},
cacheNames = []
;
function getOrCreateCache(prefix, maxAge, onExpire){
prefix = prefix || 'csCache-';
maxAge = maxAge || constants.SHORT;
var cacheName = prefix + maxAge;
if (!onExpire) {
if (!cacheNames[cacheName]) {
cacheNames[cacheName] = true;
}
return CacheFactory.get(cacheName) ||
CacheFactory.createCache(cacheName, {
maxAge: maxAge,
deleteOnExpire: 'aggressive',
//cacheFlushInterval: 60 * 60 * 1000, // clear itself every hour
recycleFreq: Math.max(maxAge - 1000, 5 * 60 * 1000 /*5min*/),
storageMode: 'memory'
// FIXME : enable this when cache is cleaning on rollback
//csSettings.data.useLocalStorage ? 'localStorage' : 'memory'
});
}
else {
var counter = 1;
while(CacheFactory.get(cacheName + counter)) {
counter++;
}
cacheName = cacheName + counter;
if (!cacheNames[cacheName]) {
cacheNames[cacheName] = true;
}
return CacheFactory.createCache(cacheName, {
maxAge: maxAge,
deleteOnExpire: 'aggressive',
//cacheFlushInterval: 60 * 60 * 1000, // This cache will clear itself every hour
recycleFreq: maxAge,
onExpire: onExpire,
storageMode: 'memory'
// FIXME : enable this when cache is cleaning on rollback
//csSettings.data.useLocalStorage ? 'localStorage' : 'memory'
});
}
}
function clearAllCaches() {
console.debug("[cache] cleaning all caches");
_.forEach(_.keys(cacheNames), function(cacheName) {
var cache = CacheFactory.get(cacheName);
if (cache) {
cache.removeAll();
}
});
}
function clearFromPrefix(cachePrefix) {
_.forEach(_.keys(cacheNames), function(cacheName) {
if (cacheName.startsWith(cachePrefix)) {
var cache = CacheFactory.get(cacheNames);
if (cache) {
cache.removeAll();
}
}
});
}
return {
get: getOrCreateCache,
clear: clearFromPrefix,
clearAll: clearAllCaches,
constants: {
LONG : constants.LONG,
SHORT: constants.SHORT
}
};
}])
;
angular.module('cesium.modal.services', [])
// Useful for modal with no controller
.controller('EmptyModalCtrl', function () {
'ngInject';
})
.controller('AboutModalCtrl', ['$scope', 'UIUtils', 'csHttp', function ($scope, UIUtils, csHttp) {
'ngInject';
$scope.openLink = function(event, uri, options) {
options = options || {};
// If unable to open, just copy value
options.onError = function() {
return UIUtils.popover.copy(event, uri);
};
return csHttp.uri.open(uri, options);
};
}])
.factory('ModalUtils', ['$ionicModal', '$rootScope', '$q', '$injector', '$controller', '$timeout', function($ionicModal, $rootScope, $q, $injector, $controller, $timeout) {
'ngInject';
function _evalController(ctrlName) {
var result = {
isControllerAs: false,
controllerName: '',
propName: ''
};
var fragments = (ctrlName || '').trim().split(/\s+/);
result.isControllerAs = fragments.length === 3 && (fragments[1] || '').toLowerCase() === 'as';
if (result.isControllerAs) {
result.controllerName = fragments[0];
result.propName = fragments[2];
} else {
result.controllerName = ctrlName;
}
return result;
}
function DefaultModalController($scope, deferred, parameters) {
$scope.deferred = deferred || $q.defer();
$scope.resolved = false;
$scope.openModal = function () {
return $scope.modal.show();
};
$scope.closeModal = function (result) {
$scope.resolved = true;
return $scope.modal.remove()
.then(function() {
$scope.deferred.resolve(result);
return result;
});
};
// Useful method for modal with forms
$scope.setForm = function (form, propName) {
if (propName) {
$scope[propName] = form;
}
else {
$scope.form = form;
}
};
// Useful method for modal to get input parameters
$scope.getParameters = function () {
return parameters;
};
$scope.$on('modal.hidden', function () {
// If not resolved yet: send result
// (after animation out)
if (!$scope.resolved) {
$scope.resolved = true;
$timeout(function() {
$scope.deferred.resolve();
return $scope.modal.remove();
}, ($scope.modal.hideDelay || 320) + 20);
}
});
}
function show(templateUrl, controller, parameters, options) {
var deferred = $q.defer();
options = options ? options : {} ;
options.animation = options.animation || 'slide-in-up';
// If modal has a controller
if (controller) {
// If a controller defined, always use a new scope
options.scope = options.scope ? options.scope.$new() : $rootScope.$new();
DefaultModalController.call({}, options.scope, deferred, parameters);
// Invoke the controller on this new scope
var locals = { '$scope': options.scope, 'parameters': parameters };
var ctrlEval = _evalController(controller);
var ctrlInstance = $controller(controller, locals);
if (ctrlEval.isControllerAs) {
ctrlInstance.openModal = options.scope.openModal;
ctrlInstance.closeModal = options.scope.closeModal;
}
}
$ionicModal.fromTemplateUrl(templateUrl, options)
.then(function (modal) {
if (controller) {
// Set modal into the controller's scope
modal.scope.$parent.modal = modal;
}
else {
var scope = modal.scope;
// Define default scope functions
DefaultModalController.call({}, scope, deferred, parameters);
// Set modal
scope.modal = modal;
}
// Show the modal
return modal.show();
},
function (err) {
deferred.reject(err);
});
return deferred.promise;
}
return {
show: show
};
}])
.factory('Modals', ['ModalUtils', function(ModalUtils) {
'ngInject';
function showTransfer(parameters) {
return ModalUtils.show('templates/wallet/modal_transfer.html','TransferModalCtrl',
parameters, {focusFirstInput: true});
}
function showLogin(options) {
var parameters = angular.copy(options||{});
delete parameters.templateUrl;
delete parameters.controller;
return ModalUtils.show(
options && options.templateUrl || 'templates/login/modal_login.html',
options && options.controller || 'LoginModalCtrl',
parameters, {focusFirstInput: true});
}
function showWotLookup(parameters) {
return ModalUtils.show('templates/wot/modal_lookup.html','WotLookupModalCtrl',
parameters || {}, {focusFirstInput: true});
}
function showNetworkLookup(parameters) {
return ModalUtils.show('templates/network/modal_network.html', 'NetworkLookupModalCtrl',
parameters, {focusFirstInput: true});
}
function showAbout(parameters) {
return ModalUtils.show('templates/modal_about.html','AboutModalCtrl',
parameters);
}
function showAccountSecurity(parameters) {
return ModalUtils.show('templates/wallet/modal_security.html', 'WalletSecurityModalCtrl',
parameters);
}
function showJoin(parameters) {
return ModalUtils.show('templates/join/modal_join.html','JoinModalCtrl',
parameters);
}
function showHelp(parameters) {
return ModalUtils.show('templates/help/modal_help.html','HelpModalCtrl',
parameters);
}
return {
showTransfer: showTransfer,
showLogin: showLogin,
showWotLookup: showWotLookup,
showNetworkLookup: showNetworkLookup,
showAbout: showAbout,
showJoin: showJoin,
showHelp: showHelp,
showAccountSecurity: showAccountSecurity
};
}]);
angular.module('cesium.http.services', ['cesium.cache.services'])
.factory('csHttp', ['$http', '$q', '$timeout', '$window', 'csSettings', 'csCache', 'Device', function($http, $q, $timeout, $window, csSettings, csCache, Device) {
'ngInject';
var timeout = csSettings.data.timeout;
var
sockets = [],
cachePrefix = 'csHttp-'
;
if (!timeout) {
timeout=4000; // default
}
function getServer(host, port) {
// Remove port if 80 or 443
return !host ? null : (host + (port && port != 80 && port != 443 ? ':' + port : ''));
}
function getUrl(host, port, path, useSsl) {
var protocol = (port == 443 || useSsl) ? 'https' : 'http';
return protocol + '://' + getServer(host, port) + (path ? path : '');
}
function getWsUrl(host, port, path, useSsl) {
var protocol = (port == 443 || useSsl) ? 'wss' : 'ws';
return protocol + '://' + getServer(host, port) + (path ? path : '');
}
function processError(reject, data, url, status) {
if (data && data.message) {
reject(data);
}
else {
if (status == 404) {
reject({ucode: 404, message: 'Resource not found' + (url ? ' ('+url+')' : '')});
}
else if (url) {
reject('Error while requesting [' + url + ']');
}
else {
reject('Unknown error from node');
}
}
}
function prepare(url, params, config, callback) {
var pkeys = [], queryParams = {}, newUri = url;
if (typeof params === 'object') {
pkeys = _.keys(params);
}
_.forEach(pkeys, function(pkey){
var prevURI = newUri;
newUri = newUri.replace(':' + pkey, params[pkey]);
if (prevURI === newUri) {
queryParams[pkey] = params[pkey];
}
});
config.params = queryParams;
return callback(newUri, config);
}
function getResource(host, port, path, useSsl, forcedTimeout) {
// Make sure host is defined - fix #537
if (!host) {
return $q.reject('[http] invalid URL from host: ' + host);
}
var url = getUrl(host, port, path, useSsl);
return function(params) {
return $q(function(resolve, reject) {
var config = {
timeout: forcedTimeout || timeout,
responseType: 'json'
};
prepare(url, params, config, function(url, config) {
$http.get(url, config)
.success(function(data, status, headers, config) {
resolve(data);
})
.error(function(data, status, headers, config) {
processError(reject, data, url, status);
});
});
});
};
}
function getResourceWithCache(host, port, path, useSsl, maxAge, autoRefresh, forcedTimeout, cachePrefix) {
var url = getUrl(host, port, path, useSsl);
maxAge = maxAge || csCache.constants.LONG;
//console.debug('[http] will cache ['+url+'] ' + maxAge + 'ms' + (autoRefresh ? ' with auto-refresh' : ''));
return function(params) {
return $q(function(resolve, reject) {
var config = {
timeout: forcedTimeout || timeout,
responseType: 'json'
};
if (autoRefresh) { // redo the request if need
config.cache = csCache.get(cachePrefix, maxAge, function (key, value, done) {
console.debug('[http] Refreshing cache for ['+key+'] ');
$http.get(key, config)
.success(function (data) {
config.cache.put(key, data);
if (done) done(key, data);
});
});
}
else {
config.cache = csCache.get(cachePrefix, maxAge);
}
prepare(url, params, config, function(url, config) {
$http.get(url, config)
.success(function(data) {
resolve(data);
})
.error(function(data, status) {
processError(reject, data, url, status);
});
});
});
};
}
function postResource(host, port, path, useSsl, forcedTimeout) {
var url = getUrl(host, port, path, useSsl);
return function(data, params) {
return $q(function(resolve, reject) {
var config = {
timeout: forcedTimeout || timeout,
headers : {'Content-Type' : 'application/json;charset=UTF-8'}
};
prepare(url, params, config, function(url, config) {
$http.post(url, data, config)
.success(function(data) {
resolve(data);
})
.error(function(data, status) {
processError(reject, data, url, status);
});
});
});
};
}
function ws(host, port, path, useSsl, timeout) {
if (!path) {
console.error('calling csHttp.ws without path argument');
throw 'calling csHttp.ws without path argument';
}
var uri = getWsUrl(host, port, path, useSsl);
timeout = timeout || csSettings.data.timeout;
function _waitOpen(self) {
if (!self.delegate) throw new Error('Websocket not opened');
if (self.delegate.readyState == 1) {
return $q.when(self.delegate);
}
if (self.delegate.readyState == 3) {
return $q.reject('Unable to connect to websocket ['+self.delegate.url+']');
}
if (self.waitDuration >= timeout) {
self.waitRetryDelay = self.waitRetryDelay && Math.min(self.waitRetryDelay + 2000, 30000) || 2000; // add 2 seconds, until 30s)
console.debug("[http] Will retry websocket [{0}] in {1}s...".format(self.path, Math.round(self.waitRetryDelay/1000)));
}
else if (Math.round(self.waitDuration / 1000) % 10 === 0){
console.debug('[http] Waiting websocket ['+self.path+']...');
}
return $timeout(function(){
self.waitDuration += self.waitRetryDelay;
return _waitOpen(self);
}, self.waitRetryDelay);
}
function _open(self, callback, params) {
if (!self.delegate) {
self.path = path;
self.callbacks = [];
self.waitDuration = 0;
self.waitRetryDelay = 200;
prepare(uri, params, {}, function(uri) {
self.delegate = new WebSocket(uri);
self.delegate.onerror = function(e) {
self.delegate.readyState=3;
};
self.delegate.onmessage = function(e) {
var obj = JSON.parse(e.data);
_.forEach(self.callbacks, function(callback) {
callback(obj);
});
};
self.delegate.onopen = function(e) {
console.debug('[http] Listening on websocket ['+self.path+']...');
sockets.push(self);
self.delegate.openTime = Date.now();
};
self.delegate.onclose = function(closeEvent) {
// Remove from sockets arrays
var index = _.findIndex(sockets, function(socket){return socket.path === self.path;});
if (index >= 0) {
sockets.splice(index,1);
}
// If close event comes from Cesium
if (self.delegate.closing) {
self.delegate = null;
}
// If unexpected close event, reopen the socket (fix #535)
else {
if (self.delegate.openTime) {
console.debug('[http] Unexpected close of websocket [{0}] (open {1} ms ago): re-opening...', path, (Date.now() - self.delegate.openTime));
// Force new connection
self.delegate = null;
// Loop, but without the already registered callback
_open(self, null, params);
}
else if (closeEvent) {
console.debug('[http] TODO -- Unexpected close of websocket [{0}]: error code: '.format(path), closeEvent);
// Force new connection
self.delegate = null;
// Loop, but without the already registered callback
_open(self, null, params);
}
}
};
});
}
if (callback) self.callbacks.push(callback);
return _waitOpen(self);
}
function _close(self) {
if (self.delegate) {
self.delegate.closing = true;
console.debug('[http] Closing websocket ['+self.path+']...');
self.delegate.close();
self.callbacks = [];
if (self.onclose) self.onclose();
}
}
function _remove(self, callback) {
self.callbacks = _.reject(self.callbacks, function(item) {
return item === callback;
});
if (!self.callbacks.length) {
_close(self);
}
}
return {
open: function(params) {
return _open(this, null, params);
},
on: function(callback, params) {
return _open(this, callback, params);
},
onListener: function(callback, params) {
var self = this;
_open(self, callback, params);
return function() {
_remove(self, callback);
};
},
send: function(data) {
var self = this;
return _waitOpen(self)
.then(function(){
if (self.delegate) self.delegate.send(data);
});
},
close: function() {
var self = this;
_close(self);
},
isClosed: function() {
var self = this;
return !self.delegate || self.delegate.closing;
}
};
}
function closeAllWs() {
if (sockets.length > 0) {
console.debug('[http] Closing all websocket...');
_.forEach(sockets, function(sock) {
sock.close();
});
sockets = []; // Reset socks list
}
}
// See doc : https://gist.github.com/jlong/2428561
function parseUri(uri) {
var protocol;
if (uri.startsWith('duniter://')) {
protocol = 'duniter';
uri = uri.replace('duniter://', 'http://');
}
var parser = document.createElement('a');
parser.href = uri;
var pathname = parser.pathname;
if (pathname && pathname.startsWith('/')) {
pathname = pathname.substring(1);
}
var result = {
protocol: protocol ? protocol : parser.protocol,
hostname: parser.hostname,
host: parser.host,
port: parser.port,
username: parser.username,
password: parser.password,
pathname: pathname,
search: parser.search,
hash: parser.hash
};
parser.remove();
return result;
}
/**
* Open a URI (url, email, phone, ...)
* @param event
* @param link
* @param type
*/
function openUri(uri, options) {
options = options || {};
if (!uri.startsWith('http://') && !uri.startsWith('https://')) {
var parts = parseUri(uri);
if (!parts.protocol && options.type) {
parts.protocol = (options.type == 'email') ? 'mailto:' :
((options.type == 'phone') ? 'tel:' : '');
uri = parts.protocol + uri;
}
// On desktop, open into external tool
if (parts.protocol == 'mailto:' && Device.isDesktop()) {
try {
nw.Shell.openExternal(uri);
return;
}
catch(err) {
console.error("[http] Failed not open 'mailto:' URI into external tool.");
}
}
// Check if device is enable, on special tel: or mailto: protocole
var validProtocol = (parts.protocol == 'mailto:' || parts.protocol == 'tel:') && Device.enable;
if (!validProtocol) {
if (options.onError && typeof options.onError == 'function') {
options.onError(uri);
}
return;
}
}
// Note: If device enable, then target=_system will use InAppBrowser cordova plugin
var openTarget = (options.target || (Device.enable ? '_system' : '_blank'));
// If desktop, try to open into external browser
if (openTarget === '_blank' || openTarget === '_system' && Device.isDesktop()) {
try {
nw.Shell.openExternal(uri);
return;
}
catch(err) {
console.error("[http] Failed not open URI into external browser.");
}
}
// If desktop, should always open in new window (no tabs)
var openOptions;
if (openTarget === '_blank' && Device.isDesktop()) {
if (nw && nw.Shell) {
nw.Shell.openExternal(uri);
return false;
}
// Override default options
openOptions= "location=1,titlebar=1,status=1,menubar=1,toolbar=1,resizable=1,scrollbars=1";
// Add width/height
if ($window.screen && $window.screen.width && $window.screen.height) {
openOptions += ",width={0},height={1}".format(Math.trunc($window.screen.width/2), Math.trunc($window.screen.height/2));
}
}
var win = $window.open(uri,
openTarget,
openOptions);
// Center the opened window
if (openOptions && $window.screen && $window.screen.width && $window.screen.height) {
win.moveTo($window.screen.width/2/2, $window.screen.height/2/2);
win.focus();
}
}
// Get time in second (UTC)
function getDateNow() {
return moment().utc().unix();
}
function isPositiveInteger(x) {
// http://stackoverflow.com/a/1019526/11236
return /^\d+$/.test(x);
}
/**
* Compare two software version numbers (e.g. 1.7.1)
* Returns:
*
* 0 if they're identical
* negative if v1 < v2
* positive if v1 > v2
* Nan if they in the wrong format
*
* E.g.:
*
* assert(version_number_compare("1.7.1", "1.6.10") > 0);
* assert(version_number_compare("1.7.1", "1.7.10") < 0);
*
* "Unit tests": http://jsfiddle.net/ripper234/Xv9WL/28/
*
* Taken from http://stackoverflow.com/a/6832721/11236
*/
function compareVersionNumbers(v1, v2){
var v1parts = v1.split('.');
var v2parts = v2.split('.');
// First, validate both numbers are true version numbers
function validateParts(parts) {
for (var i = 0; i < parts.length; ++i) {
if (!isPositiveInteger(parts[i])) {
return false;
}
}
return true;
}
if (!validateParts(v1parts) || !validateParts(v2parts)) {
return NaN;
}
for (var i = 0; i < v1parts.length; ++i) {
if (v2parts.length === i) {
return 1;
}
if (v1parts[i] === v2parts[i]) {
continue;
}
if (v1parts[i] > v2parts[i]) {
return 1;
}
return -1;
}
if (v1parts.length != v2parts.length) {
return -1;
}
return 0;
}
function isVersionCompatible(minVersion, actualVersion) {
console.debug('[http] Checking actual version [{0}] is compatible with min expected version [{1}]'.format(actualVersion, minVersion));
return compareVersionNumbers(minVersion, actualVersion) <= 0;
}
var cache = angular.copy(csCache.constants);
cache.clear = function() {
console.debug('[http] Cleaning cache...');
csCache.clear(cachePrefix);
};
return {
get: getResource,
getWithCache: getResourceWithCache,
post: postResource,
ws: ws,
closeAllWs: closeAllWs,
getUrl : getUrl,
getServer: getServer,
uri: {
parse: parseUri,
open: openUri
},
date: {
now: getDateNow
},
version: {
compare: compareVersionNumbers,
isCompatible: isVersionCompatible
},
cache: cache
};
}])
;
angular.module('cesium.storage.services', [ 'cesium.config'])
.factory('sessionStorage', ['$window', '$q', function($window, $q) {
'ngInject';
var
exports = {
storage: $window.sessionStorage || {}
};
exports.put = function(key, value) {
exports.storage[key] = value;
return $q.when();
};
exports.get = function(key, defaultValue) {
return $q.when(exports.storage[key] || defaultValue);
};
exports.setObject = function(key, value) {
exports.storage[key] = JSON.stringify(value);
return $q.when();
};
exports.getObject = function(key) {
return $q.when(JSON.parse(exports.storage[key] || 'null'));
};
return exports;
}])
.factory('localStorage', ['$window', '$q', '$log', 'sessionStorage', function($window, $q, $log, sessionStorage) {
'ngInject';
var
appName = "Cesium",
started = false,
startPromise,
isDevice = true, // default for device (override later)
exports = {
standard: {
storage: null
},
secure: {
storage: null
}
};
// removeIf(device)
// Use this workaround to avoid to wait ionicReady() event
isDevice = false;
// endRemoveIf(device)
/* -- Use standard browser implementation -- */
exports.standard.put = function(key, value) {
if (angular.isDefined(value) && value != null) {
exports.standard.storage[key] = value;
}
else {
exports.standard.storage.removeItem(key);
}
return $q.when();
};
exports.standard.get = function(key, defaultValue) {
return $q.when(exports.standard.storage[key] || defaultValue);
};
exports.standard.setObject = function(key, value) {
exports.standard.storage[key] = JSON.stringify(value);
return $q.when();
};
exports.standard.getObject = function(key) {
return $q.when(JSON.parse(exports.standard.storage[key] || 'null'));
};
/* -- Use secure storage (using a cordova plugin) -- */
// Set a value to the secure storage (or remove if value is not defined)
exports.secure.put = function(key, value) {
return $q(function(resolve, reject) {
if (value !== undefined && value !== null) {
exports.secure.storage.set(
function (key) {
resolve();
},
function (err) {
$log.error(err);
reject(err);
},
key, value);
}
// Remove
else {
exports.secure.storage.remove(
function () {
resolve();
},
function (err) {
$log.error(err);
resolve(); // Error = not found
},
key);
}
});
};
// Get a value from the secure storage
exports.secure.get = function(key, defaultValue) {
return $q(function(resolve, reject) {
exports.secure.storage.get(
function (value) {
if (!value && defaultValue) {
resolve(defaultValue);
}
else {
resolve(value);
}
},
function (err) {
$log.error(err);
resolve(); // Error = not found
},
key);
});
};
// Set a object to the secure storage
exports.secure.setObject = function(key, value) {
$log.debug("[storage] Setting object into secure storage, using key=" + key);
return $q(function(resolve, reject){
exports.secure.storage.set(
resolve,
reject,
key,
value ? JSON.stringify(value) : undefined);
});
};
// Get a object from the secure storage
exports.secure.getObject = function(key) {
$log.debug("[storage] Getting object from secure storage, using key=" + key);
return $q(function(resolve, reject){
exports.secure.storage.get(
function(value) {resolve(JSON.parse(value||'null'));},
function(err) {
$log.error(err);
resolve(); // Error = not found
},
key);
});
};
function initStandardStorage() {
// use local browser storage
if ($window.localStorage) {
console.debug('[storage] Starting {local} storage...');
exports.standard.storage = $window.localStorage;
// Set standard storage as default
_.forEach(_.keys(exports.standard), function(key) {
exports[key] = exports.standard[key];
});
}
// Fallback to session storage (locaStorage could have been disabled on some browser)
else {
console.debug('[storage] Starting {session} storage...');
// Set standard storage as default
_.forEach(_.keys(sessionStorage), function(key) {
exports[key] = sessionStorage[key];
});
}
return $q.when();
}
function initSecureStorage() {
console.debug('[storage] Starting {secure} storage...');
// Set secure storage as default
_.forEach(_.keys(exports.secure), function(key) {
exports[key] = exports.secure[key];
});
var deferred = $q.defer();
// No secure storage plugin: fall back to standard storage
if (!cordova.plugins || !cordova.plugins.SecureStorage) {
initStandardStorage();
deferred.resolve();
}
else {
exports.secure.storage = new cordova.plugins.SecureStorage(
function () {
deferred.resolve();
},
function (err) {
console.error('[storage] Could not use secure storage. Will use standard.', err);
initStandardStorage();
deferred.resolve();
},
appName);
}
return deferred.promise;
}
exports.isStarted = function() {
return started;
};
exports.ready = function() {
if (started) return $q.when();
return startPromise || start();
};
function start() {
if (startPromise) return startPromise;
var now = Date.now();
// Use Cordova secure storage plugin
if (isDevice) {
startPromise = initSecureStorage();
}
// Use default browser local storage
else {
startPromise = initStandardStorage();
}
return startPromise
.then(function() {
console.debug('[storage] Started in ' + (Date.now() - now) + 'ms');
started = true;
startPromise = null;
});
}
// default action
start();
return exports;
}])
;
angular.module('cesium.device.services', ['ngResource', 'cesium.utils.services', 'cesium.settings.services'])
.factory('Device',
['$translate', '$ionicPopup', '$q', 'ionicReady', function($translate, $ionicPopup, $q,
ionicReady) {
'ngInject';
var
CONST = {
MAX_HEIGHT: 400,
MAX_WIDTH: 400
},
exports = {
// workaround to quickly no is device or not (even before the ready() event)
enable: true
},
cache = {},
started = false,
startPromise;
// removeIf(device)
// workaround to quickly no is device or not (even before the ready() event)
exports.enable = false;
// endRemoveIf(device)
function getPicture(options) {
if (!exports.camera.enable) {
return $q.reject("Camera not enable. Please call 'Device.ready()' once before use (e.g in app.js).");
}
// Options is the sourceType by default
if (options && (typeof options === "string")) {
options = {
sourceType: options
};
}
options = options || {};
// Make sure a source type has been given (if not, ask user)
if (angular.isUndefined(options.sourceType)) {
return $translate(['SYSTEM.PICTURE_CHOOSE_TYPE', 'SYSTEM.BTN_PICTURE_GALLERY', 'SYSTEM.BTN_PICTURE_CAMERA'])
.then(function(translations){
return $ionicPopup.show({
title: translations['SYSTEM.PICTURE_CHOOSE_TYPE'],
buttons: [
{
text: translations['SYSTEM.BTN_PICTURE_GALLERY'],
type: 'button',
onTap: function(e) {
return navigator.camera.PictureSourceType.PHOTOLIBRARY;
}
},
{
text: translations['SYSTEM.BTN_PICTURE_CAMERA'],
type: 'button button-positive',
onTap: function(e) {
return navigator.camera.PictureSourceType.CAMERA;
}
}
]
})
.then(function(sourceType){
console.info('[camera] User select sourceType:' + sourceType);
options.sourceType = sourceType;
return exports.camera.getPicture(options);
});
});
}
options.quality = options.quality || 50;
options.destinationType = options.destinationType || navigator.camera.DestinationType.DATA_URL;
options.encodingType = options.encodingType || navigator.camera.EncodingType.PNG;
options.targetWidth = options.targetWidth || CONST.MAX_WIDTH;
options.targetHeight = options.targetHeight || CONST.MAX_HEIGHT;
return $cordovaCamera.getPicture(options);
}
function scan(n) {
if (!exports.enable) {
return $q.reject("Barcode scanner not enable. Please call 'Device.ready()' once before use (e.g in app.js).");
}
var deferred = $q.defer();
cordova.plugins.barcodeScanner.scan(
function(result) {
console.debug('[device] bar code result', result);
if (!result.cancelled) {
deferred.resolve(result.text); // make sure to convert into String
}
else {
deferred.resolve();
}
},
function(err) {
console.error('[device] Error while using barcode scanner -> ' + err);
deferred.reject(err);
},
n);
return deferred.promise;
}
function copy(text, callback) {
if (!exports.enable) {
return $q.reject('Device disabled');
}
var deferred = $q.defer();
$cordovaClipboard
.copy(text)
.then(function () {
// success
if (callback) {
callback();
}
deferred.resolve();
}, function () {
// error
deferred.reject({message: 'ERROR.COPY_CLIPBOARD'});
});
return deferred.promise;
}
exports.clipboard = {copy: copy};
exports.camera = {
getPicture : getPicture,
scan: function(n){
console.warn('Deprecated use of Device.camera.scan(). Use Device.barcode.scan() instead');
return scan(n);
}
};
exports.barcode = {
enable : false,
scan: scan
};
exports.keyboard = {
enable: false,
close: function() {
if (!exports.keyboard.enable) return;
cordova.plugins.Keyboard.close();
}
};
// Numerical keyboard - fix #30
exports.keyboard.digit = {
settings: {
bindModel: function(modelScope, modelPath, settings) {
settings = settings || {};
modelScope = modelScope || $rootScope;
var getModelValue = function() {
return (modelPath||'').split('.').reduce(function(res, path) {
return res ? res[path] : undefined;
}, modelScope);
};
var setModelValue = function(value) {
var paths = (modelPath||'').split('.');
var property = paths.length && paths[paths.length-1];
paths.reduce(function(res, path) {
if (path == property) {
res[property] = value;
return;
}
return res[path];
}, modelScope);
};
settings.action = settings.action || function(number) {
setModelValue((getModelValue() ||'') + number);
};
if (settings.decimal) {
settings.decimalSeparator = settings.decimalSeparator || '.';
settings.leftButton = settings.leftButton = {
html: '<span>.</span>',
action: function () {
var text = getModelValue() || '';
// only one '.' allowed
if (text.indexOf(settings.decimalSeparator) >= 0) return;
// Auto add zero when started with '.'
if (!text.trim().length) {
text = '0';
}
setModelValue(text + settings.decimalSeparator);
}
};
}
settings.rightButton = settings.rightButton || {
html: '<i class="icon ion-backspace-outline"></i>',
action: function() {
var text = getModelValue();
if (text && text.length) {
text = text.slice(0, -1);
setModelValue(text);
}
}
};
return settings;
}
}
};
exports.isIOS = function() {
return !!navigator.userAgent.match(/iPhone | iPad | iPod/i) || ionic.Platform.isIOS();
};
exports.isDesktop = function() {
if (!angular.isDefined(cache.isDesktop)) {
try {
// Should have NodeJs and NW
cache.isDesktop = !exports.enable && !!process && !!nw && !!nw.App;
} catch (err) {
cache.isDesktop = false;
}
}
return cache.isDesktop;
};
exports.isWeb = function() {
return !exports.enable && !exports.isDesktop();
};
exports.ready = function() {
if (started) return $q.when();
return startPromise || exports.start();
};
exports.start = function() {
startPromise = ionicReady()
.then(function(){
exports.enable = window.cordova && cordova && cordova.plugins;
if (exports.enable){
exports.camera.enable = !!navigator.camera;
exports.keyboard.enable = cordova && cordova.plugins && !!cordova.plugins.Keyboard;
exports.barcode.enable = cordova && cordova.plugins && !!cordova.plugins.barcodeScanner;
exports.clipboard.enable = cordova && cordova.plugins && !!cordova.plugins.clipboard;
if (exports.keyboard.enable) {
angular.extend(exports.keyboard, cordova.plugins.Keyboard);
}
console.debug('[device] Ionic platform ready, with [camera: {0}] [barcode scanner: {1}] [keyboard: {2}] [clipboard: {3}]'
.format(exports.camera.enable, exports.barcode.enable, exports.keyboard.enable, exports.clipboard.enable));
}
else {
console.debug('[device] Ionic platform ready - no device detected.');
}
started = true;
startPromise = null;
});
return startPromise;
};
return exports;
}])
;
angular.module('cesium.currency.services', ['ngApi', 'cesium.bma.services'])
.factory('csCurrency', ['$rootScope', '$q', '$timeout', 'BMA', 'Api', 'csSettings', function($rootScope, $q, $timeout, BMA, Api, csSettings) {
'ngInject';
function factory(id, BMA) {
var
constants = {
// Avoid to many call on well known currencies
WELL_KNOWN_CURRENCIES: {
g1: {
firstBlockTime: 1488987127,
medianTimeOffset: 3600
}
}
},
data = {},
started = false,
startPromise,
listeners,
api = new Api(this, "csCurrency-" + id);
function powBase(amount, base) {
return base <= 0 ? amount : amount * Math.pow(10, base);
}
function resetData() {
data.name = null;
data.parameters = null;
data.firstBlockTime = null;
data.membersCount = null;
data.cache = {};
data.node = BMA;
data.currentUD = null;
data.medianTimeOffset = 0;
started = false;
startPromise = undefined;
api.data.raise.reset(data);
}
function loadData() {
// Load currency from default node
return $q.all([
// get parameters
loadParameters()
.then(function(parameters) {
// load first block info
return loadFirstBlock(parameters.currency);
}),
// get current UD
loadCurrentUD(),
// call extensions
api.data.raisePromise.load(data)
])
.catch(function(err) {
resetData();
throw err;
});
}
function loadParameters() {
return BMA.blockchain.parameters()
.then(function(res){
data.name = res.currency;
data.parameters = res;
data.medianTimeOffset = res.avgGenTime * res.medianTimeBlocks / 2;
return res;
});
}
function loadFirstBlock(currencyName) {
// Well known currencies
if (constants.WELL_KNOWN_CURRENCIES[currencyName]){
angular.merge(data, constants.WELL_KNOWN_CURRENCIES[currencyName]);
return $q.when();
}
return BMA.blockchain.block({block:0})
.then(function(json) {
// Need by graph plugin
data.firstBlockTime = json.medianTime;
})
.catch(function(err) {
// Special case, when currency not started yet
if (err && err.ucode === BMA.errorCodes.BLOCK_NOT_FOUND) {
data.firstBlockTime = 0;
data.initPhase = true;
console.warn('[currency] Blockchain not launched: Enable init phase mode');
return;
}
throw err;
});
}
function loadCurrentUD() {
return BMA.blockchain.stats.ud()
.then(function(res) {
// Special case for currency init
if (!res.result.blocks.length) {
data.currentUD = data.parameters ? data.parameters.ud0 : -1;
return data.currentUD ;
}
return _safeLoadCurrentUD(res, res.result.blocks.length - 1);
})
.catch(function(err) {
data.currentUD = null;
throw err;
});
}
/**
* Load the last UD, with a workaround if last block with UD is not found in the node
* @param res
* @param blockIndex
* @returns {*}
* @private
*/
function _safeLoadCurrentUD(res, blockIndex) {
// Special case for currency init
if (!res.result.blocks.length || blockIndex < 0) {
data.currentUD = data.parameters ? data.parameters.ud0 : -1;
return data.currentUD ;
}
else {
var lastBlockWithUD = res.result.blocks[blockIndex];
return BMA.blockchain.block({ block: lastBlockWithUD })
.then(function(block){
data.currentUD = powBase(block.dividend, block.unitbase);
return data.currentUD;
})
.catch(function(err) {
console.error("[currency] Unable to load last block with UD, with number {0}".format(lastBlockWithUD));
if (blockIndex > 0) {
console.error("[currency] Retrying to load UD from a previous block...");
return _safeLoadCurrentUD(res, blockIndex-1);
}
data.currentUD = null;
throw err;
});
}
}
function getData() {
if (started) { // load only once
return $q.when(data);
}
// Previous load not finished: return the existing promise - fix #452
return startPromise || start();
}
function getDataField(field) {
return function() {
if (started) { // load only once
return $q.when(data[field]);
}
// Previous load not finished: return the existing promise - fix #452
return startPromise || start() // load only once
.then(function(){
return data[field];
});
};
}
function onBlock(json) {
var block = new Block(json);
block.cleanData(); // Remove unused content (arrays...) and keep items count
console.debug('[currency] Received new block [' + block.number + '-' + block.hash + ']');
data.currentBlock = block;
data.currentBlock.receivedAt = moment().utc().unix();
data.medianTime = block.medianTime;
data.membersCount = block.membersCount;
// Update UD
if (block.dividend) {
data.currentUD = block.dividend;
}
// Dispatch to extensions
api.data.raise.newBlock(block);
}
function addListeners() {
listeners = [
// Listen if node changed
BMA.api.node.on.restart($rootScope, restart, this),
// open web socket on block
BMA.websocket.block().onListener(onBlock)
];
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function ready() {
if (started) return $q.when(data);
return startPromise || start();
}
function stop() {
console.debug('[currency] Stopping...');
removeListeners();
resetData();
}
function restart() {
stop();
return $timeout(start, 200);
}
function start() {
console.debug('[currency] Starting...');
var now = Date.now();
startPromise = BMA.ready()
// Load data
.then(loadData)
// Emit ready event
.then(function() {
addListeners();
console.debug('[currency] Started in ' + (Date.now() - now) + 'ms');
started = true;
startPromise = null;
// Emit event (used by plugins)
api.data.raise.ready(data);
})
.then(function(){
return data;
});
return startPromise;
}
var currentBlockField = getDataField('currentBlock');
function getCurrent(cache) {
// Get field (and make sure service is started)
return currentBlockField()
.then(function(currentBlock) {
var now = moment().utc().unix();
if (cache) {
if (currentBlock && (now - currentBlock.receivedAt) < 60/*1min*/) {
//console.debug('[currency] Use current block #'+ currentBlock.number +' from cache (age='+ (now - currentBlock.receivedAt) + 's)');
return currentBlock;
}
if (!currentBlock) {
// Should never occur, if websocket /ws/block works !
console.warn('[currency] No current block in cache: get it from network. Websocket [/ws/block] may not be started ?');
}
}
return BMA.blockchain.current()
.catch(function(err){
// Special case for currency init (root block not exists): use fixed values
if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) {
return {number: 0, hash: BMA.constants.ROOT_BLOCK_HASH, medianTime: moment().utc().unix()};
}
throw err;
})
.then(function(current) {
data.currentBlock = current;
data.currentBlock.receivedAt = now;
return current;
});
});
}
function getLastValidBlock() {
if (csSettings.data.blockValidityWindow <= 0) {
return getCurrent(true);
}
return getCurrent(true)
.then(function(current) {
var number = current.number - csSettings.data.blockValidityWindow;
return (number > 0) ? BMA.blockchain.block({block: number}) : current;
});
}
// Get time in second (UTC - medianTimeOffset)
function getDateNow() {
return moment().utc().unix() - (data.medianTimeOffset || constants.WELL_KNOWN_CURRENCIES.g1.medianTimeOffset);
}
// TODO register new block event, to get new UD value
// Register extension points
api.registerEvent('data', 'ready');
api.registerEvent('data', 'load');
api.registerEvent('data', 'reset');
api.registerEvent('data', 'newBlock');
// init data
resetData();
// Default action
//start();
return {
ready: ready,
start: start,
stop: stop,
data: data,
get: getData,
name: getDataField('name'),
parameters: getDataField('parameters'),
currentUD: getDataField('currentUD'),
medianTimeOffset: getDataField('medianTimeOffset'),
blockchain: {
current: getCurrent,
lastValid: getLastValidBlock
},
date: {
now: getDateNow
},
// api extension
api: api,
// deprecated methods
default: function() {
console.warn('[currency] \'csCurrency.default()\' has been DEPRECATED - Please use \'csCurrency.get()\' instead.');
return getData();
}
};
}
var service = factory('default', BMA);
service.instance = factory;
return service;
}]);
//var Base58, Base64, scrypt_module_factory = null, nacl_factory = null;
angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.settings.services'])
.factory('BMA', ['$q', '$window', '$rootScope', '$timeout', 'Api', 'Device', 'csConfig', 'csSettings', 'csHttp', function($q, $window, $rootScope, $timeout, Api, Device, csConfig, csSettings, csHttp) {
'ngInject';
function BMA(host, port, useSsl, useCache) {
var pubkey = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}";
var
// TX output conditions
SIG = "SIG\\(([0-9a-zA-Z]{43,44})\\)",
XHX = 'XHX\\(([A-F0-9]{1,64})\\)',
CSV = 'CSV\\(([0-9]{1,8})\\)',
CLTV = 'CLTV\\(([0-9]{1,10})\\)',
OUTPUT_FUNCTION = SIG+'|'+XHX+'|'+CSV+'|'+CLTV,
OUTPUT_OPERATOR = '(&&)|(\\|\\|)',
OUTPUT_FUNCTIONS = OUTPUT_FUNCTION+'([ ]*' + OUTPUT_OPERATOR + '[ ]*' + OUTPUT_FUNCTION +')*',
OUTPUT_OBJ = 'OBJ\\(([0-9]+)\\)',
OUTPUT_OBJ_OPERATOR = OUTPUT_OBJ + '[ ]*' + OUTPUT_OPERATOR + '[ ]*' + OUTPUT_OBJ,
REGEX_ENDPOINT_PARAMS = "( ([a-z_][a-z0-9-_.ğĞ]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))( (.+))?",
api = {
BMA: 'BASIC_MERKLED_API',
BMAS: 'BMAS',
WS2P: 'WS2P',
BMATOR: 'BMATOR',
WS2PTOR: 'WS2PTOR'
},
regexp = {
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-_:/;*\\[\\]()?!^\\+=@&~#{}|\\\\<>%.]*",
// 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: api.BMA + REGEX_ENDPOINT_PARAMS,
BMAS_ENDPOINT: api.BMAS + 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]+))?(?: (.+))?"
},
errorCodes = {
REVOCATION_ALREADY_REGISTERED: 1002,
HTTP_LIMITATION: 1006,
IDENTITY_SANDBOX_FULL: 1007,
NO_MATCHING_IDENTITY: 2001,
UID_ALREADY_USED: 2003,
NO_MATCHING_MEMBER: 2004,
NO_IDTY_MATCHING_PUB_OR_UID: 2021,
WRONG_SIGNATURE_MEMBERSHIP: 2006,
MEMBERSHIP_ALREADY_SEND: 2007,
NO_CURRENT_BLOCK: 2010,
BLOCK_NOT_FOUND: 2011,
SOURCE_ALREADY_CONSUMED: 2015,
TX_INPUTS_OUTPUTS_NOT_EQUAL: 2024,
TX_OUTPUT_SUM_NOT_EQUALS_PREV_DELTAS: 2025,
TX_ALREADY_PROCESSED: 2030
},
constants = {
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
regexp: regexp,
api: api
},
listeners,
that = this;
that.api = new Api(this, 'BMA-' + that.server);
that.started = false;
that.init = init;
// Allow to force SSL connection with port different from 443
that.forceUseSsl = (csConfig.httpsMode === 'true' || csConfig.httpsMode === true || csConfig.httpsMode === 'force') ||
($window.location && $window.location.protocol === 'https:') ? true : false;
if (that.forceUseSsl) {
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
function init(host, port, useSsl, useCache) {
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;
}
if (!host) {
return; // could not init yet
}
that.host = host;
that.port = port || 80;
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);
}
function exact(regexpContent) {
return new RegExp("^" + regexpContent + "$");
}
function test(regexpContent) {
return new RegExp(regexpContent);
}
function _emptyCache() {
return {
getByPath: {},
postByPath: {},
wsByPath: {}
};
}
function closeWs() {
if (!that.cache) return;
console.warn('[BMA] Closing all websockets...');
_.keys(that.cache.wsByPath||{}).forEach(function(key) {
var sock = that.cache.wsByPath[key];
sock.close();
});
that.cache.wsByPath = {};
}
that.cleanCache = function() {
console.debug('[BMA] Cleaning requests cache...');
closeWs();
that.cache = _emptyCache();
};
get = function (path, cacheTime) {
cacheTime = that.useCache && cacheTime;
var cacheKey = path + (cacheTime ? ('#'+cacheTime) : '');
var getRequestFn = function(params) {
if (!that.started) {
if (!that._startPromise) {
console.warn('[BMA] Trying to get [{0}] before start(). Waiting...'.format(path));
}
return that.ready().then(function() {
return getRequestFn(params);
});
}
var request = that.cache.getByPath[cacheKey];
if (!request) {
if (cacheTime) {
request = csHttp.getWithCache(that.host, that.port, path, that.useSsl, cacheTime);
}
else {
request = csHttp.get(that.host, that.port, path, that.useSsl);
}
that.cache.getByPath[cacheKey] = request;
}
var execCount = 1;
return request(params)
.catch(function(err){
// If node return too many requests error
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) {
console.warn("[BMA] Too many HTTP requests: Will wait then retry...");
// Update the loading message (if exists)
UIUtils.loading.update({template: "COMMON.LOADING_WAIT"});
}
// Wait 1s then retry
return $timeout(function() {
execCount++;
return request(params);
}, exports.constants.LIMIT_REQUEST_DELAY);
}
}
throw err;
});
};
return getRequestFn;
};
post = function(path) {
var postRequest = function(obj, params) {
if (!that.started) {
if (!that._startPromise) {
console.error('[BMA] Trying to post [{0}] before start()...'.format(path));
}
return that.ready().then(function() {
return postRequest(obj, params);
});
}
var request = that.cache.postByPath[path];
if (!request) {
request = csHttp.post(that.host, that.port, path, that.useSsl);
that.cache.postByPath[path] = request;
}
return request(obj, params);
};
return postRequest;
};
ws = function(path) {
return function() {
var sock = that.cache.wsByPath[path];
if (!sock || sock.isClosed()) {
sock = csHttp.ws(that.host, that.port, path, that.useSsl);
// When close, remove from cache
sock.onclose = function() {
delete that.cache.wsByPath[path];
};
that.cache.wsByPath[path] = sock;
}
return sock;
};
};
that.isAlive = function() {
return csHttp.get(that.host, that.port, '/node/summary', that.useSsl)()
.then(function(json) {
var software = json && json.duniter && json.duniter.software;
var isCompatible = true;
// Check duniter min version
if (software === 'duniter' && json.duniter.version && csSettings.data.minVersion && true) {
isCompatible = csHttp.version.isCompatible(csSettings.data.minVersion, json.duniter.version);
}
// gchange-pod
else if (software === 'gchange-pod' && json.duniter.version && csSettings.data.plugins.es.minVersion && true) {
isCompatible = csHttp.version.isCompatible(csSettings.data.plugins.es.minVersion, json.duniter.version);
}
// TODO: check version of 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));
}
return isCompatible;
})
.catch(function() {
return false;
});
};
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
listeners = [
// Listen if node changed
csSettings.api.data.on.changed($rootScope, onSettingsChanged, this)
];
}
function onSettingsChanged(settings) {
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();
}
}
that.isStarted = function() {
return that.started;
};
that.ready = function() {
if (that.started) return $q.when(true);
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) {
return csSettings.ready()
.then(function() {
that.init();
// Always enable cache
that.useCache = true;
return that.start(); // recursive call
});
}
if (that.useSsl) {
console.debug('[BMA] Starting [{0}] (SSL on)...'.format(that.server));
}
else {
console.debug('[BMA] Starting [{0}]...'.format(that.server));
}
var now = Date.now();
that._startPromise = $q.all([
csSettings.ready,
that.isAlive()
])
.then(function(res) {
that.alive = res[1];
if (!that.alive) {
console.error('[BMA] Could not start [{0}]: node unreachable'.format(that.server));
that.started = true;
delete that._startPromise;
return false;
}
// Add listeners
if (!listeners || listeners.length === 0) {
addListeners();
}
console.debug('[BMA] Started in '+(Date.now()-now)+'ms');
that.api.node.raise.start();
that.started = true;
delete that._startPromise;
return true;
});
return that._startPromise;
};
that.stop = function() {
console.debug('[BMA] Stopping...');
removeListeners();
csHttp.cache.clear();
that.cleanCache();
that.alive = false;
that.started = false;
delete that._startPromise;
that.api.node.raise.stop();
};
that.restart = function() {
that.stop();
return $timeout(that.start, 200)
.then(function(alive) {
if (alive) {
that.api.node.raise.restart();
}
return alive;
});
};
that.api.registerEvent('node', 'start');
that.api.registerEvent('node', 'stop');
that.api.registerEvent('node', 'restart');
var exports = {
errorCodes: errorCodes,
constants: constants,
regexp: {
USER_ID: exact(regexp.USER_ID),
COMMENT: exact(regexp.COMMENT),
PUBKEY: exact(regexp.PUBKEY),
PUBKEY_WITH_CHECKSUM: exact(regexp.PUBKEY_WITH_CHECKSUM),
CURRENCY: exact(regexp.CURRENCY),
URI: exact(regexp.URI),
BMA_ENDPOINT: exact(regexp.BMA_ENDPOINT),
BMAS_ENDPOINT: exact(regexp.BMAS_ENDPOINT),
WS2P_ENDPOINT: exact(regexp.WS2P_ENDPOINT),
BMATOR_ENDPOINT: exact(regexp.BMATOR_ENDPOINT),
WS2PTOR_ENDPOINT: exact(regexp.WS2PTOR_ENDPOINT),
// TX output conditions
TX_OUTPUT_SIG: exact(SIG),
TX_OUTPUT_FUNCTION: test(OUTPUT_FUNCTION),
TX_OUTPUT_OBJ_OPERATOR_AND: test(OUTPUT_OBJ + '([ ]*&&[ ]*(' + OUTPUT_OBJ + '))+'),
TX_OUTPUT_OBJ_OPERATOR_OR: test(OUTPUT_OBJ + '([ ]*\\|\\|[ ]*(' + OUTPUT_OBJ + '))+'),
TX_OUTPUT_OBJ: test(OUTPUT_OBJ),
TX_OUTPUT_OBJ_OPERATOR: test(OUTPUT_OBJ_OPERATOR),
TX_OUTPUT_OBJ_PARENTHESIS: test('\\(('+OUTPUT_OBJ+')\\)'),
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));
},
forceUseSsl: that.forceUseSsl
},
network: {
peering: {
self: get('/network/peering'),
peers: get('/network/peering/peers')
},
peers: get('/network/peers'),
ws2p: {
info: get('/network/ws2p/info'),
heads: get('/network/ws2p/heads')
}
},
wot: {
lookup: get('/wot/lookup/:search', csHttp.cache.MEDIUM),
certifiedBy: get('/wot/certified-by/:pubkey'),
certifiersOf: get('/wot/certifiers-of/:pubkey'),
member: {
all: get('/wot/members', csHttp.cache.LONG),
pending: get('/wot/pending', csHttp.cache.SHORT)
},
requirements: function(params, withCache) {
// No cache by default
if (withCache !== true) return exports.raw.wot.requirements(params);
return exports.raw.wot.requirementsWithCache(params);
},
add: post('/wot/add'),
certify: post('/wot/certify'),
revoke: post('/wot/revoke')
},
blockchain: {
parameters: get('/blockchain/parameters', csHttp.cache.VERY_LONG),
block: get('/blockchain/block/:block', csHttp.cache.SHORT),
blocksSlice: get('/blockchain/blocks/:count/:from'),
current: get('/blockchain/current', csHttp.cache.SHORT),
membership: post('/blockchain/membership'),
stats: {
ud: get('/blockchain/with/ud', csHttp.cache.MEDIUM),
tx: get('/blockchain/with/tx'),
newcomers: get('/blockchain/with/newcomers', csHttp.cache.MEDIUM),
hardship: get('/blockchain/hardship/:pubkey'),
difficulties: get('/blockchain/difficulties')
}
},
tx: {
sources: get('/tx/sources/:pubkey', csHttp.cache.SHORT),
process: post('/tx/process'),
history: {
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, withCache) {
// No cache by default
return ((withCache !== 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', csHttp.cache.LONG),
pending: get('/tx/history/:pubkey/pending')
}
},
ud: {
history: get('/ud/history/:pubkey')
},
uri: {},
version: {},
raw: {
wot: {
requirementsWithCache: get('/wot/requirements/:pubkey', csHttp.cache.LONG),
requirements: get('/wot/requirements/:pubkey')
},
tx: {
history: {
timesWithCache: get('/tx/history/:pubkey/times/:from/:to', csHttp.cache.LONG),
times: get('/tx/history/:pubkey/times/:from/:to'),
all: get('/tx/history/:pubkey')
}
},
}
};
exports.tx.parseUnlockCondition = function(unlockCondition) {
//console.debug('[BMA] Parsing unlock condition: {0}.'.format(unlockCondition));
var convertedOutput = unlockCondition;
var treeItems = [];
var treeItem;
var treeItemId;
var childrenContent;
var childrenMatches;
var functions = {};
// Parse functions, then replace with an 'OBJ()' generic function, used to build a object tree
var matches = exports.regexp.TX_OUTPUT_FUNCTION.exec(convertedOutput);
while(matches) {
treeItem = {};
treeItemId = 'OBJ(' + treeItems.length + ')';
treeItem.type = convertedOutput.substr(matches.index, matches[0].indexOf('('));
treeItem.value = matches[1] || matches[2] || matches[3] || matches[4]; // get value from regexp OUTPUT_FUNCTION
treeItems.push(treeItem);
functions[treeItem.type] = functions[treeItem.type]++ || 1;
convertedOutput = convertedOutput.replace(matches[0], treeItemId);
matches = exports.regexp.TX_OUTPUT_FUNCTION.exec(convertedOutput);
}
var loop = true;
while(loop) {
// Parse AND operators
matches = exports.regexp.TX_OUTPUT_OBJ_OPERATOR_AND.exec(convertedOutput);
loop = !!matches;
while (matches) {
treeItem = {};
treeItemId = 'OBJ(' + treeItems.length + ')';
treeItem.type = 'AND';
treeItem.children = [];
treeItems.push(treeItem);
childrenContent = matches[0];
childrenMatches = exports.regexp.TX_OUTPUT_OBJ.exec(childrenContent);
while(childrenMatches) {
treeItem.children.push(treeItems[childrenMatches[1]]);
childrenContent = childrenContent.replace(childrenMatches[0], '');
childrenMatches = exports.regexp.TX_OUTPUT_OBJ.exec(childrenContent);
}
convertedOutput = convertedOutput.replace(matches[0], treeItemId);
matches = exports.regexp.TX_OUTPUT_OBJ_OPERATOR_AND.exec(childrenContent);
}
// Parse OR operators
matches = exports.regexp.TX_OUTPUT_OBJ_OPERATOR_OR.exec(convertedOutput);
loop = loop || !!matches;
while (matches) {
treeItem = {};
treeItemId = 'OBJ(' + treeItems.length + ')';
treeItem.type = 'OR';
treeItem.children = [];
treeItems.push(treeItem);
childrenContent = matches[0];
childrenMatches = exports.regexp.TX_OUTPUT_OBJ.exec(childrenContent);
while(childrenMatches) {
treeItem.children.push(treeItems[childrenMatches[1]]);
childrenContent = childrenContent.replace(childrenMatches[0], '');
childrenMatches = exports.regexp.TX_OUTPUT_OBJ.exec(childrenContent);
}
convertedOutput = convertedOutput.replace(matches[0], treeItemId);
matches = exports.regexp.TX_OUTPUT_OBJ_OPERATOR_AND.exec(convertedOutput);
}
// Remove parenthesis
matches = exports.regexp.TX_OUTPUT_OBJ_PARENTHESIS.exec(convertedOutput);
loop = loop || !!matches;
while (matches) {
convertedOutput = convertedOutput.replace(matches[0], matches[1]);
matches = exports.regexp.TX_OUTPUT_OBJ_PARENTHESIS.exec(convertedOutput);
}
}
functions = _.keys(functions);
if (functions.length === 0) {
console.error('[BMA] Unparseable unlock condition: ', output);
return;
}
console.debug('[BMA] Unlock conditions successfully parsed:', treeItem);
return {
unlockFunctions: functions,
unlockTree: treeItem
};
};
exports.node.parseEndPoint = function(endpoint, epPrefix) {
// Try BMA
var matches = exports.regexp.BMA_ENDPOINT.exec(endpoint);
if (matches) {
return {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"useSsl": matches[8] && matches[8] == 443,
"path": matches[10],
"useBma": true
};
}
// Try BMAS
matches = exports.regexp.BMAS_ENDPOINT.exec(endpoint);
if (matches) {
return {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"useSsl": true,
"path": matches[10],
"useBma": true
};
}
// Try BMATOR
matches = exports.regexp.BMATOR_ENDPOINT.exec(endpoint);
if (matches) {
return {
"dns": matches[1] || '',
"port": matches[2] || 80,
"useSsl": false,
"useTor": true,
"useBma": true
};
}
// Try WS2P
matches = exports.regexp.WS2P_ENDPOINT.exec(endpoint);
if (matches) {
return {
"ws2pid": matches[1] || '',
"dns": matches[3] || '',
"ipv4": matches[5] || '',
"ipv6": matches[7] || '',
"port": matches[9] || 80,
"useSsl": matches[9] && matches[9] == 443,
"path": matches[11] || '',
"useWs2p": true
};
}
// Try WS2PTOR
matches = exports.regexp.WS2PTOR_ENDPOINT.exec(endpoint);
if (matches) {
return {
"ws2pid": matches[1] || '',
"dns": matches[2] || '',
"port": matches[3] || 80,
"path": matches[4] || '',
"useSsl": false,
"useTor": true,
"useWs2p": true
};
}
// Use generic match
if (epPrefix) {
matches = exact(epPrefix + REGEX_ENDPOINT_PARAMS).exec(endpoint);
if (matches) {
return {
"dns": matches[2] || '',
"ipv4": matches[4] || '',
"ipv6": matches[6] || '',
"port": matches[8] || 80,
"useSsl": matches[8] && matches[8] == 443,
"path": matches[10],
"useBma": false
};
}
}
};
exports.copy = function(otherNode) {
var wasStarted = that.started;
var server = csHttp.getUrl(otherNode.host, otherNode.port, ''/*path*/, otherNode.useSsl);
var hasChanged = (server !== that.url);
if (hasChanged) {
that.init(otherNode.host, otherNode.port, otherNode.useSsl, that.useCache/*keep original value*/);
// Restart (only if was already started)
return wasStarted ? that.restart() : $q.when();
}
};
exports.wot.member.uids = function() {
return exports.wot.member.all()
.then(function(res){
return res.results.reduce(function(res, member){
res[member.pubkey] = member.uid;
return res;
}, {});
});
};
exports.wot.member.get = function(pubkey) {
return exports.wot.member.uids()
.then(function(memberUidsByPubkey){
var uid = memberUidsByPubkey[pubkey];
return {
pubkey: pubkey,
uid: (uid ? uid : null)
};
});
};
exports.wot.member.getByUid = function(uid) {
return exports.wot.member.all()
.then(function(res){
return _.findWhere(res.results, {uid: uid});
});
};
/**
* Return all expected blocks
* @param blockNumbers a rray of block number
*/
exports.blockchain.blocks = function(blockNumbers){
return exports.raw.getHttpRecursive(exports.blockchain.block, 'block', blockNumbers);
};
/**
* Return all expected blocks
* @param blockNumbers a rray of block number
*/
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);
}
});
});
};
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()
.then(function(res) {
if (!res.result.blocks || !res.result.blocks.length) {
return null;
}
var lastBlockWithUD = res.result.blocks[res.result.blocks.length - 1];
return exports.blockchain.block({block: lastBlockWithUD})
.then(function(block){
return (block.unitbase > 0) ? block.dividend * Math.pow(10, block.unitbase) : block.dividend;
});
});
};
exports.uri.parse = function(uri) {
return $q(function(resolve, reject) {
var pubkey;
// If pubkey: not need to parse
if (exact(regexp.PUBKEY).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 {
resolve({
pubkey: pubkey
});
}
}
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;
}
}
if (!currency){
if (host) {
csHttp.get(host + '/blockchain/parameters')()
.then(function(parameters){
resolve({
uid: uid,
pubkey: pubkey,
host: host,
currency: parameters.currency
});
})
.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) {
resolve({
uid: uid,
pubkey: pubkey,
currency: currency
});
}
// 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;
}
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'});
});
}
}
else {
console.debug("[BMA.parse] Could not parse URI: " + uri);
reject({message: 'ERROR.UNKNOWN_URI_FORMAT'});
}
})
// 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+"]"};
}
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
) :
// No URL define: use a fake function
function() {
return $q.when();
};
exports.version.latest = function() {
return exports.raw.getLatestRelease()
.then(function (json) {
if (!json) return;
if (json.name && json.html_url) {
return {
version: json.name,
url: json.html_url
};
}
if (json.tag_name && json.html_url) {
return {
version: json.tag_name.substring(1),
url: json.html_url
};
}
})
.catch(function(err) {
// silent (just log it)
console.error('[BMA] Failed to get Duniter latest version', err);
});
};
exports.websocket = {
block: ws('/ws/block'),
peer: ws('/ws/peer'),
close : closeWs
};
angular.merge(that, exports);
}
var service = new BMA(undefined, undefined, undefined, true);
service.instance = function(host, port, useSsl, useCache) {
return new BMA(host, port, useSsl, useCache);
};
service.lightInstance = function(host, port, useSsl, timeout) {
port = port || 80;
useSsl = angular.isDefined(useSsl) ? useSsl : (port == 443);
return {
host: host,
port: port,
useSsl: useSsl,
url: csHttp.getUrl(host, port, ''/*path*/, useSsl),
node: {
summary: csHttp.getWithCache(host, port, '/node/summary', useSsl, csHttp.cache.LONG, false, timeout)
},
network: {
peering: {
self: csHttp.get(host, port, '/network/peering', useSsl, timeout)
},
peers: csHttp.get(host, port, '/network/peers', useSsl, timeout)
},
blockchain: {
current: csHttp.get(host, port, '/blockchain/current', useSsl, timeout),
stats: {
hardship: csHttp.get(host, port, '/blockchain/hardship/:pubkey', useSsl, timeout)
}
}
};
};
// default action
//service.start();
return service;
}])
;
angular.module('cesium.wot.services', ['ngApi', 'cesium.bma.services', 'cesium.crypto.services', 'cesium.utils.services',
'cesium.settings.services'])
.factory('csWot', ['$q', '$timeout', 'BMA', 'Api', 'CacheFactory', 'csConfig', 'csCurrency', 'csSettings', 'csCache', function($q, $timeout, BMA, Api, CacheFactory, csConfig, csCurrency, csSettings, csCache) {
'ngInject';
function factory(id) {
var
api = new Api(this, "csWot-" + id),
identityCache = csCache.get('csWot-idty-', csCache.constants.SHORT),
// Add id, and remove duplicated id
_addUniqueIds = function(idties) {
var idtyKeys = {};
return idties.reduce(function(res, idty) {
idty.id = idty.id || idty.uid + '-' + idty.pubkey;
if (!idtyKeys[idty.id]) {
idtyKeys[idty.id] = true;
return res.concat(idty);
}
return res;
}, []);
},
_sortAndSliceIdentities = function(idties, offset, size) {
offset = offset || 0;
// Add unique ids
idties = _addUniqueIds(idties);
// Sort by block and
idties = _.sortBy(idties, function(idty){
var score = 1;
score += (1000000 * (idty.block));
score += (10 * (900 - idty.uid.toLowerCase().charCodeAt(0)));
return -score;
});
if (angular.isDefined(size) && idties.length > size) {
idties = idties.slice(offset, offset+size); // limit if more than expected size
}
return idties;
},
_sortCertifications = function(certifications) {
certifications = _.sortBy(certifications, function(cert){
var score = 1;
score += (1000000000000 * (cert.expiresIn ? cert.expiresIn : 0));
score += (10000000 * (cert.isMember ? 1 : 0));
score += (10 * (cert.block ? cert.block : 0));
return -score;
});
return certifications;
},
loadRequirements = function(pubkey, uid) {
if (!pubkey) return $q.when({});
// Get requirements
return BMA.wot.requirements({pubkey: pubkey})
.then(function(res){
if (!res.identities || !res.identities.length) return;
// Sort to select the best identity
if (res.identities.length > 1) {
// Select the best identity, by sorting using this order
// - same wallet uid
// - is member
// - has a pending membership
// - is not expired (in sandbox)
// - is not outdistanced
// - if has certifications
// max(count(certification)
// else
// max(membershipPendingExpiresIn) = must recent membership
res.identities = _.sortBy(res.identities, function(idty) {
var score = 0;
score += (10000000000 * ((uid && idty.uid === uid) ? 1 : 0));
score += (1000000000 * (idty.membershipExpiresIn > 0 ? 1 : 0));
score += (100000000 * (idty.membershipPendingExpiresIn > 0 ? 1 : 0));
score += (10000000 * (!idty.expired ? 1 : 0));
score += (1000000 * (!idty.outdistanced ? 1 : 0));
var certCount = !idty.expired && idty.certifications ? idty.certifications.length : 0;
score += (1 * (certCount ? certCount : 0));
score += (1 * (!certCount && idty.membershipPendingExpiresIn > 0 ? idty.membershipPendingExpiresIn/1000 : 0));
return -score;
});
console.debug('Found {0} identities. Will selected the best one'.format(res.identities.length));
}
var requirements = res.identities[0];
// Add useful custom fields
requirements.hasSelf = true;
requirements.needMembership = (requirements.membershipExpiresIn <= 0 &&
requirements.membershipPendingExpiresIn <= 0 );
requirements.needRenew = (!requirements.needMembership &&
requirements.membershipExpiresIn <= csSettings.data.timeWarningExpire &&
requirements.membershipPendingExpiresIn <= 0 );
requirements.canMembershipOut = (requirements.membershipExpiresIn > 0);
requirements.pendingMembership = (requirements.membershipExpiresIn <= 0 && requirements.membershipPendingExpiresIn > 0);
requirements.isMember = (requirements.membershipExpiresIn > 0);
// Force certification count to 0, is not a member yet - fix #269
requirements.certificationCount = (requirements.isMember && requirements.certifications) ? requirements.certifications.length : 0;
requirements.willExpireCertificationCount = requirements.certifications ? requirements.certifications.reduce(function(count, cert){
if (cert.expiresIn <= csSettings.data.timeWarningExpire) {
cert.willExpire = true;
return count + 1;
}
return count;
}, 0) : 0;
requirements.pendingRevocation = !requirements.revoked && !!requirements.revocation_sig;
return requirements;
})
.catch(function(err) {
// If not a member: continue
if (!!err &&
(err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER ||
err.ucode == BMA.errorCodes.NO_IDTY_MATCHING_PUB_OR_UID)) {
return {
hasSelf: false,
needMembership: true,
canMembershipOut: false,
needRenew: false,
pendingMembership: false,
needCertifications: false,
needCertificationCount: 0,
willNeedCertificationCount: 0
};
}
throw err;
});
},
loadIdentityByLookup = function(pubkey, uid) {
return BMA.wot.lookup({ search: pubkey||uid })
.then(function(res) {
if (!res || !res.results || !res.results.length) {
return {
uid: null,
pubkey: pubkey,
hasSelf: false
};
}
var identities = res.results.reduce(function(idties, res) {
return idties.concat(res.uids.reduce(function(uids, idty) {
var blockUid = idty.meta.timestamp.split('-', 2);
return uids.concat({
uid: idty.uid,
pubkey: res.pubkey,
timestamp: idty.meta.timestamp,
number: parseInt(blockUid[0]),
hash: blockUid[1],
revoked: idty.revoked,
revocationNumber: idty.revoked_on,
sig: idty.self
});
}, []));
}, []);
// Sort identities if need
if (identities.length) {
// Select the best identity, by sorting using this order
// - same given uid
// - not revoked
// - max(block_number)
identities = _.sortBy(identities, function(idty) {
var score = 0;
score += (10000000000 * ((uid && idty.uid === uid) ? 1 : 0));
score += (1000000000 * (!idty.revoked ? 1 : 0));
score += (1 * (idty.number ? idty.number : 0));
return -score;
});
}
var identity = identities[0];
// Retrieve time (self and revocation)
var blocks = [identity.number];
if (identity.revocationNumber) {
blocks.push(identity.revocationNumber);
}
return BMA.blockchain.blocks(blocks)
.then(function(blocks){
identity.sigDate = blocks[0].medianTime;
// Check if self has been done on a valid block
if (identity.number !== 0 && identity.hash !== blocks[0].hash) {
identity.hasBadSelfBlock = true;
}
// Set revocation time
if (identity.revocationNumber) {
identity.revocationTime = blocks[1].medianTime;
}
return identity;
})
.catch(function(err){
// Special case for currency init (root block not exists): use now
if (err && err.ucode == BMA.errorCodes.BLOCK_NOT_FOUND && identity.number === 0) {
identity.sigDate = csCurrency.date.now();
return identity;
}
else {
throw err;
}
});
})
.catch(function(err) {
if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_IDENTITY) { // Identity not found (if no self)
var identity = {
uid: null,
pubkey: pubkey,
hasSelf: false
};
return identity;
}
else {
throw err;
}
});
},
loadCertifications = function(getFunction, pubkey, lookupCertifications, parameters, medianTime, certifiersOf) {
function _certId(pubkey, block) {
return pubkey + '-' + block;
}
// TODO : remove this later (when all node will use duniter v0.50+)
var lookupHasCertTime = true; // Will be set ti FALSE before Duniter v0.50
var lookupCerticationsByCertId = lookupCertifications ? lookupCertifications.reduce(function(res, cert){
var certId = _certId(cert.pubkey, cert.cert_time ? cert.cert_time.block : cert.sigDate);
if (!cert.cert_time) lookupHasCertTime = false;
res[certId] = cert;
return res;
}, {}) : {};
var isMember = true;
return getFunction({ pubkey: pubkey })
.then(function(res) {
return res.certifications.reduce(function (res, cert) {
// Rappel :
// cert.sigDate = blockstamp de l'identité
// cert.cert_time.block : block au moment de la certification
// cert.written.number : block où la certification est écrite
var pending = !cert.written;
var certTime = cert.cert_time ? cert.cert_time.medianTime : null;
var expiresIn = (!certTime) ? 0 : (pending ?
(certTime + parameters.sigWindow - medianTime) :
(certTime + parameters.sigValidity - medianTime));
expiresIn = (expiresIn < 0) ? 0 : expiresIn;
// Remove from lookup certs
var certId = _certId(cert.pubkey, lookupHasCertTime && cert.cert_time ? cert.cert_time.block : cert.sigDate);
delete lookupCerticationsByCertId[certId];
// Add to result list
return res.concat({
pubkey: cert.pubkey,
uid: cert.uid,
time: certTime,
isMember: cert.isMember,
wasMember: cert.wasMember,
expiresIn: expiresIn,
willExpire: (expiresIn && expiresIn <= csSettings.data.timeWarningExpire),
pending: pending,
block: (cert.written !== null) ? cert.written.number :
(cert.cert_time ? cert.cert_time.block : null),
valid: (expiresIn > 0)
});
}, []);
})
.catch(function(err) {
if (!!err && err.ucode == BMA.errorCodes.NO_MATCHING_MEMBER) { // member not found
isMember = false;
return []; // continue (append pendings cert if exists in lookup)
}
else {
throw err;
}
})
// Add pending certs (found in lookup - see loadIdentityByLookup())
.then(function(certifications) {
var pendingCertifications = _.values(lookupCerticationsByCertId);
if (!pendingCertifications.length) return certifications; // No more pending continue
// Special case for initPhase - issue #
if (csCurrency.data.initPhase) {
return pendingCertifications.reduce(function(res, cert) {
return res.concat({
pubkey: cert.pubkey,
uid: cert.uid,
isMember: cert.isMember,
wasMember: cert.wasMember,
time: null,
expiresIn: parameters.sigWindow,
willExpire: false,
pending: true,
block: 0,
valid: true
});
}, certifications);
}
var pendingCertByBlocks = pendingCertifications.reduce(function(res, cert){
var block = lookupHasCertTime && cert.cert_time ? cert.cert_time.block :
(cert.sigDate ? cert.sigDate.split('-')[0] : null);
if (angular.isDefined(block)) {
if (!res[block]) {
res[block] = [cert];
}
else {
res[block].push(cert);
}
}
return res;
}, {});
// Set time to pending cert, from blocks
return BMA.blockchain.blocks(_.keys(pendingCertByBlocks)).then(function(blocks){
certifications = blocks.reduce(function(res, block){
return res.concat(pendingCertByBlocks[block.number].reduce(function(res, cert) {
var certTime = block.medianTime;
var expiresIn = Math.max(0, certTime + parameters.sigWindow - medianTime);
var validBuid = (!cert.cert_time || !cert.cert_time.block_hash || cert.cert_time.block_hash == block.hash);
if (!validBuid) {
console.debug("[wot] Invalid cert {0}: block hash changed".format(cert.pubkey.substring(0,8)));
}
var valid = (expiresIn > 0) && (!certifiersOf || cert.isMember) && validBuid;
return res.concat({
pubkey: cert.pubkey,
uid: cert.uid,
isMember: cert.isMember,
wasMember: cert.wasMember,
time: certTime,
expiresIn: expiresIn,
willExpire: (expiresIn && expiresIn <= csSettings.data.timeWarningExpire),
pending: true,
block: lookupHasCertTime && cert.cert_time ? cert.cert_time.block :
(cert.sigDate ? cert.sigDate.split('-')[0] : null),
valid: valid
});
}, []));
}, certifications);
return certifications;
});
})
// Sort and return result
.then(function(certifications) {
// Remove pending cert duplicated with a written & valid cert
var writtenCertByPubkey = certifications.reduce(function(res, cert) {
if (!cert.pending && cert.valid && cert.expiresIn >= parameters.sigWindow) {
res[cert.pubkey] = true;
}
return res;
}, {});
// Final sort
certifications = _sortCertifications(certifications);
// Split into valid/pending/error
var pendingCertifications = [];
var errorCertifications = [];
var validCertifications = certifications.reduce(function(res, cert) {
if (cert.pending) {
if (cert.valid && !writtenCertByPubkey[cert.pubkey]) {
pendingCertifications.push(cert);
}
else if (!cert.valid && !writtenCertByPubkey[cert.pubkey]){
errorCertifications.push(cert);
}
return res;
}
return res.concat(cert);
}, []);
return {
valid: validCertifications,
pending: pendingCertifications,
error: errorCertifications
};
})
;
},
finishLoadRequirements = function(data) {
data.requirements.needCertificationCount = (!data.requirements.needMembership && (data.requirements.certificationCount < data.sigQty)) ?
(data.sigQty - data.requirements.certificationCount) : 0;
data.requirements.willNeedCertificationCount = (!data.requirements.needMembership && !data.requirements.needCertificationCount &&
(data.requirements.certificationCount - data.requirements.willExpireCertificationCount) < data.sigQty) ?
(data.sigQty - data.requirements.certificationCount + data.requirements.willExpireCertificationCount) : 0;
data.requirements.pendingCertificationCount = data.received_cert_pending ? data.received_cert_pending.length : 0;
// Use /wot/lookup.revoked when requirements not filled
data.requirements.revoked = angular.isDefined(data.requirements.revoked) ? data.requirements.revoked : data.revoked;
// Add events
if (data.requirements.revoked) {
delete data.hasBadSelfBlock;
addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_REVOKED', messageParams: {revocationTime: data.revocationTime}});
console.debug("[wot] Identity [{0}] has been revoked".format(data.uid));
}
else if (data.requirements.pendingRevocation) {
addEvent(data, {type:'error', message: 'ERROR.IDENTITY_PENDING_REVOCATION'});
console.debug("[wot] Identity [{0}] has pending revocation".format(data.uid));
}
else if (data.hasBadSelfBlock) {
delete data.hasBadSelfBlock;
if (!data.isMember) {
addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_INVALID_BLOCK_HASH'});
console.debug("[wot] Invalid membership for {0}: block hash changed".format(data.uid));
}
}
else if (data.requirements.expired) {
addEvent(data, {type: 'error', message: 'ERROR.IDENTITY_EXPIRED'});
console.debug("[wot] Identity {0} expired (in sandbox)".format(data.uid));
}
else if (data.requirements.willNeedCertificationCount > 0) {
addEvent(data, {type: 'error', message: 'INFO.IDENTITY_WILL_MISSING_CERTIFICATIONS', messageParams: data.requirements});
console.debug("[wot] Identity {0} will need {1} certification(s)".format(data.uid, data.requirements.willNeedCertificationCount));
}
},
loadData = function(pubkey, withCache, uid, force) {
var data;
if (!pubkey && uid && !force) {
return BMA.wot.member.getByUid(uid)
.then(function(member) {
if (member) return loadData(member.pubkey, withCache, member.uid); // recursive call
//throw {message: 'NOT_A_MEMBER'};
return loadData(pubkey, withCache, uid, true/*force*/);
});
}
// Check cached data
if (pubkey) {
data = withCache ? identityCache.get(pubkey) : null;
if (data && (!uid || data.uid == uid)) {
console.debug("[wot] Identity " + pubkey.substring(0, 8) + " found in cache");
return $q.when(data);
}
console.debug("[wot] Loading identity " + pubkey.substring(0, 8) + "...");
data = {pubkey: pubkey};
}
else {
console.debug("[wot] Loading identity from uid " + uid);
data = {};
}
var now = Date.now();
var parameters;
var medianTime;
return $q.all([
// Get parameters
csCurrency.parameters()
.then(function(res) {
parameters = res;
data.sigQty = parameters.sigQty;
data.sigStock = parameters.sigStock;
}),
// Get current time
csCurrency.blockchain.current()
.then(function(current) {
medianTime = current.medianTime;
})
.catch(function(err){
// Special case for currency init (root block not exists): use now
if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) {
medianTime = Math.trunc(new Date().getTime()/1000);
}
else {
throw err;
}
}),
// Get requirements
$q.when()
.then(function () {
data.requirements = {};
data.isMember = false;
}),
// loadRequirements(pubkey, uid)
// .then(function (requirements) {
// data.requirements = requirements;
// data.isMember = requirements.isMember;
// }),
// Get identity using lookup
loadIdentityByLookup(pubkey, uid)
.then(function (identity) {
angular.merge(data, identity);
})
])
.then(function() {
if (!data.requirements.uid) {
return;
}
var idtyFullKey = data.requirements.uid + '-' + data.requirements.meta.timestamp;
return $q.all([
// Get received certifications
loadCertifications(BMA.wot.certifiersOf, data.pubkey, data.lookup ? data.lookup.certifications[idtyFullKey] : null, parameters, medianTime, true /*certifiersOf*/)
.then(function (res) {
data.received_cert = res.valid;
data.received_cert_pending = res.pending;
data.received_cert_error = res.error;
}),
// Get given certifications
loadCertifications(BMA.wot.certifiedBy, data.pubkey, data.lookup ? data.lookup.givenCertifications : null, parameters, medianTime, false/*certifiersOf*/)
.then(function (res) {
data.given_cert = res.valid;
data.given_cert_pending = res.pending;
data.given_cert_error = res.error;
})
]);
})
.then(function() {
// Add compute some additional requirements (that required all data like certifications)
finishLoadRequirements(data);
// API extension
return api.data.raisePromise.load(data)
.catch(function(err) {
console.debug('Error while loading identity data, on extension point.');
console.error(err);
});
})
.then(function() {
if (!data.pubkey) return undefined; // not found
delete data.lookup; // not need anymore
identityCache.put(data.pubkey, data); // add to cache
console.debug('[wot] Identity '+ data.pubkey.substring(0, 8) +' loaded in '+ (Date.now()-now) +'ms');
return data;
});
},
search = function(text, options) {
if (!text || text.trim() !== text) {
return $q.when(undefined);
}
// Remove first special characters (to avoid request error)
var safeText = text.replace(/(^|\s)#\w+/g, ''); // remove tags
safeText = safeText.replace(/[^a-zA-Z0-9_-\s]+/g, '');
safeText = safeText.replace(/\s+/g, ' ').trim();
options = options || {};
options.addUniqueId = angular.isDefined(options.addUniqueId) ? options.addUniqueId : true;
options.allowExtension = angular.isDefined(options.allowExtension) ? options.allowExtension : true;
options.excludeRevoked = angular.isDefined(options.excludeRevoked) ? options.excludeRevoked : false;
var promise;
if (!safeText) {
promise = $q.when([]);
}
else {
promise = $q.all(
safeText.split(' ').reduce(function(res, text) {
console.debug('[wot] Will search on: \'' + text + '\'');
return res.concat(BMA.wot.lookup({ search: text }));
}, [])
).then(function(res){
return res.reduce(function(idties, res) {
return idties.concat(res.results.reduce(function(idties, res) {
return idties.concat(res.uids.reduce(function(uids, idty) {
var blocUid = idty.meta.timestamp.split('-', 2);
var revoked = !idty.revoked && idty.revocation_sig;
if (!options.excludeRevoked || !revoked) {
return uids.concat({
uid: idty.uid,
pubkey: res.pubkey,
number: blocUid[0],
hash: blocUid[1],
revoked: revoked
});
}
return uids;
}, []));
}, []));
}, []);
})
.catch(function(err) {
if (err && err.ucode == BMA.errorCodes.NO_MATCHING_IDENTITY) {
return [];
}
else {
throw err;
}
});
}
return promise
.then(function(idties) {
if (!options.allowExtension) {
// Add unique id (if enable)
return options.addUniqueId ? _addUniqueIds(idties) : idties;
}
var lookupResultCount = idties.length;
// call extension point
return api.data.raisePromise.search(text, idties, 'pubkey')
.then(function() {
// Make sure to add uid to new results - fix #488
if (idties.length > lookupResultCount) {
var idtiesWithoutUid = _.filter(idties, function(idty) {
return !idty.uid && idty.pubkey;
});
if (idtiesWithoutUid.length) {
return BMA.wot.member.uids()
.then(function(uids) {
_.forEach(idties, function(idty) {
if (!idty.uid && idty.pubkey) {
idty.uid = uids[idty.pubkey];
}
});
});
}
}
})
.then(function() {
// Add unique id (if enable)
return options.addUniqueId ? _addUniqueIds(idties) : idties;
});
});
},
getNewcomers = function(offset, size) {
offset = offset || 0;
size = size || 20;
return BMA.blockchain.stats.newcomers()
.then(function(res) {
if (!res.result.blocks || !res.result.blocks.length) {
return null;
}
var blocks = _.sortBy(res.result.blocks, function (n) {
return -n;
});
return getNewcomersRecursive(blocks, 0, 5, offset+size);
})
.then(function(idties){
if (!idties || !idties.length) {
return null;
}
idties = _sortAndSliceIdentities(idties, offset, size);
// Extension point
return extendAll(idties, 'pubkey', true/*skipAddUid*/);
});
},
getNewcomersRecursive = function(blocks, offset, size, maxResultSize) {
return $q(function(resolve, reject) {
var result = [];
var jobs = [];
_.each(blocks.slice(offset, offset+size), function(number) {
jobs.push(
BMA.blockchain.block({block: number})
.then(function(block){
if (!block || !block.joiners) return;
_.each(block.joiners, function(joiner){
var parts = joiner.split(':');
var idtyKey = parts[parts.length-1]/*uid*/ + '-' + parts[0]/*pubkey*/;
result.push({
id: idtyKey,
uid: parts[parts.length-1],
pubkey:parts[0],
memberDate: block.medianTime,
block: block.number
});
});
})
);
});
$q.all(jobs)
.then(function() {
if (result.length < maxResultSize && offset < blocks.length - 1) {
$timeout(function() {
getNewcomersRecursive(blocks, offset+size, size, maxResultSize - result.length)
.then(function(res) {
resolve(result.concat(res));
})
.catch(function(err) {
reject(err);
});
}, 1000);
}
else {
resolve(result);
}
})
.catch(function(err){
if (err && err.ucode === BMA.errorCodes.HTTP_LIMITATION) {
resolve(result);
}
else {
reject(err);
}
});
});
},
getPending = function(offset, size) {
offset = offset || 0;
size = size || 20;
return $q.all([
BMA.wot.member.uids(),
BMA.wot.member.pending()
.then(function(res) {
return (res.memberships && res.memberships.length) ? res.memberships : undefined;
})
])
.then(function(res) {
var uids = res[0];
var memberships = res[1];
if (!memberships) return;
var idtiesByBlock = {};
var idtiesByPubkey = {};
_.forEach(memberships, function(ms){
if (ms.membership == 'IN' && !uids[ms.pubkey]) {
var idty = {
uid: ms.uid,
pubkey: ms.pubkey,
block: ms.blockNumber,
blockHash: ms.blockHash
};
var otherIdtySamePubkey = idtiesByPubkey[ms.pubkey];
if (otherIdtySamePubkey && idty.block > otherIdtySamePubkey.block) {
return; // skip
}
idtiesByPubkey[idty.pubkey] = idty;
if (!idtiesByBlock[idty.block]) {
idtiesByBlock[idty.block] = [idty];
}
else {
idtiesByBlock[idty.block].push(idty);
}
// Remove previous idty from map
if (otherIdtySamePubkey) {
idtiesByBlock[otherIdtySamePubkey.block] = idtiesByBlock[otherIdtySamePubkey.block].reduce(function(res, aidty){
if (aidty.pubkey == otherIdtySamePubkey.pubkey) return res; // if match idty to remove, to NOT add
return (res||[]).concat(aidty);
}, null);
if (idtiesByBlock[otherIdtySamePubkey.block] === null) {
delete idtiesByBlock[otherIdtySamePubkey.block];
}
}
}
});
var idties = _sortAndSliceIdentities(_.values(idtiesByPubkey), offset, size);
var blocks = idties.reduce(function(res, aidty) {
return res.concat(aidty.block);
}, []);
return $q.all([
// Get time from blocks
BMA.blockchain.blocks(_.uniq(blocks))
.then(function(blocks) {
_.forEach(blocks, function(block){
_.forEach(idtiesByBlock[block.number], function(idty) {
idty.sigDate = block.medianTime;
if (block.number !== 0 && idty.blockHash !== block.hash) {
addEvent(idty, {type:'error', message: 'ERROR.WOT_PENDING_INVALID_BLOCK_HASH'});
console.debug("Invalid membership for uid={0}: block hash changed".format(idty.uid));
}
});
});
}),
// Extension point
extendAll(idties, 'pubkey', true/*skipAddUid*/)
])
.then(function() {
return idties;
});
});
},
getAll = function() {
var letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','u','v','w','x','y','z'];
return getAllRecursive(letters, 0, BMA.constants.LIMIT_REQUEST_COUNT)
.then(function(idties) {
return extendAll(idties, 'pubkey', true/*skipAddUid*/)
.then(function() {
return _addUniqueIds(idties);
});
});
},
getAllRecursive = function(letters, offset, size) {
return $q(function(resolve, reject) {
var result = [];
var pubkeys = {};
var jobs = [];
_.each(letters.slice(offset, offset+size), function(letter) {
jobs.push(
search(letter, {
addUniqueId: false, // will be done in parent method
allowExtension: false // extension point will be called in parent method
})
.then(function(idties){
if (!idties || !idties.length) return;
result = idties.reduce(function(res, idty) {
if (!pubkeys[idty.pubkey]) {
pubkeys[idty.pubkey] = true;
return res.concat(idty);
}
return res;
}, result);
})
);
});
$q.all(jobs)
.then(function() {
if (offset < letters.length - 1) {
$timeout(function() {
getAllRecursive(letters, offset+size, size)
.then(function(idties) {
if (!idties || !idties.length) {
resolve(result);
return;
}
resolve(idties.reduce(function(res, idty) {
if (!pubkeys[idty.pubkey]) {
pubkeys[idty.pubkey] = true;
return res.concat(idty);
}
return res;
}, result));
})
.catch(function(err) {
reject(err);
});
}, BMA.constants.LIMIT_REQUEST_DELAY);
}
else {
resolve(result);
}
})
.catch(function(err){
if (err && err.ucode === BMA.errorCodes.HTTP_LIMITATION) {
resolve(result);
}
else {
reject(err);
}
});
});
},
extend = function(idty, pubkeyAttributeName, skipAddUid) {
return extendAll([idty], pubkeyAttributeName, skipAddUid)
.then(function(res) {
return res[0];
});
},
extendAll = function(idties, pubkeyAttributeName, skipAddUid) {
pubkeyAttributeName = pubkeyAttributeName || 'pubkey';
var jobs = [];
if (!skipAddUid) jobs.push(BMA.wot.member.uids());
jobs.push(api.data.raisePromise.search(null, idties, pubkeyAttributeName)
.catch(function(err) {
console.debug('Error while search identities, on extension point.');
console.error(err);
}));
return $q.all(jobs)
.then(function(res) {
if (!skipAddUid) {
var uidsByPubkey = res[0];
// Set uid (on every data)
_.forEach(idties, function(data) {
if (!data.uid && data[pubkeyAttributeName]) {
data.uid = uidsByPubkey[data[pubkeyAttributeName]];
// Remove name if redundant with uid
if (data.uid && data.uid == data.name) {
delete data.name;
}
}
});
}
return idties;
});
},
addEvent = function(data, event) {
event = event || {};
event.type = event.type || 'info';
event.message = event.message || '';
event.messageParams = event.messageParams || {};
data.events = data.events || [];
data.events.push(event);
}
;
// Register extension points
api.registerEvent('data', 'load');
api.registerEvent('data', 'search');
return {
id: id,
load: loadData,
search: search,
newcomers: getNewcomers,
pending: getPending,
all: getAll,
extend: extend,
extendAll: extendAll,
// api extension
api: api
};
}
var service = factory('default', BMA);
service.instance = factory;
return service;
}]);
angular.module('cesium.tx.services', ['ngApi', 'cesium.bma.services',
'cesium.settings.services', 'cesium.wot.services' ])
.factory('csTx', ['$q', '$timeout', '$filter', '$translate', 'UIUtils', 'BMA', 'Api', 'csConfig', 'csSettings', 'csWot', 'FileSaver', function($q, $timeout, $filter, $translate, UIUtils, BMA, Api, csConfig, csSettings, csWot, FileSaver) {
'ngInject';
function factory(id, BMA) {
var
api = new Api(this, "csTx-" + id),
_reduceTxAndPush = function(pubkey, txArray, result, processedTxMap, allowPendings) {
if (!txArray || txArray.length === 0) {
return;
}
_.forEach(txArray, function(tx) {
if (tx.block_number || allowPendings) {
var walletIsIssuer = false;
var otherIssuer = tx.issuers.reduce(function(issuer, res) {
walletIsIssuer = (res === pubkey) ? true : walletIsIssuer;
return issuer + ((res !== pubkey) ? ', ' + res : '');
}, '');
if (otherIssuer.length > 0) {
otherIssuer = otherIssuer.substring(2);
}
var otherReceiver;
var outputBase;
var sources = [];
var lockedOutputs;
var amount = tx.outputs.reduce(function(sum, output, noffset) {
var outputArray = output.split(':',3);
outputBase = parseInt(outputArray[1]);
var outputAmount = powBase(parseInt(outputArray[0]), outputBase);
var outputCondition = outputArray[2];
var sigMatches = BMA.regexp.TX_OUTPUT_SIG.exec(outputCondition);
// Simple unlock condition
if (sigMatches) {
var outputPubkey = sigMatches[1];
if (outputPubkey == pubkey) { // output is for the wallet
if (!walletIsIssuer) {
return sum + outputAmount;
}
// If pending: use output as new sources
else if (tx.block_number === null) {
sources.push({
amount: parseInt(outputArray[0]),
base: outputBase,
type: 'T',
identifier: tx.hash,
noffset: noffset,
consumed: false
});
}
}
else { // output is for someone else
if (outputPubkey !== '' && outputPubkey != otherIssuer) {
otherReceiver = outputPubkey;
}
if (walletIsIssuer) {
return sum - outputAmount;
}
}
}
// Complex unlock condition, on the issuer pubkey
else if (outputCondition.indexOf('SIG('+pubkey+')') != -1) {
var lockedOutput = BMA.tx.parseUnlockCondition(outputCondition);
if (lockedOutput) {
// Add a source
// FIXME: should be uncomment when filtering source on transfer()
/*sources.push(angular.merge({
amount: parseInt(outputArray[0]),
base: outputBase,
type: 'T',
identifier: tx.hash,
noffset: noffset,
consumed: false
}, lockedOutput));
*/
lockedOutput.amount = outputAmount;
lockedOutputs = lockedOutputs || [];
lockedOutputs.push(lockedOutput);
console.debug('[tx] has locked output:', lockedOutput);
return sum + outputAmount;
}
}
return sum;
}, 0);
var txPubkey = amount > 0 ? otherIssuer : otherReceiver;
var time = tx.time || tx.blockstampTime;
// Avoid duplicated tx, or tx to him self
var txKey = amount + ':' + tx.hash + ':' + time;
if (!processedTxMap[txKey] && amount !== 0) {
processedTxMap[txKey] = true;
var newTx = {
time: time,
amount: amount,
pubkey: txPubkey,
comment: tx.comment,
isUD: false,
hash: tx.hash,
locktime: tx.locktime,
block_number: tx.block_number
};
// If pending: store sources and inputs for a later use - see method processTransactionsAndSources()
if (walletIsIssuer && tx.block_number === null) {
newTx.inputs = tx.inputs;
newTx.sources = sources;
}
if (lockedOutputs) {
newTx.lockedOutputs = lockedOutputs;
}
result.push(newTx);
}
}
});
},
loadTx = function(pubkey, fromTime, existingPendings) {
return $q(function(resolve, reject) {
var txHistory = [];
var udHistory = [];
var txPendings = [];
var nowInSec = Math.trunc(new Date().getTime() / 1000); // TODO test to replace using moment().utc().unix()
fromTime = fromTime || (nowInSec - csSettings.data.walletHistoryTimeSecond);
var processedTxMap = {};
var tx = {
pendings: []
};
var _reduceTx = function(res){
_reduceTxAndPush(pubkey, res.history.sent, txHistory, processedTxMap);
_reduceTxAndPush(pubkey, res.history.received, txHistory, processedTxMap);
_reduceTxAndPush(pubkey, res.history.sending, txHistory, processedTxMap);
_reduceTxAndPush(pubkey, res.history.pending, txPendings, processedTxMap, true /*allow pendings*/);
};
var jobs = [
// get pendings history
BMA.tx.history.pending({pubkey: pubkey})
.then(_reduceTx)
];
// get TX history since
if (fromTime !== -1) {
var sliceTime = csSettings.data.walletHistorySliceSecond;
for(var i = fromTime - (fromTime % sliceTime); i - sliceTime < nowInSec; i += sliceTime) {
jobs.push(BMA.tx.history.times({pubkey: pubkey, from: i, to: i+sliceTime-1})
.then(_reduceTx)
);
}
jobs.push(BMA.tx.history.timesNoCache({pubkey: pubkey, from: nowInSec - (nowInSec % sliceTime), to: nowInSec+999999999})
.then(_reduceTx));
}
// get all TX
else {
jobs.push(BMA.tx.history.all({pubkey: pubkey})
.then(_reduceTx)
);
}
// get UD history
// FIXME issue#232
/*
if (csSettings.data.showUDHistory) {
jobs.push(
BMA.ud.history({pubkey: pubkey})
.then(function(res){
udHistory = !res.history || !res.history.history ? [] :
res.history.history.reduce(function(res, ud){
if (ud.time < fromTime) return res; // skip to old UD
var amount = powBase(ud.amount, ud.base);
return res.concat({
time: ud.time,
amount: amount,
isUD: true,
block_number: ud.block_number
});
}, []);
}));
}
*/
// Execute jobs
$q.all(jobs)
.then(function(){
// sort by time desc
tx.history = txHistory.concat(udHistory).sort(function(tx1, tx2) {
return (tx2.time - tx1.time);
});
tx.pendings = txPendings;
tx.fromTime = fromTime;
tx.toTime = tx.history.length ? tx.history[0].time /*=max(tx.time)*/: fromTime;
resolve(tx);
})
.catch(function(err) {
tx.history = [];
tx.pendings = [];
tx.errors = [];
delete tx.fromTime;
delete tx.toTime;
reject(err);
});
});
},
powBase = function(amount, base) {
return base <= 0 ? amount : amount * Math.pow(10, base);
},
addSource = function(src, sources, sourcesIndexByKey) {
var srcKey = src.type+':'+src.identifier+':'+src.noffset;
if (angular.isUndefined(sourcesIndexByKey[srcKey])) {
sources.push(src);
sourcesIndexByKey[srcKey] = sources.length - 1;
}
},
addSources = function(result, sources) {
_(sources).forEach(function(src) {
addSource(src, result.sources, result.sourcesIndexByKey);
});
},
loadSourcesAndBalance = function(pubkey) {
return BMA.tx.sources({pubkey: pubkey})
.then(function(res){
var result = {
sources: [],
sourcesIndexByKey: [],
balance: 0
};
if (res.sources && res.sources.length) {
_.forEach(res.sources, function(src) {
src.consumed = false;
result.balance += powBase(src.amount, src.base);
});
addSources(result, res.sources);
}
return result;
});
},
loadData = function(pubkey, fromTime) {
var now = new Date().getTime();
var data = {};
return $q.all([
// Load Sources
loadSourcesAndBalance(pubkey),
// Load Tx
loadTx(pubkey, fromTime)
])
.then(function(res) {
angular.merge(data, res[0]);
data.tx = res[1];
var txPendings = [];
var txErrors = [];
var balance = data.balance;
function _processPendingTx(tx) {
var consumedSources = [];
var valid = true;
if (tx.amount > 0) { // do not check sources from received TX
valid = false;
// TODO get sources from the issuer ?
}
else {
_.forEach(tx.inputs, function(input) {
var inputKey = input.split(':').slice(2).join(':');
var srcIndex = data.sourcesIndexByKey[inputKey];
if (angular.isDefined(srcIndex)) {
consumedSources.push(data.sources[srcIndex]);
}
else {
valid = false;
return false; // break
}
});
if (tx.sources) { // add source output
addSources(data, tx.sources);
}
delete tx.sources;
delete tx.inputs;
}
if (valid) {
balance += tx.amount; // update balance
txPendings.push(tx);
_.forEach(consumedSources, function(src) {
src.consumed=true;
});
}
else {
txErrors.push(tx);
}
}
var txs = data.tx.pendings;
var retry = true;
while(txs && txs.length > 0) {
// process TX pendings
_.forEach(txs, _processPendingTx);
// Retry once (TX could be chained and processed in a wrong order)
if (txErrors.length > 0 && txPendings.length > 0 && retry) {
txs = txErrors;
txErrors = [];
retry = false;
}
else {
txs = null;
}
}
data.tx.pendings = txPendings;
data.tx.errors = txErrors;
data.balance = balance;
// Will add uid (+ plugin will add name, avatar, etc. if enable)
return csWot.extendAll((data.tx.history || []).concat(data.tx.pendings||[]), 'pubkey');
})
.then(function() {
console.debug('[tx] TX and sources loaded in '+ (new Date().getTime()-now) +'ms');
return data;
});
};
// Download TX history file
downloadHistoryFile = function(pubkey, options) {
options = options || {};
options.fromTime = options.fromTime || -1;
console.debug("[tx] Exporting TX history for pubkey [{0}]".format(pubkey.substr(0,8)));
return $q.all([
$translate(['ACCOUNT.HEADERS.TIME',
'COMMON.UID',
'COMMON.PUBKEY',
'ACCOUNT.HEADERS.AMOUNT',
'ACCOUNT.HEADERS.COMMENT']),
//TODO : Utiliser plutôt csCurency pour avoir le bloc courant
BMA.blockchain.current(),
loadData(pubkey, options.fromTime)
])
.then(function(result){
var translations = result[0];
var currentBlock = result[1];
var currentTime = (currentBlock && currentBlock.medianTime) || moment().utc().unix();
var currency = currentBlock && currentBlock.currency;
result = result[2];
// no TX
if (!result || !result.tx || !result.tx.history) {
return UIUtils.toast.show('INFO.EMPTY_TX_HISTORY');
}
return $translate('ACCOUNT.FILE_NAME', {currency: currency, pubkey: pubkey, currentTime : currentTime})
.then(function(result){
var formatDecimal = $filter('formatDecimal');
var formatPubkey = $filter('formatPubkey');
var formatDate = $filter('formatDate');
var formatDateForFile = $filter('formatDateForFile');
var formatSymbol = $filter('currencySymbolNoHtml');
var headers = [
translations['ACCOUNT.HEADERS.TIME'],
translations['COMMON.UID'],
translations['COMMON.PUBKEY'],
translations['ACCOUNT.HEADERS.AMOUNT'] + ' (' + formatSymbol(currency) + ')',
translations['ACCOUNT.HEADERS.COMMENT']
];
var content = result.tx.history.reduce(function(res, tx){
return res.concat([
formatDate(tx.time),
tx.uid,
tx.pubkey,
formatDecimal(tx.amount/100),
'"' + tx.comment + '"'
].join(';') + '\n');
}, [headers.join(';') + '\n']);
var file = new Blob(content, {type: 'text/plain; charset=utf-8'});
FileSaver.saveAs(file, result);
});
});
};
return {
id: id,
load: loadData,
downloadHistoryFile: downloadHistoryFile,
// api extension
api: api
};
}
var service = factory('default', BMA);
service.instance = factory;
return service;
}]);
angular.module('cesium.wallet.services', ['ngApi', 'ngFileSaver', 'cesium.bma.services', 'cesium.crypto.services', 'cesium.utils.services',
'cesium.settings.services'])
.factory('csWallet', ['$q', '$rootScope', '$timeout', '$translate', '$filter', 'Api', 'localStorage', 'CryptoUtils', 'BMA', 'csConfig', 'csSettings', 'FileSaver', 'Blob', 'csWot', 'csTx', 'csCurrency', function($q, $rootScope, $timeout, $translate, $filter, Api, localStorage,
CryptoUtils, BMA, csConfig, csSettings, FileSaver, Blob, csWot, csTx, csCurrency) {
'ngInject';
function factory(id, BMA) {
var
constants = {
OLD_STORAGE_KEY: 'CESIUM_DATA',
STORAGE_KEY: 'GCHANGE_DATA'
},
data = {},
listeners,
started,
startPromise,
api = new Api(this, 'csWallet-' + id),
resetData = function(init) {
data.loaded = false;
data.pubkey= null;
data.keypair = {
signSk: null,
signPk: null
};
data.uid = null;
data.isNew = null;
data.events = [];
resetKeypair();
started = false;
startPromise = undefined;
if (init) {
api.data.raise.init(data);
}
else {
if (!csSettings.data.useLocalStorage) {
csSettings.reset();
}
api.data.raise.reset(data);
}
},
resetKeypair = function(){
data.keypair = {
signSk: null,
signPk: null
};
},
login = function(salt, password) {
return CryptoUtils.scryptKeypair(salt, password)
.then(function(keypair) {
// Copy result to properties
data.pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
// FOR DEV ONLY - on crosschain
// console.error('TODO REMOVE this code - dev only'); data.pubkey = '36j6pCNzKDPo92m7UXJLFpgDbcLFAZBgThD2TCwTwGrd';
data.keypair = keypair;
// Call login check (can stop the login process)
return api.data.raisePromise.loginCheck(data)
// reset data then stop process
.catch(function(err) {
resetData();
throw err;
})
// Call extend api (cannot stop login process)
.then(function() {
return api.data.raisePromise.login(data);
});
})
// store if need
.then(function() {
if (csSettings.data.useLocalStorage) {
store();
}
return data;
});
},
logout = function() {
return $q(function(resolve, reject) {
resetData(); // will reset keypair
store(); // store (if local storage enable)
// Send logout event
api.data.raise.logout();
// Send unauth event (compat with new Cesium auth)
api.data.raise.unauth();
resolve();
});
},
isLogin = function() {
return !!data.pubkey;
},
getKeypair = function(options) {
if (!started) {
return (startPromise || start())
.then(function () {
return getKeypair(options); // loop
});
}
if (isLogin()) {
return $q.when(data.keypair);
}
return $q.reject('Not auth');
},
isDataLoaded = function(options) {
if (options && options.minData) return data.loaded;
return data.loaded; // && data.sources; -- Gchange not use sources
},
isNeverUsed = function() {
if (!data.loaded) return undefined; // undefined if not full loaded
return !data.pubkey || !(
// Check extended data (name, profile, avatar)
data.name ||
data.profile ||
data.avatar
);
},
isNew = function() {return !!data.isNew;},
// If connected and same pubkey
isUserPubkey = function(pubkey) {
return isLogin() && data.pubkey === pubkey;
},
store = function() {
if (csSettings.data.useLocalStorage) {
if (isLogin() && csSettings.data.rememberMe) {
var dataToStore = {
keypair: data.keypair,
pubkey: data.pubkey,
version: csConfig.version
};
localStorage.setObject(constants.STORAGE_KEY, dataToStore);
}
else {
localStorage.setObject(constants.STORAGE_KEY, null);
}
}
else {
localStorage.setObject(constants.STORAGE_KEY, null);
}
// Remove old storage key
localStorage.setObject(constants.OLD_STORAGE_KEY, null);
},
restore = function() {
return $q.all([
localStorage.get(constants.STORAGE_KEY),
localStorage.get(constants.OLD_STORAGE_KEY)
])
.then(function(res) {
var dataStr = res[0] || res[1];
if (!dataStr) return;
return fromJson(dataStr, false)
.then(function(storedData){
if (storedData && storedData.keypair && storedData.pubkey) {
data.keypair = storedData.keypair;
data.pubkey = storedData.pubkey;
data.loaded = false;
// Call extend api
return api.data.raisePromise.login(data);
}
})
.then(function(){
return data;
});
});
},
getData = function() {
return data;
},
loadData = function(options) {
if (options && options.minData) {
return loadMinData(options);
}
if (options || data.loaded) {
return refreshData(options);
}
return loadFullData();
},
loadFullData = function() {
data.loaded = false;
return $q.all([
// API extension
api.data.raisePromise.load(data, null)
.catch(function(err) {
console.error('Error while loading wallet data, on extension point. Try to continue');
console.error(err);
})
])
.then(function() {
return api.data.raisePromise.finishLoad(data)
.catch(function(err) {
console.error('Error while finishing wallet data load, on extension point. Try to continue');
console.error(err);
});
})
.then(function() {
data.loaded = true;
return data;
})
.catch(function(err) {
data.loaded = false;
throw err;
});
},
loadMinData = function(options) {
options = options || {};
return refreshData(options);
},
refreshData = function(options) {
options = options || {
api: true
};
// Force some load (parameters & requirements) if not already loaded
var jobs = [];
// Reset events
cleanEventsByContext('requirements');
// API extension (force if no other jobs)
if (!jobs.length || options.api) jobs.push(api.data.raisePromise.load(data, options));
return $q.all(jobs)
.then(function(){
return api.data.raisePromise.finishLoad(data);
})
.then(function(){
return data;
});
},
addEvent = function(event, insertAtFirst) {
event = event || {};
event.type = event.type || 'info';
event.message = event.message || '';
event.messageParams = event.messageParams || {};
event.context = event.context || 'undefined';
if (event.message.trim().length) {
if (!insertAtFirst) {
data.events.push(event);
}
else {
data.events.splice(0, 0, event);
}
}
else {
console.debug('Event without message. Skipping this event');
}
},
getkeypairSaveId = function(record) {
var nbCharSalt = Math.round(record.answer.length / 2);
var salt = record.answer.substr(0, nbCharSalt);
var pwd = record.answer.substr(nbCharSalt);
return CryptoUtils.scryptKeypair(salt, pwd)
.then(function (keypair) {
record.pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
record.keypair = keypair;
return record;
});
},
getCryptedId = function(record){
return getkeypairSaveId(record)
.then(function() {
return CryptoUtils.util.random_nonce();
})
.then(function(nonce) {
record.nonce = nonce;
return CryptoUtils.box.pack(record.salt, record.nonce, record.keypair.boxPk, record.keypair.boxSk);
})
.then(function (cypherSalt) {
record.salt = cypherSalt;
return CryptoUtils.box.pack(record.pwd, record.nonce, record.keypair.boxPk, record.keypair.boxSk);
})
.then(function (cypherPwd) {
record.pwd = cypherPwd;
record.nonce = CryptoUtils.util.encode_base58(record.nonce);
return record;
});
},
recoverId = function(recover) {
var nonce = CryptoUtils.util.decode_base58(recover.cypherNonce);
return getkeypairSaveId(recover)
.then(function (recover) {
return CryptoUtils.box.open(recover.cypherSalt, nonce, recover.keypair.boxPk, recover.keypair.boxSk);
})
.then(function (salt) {
recover.salt = salt;
return CryptoUtils.box.open(recover.cypherPwd, nonce, recover.keypair.boxPk, recover.keypair.boxSk);
})
.then(function (pwd) {
recover.pwd = pwd;
return recover;
})
.catch(function(err){
console.warn('Incorrect answers - Unable to recover passwords');
});
},
getSaveIDDocument = function(record) {
var saveId = 'Version: 10 \n' +
'Type: SaveID\n' +
'Questions: ' + '\n' + record.questions +
'Issuer: ' + data.pubkey + '\n' +
'Crypted-Nonce: '+ record.nonce + '\n'+
'Crypted-Pubkey: '+ record.pubkey +'\n' +
'Crypted-Salt: '+ record.salt + '\n' +
'Crypted-Pwd: '+ record.pwd + '\n';
// Sign SaveId document
return CryptoUtils.sign(saveId, data.keypair)
.then(function(signature) {
saveId += signature + '\n';
console.debug('Has generate an SaveID document:\n----\n' + saveId + '----');
return saveId;
});
},
downloadSaveId = function(record){
return getSaveIDDocument(record)
.then(function(saveId) {
var saveIdFile = new Blob([saveId], {type: 'text/plain; charset=utf-8'});
FileSaver.saveAs(saveIdFile, 'saveID.txt');
});
},
cleanEventsByContext = function(context){
data.events = data.events.reduce(function(res, event) {
if (event.context && event.context == context) return res;
return res.concat(event);
},[]);
},
/**
* De-serialize from JSON string
*/
fromJson = function(json, failIfInvalid) {
failIfInvalid = angular.isUndefined(failIfInvalid) ? true : failIfInvalid;
return $q(function(resolve, reject) {
var obj = JSON.parse(json || '{}');
// FIXME #379
/*if (obj && obj.pubkey) {
resolve({
pubkey: obj.pubkey
});
}
else */
if (obj && obj.keypair && obj.keypair.signPk && obj.keypair.signSk) {
var keypair = {};
var i;
// sign Pk : Convert to Uint8Array type
var signPk = new Uint8Array(32);
for (i = 0; i < 32; i++) signPk[i] = obj.keypair.signPk[i];
keypair.signPk = signPk;
var signSk = new Uint8Array(64);
for (i = 0; i < 64; i++) signSk[i] = obj.keypair.signSk[i];
keypair.signSk = signSk;
// box Pk : Convert to Uint8Array type
if (obj.version && obj.keypair.boxPk) {
var boxPk = new Uint8Array(32);
for (i = 0; i < 32; i++) boxPk[i] = obj.keypair.boxPk[i];
keypair.boxPk = boxPk;
}
if (obj.version && obj.keypair.boxSk) {
var boxSk = new Uint8Array(32);
for (i = 0; i < 64; i++) boxSk[i] = obj.keypair.boxSk[i];
keypair.boxSk = boxSk;
}
resolve({
pubkey: obj.pubkey,
keypair: keypair,
tx: obj.tx
});
}
else if (failIfInvalid) {
reject('Not a valid Wallet.data object');
}
else {
resolve();
}
});
}
;
function addListeners() {
listeners = [
// Listen if settings changed
csSettings.api.data.on.changed($rootScope, store, this),
// Listen if node changed
BMA.api.node.on.restart($rootScope, restart, this)
];
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function ready() {
if (started) return $q.when();
return startPromise || start();
}
function stop() {
console.debug('[wallet] Stopping...');
removeListeners();
resetData();
}
function restart() {
stop();
return $timeout(start, 200);
}
function start() {
console.debug('[wallet] Starting...');
var now = new Date().getTime();
startPromise = $q.all([
csSettings.ready(),
csCurrency.ready(),
BMA.ready()
])
// Restore
.then(restore)
// Load data (if a wallet restored)
.then(function(data) {
if (data && data.pubkey) {
return loadData({minData: true});
}
})
// Emit ready event
.then(function() {
addListeners();
console.debug('[wallet] Started in ' + (new Date().getTime() - now) + 'ms');
started = true;
startPromise = null;
// Emit event (used by plugins)
api.data.raise.ready(data);
})
.then(function(){
return data;
});
return startPromise;
}
// Register extension points
api.registerEvent('data', 'ready');
api.registerEvent('data', 'init');
api.registerEvent('data', 'loginCheck'); // allow to stop the login process
api.registerEvent('data', 'login'); // executed after login check (cannot stop the login process)
api.registerEvent('data', 'load');
api.registerEvent('data', 'finishLoad');
api.registerEvent('data', 'logout');
api.registerEvent('data', 'unauth');
api.registerEvent('data', 'reset');
api.registerEvent('error', 'send');
api.registerEvent('action', 'certify');
// init data
resetData(true);
return {
id: id,
data: data,
ready: ready,
start: start,
stop: stop,
// auth
login: login,
logout: logout,
isLogin: isLogin,
getKeypair: getKeypair,
isDataLoaded: isDataLoaded,
isNeverUsed: isNeverUsed,
isNew: function() {return !!data.isNew;},
isUserPubkey: isUserPubkey,
getData: getData,
loadData: loadData,
refreshData: refreshData,
downloadSaveId: downloadSaveId,
getCryptedId: getCryptedId,
recoverId: recoverId,
events: {
add: addEvent,
cleanByContext: cleanEventsByContext
},
api: api
};
}
var service = factory('default', BMA);
service.instance = factory;
return service;
}]);
angular.module('cesium.plugin.services', [])
.provider('PluginService', function PluginServiceProvider() {
'ngInject';
var eagerLoadingServices = [];
var extensionByStates = {};
this.registerEagerLoadingService = function(serviceName) {
eagerLoadingServices.push(serviceName);
return this;
};
this.extendState = function(stateName, extension) {
if (angular.isDefined(stateName) && angular.isDefined(extension)) {
if (!extensionByStates[stateName]) {
extensionByStates[stateName] = [];
}
extensionByStates[stateName].push(extension);
}
return this;
};
this.extendStates = function(stateNames, extension) {
var that = this;
stateNames.forEach(function(stateName) {
that.extendState(stateName, extension);
});
return this;
};
this.$get = ['$injector', '$state', function($injector, $state) {
var currentExtensionPointName;
function start() {
if (eagerLoadingServices.length>0) {
_.forEach(eagerLoadingServices, function(name) {
$injector.get(name);
});
}
}
function getActiveExtensionPointsByName(extensionPointName) {
var extensions = _.keys(extensionByStates).reduce(function(res, stateName){
return $state.includes(stateName) ? res.concat(extensionByStates[stateName]) : res;
}, []);
return extensions.reduce(function(res, extension){
return extension.points && extension.points[extensionPointName] ? res.concat(extension.points[extensionPointName]) : res;
}, []);
}
function setCurrentExtensionPointName(extensionPointName) {
currentExtensionPointName = extensionPointName;
}
function getCurrentExtensionPointName() {
return currentExtensionPointName;
}
return {
start: start,
extensions: {
points: {
getActivesByName: getActiveExtensionPointsByName,
current: {
get: getCurrentExtensionPointName,
set: setCurrentExtensionPointName
}
}
}
};
}];
})
;
angular.module('cesium.services', [
'cesium.config',
'cesium.settings.services',
'cesium.http.services',
'cesium.bma.services',
'cesium.crypto.services',
'cesium.utils.services',
'cesium.modal.services',
'cesium.storage.services',
'cesium.device.services',
'cesium.currency.services',
'cesium.wallet.services',
'cesium.tx.services',
'cesium.wot.services',
'cesium.plugin.services'
])
;
/**
* Created by blavenie on 01/02/17.
*/
function Block(json, attributes) {
"use strict";
var that = this;
// Copy default fields
if (!attributes || !attributes.length) {
["currency", "issuer", "medianTime", "number", "version", "powMin", "dividend", "membersCount", "hash", "identities", "joiners", "actives", "leavers", "revoked", "excluded", "certifications", "transactions"]
.forEach(function (key) {
that[key] = json[key];
});
}
// or just given
else {
_.forEach(attributes, function (key) {
that[key] = json[key];
});
}
that.identitiesCount = that.identities ? that.identities.length : 0;
that.joinersCount = that.joiners ? that.joiners.length : 0;
that.activesCount = that.actives ? that.actives.length : 0;
that.leaversCount = that.leavers ? that.leavers.length : 0;
that.revokedCount = that.revoked ? that.revoked.length : 0;
that.excludedCount = that.excluded ? that.excluded.length : 0;
that.certificationsCount = that.certifications ? that.certifications.length : 0;
that.transactionsCount = that.transactions ? that.transactions.length : 0;
that.empty = that.isEmpty();
}
Block.prototype.isEmpty = function(){
"use strict";
return !this.transactionsCount &&
!this.certificationsCount &&
!this.joinersCount &&
!this.dividend &&
!this.activesCount &&
!this.identitiesCount &&
!this.leaversCount &&
!this.excludedCount &&
!this.revokedCount;
};
Block.prototype.parseData = function() {
this.identities = this.parseArrayValues(this.identities, ['pubkey', 'signature', 'buid', 'uid']);
this.joiners = this.parseArrayValues(this.joiners, ['pubkey', 'signature', 'mBuid', 'iBuid', 'uid']);
this.actives = this.parseArrayValues(this.actives, ['pubkey', 'signature', 'mBuid', 'iBuid', 'uid']);
this.leavers = this.parseArrayValues(this.leavers, ['pubkey', 'signature', 'mBuid', 'iBuid', 'uid']);
this.revoked = this.parseArrayValues(this.revoked, ['pubkey', 'signature']);
this.excluded = this.parseArrayValues(this.excluded, ['pubkey']);
// certifications
this.certifications = this.parseArrayValues(this.certifications, ['from', 'to', 'block', 'signature']);
//this.certifications = _.groupBy(this.certifications, 'to');
// TX
this.transactions = this.parseTransactions(this.transactions);
delete this.raw; // not need
};
Block.prototype.cleanData = function() {
delete this.identities;
delete this.joiners;
delete this.actives;
delete this.leavers;
delete this.revoked;
delete this.excluded;
delete this.certifications;
delete this.transactions;
delete this.raw; // not need
};
Block.prototype.parseArrayValues = function(array, itemObjProperties){
if (!array || !array.length) return [];
return array.reduce(function(res, raw) {
var parts = raw.split(':');
if (parts.length != itemObjProperties.length) {
console.debug('[block] Bad format for \'{0}\': [{1}]. Expected {1} parts. Skipping'.format(arrayProperty, raw, itemObjProperties.length));
return res;
}
var obj = {};
for (var i=0; i<itemObjProperties.length; i++) {
obj[itemObjProperties[i]] = parts[i];
}
return res.concat(obj);
}, []);
};
function exact(regexpContent) {
return new RegExp("^" + regexpContent + "$");
}
Block.prototype.regexp = {
TX_OUTPUT_SIG: exact("SIG\\(([0-9a-zA-Z]{43,44})\\)")
};
Block.prototype.parseTransactions = function(transactions) {
if (!transactions || !transactions.length) return [];
return transactions.reduce(function (res, tx) {
var obj = {
issuers: tx.issuers,
time: tx.time
};
obj.outputs = tx.outputs.reduce(function(res, output) {
var parts = output.split(':');
if (parts.length != 3) {
console.debug('[block] Bad format a \'transactions\': [{0}]. Expected 3 parts. Skipping'.format(output));
return res;
}
var amount = parts[0];
var unitbase = parts[1];
var unlockCondition = parts[2];
var matches = Block.prototype.regexp.TX_OUTPUT_SIG.exec(parts[2]);
// Simple expression SIG(x)
if (matches) {
var pubkey = matches[1];
if (!tx.issuers || tx.issuers.indexOf(pubkey) != -1) return res;
return res.concat({
amount: unitbase <= 0 ? amount : amount * Math.pow(10, unitbase),
unitbase: unitbase,
pubkey: pubkey
});
}
// Parse complex unlock condition
else {
//console.debug('[block] [TX] Detecting unlock condition: {0}.'.format(output));
return res.concat({
amount: unitbase <= 0 ? amount : amount * Math.pow(10, unitbase),
unitbase: unitbase,
unlockCondition: unlockCondition
});
}
}, []);
// Special cas for TX to himself
if (!obj.error && !obj.outputs.length) {
obj.toHimself = true;
}
return res.concat(obj);
}, []);
};
AppController.$inject = ['$scope', '$rootScope', '$state', '$ionicSideMenuDelegate', '$q', '$timeout', '$ionicHistory', '$controller', '$window', 'csPlatform', 'UIUtils', 'BMA', 'csWallet', 'Device', 'Modals', 'csSettings', 'csConfig', 'csHttp'];
HomeController.$inject = ['$scope', '$state', '$timeout', '$ionicHistory', '$translate', 'UIUtils', 'csPlatform', 'csCurrency', 'csSettings'];
PluginExtensionPointController.$inject = ['$scope', 'PluginService'];angular.module('cesium.app.controllers', ['cesium.services'])
.config(['$httpProvider', function($httpProvider) {
'ngInject';
//Enable cross domain calls
$httpProvider.defaults.useXDomain = true;
//Remove the header used to identify ajax call that would prevent CORS from working
delete $httpProvider.defaults.headers.common['X-Requested-With'];
}])
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
'ngInject';
$stateProvider
.state('app', {
url: "/app",
abstract: true,
templateUrl: "templates/menu.html",
controller: 'AppCtrl',
data: {
large: false
}
})
.state('app.home', {
url: "/home?error",
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('AppCtrl', AppController)
.controller('HomeCtrl', HomeController)
.controller('PluginExtensionPointCtrl', PluginExtensionPointController)
;
/**
* Useful controller that could be reuse in plugin, using $scope.extensionPoint for condition rendered in templates
*/
function PluginExtensionPointController($scope, PluginService) {
'ngInject';
$scope.extensionPoint = PluginService.extensions.points.current.get();
}
/**
* Abstract controller (inherited by other controllers)
*/
function AppController($scope, $rootScope, $state, $ionicSideMenuDelegate, $q, $timeout,
$ionicHistory, $controller, $window, csPlatform,
UIUtils, BMA, csWallet, Device, Modals, csSettings, csConfig, csHttp
) {
'ngInject';
$scope.walletData = csWallet.data;
$scope.search = {};
$scope.login = csWallet.isLogin();
$scope.motion = UIUtils.motion.default;
$scope.fullscreen = UIUtils.screen.fullscreen.isEnabled();
$scope.showHome = function() {
$ionicHistory.nextViewOptions({
historyRoot: true
});
return $state.go('app.home')
.then(UIUtils.loading.hide);
};
////////////////////////////////////////
// Show Help tour
////////////////////////////////////////
$scope.createHelptipScope = function(isTour) {
if (!isTour && ($rootScope.tour || !$rootScope.settings.helptip.enable || UIUtils.screen.isSmall())) {
return; // avoid other helptip to be launched (e.g. csWallet)
}
// Create a new scope for the tour controller
var helptipScope = $scope.$new();
$controller('HelpTipCtrl', { '$scope': helptipScope});
return helptipScope;
};
$scope.startHelpTour = function(skipClearCache) {
$rootScope.tour = true; // to avoid other helptip to be launched (e.g. csWallet)
// Clear cache history
if (!skipClearCache) {
$ionicHistory.clearHistory();
return $ionicHistory.clearCache()
.then(function() {
$scope.startHelpTour(true/*continue*/);
});
}
var helptipScope = $scope.createHelptipScope(true);
return helptipScope.startHelpTour()
.then(function() {
helptipScope.$destroy();
delete $rootScope.tour;
})
.catch(function(err){
delete $rootScope.tour;
});
};
////////////////////////////////////////
// Login & wallet
////////////////////////////////////////
$scope.isLogin = function() {
return $scope.login;
};
// Load wallet data (after login)
$scope.loadWalletData = function(options) {
console.warn("[app-controller] DEPRECATED - Please use csWallet.load() instead of $scope.loadWalletData()", new Error());
options = options || {};
return csWallet.loadData(options)
.then(function(walletData) {
// cancel login
if (!walletData) throw 'CANCELLED';
return walletData;
});
};
// Login and load wallet
$scope.loadWallet = function(options) {
console.warn("[app-controller] DEPRECATED - Please use csWallet.loadData() instead of $scope.loadWallet()", new Error());
// Make sure the platform is ready
if (!csPlatform.isStarted()) {
return csPlatform.ready().then(function(){
return $scope.loadWallet(options);
});
}
options = options || {};
// If need login
if (!csWallet.isLogin()) {
return $scope.showLoginModal(options)
.then(function (walletData) {
if (walletData) {
// Force full load, even if min data asked
// Because user can wait when just filled login (by modal)
if (options && options.minData) options.minData = false;
return csWallet.loadData(options);
}
})
.then(function (walletData) {
if (walletData) return walletData;
// failed to login
throw 'CANCELLED';
});
}
else if (!csWallet.data.loaded) {
return csWallet.loadData(options);
}
else {
return $q.when(csWallet.data);
}
};
// Login and go to a state (or wallet if not)
$scope.loginAndGo = function(state, options) {
$scope.closeProfilePopover();
state = state || 'app.view_wallet';
if (!csWallet.isLogin()) {
// Make sure to protect login modal, if HTTPS enable - fix #340
if (csConfig.httpsMode && $window.location && $window.location.protocol !== 'https:') {
var href = $window.location.href;
var hashIndex = href.indexOf('#');
var rootPath = (hashIndex !== -1) ? href.substr(0, hashIndex) : href;
rootPath = 'https' + rootPath.substr(4);
href = rootPath + $state.href(state);
if (csConfig.httpsModeDebug) {
// Debug mode: just log, then continue
console.debug('[httpsMode] --- Should redirect to: ' + href);
}
else {
$window.location.href = href;
return;
}
}
return $scope.showLoginModal()
.then(function(walletData){
if (walletData) {
return $state.go(state, options)
.then(UIUtils.loading.hide);
}
});
}
else {
return $state.go(state, options);
}
};
// Show login modal
$scope.showLoginModal = function(options) {
options = options || {};
options.templateUrl = options.templateUrl ||
(csConfig.login && csConfig.login.templateUrl);
options.controller = options.controller ||
(csConfig.login && csConfig.login.controller);
return Modals.showLogin(options)
.then(function(formData){
if (!formData) return;
var rememberMeChanged = (csSettings.data.rememberMe !== formData.rememberMe);
if (rememberMeChanged) {
csSettings.data.rememberMe = formData.rememberMe;
csSettings.data.useLocalStorage = csSettings.data.rememberMe ? true : csSettings.data.useLocalStorage;
csSettings.store();
}
return csWallet.login(formData.username, formData.password);
})
.then(function(walletData){
if (walletData) {
$rootScope.walletData = walletData;
}
return walletData;
})
.catch(function(err) {
if (err === "RETRY") {
UIUtils.loading.hide();
return $scope.showLoginModal(options); // loop
}
else {
UIUtils.onError('ERROR.CRYPTO_UNKNOWN_ERROR')(err);
}
});
};
// Logout
$scope.logout = function(options) {
options = options || {};
if (!options.force && $scope.profilePopover) {
// Make the popover if really closed, to avoid UI refresh on popover buttons
return $scope.profilePopover.hide()
.then(function(){
options.force = true;
return $scope.logout(options);
});
}
if (options.askConfirm) {
return UIUtils.alert.confirm('CONFIRM.LOGOUT')
.then(function(confirm) {
if (confirm) {
options.askConfirm=false;
return $scope.logout(options);
}
});
}
UIUtils.loading.show();
return csWallet.logout()
.then(function() {
// Close left menu if open
if ($ionicSideMenuDelegate.isOpenLeft()) {
$ionicSideMenuDelegate.toggleLeft();
}
$ionicHistory.clearHistory();
return $ionicHistory.clearCache()
.then(function() {
return $scope.showHome();
});
})
.catch(UIUtils.onError());
};
// If connected and same pubkey
$scope.isUserPubkey = function(pubkey) {
return csWallet.isUserPubkey(pubkey);
};
// add listener on wallet event
csWallet.api.data.on.login($scope, function(data, deferred) {
$scope.login = true;
return deferred ? deferred.resolve() : $q.when();
});
csWallet.api.data.on.logout($scope, function() {
$scope.login = false;
});
////////////////////////////////////////
// Useful modals
////////////////////////////////////////
// Open transfer modal
$scope.showTransferModal = function(parameters) {
// NOT NEED
};
$scope.showAboutModal = function() {
return Modals.showAbout();
};
$scope.showJoinModal = function() {
$scope.closeProfilePopover();
return Modals.showJoin();
};
$scope.showSettings = function() {
$scope.closeProfilePopover();
return $state.go('app.settings');
};
$scope.showHelpModal = function(parameters) {
return Modals.showHelp(parameters);
};
////////////////////////////////////////
// Useful popovers
////////////////////////////////////////
$scope.showProfilePopover = function(event) {
return UIUtils.popover.show(event, {
templateUrl :'templates/common/popover_profile.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.profilePopover = popover;
$timeout(function() {
UIUtils.ink({selector: '#profile-popover .ink, #profile-popover .ink-dark'});
}, 100);
}
});
};
$scope.closeProfilePopover = function() {
if ($scope.profilePopover && $scope.profilePopover.isShown()) {
$timeout(function(){$scope.profilePopover.hide();});
}
};
// Change peer info
$scope.showPeerInfoPopover = function(event) {
return UIUtils.popover.show(event, {
templateUrl: 'templates/network/popover_peer_info.html',
autoremove: true,
scope: $scope.$new(true)
});
};
////////////////////////////////////////
// Link management
////////////////////////////////////////
$scope.openLink = function($event, uri, options) {
$event.stopPropagation();
$event.preventDefault();
options = options || {};
// If unable to open, just copy value
options.onError = function() {
return UIUtils.popover.copy($event, uri);
};
csHttp.uri.open(uri, options);
return false;
};
////////////////////////////////////////
// Layout Methods
////////////////////////////////////////
$scope.showFab = function(id, timeout) {
UIUtils.motion.toggleOn({selector: '#'+id + '.button-fab'}, timeout);
};
$scope.hideFab = function(id, timeout) {
UIUtils.motion.toggleOff({selector: '#'+id + '.button-fab'}, timeout);
};
// Could be override by subclass
$scope.doMotion = function(options) {
return $scope.motion.show(options);
};
////////////////////////////////////////
// Fullscreen mode
////////////////////////////////////////
$scope.askFullscreen = function() {
var skip = $scope.fullscreen || !UIUtils.screen.isSmall() || !Device.isWeb();
if (skip) return;
return UIUtils.alert.confirm('CONFIRM.FULLSCREEN', null, {
cancelText: 'COMMON.BTN_NO',
okText: 'COMMON.BTN_YES'
})
.then(function(confirm) {
if (!confirm) return;
$scope.toggleFullscreen();
});
};
$scope.toggleFullscreen = function() {
$scope.fullscreen = !UIUtils.screen.fullscreen.isEnabled();
UIUtils.screen.fullscreen.toggleAll();
};
// removeIf(device)
// Ask switching fullscreen
$scope.askFullscreen();
// endRemoveIf(device)
}
function HomeController($scope, $state, $timeout, $ionicHistory, $translate, UIUtils, csPlatform, csCurrency, csSettings) {
'ngInject';
$scope.loading = true;
$scope.locales = angular.copy(csSettings.locales);
function getRandomImage() {
var imageCountByKind = {
'service': 12,
'spring': 7,
'summer': 11,
'autumn': 7,
'winter': 5
};
var kind;
// Or landscape
if (Math.random() < 0.5) {
kind = 'service';
}
else {
var day = moment().format('D');
var month = moment().format('M');
if ((month < 3) || (month == 3 && day < 21) || (month == 12 && day >= 21)) {
kind = 'winter';
}
else if ((month == 3 && day >= 21) || (month < 6) || (month == 6 && day < 21)) {
kind = 'spring';
}
else if ((month == 6 && day >= 21) || (month < 9) || (month == 9 && day < 21)) {
kind = 'summer';
}
else {
kind = 'autumn';
}
}
var imageCount = imageCountByKind[kind];
var imageIndex = Math.floor(Math.random()*imageCount)+1;
return './img/bg/{0}-{1}.jpg'.format(kind, imageIndex);
}
$scope.bgImage = getRandomImage();
$scope.enter = function(e, state) {
if (ionic.Platform.isIOS()) {
if(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.error) { // Error query parameter
$scope.error = state.stateParams.error;
$scope.node = csCurrency.data.node;
$scope.loading = false;
$ionicHistory.nextViewOptions({
disableAnimate: true,
disableBack: true,
historyRoot: true
});
$state.go('app.home', {error: undefined}, {
reload: false,
inherit: true,
notify: false});
}
else {
// Start platform
csPlatform.ready()
.then(function() {
$scope.loading = false;
})
.catch(function(err) {
$scope.node = csCurrency.data.node;
$scope.loading = false;
$scope.error = err;
});
}
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.reload = function() {
$scope.loading = true;
delete $scope.error;
$timeout($scope.enter, 200);
};
/**
* Catch click for quick fix
* @param event
*/
$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});
};
/* -- show/hide locales popup -- */
$scope.showLocalesPopover = function(event) {
UIUtils.popover.show(event, {
templateUrl: 'templates/api/locales_popover.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.localesPopover = popover;
}
});
};
$scope.hideLocalesPopover = function() {
if ($scope.localesPopover) {
$scope.localesPopover.hide();
$scope.localesPopover = null;
}
};
// For DEV ONLY
/*$timeout(function() {
$scope.loginAndGo();
}, 500);*/
}
JoinController.$inject = ['$timeout', 'Modals'];
JoinModalController.$inject = ['$scope', '$state', 'UIUtils', 'CryptoUtils', 'csSettings', 'Modals', 'csWallet', 'mkWallet', 'BMA'];
angular.module('cesium.join.controllers', ['cesium.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.join', {
url: "/join",
views: {
'menuContent': {
templateUrl: "templates/home/home.html",
controller: 'JoinCtrl'
}
}
})
;
}])
.controller('JoinCtrl', JoinController)
.controller('JoinModalCtrl', JoinModalController)
;
function JoinController($timeout, Modals) {
'ngInject';
// Open join modal
$timeout(function() {
Modals.showJoin();
}, 100);
}
function JoinModalController($scope, $state, UIUtils, CryptoUtils, csSettings, Modals, csWallet, mkWallet, BMA) {
'ngInject';
$scope.formData = {
pseudo: ''
};
$scope.slides = {
slider: null,
options: {
loop: false,
effect: 'slide',
speed: 500
}
};
$scope.isLastSlide = false;
$scope.search = {
looking: true
};
$scope.showUsername = false;
$scope.showPassword = false;
$scope.smallscreen = UIUtils.screen.isSmall();
$scope.userIdPattern = BMA.constants.regex.USER_ID;
$scope.slidePrev = function() {
$scope.slides.slider.unlockSwipes();
$scope.slides.slider.slidePrev();
$scope.slides.slider.lockSwipes();
$scope.isLastSlide = false;
};
$scope.slideNext = function() {
$scope.slides.slider.unlockSwipes();
$scope.slides.slider.slideNext();
$scope.slides.slider.lockSwipes();
$scope.isLastSlide = $scope.slides.slider.activeIndex === 4;
};
$scope.showAccountPubkey = function() {
$scope.formData.computing=true;
CryptoUtils.scryptKeypair($scope.formData.username, $scope.formData.password)
.then(function(keypair) {
$scope.formData.pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
$scope.formData.computing=false;
})
.catch(function(err) {
$scope.formData.computing=false;
console.error('>>>>>>>' , err);
UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR');
});
};
$scope.formDataChanged = function() {
$scope.formData.computing=false;
$scope.formData.pubkey=null;
};
$scope.doNext = function(formName) {
console.debug("[join] form " + formName + " OK. index=" + $scope.slides.slider.activeIndex);
if (!formName) {
formName = ($scope.slides.slider.activeIndex === 1 ? 'passwordForm' : ($scope.slides.slider.activeIndex === 2 ? 'pseudoForm' : formName));
}
if (formName) {
$scope[formName].$submitted=true;
if(!$scope[formName].$valid) {
return;
}
if (formName === 'passwordForm') {
$scope.slideNext();
$scope.showAccountPubkey();
}
else {
$scope.slideNext();
if (formName === 'pseudoForm') {
$scope.showAccountPubkey();
}
}
}
};
$scope.doNewAccount = function(confirm) {
if (!confirm) {
return UIUtils.alert.confirm('ACCOUNT.NEW.CONFIRMATION_WALLET_ACCOUNT')
.then(function(confirm) {
if (confirm) {
$scope.doNewAccount(true);
}
});
}
UIUtils.loading.show();
csWallet.login($scope.formData.username, $scope.formData.password)
.then(function(data) {
$scope.closeModal();
csSettings.data.wallet = csSettings.data.wallet || {};
csSettings.data.wallet.alertIfUnusedWallet = false; // do not alert if empty
// Fill a default profile
mkWallet.setDefaultProfile({
title: $scope.formData.pseudo
});
// Redirect to wallet
$state.go('app.view_wallet');
})
.catch(function(err) {
UIUtils.loading.hide();
console.error('>>>>>>>' , err);
UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR');
});
};
$scope.showHelpModal = function(helpAnchor) {
if (!helpAnchor) {
helpAnchor = $scope.slides.slider.activeIndex == 1 ?
'join-salt' : ( $scope.slides.slider.activeIndex == 2 ?
'join-password' : 'join-pseudo');
}
Modals.showHelp({anchor: helpAnchor});
};
// TODO: remove auto add account when done
/*$timeout(function() {
$scope.formData.username="azertypoi";
$scope.formData.confirmUsername=$scope.formData.username;
$scope.formData.password="azertypoi";
$scope.formData.confirmPassword=$scope.formData.password;
$scope.formData.pseudo="azertypoi";
$scope.doNext();
$scope.doNext();
//$scope.form = {$valid:true};
}, 2000);*/
}
LoginModalController.$inject = ['$scope', '$timeout', 'CryptoUtils', 'UIUtils', 'Modals', 'csSettings', 'Device'];
angular.module('cesium.login.controllers', ['cesium.services'])
.controller('LoginModalCtrl', LoginModalController)
;
function LoginModalController($scope, $timeout, CryptoUtils, UIUtils, Modals, csSettings, Device) {
'ngInject';
$scope.computing = false;
$scope.pubkey = null;
$scope.formData = {
rememberMe: csSettings.data.rememberMe
};
$scope.showSalt = csSettings.data.showLoginSalt;
$scope.showPubkeyButton = false;
$scope.autoComputePubkey = false;
Device.ready().then(function() {
$scope.autoComputePubkey = ionic.Platform.grade.toLowerCase()==='a' &&
!UIUtils.screen.isSmall();
});
// Login form submit
$scope.doLogin = function() {
if(!$scope.form.$valid) {
return;
}
UIUtils.loading.show();
$scope.closeModal($scope.formData);
};
$scope.formDataChanged = function() {
$scope.computing=false;
$scope.pubkey = null;
if ($scope.autoComputePubkey && $scope.formData.username && $scope.formData.password) {
$scope.showPubkey();
}
else {
$scope.showPubkeyButton = $scope.formData.username && $scope.formData.password;
}
};
$scope.$watch('formData.username', $scope.formDataChanged, true);
$scope.$watch('formData.password', $scope.formDataChanged, true);
$scope.showPubkey = function() {
$scope.computing=true;
$scope.showPubkeyButton = false;
$scope.pubkey = '';
$timeout(function() {
var salt = $scope.formData.username;
var pwd = $scope.formData.password;
CryptoUtils.scryptKeypair(salt, pwd).then(
function (keypair) {
// form has changed: retry
if (salt !== $scope.formData.username || pwd !== $scope.formData.password) {
$scope.showPubkey();
}
else {
$scope.pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
$scope.computing = false;
}
}
)
.catch(function (err) {
$scope.pubkey = '';
$scope.computing = false;
UIUtils.loading.hide();
console.error('>>>>>>>', err);
UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR');
});
}, 500);
};
$scope.showJoinModal = function() {
$scope.closeModal();
$timeout(function() {
Modals.showJoin();
}, 300);
};
$scope.showAccountSecurityModal = function() {
$scope.closeModal();
$timeout(function() {
Modals.showAccountSecurity();
}, 300);
};
/*
// TODO : for DEV only
$timeout(function() {
$scope.formData = {
username: 'abc',
password: 'def',
};
//$scope.form = {$valid:true};
}, 900);*/
}
HelpController.$inject = ['$scope', '$state', '$timeout', '$anchorScroll', 'csSettings'];
HelpModalController.$inject = ['$scope', '$timeout', '$anchorScroll', 'csSettings', 'parameters'];
HelpTipController.$inject = ['$scope', '$rootScope', '$state', '$window', '$ionicSideMenuDelegate', '$timeout', '$q', '$anchorScroll', 'UIUtils', 'csConfig', 'csSettings', 'csCurrency', 'Device', 'csWallet'];
HelpTourController.$inject = ['$scope'];
angular.module('cesium.help.controllers', ['cesium.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.help_tour', {
url: "/tour",
views: {
'menuContent': {
templateUrl: "templates/home/home.html",
controller: 'HelpTourCtrl'
}
}
})
.state('app.help', {
url: "/help?anchor",
views: {
'menuContent': {
templateUrl: "templates/help/view_help.html",
controller: 'HelpCtrl'
}
}
})
.state('app.help_anchor', {
url: "/help/:anchor",
views: {
'menuContent': {
templateUrl: "templates/help/view_help.html",
controller: 'HelpCtrl'
}
}
})
;
}])
.controller('HelpCtrl', HelpController)
.controller('HelpModalCtrl', HelpModalController)
.controller('HelpTipCtrl', HelpTipController)
.controller('HelpTourCtrl', HelpTourController)
;
function HelpController($scope, $state, $timeout, $anchorScroll, csSettings) {
'ngInject';
$scope.$on('$ionicView.enter', function(e) {
$scope.locale = csSettings.data.locale.id;
if ($state.stateParams && $state.stateParams.anchor) {
$timeout(function () {
$anchorScroll($state.stateParams.anchor);
}, 100);
}
});
}
function HelpModalController($scope, $timeout, $anchorScroll, csSettings, parameters) {
'ngInject';
$scope.locale = csSettings.data.locale.id;
if (parameters && parameters.anchor) {
$timeout(function() {
$anchorScroll(parameters.anchor);
}, 100);
}
}
/* ----------------------------
* Help Tip
* ---------------------------- */
function HelpTipController($scope, $rootScope, $state, $window, $ionicSideMenuDelegate, $timeout, $q, $anchorScroll,
UIUtils, csConfig, csSettings, csCurrency, Device, csWallet) {
$scope.tour = false; // Is a tour or a helptip ?
$scope.continue = true;
$scope.executeStep = function(partName, steps, index) {
index = angular.isDefined(index) ? index : 0;
if (index >= steps.length) {
return $q.when(true); // end
}
var step = steps[index];
if (typeof step !== 'function') {
throw new Error('[helptip] Invalid step at index {0} of \'{1}\' tour: step must be a function'.format(index, partName));
}
var promise = step();
if (typeof promise === 'boolean') {
promise = $q.when(promise);
}
return promise
.then(function(next) {
if (angular.isUndefined(next)) {
$scope.continue = false;
return index; // keep same index (no button press: popover just closed)
}
if (!next || index === steps.length - 1) {
return next ? -1 : index+1; // last step OK, so mark has finished
}
return $scope.executeStep(partName, steps, index+1);
})
.catch(function(err) {
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 {
console.error('ERROR: in help tour [{0}], in step [{1}] : {2}'.format(partName, index, err));
}
$scope.continue = false;
return index;
});
};
$scope.showHelpTip = function(id, options) {
options = options || {};
options.bindings = options.bindings || {};
options.bindings.value =options.bindings.value || '';
options.bindings.hasNext = angular.isDefined(options.bindings.hasNext) ? options.bindings.hasNext : true;
options.timeout = options.timeout || (Device.enable ? 900 : 500);
options.autoremove = true; // avoid memory leak
options.bindings.tour = $scope.tour;
options.backdropClickToClose = !$scope.tour;
return UIUtils.popover.helptip(id, options);
};
$scope.showHelpModal = function(helpAnchor) {
Modals.showHelp({anchor: helpAnchor});
};
$scope.startHelpTour = function() {
$scope.tour = true;
$scope.continue = true;
// Currency tour
return $scope.startCurrencyTour(0, true)
.then(function(endIndex){
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.currency=endIndex;
csSettings.store();
return $scope.continue;
})
// 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;
});
})
// Wot tour
.then(function(next){
if (!next) return false;
return $scope.startWotTour(0, true)
.then(function(endIndex){
if (!endIndex || $scope.cancelled) return false;
csSettings.data.helptip.wot=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Identity certifications tour
.then(function(next){
if (!next) return false;
return $scope.startWotCertTour(0, true)
.then(function(endIndex){
if (!endIndex) return false;
csSettings.data.helptip.wotCerts=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Wallet tour (if NOT login)
.then(function(next){
if (!next) return false;
return $scope.startWalletNoLoginTour(0, true);
})
// Wallet tour (if login)
.then(function(next){
if (!next) return false;
if (!csWallet.isLogin()) return true; // not login: continue
return $scope.startWalletTour(0, true)
.then(function(endIndex){
if (!endIndex) return false;
csSettings.data.helptip.wallet=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Wallet certifications tour
.then(function(next){
if (!next) return false;
if (!csWallet.isLogin()) return true; // not login: continue
return $scope.startWalletCertTour(0, true)
.then(function(endIndex){
if (!endIndex) return false;
csSettings.data.helptip.walletCerts=endIndex;
csSettings.store();
return $scope.continue;
});
})
// TX tour (if login)
.then(function(next){
if (!next) return false;
if (!csWallet.isLogin()) return true; // not login: continue
return $scope.startTxTour(0, true)
.then(function(endIndex){
if (!endIndex) return false;
csSettings.data.helptip.tx=endIndex;
csSettings.store();
return $scope.continue;
});
})
// Header tour
.then(function(next){
if (!next) return false;
return $scope.startHeaderTour(0, true);
})
// Settings tour
.then(function(next){
if (!next) return false;
return $scope.startSettingsTour(0, true);
})
// Finish tour
.then(function(next){
if (!next) return false;
return $scope.finishTour();
});
};
/**
* Features tour on currency
* @returns {*}
*/
$scope.startCurrencyTour = function(startIndex, hasNext) {
var showWotTabIfNeed = function() {
if ($state.is('app.currency.tab_parameters')) {
$state.go('app.currency.tab_wot');
}
};
var contentParams;
var steps = [
function(){
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-currency', {
bindings: {
content: 'HELP.TIP.MENU_BTN_CURRENCY',
icon: {
position: 'left'
}
}
});
},
function () {
if ($ionicSideMenuDelegate.isOpen()) {
$ionicSideMenuDelegate.toggleLeft(false);
}
return $state.go(UIUtils.screen.isSmall() ? 'app.currency' : 'app.currency_view_lg')
.then(function () {
return $scope.showHelpTip('helptip-currency-mass-member', {
bindings: {
content: 'HELP.TIP.CURRENCY_MASS',
icon: {
position: 'center'
}
}
});
});
},
function () {
if (!csSettings.data.useRelative) return true; //skip but continue
return $scope.showHelpTip('helptip-currency-mass-member-unit', {
bindings: {
content: 'HELP.TIP.CURRENCY_UNIT_RELATIVE',
contentParams: contentParams,
icon: {
position: UIUtils.screen.isSmall() ? 'right' : 'center'
}
}
});
},
function () {
if (!csSettings.data.useRelative) return true; //skip but continue
return $scope.showHelpTip('helptip-currency-change-unit', {
bindings: {
content: 'HELP.TIP.CURRENCY_CHANGE_UNIT',
contentParams: contentParams,
icon: {
position: UIUtils.screen.isSmall() ? 'right' : 'center'
}
}
});
},
function () {
if (csSettings.data.useRelative) return true; //skip but continue
return $scope.showHelpTip('helptip-currency-change-unit', {
bindings: {
content: 'HELP.TIP.CURRENCY_CHANGE_UNIT_TO_RELATIVE',
contentParams: contentParams,
icon: {
position: UIUtils.screen.isSmall() ? 'right' : 'center'
}
}
});
},
function () {
if (UIUtils.screen.isSmall()) {
$anchorScroll('helptip-currency-rules-anchor');
}
return $scope.showHelpTip('helptip-currency-rules', {
bindings: {
content: 'HELP.TIP.CURRENCY_RULES',
icon: {
position: 'center',
glyph: 'ion-information-circled'
}
}
});
},
function () {
showWotTabIfNeed();
return $scope.showHelpTip('helptip-currency-newcomers', {
bindings: {
content: 'HELP.TIP.CURRENCY_WOT',
icon: {
position: 'center'
}
},
timeout: 1200 // need for Firefox
});
}
];
// Get currency parameters, with currentUD
return csCurrency.default().then(function(currency) {
contentParams = currency.parameters;
// Launch steps
return $scope.executeStep('currency', steps, startIndex);
});
};
/**
* Features tour on network
* @returns {*}
*/
$scope.startNetworkTour = function(startIndex, hasNext) {
var showNetworkTabIfNeed = function() {
if ($state.is('app.currency')) {
// Select the second tabs
$timeout(function () {
var tabs = $window.document.querySelectorAll('ion-tabs .tabs a');
if (tabs && tabs.length == 3) {
angular.element(tabs[2]).triggerHandler('click');
}
}, 100);
}
};
var contentParams;
var steps = [
function(){
if (UIUtils.screen.isSmall()) return true; // skip but continue
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-network', {
bindings: {
content: 'HELP.TIP.MENU_BTN_NETWORK',
icon: {
position: 'left'
}
}
});
},
function () {
if ($ionicSideMenuDelegate.isOpen()) {
$ionicSideMenuDelegate.toggleLeft(false);
}
return $state.go(UIUtils.screen.isSmall() ? 'app.currency.tab_network' : 'app.network')
.then(function () {
showNetworkTabIfNeed();
return $scope.showHelpTip('helptip-network-peers', {
bindings: {
content: 'HELP.TIP.NETWORK_BLOCKCHAIN',
icon: {
position: 'center',
glyph: 'ion-information-circled'
}
},
timeout: 1200 // need for Firefox
});
});
},
function() {
showNetworkTabIfNeed();
return $scope.showHelpTip('helptip-network-peer-0', {
bindings: {
content: 'HELP.TIP.NETWORK_PEERS',
icon: {
position: UIUtils.screen.isSmall() ? undefined : 'center'
}
},
timeout: 1000,
retry: 20
});
},
function() {
showNetworkTabIfNeed();
return $scope.showHelpTip('helptip-network-peer-0-block', {
bindings: {
content: 'HELP.TIP.NETWORK_PEERS_BLOCK_NUMBER',
icon: {
position: UIUtils.screen.isSmall() ? undefined : 'center'
}
}
});
},
function() {
showNetworkTabIfNeed();
var locale = csSettings.data.locale.id;
return $scope.showHelpTip('helptip-network-peers', {
bindings: {
content: 'HELP.TIP.NETWORK_PEERS_PARTICIPATE',
contentParams: {
installDocUrl: (csConfig.helptip && csConfig.helptip.installDocUrl) ?
(csConfig.helptip.installDocUrl[locale] ? csConfig.helptip.installDocUrl[locale] : csConfig.helptip.installDocUrl) :
'http://duniter.org'
},
icon: {
position: 'center',
glyph: 'ion-information-circled'
},
hasNext: hasNext
}
});
}
];
// Get currency parameters, with currentUD
return csCurrency.default().then(function(currency) {
contentParams = currency.parameters;
// Launch steps
return $scope.executeStep('network', steps, startIndex);
});
};
/**
* Features tour on WOT registry
* @returns {*}
*/
$scope.startWotTour = function(startIndex, hasNext) {
var contentParams;
var steps = [
function() {
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-wot', {
bindings: {
content: 'HELP.TIP.MENU_BTN_WOT',
icon: {
position: 'left'
}
},
onError: 'continue'
});
},
function() {
if ($ionicSideMenuDelegate.isOpen()) {
$ionicSideMenuDelegate.toggleLeft(false);
}
return $state.go('app.wot_lookup')
.then(function(){
return $scope.showHelpTip('helptip-wot-search-text', {
bindings: {
content: UIUtils.screen.isSmall() ? 'HELP.TIP.WOT_SEARCH_TEXT_XS' : 'HELP.TIP.WOT_SEARCH_TEXT',
icon: {
position: 'center'
}
}
});
});
},
function() {
return $scope.showHelpTip('helptip-wot-search-result-0', {
bindings: {
content: 'HELP.TIP.WOT_SEARCH_RESULT',
icon: {
position: 'center'
}
},
timeout: 700,
retry: 15
});
},
function() {
var element = $window.document.getElementById('helptip-wot-search-result-0');
if (!element) return true;
$timeout(function() {
angular.element(element).triggerHandler('click');
});
return $scope.showHelpTip('helptip-wot-view-certifications', {
bindings: {
content: 'HELP.TIP.WOT_VIEW_CERTIFICATIONS'
},
timeout: 2500
});
},
function() {
return $scope.showHelpTip('helptip-wot-view-certifications', {
bindings: {
content: 'HELP.TIP.WOT_VIEW_CERTIFICATIONS_COUNT',
contentParams: contentParams,
icon: {
position: 'center',
glyph: 'ion-information-circled'
}
}
});
},
function() {
return $scope.showHelpTip('helptip-wot-view-certifications-count', {
bindings: {
content: 'HELP.TIP.WOT_VIEW_CERTIFICATIONS_CLICK',
icon: {
position: 'center'
},
hasNext: hasNext
}
});
}
];
// Get currency parameters, with currentUD
return csCurrency.default().then(function(currency) {
contentParams = currency.parameters;
contentParams.currentUD = $rootScope.walletData.currentUD;
// Launch steps
return $scope.executeStep('wot', steps, startIndex);
});
};
/**
* Features tour on wot certifications
* @returns {*}
*/
$scope.startWotCertTour = function(startIndex, hasNext) {
var steps = [
function() {
// If on identity: click on certifications
if ($state.is('app.wot_identity')) {
var element = $window.document.getElementById('helptip-wot-view-certifications');
if (!element) return true;
$timeout(function() {
angular.element(element).triggerHandler('click');
});
}
return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'fab-certify': 'helptip-certs-certify', {
bindings: {
content: 'HELP.TIP.WOT_VIEW_CERTIFY',
icon: {
position: UIUtils.screen.isSmall() ? 'bottom-right' : 'center'
}
},
timeout: UIUtils.screen.isSmall() ? 2000 : 1000,
retry: 10
});
},
function() {
return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'fab-certify': 'helptip-certs-certify', {
bindings: {
content: 'HELP.TIP.CERTIFY_RULES',
icon: {
position: 'center',
glyph: 'ion-alert-circled'
},
hasNext: hasNext
}
});
}
];
return $scope.executeStep('certs', steps, startIndex);
};
/**
* Features tour on wallet (if not login)
* @returns {*}
*/
$scope.startWalletNoLoginTour = function(startIndex, hasNext) {
if (csWallet.isLogin()) return $q.when(true); // skip if login
var steps = [
function () {
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-account', {
bindings: {
content: $rootScope.walletData.isMember ? 'HELP.TIP.MENU_BTN_ACCOUNT_MEMBER' : 'HELP.TIP.MENU_BTN_ACCOUNT',
icon: {
position: 'left'
},
hasNext: hasNext
}
});
}
];
return $scope.executeStep('wallet-no-login', steps, startIndex);
};
/**
* Features tour on wallet screens
* @returns {*}
*/
$scope.startWalletTour = function(startIndex, hasNext) {
if (!csWallet.isLogin()) return $q.when(true); // skip if not login
var contentParams;
var steps = [
function () {
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-account', {
bindings: {
content: $rootScope.walletData.isMember ? 'HELP.TIP.MENU_BTN_ACCOUNT_MEMBER' : 'HELP.TIP.MENU_BTN_ACCOUNT',
icon: {
position: 'left'
}
}
});
},
function () {
if ($ionicSideMenuDelegate.isOpen()) {
$ionicSideMenuDelegate.toggleLeft(false);
}
// Go to wallet
return $state.go('app.view_wallet')
.then(function () {
return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'helptip-wallet-options-xs' : 'helptip-wallet-options', {
bindings: {
content: 'HELP.TIP.WALLET_OPTIONS',
icon: {
position: UIUtils.screen.isSmall() ? 'right' : 'center'
}
}
});
});
},
// Wallet pubkey
function () {
$anchorScroll('helptip-wallet-pubkey');
return $scope.showHelpTip('helptip-wallet-pubkey', {
bindings: {
content: 'HELP.TIP.WALLET_PUBKEY',
icon: {
position: 'bottom-center'
}
},
timeout: UIUtils.screen.isSmall() ? 2000 : 500,
retry: 10
});
},
function () {
$anchorScroll('helptip-wallet-certifications');
return $scope.showHelpTip('helptip-wallet-certifications', {
bindings: {
content: UIUtils.screen.isSmall() ? 'HELP.TIP.WALLET_RECEIVED_CERTIFICATIONS': 'HELP.TIP.WALLET_CERTIFICATIONS',
icon: {
position: 'center'
}
},
timeout: 500,
onError: 'continue',
hasNext: hasNext
});
}
];
// Get currency parameters, with currentUD
return csCurrency.default()
.then(function(currency) {
contentParams = currency.parameters;
contentParams.currentUD = $rootScope.walletData.currentUD;
// Launch steps
return $scope.executeStep('wallet', steps, startIndex);
});
};
/**
* Features tour on wallet certifications
* @returns {*}
*/
$scope.startWalletCertTour = function(startIndex, hasNext) {
if (!csWallet.isLogin()) return $q.when(true);
var contentParams;
var skipAll = false;
var steps = [
function() {
// If on wallet : click on certifications
if ($state.is('app.view_wallet')) {
var element = $window.document.getElementById('helptip-wallet-certifications');
if (!element) {
skipAll = true;
return true;
}
$timeout(function() {
angular.element(element).triggerHandler('click');
});
}
if (!UIUtils.screen.isSmall()) return true; // skip this helptip if not in tabs mode
return $scope.showHelpTip('helptip-received-certs', {
bindings: {
content: 'HELP.TIP.WALLET_RECEIVED_CERTS'
}
});
},
function() {
if (skipAll || !UIUtils.screen.isSmall()) return true;
return $state.go('app.view_wallet') // go back to wallet (small device only)
.then(function() {
return $scope.showHelpTip('helptip-wallet-given-certifications', {
bindings: {
content: 'HELP.TIP.WALLET_GIVEN_CERTIFICATIONS',
icon: {
position: 'center'
}
},
timeout: 500
});
});
},
function() {
if (skipAll) return true;
// Click on given cert link (small device only)
if ($state.is('app.view_wallet')) {
var element = $window.document.getElementById('helptip-wallet-given-certifications');
if (!element) {
skipAll = true;
return true;
}
$timeout(function() {
angular.element(element).triggerHandler('click');
}, 500);
}
return $scope.showHelpTip(UIUtils.screen.isSmall() ? 'fab-select-certify': 'helptip-certs-select-certify', {
bindings: {
content: 'HELP.TIP.WALLET_CERTIFY',
icon: {
position: UIUtils.screen.isSmall() ? 'bottom-right' : 'center'
}
},
timeout: UIUtils.screen.isSmall() ? 2000 : 500,
retry: 10
});
},
function() {
if ($scope.tour || skipAll) return hasNext; // skip Rules if features tour (already display)
return $scope.showHelpTip('helptip-certs-stock', {
bindings: {
content: 'HELP.TIP.CERTIFY_RULES',
icon: {
position: 'center',
glyph: 'ion-alert-circled'
},
hasNext: hasNext
}
});
}
/* FIXME : how to select the left tab ?
,function() {
return $scope.showHelpTip('helptip-certs-stock', {
bindings: {
content: 'HELP.TIP.WALLET_CERT_STOCK',
contentParams: contentParams,
icon: {
position: 'center'
},
hasNext: hasNext
}
});
}*/
];
return csCurrency.default().then(function(currency) {
contentParams = currency.parameters;
return $scope.executeStep('certs', steps, startIndex);
});
};
/**
* Features tour on TX screen
* @returns {*}
*/
$scope.startTxTour = function(startIndex, hasNext) {
if (!csWallet.isLogin()) return $q.when(true); // skip if not login
var contentParams;
var steps = [
function () {
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-tx', {
bindings: {
content: $rootScope.walletData.isMember ? 'HELP.TIP.MENU_BTN_TX_MEMBER' : 'HELP.TIP.MENU_BTN_TX',
icon: {
position: 'left'
}
}
});
},
function () {
if ($ionicSideMenuDelegate.isOpen()) {
$ionicSideMenuDelegate.toggleLeft(false);
}
// Go to wallet
return $state.go('app.view_wallet_tx')
.then(function () {
return $scope.showHelpTip('helptip-wallet-balance', {
bindings: {
content: csSettings.data.useRelative ? 'HELP.TIP.WALLET_BALANCE_RELATIVE' : 'HELP.TIP.WALLET_BALANCE',
contentParams: contentParams,
icon: {
position: 'center'
}
},
retry: 20 // 10 * 500 = 5s max
});
});
},
function () {
return $scope.showHelpTip('helptip-wallet-balance', {
bindings: {
content: 'HELP.TIP.WALLET_BALANCE_CHANGE_UNIT',
contentParams: contentParams,
icon: {
position: 'center',
glyph: 'ion-information-circled'
}
}
});
}
];
// Get currency parameters, with currentUD
return csCurrency.default()
.then(function(currency) {
contentParams = currency.parameters;
contentParams.currentUD = $rootScope.walletData.currentUD;
// Launch steps
return $scope.executeStep('tx', steps, startIndex);
});
};
/**
* header tour
* @returns {*}
*/
$scope.startHeaderTour = function(startIndex, hasNext) {
if (UIUtils.screen.isSmall()) return $q.when(true);
function _getProfilBtnElement() {
var elements = $window.document.querySelectorAll('#helptip-header-bar-btn-profile');
if (!elements || !elements.length) return null;
return _.find(elements, function(el) {return el.offsetWidth > 0;});
}
var steps = [
function () {
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'
}
}
});
},
function () {
// small screens
if (UIUtils.screen.isSmall()) {
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-settings', {
bindings: {
content: 'HELP.TIP.MENU_BTN_SETTINGS',
icon: {
position: 'left'
},
hasNext: hasNext
},
timeout: 1000
});
}
// wide screens
else {
var element = _getProfilBtnElement();
if (!element) return true;
$timeout(function() {
angular.element(element).triggerHandler('click');
});
return $scope.showHelpTip('helptip-popover-profile-btn-settings', {
bindings: {
content: 'HELP.TIP.MENU_BTN_SETTINGS',
icon: {
position: 'center'
},
hasNext: hasNext
},
timeout: 1000
})
.then(function(res) {
// close profile popover
$scope.closeProfilePopover();
return res;
});
}
}
];
return $scope.executeStep('header', steps, startIndex);
};
/**
* Settings tour
* @returns {*}
*/
$scope.startSettingsTour = function(startIndex, hasNext) {
var contentParams;
var steps = [
function () {
if (!UIUtils.screen.isSmall()) return true;
$ionicSideMenuDelegate.toggleLeft(true);
return $scope.showHelpTip('helptip-menu-btn-settings', {
bindings: {
content: 'HELP.TIP.MENU_BTN_SETTINGS',
icon: {
position: 'left'
}
},
timeout: 1000
});
},
function () {
if ($ionicSideMenuDelegate.isOpen()) {
$ionicSideMenuDelegate.toggleLeft(false);
}
// Go to settings
return $state.go('app.settings')
.then(function () {
return $scope.showHelpTip('helptip-settings-btn-unit-relative', {
bindings: {
content: 'HELP.TIP.SETTINGS_CHANGE_UNIT',
contentParams: contentParams,
icon: {
position: 'right',
style: 'margin-right: 60px'
},
hasNext: hasNext
},
timeout: 1000
});
});
}
];
return csCurrency.default()
.then(function(currency) {
contentParams = currency.parameters;
return $scope.executeStep('settings', steps, startIndex);
});
};
/**
* Finish the features tour (last step)
* @returns {*}
*/
$scope.finishTour = function() {
if ($ionicSideMenuDelegate.isOpen()) {
$ionicSideMenuDelegate.toggleLeft(false);
}
// If login: redirect to wallet
if (csWallet.isLogin()) {
return $state.go('app.view_wallet')
.then(function(){
return $scope.showHelpTip('helptip-wallet-certifications', {
bindings: {
content: 'HELP.TIP.END_LOGIN',
hasNext: false
}
});
});
}
// If not login: redirect to home
else {
var contentParams;
return $q.all([
$scope.showHome(),
csCurrency.default()
.then(function(parameters) {
contentParams = parameters;
})
])
.then(function(){
return $scope.showHelpTip('helptip-home-logo', {
bindings: {
content: 'HELP.TIP.END_NOT_LOGIN',
contentParams: contentParams,
hasNext: false
}
});
});
}
};
}
/* ----------------------------
* Help tour (auto start from home page)
* ---------------------------- */
function HelpTourController($scope) {
$scope.$on('$ionicView.enter', function(e, state) {
$scope.startHelpTour();
});
}
WalletController.$inject = ['$scope', '$q', '$ionicPopup', '$timeout', '$state', 'UIUtils', 'csWallet', '$translate', '$ionicPopover', 'Modals', 'csSettings', 'esHttp'];angular.module('cesium.wallet.controllers', ['cesium.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.view_wallet', {
url: "/wallet",
views: {
'menuContent': {
templateUrl: "templates/wallet/view_wallet.html",
controller: 'WalletCtrl'
}
}
})
;
}])
.controller('WalletCtrl', WalletController)
;
function WalletController($scope, $q, $ionicPopup, $timeout, $state,
UIUtils, csWallet, $translate, $ionicPopover, Modals, csSettings,
esHttp) {
'ngInject';
$scope.loading = true;
$scope.settings = csSettings.data;
$scope.likeData = {
views: {},
likes: {},
follows: {},
abuses: {},
stars: {}
};
$scope.$on('$ionicView.enter', function(e, state) {
if ($scope.loading) { // load once
$scope.loadWallet()
.then(function(walletData) {
$scope.formData = walletData;
$scope.loading=false; // very important, to avoid TX to be display before wallet.currentUd is loaded
$scope.updateView();
//$scope.showQRCode('qrcode', $scope.formData.pubkey, 1100);
//$scope.showHelpTip();
UIUtils.loading.hide(); // loading could have be open (e.g. new account)
})
.catch(function(err){
if (err === 'CANCELLED') {
$scope.showHome();
}
});
}
else {
// update view (to refresh profile and subscriptions)
$scope.updateView();
}
$scope.$broadcast('$recordView.enter', state);
});
$scope.updateView = function() {
$scope.motion.show({selector: '#wallet .item'});
$scope.$broadcast('$$rebind::' + 'rebind'); // force rebind
};
// Listen new events (can appears from security wizard also)
$scope.$watchCollection('formData.events', function(newEvents, oldEvents) {
if (!oldEvents || $scope.loading || angular.equals(newEvents, oldEvents)) return;
$scope.updateView();
});
$scope.setRegisterForm = function(registerForm) {
$scope.registerForm = registerForm;
};
// Clean controller data when logout
$scope.onWalletLogout = function() {
delete $scope.qrcode; // clean QRcode
delete $scope.formData;
$scope.loading = true;
};
csWallet.api.data.on.logout($scope, $scope.onWalletLogout);
// Ask uid
$scope.showUidPopup = function() {
return $q(function(resolve, reject) {
$translate(['ACCOUNT.NEW.TITLE', 'ACCOUNT.POPUP_REGISTER.TITLE', 'ACCOUNT.POPUP_REGISTER.HELP', 'COMMON.BTN_OK', 'COMMON.BTN_CANCEL'])
.then(function (translations) {
$scope.formData.newUid = (!!$scope.formData.uid ? ''+$scope.formData.uid : '');
// Choose UID popup
$ionicPopup.show({
templateUrl: 'templates/wallet/popup_register.html',
title: translations['ACCOUNT.POPUP_REGISTER.TITLE'],
subTitle: translations['ACCOUNT.POPUP_REGISTER.HELP'],
scope: $scope,
buttons: [
{ text: translations['COMMON.BTN_CANCEL'] },
{
text: translations['COMMON.BTN_OK'],
type: 'button-positive',
onTap: function(e) {
$scope.registerForm.$submitted=true;
if(!$scope.registerForm.$valid || !$scope.formData.newUid) {
//don't allow the user to close unless he enters a uid
e.preventDefault();
} else {
return $scope.formData.newUid;
}
}
}
]
})
.then(function(uid) {
if (!uid) { // user cancel
delete $scope.formData.uid;
UIUtils.loading.hide();
return;
}
resolve(uid);
});
});
});
};
// Send self identity
$scope.self = function() {
$scope.hideActionsPopover();
return $scope.showUidPopup()
.then(function(uid) {
UIUtils.loading.show();
return csWallet.self(uid)
.then(function() {
$scope.updateView();
UIUtils.loading.hide();
})
.catch(function(err){
UIUtils.onError('ERROR.SEND_IDENTITY_FAILED')(err)
.then(function() {
$scope.self(); // loop
});
});
});
};
$scope.doMembershipIn = function(retryCount) {
return csWallet.membership.inside()
.then(function() {
$scope.updateView();
UIUtils.loading.hide();
})
.catch(function(err) {
if (!retryCount || retryCount <= 2) {
$timeout(function() {
$scope.doMembershipIn(retryCount ? retryCount+1 : 1);
}, 1000);
}
else {
UIUtils.onError('ERROR.SEND_MEMBERSHIP_IN_FAILED')(err)
.then(function() {
$scope.membershipIn(); // loop
});
}
});
};
// Send membership IN
$scope.membershipIn = function() {
$scope.hideActionsPopover();
if ($scope.formData.isMember) {
return UIUtils.alert.info("INFO.NOT_NEED_MEMBERSHIP");
}
return $scope.showUidPopup()
.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;
csWallet.self(uid, false/*do NOT load membership here*/)
.then(function() {
$scope.doMembershipIn();
})
.catch(function(err){
UIUtils.onError('ERROR.SEND_IDENTITY_FAILED')(err)
.then(function() {
$scope.membershipIn(); // loop
});
});
}
else {
$scope.doMembershipIn();
}
})
.catch(function(err){
UIUtils.loading.hide();
UIUtils.alert.info(err);
$scope.membershipIn(); // loop
});
};
// Send membership OUT
$scope.membershipOut = function(confirm, confirmAgain) {
$scope.hideActionsPopover();
// Ask user confirmation
if (!confirm) {
return UIUtils.alert.confirm('CONFIRM.MEMBERSHIP_OUT', 'CONFIRM.POPUP_WARNING_TITLE', {
cssClass: 'warning',
okText: 'COMMON.BTN_YES',
okType: 'button-assertive'
})
.then(function(confirm) {
if (confirm) $scope.membershipOut(true); // loop with confirmation
});
}
if (!confirmAgain) {
return UIUtils.alert.confirm("CONFIRM.MEMBERSHIP_OUT_2", 'CONFIRM.POPUP_TITLE', {
cssClass: 'warning',
okText: 'COMMON.BTN_YES',
okType: 'button-assertive'
})
.then(function (confirm) {
if (confirm) $scope.membershipOut(true, true); // loop with all confirmations
});
}
UIUtils.loading.show();
return csWallet.membership.out()
.then(function() {
UIUtils.loading.hide();
UIUtils.toast.show('INFO.MEMBERSHIP_OUT_SENT');
})
.catch(UIUtils.onError('ERROR.SEND_MEMBERSHIP_OUT_FAILED'));
};
// Updating wallet data
$scope.doUpdate = function() {
console.debug('[wallet] Updating wallet...');
return UIUtils.loading.show()
.then(function() {
return csWallet.refreshData();
})
.then(function() {
return UIUtils.loading.hide();
})
.then(function() {
$scope.updateView();
})
.catch(UIUtils.onError('ERROR.REFRESH_WALLET_DATA'));
};
/**
* Renew membership
*/
$scope.renewMembership = function(confirm) {
if (!$scope.formData.isMember) {
return UIUtils.alert.error("ERROR.ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION");
}
if (!confirm && !$scope.formData.requirements.needRenew) {
return $translate("CONFIRM.NOT_NEED_RENEW_MEMBERSHIP", {membershipExpiresIn: $scope.formData.requirements.membershipExpiresIn})
.then(function(message) {
return UIUtils.alert.confirm(message);
})
.then(function(confirm) {
if (confirm) $scope.renewMembership(true); // loop with confirm
});
}
return UIUtils.alert.confirm("CONFIRM.RENEW_MEMBERSHIP")
.then(function(confirm) {
if (confirm) {
UIUtils.loading.show();
return $scope.doMembershipIn();
}
})
.catch(function(err){
UIUtils.loading.hide();
UIUtils.alert.error(err)
// loop
.then($scope.renewMembership);
});
};
/**
* Fix identity (e.g. when identity expired)
*/
$scope.fixIdentity = function() {
if (!$scope.formData.uid) return;
return $translate('CONFIRM.FIX_IDENTITY', {uid: $scope.formData.uid})
.then(function(message) {
return UIUtils.alert.confirm(message);
})
.then(function(confirm) {
if (!confirm) return;
UIUtils.loading.show();
// Reset membership data
$scope.formData.blockUid = null;
$scope.formData.sigDate = null;
return csWallet.self($scope.formData.uid);
})
.then(function() {
return $scope.doMembershipIn();
})
.catch(function(err){
UIUtils.loading.hide();
UIUtils.alert.error(err)
.then(function() {
$scope.fixIdentity(); // loop
});
});
};
/**
* Fix membership, when existing MS reference an invalid block
*/
$scope.fixMembership = function() {
if (!$scope.formData.uid) return;
return UIUtils.alert.confirm("CONFIRM.FIX_MEMBERSHIP")
.then(function(confirm) {
if (!confirm) return;
UIUtils.loading.show();
// Reset membership data
$scope.formData.blockUid = null;
$scope.formData.sigDate = null;
return Wallet.self($scope.formData.uid, false/*do NOT load membership here*/);
})
.then(function() {
return $scope.doMembershipIn();
})
.catch(function(err){
UIUtils.loading.hide();
UIUtils.alert.info(err);
$scope.fixMembership(); // loop
});
};
/**
* Catch click for quick fix
* @param fix
*/
$scope.doQuickFix = function(event) {
if (event === 'renew') {
$scope.renewMembership();
}
else if (event === 'fixMembership') {
$scope.fixMembership();
}
else if (event === 'fixIdentity') {
$scope.fixIdentity();
}
};
/* -- popup / UI -- */
// Transfer
$scope.showTransferModal = function() {
var hasCredit = (!!$scope.walletData.balance && $scope.walletData.balance > 0);
if (!hasCredit) {
UIUtils.alert.info('INFO.NOT_ENOUGH_CREDIT');
return;
}
Modals.showTransfer()
.then(function(done){
if (done) {
UIUtils.toast.show('INFO.TRANSFER_SENT');
$scope.$broadcast('$$rebind::' + 'balance'); // force rebind balance
$scope.motion.show({selector: '.item-pending'});
}
});
};
$scope.startWalletTour = function() {
$scope.hideActionsPopover();
return $scope.showHelpTip(0, true);
};
$scope.showHelpTip = function(index, isTour) {
index = angular.isDefined(index) ? index : csSettings.data.helptip.wallet;
isTour = angular.isDefined(isTour) ? isTour : false;
if (index < 0) return;
// Create a new scope for the tour controller
var helptipScope = $scope.createHelptipScope(isTour);
if (!helptipScope) return; // could be undefined, if a global tour already is already started
helptipScope.tour = isTour;
return helptipScope.startWalletTour(index, false)
.then(function(endIndex) {
helptipScope.$destroy();
if (!isTour) {
csSettings.data.helptip.wallet = endIndex;
csSettings.store();
}
});
};
$scope.showQRCode = function(id, text, timeout) {
if (!!$scope.qrcode) {
return;
}
$scope.qrcode = new QRCode(id, {
text: text,
width: 200,
height: 200,
correctLevel: QRCode.CorrectLevel.L
});
UIUtils.motion.toggleOn({selector: '#wallet #'+id+'.qrcode'}, timeout || 1100);
};
$scope.showCertifications = function() {
// Warn: do not use a simple link here (a ng-click is mandatory for help tour)
$state.go(UIUtils.screen.isSmall() ? 'app.wallet_cert' : 'app.wallet_cert_lg', {
pubkey: $scope.formData.pubkey,
uid: $scope.formData.name || $scope.formData.uid,
type: 'received'
});
};
$scope.showGivenCertifications = function() {
// Warn: do not use a simple link here (a ng-click is mandatory for help tour)
$state.go(UIUtils.screen.isSmall() ? 'app.wallet_cert' : 'app.wallet_cert_lg', {
pubkey: $scope.formData.pubkey,
uid: $scope.formData.name || $scope.formData.uid,
type: 'given'
});
};
$scope.showActionsPopover = function(event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('templates/wallet/popover_actions.html', {
scope: $scope
}).then(function(popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
$scope.showSharePopover = function(event) {
$scope.hideActionsPopover();
var title = $scope.formData.name || $scope.formData.uid || $scope.formData.pubkey;
// Use pod share URL - see issue #69
var url = esHttp.getUrl('/user/profile/' + $scope.formData.pubkey + '/_share');
// Override default position, is small screen - fix #25
if (UIUtils.screen.isSmall()) {
event = angular.element(document.querySelector('#wallet-share-anchor')) || event;
}
UIUtils.popover.share(event, {
bindings: {
url: url,
titleKey: 'WOT.VIEW.POPOVER_SHARE_TITLE',
titleValues: {title: title},
postMessage: title
}
});
};
$scope.showSecurityModal = function(){
$scope.hideActionsPopover();
Modals.showAccountSecurity();
};
}
WotLookupController.$inject = ['$scope', '$state', '$timeout', '$focus', '$ionicPopover', '$ionicHistory', 'UIUtils', 'csConfig', 'csCurrency', 'csSettings', 'Device', 'BMA', 'csWallet', 'esDocument', 'esProfile'];
WotLookupModalController.$inject = ['$scope', '$controller', '$focus', 'parameters'];
WotIdentityAbstractController.$inject = ['$scope', '$rootScope', '$state', '$translate', '$ionicHistory', 'UIUtils', 'Modals', 'esHttp', 'csCurrency', 'csWot', 'csWallet'];
WotIdentityViewController.$inject = ['$scope', '$rootScope', '$controller', '$timeout', 'UIUtils', 'csWallet'];angular.module('cesium.wot.controllers', ['cesium.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.wot_lookup', {
url: "/wot?q&type&hash",
views: {
'menuContent': {
templateUrl: "templates/wot/lookup.html",
controller: 'WotLookupCtrl'
}
}
})
.state('app.wot_identity', {
url: "/wot/:pubkey/:uid?action",
views: {
'menuContent': {
templateUrl: "templates/wot/view_identity.html",
controller: 'WotIdentityViewCtrl'
}
}
})
.state('app.wot_identity_uid', {
url: "/lookup/:uid?action",
views: {
'menuContent': {
templateUrl: "templates/wot/view_identity.html",
controller: 'WotIdentityViewCtrl'
}
}
});
}])
.controller('WotLookupCtrl', WotLookupController)
.controller('WotLookupModalCtrl', WotLookupModalController)
.controller('WotIdentityAbstractCtrl', WotIdentityAbstractController)
.controller('WotIdentityViewCtrl', WotIdentityViewController)
;
function WotLookupController($scope, $state, $timeout, $focus, $ionicPopover, $ionicHistory,
UIUtils, csConfig, csCurrency, csSettings, Device, BMA, csWallet, esDocument, esProfile) {
'ngInject';
var defaultSearchLimit = 10;
$scope.search = {
text: '',
loading: true,
type: null,
results: []
};
$scope._source = ["issuer", "title", "city", "time", "avatar._content_type"];
$scope.entered = false;
$scope.wotSearchTextId = 'wotSearchText';
$scope.enableFilter = true;
$scope.allowMultiple = false;
$scope.selection = [];
$scope.showResultLabel = true;
$scope.parameters = {}; // override in the modal controller
$scope.enter = function(e, state) {
if (!$scope.entered) {
if (state.stateParams && state.stateParams.q) { // Query parameter
$scope.search.text = state.stateParams.q;
$timeout(function() {
$scope.doSearch();
}, 100);
}
else if (state.stateParams && state.stateParams.hash) { // hash tag parameter
$scope.search.text = '#' + state.stateParams.hash;
$timeout(function() {
$scope.doSearch();
}, 100);
}
else {
$timeout(function() {
// get new comers
if (state.stateParams.type === 'newcomers' || (!csConfig.initPhase && !state.stateParams.type)) {
$scope.doGetNewcomers(0, undefined, true/*skipLocationUpdate*/);
}
}, 100);
}
// removeIf(device)
// Focus on search text (only if NOT device, to avoid keyboard opening)
$focus($scope.wotSearchTextId);
// endRemoveIf(device)
$scope.entered = true;
$timeout(UIUtils.ink, 100);
$scope.showHelpTip();
}
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.resetWotSearch = function() {
$scope.search = {
text: null,
loading: false,
type: 'newcomers',
results: []
};
};
$scope.updateLocationHref = function() {
// removeIf(device)
var stateParams = {
q: undefined,
hash: undefined,
type: undefined
};
if ($scope.search.type === 'text') {
var text = $scope.search.text.trim();
if (text.match(/^#[\wḡĞǦğàáâãäåçèéêëìíîïðòóôõöùúûüýÿ]+$/)) {
stateParams.hash = text.substr(1);
}
else {
stateParams.q = text;
}
}
else {
stateParams.type = $scope.search.type;
}
// Update location href
$ionicHistory.nextViewOptions({
disableAnimate: true,
disableBack: true,
historyRoot: true
});
$state.go('app.wot_lookup', stateParams,
{
reload: false,
inherit: true,
notify: false
});
// endRemoveIf(device)
};
$scope.doSearchText = function() {
$scope.doSearch();
$scope.updateLocationHref();
};
$scope.doSearch = function(offset, size) {
var text = $scope.search.text.trim();
if ((UIUtils.screen.isSmall() && text.length < 3) || !text.length) {
$scope.search.results = [];
$scope.search.type = 'none';
return $q.when();
}
$scope.search.loading = true;
var options = {
from: offset || 0,
size: size || defaultSearchLimit,
_source: $scope._source
};
$scope.search.type = 'text';
return esProfile.searchText(text, options)
.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 ((!idties || !idties.length) && BMA.regexp.PUBKEY.test(text)) {
$scope.doDisplayResult([{pubkey: text}]);
}
else {
$scope.doDisplayResult(idties, offset, size);
}
})
.catch(UIUtils.onError('ERROR.WOT_LOOKUP_FAILED'));
};
$scope.doGetNewcomers = function(offset, size, skipLocationUpdate) {
offset = offset || 0;
size = size || defaultSearchLimit;
if (size < defaultSearchLimit) size = defaultSearchLimit;
$scope.hideActionsPopover();
$scope.search.loading = (offset === 0);
$scope.search.type = 'newcomers';
// Update location href
if (!offset && !skipLocationUpdate) {
$scope.updateLocationHref();
}
var options = {
index: 'user',
type: 'profile',
from: offset,
size: size,
sort: {time: 'desc'}
};
return esProfile.search(options)
.then(function(idties){
if ($scope.search.type !== 'newcomers') return false; // could have change
$scope.doDisplayResult(idties, offset, size);
return true;
})
.catch(function(err) {
$scope.search.loading = false;
$scope.search.results = (offset > 0) ? $scope.search.results : [];
$scope.search.hasMore = false;
UIUtils.onError('ERROR.LOAD_NEWCOMERS_FAILED')(err);
});
};
$scope.showMore = function() {
var offset = $scope.search.results ? $scope.search.results.length : 0;
$scope.search.loadingMore = true;
var searchFunction = ($scope.search.type === 'newcomers') ?
$scope.doGetNewcomers :
$scope.doGetPending;
return searchFunction(offset)
.then(function(ok) {
if (ok) {
$scope.search.loadingMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
}
})
.catch(function(err) {
console.error(err);
$scope.search.loadingMore = false;
$scope.search.hasMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
$scope.select = function(identity) {
// identity = self -> open the user wallet
if (csWallet.isUserPubkey(identity.pubkey)) {
$state.go('app.view_wallet');
}
// Open identity view
else {
$state.go('app.wot_identity', {
pubkey: identity.pubkey,
uid: identity.uid
});
}
};
$scope.next = function() {
// This method should be override by sub controller (e.g. modal controller)
console.log('Selected identities:', $scope.selection);
};
$scope.toggleCheck = function(index, e) {
var identity = $scope.search.results[index];
if (identity.checked) {
$scope.addToSelection(identity);
}
else {
$scope.removeSelection(identity, e);
}
};
$scope.toggleSelect = function(identity){
identity.selected = !identity.selected;
};
$scope.addToSelection = function(identity) {
var copyIdty = angular.copy(identity);
if (copyIdty.name) {
copyIdty.name = copyIdty.name.replace('<em>', '').replace('</em>', ''); // remove highlight
}
$scope.selection.push(copyIdty);
};
$scope.removeSelection = function(identity, e) {
// Remove from selection array
var identityInSelection = _.findWhere($scope.selection, {id: identity.id});
if (identityInSelection) {
$scope.selection.splice($scope.selection.indexOf(identityInSelection), 1);
}
// Uncheck in result array, if exists
if (!$scope.search.loading) {
var existIdtyInResult = _.findWhere($scope.search.results, {id: identity.id});
if (existIdtyInResult && existIdtyInResult.checked) {
existIdtyInResult.checked = false;
}
}
//e.preventDefault();
};
$scope.scanQrCode = function(){
if (!Device.barcode.enable) {
return;
}
Device.barcode.scan()
.then(function(result) {
if (!result) {
return;
}
BMA.uri.parse(result)
.then(function(obj){
if (obj.pubkey) {
$scope.search.text = obj.pubkey;
}
else if (result.uid) {
$scope.search.text = obj.uid;
}
else {
$scope.search.text = result;
}
$scope.doSearch();
});
})
.catch(UIUtils.onError('ERROR.SCAN_FAILED'));
};
// Show help tip (show only not already shown)
$scope.showHelpTip = function() {
if (!$scope.isLogin()) return;
var index = angular.isDefined(index) ? index : csSettings.data.helptip.wot;
if (index < 0) return;
if (index === 0) index = 1; // skip first step
// Create a new scope for the tour controller
var helptipScope = $scope.createHelptipScope();
if (!helptipScope) return; // could be undefined, if a global tour already is already started
return helptipScope.startWotTour(index, false)
.then(function(endIndex) {
helptipScope.$destroy();
csSettings.data.helptip.wot = endIndex;
csSettings.store();
});
};
$scope.doDisplayResult = function(res, offset, size) {
res = res || {};
// pre-check result if already in selection
if ($scope.allowMultiple && res.length && $scope.selection.length) {
_.forEach($scope.selection, function(identity) {
var identityInRes = _.findWhere(res, {id: identity.id});
if (identityInRes) {
identityInRes.checked = true;
}
});
}
if (!offset) {
$scope.search.results = res || [];
}
else {
$scope.search.results = $scope.search.results.concat(res);
}
$scope.search.loading = false;
$scope.search.hasMore = res.length && $scope.search.results.length >= (offset + size);
$scope.smallscreen = UIUtils.screen.isSmall();
if (!$scope.search.results.length) return;
// Motion
if (res.length > 0 && $scope.motion) {
$scope.motion.show({selector: '.lookupForm .list .item', ink: true});
}
};
/* -- show/hide popup -- */
$scope.showActionsPopover = function(event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('templates/wot/lookup_popover_actions.html', {
scope: $scope
}).then(function(popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
}
function WotLookupModalController($scope, $controller, $focus, parameters){
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('WotLookupCtrl', {$scope: $scope}));
parameters = parameters || {};
$scope.search.loading = false;
$scope.enableFilter = angular.isDefined(parameters.enableFilter) ? parameters.enableFilter : false;
$scope.allowMultiple = angular.isDefined(parameters.allowMultiple) ? parameters.allowMultiple : false;
$scope.parameters = parameters;
$scope.showResultLabel = false;
$scope.wotSearchTextId = 'wotSearchTextModal';
if ($scope.allowMultiple && parameters.selection) {
$scope.selection = parameters.selection;
}
$scope.cancel = function(){
$scope.closeModal();
};
$scope.select = function(identity){
$scope.closeModal({
pubkey: identity.pubkey,
uid: identity.uid,
name: identity.name && identity.name.replace(/<\/?em>/ig, '')
});
};
$scope.next = function() {
$scope.closeModal($scope.selection);
};
$scope.updateLocationHref = function() {
// Do NOT change location href
};
$scope.showHelpTip = function() {
// silent
};
// removeIf(device)
// Focus on search text (only if NOT device, to avoid keyboard opening)
$focus($scope.wotSearchTextId);
// endRemoveIf(device)
}
/**
* Abtract controller that load identity, that expose some useful methods in $scope, like 'certify()'
* @param $scope
* @param $state
* @param $timeout
* @param UIUtils
* @param Modals
* @param csConfig
* @param csWot
* @param csWallet
* @constructor
*/
function WotIdentityAbstractController($scope, $rootScope, $state, $translate, $ionicHistory,
UIUtils, Modals, esHttp, csCurrency, csWot, csWallet) {
'ngInject';
$scope.formData = {
hasSelf: true
};
$scope.disableCertifyButton = true;
$scope.loading = true;
$scope.load = function(pubkey, withCache, uid) {
return csWot.load(pubkey, withCache, uid)
.then(function(identity){
if (!identity) return UIUtils.onError('ERROR.IDENTITY_NOT_FOUND')().then($scope.showHome);
$scope.formData = identity;
$scope.revoked = identity.requirements && (identity.requirements.revoked || identity.requirements.pendingRevocation);
$scope.canCertify = identity.hasSelf && (!csWallet.isLogin() || (!csWallet.isUserPubkey(pubkey))) && !$scope.revoked;
$scope.canSelectAndCertify = identity.hasSelf && csWallet.isUserPubkey(pubkey);
$scope.alreadyCertified = !$scope.canCertify || !csWallet.isLogin() ? false :
(!!_.findWhere(identity.received_cert, { pubkey: csWallet.data.pubkey, valid: true }) ||
!!_.findWhere(identity.received_cert_pending, { pubkey: csWallet.data.pubkey, valid: true }));
$scope.disableCertifyButton = $scope.alreadyCertified || $scope.revoked;
$scope.loading = false;
})
.catch(function(err) {
$scope.loading = false;
UIUtils.onError('ERROR.LOAD_IDENTITY_FAILED')(err);
});
};
$scope.removeActionParamInLocationHref = function(state) {
if (!state || !state.stateParams || !state.stateParams.action) return;
var stateParams = angular.copy(state.stateParams);
// Reset action param
stateParams.action = null;
// Update location href
$ionicHistory.nextViewOptions({
disableAnimate: true,
disableBack: false,
historyRoot: false
});
$state.go(state.stateName, stateParams,
{
reload: false,
inherit: true,
notify: false
});
};
/* -- open screens -- */
$scope.showSharePopover = function(event) {
var title = $scope.formData.name || $scope.formData.uid || $scope.formData.pubkey;
// Use pod share URL - see issue #69
var url = esHttp.getUrl('/user/profile/' + $scope.formData.pubkey + '/_share');
// Override default position, is small screen - fix #25
if (UIUtils.screen.isSmall()) {
event = angular.element(document.querySelector('#wot-share-anchor-'+$scope.formData.pubkey)) || event;
}
UIUtils.popover.share(event, {
bindings: {
url: url,
titleKey: 'WOT.VIEW.POPOVER_SHARE_TITLE',
titleValues: {title: title},
postMessage: title
}
});
};
}
/**
* Identity view controller - should extend WotIdentityAbstractCtrl
*/
function WotIdentityViewController($scope, $rootScope, $controller, $timeout, UIUtils, csWallet) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('WotIdentityAbstractCtrl', {$scope: $scope}));
$scope.motion = UIUtils.motion.fadeSlideInRight;
// Init likes here, to be able to use in extension
$scope.options = $scope.options || {};
$scope.options.like = {
kinds: ['VIEW', 'LIKE', 'ABUSE', 'FOLLOW', 'STAR'],
index: 'user',
type: 'profile'
};
$scope.likeData = {
views: {},
likes: {},
follows: {},
abuses: {},
stars: {}
};
$scope.$on('$ionicView.enter', function(e, state) {
var onLoadSuccess = function() {
$scope.doMotion();
if (state.stateParams && state.stateParams.action) {
$timeout(function() {
$scope.doAction(state.stateParams.action.trim());
}, 100);
$scope.removeActionParamInLocationHref(state);
$scope.likeData.id = $scope.formData.pubkey;
}
};
if (state.stateParams &&
state.stateParams.pubkey &&
state.stateParams.pubkey.trim().length > 0) {
if ($scope.loading) { // load once
return $scope.load(state.stateParams.pubkey.trim(), true /*withCache*/, state.stateParams.uid)
.then(onLoadSuccess);
}
}
else if (state.stateParams &&
state.stateParams.uid &&
state.stateParams.uid.trim().length > 0) {
if ($scope.loading) { // load once
return $scope.load(null, true /*withCache*/, state.stateParams.uid)
.then(onLoadSuccess);
}
}
// Load from wallet pubkey
else if (csWallet.isLogin()){
if ($scope.loading) {
return $scope.load(csWallet.data.pubkey, true /*withCache*/, csWallet.data.uid)
.then(onLoadSuccess);
}
}
// Redirect to home
else {
$scope.showHome();
}
});
$scope.doMotion = function() {
$scope.motion.show({selector: '.view-identity .list .item'});
$scope.$broadcast('$csExtension.motion');
};
}
SettingsController.$inject = ['$scope', '$q', '$window', '$ionicHistory', '$ionicPopup', '$timeout', '$translate', 'UIUtils', 'Modals', 'BMA', 'csHttp', 'csConfig', 'csSettings', 'csPlatform'];
angular.module('cesium.settings.controllers', ['cesium.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.settings', {
url: "/settings",
views: {
'menuContent': {
templateUrl: "templates/settings/settings.html",
controller: 'SettingsCtrl'
}
}
})
;
}])
.controller('SettingsCtrl', SettingsController)
;
function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $timeout, $translate,
UIUtils, Modals, BMA, csHttp, csConfig, csSettings, csPlatform) {
'ngInject';
$scope.formData = angular.copy(csSettings.data);
$scope.popupData = {}; // need for the node popup
$scope.loading = true;
$scope.nodePopup = {};
$scope.bma = BMA;
$scope.$on('$ionicView.enter', function() {
csSettings.ready().then($scope.load);
});
$scope.setPopupForm = function(popupForm) {
$scope.popupForm = popupForm;
};
$scope.load = function() {
$scope.loading = true; // to avoid the call of csWallet.store()
// Fill locales
$scope.locales = angular.copy(csSettings.locales);
// Apply settings
angular.merge($scope.formData, csSettings.data);
// Make sure to use full locale object (id+name)
$scope.formData.locale = (csSettings.data.locale && csSettings.data.locale.id && _.findWhere($scope.locales, {id: csSettings.data.locale.id})) ||
_.findWhere($scope.locales, {id: csSettings.defaultSettings.locale.id});
return $timeout(function() {
$scope.loading = false;
// Set Ink
UIUtils.ink({selector: '.item'});
$scope.showHelpTip();
}, 100);
};
$scope.reset = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
$scope.pendingSaving = true;
csSettings.reset()
.then(csPlatform.restart)
.then(function() {
// reload
$scope.load();
$scope.pendingSaving = false;
});
};
$scope.changeLanguage = function(langKey) {
$translate.use(langKey);
};
// Change node
$scope.changeNode= function(node) {
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,
port: port,
useSsl: angular.isDefined($scope.formData.node.useSsl) ?
$scope.formData.node.useSsl :
($scope.formData.node.port == 443)
};
$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) {
return; // same node = nothing to do
}
UIUtils.loading.show();
var nodeBMA = BMA.instance(newNode.host, newNode.port, newNode.useSsl, true /*cache*/);
nodeBMA.isAlive()
.then(function(alive) {
if (!alive) {
UIUtils.loading.hide();
return UIUtils.alert.error('ERROR.INVALID_NODE_SUMMARY')
.then(function(){
$scope.changeNode(newNode); // loop
});
}
UIUtils.loading.hide();
angular.merge($scope.formData.node, newNode);
delete $scope.formData.node.temporary;
BMA.copy(nodeBMA);
$scope.bma = BMA;
// Restart platform (or start if not already started)
csPlatform.restart();
// Reset history cache
return $ionicHistory.clearCache();
});
});
};
$scope.showNodeList = function() {
// Check if need a filter on SSL node
var forceUseSsl = (csConfig.httpsMode === 'true' || csConfig.httpsMode === true || csConfig.httpsMode === 'force') ||
($window.location && $window.location.protocol === 'https:') ? true : false;
$ionicPopup._popupStack[0].responseDeferred.promise.close();
return Modals.showNetworkLookup({
enableFilter: true, // enable filter button
bma: true, // only BMA node
ssl: forceUseSsl ? true : undefined
})
.then(function (peer) {
if (peer) {
var bma = peer.getBMA();
return {
host: (bma.dns ? bma.dns :
(peer.hasValid4(bma) ? bma.ipv4 : bma.ipv6)),
port: bma.port || 80,
useSsl: bma.useSsl || bma.port == 443
};
}
})
.then(function(newNode) {
$scope.changeNode(newNode);
});
};
// 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;
if (!!$scope.popupForm) {
$scope.popupForm.$setPristine();
}
$translate(['SETTINGS.POPUP_PEER.TITLE', 'COMMON.BTN_OK', 'COMMON.BTN_CANCEL'])
.then(function (translations) {
// Choose UID popup
$ionicPopup.show({
templateUrl: 'templates/settings/popup_node.html',
title: translations['SETTINGS.POPUP_PEER.TITLE'],
scope: $scope,
buttons: [
{ text: translations['COMMON.BTN_CANCEL'] },
{
text: translations['COMMON.BTN_OK'],
type: 'button-positive',
onTap: function(e) {
$scope.popupForm.$submitted=true;
if(!$scope.popupForm.$valid || !$scope.popupForm.newNode) {
//don't allow the user to close unless he enters a node
e.preventDefault();
} else {
return {
server: $scope.popupData.newNode,
useSsl: $scope.popupData.useSsl
};
}
}
}
]
})
.then(function(res) {
if (!res) { // user cancel
UIUtils.loading.hide();
return;
}
var parts = res.server.split(':');
parts[1] = parts[1] ? parts[1] : (res.useSsl ? 443 : 80);
resolve({
host: parts[0],
port: parts[1],
useSsl: res.useSsl
});
});
});
});
};
$scope.save = function() {
if ($scope.loading || $scope.pendingSaving) return $q.when();
if ($scope.saving) {
$scope.pendingSaving = true;
// Retry later
return $timeout(function() {
$scope.pendingSaving = false;
return $scope.save();
}, 500);
}
$scope.saving = true;
// Async - to avoid UI lock
return $timeout(function() {
// Make sure to format helptip
$scope.cleanupHelpTip();
// Applying
csSettings.apply($scope.formData);
// Store
return csSettings.store();
}, 100)
.then(function() {
//return $timeout(function() {
$scope.saving = false;
//}, 100);
});
};
$scope.onDataChanged = function(oldValue, newValue, scope) {
if ($scope.loading || $scope.pendingSaving) return $q.when();
if ($scope.saving) {
$scope.pendingSaving = true;
// Retry later
return $timeout(function() {
$scope.pendingSaving = false;
return $scope.onDataChanged(oldValue, newValue, scope);
}, 500);
}
// Changes from the current scope: save changes
if ((scope === $scope) && !angular.equals(oldValue, newValue)) {
$scope.save();
}
};
$scope.$watch('formData', $scope.onDataChanged, true);
$scope.getServer = function() {
if (!$scope.formData.node || !$scope.formData.node.host) return '';
return csHttp.getServer($scope.formData.node.host, $scope.formData.node.port);
};
$scope.cleanupHelpTip = function() {
var helptipChanged = $scope.formData.helptip.enable !== csSettings.data.helptip.enable;
if (helptipChanged) {
var enable = $scope.formData.helptip.enable;
// Apply default values
$scope.formData.helptip = angular.merge({}, csSettings.defaultSettings.helptip);
// Then restore the enable flag
$scope.formData.helptip.enable = enable;
}
};
/* -- modals & popover -- */
$scope.showActionsPopover = function(event) {
UIUtils.popover.show(event, {
templateUrl: 'templates/settings/popover_actions.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.actionsPopover = popover;
}
});
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
$scope.actionsPopover = null;
}
};
$scope.startSettingsTour = function() {
$scope.hideActionsPopover();
return $scope.showHelpTip(0, true);
};
// Show help tip (show only not already shown)
$scope.showHelpTip = function(index, tour) {
if (!$scope.isLogin() && !tour) return;
index = angular.isDefined(index) ? index : csSettings.data.helptip.settings;
if (index < 0) return;
if (index === 0) index = 1; // skip first step
// Create a new scope for the tour controller
var helptipScope = $scope.createHelptipScope(tour);
if (!helptipScope) return; // could be undefined, if a global tour already is already started
return helptipScope.startSettingsTour(index, false)
.then(function(endIndex) {
helptipScope.$destroy();
csSettings.data.helptip.settings = endIndex;
csSettings.store();
});
};
}
angular.module('cesium.controllers', [
'cesium.app.controllers',
'cesium.join.controllers',
'cesium.login.controllers',
'cesium.help.controllers',
'cesium.settings.controllers',
'cesium.wallet.controllers',
'cesium.wot.controllers'
])
;
angular.module('cesium.templates', []).run(['$templateCache', function($templateCache) {$templateCache.put('templates/menu.html','<ion-side-menus enable-menu-with-back-views="true" bind-notifier="{locale:$root.settings.locale.id}">\n <!-- HEADER -->\n <ion-side-menu-content>\n <ion-nav-bar class="bar-dark" title-align="left">\n <ion-nav-back-button class="no-text">\n </ion-nav-back-button>\n\n <ion-nav-buttons side="left">\n <button class="button button-icon button-clear icon ion-navicon visible-nomenu" menu-toggle="left"></button>\n </ion-nav-buttons>\n <ion-nav-buttons side="right">\n <!-- Allow extension here -->\n <cs-extension-point name="nav-buttons-right"></cs-extension-point>\n\n <!-- profile -->\n <a id="helptip-header-bar-btn-profile" class="button button-icon button-clear hidden-xs hidden-sm" ng-click="showProfilePopover($event)">\n <i class="avatar avatar-member" ng-if="!$root.walletData.avatar" ng-class="{\'disable\': !login, \'royal-bg\': login}">\n </i>\n <i class="avatar" ng-if="$root.walletData.avatar" style="background-image: url(\'{{$root.walletData.avatar.src}}\')">\n </i>\n </a>\n </ion-nav-buttons>\n </ion-nav-bar>\n <ion-nav-view name="menuContent"></ion-nav-view>\n </ion-side-menu-content>\n\n <!-- MENU -->\n <ion-side-menu id="menu" side="left" expose-aside-when="large" enable-menu-with-back-views="false" width="225">\n <ion-header-bar>\n <h1 class="title dark hidden-sm hidden-xs" translate>\n COMMON.APP_NAME\n </h1>\n\n <div class="visible-sm visible-xs hero">\n <div class="content">\n <i class="avatar avatar-member hero-icon" ng-if="!$root.walletData.avatar" ng-class="{\'royal-bg\': login, \'stable-bg\': !login}" ng-click="!login ? showHome() : loginAndGo(\'app.view_wallet\')" menu-close></i>\n <i class="avatar hero-icon" ng-if="$root.walletData.avatar" style="background-image: url(\'{{$root.walletData.avatar.src}}\')" ng-click="loginAndGo(\'app.view_wallet\')" menu-close></i>\n <h4 ng-if="login">\n <a class="light" ng-click="loginAndGo(\'app.view_wallet\')" menu-close>\n {{$root.walletData.name||$root.walletData.uid}}\n <span ng-if="!$root.walletData.name && !$root.walletData.uid"><i class="icon ion-key"></i>&nbsp;{{$root.walletData.pubkey|formatPubkey}}</span>\n </a>\n </h4>\n <h4 ng-if="!login">\n <a class="light" ui-sref="app.view_wallet" menu-close>\n {{\'COMMON.BTN_LOGIN\'|translate}}\n <i class="ion-arrow-right-b"></i>\n </a>\n </h4>\n <cs-extension-point name="menu-profile-user"></cs-extension-point>\n </div>\n <!-- logout -->\n <a ng-if="login" class="button-icon" ng-click="logout({askConfirm: true})" style="position: absolute; top: 5px; left: 5px; z-index: 999">\n <i class="icon stable ion-android-exit"></i>\n </a>\n </div>\n\n <!-- Fullscreen button -->\n <!-- removeIf(device) -->\n <a ng-if="::$root.device.isWeb()" ng-click="toggleFullscreen()" class="button-icon visible-sm visible-xs" style="position: absolute; top: 5px; right: 5px; z-index: 999">\n <i class="icon ion-arrow-expand light" ng-class="{\'ion-arrow-shrink\': fullscreen}"></i>\n </a>\n <!-- endRemoveIf(device) -->\n </ion-header-bar>\n\n <ion-content scroll="false">\n <ion-list class="list">\n\n <!-- Home -->\n <ion-item menu-close class="item-icon-left" ui-sref="app.home" active-link-path-prefix="#/app/home" active-link="active">\n <i class="icon ion-home"></i>\n {{:locale:\'MENU.HOME\'|translate}}\n </ion-item>\n\n <!-- Allow extension here -->\n <cs-extension-point name="menu-discover"></cs-extension-point>\n\n <!-- MAIN Section -->\n <div class="item item-divider"></div>\n\n <!-- Allow extension here -->\n <cs-extension-point name="menu-main"></c
$templateCache.put('templates/modal_about.html','<ion-modal-view class="about">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear visible-xs" ng-click="closeModal()" translate>COMMON.BTN_CLOSE\n </button>\n <h1 class="title" translate>ABOUT.TITLE</h1>\n </ion-header-bar>\n\n <ion-content class="text-center" scroll="true">\n\n <div class="list item-wrap-text">\n <ion-item class="item-icon-left item-text-wrap">\n <ng-bind-html ng-bind-html="\'COMMON.APP_NAME\'|translate"></ng-bind-html>&nbsp;<b>{{\'COMMON.APP_VERSION\'|translate:$root.config}}</b>\n <i ng-if="$root.newRelease" class="assertive ion-alert-circled"></i>\n <h3 ng-if="$root.config.build" class="gray">{{\'COMMON.APP_BUILD\'|translate:$root.config}}</h3>\n <span translate>ABOUT.LICENSE</span>\n </ion-item>\n\n <!-- new version -->\n <ion-item class="item-icon-left" ng-if="$root.newRelease">\n <i class="item-image icon ion-alert-circled assertive"></i>\n\n <span ng-if="!$root.device.isWeb()" ng-bind-html="\'ABOUT.PLEASE_UPDATE\' | translate:$root.newRelease "></span>\n <span ng-if="$root.device.isWeb()" ng-bind-html="\'ABOUT.LATEST_RELEASE\' | translate:$root.newRelease "></span>\n\n <!-- link to release page -->\n <h3 ng-if="!$root.device.enable">\n <a ng-click="openLink($event, $root.newRelease.url)" translate>{{::$root.newRelease.url}}</a>\n </h3>\n </ion-item>\n\n <!-- source code -->\n <div class="item item-icon-left">\n <i class="item-image icon ion-social-github"></i>\n {{\'ABOUT.CODE\' | translate}}\n <h3><a ng-click="openLink($event, \'https://github.com/duniter-gchange/gchange-client\')">https://github.com/duniter-gchange/gchange-client</a></h3>\n </div>\n\n <!-- forum -->\n <div class="item item-icon-left">\n <i class="item-image icon ion-chatbubbles"></i>\n {{\'ABOUT.FORUM\' | translate}}\n <h3><a ng-click="openLink($event, $root.settings.userForumUrl)">{{::$root.settings.userForumUrl}}</a></h3>\n </div>\n\n <div class="item item-icon-left">\n <i class="item-image icon ion-person-stalker"></i>\n {{\'ABOUT.DEVELOPERS\' | translate}}\n <h3>\n <a ng-click="openLink($event, \'https://github.com/blavenie\')">Benoit Lavenier</a>\n </h3>\n </div>\n <ion-item class="item item-icon-left item-text-wrap">\n <i class="item-image icon ion-bug"></i>\n <h2>{{\'ABOUT.DEV_WARNING\'|translate}}</h2>\n <span translate>ABOUT.DEV_WARNING_MESSAGE</span>\n <br>\n <a ng-click="openLink($event, $root.settings.newIssueUrl)" translate>ABOUT.REPORT_ISSUE</a>\n </ion-item>\n\n <div class="padding hidden-xs text-center">\n <button class="button button-positive ink" type="submit" ng-click="closeModal()">\n {{\'COMMON.BTN_CLOSE\' | translate}}\n </button>\n </div>\n\n </div>\n </ion-content>\n</ion-modal-view>\n');
$templateCache.put('templates/api/locales_popover.html','<ion-popover-view class="fit popover-locales" style="height: {{locales.length*48}}px">\n <ion-content scroll="false">\n <div class="list item-text-wrap block">\n\n <a ng-repeat="l in locales track by l.id" class="item item-icon-left ink" ng-click="changeLanguage(l.id)">\n <i class="item-image avatar" style="border-radius: 0; background-image: url(https://www.countryflags.io/{{l.country}}/shiny/64.png)"></i>\n {{l.label | translate}}\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('templates/common/form_error_messages.html',' <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT"></span>\n </div>\n <div class="form-error" ng-message="maxlength">\n <span translate="ERROR.FIELD_TOO_LONG"></span>\n </div>\n <div class="form-error" ng-message="pattern">\n <span translate="ERROR.FIELD_ACCENT"></span>\n </div>\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n');
$templateCache.put('templates/common/popover_copy.html','<ion-popover-view class="popover-copy" style="height: {{(!rows || rows &lt;= 1) ? 50 : rows*22}}px">\n <ion-content scroll="false">\n <div class="list">\n <div class="item item-input">\n <input type="text" ng-if="!rows || rows &lt;= 1" ng-model="value">\n <textarea ng-if="rows && rows > 1" ng-model="value" rows="{{rows}}" cols="10">\n </textarea></div>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('templates/common/popover_helptip.html','<ion-popover-view class="popover-helptip">\n <ion-content scroll="false" class="list">\n <p>\n <i ng-if="icon.position && !icon.position.startsWith(\'bottom-\')" class="{{icon.class}} icon-{{icon.position}} hidden-xs" style="{{icon.style}}"></i>\n\n <!-- close button-->\n <a ng-click="closePopover()" class="pull-right button-close" ng-class="{\'pull-left\': icon.position === \'right\', \'pull-right\': icon.position !== \'right\'}">\n <i class="ion-close"></i>\n </a>\n\n <span>&nbsp;</span>\n </p>\n\n <p class="padding light">\n <ng-bind-html ng-bind-html="content | translate:contentParams"></ng-bind-html>\n <ng-bind-html ng-bind-html="trustContent"></ng-bind-html>\n </p>\n\n <!-- buttons (if helptip) -->\n <div class="text-center" ng-if="!tour">\n <button class="button button-small button-stable" ng-if="!hasNext" ng-click="closePopover(true)" translate>COMMON.BTN_UNDERSTOOD</button>\n <button class="button button-small button-stable" id="helptip-btn-ok" ng-if="hasNext" ng-click="closePopover(false)" translate>COMMON.BTN_UNDERSTOOD</button>\n <button id="helptip-btn-ok" class="button button-small button-positive icon-right ink" ng-if="hasNext" ng-click="closePopover(true)">\n <i class="icon ion-chevron-right"></i>\n </button>\n </div>\n\n <!-- buttons (if feature tour) -->\n <div class="text-center" ng-if="tour">\n <button class="button button-small button-positive" id="helptip-btn-ok" ng-if="!hasNext" ng-click="closePopover(false)" translate>COMMON.BTN_CLOSE</button>\n <button id="helptip-btn-ok" class="button button-small button-positive icon-right ink" ng-if="hasNext" ng-click="closePopover(true)">\n {{\'COMMON.BTN_CONTINUE\'|translate}}\n <i class="icon ion-chevron-right"></i>\n </button>\n </div>\n\n <p>\n <i ng-if="icon.position && icon.position.startsWith(\'bottom-\')" class="{{icon.class}} icon-{{icon.position}} hidden-xs"></i>\n </p>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('templates/common/popover_profile.html','<ion-popover-view class="fit has-header popover-profile hidden-xs hidden-sm">\n <ion-content scroll="false">\n <div class="row">\n <div class="col col-33">\n <i class="avatar avatar-member" ng-if="!$root.walletData.avatar" ng-class="{\'royal-bg\': login, \'stable-bg\': !login}"></i>\n <i class="avatar" ng-if="$root.walletData.avatar" style="background-image: url(\'{{$root.walletData.avatar.src}}\')"></i>\n </div>\n <div class="col col-66" ng-if="login">\n <h4>{{$root.walletData.name||$root.walletData.uid}}</h4>\n\n <h4 class="gray" ng-if="!$root.walletData.name && !$root.walletData.uid" copy-on-click="{{$root.walletData.pubkey}}">\n <i class="icon ion-key"></i> {{$root.walletData.pubkey|formatPubkey}}\n </h4>\n </div>\n </div>\n\n <div class="row" ng-show="login">\n <div class="col col-66 col-offset-33">\n\n <!-- Allow extension here -->\n <cs-extension-point name="profile-popover-user"></cs-extension-point>\n </div>\n </div>\n\n <div class="row" ng-show="!login">\n <div class="col col-66 col-offset-33">\n <div class="text-center no-padding gray">\n {{\'LOGIN.NO_ACCOUNT_QUESTION\'|translate}}\n <br class="visible-xs">\n <b>\n <button class="button button-calm button-small ink" ng-click="showJoinModal()">\n {{\'LOGIN.CREATE_ACCOUNT\'|translate}}\n </button>\n </b>\n </div>\n </div>\n </div>\n </ion-content>\n <ion-footer-bar class="stable-bg row">\n <div class="col">\n <!-- settings -->\n <button class="button button-raised button-block button-stable ink ink-dark" id="helptip-popover-profile-btn-settings" ng-click="showSettings()" ui-sref="app.settings">\n <i class="icon ion-android-settings"></i>\n {{\'MENU.SETTINGS\' | translate}}\n </button>\n </div>\n <div class="col">\n <button class="button button-raised button-block button-stable ink ink-dark" ng-show="login" ng-click="logout()" translate>COMMON.BTN_LOGOUT</button>\n <button class="button button-raised button-block button-positive ink" ng-show="!login" ng-click="loginAndGo(\'app.view_wallet\')" translate>COMMON.BTN_LOGIN</button>\n </div>\n </ion-footer-bar>\n</ion-popover-view>\n');
$templateCache.put('templates/common/popover_share.html','<ion-popover-view class="popover-share">\n <ion-content scroll="false">\n <div class="bar bar-header">\n <h1 class="title" ng-bind-html="titleKey|translate:titleValues"></h1>\n <span class="gray pull-right hidden-xs">{{time|formatDate}}</span>\n </div>\n <div class="list no-margin no-padding has-header has-footer block">\n <div class="item item-input">\n <input type="text" ng-model="value">\n </div>\n </div>\n\n <div class="bar bar-footer">\n <div class="button-bar">\n\n <a class="button button-icon positive icon ion-social-facebook" href="https://www.facebook.com/sharer/sharer.php?u={{postUrl|formatEncodeURI}}&amp;title={{postMessage|formatEncodeURI}}" onclick="window.open(this.href, \'facebook-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_FACEBOOK\'|translate}}">\n </a>\n\n <a class="button button-icon positive icon ion-social-twitter" href="https://twitter.com/intent/tweet?url={{postUrl|formatEncodeURI}}&amp;text={{postMessage|formatEncodeURI}}" onclick="window.open(this.href, \'twitter-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_TWITTER\'|translate}}">\n </a>\n\n <a class="button button-icon positive icon ion-social-googleplus" href="https://plus.google.com/share?url={{postUrl|formatEncodeURI}}" onclick="window.open(this.href, \'google-plus-share\', \'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=296,width=580\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_GOOGLEPLUS\'|translate}}">\n </a>\n\n <a class="button button-icon positive icon ion-social-diaspora" href="https://sharetodiaspora.github.io/?title={{postMessage|formatEncodeURI}}&amp;url={{postUrl|formatEncodeURI}}" onclick="window.open(this.href, \'diaspora-share\',\'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=580,height=296\');return false;" title="{{\'COMMON.POPOVER_SHARE.SHARE_ON_DIASPORA\'|translate}}">\n </a>\n\n <a class="button-close" title="{{\'COMMON.BTN_CLOSE\'|translate}}" ng-click="closePopover()">\n <i class="icon ion-close"></i>\n </a>\n </div>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('templates/join/modal_join.html','<ion-modal-view class="modal-full-height">\n\n <ion-header-bar class="bar-positive">\n\n <button class="button button-clear visible-xs" ng-if="!slides.slider.activeIndex" ng-click="closeModal()" translate>COMMON.BTN_CANCEL\n </button>\n <button class="button button-icon button-clear icon ion-ios-arrow-back buttons header-item" ng-click="slidePrev()" ng-if="slides.slider.activeIndex">\n </button>\n <button class="button button-icon button-clear icon ion-ios-help-outline visible-xs" ng-if="!isLastSlide" ng-click="showHelpModal()"></button>\n\n <h1 class="title" translate>ACCOUNT.NEW.TITLE</h1>\n\n <button class="button button-clear icon-right visible-xs" ng-if="!isLastSlide" ng-click="doNext()">\n <span translate>COMMON.BTN_NEXT</span>\n <i class="icon ion-ios-arrow-right"></i>\n </button>\n <button class="button button-clear icon-right visible-xs" ng-if="isLastSlide" ng-click="doNewAccount()">\n <i class="icon ion-android-send"></i>\n </button>\n </ion-header-bar>\n\n\n <ion-slides options="slides.options" slider="slides.slider">\n\n <!-- STEP: salt -->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n <form name="saltForm" novalidate="" ng-submit="doNext(\'saltForm\')">\n\n <div class="list" ng-init="setForm(saltForm, \'saltForm\')">\n\n <div class="item item-text-wrap text-center padding hidden-xs">\n <a class="pull-right icon-help" ng-click="showHelpModal(\'join-salt\')"></a>\n <span translate>ACCOUNT.NEW.SALT_WARNING</span>\n </div>\n\n <!-- salt -->\n <div class="item item-input" ng-class="{ \'item-input-error\': saltForm.$submitted && saltForm.username.$invalid}">\n <span class="input-label" translate>LOGIN.SALT</span>\n <input ng-if="!showUsername" name="username" type="password" placeholder="{{\'LOGIN.SALT_HELP\' | translate}}" ng-change="formDataChanged()" ng-model="formData.username" ng-minlength="8" required>\n <input ng-if="showUsername" name="username" type="text" placeholder="{{\'LOGIN.SALT_HELP\' | translate}}" ng-change="formDataChanged()" ng-model="formData.username" ng-minlength="8" required>\n </div>\n <div class="form-errors" ng-show="saltForm.$submitted && saltForm.username.$error" ng-messages="saltForm.username.$error">\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 8}"></span>\n </div>\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- confirm salt -->\n <div class="item item-input" ng-class="{ \'item-input-error\': saltForm.$submitted && saltForm.confirmSalt.$invalid}">\n <span class="input-label pull-right" translate>ACCOUNT.NEW.SALT_CONFIRM</span>\n <input ng-if="!showUsername" name="confirmUsername" type="password" placeholder="{{\'ACCOUNT.NEW.SALT_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmUsername" compare-to="formData.username">\n <input ng-if="showUsername" name="confirmUsername" type="text" placeholder="{{\'ACCOUNT.NEW.SALT_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmUsername" compare-to="formData.username">\n </div>\n <div class="form-errors" ng-show="saltForm.$submitted && saltForm.confirmUsername.$error" ng-messages="saltForm.confirmUsername.$error">\n <div class="form-error" ng-message="compareTo">\n <span translate="ERROR.SALT_NOT_CONFIRMED"></span>\n </div>\n </div>\n\n <!-- Show values -->\n <div class="item item-toggle dark">\n <span translate>COMMON.SHOW_VALUES</span>\n <label
$templateCache.put('templates/help/help.html','\n <a name="join"></a>\n <h2 translate>HELP.JOIN.SECTION</h2>\n\n <a name="join-salt"></a>\n <div class="row responsive-sm">\n <div class="col col-20 gray" translate>LOGIN.SALT</div>\n <div class="col" translate>HELP.JOIN.SALT</div>\n </div>\n\n <a name="join-password"></a>\n <div class="row responsive-sm">\n <div class="col col-20 gray" translate>LOGIN.PASSWORD</div>\n <div class="col" translate>HELP.JOIN.PASSWORD</div>\n </div>\n\n <a name="glossary"></a>\n <h2 translate>HELP.GLOSSARY.SECTION</h2>\n\n <a name="pubkey"></a>\n <div class="row responsive-sm">\n <div class="col col-20 gray" translate>COMMON.PUBKEY</div>\n <div class="col" translate>HELP.GLOSSARY.PUBKEY_DEF</div>\n </div>\n\n <a name="universal_dividend"></a>\n <div class="row responsive-sm">\n <div class="col col-20 gray" translate>COMMON.UNIVERSAL_DIVIDEND</div>\n <div class="col" translate>HELP.GLOSSARY.UNIVERSAL_DIVIDEND_DEF</div>\n </div>\n\n');
$templateCache.put('templates/help/modal_help.html','<ion-view class="modal slide-in-up ng-enter active ng-enter-active">\n\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CLOSE\n </button>\n\n <h1 class="title" translate>HELP.TITLE</h1>\n </ion-header-bar>\n\n <ion-content scroll="true" class="padding">\n\n <ng-include src="\'plugins/market/templates/help/help.html\'"></ng-include>\n\n <div class="padding hidden-xs text-center">\n <button class="button button-positive ink" type="submit" ng-click="closeModal()">\n {{\'COMMON.BTN_CLOSE\' | translate}}\n </button>\n </div>\n\n </ion-content>\n</ion-view>\n');
$templateCache.put('templates/help/view_help.html','<ion-view left-buttons="leftButtons">\n <ion-nav-title>\n <span class="visible-xs visible-sm" translate>HELP.TITLE</span>\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding">\n\n <h1 class="hidden-xs hidden-sm" translate>HELP.TITLE</h1>\n\n <ng-include src="\'templates/help/help.html\'"></ng-include>\n\n </ion-content>\n</ion-view>\n');
$templateCache.put('templates/home/home.html','<ion-view id="home">\n <ion-nav-title>\n\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <!-- locales -->\n <button class="button button-clear hidden-xs hidden-sm gray" ng-if="!login" ng-click="showLocalesPopover($event)" style="align-content: center">\n <img ng-if=":locale:$root.settings.locale.country" ng-src="https://www.countryflags.io/{{:locale:$root.settings.locale.country}}/shiny/32.png">\n <span ng-if=":locale:!$root.settings.locale.country">{{:locale:$root.settings.locale.label}}&nbsp;</span>\n <small class="ion-arrow-down-b"></small>\n </button>\n </ion-nav-buttons>\n\n <ion-content class="has-header text-center padding-xs bg-image-cover" style="background-image: url({{bgImage}})" scroll="false">\n\n <div class="logo"></div>\n <p class="hidden-xs">&nbsp;</p>\n\n <div class="center padding light-bg" style="max-width: 382px">\n\n <h4>\n <span class="hidden-xs" translate>HOME.MESSAGE</span>\n\n <span ng-show="!loading" ng-bind-html="\'HOME.MESSAGE_CURRENCY\'|translate:{currency: $root.currency.name}"></span>\n </h4>\n\n <br class="hidden-xs">\n\n <ion-spinner icon="android" ng-if="loading"></ion-spinner>\n\n <div class="animate-fade-in animate-show-hide ng-hide" ng-show="!loading && error">\n <div class="card card-item padding">\n <p class="item-content item-text-wrap">\n <span class="dark" trust-as-html="\'HOME.CONNECTION_ERROR\'|translate:node"></span>\n </p>\n\n <!-- Retry-->\n <button type="button" class="button button-positive icon icon-left ion-refresh ink" ng-click="reload()">{{\'COMMON.BTN_REFRESH\'|translate}}</button>\n </div>\n </div>\n\n <div class="center animate-fade-in animate-show-hide ng-hide" ng-show="!loading && !error">\n\n\n <!-- Help tour (NOT ready yet for small device)\n <button type="button"\n ng-show="login"\n class="button button-block button-stable button-raised icon-left icon ion-easel ink-dark hidden-xs"\n ng-click="startHelpTour()" >\n {{\'COMMON.BTN_HELP_TOUR\'|translate}}\n </button>-->\n\n <cs-extension-point name="buttons"></cs-extension-point>\n\n <button type="button" class="button button-block button-positive button-raised icon ink-dark" ng-click="showJoinModal()" ng-if="!login" translate>LOGIN.CREATE_FREE_ACCOUNT</button>\n\n <button type="button" class="button button-block button-positive button-raised icon icon-left ion-person ink-dark" ui-sref="app.view_wallet" ng-show="login" translate>MENU.ACCOUNT</button>\n\n <br class="visible-xs visible-sm">\n <!-- login link -->\n <div class="text-center no-padding" ng-show="!login">\n <br class="visible-xs visible-sm">\n {{\'LOGIN.HAVE_ACCOUNT_QUESTION\'|translate}}\n <b>\n <a class="positive hidden-xs hidden-sm" ng-click="loginAndGo(\'app.view_wallet\')" translate>\n COMMON.BTN_LOGIN\n </a>\n </b>\n </div>\n\n <!-- disconnect link -->\n <div class="text-center no-padding hidden-xs hidden-sm" ng-show="login">\n <span class="dark" ng-bind-html="\'HOME.NOT_YOUR_ACCOUNT_QUESTION\'|translate:$root.walletData"></span>\n <a class="bold positive" ng-click="logout()" translate>\n HOME.BTN_CHANGE_ACCOUNT\n </a>\n </div>\n\n <button type="button" class="button button-block button-stable button-raised ink visible-xs visible-sm" ui-sref="app.view_wallet" ng-if="!login" translate>COMMON.BTN_LOGIN</button>\n <button type="button" class="button button-block button-assertive button-raised icon icon-left ion-log-out ink-dark visible-xs visible-sm" ng-click="logout()" ng-if="login" translate>COMMON.BTN_LOGOUT</button>\n\n </div>\n\n <br>\n\n <div class="text-center no-padding visible-xs stable">\n <br>\n <!-- version -->\n <span translate="COMMO
$templateCache.put('templates/login/modal_login.html','<ion-modal-view class="modal-full-height">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear visible-xs" ng-click="closeModal()" translate>COMMON.BTN_CANCEL\n </button>\n <h1 class="title" ng-bind-html="\'LOGIN.TITLE\' | translate">\n </h1>\n <div class="buttons buttons-right">\n <span class="secondary-buttons visible-xs">\n <button class="button button-positive button-icon button-clear icon ion-android-done" style="color: #fff" ng-click="doLogin()">\n </button>\n </span></div>\n\n </ion-header-bar>\n\n <ion-content>\n <form name="loginForm" novalidate="" ng-submit="doLogin()">\n\n <div class="list" ng-init="setForm(loginForm)">\n\n <!-- salt (=username, to enable browser login cache) -->\n <label class="item item-input" ng-class="{ \'item-input-error\': form.$submitted && form.username.$invalid}">\n <span class="input-label hidden-xs" translate>LOGIN.SALT</span>\n <input ng-if="!showSalt" name="username" type="password" placeholder="{{\'LOGIN.SALT_HELP\' | translate}}" autocomplete="off" ng-model="formData.username" ng-model-options="{ debounce: 650 }" class="highlight-light" required>\n <input ng-if="showSalt" name="username" type="text" placeholder="{{\'LOGIN.SALT_HELP\' | translate}}" autocomplete="off" ng-model="formData.username" ng-model-options="{ debounce: 650 }" class="highlight-light" required>\n </label>\n <div class="form-errors" ng-show="form.$submitted && form.username.$error" ng-messages="form.username.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- Show salt -->\n <div class="item item-toggle dark">\n <span translate>LOGIN.SHOW_SALT</span>\n <label class="toggle toggle-stable">\n <input type="checkbox" ng-model="showSalt">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n <!-- password-->\n <label class="item item-input" ng-class="{ \'item-input-error\': form.$submitted && form.password.$invalid}">\n <span class="input-label hidden-xs" translate>LOGIN.PASSWORD</span>\n <input name="password" type="password" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}" ng-model="formData.password" ng-model-options="{ debounce: 650 }" select-on-click required>\n </label>\n <div class="form-errors" ng-show="form.$submitted && form.password.$error" ng-messages="form.password.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n\n <!-- remember me -->\n <div class="item item-toggle dark hidden-xs">\n <span translate>SETTINGS.REMEMBER_ME</span>\n <label class="toggle toggle-calm">\n <input type="checkbox" ng-model="formData.rememberMe">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n\n <!-- Show public key -->\n <div class="item item-button-right left">\n <span ng-if="formData.username && formData.password" class="input-label" translate>COMMON.PUBKEY</span>\n <a class="button button-light button-small ink animate-if" ng-click="showPubkey()" ng-if="showPubkeyButton">\n {{\'COMMON.BTN_SHOW_PUBKEY\' | translate}}\n </a>\n <h3 class="gray text-no-wrap" ng-if="!computing">\n {{pubkey}}\n </h3>\n <h3 ng-if="computing">\n <ion-spinner icon="android"></ion-spinner>\n </h3>\n </div>\n\n </div>\n\n <div class="padding hidden-xs text-right">\n <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL\n </button>\n <
$templateCache.put('templates/settings/popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink visible-xs visible-sm" ng-click="reset()">\n <i class="icon ion-refresh"></i>\n {{\'SETTINGS.BTN_RESET\' | translate}}\n </a>\n\n <!-- help tour -->\n <a class="item item-icon-left ink" ng-click="startSettingsTour()">\n <i class="icon ion-easel"></i>\n {{\'COMMON.BTN_HELP_TOUR_SCREEN\' | translate}}\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('templates/settings/popup_node.html','<form name="popupForm" ng-submit="">\n\n <div class="list no-padding" ng-init="setPopupForm(popupForm)">\n <div class="item item-input item-floating-label" ng-class="{\'item-input-error\': popupForm.$submitted && popupForm.newNode.$invalid}">\n <span class="input-label" ng-bind-html="\'SETTINGS.POPUP_PEER.HOST\'|translate"></span>\n <input name="newNode" type="text" placeholder="{{\'SETTINGS.POPUP_PEER.HOST_HELP\' | translate}}" ng-model="popupData.newNode" ng-minlength="3" required>\n </div>\n <div class="form-errors" ng-if="popupForm.$submitted && popupForm.newNode.$error" ng-messages="popupForm.newNode.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT"></span>\n </div>\n </div>\n\n <div class="item item-toggle">\n <span class="input-label">\n {{\'SETTINGS.POPUP_PEER.USE_SSL\' | translate}}\n </span>\n <h4>\n <small class="gray" ng-bind-html="\'SETTINGS.POPUP_PEER.USE_SSL_HELP\' | translate">\n </small>\n </h4>\n <label class="toggle toggle-royal no-padding-right">\n <input type="checkbox" ng-model="popupData.useSsl">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n\n <a class="button button-positive button-clear positive button-outline button-full button-small-padding icon-left ink no-padding" ng-click="showNodeList()">\n <i class="icon ion-search"></i>\n {{\'SETTINGS.POPUP_PEER.BTN_SHOW_LIST\' | translate}}\n </a>\n </div>\n\n <button type="submit" class="hide"></button>\n</form>\n\n\n');
$templateCache.put('templates/settings/settings.html','<ion-view left-buttons="leftButtons" cache-view="false" class="settings">\n <ion-nav-title translate>SETTINGS.TITLE</ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n </ion-nav-buttons>\n\n <ion-content>\n\n <!-- Buttons bar-->\n <div class="padding text-center hidden-xs hidden-sm">\n <button class="button button-raised button-stable ink" ng-click="reset()">\n <i class="icon ion-refresh"></i>\n {{\'SETTINGS.BTN_RESET\' | translate}}\n </button>\n\n <!--button class="button button-stable button-small-padding icon ion-android-more-vertical"\n ng-click="showActionsPopover($event)"\n title="{{\'COMMON.BTN_OPTIONS\' | translate}}">\n </button-->\n </div>\n\n <div class="row no-padding responsive-sm responsive-md responsive-lg">\n\n <!-- first column -->\n <div class="col col-50 list item-border-large padding-left padding-right no-padding-xs no-padding-sm" style="margin-bottom: 2px">\n\n <span class="item item-divider" translate>SETTINGS.DISPLAY_DIVIDER</span>\n\n <label class="item item-input item-select">\n <div class="input-label" translate>COMMON.LANGUAGE</div>\n <select ng-model="formData.locale" ng-change="changeLanguage(formData.locale.id)" ng-options="l as l.label for l in locales track by l.id">\n </select>\n </label>\n\n <div class="item item-toggle dark">\n <div class="input-label">\n {{\'COMMON.BTN_RELATIVE_UNIT\' | translate}}\n </div>\n <label class="toggle toggle-royal" id="helptip-settings-btn-unit-relative">\n <input type="checkbox" ng-model="formData.useRelative">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n <!--div class="item item-toggle dark item-text-wrap">\n <div class="input-label" ng-bind-html="\'SETTINGS.ENABLE_HELPTIP\' | translate">\n </div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.helptip.enable" >\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div-->\n\n <!--div class="item item-toggle dark item-text-wrap">\n <div class="input-label" ng-bind-html="\'SETTINGS.ENABLE_UI_EFFECTS\' | translate">\n </div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.uiEffects" >\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div-->\n\n <span class="item item-divider" translate>SETTINGS.STORAGE_DIVIDER</span>\n\n <div class="item item-text-wrap item-toggle dark">\n <div class="input-label">\n {{\'SETTINGS.USE_LOCAL_STORAGE\' | translate}}\n </div>\n <h4 class="gray" ng-bind-html="\'SETTINGS.USE_LOCAL_STORAGE_HELP\' | translate">\n </h4>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.useLocalStorage">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n <!-- Allow extension here -->\n <cs-extension-point name="common"></cs-extension-point>\n\n <span class="item item-divider">\n {{\'SETTINGS.AUTHENTICATION_SETTINGS\' | translate}}\n </span>\n\n <div class="item item-toggle item-text-wrap">\n <div class="input-label" ng-class="{\'gray\': !formData.useLocalStorage}">\n {{\'SETTINGS.REMEMBER_ME\' | translate}}\n </div>\n <h4 class="gray text-wrap" ng-bind-html="\'SETTINGS.REMEMBER_ME_HELP\' | translate"></h4>\n\n
$templateCache.put('templates/wallet/popover_actions.html','<ion-popover-view class="fit has-header popover-wallet-actions">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink visible-xs visible-sm" ng-click="showSharePopover($event)">\n <i class="icon ion-android-share-alt"></i>\n {{\'COMMON.BTN_SHARE\' | translate}}\n </a>\n\n <!-- identity -->\n <a class="item item-icon-left ink" ng-if="walletData.requirements.needSelf" ng-click="self()">\n <i class="icon ion-flag"></i>\n {{\'ACCOUNT.BTN_SEND_IDENTITY_DOTS\' | translate}}\n </a>\n\n <!-- membership in -->\n <a class="item item-icon-left ink visible-xs visible-sm" ng-if="walletData.requirements.needMembership" ng-click="membershipIn()">\n <i class="icon ion-person"></i>\n {{\'ACCOUNT.BTN_MEMBERSHIP_IN_DOTS\' | translate}}\n </a>\n <a class="item item-icon-left ink hidden-xs hidden-sm" ng-class="{\'gray\':!walletData.requirements.needMembership}" ng-click="membershipIn()">\n <i class="icon ion-person"></i>\n {{\'ACCOUNT.BTN_MEMBERSHIP_IN_DOTS\' | translate}}\n </a>\n\n <!-- renew membership -->\n <a class="item item-icon-left ink visible-xs visible-sm" ng-if="walletData.requirements.needRenew" ng-click="renewMembership()">\n <i class="icon ion-loop"></i>\n {{\'ACCOUNT.BTN_MEMBERSHIP_RENEW_DOTS\' | translate}}\n </a>\n <a class="item item-icon-left ink hidden-xs hidden-sm" ng-class="{\'gray\':!walletData.requirements.needRenew}" ng-click="renewMembership()">\n <i class="icon ion-loop"></i>\n {{\'ACCOUNT.BTN_MEMBERSHIP_RENEW_DOTS\' | translate}}\n </a>\n\n <a class="item item-icon-left assertive ink" ng-if="walletData.requirements.canMembershipOut" ng-click="membershipOut()">\n <i class="icon ion-log-out"></i>\n {{\'ACCOUNT.BTN_MEMBERSHIP_OUT_DOTS\' | translate}}\n </a>\n\n <a class="item item-icon-left ink" ng-click="showSecurityModal()">\n <i class="icon ion-android-lock"></i>\n <span ng-bind-html="\'ACCOUNT.BTN_SECURITY_DOTS\' | translate"></span>\n\n </a>\n\n <div class="item-divider hidden-sm hidden-xs"></div>\n\n <!-- help tour -->\n <a class="item item-icon-left ink hidden-sm hidden-xs" ng-click="startWalletTour()">\n <i class="icon ion-easel"></i>\n {{\'COMMON.BTN_HELP_TOUR_SCREEN\' | translate}}\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('templates/wallet/popover_unit.html','<ion-popover-view class="popover-unit">\n <ion-content scroll="false">\n <div class="list">\n <a class="item item-icon-left" ng-class="{ \'selected\': !formData.useRelative}" ng-click="closePopover(false)">\n <i class="icon" ng-class="{ \'ion-ios-checkmark-empty\': !formData.useRelative}"></i>\n <i ng-bind-html="$root.currency.name | currencySymbol:false"></i>\n </a>\n <a class="item item-icon-left" ng-class="{ \'selected\': formData.useRelative}" ng-click="closePopover(true)">\n <i class="icon" ng-class="{ \'ion-ios-checkmark-empty\': formData.useRelative}"></i>\n <i ng-bind-html="$root.currency.name | currencySymbol:true"></i>\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('templates/wallet/view_wallet.html','<ion-view left-buttons="leftButtons" class="view-wallet" id="wallet">\n <ion-nav-title>\n <!-- no title-->\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="doUpdate()">\n </button>\n\n <cs-extension-point name="nav-buttons"></cs-extension-point>\n\n <!--<button class="button button-icon button-clear visible-xs visible-sm"\n id="helptip-wallet-options-xs"\n ng-click="showActionsPopover($event)">\n <i class="icon ion-android-more-vertical"></i>\n </button>-->\n </ion-nav-buttons>\n\n <ion-content scroll="true" bind-notifier="{ rebind:settings.useRelative, locale:$root.settings.locale.id}">\n <div class="positive-900-bg hero" id="wallet-header" ng-class="{\'hero-qrcode-active\': toggleQRCode}">\n <div class="content" ng-if="!loading">\n <i class="avatar avatar-member" ng-if=":rebind:!formData.avatar"></i>\n <i class="avatar" ng-if=":rebind:formData.avatar" style="background-image: url({{:rebind:formData.avatar.src}})"></i>\n <ng-if ng-if=":rebind:formData.name">\n <h3 class="light">{{:rebind:formData.name}}</h3>\n </ng-if>\n <!--ng-if ng-if=":rebind:!formData.name">\n <h3 class="light" ng-if=":rebind:formData.uid">{{:rebind:formData.uid}}</h3>\n <h3 class="light" ng-if=":rebind:!formData.uid"><i class="ion-key"></i> {{:rebind:formData.pubkey | formatPubkey}}</h3>\n </ng-if-->\n <cs-extension-point name="hero"></cs-extension-point>\n </div>\n <h4 class="content light" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </h4>\n </div>\n\n <!--div id="qrcode" class="qrcode visible-xs visible-sm spin"\n ng-class="{\'active\': toggleQRCode}"\n ng-click="toggleQRCode = !toggleQRCode"></div-->\n\n <!-- Buttons bar-->\n <a id="wallet-share-anchor"></a>\n <div class="hidden-xs hidden-sm padding text-center" ng-if="!loading">\n\n <button class="button button-stable button-small-padding icon ion-android-share-alt ink" ng-click="showSharePopover($event)" title="{{\'COMMON.BTN_SHARE\' | translate}}">\n </button>\n\n <button class="button button-stable button-small-padding icon ion-loop ink" ng-click="doUpdate()" title="{{\'COMMON.BTN_REFRESH\' | translate}}">\n </button>\n\n <cs-extension-point name="buttons"></cs-extension-point>\n\n &nbsp;&nbsp;\n\n <!--<button id="helptip-wallet-options"\n class="button button-stable icon-right ink"\n ng-click="showActionsPopover($event)">\n &nbsp; <i class="icon ion-android-more-vertical"></i>&nbsp;\n {{:locale:\'COMMON.BTN_OPTIONS\' | translate}}\n </button>-->\n\n <div ng-if="formData.requirements.needRenew">\n <br>\n <button class="button button-raised button-stable ink" ng-click="renewMembership()">\n <span class="assertive">{{:locale:\'ACCOUNT.BTN_MEMBERSHIP_RENEW\' | translate}}</span>\n </button>\n </div>\n </div>\n\n <div class="visible-xs visible-sm padding text-center" ng-if="!loading">\n <button class="button button-assertive button-small-padding ink" ng-click="logout({askConfirm: true})">\n <i class="icon ion-log-out"></i>\n {{\'COMMON.BTN_LOGOUT\' | translate}}\n </button>\n </div>\n\n <div class="row no-padding">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col">\n\n <div class="list" ng-class="::motion.ionListClass" ng-hide="loading">\n\n <span class="item item-divider" translate>WOT.GENERAL_DIVIDER</span>\n\n <!-- Public key\n <span id="helptip-wallet-pubkey"\n class="item item-icon-left item-text-wrap ink"\n on-hold="copy(formData.pubkey)"\n copy-on-click="{{:rebind:formData.pubkey}}">\n <i class="icon ion-key"></i>\n {{:locale:\'COMMO
$templateCache.put('templates/wot/identity.html','<i ng-if="::!identity.avatar" class="item-image icon ion-person"></i>\n<i ng-if="::identity.avatar" class="item-image avatar" style="background-image: url({{::identity.avatar.src}})"></i>\n\n<h2>\n <span ng-bind-html="::identity.name"></span>\n</h2>\n\n<!--<h4 class="gray"-->\n <!--ng-class="{\'pull-right\': !smallscreen}"-->\n <!--ng-if="::identity.sigDate">-->\n <!--<i class="ion-clock"></i>-->\n <!--{{::\'WOT.LOOKUP.REGISTERED\' | translate:identity}}-->\n<!--</h4>-->\n<h4 class="gray" ng-class="{\'pull-right\': !smallscreen}" ng-if="identity.creationTime">\n <i class="ion-clock"></i>\n {{::\'WOT.LOOKUP.MEMBER_FROM\' | translate:{time: identity.creationTime} }}\n</h4>\n<h4 class="gray">\n <span class="positive" ng-if="::identity.city">\n <i class="ion-location"></i>\n {{::identity.city}}&nbsp;\n </span>\n</h4>\n<h4 ng-if="::identity.events||identity.tags">\n <span ng-repeat="event in ::identity.events" class="assertive">\n <i class="ion-alert-circled" ng-if="::!identity.valid"></i>\n <span ng-bind-html="::event.message|translate:event.messageParams"></span>\n </span>\n <span ng-if="::identity.tags" class="dark">\n <ng-repeat ng-repeat="tag in ::identity.tags">\n #<ng-bind-html ng-bind-html="::tag"></ng-bind-html>\n </ng-repeat>\n </span>\n</h4>\n');
$templateCache.put('templates/wot/lookup.html','<ion-view>\n <ion-nav-title>\n {{\'WOT.LOOKUP.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <!--<button class="button button-icon button-clear icon ion-qr-scanner hidden-no-device"-->\n <!--ng-if="$root.device.barcode.enable"-->\n <!--ng-click="scanQrCode()">-->\n <!--</button>-->\n <button class="button button-icon button-clear visible-xs visible-sm" ng-click="showActionsPopover($event)">\n <i class="icon ion-android-funnel"></i>\n </button>\n </ion-nav-buttons>\n\n <ion-content class="padding no-padding-xs" scroll="true">\n <ng-include src="\'templates/wot/lookup_form.html\'">\n </ion-content>\n</ion-view>\n');
$templateCache.put('templates/wot/lookup_form.html','<div class="lookupForm">\n\n <div class="item no-padding">\n\n <div class="double-padding-x padding-top-xs item-text-wrap" ng-if="::allowMultiple" style="height: 36px">\n\n <div class="gray padding-top" ng-if="!selection.length && parameters.help">{{::parameters.help|translate}}</div>\n\n <div ng-repeat="identity in selection track by identity.id" class="button button-small button-text button-stable button-icon-event ink" ng-class="{\'button-text-positive\': identity.selected}">\n <span ng-bind-html="identity.name||identity.uid||(identity.pubkey|formatPubkey)"></span>\n <i class="icon ion-close" ng-click="removeSelection(identity, $event)">&nbsp;&nbsp;</i>\n </div>\n\n </div>\n\n <div class="item-input">\n <i class="icon ion-search placeholder-icon"></i>\n\n <input type="text" class="visible-xs visible-sm" placeholder="{{\'WOT.SEARCH_HELP\'|translate}}" ng-model="search.text" ng-model-options="{ debounce: 650 }" ng-change="doSearch()">\n <input type="text" class="hidden-xs hidden-sm" id="{{wotSearchTextId}}" placeholder="{{\'WOT.SEARCH_HELP\'|translate}}" ng-model="search.text" on-return="doSearchText()">\n <div class="helptip-anchor-center">\n <a id="helptip-wot-search-text"></a>\n </div>\n </div>\n </div>\n\n <div class="padding-top padding-xs" style="display: block; height: 60px" ng-class="::{\'hidden-xs\': !showResultLabel}">\n <div class="pull-left" ng-if="!search.loading && showResultLabel">\n <h4 ng-if="search.type==\'newcomers\'" translate>\n WOT.LOOKUP.NEWCOMERS\n </h4>\n <!--<h4-->\n <!--ng-if="search.type==\'pending\'" translate>-->\n <!--WOT.LOOKUP.PENDING-->\n <!--</h4>-->\n <h4 ng-if="search.type==\'text\'" translate>\n COMMON.RESULTS_LIST\n </h4>\n </div>\n\n <div class="pull-right hidden-xs hidden-sm">\n <a ng-if="enableFilter" class="button button-text button-small ink" ng-class="{\'button-text-positive\': search.type==\'newcomers\'}" ng-click="doGetNewcomers()">\n <i class="icon ion-person-stalker"></i>\n {{\'WOT.LOOKUP.BTN_NEWCOMERS\' | translate}}\n </a>\n &nbsp;\n <!--<a ng-if="enableFilter"-->\n <!--class="button button-text button-small"-->\n <!--ng-class="{\'button-text-positive\': search.type==\'pending\'}"-->\n <!--ng-click="doGetPending()" class="badge-balanced">-->\n <!--<i class="icon ion-clock"></i>-->\n <!--{{\'WOT.LOOKUP.BTN_PENDING\' | translate}}-->\n <!--</a>-->\n &nbsp;\n <button class="button button-small button-stable ink" ng-click="doSearch()">\n {{\'COMMON.BTN_SEARCH\' | translate}}\n </button>\n\n <button class="button button-small button-positive {{parameters.okType}} ink" ng-if="::allowMultiple" ng-disabled="!selection.length" ng-click="next()">\n {{parameters.okText||\'COMMON.BTN_NEXT\' | translate}}\n </button>\n </div>\n </div>\n\n <div class="text-center" ng-if="search.loading">\n <p class="gray" ng-if="::$root.currency.initPhase" translate>WOT.SEARCH_INIT_PHASE_WARNING</p>\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <ng-if ng-if="!search.loading">\n <div class="assertive padding" ng-if="!search.results.length">\n <span ng-if="search.type==\'text\'" translate>COMMON.SEARCH_NO_RESULT</span>\n <span ng-if="search.type==\'newcomers\'" translate>WOT.LOOKUP.NO_NEWCOMERS</span>\n </div>\n\n <!-- simple selection + device -->\n \n\n <!-- simple selection + no device -->\n <!--removeIf(device)-->\n <div ng-if="!allowMultiple" class="list {{::motion.ionListClass}}">\n\n <div ng-repeat="identity in search.results" id="helptip-wot-search-result-{{$index}}" class="item item-border-large item-avatar item-icon-right ink" ng-click="::select(identity)">\n\n <ng-include src="\'templates/wot/identity.html\'"></ng-include>\n\n <i class="icon ion-ios-arrow-right"></i>\n </div>\n </div>\n <!--endRemove
$templateCache.put('templates/wot/lookup_popover_actions.html','<ion-popover-view class="fit has-header visible-sm visible-xs">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_FILTER_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink" ng-click="doGetNewcomers()">\n <i class="icon ion-person"></i>\n {{\'WOT.LOOKUP.BTN_NEWCOMERS\' | translate}}\n </a>\n\n <a class="item item-icon-left ink" ng-click="doGetPending()">\n <i class="icon ion-clock"></i>\n {{\'WOT.LOOKUP.BTN_PENDING\' | translate}}\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('templates/wot/modal_lookup.html','<ion-modal-view id="wotLookup" class="modal-full-height">\n\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate="">COMMON.BTN_CANCEL</button>\n\n <h1 class="title hidden-xs">\n {{::parameters.title?parameters.title:\'WOT.MODAL.TITLE\'|translate}}\n </h1>\n\n <button class="button button-clear icon-right visible-xs ink" ng-if="allowMultiple && selection.length" ng-click="closeModal(selection)">\n {{::parameters.okText||\'COMMON.BTN_NEXT\' | translate}}\n <i ng-if="::!parameters.okText||parameters.okIcon" class="icon {{::parameters.okIcon||\'ion-ios-arrow-right\'}}"></i>\n </button>\n </ion-header-bar>\n\n <ion-content class="padding no-padding-xs" scroll="true">\n\n <div class="visible-xs visible-sm text-right stable-bg stable">\n \n <button class="button button-icon button-small-padding dark ink" ng-click="showActionsPopover($event)">\n <i class="icon ion-android-funnel"></i>\n </button>\n </div>\n\n <ng-include src="\'templates/wot/lookup_form.html\'"></ng-include>\n </ion-content>\n</ion-modal-view>\n');
$templateCache.put('templates/wot/view_identity.html','<ion-view left-buttons="leftButtons" class="view-identity">\n <ion-nav-title>\n </ion-nav-title>\n\n <ion-content scroll="true">\n\n <div class="hero dark-bg" ng-class="{\'positive-900-bg\': !loading && formData.isMember}">\n <div class="content" ng-if="!loading">\n <i class="avatar" ng-if="::!formData.avatar" ng-class="{\'avatar-wallet\': !formData.isMember, \'avatar-member\': formData.isMember}"></i>\n <i class="avatar" ng-if="::formData.avatar" style="background-image: url({{::formData.avatar.src}})"></i>\n <ng-if ng-if="::formData.name" title="{{::formData.name}}">\n <h3 class="light">{{::formData.name|truncText: 30}}</h3>\n </ng-if>\n <ng-if ng-if="::!formData.name">\n <h3 class="light" ng-if="::formData.uid">{{::formData.uid}}</h3>\n <h3 class="light" ng-if="::!formData.uid"><i class="ion-key"></i> {{::formData.pubkey | formatPubkey}}</h3>\n </ng-if>\n <!--<h4 class="assertive">\n <ng-if ng-if="::(formData.name || formData.uid) && !formData.isMember && !revoked" translate>WOT.NOT_MEMBER_PARENTHESIS</ng-if>\n <ng-if ng-if="::(formData.name || formData.uid) && !formData.isMember && revoked" translate>WOT.IDENTITY_REVOKED_PARENTHESIS</ng-if>\n <ng-if ng-if="::(formData.name || formData.uid) && formData.isMember && revoked" translate>WOT.MEMBER_PENDING_REVOCATION_PARENTHESIS</ng-if>\n </h4-->\n <cs-extension-point name="hero"></cs-extension-point>\n </div>\n <h4 class="content light" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </h4>\n\n\n </div>\n\n <!-- button bar-->\n <a id="wot-share-anchor-{{::formData.pubkey}}"></a>\n <div class="hidden-xs hidden-sm padding text-center">\n <button class="button button-stable button-small-padding icon ion-android-share-alt ink" ng-click="showSharePopover($event)" title="{{\'COMMON.BTN_SHARE\' | translate}}">\n </button>\n\n <!-- Allow extension here -->\n <cs-extension-point name="buttons"></cs-extension-point>\n\n <!-- <button class="button button-stable button-small-padding icon ion-ribbon-b ink"\n ng-click="certify()"\n ng-if=":rebind:formData.hasSelf"\n title="{{\'WOT.BTN_CERTIFY\' | translate}}"\n ng-disabled="disableCertifyButton">\n </button>-->\n\n <!--<button class="button button-calm ink"\n ng-click="showTransferModal({pubkey:formData.pubkey, uid: formData.name||formData.uid})">\n {{\'COMMON.BTN_SEND_MONEY\' | translate}}\n </button>-->\n </div>\n\n <div class="row no-padding">\n\n <div class="visible-xs visible-sm">\n <!--<button id="fab-certify-{{:rebind:formData.uid}}" style="top: 170px;"\n class="button button-fab button-fab-top-left button-fab-hero button-calm spin"\n ng-if=":rebind:(canCertify && !alreadyCertified)"\n ng-click="certify()">\n <i class="icon ion-ribbon-b"></i>\n </button>-->\n\n <cs-extension-point name="buttons-top-fab"></cs-extension-point>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col list" ng-class="::motion.ionListClass" bind-notifier="{ rebind:loading}">\n\n <span class="item item-divider" translate>WOT.GENERAL_DIVIDER</span>\n\n <!-- Pubkey\n <ion-item class="item-icon-left item-text-wrap ink"\n copy-on-click="{{:rebind:formData.pubkey}}">\n <i class="icon ion-key"></i>\n <span translate>COMMON.PUBKEY</span>\n <h4 id="pubkey" class="dark text-left">{{:rebind:formData.pubkey}}</h4>\n </ion-item>-->\n\n <!--<div class="item item-icon-left item-text-wrap"\n ng-if=":rebind:!formData.hasSelf">\n <i class="icon ion-ios-help-outline positive"></i>\n <span translate>WOT.NOT_MEMBER_ACCOUNT</span>\n <h4 class="gray" translate>WOT.NOT_MEMBER_AC
$templateCache.put('templates/wot/view_identity_tx.html','<ion-view left-buttons="leftButtons">\n <ion-nav-title>\n <span class="visible-xs visible-sm">{{::formData.name||formData.uid}}</span>\n <span class="hidden-xs hidden-sm" ng-if="!loading" translate="WOT.OPERATIONS.TITLE" translate-values="{uid: formData.name || formData.uid}"></span>\n </ion-nav-title>\n\n <ion-content>\n\n <div class="hidden-xs hidden-sm padding text-center" ng-if="!loading">\n\n <button class="button button-stable button-small-padding icon ion-loop ink" ng-click="doUpdate()" title="{{\'COMMON.BTN_REFRESH\' | translate}}">\n </button>\n\n <button class="button button-stable button-small-padding icon ion-android-download ink" ng-click="downloadHistoryFile()" title="{{\'COMMON.BTN_DOWNLOAD_ACCOUNT_STATEMENT\' | translate}}">\n </button>\n\n <cs-extension-point name="buttons"></cs-extension-point>\n\n </div>\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list {{motion.ionListClass}}" ng-if="!loading">\n\n <div class="row">\n\n <div class="col col-15 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col">\n\n <!-- the balance -->\n <div class="item item-divider item-tx">\n {{\'ACCOUNT.BALANCE_ACCOUNT\'|translate}}\n <div class="badge badge-balanced">\n {{balance|formatAmount}} <span ng-bind-html="$root.currency.name|currencySymbol"></span>\n </div>\n </div>\n\n <span class="item item-divider" ng-if="!loading">\n {{:locale:\'ACCOUNT.LAST_TX\'|translate}}\n <a id="helptip-wallet-tx" style="position: relative; bottom: 0; right: 0px">&nbsp;</a>\n </span>\n\n <!-- iterate on each TX -->\n <div class="item item-tx item-icon-left" ng-repeat="tx in history" ng-include="\'templates/wallet/item_tx.html\'">\n </div>\n\n <div class="item item-text-wrap text-center" ng-if="tx.fromTime > 0">\n <p>\n <a ng-click="showMoreTx()">{{:locale:\'ACCOUNT.SHOW_MORE_TX\'|translate}}</a>\n <span class="gray" translate="ACCOUNT.TX_FROM_DATE" translate-values="{fromTime: tx.fromTime}"></span>\n <span class="gray">|</span>\n <a ng-click="showMoreTx(-1)" translate>ACCOUNT.SHOW_ALL_TX</a>\n </p>\n </div>\n </div>\n\n <div class="col col-15 hidden-xs hidden-sm">&nbsp;</div>\n\n </div>\n </div>\n </ion-content>\n</ion-view>\n');}]);
angular.module("cesium.translations", []).config(["$translateProvider", function($translateProvider) {
$translateProvider.translations("en-GB", {
"COMMON": {
"APP_NAME": "ğ<b>change</b>",
"APP_VERSION": "v{{version}}",
"APP_BUILD": "build {{build}}",
"PUBKEY": "Public key",
"MEMBER": "Member",
"BLOCK" : "Block",
"BTN_OK": "OK",
"BTN_YES": "Yes",
"BTN_NO": "No",
"BTN_SEND": "Send",
"BTN_SEND_MONEY": "Transfer money",
"BTN_SEND_MONEY_SHORT": "Transfer",
"BTN_SAVE": "Save",
"BTN_YES_SAVE": "Yes, Save",
"BTN_YES_CONTINUE": "Yes, Continue",
"BTN_SHOW": "Show",
"BTN_SHOW_PUBKEY": "Show key",
"BTN_RELATIVE_UNIT": "Display amounts in UD?",
"BTN_BACK": "Back",
"BTN_NEXT": "Next",
"BTN_IMPORT": "Import",
"BTN_CANCEL": "Cancel",
"BTN_CLOSE": "Close",
"BTN_LATER": "Later",
"BTN_LOGIN": "Sign In",
"BTN_LOGOUT": "Logout",
"BTN_ADD_ACCOUNT": "New Account",
"BTN_SHARE": "Share",
"BTN_EDIT": "Edit",
"BTN_DELETE": "Delete",
"BTN_ADD": "Add",
"BTN_SEARCH": "Search",
"BTN_REFRESH": "Refresh",
"BTN_RETRY": "Retry",
"BTN_START": "Start",
"BTN_CONTINUE": "Continue",
"BTN_CREATE": "Create",
"BTN_UNDERSTOOD": "I understand",
"BTN_OPTIONS": "Options",
"BTN_HELP_TOUR": "Features tour",
"BTN_HELP_TOUR_SCREEN": "Discover this screen",
"BTN_DOWNLOAD": "Download",
"BTN_DOWNLOAD_ACCOUNT_STATEMENT": "Download account statement",
"BTN_MODIFY": "Modify",
"CHOOSE_FILE": "Drag your file<br/>or click to select",
"DAYS": "days",
"NO_ACCOUNT_QUESTION": "Not a member yet? Register now!",
"SEARCH_NO_RESULT": "No result found",
"LOADING": "Loading...",
"LOADING_WAIT": "Loading...<br/><small>(Waiting for node availability)</small>",
"SEARCHING": "Searching...",
"FROM": "From",
"TO": "To",
"COPY": "Copy",
"LANGUAGE": "Language",
"UNIVERSAL_DIVIDEND": "Universal dividend",
"UD": "UD",
"DATE_PATTERN": "DD/MM/YYYY HH:mm",
"DATE_FILE_PATTERN": "YYYY-MM-DD",
"DATE_SHORT_PATTERN": "DD/MM/YY",
"DATE_MONTH_YEAR_PATTERN": "MM/YYYY",
"EMPTY_PARENTHESIS": "(empty)",
"UID": "Pseudonym",
"ENABLE": "Enabled",
"DISABLE": "Disabled",
"RESULTS_LIST": "Results:",
"RESULTS_COUNT": "{{count}} results",
"EXECUTION_TIME": "executed in {{duration|formatDurationMs}}",
"SHOW_VALUES": "Display values openly?",
"POPOVER_ACTIONS_TITLE": "Options",
"POPOVER_FILTER_TITLE": "Filters",
"SHOW_MORE": "Show more",
"SHOW_MORE_COUNT": "(current limit at {{limit}})",
"POPOVER_SHARE": {
"TITLE": "Share",
"SHARE_ON_TWITTER": "Share on Twitter",
"SHARE_ON_FACEBOOK": "Share on Facebook",
"SHARE_ON_DIASPORA": "Share on Diaspora*",
"SHARE_ON_GOOGLEPLUS": "Share on Google+"
},
"FILE": {
"DATE" : "Date:",
"TYPE" : "Type:",
"SIZE": "Size:",
"VALIDATING": "Validating..."
}
},
"SYSTEM": {
"PICTURE_CHOOSE_TYPE": "Choose source:",
"BTN_PICTURE_GALLERY": "Gallery",
"BTN_PICTURE_CAMERA": "<b>Camera</b>"
},
"MENU": {
"HOME": "Home",
"WOT": "Registry",
"CURRENCY": "Currency",
"ACCOUNT": "My Account",
"WALLETS": "My wallets",
"TRANSFER": "Transfer",
"SCAN": "Scan",
"SETTINGS": "Settings",
"NETWORK": "Network",
"TRANSACTIONS": "My transactions"
},
"ABOUT": {
"TITLE": "About",
"LICENSE": "<b>Free/libre software</b> (License GNU AGPLv3).",
"LATEST_RELEASE": "There is a <b>newer version</ b> of {{'COMMON.APP_NAME' | translate}} (<b>v{{version}}</b>)",
"PLEASE_UPDATE": "Please update {{'COMMON.APP_NAME' | translate}} (latest version: <b>v{{version}}</b>)",
"CODE": "Source code:",
"OFFICIAL_WEB_SITE": "Official web site:",
"DEVELOPERS": "Developers:",
"FORUM": "Forum:",
"DEV_WARNING": "Warning",
"DEV_WARNING_MESSAGE": "This application is still in active development.<br/>Please report any issue to us!",
"DEV_WARNING_MESSAGE_SHORT": "This App is still unstable (still under development).",
"REPORT_ISSUE": "Report an issue"
},
"HOME": {
"TITLE": "Cesium",
"MESSAGE": "Welcome to the {{'COMMON.APP_NAME'|translate}} Application!",
"MESSAGE_CURRENCY": "Make exchanges in the libre currency {{currency|abbreviate}}!",
"BTN_CURRENCY": "Explore currency",
"BTN_ABOUT": "about",
"BTN_HELP": "Help",
"REPORT_ISSUE": "Report an issue",
"NOT_YOUR_ACCOUNT_QUESTION" : "You do not own the account <b><i class=\"ion-key\"></i> {{pubkey|formatPubkey}}</b>?",
"BTN_CHANGE_ACCOUNT": "Disconnect this account",
"CONNECTION_ERROR": "Peer <b>{{server}}</b> unreachable or invalid address.<br/><br/>Check your Internet connection, or change node <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in the settings</a>."
},
"SETTINGS": {
"TITLE": "Settings",
"DISPLAY_DIVIDER": "Display",
"STORAGE_DIVIDER": "Storage",
"NETWORK_SETTINGS": "Network",
"PEER": "Duniter peer address",
"PEER_SHORT": "Peer address",
"PEER_CHANGED_TEMPORARY": "Address used temporarily",
"USE_LOCAL_STORAGE": "Enable local storage",
"USE_LOCAL_STORAGE_HELP": "Allows you to save your settings",
"WALLETS_SETTINGS": "My wallets",
"USE_WALLETS_ENCRYPTION": "Secure the list",
"USE_WALLETS_ENCRYPTION_HELP": "Enables you to encrypt the list of your wallets. Authentication required to access it.",
"ENABLE_HELPTIP": "Enable contextual help tips",
"ENABLE_UI_EFFECTS": "Enable visual effects",
"HISTORY_SETTINGS": "Account operations",
"DISPLAY_UD_HISTORY": "Display produced dividends?",
"TX_HISTORY_AUTO_REFRESH": "Enable automatic refresh?",
"TX_HISTORY_AUTO_REFRESH_HELP": "Updates the list of operations to each new block.",
"AUTHENTICATION_SETTINGS": "Authentication",
"KEEP_AUTH": "Expiration of authentication",
"KEEP_AUTH_SHORT": "Expiration",
"KEEP_AUTH_HELP": "Define when authentication is cleared from memory.",
"KEEP_AUTH_OPTION": {
"NEVER": "After each operation",
"SECONDS": "After {{value}}s of inactivity",
"MINUTE": "After {{value}}min of inactivity",
"MINUTES": "After {{value}}min of inactivity",
"HOUR": "After {{value}}h of inactivity",
"ALWAYS": "At the end of the session"
},
"KEYRING_FILE": "Keyring file",
"KEYRING_FILE_HELP": "Allow auto-connect at startup, or to authenticate (only if \"Expiration of authentication\" is \"at the end of the session\"",
"REMEMBER_ME": "Remember me ?",
"REMEMBER_ME_HELP": "Allows to remain identified from one session to another, keeping the public key locally.",
"PLUGINS_SETTINGS": "Extensions",
"BTN_RESET": "Restore default values",
"EXPERT_MODE": "Enable expert mode",
"EXPERT_MODE_HELP": "Allow to see more details",
"BLOCK_VALIDITY_WINDOW": "Block uncertainty time",
"BLOCK_VALIDITY_WINDOW_SHORT": "Time of uncertainty",
"BLOCK_VALIDITY_WINDOW_HELP": "Time to wait before considering an information is validated",
"BLOCK_VALIDITY_OPTION": {
"NONE": "No delay",
"N": "{{time | formatDuration}} ({{count}} blocks)"
},
"POPUP_PEER": {
"TITLE" : "Duniter peer",
"HOST" : "Address",
"HOST_HELP": "Address: server:port",
"USE_SSL" : "Secured?",
"USE_SSL_HELP" : "(SSL Encryption)",
"BTN_SHOW_LIST" : "Peer's list"
}
},
"BLOCKCHAIN": {
"HASH": "Hash: {{hash}}",
"VIEW": {
"HEADER_TITLE": "Block #{{number}}-{{hash|formatHash}}",
"TITLE_CURRENT": "Current block",
"TITLE": "Block #{{number|formatInteger}}",
"COMPUTED_BY": "Computed by",
"SHOW_RAW": "Show raw data",
"TECHNICAL_DIVIDER": "Technical informations",
"VERSION": "Format version",
"HASH": "Computed hash",
"UNIVERSAL_DIVIDEND_HELP": "Money co-produced by each of the {{membersCount}} members",
"EMPTY": "Aucune donnée dans ce bloc",
"POW_MIN": "Minimal difficulty",
"POW_MIN_HELP": "Difficulty imposed in calculating hash",
"DATA_DIVIDER": "Data",
"IDENTITIES_COUNT": "New identities",
"JOINERS_COUNT": "Joiners",
"ACTIVES_COUNT": "Renewals",
"ACTIVES_COUNT_HELP": "Members having renewed their membership",
"LEAVERS_COUNT": "Leavers",
"LEAVERS_COUNT_HELP": "Members that now refused certification",
"EXCLUDED_COUNT": "Excluded members",
"EXCLUDED_COUNT_HELP": "Old members, excluded because missing membreship renewal or certifications",
"REVOKED_COUNT": "Revoked identities",
"REVOKED_COUNT_HELP": "These accounts may no longer be member",
"TX_COUNT": "Transactions",
"CERT_COUNT": "Certifications",
"TX_TO_HIMSELF": "Change",
"TX_OUTPUT_UNLOCK_CONDITIONS": "Unlock conditions",
"TX_OUTPUT_OPERATOR": {
"AND": "and",
"OR": "or"
},
"TX_OUTPUT_FUNCTION": {
"SIG": "<b>Sign</b> of the public key",
"XHX": "<b>Password</b>, including SHA256 =",
"CSV": "Blocked during",
"CLTV": "Bloqué until"
}
},
"LOOKUP": {
"TITLE": "Blocks",
"NO_BLOCK": "No bloc",
"LAST_BLOCKS": "Last blocks:",
"BTN_COMPACT": "Compact"
}
},
"CURRENCY": {
"VIEW": {
"TITLE": "Currency",
"TAB_CURRENCY": "Currency",
"TAB_WOT": "Web of trust",
"TAB_NETWORK": "Network",
"TAB_BLOCKS": "Blocks",
"CURRENCY_SHORT_DESCRIPTION": "{{currency|capitalize}} is a <b>libre money</b>, started {{firstBlockTime | formatFromNow}}. It currently counts <b>{{N}} members </b>, who produce and collect a <a ng-click=\"showHelpModal('ud')\">Universal Dividend</a> (DU), each {{dt | formatPeriod}}.",
"NETWORK_RULES_DIVIDER": "Network rules",
"CURRENCY_NAME": "Currency name",
"MEMBERS": "Members count",
"MEMBERS_VARIATION": "Variation since {{duration|formatDuration}} (since last UD)",
"MONEY_DIVIDER": "Money",
"MASS": "Monetary mass",
"SHARE": "Money share",
"UD": "Universal Dividend",
"C_ACTUAL": "Current growth",
"MEDIAN_TIME": "Current blockchain time",
"POW_MIN": "Common difficulty",
"MONEY_RULES_DIVIDER": "Rules of currency",
"C_RULE": "Theoretical growth target",
"UD_RULE": "Universal dividend (formula)",
"DT_REEVAL": "Period between two re-evaluation of the UD",
"REEVAL_SYMBOL": "reeval",
"DT_REEVAL_VALUE": "Every <b>{{dtReeval|formatDuration}}</b> ({{dtReeval/86400}} {{'COMMON.DAYS'|translate}})",
"UD_REEVAL_TIME0": "Date of first reevaluation of the UD",
"SIG_QTY_RULE": "Required number of certifications to become a member",
"SIG_STOCK": "Maximum number of certifications sent by a member",
"SIG_PERIOD": "Minimum delay between 2 certifications sent by one and the same issuer.",
"SIG_WINDOW": "Maximum delay before a certification will be treated",
"SIG_VALIDITY": "Lifetime of a certification that has been treated",
"MS_WINDOW": "Maximum delay before a pending membership will be treated",
"MS_VALIDITY": "Lifetime of a membership that has been treated",
"STEP_MAX": "Maximum distance between a newcomer and each referring members.",
"WOT_RULES_DIVIDER": "Rules for web of trust",
"SENTRIES": "Required number of certifications (given <b>and</b> received) to become a referring member",
"SENTRIES_FORMULA": "Required number of certifications to become a referring member (formula)",
"XPERCENT":"Minimum percent of referring member to reach to match the distance rule",
"AVG_GEN_TIME": "The average time between 2 blocks",
"CURRENT": "current",
"MATH_CEILING": "CEILING",
"DISPLAY_ALL_RULES": "Display all rules?",
"BTN_SHOW_LICENSE": "Show license",
"WOT_DIVIDER": "Web of trust"
},
"LICENSE": {
"TITLE": "Currency license",
"BTN_DOWNLOAD": "Download file",
"NO_LICENSE_FILE": "License file not found."
}
},
"NETWORK": {
"VIEW": {
"MEDIAN_TIME": "Blockchain time",
"LOADING_PEERS": "Loading peers...",
"NODE_ADDRESS": "Address:",
"SOFTWARE": "Software:",
"WARN_PRE_RELEASE": "Pre-release (latest stable: <b>{{version}}</b>)",
"WARN_NEW_RELEASE": "Version <b>{{version}}</b> available",
"WS2PID": "Identifier:",
"PRIVATE_ACCESS": "Private access",
"POW_PREFIX": "Proof of work prefix:",
"ENDPOINTS": {
"BMAS": "Secure endpoint (SSL)",
"BMATOR": "TOR endpoint",
"WS2P": "WS2P endpoint",
"ES_USER_API": "Cesium+ data node"
}
},
"INFO": {
"ONLY_SSL_PEERS": "Non-SSL nodes have a degraded display because Cesium works in HTTPS mode."
}
},
"PEER": {
"PEERS": "Peers",
"SIGNED_ON_BLOCK": "Signed on block",
"MIRROR": "mirror",
"MIRRORS": "Mirrors",
"MIRROR_PEERS": "Mirror peers",
"PEER_LIST" : "Peer's list",
"MEMBERS" : "Members",
"MEMBER_PEERS" : "Member peers",
"ALL_PEERS" : "All peers",
"DIFFICULTY" : "Difficulty",
"API" : "API",
"CURRENT_BLOCK" : "Block #",
"POPOVER_FILTER_TITLE": "Filter",
"OFFLINE": "Offline",
"OFFLINE_PEERS": "Offline peers",
"BTN_SHOW_PEER": "Show peer",
"VIEW": {
"TITLE": "Peer",
"OWNER": "Owned by ",
"SHOW_RAW_PEERING": "See peering document",
"SHOW_RAW_CURRENT_BLOCK": "See current block (raw format)",
"LAST_BLOCKS": "Last blocks",
"KNOWN_PEERS": "Known peers :",
"GENERAL_DIVIDER": "General information",
"ERROR": {
"LOADING_TOR_NODE_ERROR": "Could not get peer data, using the TOR network.",
"LOADING_NODE_ERROR": "Could not get peer data"
}
}
},
"WOT": {
"SEARCH_HELP": "Search (member or public key)",
"SEARCH_INIT_PHASE_WARNING": "During the pre-registration phase, the search for pending registrations <b>may be long</b>. Please wait ...",
"REGISTERED_SINCE": "Registered on",
"REGISTERED_SINCE_BLOCK": "Registered since block #",
"NO_CERTIFICATION": "No validated certification",
"NO_GIVEN_CERTIFICATION": "No given certification",
"NOT_MEMBER_PARENTHESIS": "(non-member)",
"IDENTITY_REVOKED_PARENTHESIS": "(identity revoked)",
"MEMBER_PENDING_REVOCATION_PARENTHESIS": "(being revoked)",
"EXPIRE_IN": "Expires",
"NOT_WRITTEN_EXPIRE_IN": "Deadline<br/>treatment",
"EXPIRED": "Expired",
"PSEUDO": "Pseudonym",
"SIGNED_ON_BLOCK": "Emitted on block #{{block}}",
"WRITTEN_ON_BLOCK": "Written on block #{{block}}",
"GENERAL_DIVIDER": "General information",
"NOT_MEMBER_ACCOUNT": "Non-member account",
"NOT_MEMBER_ACCOUNT_HELP": "This is a simple wallet, with no pending membership application.",
"TECHNICAL_DIVIDER": "Technical data",
"BTN_CERTIFY": "Certify",
"BTN_YES_CERTIFY": "Yes, certify",
"BTN_SELECT_AND_CERTIFY": "New certification",
"ACCOUNT_OPERATIONS": "Account operations",
"VIEW": {
"POPOVER_SHARE_TITLE": "Identity {{title}}"
},
"LOOKUP": {
"TITLE": "Registry",
"NEWCOMERS": "New members:",
"NEWCOMERS_COUNT": "{{count}} members",
"PENDING": "Pending registrations:",
"PENDING_COUNT": "{{count}} pending registrations",
"REGISTERED": "Registered {{time | formatFromNow}}",
"MEMBER_FROM": "Member since {{time|formatFromNowShort}}",
"BTN_NEWCOMERS": "Latest members",
"BTN_PENDING": "Pending registrations",
"SHOW_MORE": "Show more",
"SHOW_MORE_COUNT": "(current limit to {{limit}})",
"NO_PENDING": "No pending registrations.",
"NO_NEWCOMERS": "No members."
},
"CONTACTS": {
"TITLE": "Contacts"
},
"MODAL": {
"TITLE": "Search"
},
"CERTIFICATIONS": {
"TITLE": "{{uid}} - Certifications",
"SUMMARY": "Received certifications",
"LIST": "Details of received certifications",
"PENDING_LIST": "Pending certifications",
"RECEIVED": "Received certifications",
"RECEIVED_BY": "Certifications received by {{uid}}",
"ERROR": "Received certifications in error",
"SENTRY_MEMBER": "Referring member"
},
"OPERATIONS": {
"TITLE": "{{uid}} - Operations"
},
"GIVEN_CERTIFICATIONS": {
"TITLE": "{{uid}} - Certifications sent",
"SUMMARY": "Sent certifications",
"LIST": "Details of sent certifications",
"PENDING_LIST": "Pending certifications",
"SENT": "Sent certifications",
"SENT_BY": "Certifications sent by {{uid}}",
"ERROR": "Sent certifications with error"
}
},
"LOGIN": {
"TITLE": "<i class=\"icon ion-log-in\"></i> Login",
"SCRYPT_FORM_HELP": "Please enter your credentials. <br> Remember to check the public key for your account.",
"PUBKEY_FORM_HELP": "Please enter a public account key:",
"FILE_FORM_HELP": "Choose the keychain file to use:",
"SCAN_FORM_HELP": "Scan the QR code of a wallet.",
"SALT": "Identifier",
"SALT_HELP": "Identifier",
"SHOW_SALT": "Display identifier?",
"PASSWORD": "Password",
"PASSWORD_HELP": "Password",
"PUBKEY_HELP": "Example: « AbsxSY4qoZRzyV2irfep1V9xw1EMNyKJw2TkuVD4N1mv »",
"NO_ACCOUNT_QUESTION": "Don't have an account yet?",
"HAVE_ACCOUNT_QUESTION": "Already have an account ?",
"CREATE_ACCOUNT": "Create an account",
"CREATE_FREE_ACCOUNT": "Create a free account",
"FORGOTTEN_ID": "Forgot password?",
"ASSOCIATED_PUBKEY": "Public key :",
"BTN_METHODS": "Other methods",
"BTN_METHODS_DOTS": "Change method...",
"METHOD_POPOVER_TITLE": "Methods",
"MEMORIZE_AUTH_FILE": "Memorize this keychain during the navigation session",
"SCRYPT_PARAMETERS": "Paramètres (Scrypt) :",
"AUTO_LOGOUT": {
"TITLE": "Information",
"MESSAGE": "<i class=\"ion-android-time\"></i> You were <b>logout</ b> automatically, due to prolonged inactivity.",
"BTN_RELOGIN": "Sign In",
"IDLE_WARNING": "You will be logout... {{countdown}}"
},
"METHOD": {
"SCRYPT_DEFAULT": "Secret and password",
"SCRYPT_ADVANCED": "Advanced salt",
"FILE": "Keychain file",
"PUBKEY": "Public key only",
"SCAN": "Scan a QR code"
},
"SCRYPT": {
"SIMPLE": "Light salt",
"DEFAULT": "Standard salt",
"SECURE": "Secure salt",
"HARDEST": "Hardest salt",
"EXTREME": "Extreme salt",
"USER": "Personal value",
"N": "N (Loop):",
"r": "r (RAM):",
"p": "p (CPU):"
},
"FILE": {
"DATE" : "Date:",
"TYPE" : "Type:",
"SIZE": "Size:",
"VALIDATING": "Validating...",
"HELP": "Expected file format: <b>.dunikey</b> (type PubSec). Other formats are under development (EWIF, WIF)."
}
},
"AUTH": {
"TITLE": "<i class=\"icon ion-locked\"></i> Authentification",
"METHOD_LABEL": "Méthode d'authentification",
"BTN_AUTH": "S'authentifier",
"SCRYPT_FORM_HELP": "Veuillez vous authentifier :",
"ERROR": {
"SCRYPT_DEFAULT": "Sallage simple (par défaut)",
"SCRYPT_ADVANCED": "Sallage avancé",
"FILE": "Fichier de trousseau"
}
},
"ACCOUNT": {
"TITLE": "My Account",
"BALANCE": "Balance",
"LAST_TX": "Latest validated transactions",
"BALANCE_ACCOUNT": "Account balance",
"NO_TX": "No transaction",
"SHOW_MORE_TX": "Show more",
"SHOW_ALL_TX": "Show all",
"TX_FROM_DATE": "(current limit to {{fromTime|medianFromNowShort}})",
"PENDING_TX": "Pending transactions",
"VALIDATING_TX": "Transactions being validated",
"ERROR_TX": "Transaction not executed",
"ERROR_TX_SENT": "Sent transactions",
"PENDING_TX_RECEIVED": "Transactions awaiting receipt",
"EVENTS": "Events",
"OUT_DISTANCED": "Your current certifications come from a group too isolated from the <a ng-click=\"showHelpModal('wot')\"> Web of Trust</a> (WoT): the <a ng-click=\"showHelpModal('distance_rule')\">maximum distance rule</a> is violated.<br/>You must obtain certifications from another area of the Web of Trust, or wait for it to tighten.",
"WAITING_MEMBERSHIP": "Membership application sent. Waiting validation.",
"WAITING_CERTIFICATIONS": "You need {{needCertificationCount}} certification(s) to become a member and produce the <a ng-click=\"showHelpModal('ud')\">Universal Dividend</a>. Your account is however already operational, to receive and send payments.",
"WAITING_CERTIFICATIONS_HELP": "To get your certifications, only request members <b>who know you enough</b>, as required by <a ng-click=\"showLicenseModal()\">the currency license</a> that you have accepted.<br/>If you do not know enough members, let them know on <a ng-click=\"openLink($event, $root.settings.userForumUrl)\">the user forum</a>.",
"WILL_MISSING_CERTIFICATIONS": "You will <b>lack certifications</b> soon (at least {{willNeedCertificationCount}} more are needed)",
"WILL_NEED_RENEW_MEMBERSHIP": "Your membership <b>will expire {{membershipExpiresIn|formatDurationTo}}</b>. Remember to <a ng-click=\"doQuickFix('renew')\">renew your membership</a> before then.",
"NEED_RENEW_MEMBERSHIP": "You are no longer a member because your membership <b>has expired</b>. Remember to <a ng-click=\"doQuickFix('renew')\">renew your membership</a>.",
"NEED_RENEW_MEMBERSHIP_AFTER_CANCELLED": "You are no longer a member because your membership <b>has been canceled</b> for lack of certifications. Remember to <a ng-click=\"doQuickFix('renew')\">renew your membership</a>.",
"NO_WAITING_MEMBERSHIP": "No membership application pending. If you'd like to <b>become a member</ b>, please <a ng-click=\"doQuickFix('membership')\">send the membership application</a>.",
"CERTIFICATION_COUNT": "Received certifications",
"CERTIFICATION_COUNT_SHORT": "Certifications",
"SIG_STOCK": "Stock of certifications to give",
"BTN_RECEIVE_MONEY": "Receive",
"BTN_SELECT_ALTERNATIVES_IDENTITIES": "Switch to another identity...",
"BTN_FIX_MEMBERSHIP": "Resubmit membership request...",
"BTN_MEMBERSHIP_RENEW": "Renew membership",
"BTN_MEMBERSHIP_RENEW_DOTS": "Renew membership...",
"BTN_MEMBERSHIP_OUT_DOTS": "Revoke membership...",
"BTN_SECURITY_DOTS": "Sign-in and security...",
"BTN_SHOW_DETAILS": "Display technical data",
"LOCKED_OUTPUTS_POPOVER": {
"TITLE": "Locked amount",
"DESCRIPTION": "Here are the conditions for unlocking this amount:",
"DESCRIPTION_MANY": "This transaction consists of several parts, of which the unlock conditions are:",
"LOCKED_AMOUNT": "Conditions for the amount:"
},
"NEW": {
"TITLE": "Registration",
"INTRO_WARNING_TIME": "Creating an account on {{name|capitalize}} is very simple. Please take sufficient time to do this correctly (not to forget the usernames, passwords, etc.).",
"INTRO_WARNING_SECURITY": "Check that the hardware you are currently using (computer, tablet, phone) <b>is secure and trustworthy </b>.",
"INTRO_WARNING_SECURITY_HELP": "Up-to-date anti-virus, firewall enabled, session protected by password or pin code...",
"INTRO_HELP": "Click <b> {{'COMMON.BTN_START'|translate}}</b> to begin creating an account. You will be guided step by step.",
"REGISTRATION_NODE": "Your registration will be registered via the Duniter peer <b>{{server}}</b> node, which will then be distributed to the rest of the currency network.",
"REGISTRATION_NODE_HELP": "If you do not trust this peer, please change <a ng-click=\"doQuickFix('settings')\">in the settings</a> of Cesium.",
"SELECT_ACCOUNT_TYPE": "Choose the type of account to create:",
"MEMBER_ACCOUNT": "Member account",
"MEMBER_ACCOUNT_TITLE": "Create a member account",
"MEMBER_ACCOUNT_HELP": "If you are not yet registered as an individual (one account possible per individual).",
"WALLET_ACCOUNT": "Simple wallet",
"WALLET_ACCOUNT_TITLE": "Create a wallet",
"WALLET_ACCOUNT_HELP": "If you represent a company, association, etc. or simply need an additional wallet. No universal dividend will be created by this account.",
"SALT_WARNING": "Choose a identifier.<br/>You need it for each connection to this account.<br/><br/><b>Make sure to remember this identifier</b>.<br/>If lost, there are no means to retrieve it!",
"PASSWORD_WARNING": "Choose a password.<br/>You need it for each connection to this account.<br/><br/><b>Make sure to remember this password</b>.<br/>If lost, there are no means to retrieve it!",
"PSEUDO_WARNING": "Choose a pseudonym.<br/>It may be used by other people to find you more easily.<br/><br/>.Use of <b>commas, spaces and accents</b> is not allowed.<br/><div class='hidden-xs'><br/>Example: <span class='gray'>JohnDalton, JackieChan, etc.</span>",
"PSEUDO": "Pseudonym",
"PSEUDO_HELP": "joe123",
"SALT_CONFIRM": "Confirm",
"SALT_CONFIRM_HELP": "Confirm the identifier",
"PASSWORD_CONFIRM": "Confirm",
"PASSWORD_CONFIRM_HELP": "Confirm the password",
"SLIDE_6_TITLE": "Confirmation:",
"COMPUTING_PUBKEY": "Computing...",
"LAST_SLIDE_CONGRATULATION": "You completed all required fields.<br/><b>You can send the account creation request</b>.<br/><br/>For information, the public key below identifies your future account.<br/>It can be communicated to third parties to receive their payment.<br/>Once your account has been approved, you can find this key under <b>{{'ACCOUNT.TITLE'|translate}}</b>.",
"CONFIRMATION_MEMBER_ACCOUNT": "<b class=\"assertive\">Warning:</b> your identifier, password and pseudonym can not be changed.<br/><b>Make sure you always remember it!</b><br/><b>Are you sure</b> you want to send this account creation request?",
"CONFIRMATION_WALLET_ACCOUNT": "<b class=\"assertive\">Warning:</b> your password and pseudonym can not be changed.<br/><b>Make sure you always remember it!</b><br/><b>Are you sure</b> you want to continue?",
"CHECKING_PSEUDO": "Checking...",
"PSEUDO_AVAILABLE": "This pseudonym is available",
"PSEUDO_NOT_AVAILABLE": "This pseudonym is not available",
"INFO_LICENSE": "To be able to adhere to the currency, we ask you to kindly read and accept this license.",
"BTN_ACCEPT": "I accept",
"BTN_ACCEPT_LICENSE": "I accept the license"
},
"POPUP_REGISTER": {
"TITLE": "Enter a pseudonym",
"HELP": "A pseudonym is needed to let other members find you."
},
"SECURITY":{
"ADD_QUESTION" : "Add custom question",
"BTN_CLEAN" : "Clean",
"BTN_RESET" : "Reset",
"DOWNLOAD_REVOKE": "Save a revocation file",
"DOWNLOAD_REVOKE_HELP" : "Having a revocation file is important, for example in case of loss of identifiers. It allows you to <b>get this account out of the Web Of Trust</b>, thus becoming a simple wallet.",
"GENERATE_KEYFILE": "Generate my keychain file ...",
"GENERATE_KEYFILE_HELP": "Generate a file allowing you to authenticate without entering your identifiers.<br/><b>Warning:</b> this file will contain your secret key; It is therefore very important to put it in a safe place!",
"KEYFILE_FILENAME": "keychain-{{pubkey|formatPubkey}}-{{currency}}-{{format}}.dunikey",
"MEMBERSHIP_IN": "Register as member...",
"MEMBERSHIP_IN_HELP": "Allows you to <b>transform </b> a simple wallet account <b>into a member account</b>, by sending a membership request. Useful only if you do not already have another member account.",
"SEND_IDENTITY": "Publish identity...",
"SEND_IDENTITY_HELP": "Allows you to associate a pseudonym to this account, but <b>without applying for membership</b> to become a member. This is not very useful because the validity of this pseudonym association is limited in time.",
"HELP_LEVEL": "Choose <strong> at least {{nb}} questions </strong> :",
"LEVEL": "Security level",
"LOW_LEVEL": "Low <span class=\"hidden-xs\">(2 questions minimum)</span>",
"MEDIUM_LEVEL": "Medium <span class=\"hidden-xs\">(4 questions minimum)</span>",
"QUESTION_1": "What was your best friend's name when you were a teen ?",
"QUESTION_2": "What was the name of your first pet ?",
"QUESTION_3": "What is the first meal you have learned to cook ?",
"QUESTION_4": "What is the first movie you saw in the cinema?",
"QUESTION_5": "Where did you go the first time you flew ?",
"QUESTION_6": "What was your favorite elementary school teacher's name ?",
"QUESTION_7": "What would you consider the ideal job ?",
"QUESTION_8": "Which children's book do you prefer?",
"QUESTION_9": "What was the model of your first vehicle?",
"QUESTION_10": "What was your nickname when you were a child ?",
"QUESTION_11": "What was your favorite movie character or actor when you were a student ?",
"QUESTION_12": "What was your favorite singer or band when you were a student ?",
"QUESTION_13": "In which city did your parents meet ?",
"QUESTION_14": "What was the name of your first boss ?",
"QUESTION_15": "What is the name of the street where you grew up ?",
"QUESTION_16": "What is the name of the first beach where you go swim ?",
"QUESTION_17": "QWhat is the first album you bought ?",
"QUESTION_18": "What is the name of your favorite sport team ?",
"QUESTION_19": "What was your grand-father's job ?",
"RECOVER_ID": "Recover my password...",
"RECOVER_ID_HELP": "If you have a <b>backup file of your identifiers</b>, you can find them by answering your personal questions correctly.",
"REVOCATION_WITH_FILE" : "Rekoke my member account...",
"REVOCATION_WITH_FILE_DESCRIPTION": "If you have <b>permanently lost your member account credentials (or if account security is compromised), you can use <b>the revocation file</b> of the account <b>to quit the Web Of Trust</b>.",
"REVOCATION_WITH_FILE_HELP": "To <b>permanently revoke</ b> a member account, please drag the revocation file in the box below, or click in the box to search for a file.",
"REVOCATION_WALLET": "Revoke this account immediately",
"REVOCATION_WALLET_HELP": "Requesting revocation of your identity causes <b>will revoke your membership</ b> (definitely for the associated pseudonym and public key). The account will no longer be able to produce a Universal Dividend.<br/>However, you can still use it as a simple wallet.",
"REVOCATION_FILENAME": "revocation-{{uid}}-{{pubkey|formatPubkey}}-{{currency}}.txt",
"SAVE_ID": "Save my credentials...",
"SAVE_ID_HELP": "Creating a backup file, to <b>retrieve your password</b> (and the identifier) <b> in case of forgetting</b>. The file is <b>secured</ b> (encrypted) using personal questions.",
"STRONG_LEVEL": "Strong <span class=\"hidden-xs \">(6 questions minimum)</span>",
"TITLE": "Account and security"
},
"FILE_NAME": "{{currency}} - Account statement {{pubkey|formatPubkey}} to {{currentTime|formatDateForFile}}.csv",
"HEADERS": {
"TIME": "Date",
"AMOUNT": "Amount",
"COMMENT": "Comment"
}
},
"TRANSFER": {
"TITLE": "Transfer",
"SUB_TITLE": "Transfer money",
"FROM": "From",
"TO": "To",
"AMOUNT": "Amount",
"AMOUNT_HELP": "Amount",
"COMMENT": "Comment",
"COMMENT_HELP": "Comment (optional)",
"BTN_SEND": "Send",
"BTN_ADD_COMMENT": "Add comment?",
"MODAL": {
"TITLE": "Transfer"
}
},
"ERROR": {
"POPUP_TITLE": "Error",
"UNKNOWN_ERROR": "Unknown error",
"CRYPTO_UNKNOWN_ERROR": "Your browser is not compatible with cryptographic features.",
"FIELD_REQUIRED": "This field is required.",
"FIELD_TOO_SHORT": "Value is too short (min {{minLength]] characters).",
"FIELD_TOO_SHORT_WITH_LENGTH": "This field value is too short.",
"FIELD_TOO_LONG": "Value is exceeding max length.",
"FIELD_TOO_LONG_WITH_LENGTH": "Value is too long (max {{maxLength}} characters).",
"FIELD_MIN": "Minimum value: {{min}}",
"FIELD_MAX": "Maximal value: {{max}}",
"FIELD_ACCENT": "Commas and accent characters not allowed",
"FIELD_NOT_NUMBER": "Value is not a number",
"FIELD_NOT_INT": "Value is not an integer",
"FIELD_NOT_EMAIL": "Email adress not valid",
"PASSWORD_NOT_CONFIRMED": "Must match previous password.",
"SALT_NOT_CONFIRMED": "Must match previous identifier.",
"SEND_IDENTITY_FAILED": "Error while trying to register.",
"SEND_CERTIFICATION_FAILED": "Could not certify identity.",
"NEED_MEMBER_ACCOUNT_TO_CERTIFY": "You could not send certification, because your account is <b>not a member account</b>.",
"NEED_MEMBER_ACCOUNT_TO_CERTIFY_HAS_SELF": "You could not send certification now, because your are <b>not a member</b> yet.<br/><br/>You still need certification to become a member.",
"NOT_MEMBER_FOR_CERTIFICATION": "Your account is not a member account yet.",
"IDENTITY_TO_CERTIFY_HAS_NO_SELF": "This account could not be certified. No registration found, or need to renew.",
"LOGIN_FAILED": "Error while sign in.",
"LOAD_IDENTITY_FAILED": "Could not load identity.",
"LOAD_REQUIREMENTS_FAILED": "Could not load identity requirements.",
"SEND_MEMBERSHIP_IN_FAILED": "Error while sending registration as member.",
"SEND_MEMBERSHIP_OUT_FAILED": "Error while sending membership revocation.",
"REFRESH_WALLET_DATA": "Could not refresh wallet.",
"GET_CURRENCY_PARAMETER": "Could not get currency parameters.",
"GET_CURRENCY_FAILED": "Could not load currency. Please retry later.",
"SEND_TX_FAILED": "Could not send transaction.",
"ALL_SOURCES_USED": "Please wait the next block computation (All transaction sources has been used).",
"NOT_ENOUGH_SOURCES": "Not enough changes to send this amount in one time.<br/>Maximum amount: {{amount}} {{unit}}<sub>{{subUnit}}</sub>.",
"ACCOUNT_CREATION_FAILED": "Error while creating your member account.",
"RESTORE_WALLET_DATA_ERROR": "Error while reloading settings from local storage",
"LOAD_WALLET_DATA_ERROR": "Error while loading wallet data.",
"COPY_CLIPBOARD_FAILED": "Could not copy to clipboard",
"TAKE_PICTURE_FAILED": "Could not get picture.",
"SCAN_FAILED": "Could not scan QR code.",
"SCAN_UNKNOWN_FORMAT": "Code not recognized.",
"WOT_LOOKUP_FAILED": "Search failed.",
"LOAD_PEER_DATA_FAILED": "Duniter peer not accessible. Please retry later.",
"NEED_LOGIN_FIRST": "Please sign in first.",
"AMOUNT_REQUIRED": "Amount is required.",
"AMOUNT_NEGATIVE": "Negative amount not allowed.",
"NOT_ENOUGH_CREDIT": "Not enough credit.",
"INVALID_NODE_SUMMARY": "Unreachable peer or invalid address",
"INVALID_USER_ID": "Field 'pseudonym' must not contains spaces or special characters.",
"INVALID_COMMENT": "Field 'reference' has a bad format.",
"INVALID_PUBKEY": "Public key has a bad format.",
"IDENTITY_REVOKED": "This identity <b>has been revoked {{revocationTime|formatFromNow}}</b> ({{revocationTime|formatDate}}). It can no longer become a member.",
"IDENTITY_PENDING_REVOCATION": "The <b>revocation of this identity</b> has been requested and is awaiting processing. Certification is therefore disabled.",
"IDENTITY_INVALID_BLOCK_HASH": "This membership application is no longer valid (because it references a block that network peers are cancelled): the person must renew its application for membership <b>before</b> being certified.",
"IDENTITY_EXPIRED": "This identity has expired: this person must re-apply <b>before</b> being certified.",
"IDENTITY_SANDBOX_FULL": "Could not register, because peer's sandbox is full.<br/><br/>Please retry later or choose another Duniter peer (in <b>Settings</b>).",
"IDENTITY_NOT_FOUND": "Identity not found",
"WOT_PENDING_INVALID_BLOCK_HASH": "Membership not valid.",
"WALLET_INVALID_BLOCK_HASH": "Your membership application is no longer valid (because it references a block that network peers are cancelled).<br/>You must <a ng-click=\"doQuickFix('renew')\">renew your application for membership</a> to fix this issue.",
"WALLET_IDENTITY_EXPIRED": "The publication of your identity <b>has expired</b>.<br/>You must <a ng-click=\"doQuickFix('fixIdentity')\">re-issue your identity</a> to resolve this issue.",
"WALLET_REVOKED": "Your identity has been <b>revoked</b>: neither your pseudonym nor your public key will be used in the future for a member account.",
"WALLET_HAS_NO_SELF": "Your identity must first have been published, and not expired.",
"AUTH_REQUIRED": "Authentication required.",
"AUTH_INVALID_PUBKEY": "The public key does not match the connected account.",
"AUTH_INVALID_SCRYPT": "Invalid username or password.",
"AUTH_INVALID_FILE": "Invalid keychain file.",
"AUTH_FILE_ERROR": "Failed to open keychain file",
"IDENTITY_ALREADY_CERTIFY": "You have <b>already certified</b> that identity.<br/><br/>Your certificate is still valid (expires {{expiresIn|formatDuration}}).",
"IDENTITY_ALREADY_CERTIFY_PENDING": "You have <b>already certified</b> that identity.<br/><br/>Your certification is still pending (Deadline for treatment {{expiresIn|formatDuration}}).",
"UNABLE_TO_CERTIFY_TITLE": "Unable to certify",
"LOAD_NEWCOMERS_FAILED": "Unable to load new members.",
"LOAD_PENDING_FAILED": "Unable to load pending registrations.",
"ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION": "You must <b>be a member</b> in order to perform this action.",
"ONLY_SELF_CAN_EXECUTE_THIS_ACTION": "You must have <b>published your identity</b> in order to perform this action.",
"GET_BLOCK_FAILED": "Error while getting block",
"INVALID_BLOCK_HASH": "Block not found (incorrect hash)",
"DOWNLOAD_REVOCATION_FAILED": "Error while downloading revocation file.",
"REVOCATION_FAILED": "Error while trying to revoke the identity.",
"SALT_OR_PASSWORD_NOT_CONFIRMED": "Wrong identifier or password ",
"RECOVER_ID_FAILED": "Could not recover password",
"LOAD_FILE_FAILED" : "Unable to load file",
"NOT_VALID_REVOCATION_FILE": "Invalid revocation file (wrong file format)",
"NOT_VALID_SAVE_ID_FILE": "Invalid credentials backup file (wrong file format)",
"NOT_VALID_KEY_FILE": "Invalid keychain file (unrecognized format)",
"EXISTING_ACCOUNT": "Your identifiers correspond to an already existing account, whose <a ng-click=\"showHelpModal('pubkey')\">public key</a> is:",
"EXISTING_ACCOUNT_REQUEST": "Please modify your credentials so that they correspond to an unused account.",
"GET_LICENSE_FILE_FAILED": "Unable to get license file",
"CHECK_NETWORK_CONNECTION": "No peer appears to be accessible.<br/><br/>Please <b>check your Internet connection</b>."
},
"INFO": {
"POPUP_TITLE": "Information",
"CERTIFICATION_DONE": "Identity successfully signed",
"NOT_ENOUGH_CREDIT": "Not enough credit",
"TRANSFER_SENT": "Transfer request successfully sent",
"COPY_TO_CLIPBOARD_DONE": "Copy succeeded",
"MEMBERSHIP_OUT_SENT": "Membership revocation sent",
"NOT_NEED_MEMBERSHIP": "Already a member.",
"IDENTITY_WILL_MISSING_CERTIFICATIONS": "This identity will soon lack certification (at least {{willNeedCertificationCount}}).",
"REVOCATION_SENT": "Revocation sent successfully",
"REVOCATION_SENT_WAITING_PROCESS": "Revocation <b>has been sent successfully</b>. It is awaiting processing.",
"FEATURES_NOT_IMPLEMENTED": "This features is not implemented yet.<br/><br/>Why not to contribute to get it faster? ;)",
"EMPTY_TX_HISTORY": "No operations to export"
},
"CONFIRM": {
"POPUP_TITLE": "<b>Confirmation</b>",
"POPUP_WARNING_TITLE": "<b>Warning</b>",
"POPUP_SECURITY_WARNING_TITLE": "<i class=\"icon ion-alert-circled\"></i> <b>Security warning</b>",
"CERTIFY_RULES_TITLE_UID": "Certify {{uid}}",
"CERTIFY_RULES": "<b class=\"assertive\">Don't certify an account</b> if you believe that: <ul><li>1.) the issuers identity might be faked.<li>2.) the issuer already has another certified account.<li>3.) the issuer purposely or carelessly violates rule 1 or 2 (he certifies faked or double accounts).</ul></small><br/>Are you sure you want to certify this identity?",
"FULLSCREEN": "View the application in full screen?",
"EXIT_APP": "Close the application ?",
"TRANSFER": "<b>Transfer summary:</b><br/><br/><ul><li> - From: <b>{{from}}</b></li><li> - To: <b>{{to}}</b></li><li> - Amount: <b>{{amount}} {{unit}}</b></li><li> - Comment: <i>{{comment}}</i></li></ul><br/><b>Are-you sure you want to do this transfer?</b>",
"MEMBERSHIP_OUT": "This operation is <b>irreversible</b>.<br/></br/><b>Are you sure you want to terminate your membership?</b>",
"MEMBERSHIP_OUT_2": "This operation is <b>irreversible</b>!<br/><br/>Are you sure you want to <b>terminate your membership</b>?",
"LOGIN_UNUSED_WALLET_TITLE": "Typing error?",
"LOGIN_UNUSED_WALLET": "The account seems to be <b>inactive</b>.<br/><br/>It's probably a <b>typing error</b> when sign in. Please try again, checking that <b>public key is yours<b/>.",
"FIX_IDENTITY": "The pseudonym <b>{{uid}}</b> will be published again, replacing the old publication that has expired.<br/></br/><b>Are you sure</b> you want to continue?",
"FIX_MEMBERSHIP": "Your application for membership will be sent.<br/></br/><b>Are you sure?</b>",
"RENEW_MEMBERSHIP": "Your membership will be renewed.<br/></br/><b>Are you sure?</b>",
"REVOKE_IDENTITY": "You will <b>definitely revoke this identity</b>.<br/><br/>The public key and the associated pseudonym <b>will never be used again</b> (for a member account).<br/></br/><b>Are you sure</b> you want to revoke this identity?",
"REVOKE_IDENTITY_2": "This operation is <b>irreversible</b>!<br/><br/>Are you sure you want to <b>revoke this identity</b>?",
"NOT_NEED_RENEW_MEMBERSHIP": "Your membership does not need to be renewed (it will only expire in {{membershipExpiresIn|formatDuration}}).<br/></br/><b>Are you sure you</b> want to renew your membership?",
"SAVE_BEFORE_LEAVE": "Do you want to <b>save your changes</b> before leaving the page?",
"SAVE_BEFORE_LEAVE_TITLE": "Changes not saved",
"LOGOUT": "Are you sure you want to logout?",
"USE_FALLBACK_NODE": "Peer <b>{{old}}</b> unreachable or invalid address.<br/><br/>Do you want to temporarily use the <b>{{new}}</b> node?"
},
"DOWNLOAD": {
"POPUP_TITLE": "<b>Revocation file</b>",
"POPUP_REVOKE_MESSAGE": "To safeguard your account, please download the <b>account revocation document</b>. It will allow you to cancel your account (in case of account theft, ID, an incorrectly created account, etc.).<br/><br/><b>Please store it in a safe place.</b>"
},
"HELP": {
"TITLE": "Online help",
"JOIN": {
"SECTION": "Join",
"SALT": "The identifier is very important. It is used to hash you password, which in turn is used to calculate your <span class=\"text-italic\">public account key</span> (its number) and the private key to access it.<br/><b>Please remeber this identifier well</b>, because there is no way to recover it when lost.<br/>Furthermore, it cannot be changed without having to create a new account.<br/><br/>A good identifier must be sufficiently long (8 characters at the very least) and as original as possible.",
"PASSWORD": "The password is very important. Together with the identifier, it is use to calculate your account number (pblic key) and the private key to access it.<br/><b>Please remember it well</b>, because there is no way to recover it when lost.<br/>Furthermore, it cannot be changed without having to create a new account.<br/><br/>A good password is made (ideally) of at least 8 characters, with at least one capital and one number.",
"PSEUDO": "A pseudonym is used only when joining as <span class=\"text-italic\">member</span>. It is always associated with a wallet (by its <span class=\"text-italic\">public key</span>).<br/>It is published on the network so that other users may identify it, certify or send money to the account.<br/>A pseudonym must be unique among all members (current and past)."
},
"LOGIN": {
"SECTION": "Log in",
"PUBKEY": "Account public key",
"PUBKEY_DEF": "The public key of the keychain is generated from the entered identifiers (any), but does not correspond to an account already used.<br/><b>Make sure your public key is the same as your account</b>. Otherwise, you will be logged into an account that is probably never used, as the risk of collision with an existing account is very small.<br/><a href=\"https://en.wikipedia.org/wiki/Elliptic_curve_cryptography\" target=\"_ system\">Learn more about cryptography</a> by public key.",
"METHOD": "Méthodes de connexion",
"METHOD_DEF": "Plusieurs options sont disponibles pour vous connecter à un portfeuille :<br/> - La connexion <b>par sallage (simple ou avancé)</b> mélange votre mot de passe grâce à l'identifiant secret, pour limiter les tentatives de piratge par force brute (par exemple à partir de mots connus).<br/> - La connexion <b>par clé publique</b> évite de saisir vos identifiants, qui vous seront demandé seulement le moment venu lors d'une opération sur le compte.<br/> - La connexion <b>par fichier de trousseau</b> va lire les clés (publique et privée) du compte, depuis un fichier, sans besoin de saisir d'identifiants. Plusieurs formats de fichier sont possibles."
},
"GLOSSARY": {
"SECTION": "Glossary",
"PUBKEY_DEF": "A public key always identifies a wallet. It may identify a member. In Cesium it is calculated using the identifier and the password.",
"MEMBER": "Member",
"MEMBER_DEF": "A member is a real and living human, wishing to participate freely to the monitary community. The member will receive universal dividend, according to the period and amount as defined in the <span class=\"text-italic\">currency parameters</span>.",
"CURRENCY_RULES": "Currency rules",
"CURRENCY_RULES_DEF": "The currency rules are defined only once, and for all. They set the parameters under which the currency will perform: universal dividend calculation, the amount of certifications needed to become a member, the maximum amount of certifications a member can send, etc.<br/><br/>The parameters cannot be modified because of the use of a <span class=\"text-italic\">Blockchain</span> which carries and executes these rules, and constantly verifies their correct application. <a href=\"#/app/currency\">See current parameters</a>.",
"BLOCKCHAIN": "Blockchain",
"BLOCKCHAIN_DEF": "The Blockchain is a decentralised system which, in case of Duniter, serves to carry and execute the <span class=\"text-italic\">currency rules</span>.<br/><a href=\"http://en.duniter.org/presentation/\" target=\"_blank\">Read more about Duniter</a> and the working of its blockchain.",
"UNIVERSAL_DIVIDEND_DEF": "The Universal Dividend (UD) is the quantity of money co-created by each member, according to the period and the calculation defined in the <span class=\"text-italic\">currency rules</span>.<br/>Every term, the members receive an equal amount of new money on their account.<br/><br/>The UD undergoes a steady growth, to remain fair under its members (current and future), calculated by an average life expectancy, as demonstrated in the Relative Theory of Money (RTM).<br/><a href=\"http://trm.creationmonetaire.info\" target=\"_system\">Read more about RTM</a> and open money."
},
"TIP": {
"MENU_BTN_CURRENCY": "Menu <b>{{'MENU.CURRENCY'|translate}}</b> allows discovery of <b>currency parameters</b> and its state.",
"CURRENCY_WOT": "The <b>member count</b> shows the <b>community's weight and evolution</b>.",
"CURRENCY_MASS": "Shown here is the <b>total amount</b> currently in circulation and its <b>average distribution</b> per member.<br/><br/>This allows to estimate the <b>worth of any amount</b>, in respect to what <b>others own</b> on their account (on average).",
"CURRENCY_UNIT_RELATIVE": "The unit used here (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifies that the amounts in {{currency|capitalize}} have been devided by the <b>Universal Dividend</b> (UD).<br/><br/><small>This relative unit is <b>relevant</b> because it is stable in contrast to the permanently growing monitary mass.</small>",
"CURRENCY_CHANGE_UNIT": "The option <b>{{'COMMON.BTN_RELATIVE_UNIT'|translate}}</b> allows to <b>switch the unit</b> to show amounts in <b>{{currency|capitalize}}</b>, undevided by the Universal Dividend (instead of in &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;).",
"CURRENCY_CHANGE_UNIT_TO_RELATIVE": "The option <b>{{'COMMON.BTN_RELATIVE_UNIT'|translate}}</b> allows to <b>switch the unit</b> to show amounts in &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;, which is relative to the Universal Dividend (the amount co-produced by each member).",
"CURRENCY_RULES": "The <b>rules</b> of the currency determine its <b>exact and predictible</b> performance.<br/><br/>As a true DNA of the currency these rules make the monetary code <b>transparent and understandable</b>.",
"MENU_BTN_NETWORK": "Menu <b>{{'MENU.NETWORK'|translate}}</b> allows discovery of <b>network's state<b>.",
"NETWORK_BLOCKCHAIN": "All monetary transactions are recoded in a <b>public and tamper proof</b> ledger, generally referred to as the <b>blockchain</b>.",
"NETWORK_PEERS": "The <b>peers</b> shown here correspond to <b>computers that update and check</b> the blockchain.<br/><br/>The more active peers there are, the more <b>decentralised</b> and therefore trustworhty the currency becomes.",
"NETWORK_PEERS_BLOCK_NUMBER": "This <b>number</b> (in green) indicates the peer's <b>latest validated block</b> (last page written in the ledger).<br/><br/>Green indicates that the block was equally validated by the <b>majority of other peers</b>.",
"NETWORK_PEERS_PARTICIPATE": "<b>Each member</b>, equiped with a computer with Internet, <b>can participate, adding a peer</b> simply by <b>installing the Duniter software</b> (free/libre). <a target=\"_new\" href=\"{{installDocUrl}}\" target=\"_system\">Read the installation manual &gt;&gt;</a>.",
"MENU_BTN_ACCOUNT": "<b>{{'ACCOUNT.TITLE'|translate}}</b> allows access to your account balance and transaction history.",
"MENU_BTN_ACCOUNT_MEMBER": "Here you can consult your account status, transaction history and your certifications.",
"WALLET_CERTIFICATIONS": "Click here to reveiw the details of your certifications (given and received).",
"WALLET_RECEIVED_CERTIFICATIONS": "Click here to review the details of your <b>received certifications</b>.",
"WALLET_GIVEN_CERTIFICATIONS": "Click here to review the details of your <b>given certifications</b>.",
"WALLET_BALANCE": "Your account <b>balance</b> is shown here.",
"WALLET_BALANCE_RELATIVE": "{{'HELP.TIP.WALLET_BALANCE'|translate}}<br/><br/>The used unit (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifies that the amount in {{currency|capitalize}} has been divided by the <b>Universal Dividend</b> (UD) co-created by each member.<br/>At this moment, 1 UD equals {{currentUD}} {{currency|capitalize}}.",
"WALLET_BALANCE_CHANGE_UNIT": "You can <b>change the unit</b> in which amounts are shown in <b><i class=\"icon ion-android-settings\"></i>&nbsp;{{'MENU.SETTINGS'|translate}}</b>.<br/><br/>For example, to display amounts <b>directly in {{currency|capitalize}}</b> instead of relative amounts.",
"WALLET_PUBKEY": "This is your account public key. You can communicate it to a third party so that it more easily identifies your account.",
"WALLET_SEND": "Issue a payment in just a few clicks.",
"WALLET_SEND_NO_MONEY": "Issue a payment in just a few clicks.<br/>(Your balance does not allow this yet)",
"WALLET_OPTIONS": "Please note that this button allows access to <b>other, less used actions</b>.<br/><br/>Don't forget to take a quick look, when you have a moment!",
"WALLET_RECEIVED_CERTS": "This shows the list of persons that certified you.",
"WALLET_CERTIFY": "The button <b>{{'WOT.BTN_SELECT_AND_CERTIFY'|translate}}</b> allows selecting an identity and certifying it.<br/><br/>Only users that are <b>already member</b> may certify others.",
"WALLET_CERT_STOCK": "Your supply of certifications (to send) is limited to <b>{{sigStock}} certifications</b>.<br/><br/>This supply will replete itself over time, as and when earlier certifications expire.",
"MENU_BTN_TX_MEMBER": "<b>{{'MENU.TRANSACTIONS'|translate}}</b> allow access to transactions history, and send new payments.",
"MENU_BTN_TX": "View the history of <b>your transactions</b> here and send new payments.",
"MENU_BTN_WOT": "The menu <b>{{'MENU.WOT'|translate}}</b> allows searching <b>users</b> of the currency (member or not).",
"WOT_SEARCH_TEXT_XS": "To search in the registry, type the <b>first letters of a users pseudonym or public key</b>.<br/><br/>The search will start automatically.",
"WOT_SEARCH_TEXT": "To search in the registry, type the <b>first letters of a users pseudonym or public key</b>.<br/><br/>Then hit <b>Enter</b> to start the search.",
"WOT_SEARCH_RESULT": "Simply click a user row to view the details sheet.",
"WOT_VIEW_CERTIFICATIONS": "The row <b>{{'ACCOUNT.CERTIFICATION_COUNT'|translate}}</b> shows how many members members validated this identity.<br/><br/>These certifications testify that the account belongs to <b>a living human</b> and this person has <b>no other member account</b>.",
"WOT_VIEW_CERTIFICATIONS_COUNT": "There are at least <b>{{sigQty}} certifications</b> needed to become a member and receive the <b>Universal Dividend</b>.",
"WOT_VIEW_CERTIFICATIONS_CLICK": "Click here to open <b>a list of all certifications</b> given to and by this identity.",
"WOT_VIEW_CERTIFY": "The button <b>{{'WOT.BTN_CERTIFY'|translate}}</b> allows to add your certification to this identity.",
"CERTIFY_RULES": "<b>Attention:</b> Only certify <b>real and living persons</b> that do not own any other certified account.<br/><br/>The trust carried by the currency depends on each member's vigilance!",
"MENU_BTN_SETTINGS": "The <b>{{'MENU.SETTINGS'|translate}}</b> allow you to configure the Cesium application.<br/><br/>For example, you can <b>change the unit</b> in which the currency will be shown.",
"HEADER_BAR_BTN_PROFILE": "Click here to access your <b>user profile</b>",
"SETTINGS_CHANGE_UNIT": "You can <b>change the display unit</b> of amounts by clicking here.<br/><br/>- Deactivate the option to show amounts in {{currency|capitalize}}.<br/>- Activate the option for relative amounts in {{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub> (<b>divided</b> by the current Universal Dividend).",
"END_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>Welcome to the <b>free economy</b>!",
"END_NOT_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>If you wish to join the currency {{currency|capitalize}}, simply click <b>{{'LOGIN.CREATE_ACCOUNT'|translate}}</b> below."
}
}
}
);
$translateProvider.translations("en", {
"COMMON": {
"APP_NAME": "ğ<b>change</b>",
"APP_VERSION": "v{{version}}",
"APP_BUILD": "build {{build}}",
"PUBKEY": "Public key",
"MEMBER": "Member",
"BLOCK" : "Block",
"BTN_OK": "OK",
"BTN_YES": "Yes",
"BTN_NO": "No",
"BTN_SEND": "Send",
"BTN_SEND_MONEY": "Transfer money",
"BTN_SEND_MONEY_SHORT": "Transfer",
"BTN_SAVE": "Save",
"BTN_YES_SAVE": "Yes, Save",
"BTN_YES_CONTINUE": "Yes, Continue",
"BTN_SHOW": "Show",
"BTN_SHOW_PUBKEY": "Show key",
"BTN_RELATIVE_UNIT": "Display amounts in UD?",
"BTN_BACK": "Back",
"BTN_NEXT": "Next",
"BTN_IMPORT": "Import",
"BTN_CANCEL": "Cancel",
"BTN_CLOSE": "Close",
"BTN_LATER": "Later",
"BTN_LOGIN": "Sign In",
"BTN_LOGOUT": "Logout",
"BTN_ADD_ACCOUNT": "New Account",
"BTN_SHARE": "Share",
"BTN_EDIT": "Edit",
"BTN_DELETE": "Delete",
"BTN_ADD": "Add",
"BTN_SEARCH": "Search",
"BTN_REFRESH": "Refresh",
"BTN_RETRY": "Retry",
"BTN_START": "Start",
"BTN_CONTINUE": "Continue",
"BTN_CREATE": "Create",
"BTN_UNDERSTOOD": "I understand",
"BTN_OPTIONS": "Options",
"BTN_HELP_TOUR": "Features tour",
"BTN_HELP_TOUR_SCREEN": "Discover this screen",
"BTN_DOWNLOAD": "Download",
"BTN_DOWNLOAD_ACCOUNT_STATEMENT": "Download account statement",
"BTN_MODIFY": "Modify",
"CHOOSE_FILE": "Drag your file<br/>or click to select",
"DAYS": "days",
"NO_ACCOUNT_QUESTION": "Not a member yet? Register now!",
"SEARCH_NO_RESULT": "No result found",
"LOADING": "Loading...",
"LOADING_WAIT": "Loading...<br/><small>(Waiting for node availability)</small>",
"SEARCHING": "Searching...",
"FROM": "From",
"TO": "To",
"COPY": "Copy",
"LANGUAGE": "Language",
"UNIVERSAL_DIVIDEND": "Universal dividend",
"UD": "UD",
"DATE_PATTERN": "MM/DD/YYYY HH:mm",
"DATE_FILE_PATTERN": "YYYY-MM-DD",
"DATE_SHORT_PATTERN": "MM/DD/YY",
"DATE_MONTH_YEAR_PATTERN": "MM/YYYY",
"EMPTY_PARENTHESIS": "(empty)",
"UID": "Pseudonym",
"ENABLE": "Enabled",
"DISABLE": "Disabled",
"RESULTS_LIST": "Results:",
"RESULTS_COUNT": "{{count}} results",
"EXECUTION_TIME": "executed in {{duration|formatDurationMs}}",
"SHOW_VALUES": "Display values openly?",
"POPOVER_ACTIONS_TITLE": "Options",
"POPOVER_FILTER_TITLE": "Filters",
"SHOW_MORE": "Show more",
"SHOW_MORE_COUNT": "(current limit at {{limit}})",
"POPOVER_SHARE": {
"TITLE": "Share",
"SHARE_ON_TWITTER": "Share on Twitter",
"SHARE_ON_FACEBOOK": "Share on Facebook",
"SHARE_ON_DIASPORA": "Share on Diaspora*",
"SHARE_ON_GOOGLEPLUS": "Share on Google+"
},
"FILE": {
"DATE" : "Date:",
"TYPE" : "Type:",
"SIZE": "Size:",
"VALIDATING": "Validating..."
}
},
"SYSTEM": {
"PICTURE_CHOOSE_TYPE": "Choose source:",
"BTN_PICTURE_GALLERY": "Gallery",
"BTN_PICTURE_CAMERA": "<b>Camera</b>"
},
"MENU": {
"HOME": "Home",
"WOT": "Registry",
"CURRENCY": "Currency",
"ACCOUNT": "My Account",
"WALLETS": "My wallets",
"TRANSFER": "Transfer",
"SCAN": "Scan",
"SETTINGS": "Settings",
"NETWORK": "Network",
"TRANSACTIONS": "My transactions"
},
"ABOUT": {
"TITLE": "About",
"LICENSE": "<b>Free/libre software</b> (License GNU AGPLv3).",
"LATEST_RELEASE": "There is a <b>newer version</ b> of {{'COMMON.APP_NAME' | translate}} (<b>v{{version}}</b>)",
"PLEASE_UPDATE": "Please update {{'COMMON.APP_NAME' | translate}} (latest version: <b>v{{version}}</b>)",
"CODE": "Source code:",
"OFFICIAL_WEB_SITE": "Official web site:",
"DEVELOPERS": "Developers:",
"FORUM": "Forum:",
"DEV_WARNING": "Warning",
"DEV_WARNING_MESSAGE": "This application is still in active development.<br/>Please report any issue to us!",
"DEV_WARNING_MESSAGE_SHORT": "This App is still unstable (still under development).",
"REPORT_ISSUE": "Report an issue"
},
"HOME": {
"TITLE": "Cesium",
"MESSAGE": "Welcome to the {{'COMMON.APP_NAME'|translate}} Application!",
"MESSAGE_CURRENCY": "Make exchanges in the libre currency {{currency|abbreviate}}!",
"BTN_CURRENCY": "Explore currency",
"BTN_ABOUT": "about",
"BTN_HELP": "Help",
"REPORT_ISSUE": "Report an issue",
"NOT_YOUR_ACCOUNT_QUESTION" : "You do not own the account <b><i class=\"ion-key\"></i> {{pubkey|formatPubkey}}</b>?",
"BTN_CHANGE_ACCOUNT": "Disconnect this account",
"CONNECTION_ERROR": "Peer <b>{{server}}</b> unreachable or invalid address.<br/><br/>Check your Internet connection, or change node <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in the settings</a>."
},
"SETTINGS": {
"TITLE": "Settings",
"DISPLAY_DIVIDER": "Display",
"STORAGE_DIVIDER": "Storage",
"NETWORK_SETTINGS": "Network",
"PEER": "Duniter peer address",
"PEER_SHORT": "Peer address",
"PEER_CHANGED_TEMPORARY": "Address used temporarily",
"USE_LOCAL_STORAGE": "Enable local storage",
"USE_LOCAL_STORAGE_HELP": "Allows you to save your settings",
"WALLETS_SETTINGS": "My wallets",
"USE_WALLETS_ENCRYPTION": "Secure the list",
"USE_WALLETS_ENCRYPTION_HELP": "Enables you to encrypt the list of your wallets. Authentication required to access it.",
"ENABLE_HELPTIP": "Enable contextual help tips",
"ENABLE_UI_EFFECTS": "Enable visual effects",
"HISTORY_SETTINGS": "Account operations",
"DISPLAY_UD_HISTORY": "Display produced dividends?",
"TX_HISTORY_AUTO_REFRESH": "Enable automatic refresh?",
"TX_HISTORY_AUTO_REFRESH_HELP": "Updates the list of operations to each new block.",
"AUTHENTICATION_SETTINGS": "Authentication",
"KEEP_AUTH": "Expiration of authentication",
"KEEP_AUTH_SHORT": "Expiration",
"KEEP_AUTH_HELP": "Define when authentication is cleared from memory.",
"KEEP_AUTH_OPTION": {
"NEVER": "After each operation",
"SECONDS": "After {{value}}s of inactivity",
"MINUTE": "After {{value}}min of inactivity",
"MINUTES": "After {{value}}min of inactivity",
"HOUR": "After {{value}}h of inactivity",
"ALWAYS": "At the end of the session"
},
"KEYRING_FILE": "Keyring file",
"KEYRING_FILE_HELP": "Allow auto-connect at startup, or to authenticate (only if \"Expiration of authentication\" is \"at the end of the session\"",
"REMEMBER_ME": "Remember me ?",
"REMEMBER_ME_HELP": "Allows to remain identified from one session to another, keeping the public key locally.",
"PLUGINS_SETTINGS": "Extensions",
"BTN_RESET": "Restore default values",
"EXPERT_MODE": "Enable expert mode",
"EXPERT_MODE_HELP": "Allow to see more details",
"BLOCK_VALIDITY_WINDOW": "Block uncertainty time",
"BLOCK_VALIDITY_WINDOW_SHORT": "Time of uncertainty",
"BLOCK_VALIDITY_WINDOW_HELP": "Time to wait before considering an information is validated",
"BLOCK_VALIDITY_OPTION": {
"NONE": "No delay",
"N": "{{time | formatDuration}} ({{count}} blocks)"
},
"POPUP_PEER": {
"TITLE" : "Duniter peer",
"HOST" : "Address",
"HOST_HELP": "Address: server:port",
"USE_SSL" : "Secured?",
"USE_SSL_HELP" : "(SSL Encryption)",
"BTN_SHOW_LIST" : "Peer's list"
}
},
"BLOCKCHAIN": {
"HASH": "Hash: {{hash}}",
"VIEW": {
"HEADER_TITLE": "Block #{{number}}-{{hash|formatHash}}",
"TITLE_CURRENT": "Current block",
"TITLE": "Block #{{number|formatInteger}}",
"COMPUTED_BY": "Computed by",
"SHOW_RAW": "Show raw data",
"TECHNICAL_DIVIDER": "Technical informations",
"VERSION": "Format version",
"HASH": "Computed hash",
"UNIVERSAL_DIVIDEND_HELP": "Money co-produced by each of the {{membersCount}} members",
"EMPTY": "Aucune donnée dans ce bloc",
"POW_MIN": "Minimal difficulty",
"POW_MIN_HELP": "Difficulty imposed in calculating hash",
"DATA_DIVIDER": "Data",
"IDENTITIES_COUNT": "New identities",
"JOINERS_COUNT": "Joiners",
"ACTIVES_COUNT": "Renewals",
"ACTIVES_COUNT_HELP": "Members having renewed their membership",
"LEAVERS_COUNT": "Leavers",
"LEAVERS_COUNT_HELP": "Members that now refused certification",
"EXCLUDED_COUNT": "Excluded members",
"EXCLUDED_COUNT_HELP": "Old members, excluded because missing membreship renewal or certifications",
"REVOKED_COUNT": "Revoked identities",
"REVOKED_COUNT_HELP": "These accounts may no longer be member",
"TX_COUNT": "Transactions",
"CERT_COUNT": "Certifications",
"TX_TO_HIMSELF": "Change",
"TX_OUTPUT_UNLOCK_CONDITIONS": "Unlock conditions",
"TX_OUTPUT_OPERATOR": {
"AND": "and",
"OR": "or"
},
"TX_OUTPUT_FUNCTION": {
"SIG": "<b>Sign</b> of the public key",
"XHX": "<b>Password</b>, including SHA256 =",
"CSV": "Blocked during",
"CLTV": "Bloqué until"
}
},
"LOOKUP": {
"TITLE": "Blocks",
"NO_BLOCK": "No bloc",
"LAST_BLOCKS": "Last blocks:",
"BTN_COMPACT": "Compact"
}
},
"CURRENCY": {
"VIEW": {
"TITLE": "Currency",
"TAB_CURRENCY": "Currency",
"TAB_WOT": "Web of trust",
"TAB_NETWORK": "Network",
"TAB_BLOCKS": "Blocks",
"CURRENCY_SHORT_DESCRIPTION": "{{currency|capitalize}} is a <b>libre money</b>, started {{firstBlockTime | formatFromNow}}. It currently counts <b>{{N}} members </b>, who produce and collect a <a ng-click=\"showHelpModal('ud')\">Universal Dividend</a> (DU), each {{dt | formatPeriod}}.",
"NETWORK_RULES_DIVIDER": "Network rules",
"CURRENCY_NAME": "Currency name",
"MEMBERS": "Members count",
"MEMBERS_VARIATION": "Variation since {{duration|formatDuration}} (since last UD)",
"MONEY_DIVIDER": "Money",
"MASS": "Monetary mass",
"SHARE": "Money share",
"UD": "Universal Dividend",
"C_ACTUAL": "Current growth",
"MEDIAN_TIME": "Current blockchain time",
"POW_MIN": "Common difficulty",
"MONEY_RULES_DIVIDER": "Rules of currency",
"C_RULE": "Theoretical growth target",
"UD_RULE": "Universal dividend (formula)",
"DT_REEVAL": "Period between two re-evaluation of the UD",
"REEVAL_SYMBOL": "reeval",
"DT_REEVAL_VALUE": "Every <b>{{dtReeval|formatDuration}}</b> ({{dtReeval/86400}} {{'COMMON.DAYS'|translate}})",
"UD_REEVAL_TIME0": "Date of first reevaluation of the UD",
"SIG_QTY_RULE": "Required number of certifications to become a member",
"SIG_STOCK": "Maximum number of certifications sent by a member",
"SIG_PERIOD": "Minimum delay between 2 certifications sent by one and the same issuer.",
"SIG_WINDOW": "Maximum delay before a certification will be treated",
"SIG_VALIDITY": "Lifetime of a certification that has been treated",
"MS_WINDOW": "Maximum delay before a pending membership will be treated",
"MS_VALIDITY": "Lifetime of a membership that has been treated",
"STEP_MAX": "Maximum distance between a newcomer and each referring members.",
"WOT_RULES_DIVIDER": "Rules for web of trust",
"SENTRIES": "Required number of certifications (given <b>and</b> received) to become a referring member",
"SENTRIES_FORMULA": "Required number of certifications to become a referring member (formula)",
"XPERCENT":"Minimum percent of referring member to reach to match the distance rule",
"AVG_GEN_TIME": "The average time between 2 blocks",
"CURRENT": "current",
"MATH_CEILING": "CEILING",
"DISPLAY_ALL_RULES": "Display all rules?",
"BTN_SHOW_LICENSE": "Show license",
"WOT_DIVIDER": "Web of trust"
},
"LICENSE": {
"TITLE": "Currency license",
"BTN_DOWNLOAD": "Download file",
"NO_LICENSE_FILE": "License file not found."
}
},
"NETWORK": {
"VIEW": {
"MEDIAN_TIME": "Blockchain time",
"LOADING_PEERS": "Loading peers...",
"NODE_ADDRESS": "Address:",
"SOFTWARE": "Software:",
"WARN_PRE_RELEASE": "Pre-release (latest stable: <b>{{version}}</b>)",
"WARN_NEW_RELEASE": "Version <b>{{version}}</b> available",
"WS2PID": "Identifier:",
"PRIVATE_ACCESS": "Private access",
"POW_PREFIX": "Proof of work prefix:",
"ENDPOINTS": {
"BMAS": "Secure endpoint (SSL)",
"BMATOR": "TOR endpoint",
"WS2P": "WS2P endpoint",
"ES_USER_API": "Cesium+ data node"
}
},
"INFO": {
"ONLY_SSL_PEERS": "Non-SSL nodes have a degraded display because Cesium works in HTTPS mode."
}
},
"PEER": {
"PEERS": "Peers",
"SIGNED_ON_BLOCK": "Signed on block",
"MIRROR": "mirror",
"MIRRORS": "Mirrors",
"MIRROR_PEERS": "Mirror peers",
"PEER_LIST" : "Peer's list",
"MEMBERS" : "Members",
"MEMBER_PEERS" : "Member peers",
"ALL_PEERS" : "All peers",
"DIFFICULTY" : "Difficulty",
"API" : "API",
"CURRENT_BLOCK" : "Block #",
"POPOVER_FILTER_TITLE": "Filter",
"OFFLINE": "Offline",
"OFFLINE_PEERS": "Offline peers",
"BTN_SHOW_PEER": "Show peer",
"VIEW": {
"TITLE": "Peer",
"OWNER": "Owned by ",
"SHOW_RAW_PEERING": "See peering document",
"SHOW_RAW_CURRENT_BLOCK": "See current block (raw format)",
"LAST_BLOCKS": "Last blocks",
"KNOWN_PEERS": "Known peers :",
"GENERAL_DIVIDER": "General information",
"ERROR": {
"LOADING_TOR_NODE_ERROR": "Could not get peer data, using the TOR network.",
"LOADING_NODE_ERROR": "Could not get peer data"
}
}
},
"WOT": {
"SEARCH_HELP": "Search (member or public key)",
"SEARCH_INIT_PHASE_WARNING": "During the pre-registration phase, the search for pending registrations <b>may be long</b>. Please wait ...",
"REGISTERED_SINCE": "Registered on",
"REGISTERED_SINCE_BLOCK": "Registered since block #",
"NO_CERTIFICATION": "No validated certification",
"NO_GIVEN_CERTIFICATION": "No given certification",
"NOT_MEMBER_PARENTHESIS": "(non-member)",
"IDENTITY_REVOKED_PARENTHESIS": "(identity revoked)",
"MEMBER_PENDING_REVOCATION_PARENTHESIS": "(being revoked)",
"EXPIRE_IN": "Expires",
"NOT_WRITTEN_EXPIRE_IN": "Deadline<br/>treatment",
"EXPIRED": "Expired",
"PSEUDO": "Pseudonym",
"SIGNED_ON_BLOCK": "Emitted on block #{{block}}",
"WRITTEN_ON_BLOCK": "Written on block #{{block}}",
"GENERAL_DIVIDER": "General information",
"NOT_MEMBER_ACCOUNT": "Non-member account",
"NOT_MEMBER_ACCOUNT_HELP": "This is a simple wallet, with no pending membership application.",
"TECHNICAL_DIVIDER": "Technical data",
"BTN_CERTIFY": "Certify",
"BTN_YES_CERTIFY": "Yes, certify",
"BTN_SELECT_AND_CERTIFY": "New certification",
"ACCOUNT_OPERATIONS": "Account operations",
"VIEW": {
"POPOVER_SHARE_TITLE": "Identity {{title}}"
},
"LOOKUP": {
"TITLE": "Registry",
"NEWCOMERS": "New members:",
"NEWCOMERS_COUNT": "{{count}} members",
"PENDING": "Pending registrations:",
"PENDING_COUNT": "{{count}} pending registrations",
"REGISTERED": "Registered {{time | formatFromNow}}",
"MEMBER_FROM": "Member since {{time|formatFromNowShort}}",
"BTN_NEWCOMERS": "Latest members",
"BTN_PENDING": "Pending registrations",
"SHOW_MORE": "Show more",
"SHOW_MORE_COUNT": "(current limit to {{limit}})",
"NO_PENDING": "No pending registrations.",
"NO_NEWCOMERS": "No members."
},
"CONTACTS": {
"TITLE": "Contacts"
},
"MODAL": {
"TITLE": "Search"
},
"CERTIFICATIONS": {
"TITLE": "{{uid}} - Certifications",
"SUMMARY": "Received certifications",
"LIST": "Details of received certifications",
"PENDING_LIST": "Pending certifications",
"RECEIVED": "Received certifications",
"RECEIVED_BY": "Certifications received by {{uid}}",
"ERROR": "Received certifications in error",
"SENTRY_MEMBER": "Referring member"
},
"OPERATIONS": {
"TITLE": "{{uid}} - Operations"
},
"GIVEN_CERTIFICATIONS": {
"TITLE": "{{uid}} - Certifications sent",
"SUMMARY": "Sent certifications",
"LIST": "Details of sent certifications",
"PENDING_LIST": "Pending certifications",
"SENT": "Sent certifications",
"SENT_BY": "Certifications sent by {{uid}}",
"ERROR": "Sent certifications with error"
}
},
"LOGIN": {
"TITLE": "<i class=\"icon ion-log-in\"></i> Login",
"SCRYPT_FORM_HELP": "Please enter your credentials. <br> Remember to check the public key for your account.",
"PUBKEY_FORM_HELP": "Please enter a public account key:",
"FILE_FORM_HELP": "Choose the keychain file to use:",
"SCAN_FORM_HELP": "Scan the QR code of a wallet.",
"SALT": "Identifier",
"SALT_HELP": "Identifier",
"SHOW_SALT": "Display identifier?",
"PASSWORD": "Password",
"PASSWORD_HELP": "Password",
"PUBKEY_HELP": "Example: « AbsxSY4qoZRzyV2irfep1V9xw1EMNyKJw2TkuVD4N1mv »",
"NO_ACCOUNT_QUESTION": "Don't have an account yet?",
"HAVE_ACCOUNT_QUESTION": "Already have an account ?",
"CREATE_ACCOUNT": "Create an account",
"CREATE_FREE_ACCOUNT": "Create a free account",
"FORGOTTEN_ID": "Forgot password?",
"ASSOCIATED_PUBKEY": "Public key :",
"BTN_METHODS": "Other methods",
"BTN_METHODS_DOTS": "Change method...",
"METHOD_POPOVER_TITLE": "Methods",
"MEMORIZE_AUTH_FILE": "Memorize this keychain during the navigation session",
"SCRYPT_PARAMETERS": "Paramètres (Scrypt) :",
"AUTO_LOGOUT": {
"TITLE": "Information",
"MESSAGE": "<i class=\"ion-android-time\"></i> You were <b>logout</ b> automatically, due to prolonged inactivity.",
"BTN_RELOGIN": "Sign In",
"IDLE_WARNING": "You will be logout... {{countdown}}"
},
"METHOD": {
"SCRYPT_DEFAULT": "Secret and password",
"SCRYPT_ADVANCED": "Advanced salt",
"FILE": "Keychain file",
"PUBKEY": "Public key only",
"SCAN": "Scan a QR code"
},
"SCRYPT": {
"SIMPLE": "Light salt",
"DEFAULT": "Standard salt",
"SECURE": "Secure salt",
"HARDEST": "Hardest salt",
"EXTREME": "Extreme salt",
"USER": "Personal value",
"N": "N (Loop):",
"r": "r (RAM):",
"p": "p (CPU):"
},
"FILE": {
"DATE" : "Date:",
"TYPE" : "Type:",
"SIZE": "Size:",
"VALIDATING": "Validating...",
"HELP": "Expected file format: <b>.dunikey</b> (type PubSec). Other formats are under development (EWIF, WIF)."
}
},
"AUTH": {
"TITLE": "<i class=\"icon ion-locked\"></i> Authentification",
"METHOD_LABEL": "Méthode d'authentification",
"BTN_AUTH": "S'authentifier",
"SCRYPT_FORM_HELP": "Veuillez vous authentifier :",
"ERROR": {
"SCRYPT_DEFAULT": "Sallage simple (par défaut)",
"SCRYPT_ADVANCED": "Sallage avancé",
"FILE": "Fichier de trousseau"
}
},
"ACCOUNT": {
"TITLE": "My Account",
"BALANCE": "Balance",
"LAST_TX": "Latest validated transactions",
"BALANCE_ACCOUNT": "Account balance",
"NO_TX": "No transaction",
"SHOW_MORE_TX": "Show more",
"SHOW_ALL_TX": "Show all",
"TX_FROM_DATE": "(current limit to {{fromTime|medianFromNowShort}})",
"PENDING_TX": "Pending transactions",
"VALIDATING_TX": "Transactions being validated",
"ERROR_TX": "Transaction not executed",
"ERROR_TX_SENT": "Sent transactions",
"PENDING_TX_RECEIVED": "Transactions awaiting receipt",
"EVENTS": "Events",
"OUT_DISTANCED": "Your current certifications come from a group too isolated from the <a ng-click=\"showHelpModal('wot')\"> Web of Trust</a> (WoT): the <a ng-click=\"showHelpModal('distance_rule')\">maximum distance rule</a> is violated.<br/>You must obtain certifications from another area of the Web of Trust, or wait for it to tighten.",
"WAITING_MEMBERSHIP": "Membership application sent. Waiting validation.",
"WAITING_CERTIFICATIONS": "You need {{needCertificationCount}} certification(s) to become a member and produce the <a ng-click=\"showHelpModal('ud')\">Universal Dividend</a>. Your account is however already operational, to receive and send payments.",
"WAITING_CERTIFICATIONS_HELP": "To get your certifications, only request members <b>who know you enough</b>, as required by <a ng-click=\"showLicenseModal()\">the currency license</a> that you have accepted.<br/>If you do not know enough members, let them know on <a ng-click=\"openLink($event, $root.settings.userForumUrl)\">the user forum</a>.",
"WILL_MISSING_CERTIFICATIONS": "You will <b>lack certifications</b> soon (at least {{willNeedCertificationCount}} more are needed)",
"WILL_NEED_RENEW_MEMBERSHIP": "Your membership <b>will expire {{membershipExpiresIn|formatDurationTo}}</b>. Remember to <a ng-click=\"doQuickFix('renew')\">renew your membership</a> before then.",
"NEED_RENEW_MEMBERSHIP": "You are no longer a member because your membership <b>has expired</b>. Remember to <a ng-click=\"doQuickFix('renew')\">renew your membership</a>.",
"NEED_RENEW_MEMBERSHIP_AFTER_CANCELLED": "You are no longer a member because your membership <b>has been canceled</b> for lack of certifications. Remember to <a ng-click=\"doQuickFix('renew')\">renew your membership</a>.",
"NO_WAITING_MEMBERSHIP": "No membership application pending. If you'd like to <b>become a member</ b>, please <a ng-click=\"doQuickFix('membership')\">send the membership application</a>.",
"CERTIFICATION_COUNT": "Received certifications",
"CERTIFICATION_COUNT_SHORT": "Certifications",
"SIG_STOCK": "Stock of certifications to give",
"BTN_RECEIVE_MONEY": "Receive",
"BTN_SELECT_ALTERNATIVES_IDENTITIES": "Switch to another identity...",
"BTN_FIX_MEMBERSHIP": "Resubmit membership request...",
"BTN_MEMBERSHIP_RENEW": "Renew membership",
"BTN_MEMBERSHIP_RENEW_DOTS": "Renew membership...",
"BTN_MEMBERSHIP_OUT_DOTS": "Revoke membership...",
"BTN_SECURITY_DOTS": "Sign-in and security...",
"BTN_SHOW_DETAILS": "Display technical data",
"LOCKED_OUTPUTS_POPOVER": {
"TITLE": "Locked amount",
"DESCRIPTION": "Here are the conditions for unlocking this amount:",
"DESCRIPTION_MANY": "This transaction consists of several parts, of which the unlock conditions are:",
"LOCKED_AMOUNT": "Conditions for the amount:"
},
"NEW": {
"TITLE": "Registration",
"INTRO_WARNING_TIME": "Creating an account on {{name|capitalize}} is very simple. Please take sufficient time to do this correctly (not to forget the usernames, passwords, etc.).",
"INTRO_WARNING_SECURITY": "Check that the hardware you are currently using (computer, tablet, phone) <b>is secure and trustworthy </b>.",
"INTRO_WARNING_SECURITY_HELP": "Up-to-date anti-virus, firewall enabled, session protected by password or pin code...",
"INTRO_HELP": "Click <b> {{'COMMON.BTN_START'|translate}}</b> to begin creating an account. You will be guided step by step.",
"REGISTRATION_NODE": "Your registration will be registered via the Duniter peer <b>{{server}}</b> node, which will then be distributed to the rest of the currency network.",
"REGISTRATION_NODE_HELP": "If you do not trust this peer, please change <a ng-click=\"doQuickFix('settings')\">in the settings</a> of Cesium.",
"SELECT_ACCOUNT_TYPE": "Choose the type of account to create:",
"MEMBER_ACCOUNT": "Member account",
"MEMBER_ACCOUNT_TITLE": "Create a member account",
"MEMBER_ACCOUNT_HELP": "If you are not yet registered as an individual (one account possible per individual).",
"WALLET_ACCOUNT": "Simple wallet",
"WALLET_ACCOUNT_TITLE": "Create a wallet",
"WALLET_ACCOUNT_HELP": "If you represent a company, association, etc. or simply need an additional wallet. No universal dividend will be created by this account.",
"SALT_WARNING": "Choose a identifier.<br/>You need it for each connection to this account.<br/><br/><b>Make sure to remember this identifier</b>.<br/>If lost, there are no means to retrieve it!",
"PASSWORD_WARNING": "Choose a password.<br/>You need it for each connection to this account.<br/><br/><b>Make sure to remember this password</b>.<br/>If lost, there are no means to retrieve it!",
"PSEUDO_WARNING": "Choose a pseudonym.<br/>It may be used by other people to find you more easily.<br/><br/>.Use of <b>commas, spaces and accents</b> is not allowed.<br/><div class='hidden-xs'><br/>Example: <span class='gray'>JohnDalton, JackieChan, etc.</span>",
"PSEUDO": "Pseudonym",
"PSEUDO_HELP": "joe123",
"SALT_CONFIRM": "Confirm",
"SALT_CONFIRM_HELP": "Confirm the identifier",
"PASSWORD_CONFIRM": "Confirm",
"PASSWORD_CONFIRM_HELP": "Confirm the password",
"SLIDE_6_TITLE": "Confirmation:",
"COMPUTING_PUBKEY": "Computing...",
"LAST_SLIDE_CONGRATULATION": "You completed all required fields.<br/><b>You can send the account creation request</b>.<br/><br/>For information, the public key below identifies your future account.<br/>It can be communicated to third parties to receive their payment.<br/>Once your account has been approved, you can find this key under <b>{{'ACCOUNT.TITLE'|translate}}</b>.",
"CONFIRMATION_MEMBER_ACCOUNT": "<b class=\"assertive\">Warning:</b> your identifier, password and pseudonym can not be changed.<br/><b>Make sure you always remember it!</b><br/><b>Are you sure</b> you want to send this account creation request?",
"CONFIRMATION_WALLET_ACCOUNT": "<b class=\"assertive\">Warning:</b> your password and pseudonym can not be changed.<br/><b>Make sure you always remember it!</b><br/><b>Are you sure</b> you want to continue?",
"CHECKING_PSEUDO": "Checking...",
"PSEUDO_AVAILABLE": "This pseudonym is available",
"PSEUDO_NOT_AVAILABLE": "This pseudonym is not available",
"INFO_LICENSE": "To be able to adhere to the currency, we ask you to kindly read and accept this license.",
"BTN_ACCEPT": "I accept",
"BTN_ACCEPT_LICENSE": "I accept the license"
},
"POPUP_REGISTER": {
"TITLE": "Enter a pseudonym",
"HELP": "A pseudonym is needed to let other members find you."
},
"SECURITY":{
"ADD_QUESTION" : "Add custom question",
"BTN_CLEAN" : "Clean",
"BTN_RESET" : "Reset",
"DOWNLOAD_REVOKE": "Save a revocation file",
"DOWNLOAD_REVOKE_HELP" : "Having a revocation file is important, for example in case of loss of identifiers. It allows you to <b>get this account out of the Web Of Trust</b>, thus becoming a simple wallet.",
"GENERATE_KEYFILE": "Generate my keychain file ...",
"GENERATE_KEYFILE_HELP": "Generate a file allowing you to authenticate without entering your identifiers.<br/><b>Warning:</b> this file will contain your secret key; It is therefore very important to put it in a safe place!",
"KEYFILE_FILENAME": "keychain-{{pubkey|formatPubkey}}-{{currency}}-{{format}}.dunikey",
"MEMBERSHIP_IN": "Register as member...",
"MEMBERSHIP_IN_HELP": "Allows you to <b>transform </b> a simple wallet account <b>into a member account</b>, by sending a membership request. Useful only if you do not already have another member account.",
"SEND_IDENTITY": "Publish identity...",
"SEND_IDENTITY_HELP": "Allows you to associate a pseudonym to this account, but <b>without applying for membership</b> to become a member. This is not very useful because the validity of this pseudonym association is limited in time.",
"HELP_LEVEL": "Choose <strong> at least {{nb}} questions </strong> :",
"LEVEL": "Security level",
"LOW_LEVEL": "Low <span class=\"hidden-xs\">(2 questions minimum)</span>",
"MEDIUM_LEVEL": "Medium <span class=\"hidden-xs\">(4 questions minimum)</span>",
"QUESTION_1": "What was your best friend's name when you were a teen ?",
"QUESTION_2": "What was the name of your first pet ?",
"QUESTION_3": "What is the first meal you have learned to cook ?",
"QUESTION_4": "What is the first movie you saw in the cinema?",
"QUESTION_5": "Where did you go the first time you flew ?",
"QUESTION_6": "What was your favorite elementary school teacher's name ?",
"QUESTION_7": "What would you consider the ideal job ?",
"QUESTION_8": "Which children's book do you prefer?",
"QUESTION_9": "What was the model of your first vehicle?",
"QUESTION_10": "What was your nickname when you were a child ?",
"QUESTION_11": "What was your favorite movie character or actor when you were a student ?",
"QUESTION_12": "What was your favorite singer or band when you were a student ?",
"QUESTION_13": "In which city did your parents meet ?",
"QUESTION_14": "What was the name of your first boss ?",
"QUESTION_15": "What is the name of the street where you grew up ?",
"QUESTION_16": "What is the name of the first beach where you go swim ?",
"QUESTION_17": "QWhat is the first album you bought ?",
"QUESTION_18": "What is the name of your favorite sport team ?",
"QUESTION_19": "What was your grand-father's job ?",
"RECOVER_ID": "Recover my password...",
"RECOVER_ID_HELP": "If you have a <b>backup file of your identifiers</b>, you can find them by answering your personal questions correctly.",
"REVOCATION_WITH_FILE" : "Rekoke my member account...",
"REVOCATION_WITH_FILE_DESCRIPTION": "If you have <b>permanently lost your member account credentials (or if account security is compromised), you can use <b>the revocation file</b> of the account <b>to quit the Web Of Trust</b>.",
"REVOCATION_WITH_FILE_HELP": "To <b>permanently revoke</ b> a member account, please drag the revocation file in the box below, or click in the box to search for a file.",
"REVOCATION_WALLET": "Revoke this account immediately",
"REVOCATION_WALLET_HELP": "Requesting revocation of your identity causes <b>will revoke your membership</ b> (definitely for the associated pseudonym and public key). The account will no longer be able to produce a Universal Dividend.<br/>However, you can still use it as a simple wallet.",
"REVOCATION_FILENAME": "revocation-{{uid}}-{{pubkey|formatPubkey}}-{{currency}}.txt",
"SAVE_ID": "Save my credentials...",
"SAVE_ID_HELP": "Creating a backup file, to <b>retrieve your password</b> (and the identifier) <b> in case of forgetting</b>. The file is <b>secured</ b> (encrypted) using personal questions.",
"STRONG_LEVEL": "Strong <span class=\"hidden-xs \">(6 questions minimum)</span>",
"TITLE": "Account and security"
},
"FILE_NAME": "{{currency}} - Account statement {{pubkey|formatPubkey}} to {{currentTime|formatDateForFile}}.csv",
"HEADERS": {
"TIME": "Date",
"AMOUNT": "Amount",
"COMMENT": "Comment"
}
},
"TRANSFER": {
"TITLE": "Transfer",
"SUB_TITLE": "Transfer money",
"FROM": "From",
"TO": "To",
"AMOUNT": "Amount",
"AMOUNT_HELP": "Amount",
"COMMENT": "Comment",
"COMMENT_HELP": "Comment (optional)",
"BTN_SEND": "Send",
"BTN_ADD_COMMENT": "Add comment?",
"MODAL": {
"TITLE": "Transfer"
}
},
"ERROR": {
"POPUP_TITLE": "Error",
"UNKNOWN_ERROR": "Unknown error",
"CRYPTO_UNKNOWN_ERROR": "Your browser is not compatible with cryptographic features.",
"FIELD_REQUIRED": "This field is required.",
"FIELD_TOO_SHORT": "Value is too short (min {{minLength]] characters).",
"FIELD_TOO_SHORT_WITH_LENGTH": "This field value is too short.",
"FIELD_TOO_LONG": "Value is exceeding max length.",
"FIELD_TOO_LONG_WITH_LENGTH": "Value is too long (max {{maxLength}} characters).",
"FIELD_MIN": "Minimum value: {{min}}",
"FIELD_MAX": "Maximal value: {{max}}",
"FIELD_ACCENT": "Commas and accent characters not allowed",
"FIELD_NOT_NUMBER": "Value is not a number",
"FIELD_NOT_INT": "Value is not an integer",
"FIELD_NOT_EMAIL": "Email adress not valid",
"PASSWORD_NOT_CONFIRMED": "Must match previous password.",
"SALT_NOT_CONFIRMED": "Must match previous identifier.",
"SEND_IDENTITY_FAILED": "Error while trying to register.",
"SEND_CERTIFICATION_FAILED": "Could not certify identity.",
"NEED_MEMBER_ACCOUNT_TO_CERTIFY": "You could not send certification, because your account is <b>not a member account</b>.",
"NEED_MEMBER_ACCOUNT_TO_CERTIFY_HAS_SELF": "You could not send certification now, because your are <b>not a member</b> yet.<br/><br/>You still need certification to become a member.",
"NOT_MEMBER_FOR_CERTIFICATION": "Your account is not a member account yet.",
"IDENTITY_TO_CERTIFY_HAS_NO_SELF": "This account could not be certified. No registration found, or need to renew.",
"LOGIN_FAILED": "Error while sign in.",
"LOAD_IDENTITY_FAILED": "Could not load identity.",
"LOAD_REQUIREMENTS_FAILED": "Could not load identity requirements.",
"SEND_MEMBERSHIP_IN_FAILED": "Error while sending registration as member.",
"SEND_MEMBERSHIP_OUT_FAILED": "Error while sending membership revocation.",
"REFRESH_WALLET_DATA": "Could not refresh wallet.",
"GET_CURRENCY_PARAMETER": "Could not get currency parameters.",
"GET_CURRENCY_FAILED": "Could not load currency. Please retry later.",
"SEND_TX_FAILED": "Could not send transaction.",
"ALL_SOURCES_USED": "Please wait the next block computation (All transaction sources has been used).",
"NOT_ENOUGH_SOURCES": "Not enough changes to send this amount in one time.<br/>Maximum amount: {{amount}} {{unit}}<sub>{{subUnit}}</sub>.",
"ACCOUNT_CREATION_FAILED": "Error while creating your member account.",
"RESTORE_WALLET_DATA_ERROR": "Error while reloading settings from local storage",
"LOAD_WALLET_DATA_ERROR": "Error while loading wallet data.",
"COPY_CLIPBOARD_FAILED": "Could not copy to clipboard",
"TAKE_PICTURE_FAILED": "Could not get picture.",
"SCAN_FAILED": "Could not scan QR code.",
"SCAN_UNKNOWN_FORMAT": "Code not recognized.",
"WOT_LOOKUP_FAILED": "Search failed.",
"LOAD_PEER_DATA_FAILED": "Duniter peer not accessible. Please retry later.",
"NEED_LOGIN_FIRST": "Please sign in first.",
"AMOUNT_REQUIRED": "Amount is required.",
"AMOUNT_NEGATIVE": "Negative amount not allowed.",
"NOT_ENOUGH_CREDIT": "Not enough credit.",
"INVALID_NODE_SUMMARY": "Unreachable peer or invalid address",
"INVALID_USER_ID": "Field 'pseudonym' must not contains spaces or special characters.",
"INVALID_COMMENT": "Field 'reference' has a bad format.",
"INVALID_PUBKEY": "Public key has a bad format.",
"IDENTITY_REVOKED": "This identity <b>has been revoked {{revocationTime|formatFromNow}}</b> ({{revocationTime|formatDate}}). It can no longer become a member.",
"IDENTITY_PENDING_REVOCATION": "The <b>revocation of this identity</b> has been requested and is awaiting processing. Certification is therefore disabled.",
"IDENTITY_INVALID_BLOCK_HASH": "This membership application is no longer valid (because it references a block that network peers are cancelled): the person must renew its application for membership <b>before</b> being certified.",
"IDENTITY_EXPIRED": "This identity has expired: this person must re-apply <b>before</b> being certified.",
"IDENTITY_SANDBOX_FULL": "Could not register, because peer's sandbox is full.<br/><br/>Please retry later or choose another Duniter peer (in <b>Settings</b>).",
"IDENTITY_NOT_FOUND": "Identity not found",
"WOT_PENDING_INVALID_BLOCK_HASH": "Membership not valid.",
"WALLET_INVALID_BLOCK_HASH": "Your membership application is no longer valid (because it references a block that network peers are cancelled).<br/>You must <a ng-click=\"doQuickFix('renew')\">renew your application for membership</a> to fix this issue.",
"WALLET_IDENTITY_EXPIRED": "The publication of your identity <b>has expired</b>.<br/>You must <a ng-click=\"doQuickFix('fixIdentity')\">re-issue your identity</a> to resolve this issue.",
"WALLET_REVOKED": "Your identity has been <b>revoked</b>: neither your pseudonym nor your public key will be used in the future for a member account.",
"WALLET_HAS_NO_SELF": "Your identity must first have been published, and not expired.",
"AUTH_REQUIRED": "Authentication required.",
"AUTH_INVALID_PUBKEY": "The public key does not match the connected account.",
"AUTH_INVALID_SCRYPT": "Invalid username or password.",
"AUTH_INVALID_FILE": "Invalid keychain file.",
"AUTH_FILE_ERROR": "Failed to open keychain file",
"IDENTITY_ALREADY_CERTIFY": "You have <b>already certified</b> that identity.<br/><br/>Your certificate is still valid (expires {{expiresIn|formatDuration}}).",
"IDENTITY_ALREADY_CERTIFY_PENDING": "You have <b>already certified</b> that identity.<br/><br/>Your certification is still pending (Deadline for treatment {{expiresIn|formatDuration}}).",
"UNABLE_TO_CERTIFY_TITLE": "Unable to certify",
"LOAD_NEWCOMERS_FAILED": "Unable to load new members.",
"LOAD_PENDING_FAILED": "Unable to load pending registrations.",
"ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION": "You must <b>be a member</b> in order to perform this action.",
"ONLY_SELF_CAN_EXECUTE_THIS_ACTION": "You must have <b>published your identity</b> in order to perform this action.",
"GET_BLOCK_FAILED": "Error while getting block",
"INVALID_BLOCK_HASH": "Block not found (incorrect hash)",
"DOWNLOAD_REVOCATION_FAILED": "Error while downloading revocation file.",
"REVOCATION_FAILED": "Error while trying to revoke the identity.",
"SALT_OR_PASSWORD_NOT_CONFIRMED": "Wrong identifier or password ",
"RECOVER_ID_FAILED": "Could not recover password",
"LOAD_FILE_FAILED" : "Unable to load file",
"NOT_VALID_REVOCATION_FILE": "Invalid revocation file (wrong file format)",
"NOT_VALID_SAVE_ID_FILE": "Invalid credentials backup file (wrong file format)",
"NOT_VALID_KEY_FILE": "Invalid keychain file (unrecognized format)",
"EXISTING_ACCOUNT": "Your identifiers correspond to an already existing account, whose <a ng-click=\"showHelpModal('pubkey')\">public key</a> is:",
"EXISTING_ACCOUNT_REQUEST": "Please modify your credentials so that they correspond to an unused account.",
"GET_LICENSE_FILE_FAILED": "Unable to get license file",
"CHECK_NETWORK_CONNECTION": "No peer appears to be accessible.<br/><br/>Please <b>check your Internet connection</b>."
},
"INFO": {
"POPUP_TITLE": "Information",
"CERTIFICATION_DONE": "Identity successfully signed",
"NOT_ENOUGH_CREDIT": "Not enough credit",
"TRANSFER_SENT": "Transfer request successfully sent",
"COPY_TO_CLIPBOARD_DONE": "Copy succeeded",
"MEMBERSHIP_OUT_SENT": "Membership revocation sent",
"NOT_NEED_MEMBERSHIP": "Already a member.",
"IDENTITY_WILL_MISSING_CERTIFICATIONS": "This identity will soon lack certification (at least {{willNeedCertificationCount}}).",
"REVOCATION_SENT": "Revocation sent successfully",
"REVOCATION_SENT_WAITING_PROCESS": "Revocation <b>has been sent successfully</b>. It is awaiting processing.",
"FEATURES_NOT_IMPLEMENTED": "This features is not implemented yet.<br/><br/>Why not to contribute to get it faster? ;)",
"EMPTY_TX_HISTORY": "No operations to export"
},
"CONFIRM": {
"POPUP_TITLE": "<b>Confirmation</b>",
"POPUP_WARNING_TITLE": "<b>Warning</b>",
"POPUP_SECURITY_WARNING_TITLE": "<i class=\"icon ion-alert-circled\"></i> <b>Security warning</b>",
"CERTIFY_RULES_TITLE_UID": "Certify {{uid}}",
"CERTIFY_RULES": "<b class=\"assertive\">Don't certify an account</b> if you believe that: <ul><li>1.) the issuers identity might be faked.<li>2.) the issuer already has another certified account.<li>3.) the issuer purposely or carelessly violates rule 1 or 2 (he certifies faked or double accounts).</ul></small><br/>Are you sure you want to certify this identity?",
"FULLSCREEN": "View the application in full screen?",
"EXIT_APP": "Close the application ?",
"TRANSFER": "<b>Transfer summary:</b><br/><br/><ul><li> - From: <b>{{from}}</b></li><li> - To: <b>{{to}}</b></li><li> - Amount: <b>{{amount}} {{unit}}</b></li><li> - Comment: <i>{{comment}}</i></li></ul><br/><b>Are-you sure you want to do this transfer?</b>",
"MEMBERSHIP_OUT": "This operation is <b>irreversible</b>.<br/></br/><b>Are you sure you want to terminate your membership?</b>",
"MEMBERSHIP_OUT_2": "This operation is <b>irreversible</b>!<br/><br/>Are you sure you want to <b>terminate your membership</b>?",
"LOGIN_UNUSED_WALLET_TITLE": "Typing error?",
"LOGIN_UNUSED_WALLET": "The account seems to be <b>inactive</b>.<br/><br/>It's probably a <b>typing error</b> when sign in. Please try again, checking that <b>public key is yours<b/>.",
"FIX_IDENTITY": "The pseudonym <b>{{uid}}</b> will be published again, replacing the old publication that has expired.<br/></br/><b>Are you sure</b> you want to continue?",
"FIX_MEMBERSHIP": "Your application for membership will be sent.<br/></br/><b>Are you sure?</b>",
"RENEW_MEMBERSHIP": "Your membership will be renewed.<br/></br/><b>Are you sure?</b>",
"REVOKE_IDENTITY": "You will <b>definitely revoke this identity</b>.<br/><br/>The public key and the associated pseudonym <b>will never be used again</b> (for a member account).<br/></br/><b>Are you sure</b> you want to revoke this identity?",
"REVOKE_IDENTITY_2": "This operation is <b>irreversible</b>!<br/><br/>Are you sure you want to <b>revoke this identity</b>?",
"NOT_NEED_RENEW_MEMBERSHIP": "Your membership does not need to be renewed (it will only expire in {{membershipExpiresIn|formatDuration}}).<br/></br/><b>Are you sure you</b> want to renew your membership?",
"SAVE_BEFORE_LEAVE": "Do you want to <b>save your changes</b> before leaving the page?",
"SAVE_BEFORE_LEAVE_TITLE": "Changes not saved",
"LOGOUT": "Are you sure you want to logout?",
"USE_FALLBACK_NODE": "Peer <b>{{old}}</b> unreachable or invalid address.<br/><br/>Do you want to temporarily use the <b>{{new}}</b> node?"
},
"DOWNLOAD": {
"POPUP_TITLE": "<b>Revocation file</b>",
"POPUP_REVOKE_MESSAGE": "To safeguard your account, please download the <b>account revocation document</b>. It will allow you to cancel your account (in case of account theft, ID, an incorrectly created account, etc.).<br/><br/><b>Please store it in a safe place.</b>"
},
"HELP": {
"TITLE": "Online help",
"JOIN": {
"SECTION": "Join",
"SALT": "The identifier is very important. It is used to hash you password, which in turn is used to calculate your <span class=\"text-italic\">public account key</span> (its number) and the private key to access it.<br/><b>Please remeber this identifier well</b>, because there is no way to recover it when lost.<br/>Furthermore, it cannot be changed without having to create a new account.<br/><br/>A good identifier must be sufficiently long (8 characters at the very least) and as original as possible.",
"PASSWORD": "The password is very important. Together with the identifier, it is use to calculate your account number (pblic key) and the private key to access it.<br/><b>Please remember it well</b>, because there is no way to recover it when lost.<br/>Furthermore, it cannot be changed without having to create a new account.<br/><br/>A good password is made (ideally) of at least 8 characters, with at least one capital and one number.",
"PSEUDO": "A pseudonym is used only when joining as <span class=\"text-italic\">member</span>. It is always associated with a wallet (by its <span class=\"text-italic\">public key</span>).<br/>It is published on the network so that other users may identify it, certify or send money to the account.<br/>A pseudonym must be unique among all members (current and past)."
},
"LOGIN": {
"SECTION": "Log in",
"PUBKEY": "Account public key",
"PUBKEY_DEF": "The public key of the keychain is generated from the entered identifiers (any), but does not correspond to an account already used.<br/><b>Make sure your public key is the same as your account</b>. Otherwise, you will be logged into an account that is probably never used, as the risk of collision with an existing account is very small.<br/><a href=\"https://en.wikipedia.org/wiki/Elliptic_curve_cryptography\" target=\"_ system\">Learn more about cryptography</a> by public key.",
"METHOD": "Méthodes de connexion",
"METHOD_DEF": "Plusieurs options sont disponibles pour vous connecter à un portfeuille :<br/> - La connexion <b>par sallage (simple ou avancé)</b> mélange votre mot de passe grâce à l'identifiant secret, pour limiter les tentatives de piratge par force brute (par exemple à partir de mots connus).<br/> - La connexion <b>par clé publique</b> évite de saisir vos identifiants, qui vous seront demandé seulement le moment venu lors d'une opération sur le compte.<br/> - La connexion <b>par fichier de trousseau</b> va lire les clés (publique et privée) du compte, depuis un fichier, sans besoin de saisir d'identifiants. Plusieurs formats de fichier sont possibles."
},
"GLOSSARY": {
"SECTION": "Glossary",
"PUBKEY_DEF": "A public key always identifies a wallet. It may identify a member. In Cesium it is calculated using the identifier and the password.",
"MEMBER": "Member",
"MEMBER_DEF": "A member is a real and living human, wishing to participate freely to the monitary community. The member will receive universal dividend, according to the period and amount as defined in the <span class=\"text-italic\">currency parameters</span>.",
"CURRENCY_RULES": "Currency rules",
"CURRENCY_RULES_DEF": "The currency rules are defined only once, and for all. They set the parameters under which the currency will perform: universal dividend calculation, the amount of certifications needed to become a member, the maximum amount of certifications a member can send, etc.<br/><br/>The parameters cannot be modified because of the use of a <span class=\"text-italic\">Blockchain</span> which carries and executes these rules, and constantly verifies their correct application. <a href=\"#/app/currency\">See current parameters</a>.",
"BLOCKCHAIN": "Blockchain",
"BLOCKCHAIN_DEF": "The Blockchain is a decentralised system which, in case of Duniter, serves to carry and execute the <span class=\"text-italic\">currency rules</span>.<br/><a href=\"http://en.duniter.org/presentation/\" target=\"_blank\">Read more about Duniter</a> and the working of its blockchain.",
"UNIVERSAL_DIVIDEND_DEF": "The Universal Dividend (UD) is the quantity of money co-created by each member, according to the period and the calculation defined in the <span class=\"text-italic\">currency rules</span>.<br/>Every term, the members receive an equal amount of new money on their account.<br/><br/>The UD undergoes a steady growth, to remain fair under its members (current and future), calculated by an average life expectancy, as demonstrated in the Relative Theory of Money (RTM).<br/><a href=\"http://trm.creationmonetaire.info\" target=\"_system\">Read more about RTM</a> and open money."
},
"TIP": {
"MENU_BTN_CURRENCY": "Menu <b>{{'MENU.CURRENCY'|translate}}</b> allows discovery of <b>currency parameters</b> and its state.",
"CURRENCY_WOT": "The <b>member count</b> shows the <b>community's weight and evolution</b>.",
"CURRENCY_MASS": "Shown here is the <b>total amount</b> currently in circulation and its <b>average distribution</b> per member.<br/><br/>This allows to estimate the <b>worth of any amount</b>, in respect to what <b>others own</b> on their account (on average).",
"CURRENCY_UNIT_RELATIVE": "The unit used here (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifies that the amounts in {{currency|capitalize}} have been devided by the <b>Universal Dividend</b> (UD).<br/><br/><small>This relative unit is <b>relevant</b> because it is stable in contrast to the permanently growing monitary mass.</small>",
"CURRENCY_CHANGE_UNIT": "The option <b>{{'COMMON.BTN_RELATIVE_UNIT'|translate}}</b> allows to <b>switch the unit</b> to show amounts in <b>{{currency|capitalize}}</b>, undevided by the Universal Dividend (instead of in &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;).",
"CURRENCY_CHANGE_UNIT_TO_RELATIVE": "The option <b>{{'COMMON.BTN_RELATIVE_UNIT'|translate}}</b> allows to <b>switch the unit</b> to show amounts in &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;, which is relative to the Universal Dividend (the amount co-produced by each member).",
"CURRENCY_RULES": "The <b>rules</b> of the currency determine its <b>exact and predictible</b> performance.<br/><br/>As a true DNA of the currency these rules make the monetary code <b>transparent and understandable</b>.",
"MENU_BTN_NETWORK": "Menu <b>{{'MENU.NETWORK'|translate}}</b> allows discovery of <b>network's state<b>.",
"NETWORK_BLOCKCHAIN": "All monetary transactions are recoded in a <b>public and tamper proof</b> ledger, generally referred to as the <b>blockchain</b>.",
"NETWORK_PEERS": "The <b>peers</b> shown here correspond to <b>computers that update and check</b> the blockchain.<br/><br/>The more active peers there are, the more <b>decentralised</b> and therefore trustworhty the currency becomes.",
"NETWORK_PEERS_BLOCK_NUMBER": "This <b>number</b> (in green) indicates the peer's <b>latest validated block</b> (last page written in the ledger).<br/><br/>Green indicates that the block was equally validated by the <b>majority of other peers</b>.",
"NETWORK_PEERS_PARTICIPATE": "<b>Each member</b>, equiped with a computer with Internet, <b>can participate, adding a peer</b> simply by <b>installing the Duniter software</b> (free/libre). <a target=\"_new\" href=\"{{installDocUrl}}\" target=\"_system\">Read the installation manual &gt;&gt;</a>.",
"MENU_BTN_ACCOUNT": "<b>{{'ACCOUNT.TITLE'|translate}}</b> allows access to your account balance and transaction history.",
"MENU_BTN_ACCOUNT_MEMBER": "Here you can consult your account status, transaction history and your certifications.",
"WALLET_CERTIFICATIONS": "Click here to reveiw the details of your certifications (given and received).",
"WALLET_RECEIVED_CERTIFICATIONS": "Click here to review the details of your <b>received certifications</b>.",
"WALLET_GIVEN_CERTIFICATIONS": "Click here to review the details of your <b>given certifications</b>.",
"WALLET_BALANCE": "Your account <b>balance</b> is shown here.",
"WALLET_BALANCE_RELATIVE": "{{'HELP.TIP.WALLET_BALANCE'|translate}}<br/><br/>The used unit (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifies that the amount in {{currency|capitalize}} has been divided by the <b>Universal Dividend</b> (UD) co-created by each member.<br/>At this moment, 1 UD equals {{currentUD}} {{currency|capitalize}}.",
"WALLET_BALANCE_CHANGE_UNIT": "You can <b>change the unit</b> in which amounts are shown in <b><i class=\"icon ion-android-settings\"></i>&nbsp;{{'MENU.SETTINGS'|translate}}</b>.<br/><br/>For example, to display amounts <b>directly in {{currency|capitalize}}</b> instead of relative amounts.",
"WALLET_PUBKEY": "This is your account public key. You can communicate it to a third party so that it more easily identifies your account.",
"WALLET_SEND": "Issue a payment in just a few clicks.",
"WALLET_SEND_NO_MONEY": "Issue a payment in just a few clicks.<br/>(Your balance does not allow this yet)",
"WALLET_OPTIONS": "Please note that this button allows access to <b>other, less used actions</b>.<br/><br/>Don't forget to take a quick look, when you have a moment!",
"WALLET_RECEIVED_CERTS": "This shows the list of persons that certified you.",
"WALLET_CERTIFY": "The button <b>{{'WOT.BTN_SELECT_AND_CERTIFY'|translate}}</b> allows selecting an identity and certifying it.<br/><br/>Only users that are <b>already member</b> may certify others.",
"WALLET_CERT_STOCK": "Your supply of certifications (to send) is limited to <b>{{sigStock}} certifications</b>.<br/><br/>This supply will replete itself over time, as and when earlier certifications expire.",
"MENU_BTN_TX_MEMBER": "<b>{{'MENU.TRANSACTIONS'|translate}}</b> allow access to transactions history, and send new payments.",
"MENU_BTN_TX": "View the history of <b>your transactions</b> here and send new payments.",
"MENU_BTN_WOT": "The menu <b>{{'MENU.WOT'|translate}}</b> allows searching <b>users</b> of the currency (member or not).",
"WOT_SEARCH_TEXT_XS": "To search in the registry, type the <b>first letters of a users pseudonym or public key</b>.<br/><br/>The search will start automatically.",
"WOT_SEARCH_TEXT": "To search in the registry, type the <b>first letters of a users pseudonym or public key</b>.<br/><br/>Then hit <b>Enter</b> to start the search.",
"WOT_SEARCH_RESULT": "Simply click a user row to view the details sheet.",
"WOT_VIEW_CERTIFICATIONS": "The row <b>{{'ACCOUNT.CERTIFICATION_COUNT'|translate}}</b> shows how many members members validated this identity.<br/><br/>These certifications testify that the account belongs to <b>a living human</b> and this person has <b>no other member account</b>.",
"WOT_VIEW_CERTIFICATIONS_COUNT": "There are at least <b>{{sigQty}} certifications</b> needed to become a member and receive the <b>Universal Dividend</b>.",
"WOT_VIEW_CERTIFICATIONS_CLICK": "Click here to open <b>a list of all certifications</b> given to and by this identity.",
"WOT_VIEW_CERTIFY": "The button <b>{{'WOT.BTN_CERTIFY'|translate}}</b> allows to add your certification to this identity.",
"CERTIFY_RULES": "<b>Attention:</b> Only certify <b>real and living persons</b> that do not own any other certified account.<br/><br/>The trust carried by the currency depends on each member's vigilance!",
"MENU_BTN_SETTINGS": "The <b>{{'MENU.SETTINGS'|translate}}</b> allow you to configure the Cesium application.<br/><br/>For example, you can <b>change the unit</b> in which the currency will be shown.",
"HEADER_BAR_BTN_PROFILE": "Click here to access your <b>user profile</b>",
"SETTINGS_CHANGE_UNIT": "You can <b>change the display unit</b> of amounts by clicking here.<br/><br/>- Deactivate the option to show amounts in {{currency|capitalize}}.<br/>- Activate the option for relative amounts in {{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub> (<b>divided</b> by the current Universal Dividend).",
"END_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>Welcome to the <b>free economy</b>!",
"END_NOT_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>If you wish to join the currency {{currency|capitalize}}, simply click <b>{{'LOGIN.CREATE_ACCOUNT'|translate}}</b> below."
}
}
}
);
$translateProvider.translations("eo-EO", {
"COMMON": {
"APP_NAME": "ğ<b>change</b>",
"APP_VERSION": "v{{version}}",
"APP_BUILD": "dato: {{build}}",
"PUBKEY": "Publika ŝlosilo",
"MEMBER": "Membro",
"BLOCK" : "Bloko",
"BTN_OK": "OK",
"BTN_YES": "Jes",
"BTN_NO": "Ne",
"BTN_SEND": "Sendi",
"BTN_SEND_MONEY": "Fari elspezon",
"BTN_SEND_MONEY_SHORT": "Elspezo",
"BTN_SAVE": "Konservi",
"BTN_YES_SAVE": "Jes, Konservi",
"BTN_YES_CONTINUE": "Jes, Daŭrigi",
"BTN_SHOW": "Vidi",
"BTN_SHOW_PUBKEY": "Afiŝi la publikan ŝlosilon",
"BTN_RELATIVE_UNIT": "Afiŝi la sumojn en UD?",
"BTN_BACK": "Reiro",
"BTN_NEXT": "Sekva",
"BTN_IMPORT": "Enporti",
"BTN_CANCEL": "Nuligi",
"BTN_CLOSE": "Fermi",
"BTN_LATER": "Poste",
"BTN_LOGIN": "Konektiĝi",
"BTN_LOGOUT": "Malkonektiĝo",
"BTN_ADD_ACCOUNT": "Nova konto",
"BTN_SHARE": "Diskonigi",
"BTN_EDIT": "Modifi",
"BTN_DELETE": "Forigi",
"BTN_ADD": "Aldoni",
"BTN_SEARCH": "Traserĉi",
"BTN_REFRESH": "Aktualigi",
"BTN_RETRY": "Rekomenci",
"BTN_START": "Komenci",
"BTN_CONTINUE": "Daŭrigi",
"BTN_CREATE": "Krei",
"BTN_UNDERSTOOD": "Mi komprenis",
"BTN_OPTIONS": "Kromeblecoj",
"BTN_HELP_TOUR": "Gvidata vizito",
"BTN_HELP_TOUR_SCREEN": "Malkovri tiun ĉi paĝon",
"BTN_DOWNLOAD": "Elŝuti",
"BTN_DOWNLOAD_ACCOUNT_STATEMENT": "Elŝuti la konto-tabelon",
"BTN_MODIFY": "Modifi",
"CHOOSE_FILE": "Almetu vian dosieron <br/>aŭ klaku por selekti ĝin",
"DAYS": "tagoj",
"NO_ACCOUNT_QUESTION": "Ankoraŭ sen konto? Kreu ĝin senpage!",
"SEARCH_NO_RESULT": "Neniu rezulto trovita",
"LOADING": "Bonvolu pacienci...",
"LOADING_WAIT": "Bonvolu pacienci...<br/><small>(Atendo pri disponebleco de la nodo)</small>",
"SEARCHING": "Serĉanta...",
"FROM": "De",
"TO": "Al",
"COPY": "Kopii",
"LANGUAGE": "Lingvo",
"UNIVERSAL_DIVIDEND": "Universala dividendo",
"UD": "UD",
"DATE_PATTERN": "DD/MM/YY HH:mm",
"DATE_FILE_PATTERN": "YYYY-MM-DD",
"DATE_SHORT_PATTERN": "DD/MM/YY",
"DATE_MONTH_YEAR_PATTERN": "MM/YYYY",
"EMPTY_PARENTHESIS": "(malplena)",
"UID": "Pseŭdonimo",
"ENABLE": "Aktiva",
"DISABLE": "Malaktiva",
"RESULTS_LIST": "Rezultoj",
"RESULTS_COUNT": "{{count}} rezultoj",
"EXECUTION_TIME": "plenumita en {{duration|formatDurationMs}}",
"SHOW_VALUES": "Afiŝi la signojn klare?",
"POPOVER_ACTIONS_TITLE": "Kromeblecoj",
"POPOVER_FILTER_TITLE": "Filtriloj",
"SHOW_MORE": "Afiŝi pli",
"SHOW_MORE_COUNT": "(nuna limo je {{limit}})",
"POPOVER_SHARE": {
"TITLE": "Diskonigi",
"SHARE_ON_TWITTER": "Diskonigi ĉe Twitter",
"SHARE_ON_FACEBOOK": "Diskonigi ĉe Facebook",
"SHARE_ON_DIASPORA": "Diskonigi ĉe Diaspora*",
"SHARE_ON_GOOGLEPLUS": "Diskonigi ĉe Google+"
},
"FILE": {
"DATE": "Dato:",
"TYPE": "Tipo:",
"SIZE": "Amplekso:",
"VALIDATING": "Validiĝanta..."
}
},
"SYSTEM": {
"PICTURE_CHOOSE_TYPE": "Elekti la fonton:",
"BTN_PICTURE_GALLERY": "Bildaro",
"BTN_PICTURE_CAMERA": "<b>Kamerao</b>"
},
"MENU": {
"HOME": "Hejmpaĝo",
"WOT": "Kontaro",
"CURRENCY": "Mono",
"ACCOUNT": "Mia konto",
"WALLETS": "Miaj monujoj",
"TRANSFER": "Elspezo",
"SCAN": "Skani",
"SETTINGS": "Parametroj",
"NETWORK": "Reto",
"TRANSACTIONS": "Miaj spezoj"
},
"ABOUT": {
"TITLE": "Prie",
"LICENSE": "Programo <b>libera</b> (licenco GNU AGPLv3).",
"LATEST_RELEASE": "Ekzistas <b>pli freŝdata versio</b> de {{'COMMON.APP_NAME'|translate}} (<b>v{{version}}</b>)",
"PLEASE_UPDATE": "Bonvolu ĝisdatigi {{'COMMON.APP_NAME'|translate}} (lasta versio: <b>v{{version}}</b>)",
"CODE": "Fonto-kodo:",
"OFFICIAL_WEB_SITE": "Oficiala retejo:",
"DEVELOPERS": "Programita de:",
"FORUM": "Forumo:",
"DEV_WARNING": "Averto",
"DEV_WARNING_MESSAGE": "Tiu ĉi programo daŭre estas programiĝanta.<br/>Ne hezitu sciigi al ni la renkontitajn fuŝaĵojn!",
"DEV_WARNING_MESSAGE_SHORT": "Tiu ĉi programo daŭre estas programiĝanta.",
"REPORT_ISSUE": "Sciigi problemon"
},
"HOME": {
"TITLE": "Cesium",
"MESSAGE": "Bonvenon ĉe {{'COMMON.APP_NAME'|translate}}!",
"MESSAGE_CURRENCY": "Interŝanĝi per {{currency|abbreviate}} fariĝas... tre simple!",
"BTN_CURRENCY": "Esplori la monon {{name|abbreviate}}",
"BTN_ABOUT": "Prie",
"BTN_HELP": "Ret-helpo",
"REPORT_ISSUE": "fuŝaĵo",
"NOT_YOUR_ACCOUNT_QUESTION" : "Vi ne posedas la konton<br/><b>{{name|| (pubkey|formatPubkey) }}</b> ?",
"BTN_CHANGE_ACCOUNT": "Malkonektu tiun ĉi konton",
"CONNECTION_ERROR": "Nodo <b>{{server}}</b> neatingebla por aliri la monon, aŭ adreso nevalida.<br/><br/>Kontrolu vian retkonekton, aŭ ŝanĝu nodon <a class=\"positive\" ng-click=\"doQuickFix('settings')\">ĉe la parametroj</a>."
},
"SETTINGS": {
"TITLE": "Parametroj",
"DISPLAY_DIVIDER": "Afiŝado",
"STORAGE_DIVIDER": "Stokado",
"NETWORK_SETTINGS": "Reto",
"PEER": "Adreso de la nodo Duniter",
"PEER_SHORT": "Adreso de la nodo",
"PEER_CHANGED_TEMPORARY": "Adreso provizore uzata",
"USE_LOCAL_STORAGE": "Aktivigi lokan stokadon",
"USE_LOCAL_STORAGE_HELP": "Ebligas konservi viajn parametrojn",
"ENABLE_HELPTIP": "Aktivigi la rilatigajn help-vezikojn",
"ENABLE_UI_EFFECTS": "Aktivigi la vid-efikojn",
"HISTORY_SETTINGS": "Miaj spezoj",
"DISPLAY_UD_HISTORY": "Afiŝi la produktitajn dividendojn?",
"AUTHENTICATION_SETTINGS": "Aŭtentigado",
"AUTO_LOGOUT": "Aŭtomata malaŭtentigado",
"AUTO_LOGOUT_OPTION_NEVER": "Neniam",
"AUTO_LOGOUT_OPTION_SECONDS": "Post {{value}} sekundoj",
"AUTO_LOGOUT_OPTION_MINUTE": "Post {{value}} minuto",
"AUTO_LOGOUT_OPTION_MINUTES": "Post {{value}} minutoj",
"AUTO_LOGOUT_OPTION_HOUR": "Post {{value}} horo",
"AUTO_LOGOUT_HELP": "Daŭro de senaktiveco antaŭ malkonektiĝo",
"REMEMBER_ME": "Memori min?",
"REMEMBER_ME_HELP": "Ebligas resti ĉiam konektita.",
"PLUGINS_SETTINGS": "Krom-programoj",
"BTN_RESET": "Restarigi la originajn valorojn",
"EXPERT_MODE": "Aktivigi la spertan moduson",
"EXPERT_MODE_HELP": "Ebligas pli detalan afiŝadon",
"POPUP_PEER": {
"TITLE": "Nodo Duniter",
"HOST": "Adreso",
"HOST_HELP": "Adreso : servilo:konektujo",
"USE_SSL": "Sekurigita?",
"USE_SSL_HELP": "(SSL-ĉifrado)",
"BTN_SHOW_LIST": "Listo de la nodoj"
}
},
"BLOCKCHAIN": {
"HASH": "Haketo: {{hash}}",
"VIEW": {
"HEADER_TITLE": "Bloko #{{number}}-{{hash|formatHash}}",
"TITLE_CURRENT": "Nuna bloko",
"TITLE": "Bloko #{{number|formatInteger}}",
"COMPUTED_BY": "Kalkulita de la nodo de",
"SHOW_RAW": "Vidi la kompletan dosieron",
"TECHNICAL_DIVIDER": "Teknikaj informoj",
"VERSION": "Versio de la daten-strukturo",
"HASH": "Kalkulita haketo",
"UNIVERSAL_DIVIDEND_HELP": "Mono kunproduktita de ĉiu el la {{membersCount}} membroj",
"EMPTY": "Neniu dateno en tiu ĉi bloko",
"POW_MIN": "Minimuma malfacileco",
"POW_MIN_HELP": "Malfacileco trudita por la haket-kalkulo",
"DATA_DIVIDER": "Darenoj",
"IDENTITIES_COUNT": "Novaj identecoj",
"JOINERS_COUNT": "Novaj membroj",
"ACTIVES_COUNT": "Revalidigoj",
"ACTIVES_COUNT_HELP": "Membroj revalidigintaj sian membrecon",
"LEAVERS_COUNT": "Membroj elirintaj",
"LEAVERS_COUNT_HELP": "Membroj ne plu dezirantaj atestaĵon",
"EXCLUDED_COUNT": "Membroj eksigitaj",
"EXCLUDED_COUNT_HELP": "Malnovaj membroj eksigitaj pro nerevalidiĝo aŭ manko de atestaĵoj",
"REVOKED_COUNT": "Nuligitaj identecoj",
"REVOKED_COUNT_HELP": "Tiuj kontoj ne plu povos esti membroj",
"TX_COUNT": "Spezoj",
"CERT_COUNT": "Atestaĵoj",
"TX_TO_HIMSELF": "Operacio pri monŝanĝo",
"TX_OUTPUT_UNLOCK_CONDITIONS": "Kondiĉoj por malblokado",
"TX_OUTPUT_OPERATOR": {
"AND": "kaj",
"OR": "aŭ"
},
"TX_OUTPUT_FUNCTION": {
"SIG": "<b>Subskribo</b> de ",
"XHX": "<b>Pasvorto</b>, el kiu SHA256 =",
"CSV": "Blokita dum",
"CLTV": "Blokita ĝis"
}
},
"LOOKUP": {
"TITLE": "Blokoj",
"NO_BLOCK": "Neniu bloko",
"LAST_BLOCKS": "Lastaj blokoj:",
"BTN_COMPACT": "Densigi"
}
},
"CURRENCY": {
"VIEW": {
"TITLE": "Mono",
"TAB_CURRENCY": "Mono",
"TAB_WOT": "Reto de fido",
"TAB_NETWORK": "Reto",
"TAB_BLOCKS": "Blokoj",
"CURRENCY_SHORT_DESCRIPTION": "{{currency|abbreviate}} estas <b>libera mono</b>, kiu ekis {{firstBlockTime|formatFromNow}}. Ĝi nombras nun <b>{{N}} membrojn</b>, kiuj produktas kaj ricevas <a ng-click=\"showHelpModal('ud')\">Universalan Dividendon</a> (UD), ĉiun {{dt|formatPeriod}}n.",
"NETWORK_RULES_DIVIDER": "Reguloj de la reto",
"CURRENCY_NAME": "Nomo de la mono",
"MEMBERS": "Nombro de membroj",
"MEMBERS_VARIATION": "Variado post la lasta UD",
"MONEY_DIVIDER": "Mono",
"MASS": "Mona maso",
"SHARE": "Maso por membro",
"UD": "Universala dividendo",
"C_ACTUAL": "Nuna kreskado",
"MEDIAN_TIME": "Horo de la blokĉeno",
"POW_MIN": "Minimuma nivelo pri malfacileco de kalkulo",
"MONEY_RULES_DIVIDER": "Reguloj de la mono",
"C_RULE": "Teoria kreskado celata",
"UD_RULE": "Kalkulo de la universala dividendo",
"DT_REEVAL": "Periodo de revalorigo de la UD",
"REEVAL_SYMBOL": "reval",
"DT_REEVAL_VALUE": "Ĉiuj <b>{{dtReeval|formatDuration}}</b> ({{dtReeval/86400}} {{'COMMON.DAYS'|translate}})",
"UD_REEVAL_TIME0": "Dato de la unua revalorigo",
"SIG_QTY_RULE": "Nombro de necesaj atestaĵoj por fariĝi membro",
"SIG_STOCK": "Maksimuma nombro da senditaj atestaĵoj por membro",
"SIG_PERIOD": "Minimuma daŭro de atendado inter 2 sinsekvaj atestaĵoj senditaj de sama persono",
"SIG_WINDOW": "Limdaŭro por akcepti atestaĵon",
"SIG_VALIDITY": "Vivdaŭro de atestaĵo, kiu estis akceptita",
"MS_WINDOW": "Limdaŭro por akcepti aliĝ-peton kiel membron",
"MS_VALIDITY": "Vivdaŭro de aliĝo, kiu estis akceptita",
"STEP_MAX": "Maksimuma distanco, per la atestaĵoj, inter nova eniranto kaj la referencaj membroj",
"WOT_RULES_DIVIDER": "Reguloj de la reto de fido",
"SENTRIES": "Nombro de atestaĵoj (senditaj <b>kaj</b> ricevitaj) por fariĝi referenca membro",
"SENTRIES_FORMULA": "Nombro de atestaĵoj (senditaj <b>kaj</b> ricevitaj) por fariĝi referenca membro (formulo)",
"XPERCENT":"Minimuma procento da referencaj membroj atingenda por konformiĝi al la regulo pri distanco",
"AVG_GEN_TIME": "Meza daŭro inter du blokoj",
"CURRENT": "nuna",
"MATH_CEILING": "PLAFONO",
"DISPLAY_ALL_RULES": "Afiŝi ĉiujn regulojn?",
"BTN_SHOW_LICENSE": "Vidi la licencon",
"WOT_DIVIDER": "Reto de fido"
},
"LICENSE": {
"TITLE": "Licenco de la mono",
"BTN_DOWNLOAD": "Elŝuti la dosieron",
"NO_LICENSE_FILE": "Dosiero pri licenco ne trovita."
}
},
"NETWORK": {
"VIEW": {
"MEDIAN_TIME": "Horo de la blokĉeno",
"LOADING_PEERS": "Nodoj ŝarĝiĝantaj...",
"NODE_ADDRESS": "Adreso:",
"SOFTWARE": "Programo",
"WARN_PRE_RELEASE": "Antaŭ-versio (lasta stabila versio: <b>{{version}}</b>)",
"WARN_NEW_RELEASE": "Versio <b>{{version}}</b> disponebla",
"WS2PID": "Identigilo:",
"PRIVATE_ACCESS": "Privata aliro",
"POW_PREFIX": "Prefikso pri labor-pruvo:",
"ENDPOINTS": {
"BMAS": "Sekurigita interfaco (SSL)",
"BMATOR": "Reta interfaco TOR",
"WS2P": "Interfaco WS2P",
"ES_USER_API": "Nodo de datenoj Cesium+"
}
},
"INFO": {
"ONLY_SSL_PEERS": "La nodoj ne-SSL estas mis-afiŝitaj, ĉar Cesium funkcias laŭ moduso HTTPS."
}
},
"PEER": {
"PEERS": "Nodoj",
"SIGNED_ON_BLOCK": "Skribita en la bloko",
"MIRROR": "spegulo",
"MIRRORS": "Speguloj",
"MIRROR_PEERS": "Spegul-nodoj",
"PEER_LIST" : "Listo de la nodoj",
"MEMBERS" : "Membroj",
"MEMBER_PEERS" : "Membro-nodoj",
"ALL_PEERS" : "Ĉiuj nodoj",
"DIFFICULTY" : "Malfacileco",
"API" : "API",
"CURRENT_BLOCK" : "Bloko #",
"POPOVER_FILTER_TITLE": "Filtrilo",
"OFFLINE": "Nekonektita",
"OFFLINE_PEERS": "Nekonektitaj nodoj",
"BTN_SHOW_PEER": "Vidi la nodon",
"VIEW": {
"TITLE": "Nodo",
"OWNER": "Apartenas al",
"SHOW_RAW_PEERING": "Vidi la samrangan dokumenton",
"SHOW_RAW_CURRENT_BLOCK": "Vidi la lastan blokon (kompleta strukturo)",
"LAST_BLOCKS": "Lastaj blokoj konataj",
"KNOWN_PEERS": "Konataj nodoj:",
"GENERAL_DIVIDER": "Ĝeneralaj informoj",
"ERROR": {
"LOADING_TOR_NODE_ERROR": "Neeblas ricevi la informojn de la nodo. La limdaŭro de atendado estas transpasita.",
"LOADING_NODE_ERROR": "Neeblas ricevi la informojn de la nodo"
}
}
},
"WOT": {
"SEARCH_HELP": "Traserĉado (pseŭdo aŭ publika ŝlosilo)",
"SEARCH_INIT_PHASE_WARNING": "Dum la periodo de antaŭ-aliĝo, la traserĉado de la atendantaj aliĝoj <b>povas esti longa</b>. Bonvolu pacienci...",
"REGISTERED_SINCE": "Enskribita la",
"REGISTERED_SINCE_BLOCK": "Enskribita en la bloko #",
"NO_CERTIFICATION": "Neniu atestaĵo validigita",
"NO_GIVEN_CERTIFICATION": "Neniu atestaĵo sendita",
"NOT_MEMBER_PARENTHESIS": "(ne membro)",
"IDENTITY_REVOKED_PARENTHESIS": "(identeco nuligita)",
"MEMBER_PENDING_REVOCATION_PARENTHESIS": "(nuliĝanta)",
"EXPIRE_IN": "Finiĝo",
"NOT_WRITTEN_EXPIRE_IN": "Limdato<br/>de traktado",
"EXPIRED": "Finiĝinta",
"PSEUDO": "Pseŭdonimo",
"SIGNED_ON_BLOCK": "Sendita en la bloko #{{block}}",
"WRITTEN_ON_BLOCK": "Enskribita en la bloko #{{block}}",
"GENERAL_DIVIDER": "Ĝeneralaj informoj",
"NOT_MEMBER_ACCOUNT": "Simpla konto (ne membro)",
"NOT_MEMBER_ACCOUNT_HELP": "Temas pri simpla monujo, sen aliĝ-peto atendanta.",
"TECHNICAL_DIVIDER": "Teknikaj informoj",
"BTN_CERTIFY": "Atesti",
"BTN_YES_CERTIFY": "Jes, atesti",
"BTN_SELECT_AND_CERTIFY": "Nova atestaĵo",
"ACCOUNT_OPERATIONS": "Spezoj en la konto",
"VIEW": {
"POPOVER_SHARE_TITLE": "Identeco {{title}}"
},
"LOOKUP": {
"TITLE": "Kontaro",
"NEWCOMERS": "Novaj membroj:",
"NEWCOMERS_COUNT": "{{count}} membroj",
"PENDING": "Atendantaj enskribiĝoj",
"PENDING_COUNT": "{{count}} atendantaj enskribiĝoj",
"REGISTERED": "Enskribita {{time | formatFromNow}}",
"MEMBER_FROM": "Membro depost {{time|formatFromNowShort}}",
"BTN_NEWCOMERS": "Novaj membroj",
"BTN_PENDING": "Atendantaj enskribiĝoj",
"SHOW_MORE": "Afiŝi pli",
"SHOW_MORE_COUNT": "(nuna limo je {{limit}})",
"NO_PENDING": "Neniu enskribiĝo atendanta.",
"NO_NEWCOMERS": "Neniu membro."
},
"CONTACTS": {
"TITLE": "Kontaktoj"
},
"MODAL": {
"TITLE": "Traserĉado"
},
"CERTIFICATIONS": {
"TITLE": "{{uid}} - Atestaĵoj",
"SUMMARY": "Ricevitaj atestaĵoj",
"LIST": "Detalo pri la ricevitaj atestaĵoj",
"PENDING_LIST": "Atestaĵoj atendantaj traktadon",
"RECEIVED": "Ricevitaj atestaĵoj",
"RECEIVED_BY": "Atestaĵoj ricevitaj de {{uid}}",
"ERROR": "Atestaĵoj erare ricevitaj",
"SENTRY_MEMBER": "Referenca membro"
},
"OPERATIONS": {
"TITLE": "{{uid}} - Spezoj"
},
"GIVEN_CERTIFICATIONS": {
"TITLE": "{{uid}} - Senditaj atestaĵoj",
"SUMMARY": "Senditaj atestaĵoj",
"LIST": "Detalo pri la senditaj atestaĵoj",
"PENDING_LIST": "Atestaĵoj atendantaj traktadon",
"SENT": "Senditaj atestaĵoj",
"SENT_BY": "Atestaĵoj senditaj de {{uid}}",
"ERROR": "Atestaĵoj erare senditaj"
}
},
"LOGIN": {
"TITLE": "<i class=\"icon ion-log-in\"></i> Konektiĝo",
"SCRYPT_FORM_HELP": "Bonvolu tajpi viajn identigilojn.<br>Pensu kontroli, ke la publika ŝlosilo estas tiu de via konto.",
"PUBKEY_FORM_HELP": "Bonvolu tajpi publikan ŝlosilon de konto:",
"FILE_FORM_HELP": "Elektu la ŝlosilaro-dosieron uzotan:",
"SCAN_FORM_HELP": "Skani la QR-kodon de monujo.",
"SALT": "Identigilo",
"SALT_HELP": "Identigilo",
"SHOW_SALT": "Afiŝi la identigilon?",
"PASSWORD": "Pasvorto",
"PASSWORD_HELP": "Pasvorto",
"PUBKEY_HELP": "Ekzemple: « AbsxSY4qoZRzyV2irfep1V9xw1EMNyKJw2TkuVD4N1mv »",
"NO_ACCOUNT_QUESTION": "Vi ankoraŭ ne havas konton?",
"HAVE_ACCOUNT_QUESTION": "Vi jam havas konton?",
"CREATE_ACCOUNT": "Krei konton...",
"CREATE_FREE_ACCOUNT": "Krei konton senpage",
"FORGOTTEN_ID": "Pasvorto forgesita?",
"ASSOCIATED_PUBKEY": "Publika ŝlosilo de la ŝlosilaro:",
"BTN_METHODS": "Aliaj metodoj",
"BTN_METHODS_DOTS": "Ŝanĝi metodon...",
"METHOD_POPOVER_TITLE": "Metodoj",
"MEMORIZE_AUTH_FILE": "Memorigi tiun ŝlosilaron por la daŭro de la sesio de retumado",
"SCRYPT_PARAMETERS": "Parametroj (Skripto):",
"AUTO_LOGOUT": {
"TITLE": "Informo",
"MESSAGE": "<i class=\"ion-android-time\"></i> Vi estis <b>malkonektita</b> aŭtomate, pro tro longa senaktiveco.",
"BTN_RELOGIN": "Rekonektiĝi",
"IDLE_WARNING": "Vi estos malkonektita... {{countdown}}"
},
"METHOD": {
"SCRYPT_DEFAULT": "Identigilo kaj pasvorto",
"SCRYPT_ADVANCED": "Sperta salumado",
"FILE": "Dosiero pri ŝlosilaro",
"PUBKEY": "Publika ŝlosilo aŭ pseŭdonimo",
"SCAN": "Skani QR-kodon"
},
"SCRYPT": {
"SIMPLE": "Malpeza salumado",
"DEFAULT": "Kutima salumado",
"SECURE": "Sekura salumado",
"HARDEST": "Plej sekura salumado",
"EXTREME": "Ekstrema salumado",
"USER": "Personigita salumado",
"N": "N (Loop):",
"r": "r (RAM):",
"p": "p (CPU):"
},
"FILE": {
"HELP": "Atendita strukturo de dosiero: <b>.yml</b> aŭ <b>.dunikey</b> (tipo PubSec, WIF aŭ EWIF)."
}
},
"AUTH": {
"TITLE": "<i class=\"icon ion-locked\"></i> Aŭtentigado",
"METHOD_LABEL": "Metodo por aŭtentiĝi",
"BTN_AUTH": "Aŭtentiĝi",
"SCRYPT_FORM_HELP": "Bonvolu aŭtentiĝi:",
"ERROR": {
"SCRYPT_DEFAULT": "Simpla salumado (implicite)",
"SCRYPT_ADVANCED": "Sperta salumado",
"FILE": "Dosiero pri ŝlosilaro"
}
},
"ACCOUNT": {
"TITLE": "Mia konto",
"BALANCE": "Saldo",
"LAST_TX": "Lastaj spezoj",
"BALANCE_ACCOUNT": "Konto-saldo",
"NO_TX": "Neniu spezo",
"SHOW_MORE_TX": "Afiŝi pli",
"SHOW_ALL_TX": "Afiŝi ĉion",
"TX_FROM_DATE": "(nuna limo je {{fromTime|medianFromNowShort}})",
"PENDING_TX": "Spezoj atendantaj traktadon",
"ERROR_TX": "Spezoj ne realigitaj",
"ERROR_TX_SENT": "Senditaj spezoj malsukcesintaj",
"PENDING_TX_RECEIVED": "Spezoj atendantaj ricevon",
"EVENTS": "Okazaĵoj",
"WAITING_MEMBERSHIP": "Aliĝo-peto sendita. Atendanta akcepton.",
"WAITING_CERTIFICATIONS": "Vi devas akiri {{needCertificationCount}} atestaĵo(j)n por fariĝi membro.",
"WILL_MISSING_CERTIFICATIONS": "Baldaŭ <b>mankos al vi atestaĵoj</b> (almenaŭ {{willNeedCertificationCount}} estas necesaj)",
"WILL_NEED_RENEW_MEMBERSHIP": "Via aliĝo kiel membro <b>estas finiĝonta {{membershipExpiresIn|formatDurationTo}}</b>. Pensu <a ng-click=\"doQuickFix('renew')\">revalidigi vian aliĝon</a> ĝis tiam..",
"NEED_RENEW_MEMBERSHIP": "Vi ne plu estas membro de la mono, ĉar <b>via aliĝo finiĝis</b>. Pensu <a ng-click=\"doQuickFix('renew')\">revalidigi vian aliĝon</a>.",
"CERTIFICATION_COUNT": "Ricevitaj atestaĵoj",
"CERTIFICATION_COUNT_SHORT": "Atestaĵoj",
"SIG_STOCK": "Senditaj atestaĵoj",
"BTN_RECEIVE_MONEY": "Enkasigi",
"BTN_MEMBERSHIP_IN_DOTS": "Fariĝi membro...",
"BTN_MEMBERSHIP_RENEW": "Revalidigi la aliĝon",
"BTN_MEMBERSHIP_RENEW_DOTS": "Revalidigi la aliĝon...",
"BTN_MEMBERSHIP_OUT_DOTS": "Ĉesigi la aliĝon...",
"BTN_SEND_IDENTITY_DOTS": "Publikigi sian identecon...",
"BTN_SECURITY_DOTS": "Konto kaj sekureco...",
"BTN_SHOW_DETAILS": "Afiŝi la teknikajn informojn",
"LOCKED_OUTPUTS_POPOVER": {
"TITLE": "Sumo blokita",
"DESCRIPTION": "Jen la kondiĉoj de malblokado de tiu sumo:",
"DESCRIPTION_MANY": "Tiu spezo entenas plurajn partojn, pri kiuj la kondiĉoj de malblokado estas:",
"LOCKED_AMOUNT": "Kondiĉoj por la sumo:"
},
"NEW": {
"TITLE": "Enskribiĝo",
"INTRO_WARNING_TIME": "La kreado de konto ĉe {{name|capitalize}} estas tre simpla. Bonvolu tamen dediĉi sufiĉe da tempo por ĝuste efektivigi tiun proceduron (por ne forgesi la identigilojn, pasvortojn, ktp.).",
"INTRO_WARNING_SECURITY": "Kontrolu ke la aparatoj, kiujn vi nun uzas (komputilo, tabuleto, telefono), <b>estas sekurigitaj kaj fidindaj</b>.",
"INTRO_WARNING_SECURITY_HELP": "Senvirusigilo ĝisdata, fajroŝirmilo aktivigita, sesio protektita per pasvorto aŭ PIN-kodo, ktp.",
"INTRO_HELP": "Alklaku <b>{{'COMMON.BTN_START'|translate}}</b> por ekigi la kreadon de konto. Vi estos gvidata paŝon post paŝo.",
"REGISTRATION_NODE": "Via aliĝo estos registrita tra la nodo Duniter <b>{{server}}</b>, kiu dissendos ĝin poste al la cetero de la mon-reto.",
"REGISTRATION_NODE_HELP": "Se vi ne fidas tiun nodon, bonvolu ŝanĝi ĝin <a ng-click=\"doQuickFix('settings')\">en la parametroj</a> de Cesium.",
"SELECT_ACCOUNT_TYPE": "Elektu la tipon de konto kreota:",
"MEMBER_ACCOUNT": "Membro-konto",
"MEMBER_ACCOUNT_TITLE": "Kreado de membro-konto",
"MEMBER_ACCOUNT_HELP": "Se vi ankoraŭ ne enskribiĝis kiel individuo (nur unu konto eblas por unu individuo). Tia konto ebligas kunprodukti la monon, ricevante <b> universalan dividendon</b> ĉiun {{parameters.dt|formatPeriod}}n.",
"WALLET_ACCOUNT": "Simpla monujo",
"WALLET_ACCOUNT_TITLE": "Kreado de monujo",
"WALLET_ACCOUNT_HELP": "Por ĉiuj aliaj kazoj, ekzemple se vi bezonas plian konton.<br/>Neniu universala dividendo estos kreita per tia konto.",
"SALT_WARNING": "Elektu vian sekretan identigilon.<br/>Oni petos ĝin de vi ĉiufoje, kiam vi konektiĝos al tiu konto.<br/><br/><b>Bone memorigu ĝin</b>: kaze de perdo, neniu alia povos aliri vian konton!",
"PASSWORD_WARNING": "Elektu pasvorton.<br/>Oni petos ĝin de vi ĉiufoje, kiam vi konektiĝos al tiu konto.<br/><br/><b>Bone memorigu tiun pasvorton</b: kaze de perdo, neniu alia povos aliri vian konton!",
"PSEUDO_WARNING": "Elektu pseŭdonimon.<br/>Ĝi utilas al la aliaj membroj, por identigi vin pli facile.<div class='hidden-xs'><br/>Ĝi <b>ne povos esti modifita</b>, sen rekrei konton.</div><br/><br/>Ĝi entenu <b>nek spacon, nek diakritan literon (kun supersigno, ktp.)</b>.<div class='hidden-xs'><br/>Ekzemple: <span class='gray'>NataljaBelulino, JohanoStelaro, ktp.</span>",
"PSEUDO": "Pseŭdonimo",
"PSEUDO_HELP": "Pseŭdonimo",
"SALT_CONFIRM": "Konfirmo",
"SALT_CONFIRM_HELP": "Konfirmo de la sekreta identigilo",
"PASSWORD_CONFIRM": "Konfirmo",
"PASSWORD_CONFIRM_HELP": "Konfirmo de la pasvorto",
"SLIDE_6_TITLE": "Konfirmo:",
"COMPUTING_PUBKEY": "Kalkulanta...",
"LAST_SLIDE_CONGRATULATION": "Vi tajpis ĉiujn necesajn informojn: Gratulon!<br/>Vi nun povas <b>sendi la peton por kreado</b> de la konto.</b><br/><br/>Por informo, la publika ŝlosilo ĉi-sube identigos vian estontan konton.<br/>Ĝi povos estis sciigita al aliuloj por ricevi iliajn pagojn.<br/><b>Ne estas devige</b> noti ĝin nun, vi ankaŭ povos fari tion poste.",
"CONFIRMATION_MEMBER_ACCOUNT": "<b class=\"assertive\">Averto:</b> la sekreta identigilo, la pasvorto kaj la pseŭdonimo ne plu povos esti modifitaj.<br/><br/><b>Certiĝu, ke vi ĉiam rememorigos ĝin!</b><br/><br/><b>Ĉu vi certas</b>, ke vi deziras sendi tiun ĉi aliĝo-peton?",
"CONFIRMATION_WALLET_ACCOUNT": "<b class=\"assertive\">Averto:</b> la sekreta identigilo kaj la pasvorto ne plu povos esti modifitaj.<br/><br/><b>Certiĝu, ke vi ĉiam rememorigos ĝin!</b><br/><br/><b>Ĉu vi certas</b>, ke vi deziras daŭrigi per tiuj ĉi identigiloj?",
"CHECKING_PSEUDO": "Kontrolo...",
"PSEUDO_AVAILABLE": "Pseŭdonimo disponebla",
"PSEUDO_NOT_AVAILABLE": "Pseŭdonimo ne disponebla",
"INFO_LICENSE": "Antaŭ ol krei membro-konton, <b>bonvolu legi kaj akcepti la licencon</b> pri uzado de la mono:",
"BTN_ACCEPT": "Mi akceptas",
"BTN_ACCEPT_LICENSE": "Mi akceptas la licencon"
},
"POPUP_REGISTER": {
"TITLE": "Elektu pseŭdonimon",
"HELP": "Pseŭdonimo estas deviga por fariĝi membro."
},
"SECURITY": {
"ADD_QUESTION": "Aldoni personigitan demandon",
"BTN_CLEAN": "Malplenigi",
"BTN_RESET": "Restartigi",
"DOWNLOAD_REVOKE": "Konservi mian dosieron pri nuligo",
"DOWNLOAD_REVOKE_HELP": "Disponi dosieron pri nuligo estas grave, ekzemple kaze de perdo de viaj identigiloj. Ĝi ebligas al vi <b>elirigi tiun konton el la reto de fido</b>, tiel ke ĝi refariĝu simpla monujo.",
"HELP_LEVEL": "Por krei konserv-dosieron pri viaj identigiloj, elektu <strong> almenaŭ {{nb}} demandojn:</strong>",
"LEVEL": "Nivelo de sekureco",
"LOW_LEVEL": "Malforta <span class=\"hidden-xs\">(2 demandoj minimume)</span>",
"MEDIUM_LEVEL": "Meza <span class=\"hidden-xs\">(4 demandoj minimume)</span>",
"QUESTION_1": "Kiel nomiĝis via plej bona amik.in.o, kiam vi estis adoleskant.in.o?",
"QUESTION_2": "Kiel nomiĝis via unua hejm-besto?",
"QUESTION_3": "Kiun pladon vi unue lernis kuiradi?",
"QUESTION_4": "Kiun filmon vi unue spektis en kinejo?",
"QUESTION_5": "Kien vi iris la unuan fojon, kiam vi vojaĝis per aviadilo?",
"QUESTION_6": "Kiel nomiĝis via preferata instruist.i.no en bazlernejo?",
"QUESTION_7": "Kio estus laŭ vi la ideala profesio?",
"QUESTION_8": "Kiun libron por infanoj vi preferas?",
"QUESTION_9": "Kio estis la marko de via unua veturilo?",
"QUESTION_10": "Kio estis via kromnomo, kiam vi estis infano?",
"QUESTION_11": "Kiun rolant.in.on aŭ aktor.in.on vi preferis en kino, kiam vi estis student.in.o?",
"QUESTION_12": "Kiun kanzonist.ino.n aŭ muzikgrupon vi preferis, kiam vi estis student.in.o?",
"QUESTION_13": "En kiu urbo renkontiĝis viaj gepatroj?",
"QUESTION_14": "Kiel nomiĝis via unua ĉefo?",
"QUESTION_15": "Kiel nomiĝas la strato, kie vi kreskis?",
"QUESTION_16": "Kiel nomiĝas la marbordo, kie vi unuafoje baniĝis?",
"QUESTION_17": "Kiun muzik-albumon vi unuafoje aĉetis?",
"QUESTION_18": "Kiel nomiĝas via preferata sporto-teamo?",
"QUESTION_19": "Kio estis la profesio de via avo?",
"RECOVER_ID": "Retrovi mian pasvorton...",
"RECOVER_ID_HELP": "Se vi disponas <b>konserv-dosieron pri viaj identigiloj</b>, vi povas retrovi ilin respondante ĝuste viajn personajn demandojn.",
"REVOCATION_WITH_FILE": "Nuligi mian membro-konton...",
"REVOCATION_WITH_FILE_HELP": "Se vi <b>definitive perdis viajn identigilojn</b> pri via membro-konto (aŭ ke la sekureco de la konto estas endanĝerigita), vi povas uzi <b>la dosieron pri nuligo</b> de la konto por <b>trudi ties definitivan eliradon el la reto de fido</b>.",
"REVOCATION_WALLET": "Nuligi tiun ĉi konton tuj",
"REVOCATION_WALLET_HELP": "Peti la nuligon de via identeco estigas la <b>eliradon el la reto de fido</b> (definitivan por la pseŭdonimo kaj la publika ŝlosilo kunligitaj). La konto ne plu povos produkti Universalan Dividendon.<br/>Vi tamen daŭre povos konektiĝi al ĝi, kiel al simpla monujo.",
"REVOCATION_FILENAME": "nuligo-{{uid}}-{{pubkey|formatPubkey}}-{{currency}}.txt",
"SAVE_ID": "Konservi miajn identigilojn...",
"SAVE_ID_HELP": "Kreado de konserv-dosiero, por <b>retrovi vian pasvorton</b> (kaj la sekretan identigilon) <b>kaze de forgeso</b>. La dosiero estas <b>sekurigita</b> (ĉifrita) dank'al personaj demandoj.",
"STRONG_LEVEL": "Forta <span class=\"hidden-xs \">(6 demandoj minimume)</span>",
"TITLE": "Konto kaj sekureco"
},
"FILE_NAME": "{{currency}} - Konto-tabelo {{pubkey|formatPubkey}} je {{currentTime|formatDateForFile}}.csv",
"HEADERS": {
"TIME": "Dato",
"AMOUNT": "Sumo",
"COMMENT": "Komento"
}
},
"TRANSFER": {
"TITLE": "Elspezo",
"SUB_TITLE": "Fari elspezon",
"FROM": "De",
"TO": "Al",
"AMOUNT": "Sumo",
"AMOUNT_HELP": "Sumo",
"COMMENT": "Komento",
"COMMENT_HELP": "Komento",
"BTN_SEND": "Sendi",
"BTN_ADD_COMMENT": "Aldoni komenton?",
"MODAL": {
"TITLE": "Elspezo"
}
},
"ERROR": {
"POPUP_TITLE": "Eraro",
"UNKNOWN_ERROR": "Eraro nekonata",
"CRYPTO_UNKNOWN_ERROR": "Via retumilo ŝajnas ne kongrua kun la kriptografiaj funkcioj.",
"FIELD_REQUIRED": "Deviga kampo",
"FIELD_TOO_SHORT": "Signaro tro mallonga",
"FIELD_TOO_SHORT_WITH_LENGTH": "Signaro tro mallonga ({{minLength}} signoj minimume)",
"FIELD_TOO_LONG": "Signaro tro longa",
"FIELD_TOO_LONG_WITH_LENGTH": "Signaro tro longa ({{maxLength}} signoj maksimume)",
"FIELD_MIN": "Minimuma longeco: {{min}}",
"FIELD_MAX": "Maksimuma longeco: {{max}}",
"FIELD_ACCENT": "Diakritaj literoj kaj komoj ne permesataj",
"FIELD_NOT_NUMBER": "Nombra valoro atendata",
"FIELD_NOT_INT": "Entjera nombro atendata",
"FIELD_NOT_EMAIL": "Retadreso nevalida",
"PASSWORD_NOT_CONFIRMED": "Ne kongruas kun la pasvorto",
"SALT_NOT_CONFIRMED": "Ne kongruas kun la sekreta identigilo",
"SEND_IDENTITY_FAILED": "Aliĝo malsukcesa",
"SEND_CERTIFICATION_FAILED": "Atestado malsukcesa",
"NEED_MEMBER_ACCOUNT_TO_CERTIFY": "Vi ne povas efektivigi atestadon, ĉar via konto <b>ne estas membro</b>.",
"NEED_MEMBER_ACCOUNT_TO_CERTIFY_HAS_SELF": "Vi ne povas efektivigi atestadon, ĉar via konto ankoraŭ ne estas membro.<br/><br/>Ankoraŭ mankas al vi atestaĵoj, aŭ tiuj ĉi ankoraŭ ne estis validigitaj.",
"NOT_MEMBER_FOR_CERTIFICATION": "Via konto ankoraŭ ne estas membro.",
"IDENTITY_TO_CERTIFY_HAS_NO_SELF": "Konto ne atestebla. Neniu aliĝo-peto estis farita, aŭ la aliĝo ne estis revalidigita.",
"LOGIN_FAILED": "Eraro dum konektiĝo.",
"LOAD_IDENTITY_FAILED": "Eraro por ŝarĝi la identecon.",
"LOAD_REQUIREMENTS_FAILED": "Eraro por ŝarĝi la antaŭ-necesaĵoj de la identeco.",
"SEND_MEMBERSHIP_IN_FAILED": "Malsukceso pri la provado eniri la komunumon.",
"SEND_MEMBERSHIP_OUT_FAILED": "Malsukceso pri la ĉesigo de la aliĝo.",
"REFRESH_WALLET_DATA": "Malsukceso pri la ĝisdatigo de la monujo.",
"GET_CURRENCY_PARAMETER": "Malsukceso por ricevi la regulojn de la mono.",
"GET_CURRENCY_FAILED": "Ne eblis ŝarĝi la monon. Bonvolu reprovi pli poste.",
"SEND_TX_FAILED": "Elspezado malsukcesa.",
"ALL_SOURCES_USED": "Bonvolu atendi la kalkulon de la venonta bloko (ĉiuj viaj monfontoj estis uzitaj).",
"NOT_ENOUGH_SOURCES": "Ne sufiĉe da mono por sendi tiun ĉi sumon per ununura spezo.<br/>Maksimuma sumo: {{amount}} {{unit}}<sub>{{subUnit}}</sub>.",
"ACCOUNT_CREATION_FAILED": "Malsukceso por krei la membro-konton.",
"RESTORE_WALLET_DATA_ERROR": "Malsukceso por reŝarĝi la parametrojn de la loka stokaĵo",
"LOAD_WALLET_DATA_ERROR": "Malsukceso por ŝarĝi la datenojn de la monujo.",
"COPY_CLIPBOARD_FAILED": "Ne eblis kopii la valoron.",
"TAKE_PICTURE_FAILED": "Malsukceso por ricevi la foton.",
"SCAN_FAILED": "Malsukceso por skani la QR-kodon.",
"SCAN_UNKNOWN_FORMAT": "Kodo nerekonata.",
"WOT_LOOKUP_FAILED": "Serĉado malsukcesa.",
"LOAD_PEER_DATA_FAILED": "Ne eblis legi la nodon Duniter. Bonvolu reprovi poste.",
"NEED_LOGIN_FIRST": "Bonvolu unue konektiĝi.",
"AMOUNT_REQUIRED": "La monsumo estas deviga.",
"AMOUNT_NEGATIVE": "Negativa sumo nepermesata.",
"NOT_ENOUGH_CREDIT": "Saldo nesufiĉa.",
"INVALID_NODE_SUMMARY": "Nodo neatingebla aŭ adreso nevalida.",
"INVALID_USER_ID": "La pseŭdonimo devas enteni nek spacon nek signon specialan aŭ kun supersigno.",
"INVALID_COMMENT": "La kampo 'referenco' ne devas enteni literojn kun supersigno.",
"INVALID_PUBKEY": "La publika ŝlosilo ne havas la atenditan strukturon.",
"IDENTITY_REVOKED": "Tiu ĉi identeco <b>estis nuligita</b>. Ĝi ne plu povas fariĝi membro.",
"IDENTITY_PENDING_REVOCATION": "La <b>nuligo de tiu ĉi identeco</b> estis petita kaj atendas traktadon. La atestado estas do malaktivigita.",
"IDENTITY_INVALID_BLOCK_HASH": "Tiu ĉi aliĝo-peto ne plu validas (ĉar ĝi rilatas al bloko, kiun nuligis la nodoj de la reto): tiu persono devas refari sian aliĝo-peton <b>antaŭ ol</b> esti atestita.",
"IDENTITY_EXPIRED": "La publikigo de tiu ĉi identeco finiĝis: tiu persono devas fari novan aliĝo-peton <b>antaŭ ol</b> esti atestita.",
"IDENTITY_SANDBOX_FULL": "La nodo Duniter uzata de Cesium ne plu povas ricevi novajn identecojn, ĉar ĝia atendo-vico estas plena.<br/><br/>Bonvolu reprovi poste aŭ ŝanĝi la nodon (per la menuo <b>Parametroj</b>).",
"IDENTITY_NOT_FOUND": "Identeco ne trovita.",
"WOT_PENDING_INVALID_BLOCK_HASH": "Aliĝo ne valida.",
"WALLET_INVALID_BLOCK_HASH": "Via aliĝo-peto ne plu validas (ĉar ĝi rilatas al bloko, kiun nuligis la nodoj de la reto).<br/>Vi devas <a ng-click=\"doQuickFix('fixMembership')\">sendi novan peton</a> por solvi tiun ĉi problemon.",
"WALLET_IDENTITY_EXPIRED": "La publikigo de <b>via identeco finiĝis</b>.<br/>Vi devas <a ng-click=\"doQuickFix('fixIdentity')\">publikigi denove vian identecon</a> por solvi tiun ĉi problemon.",
"WALLET_REVOKED": "Via identeco estis <b>nuligita</b>: nek via pseŭdonimo nek via publika ŝlosilo povos esti uzata en la estonteco por membro-konto.",
"WALLET_HAS_NO_SELF": "Via identeco devas unue esti publikigita, kaj ne esti finiĝinta.",
"AUTH_REQUIRED": "Aŭtentigado necesa.",
"AUTH_INVALID_PUBKEY": "La publika ŝlosilo ne rilatas al la konektita konto.",
"AUTH_INVALID_SCRYPT": "Identigilo aŭ pasvorto nevalida.",
"AUTH_INVALID_FILE": "Dosiero pri ŝlosilaro nevalida.",
"AUTH_FILE_ERROR": "Malsukceso por malfermi la dosieron pri ŝlosilaro.",
"IDENTITY_ALREADY_CERTIFY": "Vi <b>jam atestis</b> tiun identecon.<br/><br/>Tiu atestado daŭre validas (finiĝo {{expiresIn|formatDurationTo}}).",
"IDENTITY_ALREADY_CERTIFY_PENDING": "Vi <b>jam atestis</b> tiun identecon.<br/><br/>Tiu atestado atendas traktadon (limdato de traktado {{expiresIn|formatDurationTo}}).",
"UNABLE_TO_CERTIFY_TITLE": "Atestado neebla",
"LOAD_NEWCOMERS_FAILED": "Malsukceso por ŝarĝi la novajn membrojn.",
"LOAD_PENDING_FAILED": "Malsukceso por ŝarĝi la atendantajn aliĝojn.",
"ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION": "Vi devas <b>esti membro</b> por rajti efektivigi tiun ĉi agon.",
"ONLY_SELF_CAN_EXECUTE_THIS_ACTION": "Via identeco devas <b>jam esti publikigita</b>, por ke vi rajtu efektivigi tiun ĉi agon.",
"GET_BLOCK_FAILED": "Malsukceso por ricevi la blokon.",
"INVALID_BLOCK_HASH": "Bloko ne trovita (haketo malsama)",
"DOWNLOAD_REVOCATION_FAILED": "Malsukceso por elŝuti la dosieron pri nuligo.",
"REVOCATION_FAILED": "Malsukceso pri nuligo.",
"SALT_OR_PASSWORD_NOT_CONFIRMED": "Sekreta identigilo aŭ pasvorto malĝusta.",
"RECOVER_ID_FAILED": "Malsukceso por ricevi la identigilojn",
"LOAD_FILE_FAILED" : "Malsukceso por ŝarĝi la dosieron",
"NOT_VALID_REVOCATION_FILE": "Dosiero pri nuligo ne valida (malĝusta strukturo de dosiero)",
"NOT_VALID_SAVE_ID_FILE": "Dosiero pri konservado ne valida (malĝusta strukturo de dosiero)",
"NOT_VALID_KEY_FILE": "Dosiero pri ŝlosilaro ne valida (strukturo ne rekonata)",
"EXISTING_ACCOUNT": "Viaj identigiloj rilatas al jam ekzistanta konto, kies <a ng-click=\"showHelpModal('pubkey')\">publika ŝlosilo</a> estas:",
"EXISTING_ACCOUNT_REQUEST": "Bonvolu modifi viajn identigilojn, por ke ili rilatu al ne uzata konto.",
"GET_LICENSE_FILE_FAILED": "La ricevo de la dosiero pri licenco ne eblis.",
"CHECK_NETWORK_CONNECTION": "Neniu nodo ŝajnas atingebla.<br/><br/>Bonvolu <b>kontroli vian retkonekton</b>.",
},
"INFO": {
"POPUP_TITLE": "Informo",
"CERTIFICATION_DONE": "Atestaĵo sendita",
"NOT_ENOUGH_CREDIT": "Saldo nesufiĉa",
"TRANSFER_SENT": "Elspezo sendita",
"COPY_TO_CLIPBOARD_DONE": "Kopio efektivigita",
"MEMBERSHIP_OUT_SENT": "Eksiĝo sendita",
"NOT_NEED_MEMBERSHIP": "Vi jam estas membro.",
"IDENTITY_WILL_MISSING_CERTIFICATIONS": "Al tiu ĉi identeco baldaŭ mankos atestaĵoj (almenaŭ {{willNeedCertificationCount}}).",
"REVOCATION_SENT": "Nuligo sendita",
"REVOCATION_SENT_WAITING_PROCESS": "La <b>nuligo de tiu ĉi identeco</b> estis petita kaj atendas traktadon.",
"FEATURES_NOT_IMPLEMENTED": "Tiu ĉi funkciaro ankoraŭ estas programiĝanta.<br/>Kial ne <b>kontribui al Cesium</b>, por ekhavi ĝin pli rapide? ;)",
"EMPTY_TX_HISTORY": "Neniu spezo elportota"
},
"CONFIRM": {
"POPUP_TITLE": "<b>Konfirmo</b>",
"POPUP_WARNING_TITLE": "<b>Averto</b>",
"POPUP_SECURITY_WARNING_TITLE": "<i class=\"icon ion-alert-circled\"></i> <b>Averto pri sekureco</b>",
"CERTIFY_RULES_TITLE_UID": "Atesti {{uid}}",
"CERTIFY_RULES": "<b class=\"assertive\">NE atestu</b> konton, se vi pensas ke:<br/><br/><ul><li>1.) ĝi ne rilatas al persono <b>fizika kaj vivanta</b>.<li>2.) ĝia posedanto <b>havas alian konton</b> jam atestitan.<li>3.) ĝia posedanto malobservas (vole aŭ ne) la regulon 1 aŭ 2 (ekzemple atestante falsajn kontojn aŭ duoblajn).</ul><br/><b>Ĉu vi certas,</b> ke vi tamen volas atesti tiun ĉi identecon?",
"FULLSCREEN": "Afiŝi la programon plen-ekrane?",
"EXIT_APP": "Fermi la programon?",
"TRANSFER": "<b>Resumo de la elspezo</b> :<br/><br/><ul><li> - De: {{from}}</li><li> - Al: <b>{{to}}</b></li><li> - Sumo: <b>{{amount}} {{unit}}</b></li><li> - Komento: <i>{{comment}}</i></li></ul><br/><b>Ĉu vi certas, ke vi volas efektivigi tiun ĉi elspezon?</b>",
"MEMBERSHIP_OUT": "Tiu ĉi ago estas <b>neinversigebla</b>.<br/></br/>Ĉu vi certas, ke vi volas <b>nuligi vian membro-konton</b>?",
"MEMBERSHIP_OUT_2": "Tiu ĉi ago estas <b>neinversigebla</b> !<br/><br/>Ĉu vi vere certas, ke vi volas <b>nuligi vian aliĝon</b> kiel membron?",
"LOGIN_UNUSED_WALLET_TITLE": "Tajperaro?",
"LOGIN_UNUSED_WALLET": "La konektita konto ŝajnas <b>neaktiva</b>.<br/><br/>Temas probable pri <b>tajperaro</b> en viaj konekto-identigiloj. Bonvolu rekomenci, kontrolante ke <b>la publika ŝlosilo estas tiu de via konto</b>.",
"FIX_IDENTITY": "La pseŭdonimo <b>{{uid}}</b> estos denove publikigita, anstataŭigante la malnovan publikigon, kiu finiĝis.<br/></br/><b>Ĉu vi certas</b>, ke vi volas daŭrigi?",
"FIX_MEMBERSHIP": "Via aliĝo-peto kiel membro tuj estos resendita.<br/></br/><b>Ĉu vi certas</b>, ke vi volas daŭrigi?",
"RENEW_MEMBERSHIP": "Via aliĝo kiel membro tuj estos revalidigita.<br/></br/><b>Ĉu vi certas</b>, ke vi volas daŭrigi?",
"REVOKE_IDENTITY": "Vi estas <b>nuligonta definitive tiun ĉi identecon</b>.<br/><br/>La publika ŝlosilo kaj la ligita pseŭdonimo <b>neniam plu povos esti uzataj</b> (por membro-konto). <br/></br/><b>Ĉu vi certas</b>, ke vi volas definitive nuligi tiun ĉi konton?",
"REVOKE_IDENTITY_2": "Tiu ĉi ago estas <b>neinversigebla</b>!<br/><br/>Ĉu vi vere certas, ke vi volas <b>definitive nuligi</b> tiun ĉi konton?",
"NOT_NEED_RENEW_MEMBERSHIP": "Via aliĝo ne bezonas esti revalidigita (ĝi finiĝos nur post {{membershipExpiresIn|formatDuration}}).<br/></br/><b>Ĉu vi certas</b>, ke vi volas revalidigi vian aliĝon?",
"SAVE_BEFORE_LEAVE": "Ĉu vi volas <b>konservi viajn modifojn</b> antaŭ ol eliri el la paĝo?",
"SAVE_BEFORE_LEAVE_TITLE": "Modifoj ne registritaj",
"LOGOUT": "Ĉu vi certas, ke vi volas malkonektiĝi?",
"USE_FALLBACK_NODE": "Nodo <b>{{old}}</b> neatingebla aŭ adreso nevalida.<br/><br/>Ĉu vi volas provizore uzi la nodon <b>{{new}}</b> ?",
},
"DOWNLOAD": {
"POPUP_TITLE": "<b>Dosiero pri nuligo</b>",
"POPUP_REVOKE_MESSAGE": "Por sekurigi vian konton, bonvolu elŝuti la <b>dokumenton pri konto-nuligo</b>. Ĝi ebligos al vi eventuale nuligi vian konton (kaze de konto-ŝtelo, ŝanĝo de identigilo, konto erare kreita, ktp.).<br/><br/><b>Bonvolu stoki ĝin en sekura loko.</b>"
},
"HELP": {
"TITLE": "Ret-helpo",
"JOIN": {
"SECTION": "Enskribiĝo",
"SALT": "La sekreta identigilo estas tre grava. Ĝi utilas por miksi la pasvorton, antaŭ ol ĝi servos por kalkuli la <span class=\"text-italic\">publikan ŝlosilon</span> de via konto (ties numeron) kaj la sekretan ŝlosilon por aliri ĝin.<br/><b>Zorgu pri ĝia bona memorigado</b>, ĉar neniu rimedo estas nuntempe planita por retrovi ĝin kaze de perdo.<br/>Krom tio, ĝi ne povas esti modifita sen devige krei novan konton.<br/><br/>Bona sekreta identigilo devas esti sufiĉe longa (kun almenaŭ 8 signoj) kaj kiel eble plej originala.",
"PASSWORD": "La pasvorto estas tre grava. Kun la sekreta identigilo, ĝi servas por kalkuli la numeron (la publikan ŝlosilon) de via konto, kaj la sekretan ŝlosilon por aliri ĝin.<br/><b>Zorgu pri ĝia bona memorigado</b>, ĉar neniu rimedo estas planita por retrovi ĝin kaze de perdo (krom se oni generas konserv-dosieron).<br/>Krom tio, ĝi ne povas esti modifita sen devige krei novan konton.<br/><br/>Bona pasvorto entenas (ideale) almenaŭ 8 signojn, inter kiuj estas almenaŭ unu majusklo kaj unu cifero.",
"PSEUDO": "La pseŭdonimo estas utila nur kaze de enskribiĝo kiel <span class=\"text-italic\">membro</span>. Ĝi ĉiam estas ligita kun monujo (tra ĝia <span class=\"text-italic\">publika ŝlosilo</span>).<br/>Ĝi estas publikigita en la reto, tiel ke la aliaj uzantoj povu identigi ĝin, atesti ĝin aŭ sendi monon al ĝia konto.<br/>Pseŭdonimo devas esti unika ene de la membroj (<u>nunaj</u> kaj eksaj)."
},
"LOGIN": {
"SECTION": "Konekto",
"PUBKEY": "Publika ŝlosilo de la ŝlosilaro",
"PUBKEY_DEF": "La publika ŝlosilo de la ŝlosilaro estas kreita per la tajpitaj identigiloj (iuj ajn), sen ke ili necese rilatu al konto jam uzata.<br/><b>Atente kontrolu, ke la publika ŝlosilo estas tiu de via konto</b>. Alikaze, vi estos konektita al konto probable neniam uzita, la risko de kolizio kun ekzistanta konto estante tre eta.<br/><a href=\"https://fr.wikipedia.org/wiki/Cryptographie_asym%C3%A9trique\" target=\"_system\">Scii pli pri kriptografio</a> per publika ŝlosilo.",
"METHOD": "Konekto-metodoj",
"METHOD_DEF": "Pluraj eblecoj disponeblas por konekti vin al monujo:<br/> - La konekto <b>per salumado (simpla aŭ sperta)</b> miksas vian pasvorton dank'al la sekreta identigilo, por limigi la provojn de <a href=\"https://fr.wikipedia.org/wiki/Attaque_par_force_brute\" target=\"_system\">kodrompado per kruda forto</a> (ekzemple per konataj vortoj.<br/> - La konekto <b>per publika ŝlosilo</b> evitigas tajpi viajn identigilojn, kiuj estos petataj de vi, nur kiam venos la momento dum operacio ĉe la konto.<br/> - La konekto <b>per dosiero pri ŝlosilaro</b> legas la ŝlosilojn (publikan kaj privatan) de la konto, per dosiero, sen la bezono tajpi identigilojn. Pluraj strukturoj de dosiero eblas."
},
"GLOSSARY": {
"SECTION": "Glosaro",
"PUBKEY_DEF": "Publika ŝlosilo identigas monujon, kiu povas identigi membron aŭ rilati al anonima monujo. Ĉe Cesium la publika ŝlosilo estas kalkulita (implicite) dank'al la sekreta identigilo kaj la pasvorto.<br/><a href=\"https://fr.wikipedia.org/wiki/Cryptographie_asym%C3%A9trique\" target=\"_system\">Scii pli pri kriptografio</a> per publika ŝlosilo.",
"MEMBER": "Membro",
"MEMBER_DEF": "Membro estas homa persono fizika kaj vivanta, kiu deziras libere partopreni en la mona komunumo. Li/ŝi kunproduktas universalan dividendon, laŭ periodo kaj sumo tiel difinitaj kiel en la <span class=\"text-italic\">reguloj de la mono</span>",
"CURRENCY_RULES": "Reguloj de la mono",
"CURRENCY_RULES_DEF": "La reguloj de la mono estas difinitaj definitive. Ili fiksas la funkciadon de la mono: la kalkulon de la universala dividendo, la nombron de necesaj atestaĵoj por esti membro, la maksimuman nombron da atestaĵoj, kiujn povas doni unu membro, ktp. <a href=\"#/app/currency\">Vidi la nuntempajn regulojn</a>.<br/>La nemodifo de la reguloj tra la tempo eblas per uzado de <span class=\"text-italic\">Blokĉeno</span>, kiu entenas kaj plenumas tiujn regulojn, kaj konstante kontrolas ties ĝustan aplikadon.",
"BLOCKCHAIN": "Ĉeno de blokoj (<span class=\"text-italic\">Blokchain/Blokĉeno</span>)",
"BLOCKCHAIN_DEF": "La Blokĉeno estas malcentrigita sistemo, kiu, kaze de Duniter, servas por enteni kaj plenumi la <span class=\"text-italic\">regulojn de la mono</span>.<br/><a href=\"https://duniter.org/fr/comprendre/\" target=\"_system\">Scii pli pri Duniter</a> kaj la funkciado de ties blokĉeno.",
"UNIVERSAL_DIVIDEND_DEF": "La Universala Dividendo (UD) estas la kvanto de mono kunkreita de ĉiu membro, laŭ la periodo kaj kalkulo difinitaj en la <span class=\"text-italic\">reguloj de la mono</span>.<br/>Por ĉiu perioda dato, la membroj ricevas en sian konton la saman kvanton da nova mono.<br/><br/>La UD spertas regulan kreskon, por resti justa inter la membroj (nunaj kaj venontaj), kalkulitan depende de la meza vivespero, kiel estas demonstrite en la Teorio Relativa pri la Mono (TRM).<br/><a href=\"http://trm.creationmonetaire.info\" target=\"_system\">Scii pli pri la TRM</a> kaj la liberaj monoj.",
},
"TIP": {
"MENU_BTN_CURRENCY": "La menuo <b>{{'MENU.CURRENCY'|translate}}</b> ebligas konsulti la <b>regulojn de la mono</b> kaj ties staton.",
"CURRENCY_WOT": "La <b>nombro de membroj</b> montras la gravecon de la komunumo kaj ebligas <b>sekvi ties evoluon</b>.",
"CURRENCY_MASS": "Sekvu ĉi tie la <b>ĉioman kvanton da mono</b> ekzistanta kaj ties <b>mezan distribuon</b> por membro.<br/><br/>Tio ĉi ebligas taksi la <b>gravecon de iu sumo</b>, kompare kun tio, kion <b>posedas la aliuloj</b> en sia konto (mezume).",
"CURRENCY_UNIT_RELATIVE": "La unuo uzata (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifas, ke la sumoj en {{currency|capitalize}} estis dividitaj per la <b> Universala Dividendo</b> (UD).<br/><br/><small>Tiu relativa unuo estas <b>trafa</b>, ĉar stabila malgraŭ la kvanto de mono, kiu kreskas seninterrompe.</small>",
"CURRENCY_CHANGE_UNIT": "La kromaĵo <b>{{'COMMON.BTN_RELATIVE_UNIT'|translate}}</b> ebligas <b>ŝanĝi la unuon</b>, por vidigi la sumojn <b>rekte en {{currency|capitalize}}</b> (prefere ol en &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;).",
"CURRENCY_CHANGE_UNIT_TO_RELATIVE": "La kromaĵo <b>{{'COMMON.BTN_RELATIVE_UNIT'|translate}}</b> ebligas <b>ŝanĝi la unuon</b>, por vidigi la sumojn en &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;, tio estas rilate al la Universala Dividendo (la sumo kunproduktita de ĉiu membro).",
"CURRENCY_RULES": "La <b>reguloj</b> de la mono fiksas ties funkciadon <b>ĝustan kaj antaŭvideblan</b>.<br/><br/>Vera DNA de la mono, ili igas sian monan kodon <b>legebla kaj travidebla</b>.",
"MENU_BTN_NETWORK": "La menuo <b>{{'MENU.NETWORK'|translate}}</b> ebligas konsulti la staton de la reto.",
"NETWORK_BLOCKCHAIN": "Ĉiuj operacioj pri la mono estas registritaj en granda konto-libro <b>publika kaj nefalsigebla</b>, ankaŭ nomata <b>blokĉeno</b> (<em>BlockChain</em> en la angla).",
"NETWORK_PEERS": "La <b>nodoj</b> videblaj ĉi tie rilatas al la <b>komputiloj, kiuj ĝisdatigas kaj kontrolas</b> la blokĉenon.<br/><br/>Ju pli estas nodoj, des pli la mono havas administradon <b>malcentrigitan</b> kaj fidindan.",
"NETWORK_PEERS_BLOCK_NUMBER": "Tiu ĉi <b>numero</b> (verda) indikas la <b>lastan blokon validigitan</b> por tiu ĉi nodo (lasta paĝo skribita en la granda konto-libro).<br/><br/>La verda koloro indikas, ke tiu ĉi bloko estas validigita ankaŭ de <b>la plej multaj el la aliaj nodoj</b>.",
"NETWORK_PEERS_PARTICIPATE": "<b>Ĉiu membro</b>, ekipita per komputilo kun interreto, <b>povas partopreni aldonante nodon</b>. Sufiĉas <b>instali la programon Duniter</b> (libera kaj senpaga). <a href=\"{{installDocUrl}}\" target=\"_system\">Vidi la gvidilon pri instalado &gt;&gt;</a>.",
"MENU_BTN_ACCOUNT": "La menuo <b>{{'ACCOUNT.TITLE'|translate}}</b> ebligas aliri la administradon de via konto.",
"MENU_BTN_ACCOUNT_MEMBER": "Konsultu ĉi tie la staton de via konto kaj la informojn pri viaj atestaĵoj.",
"WALLET_CERTIFICATIONS": "Alklaku ĉi tien por konsulti la detalon pri viaj atestaĵoj (ricevitaj kaj senditaj).",
"WALLET_RECEIVED_CERTIFICATIONS": "Alklaku ĉi tien por konsulti la detalon pri viaj <b>ricevitaj atestaĵoj</b>.",
"WALLET_GIVEN_CERTIFICATIONS": "Alklaku ĉi tien por konsulti la detalon pri viaj <b>senditaj atestaĵoj</b>.",
"WALLET_BALANCE": "La <b>saldo</b> de via konto afiŝiĝas tie ĉi.",
"WALLET_BALANCE_RELATIVE": "{{'HELP.TIP.WALLET_BALANCE'|translate}}<br/><br/>La uzata unuo (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifas, ke la sumo en {{currency|capitalize}} estis dividita per la <b>Universala Dividendo</b> (UD) kunkreita de ĉiu membro.<br/><br/>Nuntempe 1 UD valoras {{currentUD|formatInteger}} {{currency|capitalize}}j.",
"WALLET_BALANCE_CHANGE_UNIT": "Vi povos <b>ŝanĝi la unuon</b> afiŝitan por la sumoj en la <b><i class=\"icon ion-android-settings\"></i>&nbsp;{{'MENU.SETTINGS'|translate}}</b>.<br/><br/>Ekzemple por vidigi la sumojn <b>rekte en {{currency|capitalize}}</b>, prefere ol en relativa unuo.",
"WALLET_PUBKEY": "Jen la publika ŝlosilo de via konto. Vi povas sciigi ĝin al aliulo, por ke li identigu pli simple vian konton.",
"WALLET_SEND": "Efektivigi pagon per kelkaj klakoj.",
"WALLET_SEND_NO_MONEY": "Efektivigi pagon per kelkaj klakoj.<br/>(Via saldo ankoraŭ ne permesas tion)",
"WALLET_OPTIONS": "Tiu ĉi butono ebligas aliri la <b>agojn pri aliĝo</b> kaj sekureco.<br/><br/>Ne forgesu okulumi al ĝi!",
"WALLET_RECEIVED_CERTS": "Afiŝiĝos ĉi tie la listo de la personoj, kiuj atestis vin.",
"WALLET_CERTIFY": "La butono <b>{{'WOT.BTN_SELECT_AND_CERTIFY'|translate}}</b> ebligas elekti identecon kaj atesti ĝin.<br/><br/>Nur uzantoj <b>jam membroj</b> povas atesti aliajn.",
"WALLET_CERT_STOCK": "Via stoko da atestaĵoj (senditaj) estas limigita je <b>{{sigStock}} atestaĵoj</b>.<br/><br/>Tiu stoko plu evoluas laŭ la tempo, samtempe kiam la atestaĵoj malvalidiĝas.",
"MENU_BTN_TX_MEMBER": "La menuo <b>{{'MENU.TRANSACTIONS'|translate}}</b> ebligas konsulti vian konton, la liston de viaj spezoj, kaj sendi pagon.",
"MENU_BTN_TX": "Konsultu ĉi tie <b>la liston de viaj spezoj</b> kaj efektivigu novajn operaciojn.",
"MENU_BTN_WOT": "La menuo <b>{{'MENU.WOT'|translate}}</b> ebligas traserĉi inter la <b>uzantoj</b> de la mono (membroj aŭ ne).",
"WOT_SEARCH_TEXT_XS": "Por traserĉi en la kontaro, tajpu la <b>unuajn literojn de pseŭdonimo</b> (aŭ de publika ŝlosilo).<br/><br/>La serĉado ekos aŭtomate.",
"WOT_SEARCH_TEXT": "Por traserĉi en la kontaro, tajpu la <b>unuajn literojn de de pseŭdonimo</b> (aŭ de publika ŝlosilo). <br/><br/>Premu poste sur la klavon <b>Enigi</b> por ekigi la serĉadon.",
"WOT_SEARCH_RESULT": "Vidigu la detalan slipon simple <b>alklakante</b> linion.",
"WOT_VIEW_CERTIFICATIONS": "La linio <b>{{'ACCOUNT.CERTIFICATION_COUNT'|translate}}</b> montras kiom da membroj validigis tiun ĉi identecon.<br/><br/>Tiuj atestaĵoj pruvas, ke la konto apartenas al <b>persono homa kaj vivanta</b>, havanta <b>neniun alian membro-konton</b>.",
"WOT_VIEW_CERTIFICATIONS_COUNT": "Necesas almenaŭ <b>{{sigQty}} atestaĵoj</b> por fariĝi membro kaj ricevi la <b>Universalan Dividendon</b>.",
"WOT_VIEW_CERTIFICATIONS_CLICK": "Alklaki ĉi tien ebligas malfermi <b>la liston de ĉiuj atestaĵoj</b> de la identeco (ricevitaj kaj senditaj).",
"WOT_VIEW_CERTIFY": "La butono <b>{{'WOT.BTN_CERTIFY'|translate}}</b> ebligas aldoni vian atestaĵon al tiu identeco.",
"CERTIFY_RULES": "<b>Atenton:</b> Atestu nur <b>personojn fizikajn vivantajn</b>, posedantajn neniun alian membro-konton.<br/><br/>La sekureco de la mono dependas de ĉies atentego!",
"MENU_BTN_SETTINGS": "La <b>{{'MENU.SETTINGS'|translate}}</b> ebligos al vi agordi la programon.",
"HEADER_BAR_BTN_PROFILE": "Alklaku ĉi tien por aliri vian <b>uzanto-profilon.</b>",
"SETTINGS_CHANGE_UNIT": "Vi povos <b>ŝanĝi la afiŝ-unuon</b> de la sumoj alklakante ĉi-supren.<br/><br/>- Malaktivigu la kromaĵon por afiŝi sumojn en {{currency|capitalize}}.<br/>- Aktivigu la kromaĵon por relativa afiŝado en {{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub> (ĉiuj sumoj estos <b>dividitaj</b> per la Universala Dividendo aktuala).",
"END_LOGIN": "Tiu ĉi gvidata vizito <b>finiĝis</b>!<br/><br/>Bonan daŭrigon al vi, en la nova mondo de la<b>libera ekonomio</b>!",
"END_NOT_LOGIN": "Tiu ĉi gvidata vizito <b>finiĝis</b>!<br/><br/>Se vi deziras partopreni en la mono {{currency|capitalize}}, sufiĉos al vi alklaki <b>{{'LOGIN.CREATE_ACCOUNT'|translate}}</b> ĉi-sube."
}
}
}
);
$translateProvider.translations("fr-FR", {
"COMMON": {
"APP_NAME": "ğ<b>change</b>",
"APP_VERSION": "v{{version}}",
"APP_BUILD": "date : {{build}}",
"PUBKEY": "Clé publique",
"MEMBER": "Membre",
"BLOCK" : "Bloc",
"BTN_OK": "OK",
"BTN_YES": "Oui",
"BTN_NO": "Non",
"BTN_SEND": "Envoyer",
"BTN_SEND_MONEY": "Faire un virement",
"BTN_SEND_MONEY_SHORT": "Virement",
"BTN_SAVE": "Enregistrer",
"BTN_YES_SAVE": "Oui, Enregistrer",
"BTN_YES_CONTINUE": "Oui, Continuer",
"BTN_SHOW": "Voir",
"BTN_SHOW_PUBKEY": "Afficher la clé publique",
"BTN_RELATIVE_UNIT": "Afficher les montants en DU ?",
"BTN_BACK": "Retour",
"BTN_NEXT": "Suivant",
"BTN_IMPORT": "Importer",
"BTN_CANCEL": "Annuler",
"BTN_CLOSE": "Fermer",
"BTN_LATER": "Plus tard",
"BTN_LOGIN": "Se connecter",
"BTN_LOGOUT": "Déconnexion",
"BTN_ADD_ACCOUNT": "Nouveau compte",
"BTN_SHARE": "Partager",
"BTN_EDIT": "Modifier",
"BTN_DELETE": "Supprimer",
"BTN_ADD": "Ajouter",
"BTN_SEARCH": "Rechercher",
"BTN_REFRESH": "Actualiser",
"BTN_RETRY": "Recommencer",
"BTN_START": "Commencer",
"BTN_CONTINUE": "Continuer",
"BTN_CREATE": "Créer",
"BTN_UNDERSTOOD": "J'ai compris",
"BTN_OPTIONS": "Options",
"BTN_HELP_TOUR": "Visite guidée",
"BTN_HELP_TOUR_SCREEN": "Découvrir cet écran",
"BTN_DOWNLOAD": "Télécharger",
"BTN_DOWNLOAD_ACCOUNT_STATEMENT": "Télécharger le relevé du compte",
"BTN_MODIFY": "Modifier",
"CHOOSE_FILE": "Déposez votre fichier <br/>ou cliquez pour le sélectionner",
"DAYS": "jours",
"NO_ACCOUNT_QUESTION": "Pas encore de compte ? Créez-en un gratuitement !",
"SEARCH_NO_RESULT": "Aucun résultat trouvé",
"LOADING": "Veuillez patienter...",
"LOADING_WAIT": "Veuillez patienter...<br/><small>(Attente de disponibilité du noeud)</small>",
"SEARCHING": "Recherche en cours...",
"FROM": "De",
"TO": "À",
"COPY": "Copier",
"LANGUAGE": "Langue",
"UNIVERSAL_DIVIDEND": "Dividende universel",
"UD": "DU",
"DATE_PATTERN": "DD/MM/YY HH:mm",
"DATE_FILE_PATTERN": "YYYY-MM-DD",
"DATE_SHORT_PATTERN": "DD/MM/YY",
"DATE_MONTH_YEAR_PATTERN": "MM/YYYY",
"EMPTY_PARENTHESIS": "(vide)",
"UID": "Pseudonyme",
"ENABLE": "Activé",
"DISABLE": "Désactivé",
"RESULTS_LIST": "Résultats",
"RESULTS_COUNT": "{{count}} résultats",
"EXECUTION_TIME": "exécuté en {{duration|formatDurationMs}}",
"SHOW_VALUES": "Afficher les valeurs en clair ?",
"POPOVER_ACTIONS_TITLE": "Options",
"POPOVER_FILTER_TITLE": "Filtres",
"SHOW_MORE": "Afficher plus",
"SHOW_MORE_COUNT": "(limite actuelle à {{limit}})",
"POPOVER_SHARE": {
"TITLE": "Partager",
"SHARE_ON_TWITTER": "Partager sur Twitter",
"SHARE_ON_FACEBOOK": "Partager sur Facebook",
"SHARE_ON_DIASPORA": "Partager sur Diaspora*",
"SHARE_ON_GOOGLEPLUS": "Partager sur Google+"
},
"FILE": {
"DATE": "Date :",
"TYPE": "Type :",
"SIZE": "Taille :",
"VALIDATING": "Validation en cours..."
}
},
"SYSTEM": {
"PICTURE_CHOOSE_TYPE": "Choisir la source :",
"BTN_PICTURE_GALLERY": "Galerie",
"BTN_PICTURE_CAMERA": "<b>Caméra</b>"
},
"MENU": {
"HOME": "Accueil",
"WOT": "Annuaire",
"CURRENCY": "Monnaie",
"ACCOUNT": "Mon compte",
"WALLETS": "Mes portefeuilles",
"TRANSFER": "Virement",
"SCAN": "Scanner",
"SETTINGS": "Paramètres",
"NETWORK": "Réseau",
"TRANSACTIONS": "Mes opérations"
},
"ABOUT": {
"TITLE": "À propos",
"LICENSE": "Application <b>libre</b> (Licence GNU AGPLv3).",
"LATEST_RELEASE": "Il existe une <b>version plus récente</b> de {{'COMMON.APP_NAME'|translate}} (<b>v{{version}}</b>)",
"PLEASE_UPDATE": "Veuillez mettre à jour {{'COMMON.APP_NAME'|translate}} (dernière version : <b>v{{version}}</b>)",
"CODE": "Code source :",
"OFFICIAL_WEB_SITE": "Site web officiel :",
"DEVELOPERS": "Développé par :",
"FORUM": "Forum :",
"DEV_WARNING": "Avertissement",
"DEV_WARNING_MESSAGE": "Cette application est toujours en développement.<br/>N'hésitez pas à nous remonter les anomalies rencontrées !",
"DEV_WARNING_MESSAGE_SHORT": "Cette application est toujours en développement.",
"REPORT_ISSUE": "Remonter un problème"
},
"HOME": {
"TITLE": "Cesium",
"MESSAGE": "Bienvenue sur {{'COMMON.APP_NAME'|translate}} !",
"MESSAGE_CURRENCY": "Échanger en {{currency|abbreviate}} devient... juste simple !",
"BTN_CURRENCY": "Explorer la monnaie {{name|abbreviate}}",
"BTN_ABOUT": "à propos",
"BTN_HELP": "Aide en ligne",
"REPORT_ISSUE": "anomalie",
"NOT_YOUR_ACCOUNT_QUESTION" : "Vous n'êtes pas propriétaire du compte<br/><b>{{name|| (pubkey|formatPubkey) }}</b> ?",
"BTN_CHANGE_ACCOUNT": "Déconnecter ce compte",
"CONNECTION_ERROR": "Nœud <b>{{server}}</b> d'accès à la monnaie injoignable ou adresse invalide.<br/><br/>Vérifiez votre connexion Internet, ou changez de nœud <a class=\"positive\" ng-click=\"doQuickFix('settings')\">dans les paramètres</a>."
},
"SETTINGS": {
"TITLE": "Paramètres",
"DISPLAY_DIVIDER": "Affichage",
"STORAGE_DIVIDER": "Stockage",
"NETWORK_SETTINGS": "Réseau",
"PEER": "Adresse du nœud Duniter",
"PEER_SHORT": "Adresse du nœud",
"PEER_CHANGED_TEMPORARY": "Adresse utilisée temporairement",
"USE_LOCAL_STORAGE": "Activer le stockage local",
"USE_LOCAL_STORAGE_HELP": "Permet de sauvegarder vos paramètres",
"ENABLE_HELPTIP": "Activer les bulles d'aide contextuelles",
"ENABLE_UI_EFFECTS": "Activer les effets visuels",
"HISTORY_SETTINGS": "Mes opérations",
"DISPLAY_UD_HISTORY": "Afficher les dividendes produits ?",
"AUTHENTICATION_SETTINGS": "Authentification",
"AUTO_LOGOUT": "Déconnexion automatique",
"AUTO_LOGOUT_OPTION_NEVER": "Jamais",
"AUTO_LOGOUT_OPTION_SECONDS": "Après {{value}} secondes",
"AUTO_LOGOUT_OPTION_MINUTE": "Après {{value}} minute",
"AUTO_LOGOUT_OPTION_MINUTES": "Après {{value}} minutes",
"AUTO_LOGOUT_OPTION_HOUR": "Après {{value}} heure",
"AUTO_LOGOUT_HELP": "Délai d'inactivité avant déconnexion",
"REMEMBER_ME": "Se souvenir de moi ?",
"REMEMBER_ME_HELP": "Permet de rester toujours connecté.",
"PLUGINS_SETTINGS": "Extensions",
"BTN_RESET": "Restaurer les valeurs par défaut",
"EXPERT_MODE": "Activer le mode expert",
"EXPERT_MODE_HELP": "Permet un affichage plus détaillé",
"POPUP_PEER": {
"TITLE": "Nœud Duniter",
"HOST": "Adresse",
"HOST_HELP": "Adresse : serveur:port",
"USE_SSL": "Sécurisé ?",
"USE_SSL_HELP": "(Chiffrement SSL)",
"BTN_SHOW_LIST": "Liste des noeuds"
}
},
"BLOCKCHAIN": {
"HASH": "Hash : {{hash}}",
"VIEW": {
"HEADER_TITLE": "Bloc #{{number}}-{{hash|formatHash}}",
"TITLE_CURRENT": "Bloc courant",
"TITLE": "Bloc #{{number|formatInteger}}",
"COMPUTED_BY": "Calculé par le noeud de",
"SHOW_RAW": "Voir le fichier brut",
"TECHNICAL_DIVIDER": "Informations techniques",
"VERSION": "Version du format",
"HASH": "Hash calculé",
"UNIVERSAL_DIVIDEND_HELP": "Monnaie co-produite par chacun des {{membersCount}} membres",
"EMPTY": "Aucune donnée dans ce bloc",
"POW_MIN": "Difficulté minimale",
"POW_MIN_HELP": "Difficulté imposée pour le calcul du hash",
"DATA_DIVIDER": "Données",
"IDENTITIES_COUNT": "Nouvelles identités",
"JOINERS_COUNT": "Nouveaux membres",
"ACTIVES_COUNT": "Renouvellements",
"ACTIVES_COUNT_HELP": "Membres ayant renouvelé leur adhésion",
"LEAVERS_COUNT": "Membres sortants",
"LEAVERS_COUNT_HELP": "Membres ne souhaitant plus de certification",
"EXCLUDED_COUNT": "Membres exclus",
"EXCLUDED_COUNT_HELP": "Anciens membres exclus par non renouvellement ou manque de certifications",
"REVOKED_COUNT": "Identités révoquées",
"REVOKED_COUNT_HELP": "Ces comptes ne pourront plus être membres",
"TX_COUNT": "Transactions",
"CERT_COUNT": "Certifications",
"TX_TO_HIMSELF": "Opération de change",
"TX_OUTPUT_UNLOCK_CONDITIONS": "Conditions de déblocage",
"TX_OUTPUT_OPERATOR": {
"AND": "et",
"OR": "ou"
},
"TX_OUTPUT_FUNCTION": {
"SIG": "<b>Signature</b> de ",
"XHX": "<b>Mot de passe</b>, dont SHA256 =",
"CSV": "Bloqué pendant",
"CLTV": "Bloqué jusqu'à"
}
},
"LOOKUP": {
"TITLE": "Blocs",
"NO_BLOCK": "Aucun bloc",
"LAST_BLOCKS": "Derniers blocs :",
"BTN_COMPACT": "Compacter"
}
},
"CURRENCY": {
"VIEW": {
"TITLE": "Monnaie",
"TAB_CURRENCY": "Monnaie",
"TAB_WOT": "Toile de confiance",
"TAB_NETWORK": "Réseau",
"TAB_BLOCKS": "Blocs",
"CURRENCY_SHORT_DESCRIPTION": "{{currency|abbreviate}} est une <b>monnaie libre</b>, démarrée {{firstBlockTime|formatFromNow}}. Elle compte actuellement <b>{{N}} membres</b>, qui produisent et perçoivent un <a ng-click=\"showHelpModal('ud')\">Dividende Universel</a> (DU), chaque {{dt|formatPeriod}}.",
"NETWORK_RULES_DIVIDER": "Règles du réseau",
"CURRENCY_NAME": "Nom de la monnaie",
"MEMBERS": "Nombre de membres",
"MEMBERS_VARIATION": "Variation depuis le dernier DU",
"MONEY_DIVIDER": "Monnaie",
"MASS": "Masse monétaire",
"SHARE": "Masse par membre",
"UD": "Dividende universel",
"C_ACTUAL": "Croissance actuelle",
"MEDIAN_TIME": "Heure de la blockchain",
"POW_MIN": "Niveau minimal de difficulté de calcul",
"MONEY_RULES_DIVIDER": "Règles de la monnaie",
"C_RULE": "Croissance théorique cible",
"UD_RULE": "Calcul du dividende universel",
"DT_REEVAL": "Période de revalorisation du DU",
"REEVAL_SYMBOL": "reval",
"DT_REEVAL_VALUE": "Tous les <b>{{dtReeval|formatDuration}}</b> ({{dtReeval/86400}} {{'COMMON.DAYS'|translate}})",
"UD_REEVAL_TIME0": "Date de la 1ère revalorisation",
"SIG_QTY_RULE": "Nombre de certifications requises pour devenir membre",
"SIG_STOCK": "Nombre maximal de certifications émises par membre",
"SIG_PERIOD": "Délai minimal d'attente entre 2 certifications successives émises par une même personne",
"SIG_WINDOW": "Délai limite de prise en compte d'une certification",
"SIG_VALIDITY": "Durée de vie d'une certification qui a été prise en compte",
"MS_WINDOW": "Délai limite de prise en compte d'une demande d'adhésion comme membre",
"MS_VALIDITY": "Durée de vie d'une adhésion qui a été prise en compte",
"STEP_MAX": "Distance maximale, par les certifications, entre un nouvel entrant et les membres référents",
"WOT_RULES_DIVIDER": "Règles de la toile de confiance",
"SENTRIES": "Nombre de certifications (émises <b>et</b> reçues) pour devenir membre référent",
"SENTRIES_FORMULA": "Nombre de certifications (émises <b>et</b> reçues) pour devenir membre référent (formule)",
"XPERCENT":"Pourcentage minimum de membres référents à atteindre pour respecter la règle de distance",
"AVG_GEN_TIME": "Temps moyen entre deux blocs",
"CURRENT": "actuel",
"MATH_CEILING": "PLAFOND",
"DISPLAY_ALL_RULES": "Afficher toutes les règles ?",
"BTN_SHOW_LICENSE": "Voir la licence",
"WOT_DIVIDER": "Toile de confiance"
},
"LICENSE": {
"TITLE": "Licence de la monnaie",
"BTN_DOWNLOAD": "Télécharger le fichier",
"NO_LICENSE_FILE": "Fichier de licence non trouvé."
}
},
"NETWORK": {
"VIEW": {
"MEDIAN_TIME": "Heure de la blockchain",
"LOADING_PEERS": "Chargement des noeuds...",
"NODE_ADDRESS": "Adresse :",
"SOFTWARE": "Logiciel",
"WARN_PRE_RELEASE": "Pré-version (dernière version stable : <b>{{version}}</b>)",
"WARN_NEW_RELEASE": "Version <b>{{version}}</b> disponible",
"WS2PID": "Identifiant :",
"PRIVATE_ACCESS": "Accès privé",
"POW_PREFIX": "Préfixe de preuve de travail :",
"ENDPOINTS": {
"BMAS": "Interface sécurisée (SSL)",
"BMATOR": "Interface réseau TOR",
"WS2P": "Interface WS2P",
"ES_USER_API": "Noeud de données Cesium+"
}
},
"INFO": {
"ONLY_SSL_PEERS": "Les noeuds non SSL ont un affichage dégradé, car Cesium fonctionne en mode HTTPS."
}
},
"PEER": {
"PEERS": "Nœuds",
"SIGNED_ON_BLOCK": "Signé sur le bloc",
"MIRROR": "miroir",
"MIRRORS": "Miroirs",
"MIRROR_PEERS": "Nœuds miroirs",
"PEER_LIST" : "Liste des nœuds",
"MEMBERS" : "Membres",
"MEMBER_PEERS" : "Nœuds membres",
"ALL_PEERS" : "Tous les nœuds",
"DIFFICULTY" : "Difficulté",
"API" : "API",
"CURRENT_BLOCK" : "Bloc #",
"POPOVER_FILTER_TITLE": "Filtre",
"OFFLINE": "Hors ligne",
"OFFLINE_PEERS": "Nœuds hors ligne",
"BTN_SHOW_PEER": "Voir le nœud",
"VIEW": {
"TITLE": "Nœud",
"OWNER": "Appartient à",
"SHOW_RAW_PEERING": "Voir la fiche de pair",
"SHOW_RAW_CURRENT_BLOCK": "Voir le dernier bloc (format brut)",
"LAST_BLOCKS": "Derniers blocs connus",
"KNOWN_PEERS": "Nœuds connus :",
"GENERAL_DIVIDER": "Informations générales",
"ERROR": {
"LOADING_TOR_NODE_ERROR": "Récupération des informations du noeud impossible. Le délai d'attente est dépassé.",
"LOADING_NODE_ERROR": "Récupération des informations du noeud impossible"
}
}
},
"WOT": {
"SEARCH_HELP": "Recherche (pseudo ou clé publique)",
"SEARCH_INIT_PHASE_WARNING": "Durant la phase de pré-inscription, la recherche des inscriptions en attente <b>peut être longue</b>. Merci de patienter...",
"REGISTERED_SINCE": "Inscrit le",
"REGISTERED_SINCE_BLOCK": "Inscrit au bloc #",
"NO_CERTIFICATION": "Aucune certification validée",
"NO_GIVEN_CERTIFICATION": "Aucune certification émise",
"NOT_MEMBER_PARENTHESIS": "(non membre)",
"IDENTITY_REVOKED_PARENTHESIS": "(identité révoquée)",
"MEMBER_PENDING_REVOCATION_PARENTHESIS": "(en cours de révocation)",
"EXPIRE_IN": "Expiration",
"NOT_WRITTEN_EXPIRE_IN": "Date limite<br/>de traitement",
"EXPIRED": "Expiré",
"PSEUDO": "Pseudonyme",
"SIGNED_ON_BLOCK": "Emise au bloc #{{block}}",
"WRITTEN_ON_BLOCK": "Ecrite au bloc #{{block}}",
"GENERAL_DIVIDER": "Informations générales",
"NOT_MEMBER_ACCOUNT": "Compte simple (non membre)",
"NOT_MEMBER_ACCOUNT_HELP": "Il s'agit d'un simple portefeuille, sans demande d'adhésion en attente.",
"TECHNICAL_DIVIDER": "Informations techniques",
"BTN_CERTIFY": "Certifier",
"BTN_YES_CERTIFY": "Oui, certifier",
"BTN_SELECT_AND_CERTIFY": "Nouvelle certification",
"ACCOUNT_OPERATIONS": "Opérations sur le compte",
"VIEW": {
"POPOVER_SHARE_TITLE": "Identité {{title}}"
},
"LOOKUP": {
"TITLE": "Annuaire",
"NEWCOMERS": "Nouveaux membres :",
"NEWCOMERS_COUNT": "{{count}} membres",
"PENDING": "Inscriptions en attente :",
"PENDING_COUNT": "{{count}} inscriptions en attente",
"REGISTERED": "Inscrit {{time | formatFromNow}}",
"MEMBER_FROM": "Membre depuis {{time|formatFromNowShort}}",
"BTN_NEWCOMERS": "Nouveaux membres",
"BTN_PENDING": "Inscriptions en attente",
"SHOW_MORE": "Afficher plus",
"SHOW_MORE_COUNT": "(limite actuelle à {{limit}})",
"NO_PENDING": "Aucune inscription en attente.",
"NO_NEWCOMERS": "Aucun membre."
},
"CONTACTS": {
"TITLE": "Contacts"
},
"MODAL": {
"TITLE": "Recherche"
},
"CERTIFICATIONS": {
"TITLE": "{{uid}} - Certifications",
"SUMMARY": "Certifications reçues",
"LIST": "Détail des certifications reçues",
"PENDING_LIST": "Certifications en attente de traitement",
"RECEIVED": "Certifications reçues",
"RECEIVED_BY": "Certifications reçues par {{uid}}",
"ERROR": "Certifications reçues en erreur",
"SENTRY_MEMBER": "Membre référent"
},
"OPERATIONS": {
"TITLE": "{{uid}} - Opérations"
},
"GIVEN_CERTIFICATIONS": {
"TITLE": "{{uid}} - Certifications émises",
"SUMMARY": "Certifications émises",
"LIST": "Détail des certifications émises",
"PENDING_LIST": "Certifications en attente de traitement",
"SENT": "Certifications émises",
"SENT_BY": "Certifications émises par {{uid}}",
"ERROR": "Certifications émises en erreur"
}
},
"LOGIN": {
"TITLE": "<i class=\"icon ion-log-in\"></i> Connexion",
"SCRYPT_FORM_HELP": "Veuillez saisir vos identifiants.<br>Pensez à vérifier que la clé publique est celle de votre compte.",
"PUBKEY_FORM_HELP": "Veuillez saisir une clé publique de compte :",
"FILE_FORM_HELP": "Choisissez le fichier de trousseau à utiliser :",
"SCAN_FORM_HELP": "Scanner le QR code d'un portefeuille.",
"SALT": "Identifiant",
"SALT_HELP": "Identifiant",
"SHOW_SALT": "Afficher l'identifiant ?",
"PASSWORD": "Mot de passe",
"PASSWORD_HELP": "Mot de passe",
"PUBKEY_HELP": "Exemple : « AbsxSY4qoZRzyV2irfep1V9xw1EMNyKJw2TkuVD4N1mv »",
"NO_ACCOUNT_QUESTION": "Vous n'avez pas encore de compte ?",
"HAVE_ACCOUNT_QUESTION": "Vous avez déjà un compte ?",
"CREATE_ACCOUNT": "Créer un compte...",
"CREATE_FREE_ACCOUNT": "Créer un compte gratuitement",
"FORGOTTEN_ID": "Mot de passe oublié ?",
"ASSOCIATED_PUBKEY": "Clé publique du trousseau :",
"BTN_METHODS": "Autres méthodes",
"BTN_METHODS_DOTS": "Changer de méthode...",
"METHOD_POPOVER_TITLE": "Méthodes",
"MEMORIZE_AUTH_FILE": "Mémoriser ce trousseau le temps de la session de navigation",
"SCRYPT_PARAMETERS": "Paramètres (Scrypt) :",
"AUTO_LOGOUT": {
"TITLE": "Information",
"MESSAGE": "<i class=\"ion-android-time\"></i> Vous avez été <b>déconnecté</b> automatiquement, suite à une inactivité prolongée.",
"BTN_RELOGIN": "Me reconnecter",
"IDLE_WARNING": "Vous allez être déconnecté... {{countdown}}"
},
"METHOD": {
"SCRYPT_DEFAULT": "Identifiant et mot de passe",
"SCRYPT_ADVANCED": "Salage avancé",
"FILE": "Fichier de trousseau",
"PUBKEY": "Clé publique ou pseudonyme",
"SCAN": "Scanner un QR code"
},
"SCRYPT": {
"SIMPLE": "Salage léger",
"DEFAULT": "Salage standard",
"SECURE": "Salage sûr",
"HARDEST": "Salage le plus sûr",
"EXTREME": "Salage extrême",
"USER": "Salage personnalisé",
"N": "N (Loop):",
"r": "r (RAM):",
"p": "p (CPU):"
},
"FILE": {
"HELP": "Format de fichier attendu : <b>.yml</b> ou <b>.dunikey</b> (type PubSec, WIF ou EWIF)."
}
},
"AUTH": {
"TITLE": "<i class=\"icon ion-locked\"></i> Authentification",
"METHOD_LABEL": "Méthode d'authentification",
"BTN_AUTH": "S'authentifier",
"SCRYPT_FORM_HELP": "Veuillez vous authentifier :",
"ERROR": {
"SCRYPT_DEFAULT": "Salage simple (par défaut)",
"SCRYPT_ADVANCED": "Salage avancé",
"FILE": "Fichier de trousseau"
}
},
"ACCOUNT": {
"TITLE": "Mon compte",
"BALANCE": "Solde",
"LAST_TX": "Dernières transactions",
"BALANCE_ACCOUNT": "Solde du compte",
"NO_TX": "Aucune transaction",
"SHOW_MORE_TX": "Afficher plus",
"SHOW_ALL_TX": "Afficher tout",
"TX_FROM_DATE": "(limite actuelle à {{fromTime|formatFromNowShort}})",
"PENDING_TX": "Transactions en cours de traitement",
"ERROR_TX": "Transactions non exécutées",
"ERROR_TX_SENT": "Transactions envoyées en échec",
"PENDING_TX_RECEIVED": "Transactions en attente de réception",
"EVENTS": "Evénements",
"WAITING_MEMBERSHIP": "Demande d'adhésion envoyée. En attente d'acceptation.",
"WAITING_CERTIFICATIONS": "Vous devez obtenir {{needCertificationCount}} certification(s) pour devenir membre.",
"WILL_MISSING_CERTIFICATIONS": "Vous allez bientôt <b>manquer de certifications</b> (au moins {{willNeedCertificationCount}} sont nécessaires)",
"WILL_NEED_RENEW_MEMBERSHIP": "Votre adhésion comme membre <b>va expirer {{membershipExpiresIn|formatDurationTo}}</b>. Pensez à <a ng-click=\"doQuickFix('renew')\">renouveler votre adhésion</a> d'ici là.",
"NEED_RENEW_MEMBERSHIP": "Vous n'êtes plus membre, car votre adhésion <b>a expiré</b>. Pensez à <a ng-click=\"doQuickFix('renew')\">renouveler votre adhésion</a>.",
"CERTIFICATION_COUNT": "Certifications reçues",
"CERTIFICATION_COUNT_SHORT": "Certifications",
"SIG_STOCK": "Certifications envoyées",
"BTN_RECEIVE_MONEY": "Encaisser",
"BTN_MEMBERSHIP_IN_DOTS": "Devenir membre...",
"BTN_MEMBERSHIP_RENEW": "Renouveler l'adhésion",
"BTN_MEMBERSHIP_RENEW_DOTS": "Renouveler l'adhésion...",
"BTN_MEMBERSHIP_OUT_DOTS": "Arrêter l'adhésion...",
"BTN_SEND_IDENTITY_DOTS": "Publier son identité...",
"BTN_SECURITY_DOTS": "Compte et sécurité...",
"BTN_SHOW_DETAILS": "Afficher les infos techniques",
"LOCKED_OUTPUTS_POPOVER": {
"TITLE": "Montant verrouillé",
"DESCRIPTION": "Voici les conditions de déverrouillage de ce montant :",
"DESCRIPTION_MANY": "Cette transaction est composée de plusieurs parties, dont voici les conditions de déverrouillage :",
"LOCKED_AMOUNT": "Conditions pour le montant :"
},
"NEW": {
"TITLE": "Inscription",
"INTRO_WARNING_TIME": "La création d'un compte sur {{name|capitalize}} est très simple. Veuillez néanmoins prendre suffisament de temps pour faire correctement cette formalité (pour ne pas oublier les identifiants, mots de passe, etc.).",
"INTRO_WARNING_SECURITY": "Vérifier que le matériel que vous utilisez actuellement (ordinateur, tablette, téléphone) <b>est sécurisé et digne de confiance</b>.",
"INTRO_WARNING_SECURITY_HELP": "Anti-virus à jour, pare-feu activé, session protégée par mot de passe ou code pin, etc.",
"INTRO_HELP": "Cliquez sur <b>{{'COMMON.BTN_START'|translate}}</b> pour débuter la création de compte. Vous serez guidé étape par étape.",
"REGISTRATION_NODE": "Votre inscription sera enregistrée via le noeud Duniter <b>{{server}}</b>, qui le diffusera ensuite au reste du réseau de la monnaie.",
"REGISTRATION_NODE_HELP": "Si vous ne faites pas confiance à ce noeud, veuillez en changer <a ng-click=\"doQuickFix('settings')\">dans les paramètres</a> de Cesium.",
"SELECT_ACCOUNT_TYPE": "Choisissez le type de compte à créer :",
"MEMBER_ACCOUNT": "Compte membre",
"MEMBER_ACCOUNT_TITLE": "Création d'un compte membre",
"MEMBER_ACCOUNT_HELP": "Si vous n'êtes pas encore inscrit en tant qu'individu (un seul compte possible par individu). Ce compte permet de co-produire la monnaie, en recevant un <b>dividende universel</b> chaque {{parameters.dt|formatPeriod}}.",
"WALLET_ACCOUNT": "Simple portefeuille",
"WALLET_ACCOUNT_TITLE": "Création d'un portefeuille",
"WALLET_ACCOUNT_HELP": "Pour tous les autres cas, par exemple si vous avez besoin d'un compte supplémentaire.<br/>Aucun dividende universel ne sera créé par ce compte.",
"SALT_WARNING": "Choisissez votre identifiant.<br/>Il vous sera demandé à chaque connexion sur ce compte.<br/><br/><b>Retenez-le bien</b>.<br/>En cas de perte, plus personne ne pourra accéder à votre compte !",
"PASSWORD_WARNING": "Choisissez un mot de passe.<br/>Il vous sera demandé à chaque connexion sur ce compte.<br/><br/><b>Retenez bien ce mot de passe</b>.<br/>En cas de perte, plus personne ne pourra accéder à votre compte !",
"PSEUDO_WARNING": "Choisissez un pseudonyme.<br/>Il sert aux autres membres, pour vous identifier plus facilement.<div class='hidden-xs'><br/>Il <b>ne pourra pas être modifié</b>, sans refaire un compte.</div><br/><br/>Il ne doit contenir <b>ni espace, ni de caractère accentué</b>.<div class='hidden-xs'><br/>Exemple : <span class='gray'>SophieDupond, MarcelChemin, etc.</span>",
"PSEUDO": "Pseudonyme",
"PSEUDO_HELP": "Pseudonyme",
"SALT_CONFIRM": "Confirmation",
"SALT_CONFIRM_HELP": "Confirmation de l'identifiant",
"PASSWORD_CONFIRM": "Confirmation",
"PASSWORD_CONFIRM_HELP": "Confirmation du mot de passe",
"SLIDE_6_TITLE": "Confirmation :",
"COMPUTING_PUBKEY": "Calcul en cours...",
"LAST_SLIDE_CONGRATULATION": "Vous avez saisi toutes les informations nécessaires : Bravo !<br/>Vous pouvez maintenant <b>envoyer la demande de création</b> de compte.</b><br/><br/>Pour information, la clé publique ci-dessous identifiera votre futur compte.<br/>Elle pourra être communiquée à des tiers pour recevoir leur paiement.<br/><b>Il n'est pas obligatoire</b> de la noter ici, vous pourrez également le faire plus tard.",
"CONFIRMATION_MEMBER_ACCOUNT": "<b class=\"assertive\">Avertissement :</b> l'identifiant, le mot de passe et le pseudonyme ne pourront plus être modifiés.<br/><br/><b>Assurez-vous de toujours vous en rappeler !</b><br/><br/><b>Êtes-vous sûr</b> de vouloir envoyer cette demande d'inscription ?",
"CONFIRMATION_WALLET_ACCOUNT": "<b class=\"assertive\">Avertissement :</b> l'identifiant et le mot de passe ne pourront plus être modifiés.<br/><br/><b>Assurez-vous de toujours vous en rappeler !</b><br/><br/><b>Êtes-vous sûr</b> de vouloir continuer avec ces identifiants ?",
"CHECKING_PSEUDO": "Vérification...",
"PSEUDO_AVAILABLE": "Pseudonyme disponible",
"PSEUDO_NOT_AVAILABLE": "Pseudonyme non disponible",
"INFO_LICENSE": "Avant de créer un compte membre, <b>veuillez lire et accepter la licence</b> d'usage de la monnaie :",
"BTN_ACCEPT": "J'accepte",
"BTN_ACCEPT_LICENSE": "J'accepte la licence"
},
"POPUP_REGISTER": {
"TITLE": "Choisissez un pseudonyme",
"HELP": "Un pseudonyme est obligatoire pour devenir membre."
},
"SECURITY": {
"ADD_QUESTION": "Ajouter une question personnalisée",
"BTN_CLEAN": "Vider",
"BTN_RESET": "Réinitialiser",
"DOWNLOAD_REVOKE": "Sauvegarder mon fichier de révocation",
"DOWNLOAD_REVOKE_HELP": "Disposer d'un fichier de révocation est important, par exemple en cas de perte de vos identifiants. Il vous permet de <b>sortir ce compte de la toile de confiance</b>, en redevenant ainsi un simple portefeuille.",
"HELP_LEVEL": "Pour générer un fichier de sauvegarde de vos identifiants, choisissez <strong> au moins {{nb}} questions :</strong>",
"LEVEL": "Niveau de sécurité",
"LOW_LEVEL": "Faible <span class=\"hidden-xs\">(2 questions minimum)</span>",
"MEDIUM_LEVEL": "Moyen <span class=\"hidden-xs\">(4 questions minimum)</span>",
"QUESTION_1": "Comment s'appelait votre meilleur ami lorsque vous étiez adolescent ?",
"QUESTION_2": "Comment s'appelait votre premier animal de compagnie ?",
"QUESTION_3": "Quel est le premier plat que vous avez appris à cuisiner ?",
"QUESTION_4": "Quel est le premier film que vous avez vu au cinéma ?",
"QUESTION_5": "Où êtes-vous allé la première fois que vous avez pris l'avion ?",
"QUESTION_6": "Comment s'appelait votre instituteur préféré à l'école primaire ?",
"QUESTION_7": "Quel serait selon vous le métier idéal ?",
"QUESTION_8": "Quel est le livre pour enfants que vous préférez ?",
"QUESTION_9": "Quel était le modèle de votre premier véhicule ?",
"QUESTION_10": "Quel était votre surnom lorsque vous étiez enfant ?",
"QUESTION_11": "Quel était votre personnage ou acteur de cinéma préféré lorsque vous étiez étudiant ?",
"QUESTION_12": "Quel était votre chanteur ou groupe préféré lorsque vous étiez étudiant ?",
"QUESTION_13": "Dans quelle ville vos parents se sont-ils rencontrés ?",
"QUESTION_14": "Comment s'appelait votre premier patron ?",
"QUESTION_15": "Quel est le nom de la rue où vous avez grandi ?",
"QUESTION_16": "Quel est le nom de la première plage où vous vous êtes baigné ?",
"QUESTION_17": "Quel est le premier album que vous avez acheté ?",
"QUESTION_18": "Quel est le nom de votre équipe de sport préférée ?",
"QUESTION_19": "Quel était le métier de votre grand-père ?",
"RECOVER_ID": "Retrouver mon mot de passe...",
"RECOVER_ID_HELP": "Si vous disposez d'un <b>fichier de sauvegarde de vos identifiants</b>, vous pouvez les retrouver en répondant correctement à vos questions personnelles.",
"REVOCATION_WITH_FILE": "Révoquer mon compte membre...",
"REVOCATION_WITH_FILE_HELP": "Si vous avez <b>définitivement perdu vos identifiants</b> de compte membre (ou que la sécurité du compte est compromise), vous pouvez utiliser <b>le fichier de révocation</b> du compte pour <b>forcer sa sortie définitive de la toile de confiance</b>.",
"REVOCATION_WALLET": "Révoquer immédiatement ce compte",
"REVOCATION_WALLET_HELP": "Demander la révocation de votre identité entraîne la <b>sortie de la toile de confiance</b> (définitive pour le pseudonyme et la clé publique associés). Le compte ne pourra plus produire de Dividende Universel.<br/>Vous pourrez toutefois encore vous y connecter, comme à un simple portefeuille.",
"REVOCATION_FILENAME": "revocation-{{uid}}-{{pubkey|formatPubkey}}-{{currency}}.txt",
"SAVE_ID": "Sauvegarder mes identifiants...",
"SAVE_ID_HELP": "Création d'un fichier de sauvegarde, pour <b>retrouver votre mot de passe</b> (et l'identifiant) <b>en cas de d'oubli</b>. Le fichier est <b>sécurisé</b> (chiffré) à l'aide de questions personnelles.",
"STRONG_LEVEL": "Fort <span class=\"hidden-xs \">(6 questions minimum)</span>",
"TITLE": "Compte et sécurité"
},
"FILE_NAME": "{{currency}} - Relevé du compte {{pubkey|formatPubkey}} au {{currentTime|formatDateForFile}}.csv",
"HEADERS": {
"TIME": "Date",
"AMOUNT": "Montant",
"COMMENT": "Commentaire"
}
},
"TRANSFER": {
"TITLE": "Virement",
"SUB_TITLE": "Faire un virement",
"FROM": "De",
"TO": "À",
"AMOUNT": "Montant",
"AMOUNT_HELP": "Montant",
"COMMENT": "Commentaire",
"COMMENT_HELP": "Commentaire",
"BTN_SEND": "Envoyer",
"BTN_ADD_COMMENT": "Saisir un commentaire ?",
"MODAL": {
"TITLE": "Virement"
}
},
"ERROR": {
"POPUP_TITLE": "Erreur",
"UNKNOWN_ERROR": "Erreur inconnue",
"CRYPTO_UNKNOWN_ERROR": "Votre navigateur ne semble pas compatible avec les fonctionnalités de cryptographie.",
"FIELD_REQUIRED": "Champ obligatoire",
"FIELD_TOO_SHORT": "Valeur trop courte",
"FIELD_TOO_SHORT_WITH_LENGTH": "Valeur trop courte ({{minLength}} caractères min)",
"FIELD_TOO_LONG": "Valeur trop longue",
"FIELD_TOO_LONG_WITH_LENGTH": "Valeur trop longue ({{maxLength}} caractères max)",
"FIELD_MIN": "Valeur minimale : {{min}}",
"FIELD_MAX": "Valeur maximale : {{max}}",
"FIELD_ACCENT": "Caractères accentués et virgules non autorisés",
"FIELD_NOT_NUMBER": "Valeur numérique attendue",
"FIELD_NOT_INT": "Valeur entière attendue",
"FIELD_NOT_EMAIL": "Adresse email non valide",
"PASSWORD_NOT_CONFIRMED": "Ne correspond pas au mot de passe",
"SALT_NOT_CONFIRMED": "Ne correspond pas à l'identifiant",
"SEND_IDENTITY_FAILED": "Echec de l'inscription",
"SEND_CERTIFICATION_FAILED": "Echec de la certification",
"NEED_MEMBER_ACCOUNT_TO_CERTIFY": "Vous ne pouvez pas effectuer de certification, car votre compte n'est <b>pas membre</b>.",
"NEED_MEMBER_ACCOUNT_TO_CERTIFY_HAS_SELF": "Vous ne pouvez pas effectuer de certification, car votre compte n'est pas encore membre.<br/><br/>Il vous manque encore des certifications, ou bien celles-ci n'ont pas encore été validées.",
"NOT_MEMBER_FOR_CERTIFICATION": "Votre compte n'est pas encore membre.",
"IDENTITY_TO_CERTIFY_HAS_NO_SELF": "Compte non certifiable. Aucune demande d'adhésion n'a été faite, ou bien elle n'a pas été renouvelée.",
"LOGIN_FAILED": "Erreur lors de la connexion.",
"LOAD_IDENTITY_FAILED": "Erreur de chargement de l'identité.",
"LOAD_REQUIREMENTS_FAILED": "Erreur de chargement des prérequis de l'identité.",
"SEND_MEMBERSHIP_IN_FAILED": "Echec de la tentative d'entrée dans la communauté.",
"SEND_MEMBERSHIP_OUT_FAILED": "Echec de l'arrêt de l'adhésion.",
"REFRESH_WALLET_DATA": "Echec du rafraîchissement du portefeuille.",
"GET_CURRENCY_PARAMETER": "Echec de la récupération des règles de la monnaie.",
"GET_CURRENCY_FAILED": "Chargement de la monnaie impossible. Veuillez réessayer plus tard.",
"SEND_TX_FAILED": "Echec du virement.",
"ALL_SOURCES_USED": "Veuillez attendre le calcul du prochain bloc (toutes vos sources de monnaie ont été utilisées).",
"NOT_ENOUGH_SOURCES": "Pas assez de change pour envoyer ce montant en une seule transaction.<br/>Montant maximum : {{amount}} {{unit}}<sub>{{subUnit}}</sub>.",
"ACCOUNT_CREATION_FAILED": "Echec de la création du compte membre.",
"RESTORE_WALLET_DATA_ERROR": "Echec du rechargement des paramètres depuis le stockage local",
"LOAD_WALLET_DATA_ERROR": "Echec du chargement des données du portefeuille.",
"COPY_CLIPBOARD_FAILED": "Copie de la valeur impossible.",
"TAKE_PICTURE_FAILED": "Echec de la récupération de la photo.",
"SCAN_FAILED": "Echec du scan de QR Code",
"SCAN_UNKNOWN_FORMAT": "Code non reconnu.",
"WOT_LOOKUP_FAILED": "Echec de la recherche",
"LOAD_PEER_DATA_FAILED": "Lecture du nœud Duniter impossible. Veuillez réessayer ultérieurement.",
"NEED_LOGIN_FIRST": "Veuillez d'abord vous connecter.",
"AMOUNT_REQUIRED": "Le montant est obligatoire.",
"AMOUNT_NEGATIVE": "Montant négatif non autorisé.",
"NOT_ENOUGH_CREDIT": "Crédit insuffisant.",
"INVALID_NODE_SUMMARY": "Nœud injoignable ou adresse invalide.",
"INVALID_USER_ID": "Le pseudonyme ne doit contenir ni espace ni caractère spécial ou accentué.",
"INVALID_COMMENT": "Le champ 'référence' ne doit pas contenir de caractères accentués.",
"INVALID_PUBKEY": "La clé publique n'a pas le format attendu.",
"IDENTITY_REVOKED": "Cette identité <b>a été révoquée {{revocationTime|formatFromNow}}</b> ({{revocationTime|formatDate}}). Elle ne peut plus devenir membre.",
"IDENTITY_PENDING_REVOCATION": "La <b>révocation de cette identité</b> a été demandée et est en attente de traitement. La certification est donc désactivée.",
"IDENTITY_INVALID_BLOCK_HASH": "Cette demande d'adhésion n'est plus valide (car elle référence un bloc que les nœuds du réseau ont annulé) : cette personne doit renouveler sa demande d'adhésion <b>avant</b> d'être certifiée.",
"IDENTITY_EXPIRED": "La publication de cette identité a expiré : cette personne doit effectuer une nouvelle demande d'adhésion <b>avant</b> d'être certifiée.",
"IDENTITY_SANDBOX_FULL": "Le nœud Duniter utilisé par Cesium ne peut plus recevoir de nouvelles identités, car sa file d'attente est pleine.<br/><br/>Veuillez réessayer ultérieurement ou changer de nœud (via le menu <b>Paramètres</b>).",
"IDENTITY_NOT_FOUND": "Identité non trouvée",
"WOT_PENDING_INVALID_BLOCK_HASH": "Adhésion non valide.",
"WALLET_INVALID_BLOCK_HASH": "Votre demande d'adhésion n'est plus valide (car elle référence un bloc que les nœuds du réseau ont annulé).<br/>Vous devez <a ng-click=\"doQuickFix('fixMembership')\">envoyer une nouvelle demande</a> pour résoudre ce problème.",
"WALLET_IDENTITY_EXPIRED": "La publication de <b>votre identité a expiré</b>.<br/>Vous devez <a ng-click=\"doQuickFix('fixIdentity')\">publier à nouveau votre identité</a> pour résoudre ce problème.",
"WALLET_REVOKED": "Votre identité a été <b>révoquée</b> : ni votre pseudonyme ni votre clef publique ne pourront être utilisés à l'avenir pour un compte membre.",
"WALLET_HAS_NO_SELF": "Votre identité doit d'abord avoir été publiée, et ne pas être expirée.",
"AUTH_REQUIRED": "Authentification requise.",
"AUTH_INVALID_PUBKEY": "La clé publique ne correspond pas au compte connecté.",
"AUTH_INVALID_SCRYPT": "Identifiant ou mot de passe invalide.",
"AUTH_INVALID_FILE": "Fichier de trousseau invalide.",
"AUTH_FILE_ERROR": "Echec de l'ouverture du fichier de trousseau",
"IDENTITY_ALREADY_CERTIFY": "Vous avez <b>déjà certifié</b> cette identité.<br/><br/>Cette certification est encore valide (expiration {{expiresIn|formatDurationTo}}).",
"IDENTITY_ALREADY_CERTIFY_PENDING": "Vous avez <b>déjà certifié</b> cette identité.<br/><br/>Cette certification est en attente de traitement (date limite de traitement {{expiresIn|formatDurationTo}}).",
"UNABLE_TO_CERTIFY_TITLE": "Certification impossible",
"LOAD_NEWCOMERS_FAILED": "Echec du chargement des nouveaux membres.",
"LOAD_PENDING_FAILED": "Echec du chargement des inscriptions en attente.",
"ONLY_MEMBER_CAN_EXECUTE_THIS_ACTION": "Vous devez <b>être membre</b> pour pouvoir effectuer cette action.",
"ONLY_SELF_CAN_EXECUTE_THIS_ACTION": "Vous devez avoir <b>publié votre identité</b> pour pouvoir effectuer cette action.",
"GET_BLOCK_FAILED": "Echec de la récupération du bloc",
"INVALID_BLOCK_HASH": "Bloc non trouvé (hash différent)",
"DOWNLOAD_REVOCATION_FAILED": "Echec du téléchargement du fichier de révocation.",
"REVOCATION_FAILED": "Echec de la révocation.",
"SALT_OR_PASSWORD_NOT_CONFIRMED": "identifiant ou mot de passe incorrect",
"RECOVER_ID_FAILED": "Echec de la récupération des identifiants",
"LOAD_FILE_FAILED" : "Echec du chargement du fichier",
"NOT_VALID_REVOCATION_FILE": "Fichier de révocation non valide (mauvais format de fichier)",
"NOT_VALID_SAVE_ID_FILE": "Fichier de récupération non valide (mauvais format de fichier)",
"NOT_VALID_KEY_FILE": "Fichier de trousseau non valide (format non reconnu)",
"EXISTING_ACCOUNT": "Vos identifiants correspondent à un compte déjà existant, dont la <a ng-click=\"showHelpModal('pubkey')\">clef publique</a> est :",
"EXISTING_ACCOUNT_REQUEST": "Veuillez modifier vos identifiants afin qu'ils correspondent à un compte non utilisé.",
"GET_LICENSE_FILE_FAILED": "Récupération du fichier de licence impossible",
"CHECK_NETWORK_CONNECTION": "Aucun nœud ne semble accessible.<br/><br/>Veuillez <b>vérifier votre connexion Internet</b>."
},
"INFO": {
"POPUP_TITLE": "Information",
"CERTIFICATION_DONE": "Certification envoyée",
"NOT_ENOUGH_CREDIT": "Crédit insuffisant",
"TRANSFER_SENT": "Virement envoyé",
"COPY_TO_CLIPBOARD_DONE": "Copie effectuée",
"MEMBERSHIP_OUT_SENT": "Résiliation envoyée",
"NOT_NEED_MEMBERSHIP": "Vous êtes déjà membre.",
"IDENTITY_WILL_MISSING_CERTIFICATIONS": "Cette identité va bientôt manquer de certifications (au moins {{willNeedCertificationCount}}).",
"REVOCATION_SENT": "Révocation envoyée",
"REVOCATION_SENT_WAITING_PROCESS": "La <b>révocation de cette identité</b> a été demandée et est en attente de traitement.",
"FEATURES_NOT_IMPLEMENTED": "Cette fonctionnalité est encore en cours de développement.<br/>Pourquoi ne pas <b>contribuer à Cesium</b>, pour l'obtenir plus rapidement ?)",
"EMPTY_TX_HISTORY": "Aucune opération à exporter"
},
"CONFIRM": {
"POPUP_TITLE": "<b>Confirmation</b>",
"POPUP_WARNING_TITLE": "<b>Avertissement</b>",
"POPUP_SECURITY_WARNING_TITLE": "<i class=\"icon ion-alert-circled\"></i> <b>Avertissement de sécurité</b>",
"CERTIFY_RULES_TITLE_UID": "Certifier {{uid}}",
"CERTIFY_RULES": "<b class=\"assertive\">Ne PAS certifier</b> un compte si vous pensez que :<br/><br/><ul><li>1.) il ne correspond pas à une personne <b>physique et vivante</b>.<li>2.) son propriétaire <b>possède un autre compte</b> déjà certifié.<li>3.) son propriétaire viole (volontairement ou non) la règle 1 ou 2 (par exemple en certifiant des comptes factices ou en double).</ul><br/><b>Êtes-vous sûr</b> de vouloir néanmoins certifier cette identité ?",
"FULLSCREEN": "Afficher l'application en plein écran ?",
"EXIT_APP": "Fermer l'application ?",
"TRANSFER": "<b>Récapitulatif du virement</b> :<br/><br/><ul><li> - De : {{from}}</li><li> - A : <b>{{to}}</b></li><li> - Montant : <b>{{amount}} {{unit}}</b></li><li> - Commentaire : <i>{{comment}}</i></li></ul><br/><b>Êtes-vous sûr de vouloir effectuer ce virement ?</b>",
"MEMBERSHIP_OUT": "Cette opération est <b>irréversible</b>.<br/></br/>Êtes-vous sûr de vouloir <b>résilier votre compte membre</b> ?",
"MEMBERSHIP_OUT_2": "Cette opération est <b>irréversible</b> !<br/><br/>Êtes-vous vraiment sûr de vouloir <b>résilier votre adhésion</b> comme membre ?",
"LOGIN_UNUSED_WALLET_TITLE": "Erreur de saisie ?",
"LOGIN_UNUSED_WALLET": "Le compte connecté semble <b>inactif</b>.<br/><br/>Il s'agit probablement d'une <b>erreur de saisie</b> dans vos identifiants de connexion. Veuillez recommencer, en vérifiant que <b>la clé publique est celle de votre compte</b>.",
"FIX_IDENTITY": "Le pseudonyme <b>{{uid}}</b> va être publié à nouveau, en remplacement de l'ancienne publication qui a expiré.<br/></br/><b>Êtes-vous sûr</b> de vouloir continuer ?",
"FIX_MEMBERSHIP": "Votre demande d'adhésion comme membre va être renvoyée.<br/></br/><b>Êtes-vous sûr</b> de vouloir continuer ?",
"RENEW_MEMBERSHIP": "Votre adhésion comme membre va être renouvelée.<br/></br/><b>Êtes-vous sûr</b> de vouloir continuer ?",
"REVOKE_IDENTITY": "Vous allez <b>révoquer définitivement cette identité</b>.<br/><br/>La clé publique et le pseudonyme associés <b>ne pourront plus jamais être utilisés</b> (pour un compte membre). <br/></br/><b>Êtes-vous sûr</b> de vouloir révoquer définitivement ce compte ?",
"REVOKE_IDENTITY_2": "Cette opération est <b>irreversible</b> !<br/><br/>Êtes-vous vraiment sûr de vouloir <b>révoquer définitivement</b> ce compte ?",
"NOT_NEED_RENEW_MEMBERSHIP": "Votre adhésion n'a pas besoin d'être renouvelée (elle n'expirera que dans {{membershipExpiresIn|formatDuration}}).<br/></br/><b>Êtes-vous sûr</b> de vouloir renouveler votre adhésion ?",
"SAVE_BEFORE_LEAVE": "Voulez-vous <b>sauvegarder vos modifications</b> avant de quitter la page ?",
"SAVE_BEFORE_LEAVE_TITLE": "Modifications non enregistrées",
"LOGOUT": "Êtes-vous de vouloir vous déconnecter ?",
"USE_FALLBACK_NODE": "Nœud <b>{{old}}</b> injoignable ou adresse invalide.<br/><br/>Voulez-vous temporairement utiliser le nœud <b>{{new}}</b> ?"
},
"DOWNLOAD": {
"POPUP_TITLE": "<b>Fichier de révocation</b>",
"POPUP_REVOKE_MESSAGE": "Pour sécuriser votre compte, veuillez télécharger le <b>document de révocation de compte</b>. Il vous permettra le cas échéant d'annuler votre compte (en cas d'un vol de compte, d'un changement d'identifiant, d'un compte créé à tort, etc.).<br/><br/><b>Veuillez le stocker en lieu sûr.</b>"
},
"HELP": {
"TITLE": "Aide en ligne",
"JOIN": {
"SECTION": "Inscription",
"SALT": "L'identifiant sert à vous identifier sur votre compte gchange.<br/><b>Veillez à bien le mémoriser</b>, car aucun moyen n'est actuellement prévu pour le retrouver en cas de perte.<br/>Par ailleurs, il ne peut pas être modifié sans devoir créer un nouveau compte.<br/><br/>Un bon identifiant doit être suffisamment long (au moins 8 caractères) et le plus original possible.",
"PASSWORD": "Le mot de passe est très essentiel pour accéder à votre compte.<br/><b>Veillez à bien le mémoriser</b>, car aucun moyen n'est actuellement prévu pour le retrouver en cas de perte (sauf à générer un fichier de sauvegarde).<br/>Par ailleurs, il ne peut pas être modifié sans devoir créer un nouveau compte.<br/><br/>Un bon mot de passe contient (idéalement) au moins 8 caractères, dont au moins une majuscule et un chiffre.",
"PSEUDO": "Le pseudonyme est utilisé uniquement dans le cas d'inscription comme <span class=\"text-italic\">membre</span>. Il est toujours associé à un portefeuille (via sa <span class=\"text-italic\">clé publique</span>).<br/>Il est publié sur le réseau, afin que les autres utilisateurs puisse l'identifier, le certifier ou envoyer de la monnaie sur le compte.<br/>Un pseudonyme doit être unique au sein des membres (<u>actuels</u> et anciens)."
},
"LOGIN": {
"SECTION": "Connexion",
"PUBKEY": "Clé publique du trousseau",
"PUBKEY_DEF": "La clef publique du trousseau est générée à partir des identifiants saisis (n'importe lesquels), sans pour autant qu'ils correspondent à un compte déjà utilisé.<br/><b>Vérifiez attentivement que la clé publique est celle de votre compte</b>. Dans le cas contraire, vous serez connecté à un compte probablement jamais utilisé, le risque de collision avec un compte existant étant infime.<br/><a href=\"https://fr.wikipedia.org/wiki/Cryptographie_asym%C3%A9trique\" target=\"_system\">En savoir plus sur la cryptographie</a> par clé publique.",
"METHOD": "Méthodes de connexion",
"METHOD_DEF": "Plusieurs options sont disponibles pour vous connecter à un portefeuille :<br/> - La connexion <b>par salage (simple ou avancé)</b> mélange votre mot de passe grâce à l'identifiant, pour limiter les tentatives de <a href=\"https://fr.wikipedia.org/wiki/Attaque_par_force_brute\" target=\"_system\">piratage par force brute</a> (par exemple à partir de mots connus).<br/> - La connexion <b>par clé publique</b> évite de saisir vos identifiants, qui vous seront demandés seulement le moment venu lors d'une opération sur le compte.<br/> - La connexion <b>par fichier de trousseau</b> va lire les clés (publique et privée) du compte, depuis un fichier, sans besoin de saisir d'identifiants. Plusieurs formats de fichier sont possibles."
},
"GLOSSARY": {
"SECTION": "Glossaire",
"PUBKEY_DEF": "Une clé publique identifie un compte. Elle est calculée grâce à l'identifiant et au mot de passe.",
"MEMBER": "Membre",
"MEMBER_DEF": "Un membre est une personne humaine physique et vivante, désireuse de participer librement à la communauté monétaire. Elle perçoit un dividende universel, suivant une période et un montant tels que définis dans les <span class=\"text-italic\">règles de la monnaie</span>",
"CURRENCY_RULES": "Règles de la monnaie",
"CURRENCY_RULES_DEF": "Les règles de la monnaie sont définies une fois pour toutes. Elle fixe le fonctionnement de la monnaie : le calcul du dividende universel, le nombre de certifications nécessaires pour être membre, le nombre de certifications maximal qu'un membre peut donner, etc. <a href=\"#/app/currency\">Voir les règles actuelles</a>.<br/>La non modification des règles dans le temps est possible par l'utilisation d'une <span class=\"text-italic\">BlockChain</span> qui porte et exécute ces règles, et en vérifie constamment la bonne application.",
"BLOCKCHAIN": "Chaîne de blocs (<span class=\"text-italic\">Blockchain</span>)",
"BLOCKCHAIN_DEF": "La BlockChain est un système décentralisé, qui, dans le cas de Duniter, sert à porter et exécuter les <span class=\"text-italic\">règles de la monnaie</span>.<br/><a href=\"https://duniter.org/fr/comprendre/\" target=\"_system\">En savoir plus sur Duniter</a> et le fonctionnement de sa blockchain.",
"UNIVERSAL_DIVIDEND_DEF": "Le Dividende Universel (DU) est la quantité de monnaie co-créée par chaque membre, suivant la période et le calcul définis dans les <span class=\"text-italic\">règles de la monnaie</span>.<br/>A chaque échéance, les membres reçoivent sur leur compte la même quantité de nouvelle monnaie.<br/><br/>Le DU subit une croissance régulière, pour rester juste entre les membres (actuels et à venir), calculée en fonction de l'espérance de vie moyenne, telle que démontré dans la Thérorie Relative de la Monnaie (TRM).<br/><a href=\"http://trm.creationmonetaire.info\" target=\"_system\">En savoir plus sur la TRM</a> et les monnaies libres."
},
"TIP": {
"MENU_BTN_CURRENCY": "Le menu <b>{{'MENU.CURRENCY'|translate}}</b> permet la consultation des <b>règles de la monnaie</b> et de son état.",
"CURRENCY_WOT": "Le <b>nombre de membres</b> montre l'importance de la communauté et permet de <b>suivre son évolution</b>.",
"CURRENCY_MASS": "Suivez ici la <b>quantité totale de monnaie</b> existante et sa <b>répartition moyenne</b> par membre.<br/><br/>Ceci permet de juger de l'<b>importance d'un montant</b>, vis à vis de ce que <b>possède les autres</b> sur leur compte (en moyenne).",
"CURRENCY_UNIT_RELATIVE": "L'unité utilisée (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifie que les montants en {{currency|capitalize}} ont été divisés par le <b>Dividende Universel</b> (DU).<br/><br/><small>Cette unité relative est <b>pertinente</b>, car stable malgré la quantitié de monnaie qui augmente en permanence.</small>",
"CURRENCY_CHANGE_UNIT": "L'option <b>{{'COMMON.BTN_RELATIVE_UNIT'|translate}}</b> permet de <b>changer d'unité</b>, pour visualiser les montants <b>directement en {{currency|capitalize}}</b> (plutôt qu'en &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;).",
"CURRENCY_CHANGE_UNIT_TO_RELATIVE": "L'option <b>{{'COMMON.BTN_RELATIVE_UNIT'|translate}}</b> permet de <b>changer d'unité</b>, pour visualiser les montants en &ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;, c'est-à-dire relativement au Dividende Universel (le montant co-produit par chaque membre).",
"CURRENCY_RULES": "Les <b>règles</b> de la monnaie fixent son fonctionnement <b>exact et prévisible</b>.<br/><br/>Véritable ADN de la monnaie, elles rendent son code monétaire <b>lisible et transparent</b>.",
"MENU_BTN_NETWORK": "Le menu <b>{{'MENU.NETWORK'|translate}}</b> permet la consultation de l'état du réseau.",
"NETWORK_BLOCKCHAIN": "Toutes les opérations de la monnaie sont enregistrées dans un grand livre de compte <b>public et infalsifiable</b>, appelé aussi <b>chaîne de blocs</b> (<em>BlockChain</em> en anglais).",
"NETWORK_PEERS": "Les <b>nœuds</b> visibles ici correspondent aux <b>ordinateurs qui actualisent et contrôlent</b> la chaîne de blocs.<br/><br/>Plus il y a de nœuds, plus la monnaie a une gestion <b>décentralisée</b> et digne de confiance.",
"NETWORK_PEERS_BLOCK_NUMBER": "Ce <b>numéro</b> (en vert) indique le <b>dernier bloc validé</b> pour ce nœud (dernière page écrite dans le grand livre de comptes).<br/><br/>La couleur verte indique que ce bloc est également validé par <b>la plupart des autres nœuds</b>.",
"NETWORK_PEERS_PARTICIPATE": "<b>Chaque membre</b>, équipé d'un ordinateur avec Internet, <b>peut participer en ajoutant un nœud</b>. Il suffit d'<b>installer le logiciel Duniter</b> (libre et gratuit). <a href=\"{{installDocUrl}}\" target=\"_system\">Voir le manuel d'installation &gt;&gt;</a>.",
"MENU_BTN_ACCOUNT": "Le menu <b>{{'ACCOUNT.TITLE'|translate}}</b> permet d'accéder à la gestion de votre compte.",
"MENU_BTN_ACCOUNT_MEMBER": "Consultez ici l'état de votre compte et les informations sur vos certifications.",
"WALLET_CERTIFICATIONS": "Cliquez ici pour consulter le détail de vos certifications (reçues et émises).",
"WALLET_RECEIVED_CERTIFICATIONS": "Cliquez ici pour consulter le détail de vos <b>certifications reçues</b>.",
"WALLET_GIVEN_CERTIFICATIONS": "Cliquez ici pour consulter le détail de vos <b>certifications émises</b>.",
"WALLET_BALANCE": "Le <b>solde</b> de votre compte s'affiche ici.",
"WALLET_BALANCE_RELATIVE": "{{'HELP.TIP.WALLET_BALANCE'|translate}}<br/><br/>L'unité utilisée (&ldquo;<b>{{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub></b>&rdquo;) signifie que le montant en {{currency|capitalize}} a été divisé par le <b>Dividende Universel</b> (DU) co-créé par chaque membre.<br/><br/>Actuellement 1 DU vaut {{currentUD|formatInteger}} {{currency|capitalize}}s.",
"WALLET_BALANCE_CHANGE_UNIT": "Vous pourrez <b>changer l'unité</b> d'affichage des montants dans les <b><i class=\"icon ion-android-settings\"></i>&nbsp;{{'MENU.SETTINGS'|translate}}</b>.<br/><br/>Par exemple pour visualiser les montants <b>directement en {{currency|capitalize}}</b>, plutôt qu'en unité relative.",
"WALLET_PUBKEY": "Voici la clé publique de votre compte. Vous pouvez la communiquer à un tiers afin qu'il identifie plus simplement votre compte.",
"WALLET_SEND": "Effectuer un paiement en quelques clics.",
"WALLET_SEND_NO_MONEY": "Effectuer un paiement en quelques clics.<br/>(Votre solde ne le permet pas encore)",
"WALLET_OPTIONS": "Ce bouton permet l'accès aux <b>actions d'adhésion</b> et de sécurité.<br/><br/>N'oubliez pas d'y jeter un oeil !",
"WALLET_RECEIVED_CERTS": "S'affichera ici la liste des personnes qui vous ont certifié.",
"WALLET_CERTIFY": "Le bouton <b>{{'WOT.BTN_SELECT_AND_CERTIFY'|translate}}</b> permet de sélectionner une identité et de la certifier.<br/><br/>Seuls des utilisateurs <b>déjà membres</b> peuvent en certifier d'autres.",
"WALLET_CERT_STOCK": "Votre stock de certifications (émises) est limité à <b>{{sigStock}} certifications</b>.<br/><br/>Ce stock se renouvelle avec le temps, au fur et à mesure que les certifications s'invalident.",
"MENU_BTN_TX_MEMBER": "Le menu <b>{{'MENU.TRANSACTIONS'|translate}}</b> permet de consulter votre solde, l'historique vos transactions, et d'envoyer un paiement.",
"MENU_BTN_TX": "Consultez ici <b>l'historique de vos transactions</b> et effectuez de nouvelles opérations.",
"MENU_BTN_WOT": "Le menu <b>{{'MENU.WOT'|translate}}</b> permet de rechercher parmi les <b>utilisateurs</b> de la monnaie (membre ou non).",
"WOT_SEARCH_TEXT_XS": "Pour rechercher dans l'annuaire, tapez les <b>premières lettres d'un pseudonyme</b> (ou d'une clé publique).<br/><br/>La recherche se lancera automatiquement.",
"WOT_SEARCH_TEXT": "Pour rechercher dans l'annuaire, tapez les <b>premières lettres d'un pseudonyme</b> (ou d'une clé publique). <br/><br/>Appuyer ensuite sur <b>Entrée</b> pour lancer la recherche.",
"WOT_SEARCH_RESULT": "Visualisez la fiche détaillée simplement en <b>cliquant</b> sur une ligne.",
"WOT_VIEW_CERTIFICATIONS": "La ligne <b>{{'ACCOUNT.CERTIFICATION_COUNT'|translate}}</b> montre combien de membres ont validé cette identité.<br/><br/>Ces certifications attestent que le compte appartient à <b>une personne humaine vivante</b> n'ayant <b>aucun autre compte membre</b>.",
"WOT_VIEW_CERTIFICATIONS_COUNT": "Il faut au moins <b>{{sigQty}} certifications</b> pour devenir membre et recevoir le <b>Dividende Universel</b>.",
"WOT_VIEW_CERTIFICATIONS_CLICK": "Un clic ici permet d'ouvrir <b>la liste de toutes les certifications</b> de l'identité (reçues et émises).",
"WOT_VIEW_CERTIFY": "Le bouton <b>{{'WOT.BTN_CERTIFY'|translate}}</b> permet d'ajouter votre certification à cette identité.",
"CERTIFY_RULES": "<b>Attention :</b> Ne certifier que des <b>personnes physiques vivantes</b>, ne possèdant aucun autre compte membre.<br/><br/>La sécurité de la monnaie dépend de la vigilance de chacun !",
"MENU_BTN_SETTINGS": "Les <b>{{'MENU.SETTINGS'|translate}}</b> vous permettront de configurer l'application.",
"HEADER_BAR_BTN_PROFILE": "Cliquez ici pour accéder à votre <b>profil utilisateur</b>",
"SETTINGS_CHANGE_UNIT": "Vous pourrez <b>changer d'unité d'affichage</b> des montants en cliquant ci-dessus.<br/><br/>- Désactivez l'option pour un affichage des montants en {{currency|capitalize}}.<br/>- Activez l'option pour un affichage relatif en {{'COMMON.UD'|translate}}<sub>{{currency|abbreviate}}</sub> (tous les montants seront <b>divisés</b> par le Dividende Universel courant).",
"END_LOGIN": "Cette visite guidée est <b>terminée</b> !<br/><br/>Bonne continuation à vous, dans le nouveau monde de l'<b>économie libre</b> !",
"END_NOT_LOGIN": "Cette visite guidée est <b>terminée</b> !<br/><br/>Si vous souhaitez rejoindre la monnaie {{currency|capitalize}}, il vous suffira de cliquer sur <b>{{'LOGIN.CREATE_ACCOUNT'|translate}}</b> ci-dessous."
}
}
}
);
}]);
angular.module('cesium.plugins', [
'cesium.plugins.translations',
'cesium.plugins.templates',
// ES plugin
'cesium.es.plugin',
// Market plugin
'cesium.market.plugin',
// Map plugin
'cesium.map.plugin',
// Graph plugin
'cesium.graph.plugin'
])
;
angular.module("cesium.plugins.translations", []).config(["$translateProvider", function($translateProvider) {
$translateProvider.translations("en-GB", {
"COMMON": {
"CATEGORY": "Category",
"CATEGORY_SELECT_HELP": "Select",
"CATEGORIES": "Categories",
"CATEGORY_SEARCH_HELP": "Search",
"COMMENT_HELP": "Comments",
"LAST_MODIFICATION_DATE": "Updated on ",
"BTN_LIKE": "I like",
"BTN_FOLLOW": "Follow",
"BTN_STOP_FOLLOW": "Stop following",
"LIKES_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} liked this page",
"DISLIKES_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} disliked this page",
"VIEWS_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} viewed this page",
"FOLLOWS_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} follows this page",
"ABUSES_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} reported a problem on this page",
"BTN_REPORT_ABUSE_DOTS": "Report a problem or an abuse...",
"BTN_REMOVE_REPORTED_ABUSE": "Cancel my problem report",
"SUBMIT_BY": "Submitted by",
"GEO_DISTANCE_SEARCH": "Search distance",
"GEO_DISTANCE_OPTION": "{{value}} km",
"BTN_PUBLISH": "Publish",
"BTN_PICTURE_DELETE": "Delete",
"BTN_PICTURE_FAVORISE": "Default",
"BTN_PICTURE_ROTATE": "Rotate",
"BTN_ADD_PICTURE": "Add picture",
"NOTIFICATION": {
"TITLE": "New notification | {{'COMMON.APP_NAME'|translate}}",
"HAS_UNREAD": "You have {{count}} unread notification{{count>0?'s':''}}"
},
"NOTIFICATIONS": {
"TITLE": "Notifications",
"MARK_ALL_AS_READ": "Mark all as read",
"NO_RESULT": "No notification",
"SHOW_ALL": "Show all",
"LOAD_NOTIFICATIONS_FAILED": "Could not load notifications"
},
"REPORT_ABUSE": {
"TITLE": "Report a problem",
"SUB_TITLE": "Please explain briefly the problem:",
"REASON_HELP": "I explain the problem...",
"ASK_DELETE": "Request removal?",
"CONFIRM": {
"SENT": "Request sent. Thnak you!"
}
}
},
"MENU": {
"REGISTRY": "Pages",
"USER_PROFILE": "My Profile",
"MESSAGES": "Messages",
"NOTIFICATIONS": "Notifications",
"INVITATIONS": "Invitations"
},
"ACCOUNT": {
"NEW": {
"ORGANIZATION_ACCOUNT": "Account for an organization",
"ORGANIZATION_ACCOUNT_HELP": "If you represent a company, association, etc.<br/>No universal dividend will be created by this account."
},
"EVENT": {
"MEMBER_WITHOUT_PROFILE": "To obtain your certification more quickly, fill in <a ui-sref=\"app.user_edit_profile\">your user profile</a>. Members will more easily put their trust in a verifiable identity."
},
"ERROR": {
"WS_CONNECTION_FAILED": "Cesium can not receive notifications because of a technical error (connection to the Cesium + data node).<br/><br/>If the problem persists, please <b>choose another data node</b> in Cesium+ settings."
}
},
"WOT": {
"BTN_SUGGEST_CERTIFICATIONS_DOTS": "Suggest identities to certify...",
"BTN_ASK_CERTIFICATIONS_DOTS": "Ask members to certify me...",
"BTN_ASK_CERTIFICATION": "Ask a certification",
"SUGGEST_CERTIFICATIONS_MODAL": {
"TITLE": "Suggest certifications",
"HELP": "Select your suggestions"
},
"ASK_CERTIFICATIONS_MODAL": {
"TITLE": "Ask certifications",
"HELP": "Select recipients"
},
"SEARCH": {
"DIVIDER_PROFILE": "Profile",
"DIVIDER_PAGE": "Pages",
"DIVIDER_GROUP": "Groups"
},
"VIEW": {
"STARS": "Trust level",
"STAR_HIT_COUNT": "{{total}} rate{{total>1 ? 's' : ''}}",
"BTN_STAR_HELP": "Rate this profile",
"BTN_STARS_REMOVE": "Remove my note",
"BTN_REDO_STAR_HELP": "Update your rate for this profile",
"BTN_FOLLOW": "Follow the activity of this profile",
"BTN_STOP_FOLLOW": "Stop following this profil"
}
},
"COMMENTS": {
"DIVIDER": "Comments",
"SHOW_MORE_COMMENTS": "Show previous comments",
"COMMENT_HELP": "Your comment, question...",
"COMMENT_HELP_REPLY_TO": "Your answer...",
"BTN_SEND": "Send",
"POPOVER_SHARE_TITLE": "Message #{{number}}",
"REPLY": "Reply",
"REPLY_TO": "Respond to:",
"REPLY_TO_LINK": "In response to ",
"REPLY_TO_DELETED_COMMENT": "In response to a deleted comment",
"REPLY_COUNT": "{{replyCount}} responses",
"DELETED_COMMENT": "Comment deleted",
"MODIFIED_ON": "modified on {{time|formatDate}}",
"MODIFIED_PARENTHESIS": "(modified then)",
"ERROR": {
"FAILED_SAVE_COMMENT": "Saving comment failed",
"FAILED_REMOVE_COMMENT": "Deleting comment failed"
}
},
"MESSAGE": {
"REPLY_TITLE_PREFIX": "Re: ",
"FORWARD_TITLE_PREFIX": "Fw: ",
"BTN_REPLY": "Reply",
"BTN_COMPOSE": "New message",
"BTN_WRITE": "Write",
"NO_MESSAGE_INBOX": "No message received",
"NO_MESSAGE_OUTBOX": "No message sent",
"NOTIFICATIONS": {
"TITLE": "Messages",
"MESSAGE_RECEIVED": "You <b>received a message</b><br/>from"
},
"LIST": {
"INBOX": "Inbox",
"OUTBOX": "Outbox",
"LAST_INBOX": "New messages",
"LAST_OUTBOX": "Sent messages",
"BTN_LAST_MESSAGES": "Recent messages",
"TITLE": "Private messages",
"SEARCH_HELP": "Search in messages",
"POPOVER_ACTIONS": {
"TITLE": "Options",
"DELETE_ALL": "Delete all messages"
}
},
"COMPOSE": {
"TITLE": "New message",
"TITLE_REPLY": "Reply",
"SUB_TITLE": "New message",
"TO": "To",
"OBJECT": "Object",
"OBJECT_HELP": "Object",
"ENCRYPTED_HELP": "Please note this message will be encrypted before sending so that only the recipient can read it and be sure you are the author.",
"MESSAGE": "Message",
"MESSAGE_HELP": "Message content",
"CONTENT_CONFIRMATION": "No message content.<br/><br/>Are your sure you want to send this message?"
},
"VIEW": {
"TITLE": "Message",
"SENDER": "Sent by",
"RECIPIENT": "Sent to",
"NO_CONTENT": "Empty message",
"DELETE": "Delete the message"
},
"CONFIRM": {
"REMOVE": "Are you sure you want to <b>delete this message</b>?<br/><br/> This operation is irreversible.",
"REMOVE_ALL": "Are you sure you want to <b>delete all messages</b>?<br/><br/> This operation is irreversible.",
"MARK_ALL_AS_READ": "Are you sure you want to <b>mark all message as read</b>?",
"USER_HAS_NO_PROFILE": "This identity has no Cesium + profile. It may not use the Cesium + extension, so it <b>will not read your message</b>.<br/><br/>Are you sure you want <b>to continue</b>?"
},
"INFO": {
"MESSAGE_REMOVED": "Message successfully deleted",
"All_MESSAGE_REMOVED": "Messages successfully deleted",
"MESSAGE_SENT": "Message sent"
},
"ERROR": {
"SEND_MSG_FAILED": "Error while sending message.",
"LOAD_MESSAGES_FAILED": "Error while loading messages.",
"LOAD_MESSAGE_FAILED": "Error while loading message.",
"MESSAGE_NOT_READABLE": "Unable to read message.",
"USER_NOT_RECIPIENT": "You are not the recipient of this message: unable to read it.",
"NOT_AUTHENTICATED_MESSAGE": "The authenticity of the message is not certain or its content is corrupted.",
"REMOVE_MESSAGE_FAILED": "Error while deleting message",
"MESSAGE_CONTENT_TOO_LONG": "Value too long ({{maxLength}} characters max).",
"MARK_AS_READ_FAILED": "Unable to mark the message as 'read'.",
"LOAD_NOTIFICATIONS_FAILED": "Error while loading messages notifications.",
"REMOVE_All_MESSAGES_FAILED": "Error while removing all messages.",
"MARK_ALL_AS_READ_FAILED": "Error while marking messages as read.",
"RECIPIENT_IS_MANDATORY": "Recipient is mandatory."
}
},
"REGISTRY": {
"CATEGORY": "Main activity",
"GENERAL_DIVIDER": "Basic information",
"LOCATION_DIVIDER": "Address",
"SOCIAL_NETWORKS_DIVIDER": "Social networks, web sites",
"TECHNICAL_DIVIDER": "Technical data",
"BTN_SHOW_WOT": "People",
"BTN_SHOW_WOT_HELP": "Search for people",
"BTN_SHOW_PAGES": "Pages",
"BTN_SHOW_PAGES_HELP": "Search for pages",
"BTN_NEW": "New page",
"MY_PAGES": "My pages",
"NO_PAGE": "No page",
"SEARCH": {
"TITLE": "Pages",
"SEARCH_HELP": "What, Who: hairdresser, Lili's restaurant, ...",
"BTN_ADD": "New",
"BTN_LAST_RECORDS": "Recent pages",
"BTN_ADVANCED_SEARCH": "Advanced search",
"BTN_OPTIONS": "Advanced search",
"TYPE": "Kind of organization",
"LOCATION_HELP": "Where: City, Country",
"RESULTS": "Results",
"RESULT_COUNT_LOCATION": "{{count}} result{{count>0?'s':''}}, near {{location}}",
"RESULT_COUNT": "{{count}} result{{count>0?'s':''}}",
"LAST_RECORDS": "Recent pages",
"LAST_RECORD_COUNT_LOCATION": "{{count}} recent page{{count>0?'s':''}}, near {{location}}",
"LAST_RECORD_COUNT": "{{count}} recent page{{count>0?'s':''}}",
"POPOVER_FILTERS": {
"BTN_ADVANCED_SEARCH": "Advanced options?"
}
},
"VIEW": {
"TITLE": "Registry",
"CATEGORY": "Main activity:",
"LOCATION": "Address:",
"MENU_TITLE": "Options",
"POPOVER_SHARE_TITLE": "{{title}}",
"REMOVE_CONFIRMATION" : "Are you sure you want to delete this reference?<br/><br/>This is irreversible."
},
"TYPE": {
"TITLE": "New page",
"SELECT_TYPE": "Kind of organization:",
"ENUM": {
"SHOP": "Local shops",
"COMPANY": "Company",
"ASSOCIATION": "Association",
"INSTITUTION": "Institution"
}
},
"EDIT": {
"TITLE": "Edit",
"TITLE_NEW": "New page",
"RECORD_TYPE":"Kind of organization",
"RECORD_TITLE": "Name",
"RECORD_TITLE_HELP": "Name",
"RECORD_DESCRIPTION": "Description",
"RECORD_DESCRIPTION_HELP": "Describe activity",
"RECORD_ADDRESS": "Street",
"RECORD_ADDRESS_HELP": "Street, building...",
"RECORD_CITY": "City",
"RECORD_CITY_HELP": "City, Country",
"RECORD_SOCIAL_NETWORKS": "Social networks and web site",
"RECORD_PUBKEY": "Public key",
"RECORD_PUBKEY_HELP": "Public key to receive payments"
},
"WALLET": {
"REGISTRY_DIVIDER": "Pages",
"REGISTRY_HELP": "Pages refer to activities accepting money or promoting it: local shops, companies, associations, institutions."
},
"ERROR": {
"LOAD_CATEGORY_FAILED": "Loading main activities failed",
"LOAD_RECORD_FAILED": "Loading failed",
"LOOKUP_RECORDS_FAILED": "Error while loading records.",
"REMOVE_RECORD_FAILED": "Deleting failed",
"SAVE_RECORD_FAILED": "Saving failed",
"RECORD_NOT_EXISTS": "Record not found",
"GEO_LOCATION_NOT_FOUND": "City or zip code not found"
},
"INFO": {
"RECORD_REMOVED" : "Page successfully deleted",
"RECORD_SAVED": "Page successfully saved"
}
},
"PROFILE": {
"PROFILE_DIVIDER": "ğchange profile",
"PROFILE_DIVIDER_HELP": "It is related data, stored in the ğchange network.",
"NO_PROFILE_DEFINED": "No ğchange profile",
"BTN_ADD": "Create my profile",
"BTN_EDIT": "Edit my profile",
"BTN_DELETE": "Delete my profile",
"BTN_REORDER": "Reorder",
"UID": "Pseudonym",
"TITLE": "Lastname, FirstName",
"TITLE_HELP": "Name",
"DESCRIPTION": "About me",
"DESCRIPTION_HELP": "About me...",
"SOCIAL_HELP": "http://...",
"GENERAL_DIVIDER": "General data",
"SOCIAL_NETWORKS_DIVIDER": "Social networks and web site",
"STAR": "Trust level",
"TECHNICAL_DIVIDER": "Technical data",
"MODAL_AVATAR": {
"TITLE": "Avatar",
"SELECT_FILE_HELP": "<b>Choose an image file</b>, by clicking on the button below:",
"BTN_SELECT_FILE": "Choose an image",
"RESIZE_HELP": "<b>Re-crop the image</b> if necessary. A click on the image allows to move it. Click on the area at the bottom left to zoom in.",
"RESULT_HELP": "<b>Here is the result</b> as seen on your profile:"
},
"CONFIRM": {
"DELETE": "Are you sure you want to <b>delete your Cesium+ profile ?</b><br/><br/>This operation is irreversible."
},
"ERROR": {
"REMOVE_PROFILE_FAILED": "Deleting profile failed",
"LOAD_PROFILE_FAILED": "Could not load user profile.",
"SAVE_PROFILE_FAILED": "Saving profile failed",
"INVALID_SOCIAL_NETWORK_FORMAT": "Invalid format: please fill a valid Internet address.<br/><br/>Examples :<ul><li>- A Facebook page (https://www.facebook.com/user)</li><li>- A web page (http://www.domain.com)</li><li>- An email address (joe@dalton.com)</li></ul>",
"IMAGE_RESIZE_FAILED": "Error while resizing picture"
},
"INFO": {
"PROFILE_REMOVED": "Profile deleted",
"PROFILE_SAVED": "Profile saved"
},
"HELP": {
"WARNING_PUBLIC_DATA": "Please note that the information published here <b>is public</b>: visible including by <b>not logged in people</b>."
}
},
"LOCATION": {
"BTN_GEOLOC_ADDRESS": "Find my address on the map",
"USE_GEO_POINT": "geo-locate (recommended)?",
"LOADING_LOCATION": "Searching address...",
"LOCATION_DIVIDER": "Localisation",
"ADDRESS": "Address",
"ADDRESS_HELP": "Address (optional)",
"CITY": "City",
"CITY_HELP": "City, Country",
"DISTANCE": "Maximum distance around the city",
"DISTANCE_UNIT": "mi",
"DISTANCE_OPTION": "{{value}} {{'LOCATION.DISTANCE_UNIT'|translate}}",
"SEARCH_HELP": "City, Country",
"PROFILE_POSITION": "Profile position",
"MODAL": {
"TITLE": "Search address",
"SEARCH_HELP": "City, Country",
"ALTERNATIVE_RESULT_DIVIDER": "Alternative results for <b>{{address}}</b>:",
"POSITION": "lat/lon : {{lat}} / {{lon}}"
},
"ERROR": {
"CITY_REQUIRED_IF_STREET": "Required if a street has been filled",
"REQUIRED_FOR_LOCATION": "Required field to appear on the map",
"INVALID_FOR_LOCATION": "Unknown address",
"GEO_LOCATION_FAILED": "Unable to retrieve your current position. Please use the search button.",
"ADDRESS_LOCATION_FAILED": "Unable to retrieve the address position"
}
},
"SUBSCRIPTION": {
"SUBSCRIPTION_DIVIDER": "Online services",
"SUBSCRIPTION_DIVIDER_HELP": "Online services offer optional additional services, delegated to a third party.",
"BTN_ADD": "Add a service",
"BTN_EDIT": "Manage my services",
"NO_SUBSCRIPTION": "No service defined",
"SUBSCRIPTION_COUNT": "Services / Subscription",
"EDIT": {
"TITLE": "Online services",
"HELP_TEXT": "Manage your subscriptions and other online services here",
"PROVIDER": "Provider:"
},
"TYPE": {
"ENUM": {
"EMAIL": "Receive email notifications"
}
},
"CONFIRM": {
"DELETE_SUBSCRIPTION": "Are you sur you want to <b>delete this subscription</b>?"
},
"ERROR": {
"LOAD_SUBSCRIPTIONS_FAILED": "Error while loading online services",
"ADD_SUBSCRIPTION_FAILED": "Error while adding subscription",
"UPDATE_SUBSCRIPTION_FAILED": "Error during subscription update",
"DELETE_SUBSCRIPTION_FAILED": "Error while deleting subscription"
},
"MODAL_EMAIL": {
"TITLE" : "Notification by email",
"HELP" : "Fill out this form to <b>be notified by email</ b> of your account's events. <br/>Your email address will be encrypted only to be visible to the service provider.",
"EMAIL_LABEL" : "Your email:",
"EMAIL_HELP": "john@domain.com",
"FREQUENCY_LABEL": "Frequency of notifications:",
"FREQUENCY_DAILY": "Daily",
"FREQUENCY_WEEKLY": "Weekly",
"PROVIDER": "Service Provider:"
}
},
"DOCUMENT": {
"HASH": "Hash: ",
"LOOKUP": {
"TITLE": "Document search",
"BTN_ACTIONS": "Actions",
"SEARCH_HELP": "issuer:AAA*, time:1508406169",
"LAST_DOCUMENTS_DOTS": "Last documents :",
"LAST_DOCUMENTS": "Last documents",
"SHOW_QUERY": "Show query",
"HIDE_QUERY": "Hide query",
"HEADER_TIME": "Time/Hour",
"HEADER_ISSUER": "Issuer",
"HEADER_RECIPIENT": "Recipient",
"READ": "Read",
"DOCUMENT_TYPE": "Type",
"DOCUMENT_TITLE": "Title",
"BTN_REMOVE": "Delete this document",
"BTN_COMPACT": "Compact",
"HAS_REGISTERED": "create or edit his profile",
"POPOVER_ACTIONS": {
"TITLE": "Actions",
"REMOVE_ALL": "Delete these documents..."
},
"TYPE": {
"USER_PROFILE": "Profile",
"MARKET_RECORD": "Ad",
"MARKET_COMMENT": "Comment on a ad",
"PAGE_RECORD": "Page",
"PAGE_COMMENT": "Comment on a page",
"GROUP_RECORD": "Group",
"GROUP_COMMENT": "Comment on a group"
}
},
"INFO": {
"REMOVED": "Deleted document"
},
"CONFIRM": {
"REMOVE": "Are you sure you want to <b>delete this document</b>?",
"REMOVE_ALL": "Are you sure you want to <b>delete these documents</b>?"
},
"ERROR": {
"LOAD_DOCUMENTS_FAILED": "Error searching documents",
"REMOVE_FAILED": "Error deleting the document",
"REMOVE_ALL_FAILED": "Error deleting documents"
}
},
"ES_SETTINGS": {
"PLUGIN_NAME": "Cesium+",
"PLUGIN_NAME_HELP": "User profiles, notifications, private messages",
"ENABLE_TOGGLE": "Enable extension?",
"ENABLE_MESSAGE_TOGGLE": "Enable messages?",
"ENABLE_SETTINGS_TOGGLE": "Enable remote storage for settings?",
"PEER": "Data peer address",
"POPUP_PEER": {
"TITLE" : "Data peer",
"HELP" : "Set the address of the peer to use:",
"PEER_HELP": "server.domain.com:port"
},
"NOTIFICATIONS": {
"DIVIDER": "Notifications",
"HELP_TEXT": "Enable the types of notifications you want to receive:",
"ENABLE_TX_SENT": "Notify the validation of <b>sent payments</b>?",
"ENABLE_TX_RECEIVED": "Notify the validation of <b>received payments</b>?",
"ENABLE_CERT_SENT": "Notify the validation of <b>sent certifications</b>?",
"ENABLE_CERT_RECEIVED": "Notify the validation of <b>received certifications</b>?"
},
"CONFIRM": {
"ASK_ENABLE_TITLE": "New features",
"ASK_ENABLE": "Some new features are available: <ul><li>&nbsp;&nbsp;<b><i class=\"icon ion-person\"></i> user profiles</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-android-notifications\"></i> Notifications</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-email\"></i> Private messages</b>.</ul><br/>They have been <b>disabled</b> in your settings.<br/><br/><b>Do you want to enable</b> these features?"
}
},
"ES_WALLET": {
"ERROR": {
"RECIPIENT_IS_MANDATORY": "A recipient is required for encryption."
}
},
"ES_PEER": {
"NAME": "Name",
"DOCUMENTS": "Documents",
"SOFTWARE": "Software",
"DOCUMENT_COUNT": "Number of documents",
"EMAIL_SUBSCRIPTION_COUNT": "{{emailSubscription}} subscribers to email notification"
},
"EVENT": {
"NODE_STARTED": "Your node ES API <b>{{params[0]}}</b> is UP",
"NODE_BMA_DOWN": "Node <b>{{params[0]}}:{{params[1]}}</b> (used by your ES API) is <b>unreachable</b>.",
"NODE_BMA_UP": "Node <b>{{params[0]}}:{{params[1]}}</b> is reachable again.",
"MEMBER_JOIN": "You are now a <b>member</b> of currency <b>{{params[0]}}</b>!",
"MEMBER_LEAVE": "You are <b>not a member anymore</b> of currency <b>{{params[0]}}</b>!",
"MEMBER_EXCLUDE": "You are <b>not more member</b> of the currency <b>{{params[0]}}</b>, for lack of renewal or lack of certifications.",
"MEMBER_REVOKE": "Your account has been revoked. It will no longer be a member of the currency <b>{{params[0]}}</b>.",
"MEMBER_ACTIVE": "Your membership to <b>{{params[0]}}</b> has been <b>renewed successfully</b>.",
"TX_SENT": "Your payment to <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> was executed.",
"TX_SENT_MULTI": "Your payment to <b>{{params[1]}}</b> was executed.",
"TX_RECEIVED": "You received a payment from <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"TX_RECEIVED_MULTI": "You received a payment from <b>{{params[1]}}</b>.",
"CERT_SENT": "Your <b>certification</b> to <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> was executed.",
"CERT_RECEIVED": "You have <b>received a certification</b> from <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"USER": {
"LIKE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> like your profile",
"FOLLOW_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> follows your activity",
"STAR_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> rated you ({{params[3]}} <i class=\"ion-star\">)",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> asks you for a moderation on the profile: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> reported a profile to be deleted: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has requested moderation on your profile"
},
"PAGE": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has commented on your referencing: <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his comment on your referencing: <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has replied to your comment on the referencing: <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his answer to your comment, on the referencing: <b>{{params[2]}}</b>",
"FOLLOW_NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has commented on the page: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his comment on the page: <b>{{params[2]}}</b>",
"FOLLOW_NEW": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> added a page: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> updated the page: <b>{{params[2]}}</b>",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> asks you for a moderation on the page: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> reported a page to be deleted: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has requested moderation on your page: <b>{{params[2]}}</b>"
}
},
"CONFIRM": {
"ES_USE_FALLBACK_NODE": "Data node <b>{{old}}</b> unreachable or invalid address.<br/><br/>Do you want to temporarily use the <b>{{new}}</b> data node?"
},
"ERROR": {
"ES_CONNECTION_ERROR": "Data node <b>{{server}}</b> unreachable or invalid address.<br/><br/>Check your Internet connection, or change data node in <a class=\"positive\" ng-click=\"doQuickFix('settings')\">advanced settings</a>.",
"ES_MAX_UPLOAD_BODY_SIZE": "The volume of data to be sent exceeds the limit set by the server.<br/><br/>Please try again after, for example, deleting photos."
}
}
);
$translateProvider.translations("en", {
"COMMON": {
"CATEGORY": "Category",
"CATEGORY_SELECT_HELP": "Select",
"CATEGORIES": "Categories",
"CATEGORY_SEARCH_HELP": "Search",
"COMMENT_HELP": "Comments",
"LAST_MODIFICATION_DATE": "Updated on ",
"BTN_LIKE": "I like",
"BTN_FOLLOW": "Follow",
"BTN_STOP_FOLLOW": "Stop following",
"LIKES_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} liked this page",
"DISLIKES_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} disliked this page",
"VIEWS_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} viewed this page",
"FOLLOWS_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} follows this page",
"ABUSES_TEXT": "{{total}} {{total > 1 ? 'people' : 'person'}} reported a problem on this page",
"BTN_REPORT_ABUSE_DOTS": "Report a problem or an abuse...",
"BTN_REMOVE_REPORTED_ABUSE": "Cancel my problem report",
"SUBMIT_BY": "Submitted by",
"GEO_DISTANCE_SEARCH": "Search distance",
"GEO_DISTANCE_OPTION": "{{value}} km",
"BTN_PUBLISH": "Publish",
"BTN_PICTURE_DELETE": "Delete",
"BTN_PICTURE_FAVORISE": "Default",
"BTN_PICTURE_ROTATE": "Rotate",
"BTN_ADD_PICTURE": "Add picture",
"NOTIFICATION": {
"TITLE": "New notification | {{'COMMON.APP_NAME'|translate}}",
"HAS_UNREAD": "You have {{count}} unread notification{{count>0?'s':''}}"
},
"NOTIFICATIONS": {
"TITLE": "Notifications",
"MARK_ALL_AS_READ": "Mark all as read",
"NO_RESULT": "No notification",
"SHOW_ALL": "Show all",
"LOAD_NOTIFICATIONS_FAILED": "Could not load notifications"
},
"REPORT_ABUSE": {
"TITLE": "Report a problem",
"SUB_TITLE": "Please explain briefly the problem:",
"REASON_HELP": "I explain the problem...",
"ASK_DELETE": "Request removal?",
"CONFIRM": {
"SENT": "Request sent. Thnak you!"
}
}
},
"MENU": {
"REGISTRY": "Pages",
"USER_PROFILE": "My Profile",
"MESSAGES": "Messages",
"NOTIFICATIONS": "Notifications",
"INVITATIONS": "Invitations"
},
"ACCOUNT": {
"NEW": {
"ORGANIZATION_ACCOUNT": "Account for an organization",
"ORGANIZATION_ACCOUNT_HELP": "If you represent a company, association, etc.<br/>No universal dividend will be created by this account."
},
"EVENT": {
"MEMBER_WITHOUT_PROFILE": "To obtain your certification more quickly, fill in <a ui-sref=\"app.user_edit_profile\">your user profile</a>. Members will more easily put their trust in a verifiable identity."
},
"ERROR": {
"WS_CONNECTION_FAILED": "Cesium can not receive notifications because of a technical error (connection to the Cesium + data node).<br/><br/>If the problem persists, please <b>choose another data node</b> in Cesium+ settings."
}
},
"WOT": {
"BTN_SUGGEST_CERTIFICATIONS_DOTS": "Suggest identities to certify...",
"BTN_ASK_CERTIFICATIONS_DOTS": "Ask members to certify me...",
"BTN_ASK_CERTIFICATION": "Ask a certification",
"SUGGEST_CERTIFICATIONS_MODAL": {
"TITLE": "Suggest certifications",
"HELP": "Select your suggestions"
},
"ASK_CERTIFICATIONS_MODAL": {
"TITLE": "Ask certifications",
"HELP": "Select recipients"
},
"SEARCH": {
"DIVIDER_PROFILE": "Profile",
"DIVIDER_PAGE": "Pages",
"DIVIDER_GROUP": "Groups"
},
"VIEW": {
"STARS": "Trust level",
"STAR_HIT_COUNT": "{{total}} rate{{total>1 ? 's' : ''}}",
"BTN_STAR_HELP": "Rate this profile",
"BTN_STARS_REMOVE": "Remove my note",
"BTN_REDO_STAR_HELP": "Update your rate for this profile",
"BTN_FOLLOW": "Follow the activity of this profile",
"BTN_STOP_FOLLOW": "Stop following this profil"
}
},
"COMMENTS": {
"DIVIDER": "Comments",
"SHOW_MORE_COMMENTS": "Show previous comments",
"COMMENT_HELP": "Your comment, question...",
"COMMENT_HELP_REPLY_TO": "Your answer...",
"BTN_SEND": "Send",
"POPOVER_SHARE_TITLE": "Message #{{number}}",
"REPLY": "Reply",
"REPLY_TO": "Respond to:",
"REPLY_TO_LINK": "In response to ",
"REPLY_TO_DELETED_COMMENT": "In response to a deleted comment",
"REPLY_COUNT": "{{replyCount}} responses",
"DELETED_COMMENT": "Comment deleted",
"MODIFIED_ON": "modified on {{time|formatDate}}",
"MODIFIED_PARENTHESIS": "(modified then)",
"ERROR": {
"FAILED_SAVE_COMMENT": "Saving comment failed",
"FAILED_REMOVE_COMMENT": "Deleting comment failed"
}
},
"MESSAGE": {
"REPLY_TITLE_PREFIX": "Re: ",
"FORWARD_TITLE_PREFIX": "Fw: ",
"BTN_REPLY": "Reply",
"BTN_COMPOSE": "New message",
"BTN_WRITE": "Write",
"NO_MESSAGE_INBOX": "No message received",
"NO_MESSAGE_OUTBOX": "No message sent",
"NOTIFICATIONS": {
"TITLE": "Messages",
"MESSAGE_RECEIVED": "You <b>received a message</b><br/>from"
},
"LIST": {
"INBOX": "Inbox",
"OUTBOX": "Outbox",
"LAST_INBOX": "New messages",
"LAST_OUTBOX": "Sent messages",
"BTN_LAST_MESSAGES": "Recent messages",
"TITLE": "Private messages",
"SEARCH_HELP": "Search in messages",
"POPOVER_ACTIONS": {
"TITLE": "Options",
"DELETE_ALL": "Delete all messages"
}
},
"COMPOSE": {
"TITLE": "New message",
"TITLE_REPLY": "Reply",
"SUB_TITLE": "New message",
"TO": "To",
"OBJECT": "Object",
"OBJECT_HELP": "Object",
"ENCRYPTED_HELP": "Please note this message will be encrypted before sending so that only the recipient can read it and be sure you are the author.",
"MESSAGE": "Message",
"MESSAGE_HELP": "Message content",
"CONTENT_CONFIRMATION": "No message content.<br/><br/>Are your sure you want to send this message?"
},
"VIEW": {
"TITLE": "Message",
"SENDER": "Sent by",
"RECIPIENT": "Sent to",
"NO_CONTENT": "Empty message",
"DELETE": "Delete the message"
},
"CONFIRM": {
"REMOVE": "Are you sure you want to <b>delete this message</b>?<br/><br/> This operation is irreversible.",
"REMOVE_ALL": "Are you sure you want to <b>delete all messages</b>?<br/><br/> This operation is irreversible.",
"MARK_ALL_AS_READ": "Are you sure you want to <b>mark all message as read</b>?",
"USER_HAS_NO_PROFILE": "This identity has no Cesium + profile. It may not use the Cesium + extension, so it <b>will not read your message</b>.<br/><br/>Are you sure you want <b>to continue</b>?"
},
"INFO": {
"MESSAGE_REMOVED": "Message successfully deleted",
"All_MESSAGE_REMOVED": "Messages successfully deleted",
"MESSAGE_SENT": "Message sent"
},
"ERROR": {
"SEND_MSG_FAILED": "Error while sending message.",
"LOAD_MESSAGES_FAILED": "Error while loading messages.",
"LOAD_MESSAGE_FAILED": "Error while loading message.",
"MESSAGE_NOT_READABLE": "Unable to read message.",
"USER_NOT_RECIPIENT": "You are not the recipient of this message: unable to read it.",
"NOT_AUTHENTICATED_MESSAGE": "The authenticity of the message is not certain or its content is corrupted.",
"REMOVE_MESSAGE_FAILED": "Error while deleting message",
"MESSAGE_CONTENT_TOO_LONG": "Value too long ({{maxLength}} characters max).",
"MARK_AS_READ_FAILED": "Unable to mark the message as 'read'.",
"LOAD_NOTIFICATIONS_FAILED": "Error while loading messages notifications.",
"REMOVE_All_MESSAGES_FAILED": "Error while removing all messages.",
"MARK_ALL_AS_READ_FAILED": "Error while marking messages as read.",
"RECIPIENT_IS_MANDATORY": "Recipient is mandatory."
}
},
"REGISTRY": {
"CATEGORY": "Main activity",
"GENERAL_DIVIDER": "Basic information",
"LOCATION_DIVIDER": "Address",
"SOCIAL_NETWORKS_DIVIDER": "Social networks, web sites",
"TECHNICAL_DIVIDER": "Technical data",
"BTN_SHOW_WOT": "People",
"BTN_SHOW_WOT_HELP": "Search for people",
"BTN_SHOW_PAGES": "Pages",
"BTN_SHOW_PAGES_HELP": "Search for pages",
"BTN_NEW": "New page",
"MY_PAGES": "My pages",
"NO_PAGE": "No page",
"SEARCH": {
"TITLE": "Pages",
"SEARCH_HELP": "What, Who: hairdresser, Lili's restaurant, ...",
"BTN_ADD": "New",
"BTN_LAST_RECORDS": "Recent pages",
"BTN_ADVANCED_SEARCH": "Advanced search",
"BTN_OPTIONS": "Advanced search",
"TYPE": "Kind of organization",
"LOCATION_HELP": "Where: City, Country",
"RESULTS": "Results",
"RESULT_COUNT_LOCATION": "{{count}} result{{count>0?'s':''}}, near {{location}}",
"RESULT_COUNT": "{{count}} result{{count>0?'s':''}}",
"LAST_RECORDS": "Recent pages",
"LAST_RECORD_COUNT_LOCATION": "{{count}} recent page{{count>0?'s':''}}, near {{location}}",
"LAST_RECORD_COUNT": "{{count}} recent page{{count>0?'s':''}}",
"POPOVER_FILTERS": {
"BTN_ADVANCED_SEARCH": "Advanced options?"
}
},
"VIEW": {
"TITLE": "Registry",
"CATEGORY": "Main activity:",
"LOCATION": "Address:",
"MENU_TITLE": "Options",
"POPOVER_SHARE_TITLE": "{{title}}",
"REMOVE_CONFIRMATION" : "Are you sure you want to delete this reference?<br/><br/>This is irreversible."
},
"TYPE": {
"TITLE": "New page",
"SELECT_TYPE": "Kind of organization:",
"ENUM": {
"SHOP": "Local shops",
"COMPANY": "Company",
"ASSOCIATION": "Association",
"INSTITUTION": "Institution"
}
},
"EDIT": {
"TITLE": "Edit",
"TITLE_NEW": "New page",
"RECORD_TYPE":"Kind of organization",
"RECORD_TITLE": "Name",
"RECORD_TITLE_HELP": "Name",
"RECORD_DESCRIPTION": "Description",
"RECORD_DESCRIPTION_HELP": "Describe activity",
"RECORD_ADDRESS": "Street",
"RECORD_ADDRESS_HELP": "Street, building...",
"RECORD_CITY": "City",
"RECORD_CITY_HELP": "City, Country",
"RECORD_SOCIAL_NETWORKS": "Social networks and web site",
"RECORD_PUBKEY": "Public key",
"RECORD_PUBKEY_HELP": "Public key to receive payments"
},
"WALLET": {
"REGISTRY_DIVIDER": "Pages",
"REGISTRY_HELP": "Pages refer to activities accepting money or promoting it: local shops, companies, associations, institutions."
},
"ERROR": {
"LOAD_CATEGORY_FAILED": "Loading main activities failed",
"LOAD_RECORD_FAILED": "Loading failed",
"LOOKUP_RECORDS_FAILED": "Error while loading records.",
"REMOVE_RECORD_FAILED": "Deleting failed",
"SAVE_RECORD_FAILED": "Saving failed",
"RECORD_NOT_EXISTS": "Record not found",
"GEO_LOCATION_NOT_FOUND": "City or zip code not found"
},
"INFO": {
"RECORD_REMOVED" : "Page successfully deleted",
"RECORD_SAVED": "Page successfully saved"
}
},
"PROFILE": {
"PROFILE_DIVIDER": "ğchange profile",
"PROFILE_DIVIDER_HELP": "It is related data, stored in the ğchange network.",
"NO_PROFILE_DEFINED": "No ğchange profile",
"BTN_ADD": "Create my profile",
"BTN_EDIT": "Edit my profile",
"BTN_DELETE": "Delete my profile",
"BTN_REORDER": "Reorder",
"UID": "Pseudonym",
"TITLE": "Lastname, FirstName",
"TITLE_HELP": "Name",
"DESCRIPTION": "About me",
"DESCRIPTION_HELP": "About me...",
"SOCIAL_HELP": "http://...",
"GENERAL_DIVIDER": "General data",
"SOCIAL_NETWORKS_DIVIDER": "Social networks and web site",
"STAR": "Trust level",
"TECHNICAL_DIVIDER": "Technical data",
"MODAL_AVATAR": {
"TITLE": "Avatar",
"SELECT_FILE_HELP": "<b>Choose an image file</b>, by clicking on the button below:",
"BTN_SELECT_FILE": "Choose an image",
"RESIZE_HELP": "<b>Re-crop the image</b> if necessary. A click on the image allows to move it. Click on the area at the bottom left to zoom in.",
"RESULT_HELP": "<b>Here is the result</b> as seen on your profile:"
},
"CONFIRM": {
"DELETE": "Are you sure you want to <b>delete your Cesium+ profile ?</b><br/><br/>This operation is irreversible."
},
"ERROR": {
"REMOVE_PROFILE_FAILED": "Deleting profile failed",
"LOAD_PROFILE_FAILED": "Could not load user profile.",
"SAVE_PROFILE_FAILED": "Saving profile failed",
"INVALID_SOCIAL_NETWORK_FORMAT": "Invalid format: please fill a valid Internet address.<br/><br/>Examples :<ul><li>- A Facebook page (https://www.facebook.com/user)</li><li>- A web page (http://www.domain.com)</li><li>- An email address (joe@dalton.com)</li></ul>",
"IMAGE_RESIZE_FAILED": "Error while resizing picture"
},
"INFO": {
"PROFILE_REMOVED": "Profile deleted",
"PROFILE_SAVED": "Profile saved"
},
"HELP": {
"WARNING_PUBLIC_DATA": "Please note that the information published here <b>is public</b>: visible including by <b>not logged in people</b>."
}
},
"LOCATION": {
"BTN_GEOLOC_ADDRESS": "Find my address on the map",
"USE_GEO_POINT": "geo-locate (recommended)?",
"LOADING_LOCATION": "Searching address...",
"LOCATION_DIVIDER": "Localisation",
"ADDRESS": "Address",
"ADDRESS_HELP": "Address (optional)",
"CITY": "City",
"CITY_HELP": "City, Country",
"DISTANCE": "Maximum distance around the city",
"DISTANCE_UNIT": "mi",
"DISTANCE_OPTION": "{{value}} {{'LOCATION.DISTANCE_UNIT'|translate}}",
"SEARCH_HELP": "City, Country",
"PROFILE_POSITION": "Profile position",
"MODAL": {
"TITLE": "Search address",
"SEARCH_HELP": "City, Country",
"ALTERNATIVE_RESULT_DIVIDER": "Alternative results for <b>{{address}}</b>:",
"POSITION": "lat/lon : {{lat}} / {{lon}}"
},
"ERROR": {
"CITY_REQUIRED_IF_STREET": "Required if a street has been filled",
"REQUIRED_FOR_LOCATION": "Required field to appear on the map",
"INVALID_FOR_LOCATION": "Unknown address",
"GEO_LOCATION_FAILED": "Unable to retrieve your current position. Please use the search button.",
"ADDRESS_LOCATION_FAILED": "Unable to retrieve the address position"
}
},
"SUBSCRIPTION": {
"SUBSCRIPTION_DIVIDER": "Online services",
"SUBSCRIPTION_DIVIDER_HELP": "Online services offer optional additional services, delegated to a third party.",
"BTN_ADD": "Add a service",
"BTN_EDIT": "Manage my services",
"NO_SUBSCRIPTION": "No service defined",
"SUBSCRIPTION_COUNT": "Services / Subscription",
"EDIT": {
"TITLE": "Online services",
"HELP_TEXT": "Manage your subscriptions and other online services here",
"PROVIDER": "Provider:"
},
"TYPE": {
"ENUM": {
"EMAIL": "Receive email notifications"
}
},
"CONFIRM": {
"DELETE_SUBSCRIPTION": "Are you sur you want to <b>delete this subscription</b>?"
},
"ERROR": {
"LOAD_SUBSCRIPTIONS_FAILED": "Error while loading online services",
"ADD_SUBSCRIPTION_FAILED": "Error while adding subscription",
"UPDATE_SUBSCRIPTION_FAILED": "Error during subscription update",
"DELETE_SUBSCRIPTION_FAILED": "Error while deleting subscription"
},
"MODAL_EMAIL": {
"TITLE" : "Notification by email",
"HELP" : "Fill out this form to <b>be notified by email</ b> of your account's events. <br/>Your email address will be encrypted only to be visible to the service provider.",
"EMAIL_LABEL" : "Your email:",
"EMAIL_HELP": "john@domain.com",
"FREQUENCY_LABEL": "Frequency of notifications:",
"FREQUENCY_DAILY": "Daily",
"FREQUENCY_WEEKLY": "Weekly",
"PROVIDER": "Service Provider:"
}
},
"DOCUMENT": {
"HASH": "Hash: ",
"LOOKUP": {
"TITLE": "Document search",
"BTN_ACTIONS": "Actions",
"SEARCH_HELP": "issuer:AAA*, time:1508406169",
"LAST_DOCUMENTS_DOTS": "Last documents :",
"LAST_DOCUMENTS": "Last documents",
"SHOW_QUERY": "Show query",
"HIDE_QUERY": "Hide query",
"HEADER_TIME": "Time/Hour",
"HEADER_ISSUER": "Issuer",
"HEADER_RECIPIENT": "Recipient",
"READ": "Read",
"DOCUMENT_TYPE": "Type",
"DOCUMENT_TITLE": "Title",
"BTN_REMOVE": "Delete this document",
"BTN_COMPACT": "Compact",
"HAS_REGISTERED": "create or edit his profile",
"POPOVER_ACTIONS": {
"TITLE": "Actions",
"REMOVE_ALL": "Delete these documents..."
},
"TYPE": {
"USER_PROFILE": "Profile",
"MARKET_RECORD": "Ad",
"MARKET_COMMENT": "Comment on a ad",
"PAGE_RECORD": "Page",
"PAGE_COMMENT": "Comment on a page",
"GROUP_RECORD": "Group",
"GROUP_COMMENT": "Comment on a group"
}
},
"INFO": {
"REMOVED": "Deleted document"
},
"CONFIRM": {
"REMOVE": "Are you sure you want to <b>delete this document</b>?",
"REMOVE_ALL": "Are you sure you want to <b>delete these documents</b>?"
},
"ERROR": {
"LOAD_DOCUMENTS_FAILED": "Error searching documents",
"REMOVE_FAILED": "Error deleting the document",
"REMOVE_ALL_FAILED": "Error deleting documents"
}
},
"ES_SETTINGS": {
"PLUGIN_NAME": "Cesium+",
"PLUGIN_NAME_HELP": "User profiles, notifications, private messages",
"ENABLE_TOGGLE": "Enable extension?",
"ENABLE_MESSAGE_TOGGLE": "Enable messages?",
"ENABLE_SETTINGS_TOGGLE": "Enable remote storage for settings?",
"PEER": "Data peer address",
"POPUP_PEER": {
"TITLE" : "Data peer",
"HELP" : "Set the address of the peer to use:",
"PEER_HELP": "server.domain.com:port"
},
"NOTIFICATIONS": {
"DIVIDER": "Notifications",
"HELP_TEXT": "Enable the types of notifications you want to receive:",
"ENABLE_TX_SENT": "Notify the validation of <b>sent payments</b>?",
"ENABLE_TX_RECEIVED": "Notify the validation of <b>received payments</b>?",
"ENABLE_CERT_SENT": "Notify the validation of <b>sent certifications</b>?",
"ENABLE_CERT_RECEIVED": "Notify the validation of <b>received certifications</b>?"
},
"CONFIRM": {
"ASK_ENABLE_TITLE": "New features",
"ASK_ENABLE": "Some new features are available: <ul><li>&nbsp;&nbsp;<b><i class=\"icon ion-person\"></i> user profiles</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-android-notifications\"></i> Notifications</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-email\"></i> Private messages</b>.</ul><br/>They have been <b>disabled</b> in your settings.<br/><br/><b>Do you want to enable</b> these features?"
}
},
"ES_WALLET": {
"ERROR": {
"RECIPIENT_IS_MANDATORY": "A recipient is required for encryption."
}
},
"ES_PEER": {
"NAME": "Name",
"DOCUMENTS": "Documents",
"SOFTWARE": "Software",
"DOCUMENT_COUNT": "Number of documents",
"EMAIL_SUBSCRIPTION_COUNT": "{{emailSubscription}} subscribers to email notification"
},
"EVENT": {
"NODE_STARTED": "Your node ES API <b>{{params[0]}}</b> is UP",
"NODE_BMA_DOWN": "Node <b>{{params[0]}}:{{params[1]}}</b> (used by your ES API) is <b>unreachable</b>.",
"NODE_BMA_UP": "Node <b>{{params[0]}}:{{params[1]}}</b> is reachable again.",
"MEMBER_JOIN": "You are now a <b>member</b> of currency <b>{{params[0]}}</b>!",
"MEMBER_LEAVE": "You are <b>not a member anymore</b> of currency <b>{{params[0]}}</b>!",
"MEMBER_EXCLUDE": "You are <b>not more member</b> of the currency <b>{{params[0]}}</b>, for lack of renewal or lack of certifications.",
"MEMBER_REVOKE": "Your account has been revoked. It will no longer be a member of the currency <b>{{params[0]}}</b>.",
"MEMBER_ACTIVE": "Your membership to <b>{{params[0]}}</b> has been <b>renewed successfully</b>.",
"TX_SENT": "Your payment to <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> was executed.",
"TX_SENT_MULTI": "Your payment to <b>{{params[1]}}</b> was executed.",
"TX_RECEIVED": "You received a payment from <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"TX_RECEIVED_MULTI": "You received a payment from <b>{{params[1]}}</b>.",
"CERT_SENT": "Your <b>certification</b> to <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> was executed.",
"CERT_RECEIVED": "You have <b>received a certification</b> from <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"USER": {
"LIKE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> like your profile",
"FOLLOW_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> follows your activity",
"STAR_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> rated you ({{params[3]}} <i class=\"ion-star\">)",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> asks you for a moderation on the profile: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> reported a profile to be deleted: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has requested moderation on your profile"
},
"PAGE": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has commented on your referencing: <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his comment on your referencing: <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has replied to your comment on the referencing: <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his answer to your comment, on the referencing: <b>{{params[2]}}</b>",
"FOLLOW_NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has commented on the page: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his comment on the page: <b>{{params[2]}}</b>",
"FOLLOW_NEW": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> added a page: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> updated the page: <b>{{params[2]}}</b>",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> asks you for a moderation on the page: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> reported a page to be deleted: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has requested moderation on your page: <b>{{params[2]}}</b>"
}
},
"CONFIRM": {
"ES_USE_FALLBACK_NODE": "Data node <b>{{old}}</b> unreachable or invalid address.<br/><br/>Do you want to temporarily use the <b>{{new}}</b> data node?"
},
"ERROR": {
"ES_CONNECTION_ERROR": "Data node <b>{{server}}</b> unreachable or invalid address.<br/><br/>Check your Internet connection, or change data node in <a class=\"positive\" ng-click=\"doQuickFix('settings')\">advanced settings</a>.",
"ES_MAX_UPLOAD_BODY_SIZE": "The volume of data to be sent exceeds the limit set by the server.<br/><br/>Please try again after, for example, deleting photos."
}
}
);
$translateProvider.translations("eo-EO", {
"COMMON": {
"CATEGORY": "Kategorio",
"CATEGORY_SELECT_HELP": "Elekti",
"CATEGORIES": "Kategorioj",
"CATEGORY_SEARCH_HELP": "Serĉado",
"COMMENT_HELP": "Komento",
"LAST_MODIFICATION_DATE": "Ĝisdatigita la",
"BTN_LIKE": "Mi ŝatas",
"BTN_FOLLOW": "Sekvi",
"BTN_STOP_FOLLOW": "Ne plu sekvi",
"LIKES_TEXT": "{{total}} persono{{total > 1 ? 'j' : ''}} ŝatis tiun ĉi paĝon",
"DISLIKES_TEXT": "{{total}} persono{{total > 1 ? 'j' : ''}} ne ŝatis tiun ĉi paĝon",
"VIEWS_TEXT": "{{total}} persono{{total > 1 ? 'j' : ''}} konsultis tiun ĉi paĝon",
"FOLLOWS_TEXT": "{{total}} persono{{total > 1 ? 'j' : ''}} sekvas tiun ĉi paĝon",
"ABUSES_TEXT": "{{total}} persono{{total > 1 ? 'j' : ''}} atentigis pri problemo",
"BTN_REPORT_ABUSE_DOTS": "Atentigi pri problemo aŭ misuzo...",
"BTN_REMOVE_REPORTED_ABUSE": "Nuligi mian atentigon",
"SUBMIT_BY": "Submetita de",
"GEO_DISTANCE_SEARCH": "Distanco por serĉado",
"GEO_DISTANCE_OPTION": "{{value}} km",
"BTN_PUBLISH": "Publikigi",
"BTN_PICTURE_DELETE": "Forigi",
"BTN_PICTURE_FAVORISE": "Precipa",
"BTN_PICTURE_ROTATE": "Turni",
"BTN_ADD_PICTURE": "Aldoni foton",
"NOTIFICATION": {
"TITLE": "Nova avizo | {{'COMMON.APP_NAME'|translate}}",
"HAS_UNREAD": "Vi havas {{count}} avizo{{count>0?'j':''}}n ne legita{{count>0?'j':''}}n"
},
"NOTIFICATIONS": {
"TITLE": "Avizoj",
"MARK_ALL_AS_READ": "Ĉion marki legita",
"NO_RESULT": "Neniu avizo",
"SHOW_ALL": "Vidi ĉion",
"LOAD_NOTIFICATIONS_FAILED": "Malsukceso por ŝarĝi la avizojn"
},
"REPORT_ABUSE": {
"TITLE": "Atentigi pri problemo",
"SUB_TITLE": "Bonvolu klarigi rapide la problemon:",
"REASON_HELP": "Mi klarigas la problemon...",
"ASK_DELETE": "Peti la forigon?",
"CONFIRM": {
"SENT": "Atentigo sendita. Dankon!"
}
}
},
"MENU": {
"REGISTRY": "Paĝoj",
"USER_PROFILE": "Mia profilo",
"MESSAGES": "Mesaĝoj",
"NOTIFICATIONS": "Avizoj",
"INVITATIONS": "Invitoj"
},
"ACCOUNT": {
"NEW": {
"ORGANIZATION_ACCOUNT": "Konto por organizaĵo",
"ORGANIZATION_ACCOUNT_HELP": "Se vi reprezentas entreprenon, asocion, ktp.<br/>Neniu universala dividendo estos kreita per tiu ĉi konto."
},
"EVENT": {
"MEMBER_WITHOUT_PROFILE": "Vi povas <a ui-sref=\"app.edit_profile\">tajpi vian profilon Cesium+</a> (kromebleco) por disponi pli bonan videblecon por via konto."
},
"ERROR": {
"WS_CONNECTION_FAILED": "ğchange ne povas ricevi la avizojn pro teknika eraro (konekto al la daten-nodo Cesium+).<br/><br/>Se la problemo daŭradas, bonvolu <b>elekti alian daten-nodon</b> ĉe la parametroj Cesium+."
}
},
"WOT": {
"BTN_SUGGEST_CERTIFICATIONS_DOTS": "Sugesti identecojn atestotajn...",
"BTN_ASK_CERTIFICATIONS_DOTS": "Peti membrojn atesti min...",
"BTN_ASK_CERTIFICATION": "Peti atestaĵon",
"SUGGEST_CERTIFICATIONS_MODAL": {
"TITLE": "Sugesti atestadojn",
"HELP": "Selekti viajn sugestojn"
},
"ASK_CERTIFICATIONS_MODAL": {
"TITLE": "Peti atestaĵojn",
"HELP": "Selekti la ricevontojn"
},
"SEARCH": {
"DIVIDER_PROFILE": "Kontoj",
"DIVIDER_PAGE": "Paĝoj",
"DIVIDER_GROUP": "Grupoj"
},
"VIEW": {
"STARS": "Fido-nivelo",
"STAR_HIT_COUNT": "{{total}} noto{{total>1 ? 'j' : ''}}",
"BTN_STAR_HELP": "Noti tiun ĉi profilon",
"BTN_STARS_REMOVE": "Forigi mian noton",
"BTN_REDO_STAR_HELP": "Aktualigi vian noton",
"BTN_FOLLOW": "Sekvi la agojn de tiu ĉi profilo",
"BTN_STOP_FOLLOW": "Ne plu sekvi tiun ĉi profilon"
}
},
"COMMENTS": {
"DIVIDER": "Komentoj",
"SHOW_MORE_COMMENTS": "Afiŝi la antaŭajn komentojn",
"COMMENT_HELP": "Via komento, demando, ktp.",
"COMMENT_HELP_REPLY_TO": "Via respondo...",
"BTN_SEND": "Sendi",
"POPOVER_SHARE_TITLE": "Mesaĝo #{{number}}",
"REPLY": "Respondi",
"REPLY_TO": "Respondo al:",
"REPLY_TO_LINK": "Responde al ",
"REPLY_TO_DELETED_COMMENT": "Responde al forigita komento",
"REPLY_COUNT": "{{replyCount}} respondoj",
"DELETED_COMMENT": "Komento forigita",
"MODIFIED_ON": "modifita la {{time|formatDate}}",
"MODIFIED_PARENTHESIS": "(modifita poste)",
"ERROR": {
"FAILED_SAVE_COMMENT": "Eraro dum la konservo de la komento",
"FAILED_REMOVE_COMMENT": "Eraro dum la forigo de la komento"
}
},
"MESSAGE": {
"REPLY_TITLE_PREFIX": "Resp: ",
"FORWARD_TITLE_PREFIX": "Tr: ",
"BTN_REPLY": "Respondi",
"BTN_COMPOSE": "Nova mesaĝo",
"BTN_WRITE": "Skribi",
"NO_MESSAGE_INBOX": "Neniu mesaĝo ricevita",
"NO_MESSAGE_OUTBOX": "Neniu mesaĝo sendita",
"NOTIFICATIONS": {
"TITLE": "Mesaĝoj",
"MESSAGE_RECEIVED": "Vi <b>ricevis mesaĝon</b><br/>de"
},
"LIST": {
"INBOX": "Ricevujo",
"OUTBOX": "Senditaj mesaĝoj",
"LAST_INBOX": "Novaj mesaĝoj",
"LAST_OUTBOX": "Senditaj mesaĝoj",
"BTN_LAST_MESSAGES": "Freŝdataj mesaĝoj",
"TITLE": "Mesaĝoj",
"SEARCH_HELP": "Serĉado en la mesaĝoj",
"POPOVER_ACTIONS": {
"TITLE": "Kromaĵoj",
"DELETE_ALL": "Forigi ĉiujn mesaĝojn"
}
},
"COMPOSE": {
"TITLE": "Nova mesaĝo",
"TITLE_REPLY": "Respondi",
"SUB_TITLE": "Nova mesaĝo",
"TO": "Al",
"OBJECT": "Temo",
"OBJECT_HELP": "Temo",
"ENCRYPTED_HELP": "Bonvolu noti, ke tiu ĉi mesaĝo estos ĉifrita antaŭ sendo, tiel ke nur la adresato povos legi ĝin, kaj ke li estos certa, ke vi ja estas ties aŭtoro.",
"MESSAGE": "Mesaĝo",
"MESSAGE_HELP": "Enhavo de la mesaĝo",
"CONTENT_CONFIRMATION": "La enhavo de la mesaĝo estas malplena.<br/><br/>Ĉu vi volas tamen sendi la mesaĝon?"
},
"VIEW": {
"TITLE": "Mesaĝo",
"SENDER": "Sendita de",
"RECIPIENT": "Sendita al",
"NO_CONTENT": "Mesaĝo malplena",
"DELETE": "Forigi la mesaĝon"
},
"CONFIRM": {
"REMOVE": "Ĉu vi certas, ke vi volas <b>forigi tiun ĉi mesaĝon</b>?<br/><br/>Tiu ago estas neinversigebla.",
"REMOVE_ALL" : "Ĉu vi certas, ke vi volas <b>forigi ĉiujn mesaĝojn</b>?<br/><br/>Tiu ago estas neinversigebla.",
"MARK_ALL_AS_READ": "Ĉu vi certas, ke vi volas <b>marki ĉiujn mesaĝojn legitaj</b>?",
"USER_HAS_NO_PROFILE": "Tiu identeco havas neniun profilon Cesium+. Eblas ke ĝi ne uzas la krom-programon Cesium+, kaj <b>do ne legos vian mesaĝon</b>.<br/><br/>Ĉu vi certas, ke vi volas tamen <b>daŭrigi</b>?"
},
"INFO": {
"MESSAGE_REMOVED": "Mesaĝo forigita",
"All_MESSAGE_REMOVED": "Ĉiuj mesaĝoj estis forigitaj",
"MESSAGE_SENT": "Mesaĝo sendita"
},
"ERROR": {
"SEND_MSG_FAILED": "Eraro dum la sendo de la mesaĝo.",
"LOAD_MESSAGES_FAILED": "Eraro dum la ricevo de la mesaĝoj.",
"LOAD_MESSAGE_FAILED": "Eraro dum la ricevo de la mesaĝo.",
"MESSAGE_NOT_READABLE": "Legado de la mesaĝo neebla.",
"USER_NOT_RECIPIENT": "Vi ne estas la adresato de tiu ĉi mesaĝo: malĉifrado neebla.",
"NOT_AUTHENTICATED_MESSAGE": "La aŭtenteco de la mesaĝo estas dubinda aŭ ties enhavo estas difektita.",
"REMOVE_MESSAGE_FAILED": "Malsukceso por forigi la mesaĝon",
"MESSAGE_CONTENT_TOO_LONG": "Signaro tro longa ({{maxLength}} signoj maksimume).",
"MARK_AS_READ_FAILED": "Neeblas marki la mesaĝon 'legita'.",
"LOAD_NOTIFICATIONS_FAILED": "Eraro dum la ricevo de la mesaĝo-avizoj.",
"REMOVE_All_MESSAGES_FAILED": "Eraro dum la forigo de ĉiuj mesaĝoj.",
"MARK_ALL_AS_READ_FAILED": "Eraro por marki la mesaĝojn legitaj.",
"RECIPIENT_IS_MANDATORY": "La adresato estas deviga."
}
},
"REGISTRY": {
"CATEGORY": "Ĉefa agado",
"GENERAL_DIVIDER": "Ĝeneralaj informoj",
"LOCATION_DIVIDER": "Adreso",
"SOCIAL_NETWORKS_DIVIDER": "Sociaj retoj kaj retejo",
"TECHNICAL_DIVIDER": "Teknikaj informoj",
"BTN_SHOW_WOT": "Personoj",
"BTN_SHOW_WOT_HELP": "Traserĉi personojn",
"BTN_SHOW_PAGES": "Paĝoj",
"BTN_SHOW_PAGES_HELP": "Traserĉi paĝojn",
"BTN_NEW": "Krei paĝon",
"MY_PAGES": "Miaj paĝoj",
"NO_PAGE": "Neniu paĝo",
"SEARCH": {
"TITLE": "Paĝoj",
"SEARCH_HELP": "Kio, Kiu: restoracio, Ĉe Marcelo, ...",
"BTN_ADD": "Nova",
"BTN_LAST_RECORDS": "Freŝdataj paĝoj",
"BTN_ADVANCED_SEARCH": "Sperta serĉado",
"BTN_OPTIONS": "Sperta serĉado",
"TYPE": "Tipo de paĝo",
"LOCATION_HELP": "Kie: Poŝto-kodo, Urbo",
"RESULTS": "Rezultoj",
"RESULT_COUNT_LOCATION": "{{count}} rezulto{{count>0?'j':''}}, proksime de {{location}}",
"RESULT_COUNT": "{{count}} rezulto{{count>0?'j':''}}",
"LAST_RECORDS": "Freŝdataj paĝoj",
"LAST_RECORD_COUNT_LOCATION": "{{count}} paĝo{{count>0?'j':''}} freŝdata{{count>0?'j':''}}, proksime de {{location}}",
"LAST_RECORD_COUNT": "{{count}} paĝo{{count>0?'j':''}} freŝdata{{count>0?'j':''}}",
"POPOVER_FILTERS": {
"BTN_ADVANCED_SEARCH": "Spertaj kromaĵoj?"
}
},
"VIEW": {
"TITLE": "Adresaro",
"CATEGORY": "Ĉefa agado:",
"LOCATION": "Adreso:",
"MENU_TITLE": "Kromaĵoj",
"POPOVER_SHARE_TITLE": "{{title}}",
"REMOVE_CONFIRMATION" : "Ĉu vi certas, ke vi volas forigi tiun ĉi paĝon?<br/><br/>Tiu ago estas neinversigebla."
},
"TYPE": {
"TITLE": "Tipoj",
"SELECT_TYPE": "Tipo de paĝo:",
"ENUM": {
"SHOP": "Loka komerco",
"COMPANY": "Entrepreno",
"ASSOCIATION": "Asocio",
"INSTITUTION": "Institucio"
}
},
"EDIT": {
"TITLE": "Redaktado",
"TITLE_NEW": "Nova paĝo",
"RECORD_TYPE":"Tipo de paĝo",
"RECORD_TITLE": "Nomo",
"RECORD_TITLE_HELP": "Nomo",
"RECORD_DESCRIPTION": "Priskribo",
"RECORD_DESCRIPTION_HELP": "Priskribo de la agado",
"RECORD_ADDRESS": "Strato",
"RECORD_ADDRESS_HELP": "Strato, konstruaĵo...",
"RECORD_CITY": "Urbo",
"RECORD_CITY_HELP": "Urbo",
"RECORD_SOCIAL_NETWORKS": "Sociaj retoj kaj retejo",
"RECORD_PUBKEY": "Publika ŝlosilo",
"RECORD_PUBKEY_HELP": "Publika ŝlosilo por ricevi la pagojn"
},
"WALLET": {
"REGISTRY_DIVIDER": "Paĝoj",
"REGISTRY_HELP": "La paĝoj referencigas agadojn akceptantajn la monon aŭ favorigantajn ĝin: komercoj, entreprenoj, asocioj, institucioj."
},
"ERROR": {
"LOAD_CATEGORY_FAILED": "Eraro dum la ŝarĝo de la listo de la agadoj",
"LOAD_RECORD_FAILED": "Eraro dum la ŝarĝo de la paĝo",
"LOOKUP_RECORDS_FAILED": "Eraro dum la serĉado",
"REMOVE_RECORD_FAILED": "Eraro dum la forigo de la paĝo",
"SAVE_RECORD_FAILED": "Eraro dum la konservado",
"RECORD_NOT_EXISTS": "Paĝo neekzistanta",
"GEO_LOCATION_NOT_FOUND": "Urbo aŭ poŝto-kodo ne trovita"
},
"INFO": {
"RECORD_REMOVED" : "Paĝo forigita",
"RECORD_SAVED": "Paĝo konservita"
}
},
"PROFILE": {
"PROFILE_DIVIDER": "Profilo ğchange",
"PROFILE_DIVIDER_HELP": "Temas pri kromaj datenoj, stokitaj ĉe la reto ğchange.",
"NO_PROFILE_DEFINED": "Neniu profilo tajpita",
"BTN_ADD": "Tajpi mian profilon",
"BTN_EDIT": "Redakti mian profilon",
"BTN_DELETE": "Forigi mian profilon",
"BTN_REORDER": "Reordigi",
"UID": "Pseŭdonimo",
"TITLE": "Familia nomo, Persona nomo",
"TITLE_HELP": "Familia nomo, Persona nomo",
"DESCRIPTION": "Pri mi",
"DESCRIPTION_HELP": "Pri mi...",
"SOCIAL_HELP": "http://...",
"GENERAL_DIVIDER": "Ĝeneralaj informoj",
"SOCIAL_NETWORKS_DIVIDER": "Sociaj retoj, retejoj",
"STAR": "Fido-nivelo",
"TECHNICAL_DIVIDER": "Teknikaj informoj",
"MODAL_AVATAR": {
"TITLE": "Profil-foto",
"SELECT_FILE_HELP": "Bonvolu <b>elekti bildo-dosieron</b>, alklakante la ĉi-suban butonon:",
"BTN_SELECT_FILE": "Elekti foton",
"RESIZE_HELP": "<b>Rekadri la bildon</b>, laŭbezone. Pluigi klakon sur la bildo ebligas movi ĝin. Alklaku la zonon malsupre maldekstre por zomi.",
"RESULT_HELP": "<b>Jen la rezulto</b> tiel videbla ĉe via profilo:"
},
"CONFIRM": {
"DELETE": "Ĉu vi certas, ke vi volas <b>forigi vian profilon ğchange?</b><br/><br/>Tiu ago estas neinversigebla."
},
"ERROR": {
"REMOVE_PROFILE_FAILED": "Malsukceso por forigi la profilon",
"LOAD_PROFILE_FAILED": "Malsukceso por ŝarĝi la profilon de la uzanto.",
"SAVE_PROFILE_FAILED": "Eraro dum la konservado",
"INVALID_SOCIAL_NETWORK_FORMAT": "Strukturo ne rekonata: bonvolu tajpi validan adreson.<br/><br/>Ezemploj:<ul><li>- Facebook-paĝo (https://www.facebook.com/uzanto)</li><li>- Retpaĝo (http://www.miaretejo.net)</li><li>- Retadreso (joe@dalton.com)</li></ul>",
"IMAGE_RESIZE_FAILED": "Eraro dum la reformatigo de la bildo"
},
"INFO": {
"PROFILE_REMOVED": "Profilo forigita",
"PROFILE_SAVED": "Profilo konservita"
},
"HELP": {
"WARNING_PUBLIC_DATA": "La informoj afiŝitaj en via profilo <b>estas publikaj</b>: videblaj inkluzive de la personoj <b>ne konektitaj</b>.<br/>{{'PROFILE.PROFILE_DIVIDER_HELP'|translate}}"
}
},
"LOCATION": {
"BTN_GEOLOC_ADDRESS": "Trovi mian adreson surmape",
"USE_GEO_POINT": "Aperi sur la mapoj {{'COMMON.APP_NAME'|translate}}?",
"LOADING_LOCATION": "Serĉado de la adreso...",
"LOCATION_DIVIDER": "Adreso",
"ADDRESS": "Strato",
"ADDRESS_HELP": "Strato, adres-aldonaĵo...",
"CITY": "Urbo",
"CITY_HELP": "Poŝto-kodo, Urbo, Lando",
"DISTANCE": "Maksimuma distanco ĉirkaŭ la urbo",
"DISTANCE_UNIT": "km",
"DISTANCE_OPTION": "{{value}} {{'LOCATION.DISTANCE_UNIT'|translate}}",
"SEARCH_HELP": "Poŝto-kodo, Urbo",
"PROFILE_POSITION": "Loko de la profilo",
"MODAL": {
"TITLE": "Serĉado de la adreso",
"SEARCH_HELP": "Urbo, Poŝto-kodo, Lando",
"ALTERNATIVE_RESULT_DIVIDER": "Alternativaj rezultoj por <b>{{address}}</b>:",
"POSITION": "Lat/Lon: {{lat}}/{{lon}}"
},
"ERROR": {
"CITY_REQUIRED_IF_STREET": "Deviga kampo (ĉar strato estas tajpita)",
"REQUIRED_FOR_LOCATION": "Deviga kampo por aperi sur la mapo",
"INVALID_FOR_LOCATION": "Adreso nekonata",
"GEO_LOCATION_FAILED": "Neeblas ricevi vian lokiĝon. Bonvolu uzi la serĉo-butonon.",
"ADDRESS_LOCATION_FAILED": "Neeblas ricevi la lokon per la adreso"
}
},
"SUBSCRIPTION": {
"SUBSCRIPTION_DIVIDER": "Retaj servoj",
"SUBSCRIPTION_DIVIDER_HELP": "La retaj servoj proponas pliajn nedevigajn servojn, delegitajn al aliulo.",
"BTN_ADD": "Aldoni servon",
"BTN_EDIT": "Mastrumi miajn servojn",
"NO_SUBSCRIPTION": "Neniu servo uzata",
"SUBSCRIPTION_COUNT": "Servoj / Abonoj",
"EDIT": {
"TITLE": "Retaj servoj",
"HELP_TEXT": "Mastrumu ĉi tie viajn abonojn kaj aliajn retajn servojn",
"PROVIDER": "Provizanto:"
},
"TYPE": {
"ENUM": {
"EMAIL": "Ricevi la avizojn per retmesaĝo"
}
},
"CONFIRM": {
"DELETE_SUBSCRIPTION": "Ĉu vi certas, ke vi volas <b>forigi tiun abonon</b>?"
},
"ERROR": {
"LOAD_SUBSCRIPTIONS_FAILED": "Eraro dum la ŝarĝo de la retaj servoj",
"ADD_SUBSCRIPTION_FAILED": "Malsukceso por sendi la abonon",
"UPDATE_SUBSCRIPTION_FAILED": "Malsukceso por ĝisdatigi la abonon",
"DELETE_SUBSCRIPTION_FAILED": "Eraro dum la forigo de la abono"
},
"MODAL_EMAIL": {
"TITLE" : "Avizo per retmesaĝo",
"HELP" : "Plenigu tiun ĉi formularon por <b>esti avizita per retmesaĝo</b> pri la okazaĵoj ĉe via konto.<br/>Via retadreso estos ĉifrita por esti videbla nur de la servo-provizanto.",
"EMAIL_LABEL" : "Via retadreso:",
"EMAIL_HELP": "johano.stelaro@esperanto.org",
"FREQUENCY_LABEL": "Periodo de la avizoj:",
"FREQUENCY_DAILY": "Ĉiutaga",
"FREQUENCY_WEEKLY": "Ĉiusemajna",
"PROVIDER": "Servo-provizanto:"
}
},
"DOCUMENT": {
"HASH": "Haketo: ",
"LOOKUP": {
"TITLE": "Serĉado de dokumentoj",
"BTN_ACTIONS": "Agoj",
"SEARCH_HELP": "Sendanto:AAA*, tempo:1508406169",
"LAST_DOCUMENTS": "Lastaj dokumentoj",
"LAST_DOCUMENTS_DOTS": "Lastaj dokumentoj:",
"SHOW_QUERY": "Vidi la informpeton",
"HIDE_QUERY": "Kaŝi la informpeton",
"HEADER_TIME": "Dato/Horo",
"HEADER_ISSUER": "Sendanto",
"HEADER_RECIPIENT": "Ricevonto",
"READ": "Legita",
"DOCUMENT_TYPE": "Tipo",
"DOCUMENT_TITLE": "Titolo",
"BTN_REMOVE": "Forigi tiun ĉi dokumenton",
"BTN_COMPACT": "Densigi",
"HAS_REGISTERED": "kreis aŭ modifis sian profilon",
"POPOVER_ACTIONS": {
"TITLE": "Agoj",
"REMOVE_ALL": "Forigi tiujn ĉi dokumentojn..."
},
"TYPE": {
"USER_PROFILE": "Profilo",
"MARKET_RECORD": "Anonco",
"MARKET_COMMENT": "Komento ĉe anonco",
"PAGE_RECORD": "Paĝo",
"PAGE_COMMENT": "Komento ĉe paĝo",
"GROUP_RECORD": "Grupo",
"GROUP_COMMENT": "Komento ĉe grupo"
}
},
"INFO": {
"REMOVED": "Dokumento forigita"
},
"CONFIRM": {
"REMOVE": "Ĉu vi certas, ke vi volas <b>forigi tiun ĉi dokumenton</b>?",
"REMOVE_ALL": "Ĉu vi certas, ke vi volas <b>forigi tiujn ĉi dokumentojn</b>?"
},
"ERROR": {
"LOAD_DOCUMENTS_FAILED": "Eraro dum la serĉado de dokumentoj",
"REMOVE_FAILED": "Eraro dum la forigo de la dokumento",
"REMOVE_ALL_FAILED": "Eraro dum la forigo de la dokumentoj"
}
},
"ES_SETTINGS": {
"PLUGIN_NAME": "Spertaj parametroj",
"PLUGIN_NAME_HELP": "Filtrado de avizoj, ktp.",
"ENABLE_TOGGLE": "Aktivigi la krom-programon?",
"ENABLE_MESSAGE_TOGGLE": "Aktivigi la privatajn mesaĝojn?",
"ENABLE_SETTINGS_TOGGLE": "Aktivigi la foran stokadon de la parametroj?",
"PEER": "Adreso de la daten-nodo",
"POPUP_PEER": {
"TITLE" : "Daten-nodo",
"HELP" : "Tajpu la adreson de la nodo, kiun vi volas uzi:",
"PEER_HELP": "servo.domajno.com:port"
},
"NOTIFICATIONS": {
"DIVIDER": "Avizoj",
"HELP_TEXT": "Aktivigu la avizo-tipojn, kiujn vi deziras ricevi:",
"ENABLE_TX_SENT": "Avizi pri la <b>senditaj pagoj</b>?",
"ENABLE_TX_RECEIVED": "Avizi pri la <b>ricevitaj pagoj</b>?",
"ENABLE_CERT_SENT": "Avizi pri la <b>senditaj atestaĵoj</b>?",
"ENABLE_CERT_RECEIVED": "Avizi pri <b>la ricevitaj atestaĵoj</b>?",
},
"CONFIRM": {
"ASK_ENABLE_TITLE": "Kromaj funkcioj",
"ASK_ENABLE": "La krom-programo Cesium+ estas <b>malaktivigita</b> ĉe viaj parametroj, kio senaktivigas la funkciojn: <ul><li>&nbsp;&nbsp;<b><i class=\"icon ion-person\"></i> Profiloj Cesium+</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-android-notifications\"></i> Avizoj</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-email\"></i> Privataj mesaĝoj</b>.<li>&nbsp;&nbsp;<b><i class=\"icon ion-location\"></i> Mapoj, ktp.</b>.</ul><br/><b>Ĉu vi deziras reaktivigi</b> la krom-programon?"
}
},
"ES_WALLET": {
"ERROR": {
"RECIPIENT_IS_MANDATORY": "Adresito estas deviga por la ĉifrado."
}
},
"ES_PEER": {
"NAME": "Nomo",
"DOCUMENTS": "Dokumentoj",
"SOFTWARE": "Programo",
"DOCUMENT_COUNT": "Nombro de dokumentoj",
"EMAIL_SUBSCRIPTION_COUNT": "{{emailSubscription}} abonantoj pri avizoj per retmesaĝoj"
},
"EVENT": {
"NODE_STARTED": "Via nodo ES API <b>{{params[0]}}</b> ekis",
"NODE_BMA_DOWN": "La nodo <b>{{params[0]}}:{{params[1]}}</b> (uzata de via nodo ES API) estas <b>neatingebla</b>.",
"NODE_BMA_UP": "La nodo <b>{{params[0]}}:{{params[1]}}</b> estas denove alirebla.",
"MEMBER_JOIN": "Vi estas nun <b>membro</b> de la mono <b>{{params[0]}}</b>!",
"MEMBER_LEAVE": "Vi <b>ne plu estas membro</b> de la mono <b>{{params[0]}}</b>!",
"MEMBER_EXCLUDE": "Vi <b>ne plu estas membro</b> de la mono <b>{{params[0]}}</b>, pro ne revalidiĝo aŭ pro manko da atestaĵoj.",
"MEMBER_REVOKE": "La nuligo de via konto efektiviĝis. Ĝi ne plu povos esti membro-konto de la mono <b>{{params[0]}}</b>.",
"MEMBER_ACTIVE": "La revalidiĝo de via aliĝo al la mono <b>{{params[0]}}</b> estis <b>ricevita</b>.",
"TX_SENT": "Via <b>pago</b> al <span ng-class=\"{'gray': !notification.uid, 'positive':notification.uid}\" ><i class=\"icon\" ng-class=\"{'ion-person': notification.uid, 'ion-key': !notification.uid}\"></i>&thinsp;{{name||uid||params[1]}}</span> efektiviĝis.",
"TX_SENT_MULTI": "Via <b>pago</b> al <b>{{params[1]}}</b> efektiviĝis.",
"TX_RECEIVED": "Vi <b>ricevis pagon</b> de <span ng-class=\"{'gray': !notification.uid, 'positive':notification.uid}\"><i class=\"icon\" ng-class=\"{'ion-person': notification.uid, 'ion-key': !notification.uid}\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"TX_RECEIVED_MULTI": "Vi <b>ricevis pagon</b> de <b>{{params[1]}}</b>.",
"CERT_SENT": "Via <b>atestado</b> al <span ng-class=\"{'gray': !notification.uid, 'positive':notification.uid}\" ><i class=\"icon\" ng-class=\"{'ion-person': notification.uid, 'ion-key': !notification.uid}\"></i>&thinsp;{{name||uid||params[1]}}</span> efektiviĝis.",
"CERT_RECEIVED": "Vi <b>ricevis atestaĵon</b> de <span ng-class=\"{'gray': !notification.uid, 'positive':notification.uid}\"><i class=\"icon\" ng-class=\"{'ion-person': notification.uid, 'ion-key': !notification.uid}\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"USER": {
"LIKE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> ŝatas vian profilon",
"FOLLOW_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> sekvas viajn agojn",
"STAR_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> notis vin ({{params[3]}} <b class=\"ion-star\">)",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> petas de vi moderigon pri la profilo: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> atentigis pri profilo foriginda: <b>{{params[2]}}</b>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> atentigis pri via profilo"
},
"PAGE": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> komentis vian paĝon: <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> modifis sian komenton ĉe via paĝo: <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> respondis al via komento ĉe la paĝo: <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> modifis sian respondon al via komento ĉe la paĝo: <b>{{params[2]}}</b>",
"FOLLOW_NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> komentis la paĝon: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> modifis sian komenton ĉe la paĝo: <b>{{params[2]}}</b>",
"FOLLOW_NEW": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> aldonis la paĝon: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> modifis la paĝon: <b>{{params[2]}}</b>",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> petas de vis moderigon pri la paĝo: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> atentigis pri paĝo foriginda: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> atentigis pri via paĝo: <b>{{params[2]}}</b>"
}
},
"CONFIRM": {
"ES_USE_FALLBACK_NODE": "Daten-nodo <b>{{old}}</b> neatingebla aŭ adreso nevalida.<br/><br/>Ĉu vi volas provizore uzi la daten-nodon <b>{{new}}</b> ?"
},
"ERROR": {
"ES_CONNECTION_ERROR": "Daten-nodo <b>{{server}}</b> neatingebla aŭ adreso nevalida.<br/><br/>Kontrolu vian retkonekton, aŭ ŝanĝu la daten-nodon en la <a class=\"positive\" ng-click=\"doQuickFix('settings')\">spertaj parametroj</a>.",
"ES_MAX_UPLOAD_BODY_SIZE": "La kvanto de datenoj sendotaj superas la limon fiksitan de la servilo.<br/>Bonvolu reprovi post, ekzemple, forigo de fotoj."
}
}
);
$translateProvider.translations("es-ES", {
"COMMON": {
"CATEGORY": "Categoría",
"CATEGORY_SELECT_HELP": "Seleccionar",
"CATEGORIES": "Categorías",
"CATEGORY_SEARCH_HELP": "Búsqueda",
"LAST_MODIFICATION_DATE": "Actualización el",
"SUBMIT_BY": "Sometido por",
"BTN_PUBLISH": "Publicar",
"BTN_PICTURE_DELETE": "Suprimir",
"BTN_PICTURE_FAVORISE": "Principal",
"BTN_PICTURE_ROTATE": "Girar",
"BTN_ADD_PICTURE": "Añadir una foto",
"NOTIFICATIONS": {
"TITLE": "Notificaciónes",
"MARK_ALL_AS_READ": "Marcar todo como leído",
"NO_RESULT": "Ningúna notificación",
"SHOW_ALL": "Ver todo",
"LOAD_NOTIFICATIONS_FAILED": "Fracaso en la carga de las notificaciónes"
}
},
"MENU": {
"REGISTRY": "Profesionales",
"USER_PROFILE": "Mi perfil",
"MESSAGES": "Mensajes",
"NOTIFICATIONS": "Notificaciónes",
"INVITATIONS": "Invitaciónes"
},
"ACCOUNT": {
"NEW": {
"ORGANIZATION_ACCOUNT": "Cuenta para una organización",
"ORGANIZATION_ACCOUNT_HELP": "Si representa una empresa, una asociación, etc.<br/>Ningún dividendo universal será creído por esta cuenta."
},
"EVENT": {
"MEMBER_WITHOUT_PROFILE": "Para obtener sus certificaciónes más rapidamente, completa <a ui-sref=\"app.user_edit_profile\">su perfil usuario</a>. Los miembros concederán más fácilmente su confianza a una identidad verificable."
},
"ERROR": {
"WS_CONNECTION_FAILED": "Cesium no puede recibir las notificaciónes, a causa de un error técnico (conexión al nodo de datos Cesium+).<br/><br/>Si el problema persiste, por favor <b>elige un otro nodo de datos</b> en las configuraciónes Cesium+."
}
},
"WOT": {
"BTN_SUGGEST_CERTIFICATIONS_DOTS": "Sugerir identidad a certificar...",
"BTN_ASK_CERTIFICATIONS_DOTS": "Solicitar otros miembros a certificarme…",
"BTN_ASK_CERTIFICATION": "Solicitar una certificación",
"SUGGEST_CERTIFICATIONS_MODAL": {
"TITLE": "Sugerir certificaciónes",
"HELP": "Selectionar sus sugerencias"
},
"ASK_CERTIFICATIONS_MODAL": {
"TITLE": "Solicitar certificaciónes",
"HELP": "Selectionar los destinatarios"
},
"SEARCH": {
"DIVIDER_PROFILE": "Cuentas",
"DIVIDER_PAGE": "Páginas",
"DIVIDER_GROUP": "Grupos"
},
"CONFIRM": {
"SUGGEST_CERTIFICATIONS": "Está usted segura/o querer <b>mandar estas sugerencia de certificatión</b> ?",
"ASK_CERTIFICATION": "Está usted segura/o querer <b>mandar una solicitud de certificación</b> ?",
"ASK_CERTIFICATIONS": "Está usted segura/o querer <b>mandar una solicitud de certificación</b> a estas personas ?"
}
},
"COMMENTS": {
"DIVIDER": "Comentarios",
"SHOW_MORE_COMMENTS": "Visualizar los comentarios anteriores",
"COMMENT_HELP": "Su comentario, preguntas, etc.",
"COMMENT_HELP_REPLY_TO": "Su repuesta…",
"BTN_SEND": "Mandar",
"POPOVER_SHARE_TITLE": "Mensaje #{{number}}",
"REPLY": "Responder",
"REPLY_TO": "Repuesta a :",
"REPLY_TO_LINK": "En repuesta a ",
"REPLY_TO_DELETED_COMMENT": "En repuesta a un comentario suprimido",
"REPLY_COUNT": "{{replyCount}} repuestas",
"DELETED_COMMENT": "Comentario suprimido",
"ERROR": {
"FAILED_SAVE_COMMENT": "Fracaso durante el respaldo del comentario",
"FAILED_REMOVE_COMMENT": "Fracaso durante la supresión del comentario"
}
},
"MESSAGE": {
"REPLY_TITLE_PREFIX": "Rep: ",
"FORWARD_TITLE_PREFIX": "Tr: ",
"BTN_REPLY": "Responder",
"BTN_COMPOSE": "Nuevo mensaje",
"BTN_WRITE": "Escribir",
"NO_MESSAGE_INBOX": "Ningun mensaje recibido",
"NO_MESSAGE_OUTBOX": "Ningun mensaje mandado",
"NOTIFICATIONS": {
"TITLE": "Mensajes",
"MESSAGE_RECEIVED": "Ha <b>recibido un mensaje</b><br/>de"
},
"LIST": {
"INBOX": "Bandeja de entrada",
"OUTBOX": "Mensajes mandados",
"TITLE": "Mensajes",
"POPOVER_ACTIONS": {
"TITLE": "Opciónes",
"DELETE_ALL": "Suprimir todos los mensajes"
}
},
"COMPOSE": {
"TITLE": "Nuevo mensaje",
"TITLE_REPLY": "Responder",
"SUB_TITLE": "Nuevo mensaje",
"TO": "A",
"OBJECT": "Objeto",
"OBJECT_HELP": "Objeto",
"ENCRYPTED_HELP": "Por favor, nota que este mensaje será cifrado antes envío, a fin que solo el destinatario pueda leerlo, y que esté asegurado que usted esté bien su autor.",
"MESSAGE": "Mensaje",
"MESSAGE_HELP": "Contenido del mensaje",
"CONTENT_CONFIRMATION": "El contenido del mensaje es vacío.<br/><br/>Sin embargo, quiere mandar el mensaje ?"
},
"VIEW": {
"TITLE": "Mensaje",
"SENDER": "Mandado por",
"RECIPIENT": "Mandado a",
"NO_CONTENT": "Mensaje vacío"
},
"CONFIRM": {
"REMOVE": "Está usted segura/o querer <b>suprimir este mensaje</b> ?<br/><br/>Esta operación es ireversible.",
"REMOVE_ALL" : "Está usted segura/o querer <b>suprimir todos los mensajes</b> ?<br/><br/>Esta operación es ireversible.",
"MARK_ALL_AS_READ": "Está usted segura/o querer <b>marcar todos los mensajes como leído</b> ?",
"USER_HAS_NO_PROFILE": "Esta identidad no tiene ningún perfil Cesium+. Se puede que no utilice la extensión Cesium+, y <b>así no consultará su mensaje</b>.<br/><br/>Está usted segura/o querer <b>continuar</b> a pesar de todo ?"
},
"INFO": {
"MESSAGE_REMOVED": "Mensaje suprimido",
"All_MESSAGE_REMOVED": "Todos los mensajes fueron suprimido",
"MESSAGE_SENT": "Mensaje mandado"
},
"ERROR": {
"SEND_MSG_FAILED": "Fracaso durante el envío del mensaje.",
"LOAD_MESSAGES_FAILED": "Fracaso durante la recuperación de los mensajes.",
"LOAD_MESSAGE_FAILED": "Fracaso durante la recuperación del mensaje.",
"MESSAGE_NOT_READABLE": "Lectura del mensaje imposible.",
"USER_NOT_RECIPIENT": "No esta el destinatario de este mensaje : deciframiento imposible.",
"NOT_AUTHENTICATED_MESSAGE": "La autenticidad del mensaje es dudosa o su contenido es corrupto.",
"REMOVE_MESSAGE_FAILED": "Fracaso en la supresión del mensaje",
"MESSAGE_CONTENT_TOO_LONG": "Valor demasiado largo ({{maxLength}} carácteres max).",
"MARK_AS_READ_FAILED": "Imposible marcar el mensaje como 'leído'.",
"LOAD_NOTIFICATIONS_FAILED": "Fracaso durante la recuperación de las notificaciónes de mensajes.",
"REMOVE_All_MESSAGES_FAILED": "Fracaso durante la supresión de todos los mensajes.",
"MARK_ALL_AS_READ_FAILED": "Fracaso durante el marcaje de los mensajes como leído.",
"RECIPIENT_IS_MANDATORY": "El destinatario es obligatorio."
}
},
"REGISTRY": {
"CATEGORY": "Actividad principal",
"GENERAL_DIVIDER": "Informaciónes generales",
"LOCATION_DIVIDER": "Dirección",
"SOCIAL_NETWORKS_DIVIDER": "Redes sociales y sitio web",
"TECHNICAL_DIVIDER": "Informaciónes técnicas",
"BTN_SHOW_WOT": "Personas",
"BTN_SHOW_WOT_HELP": "Buscar personas",
"BTN_SHOW_PAGES": "Páginas",
"BTN_SHOW_PAGES_HELP": "Búsqueda de páginas",
"BTN_NEW": "Creer una página",
"MY_PAGES": "Mis páginas",
"NO_PAGE": "Sin página",
"SEARCH": {
"TITLE": "Páginas",
"TITLE_SMALL_DEVICE": "Páginas",
"SEARCH_HELP": "Qué, Quién : restaurante, Con Marcel, ...",
"BTN_ADD": "Nuevo",
"BTN_OPTIONS": "Búsqueda avanzada",
"TYPE": "Tipo de página",
"LOCATION": "Localización",
"LOCATION_HELP": "Ciudad",
"LAST_RECORDS": "últimos registrados :",
"RESULTS": "Resultados :"
},
"VIEW": {
"TITLE": "Anuario",
"CATEGORY": "Actividad principal :",
"LOCATION": "Dirección :",
"MENU_TITLE": "Opciónes",
"POPOVER_SHARE_TITLE": "{{title}}",
"REMOVE_CONFIRMATION" : "Está usted segura/o querer suprimir esta página ?<br/><br/>Esta operación es ireversible."
},
"TYPE": {
"TITLE": "Nueva página",
"SELECT_TYPE": "Tipo de página :",
"ENUM": {
"SHOP": "Comercio local",
"COMPANY": "Empresa",
"ASSOCIATION": "Asociación",
"INSTITUTION": "Institución"
}
},
"EDIT": {
"TITLE": "Edición",
"TITLE_NEW": "Nueva página",
"RECORD_TYPE":"Tipo de página",
"RECORD_TITLE": "Nombre",
"RECORD_TITLE_HELP": "Nombre",
"RECORD_DESCRIPTION": "Descripción",
"RECORD_DESCRIPTION_HELP": "Descripción de la actividad",
"RECORD_ADDRESS": "Calle",
"RECORD_ADDRESS_HELP": "Dirección : calle, edificio...",
"RECORD_CITY": "Ciudad",
"RECORD_CITY_HELP": "Ciudad",
"RECORD_SOCIAL_NETWORKS": "Redes sociales y sitio web",
"RECORD_PUBKEY": "Llave pública" ,
"RECORD_PUBKEY_HELP": "Llave pública de recepción de los pagos"
},
"WALLET": {
"REGISTRY_DIVIDER": "Páginas",
"REGISTRY_HELP": "Las páginas se refieren a actividades que aceptan dinero o lo favorecen: empresas, negocios, asociaciones, instituciones."
},
"ERROR": {
"LOAD_CATEGORY_FAILED": "Fracaso en la carga de la lista de actividades",
"LOAD_RECORD_FAILED": "Fracaso durante la carga de la página",
"LOOKUP_RECORDS_FAILED": "Fracaso durante la ejecución de la búsqueda.",
"REMOVE_RECORD_FAILED": "Fracaso en la supresión de la página",
"SAVE_RECORD_FAILED": "Fracaso durante el respaldo",
"RECORD_NOT_EXISTS": "Página inexistente",
"GEO_LOCATION_NOT_FOUND": "Ciudad o código postal no encontrado"
},
"INFO": {
"RECORD_REMOVED" : "Página suprimida",
"RECORD_SAVED": "Página guardada"
}
},
"PROFILE": {
"PROFILE_DIVIDER": "Perfil ğchange",
"PROFILE_DIVIDER_HELP": "Se trata de datos auxiliares, almacenados en la red de intercambio.",
"NO_PROFILE_DEFINED": "Ningún perfil Cesium+",
"BTN_ADD": "Ingresar mi perfil",
"BTN_EDIT": "Editar mi perfil",
"BTN_DELETE": "Borrar mi perfil",
"BTN_REORDER": "Reordenar",
"UID": "Seudónimo",
"TITLE": "Nombre, Apellido",
"TITLE_HELP": "Nombre, Apellido",
"DESCRIPTION": "A propósito de yo",
"DESCRIPTION_HELP": "A propósito de yo...",
"SOCIAL_HELP": "http://...",
"GENERAL_DIVIDER": "Informaciónes generales",
"SOCIAL_NETWORKS_DIVIDER": "Redes sociales, sitios web",
"TECHNICAL_DIVIDER": "Informaciónes técnicas",
"MODAL_AVATAR": {
"TITLE": "Foto de perfil",
"SELECT_FILE_HELP": "Por favor, <b>elige un fichero imagen</b>, haciendo un clic sobre el botón por debajo :",
"BTN_SELECT_FILE": "Eligir una foto",
"RESIZE_HELP": "<b>Encuadra la imagen</b>, si es necesario. Un clic mantenido sobre la imagen permite desplazarla. Hace un clic sobre la zona abajo a la izquierda para hacer zoom.",
"RESULT_HELP": "<b>Aquí está el resultado</b> tal como está visible sobre su perfil :"
},
"CONFIRM": {
"DELETE": "¿Está seguro de que desea <b>eliminar su perfil de ğchange?</b><br/><br/>Esta operación es irreversible."
},
"ERROR": {
"REMOVE_PROFILE_FAILED": "Error de eliminación de perfil",
"LOAD_PROFILE_FAILED": "Fracaso en la carga del perfil usuario.",
"SAVE_PROFILE_FAILED": "Fracaso durante el respaldo",
"INVALID_SOCIAL_NETWORK_FORMAT": "Formato no tomado en cuenta : por favor, indica una dirección válida.<br/><br/>Ejemplos :<ul><li>- Una página Facebook (https://www.facebook.com/user)</li><li>- Una página web (http://www.misitio.es)</li><li>- Una dirección email (joe@dalton.com)</li></ul>",
"IMAGE_RESIZE_FAILED": "Fracaso durante el redimensionamiento de la imagen"
},
"INFO": {
"PROFILE_REMOVED": "Perfil eliminado",
"PROFILE_SAVED": "Perfil respaldado"
},
"HELP": {
"WARNING_PUBLIC_DATA": "Las informaciónes informadas en su perfil <b>están públicas</b> : visibles también por personas <b>no conectadas</b>."
}
},
"LOCATION": {
"BTN_GEOLOC_ADDRESS": "Actualizar desde la dirección",
"USE_GEO_POINT": "Geo-localizar (recomendado)?",
"LOADING_LOCATION": "Encontrar la dirección ...",
"LOCATION_DIVIDER": "Dirección",
"ADDRESS": "Calle",
"ADDRESS_HELP": "Calle, complemento de dirección...",
"CITY": "Ciudad",
"CITY_HELP": "Ciudad, País",
"DISTANCE": "Distancia máxima alrededor de la ciudad",
"DISTANCE_UNIT": "km",
"DISTANCE_OPTION": "{{value}} {{'LOCATION.DISTANCE_UNIT'|translate}}",
"SEARCH_HELP": "Ciudad, País",
"PROFILE_POSITION": "Posición del perfil",
"MODAL": {
"TITLE": "Búsqueda de dirección",
"SEARCH_HELP": "Ciudad, País",
"ALTERNATIVE_RESULT_DIVIDER": "Resultados alternativos para <b>{{address}}</b> :",
"POSITION": "Latitud/Longitud : {{lat}} {{lon}}"
},
"ERROR": {
"CITY_REQUIRED_IF_STREET": "Requerido si una calle ha sido llenada",
"REQUIRED_FOR_LOCATION": "Campo obligatorio para aparecer en el mapa",
"INVALID_FOR_LOCATION": "Dirección desconocida",
"GEO_LOCATION_FAILED": "No se puede recuperar su ubicación Por favor usa el botón de búsqueda.",
"ADDRESS_LOCATION_FAILED": "No se puede recuperar la posición de la dirección."
}
},
"ES_SETTINGS": {
"PLUGIN_NAME": "Cesium+",
"PLUGIN_NAME_HELP": "Perfiles, notificaciónes, mensajes privados",
"ENABLE_TOGGLE": "Activar la extensión ?",
"ENABLE_MESSAGE_TOGGLE": "Activar los mensajes privados ?",
"ENABLE_SETTINGS_TOGGLE": "Activar el almacenamiento a distancia de las configuraciónes ?",
"PEER": "Dirección del nodo de datos",
"POPUP_PEER": {
"TITLE" : "Nodo de datos",
"HELP" : "Ingresa la dirección del nodo que quiere utilizar :",
"PEER_HELP": "servidor.dominio.com:puerto"
},
"NOTIFICATIONS": {
"DIVIDER": "Notificaciónes",
"HELP_TEXT": "Activa los tipos de notificaciónes que usted desea recibir :",
"ENABLE_TX_SENT": "Notificar la validación de los <b>pagos emitidos</b> ?",
"ENABLE_TX_RECEIVED": "Notificar la validación de los <b>pagos recibidos</b> ?",
"ENABLE_CERT_SENT": "Notificar la validación de las <b>certificaciónes emitidas</b> ?",
"ENABLE_CERT_RECEIVED": "Notificar la validación de las <b>certificaciónes recibidas</b> ?"
},
"CONFIRM": {
"ASK_ENABLE_TITLE": "Nuevas funcionalidades",
"ASK_ENABLE": "Nuevas funcionalidades son disponibles : <ul><li>&nbsp;&nbsp;<b><i class=\"icon ion-person\"></i> Perfiles Cesium+</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-android-notifications\"></i> Notificaciónes</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-email\"></i> Mensajes privados</b>.</ul><br/>Fueron <b>desactivadas</b> en sus configuraciones.<br/><br/><b>Quiere usted activarlas</b> ?"
}
},
"ES_WALLET": {
"ERROR": {
"RECIPIENT_IS_MANDATORY": "Un destinatario es obligatorio para el cifrado."
}
},
"EVENT": {
"NODE_STARTED": "Su nodo ES API <b>{{params[0]}}</b> es comenzado",
"NODE_BMA_DOWN": "El nodo <b>{{params[0]}}:{{params[1]}}</b> (utilizado por su nodo ES API) <b>no es localizable</b>.",
"NODE_BMA_UP": "El nodo <b>{{params[0]}}:{{params[1]}}</b> es de nuevo accesible.",
"MEMBER_JOIN": "Ahora usted está <b>miembro</b> de la moneda <b>{{params[0]}}</b> !",
"MEMBER_LEAVE": "No está <b>miembro</b> de la moneda <b>{{params[0]}}</b>!",
"MEMBER_EXCLUDE": "Usted ya no es un miembro de la moneda <b>{{params[0]}}</b>, la falta de no renovación o la falta de certificaciones.",
"MEMBER_REVOKE": "La revocación de su cuenta se ha hecho. Puede que no sea un miembro de la cuenta en moneda <b>{{params[0]}}</b>.",
"MEMBER_ACTIVE": "Su renovación de adhesión a la moneda <b>{{params[0]}}</b> fue <b>tomado en cuenta</b>.",
"TX_SENT": "Su <b>pago</b> a <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> fue efectuado.",
"TX_SENT_MULTI": "Su <b>pago</b> a <b>{{params[1]}}</b> fue efectuado.",
"TX_RECEIVED": "Ha <b>recibido un pago</b> de <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"TX_RECEIVED_MULTI": "Ha <b>recibido un pago</b> de <b>{{params[1]}}</b>.",
"CERT_SENT": "Su <b>certificación</b> a <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> fue efectuada.",
"CERT_RECEIVED": "Ha <b>recibido una certificación</b> de <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"REGISTRY": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> ha comentado su referencia : <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> ha modificado su comentario sobre su referencia : <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> ha contestado a su comentario sobre el referencia : <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> ha modificado la repuesta a su comentario sobre el referencia : <b>{{params[2]}}</b>"
},
"CONFIRM": {
"ES_USE_FALLBACK_NODE": "Nodo de datos <b>{{old}}</b> dirección inaccesible o no válida.<br/><br/>¿Desea utilizar temporalmente el nodo de datos <b>{{new}}</b>?"
}
},
"ERROR": {
"ES_CONNECTION_ERROR": "Nodo de datos <b>{{server}}</b> dirección no válida o no válida.<br/><br/>Verifique su conexión a Internet o cambie el nodo de datos en <a class=\"positive\" ng-click=\"doQuickFix('settings')\">configuración avanzada</a>.",
"ES_MAX_UPLOAD_BODY_SIZE": "El volumen de datos a enviar excede el límite establecido por el servidor.<br/><br/>Por favor, inténtelo de nuevo después, por ejemplo, borrando fotos."
}
}
);
$translateProvider.translations("fr-FR", {
"COMMON": {
"CATEGORY": "Catégorie",
"CATEGORY_SELECT_HELP": "Sélectionner",
"CATEGORIES": "Catégories",
"CATEGORY_SEARCH_HELP": "Recherche",
"COMMENT_HELP": "Commentaire",
"LAST_MODIFICATION_DATE": "Mise à jour le",
"BTN_LIKE": "J'aime",
"BTN_FOLLOW": "Suivre",
"BTN_STOP_FOLLOW": "Ne plus suivre",
"LIKES_TEXT": "{{total}} personne{{total > 1 ? 's' : ''}} {{total > 1 ? 'ont' : 'a'}} aimé cette page",
"DISLIKES_TEXT": "{{total}} personne{{total > 1 ? 's' : ''}} {{total > 1 ? 'n\\'ont' : 'n\\'a'}} pas aimé cette page",
"VIEWS_TEXT": "{{total}} personne{{total > 1 ? 's' : ''}} {{total > 1 ? 'ont' : 'a'}} consulté cette page",
"FOLLOWS_TEXT": "{{total}} personne{{total > 1 ? 's' : ''}} {{total > 1 ? 'suivent' : 'suit'}} cette page",
"ABUSES_TEXT": "{{total}} personne{{total > 1 ? 's' : ''}} {{total > 1 ? 'ont' : 'a'}} signalé un problème",
"BTN_REPORT_ABUSE_DOTS": "Signaler un problème ou un abus...",
"BTN_REMOVE_REPORTED_ABUSE": "Annuler mon signalement",
"SUBMIT_BY": "Soumis par",
"GEO_DISTANCE_SEARCH": "Distance de recherche",
"GEO_DISTANCE_OPTION": "{{value}} km",
"BTN_PUBLISH": "Publier",
"BTN_PICTURE_DELETE": "Supprimer",
"BTN_PICTURE_FAVORISE": "Principale",
"BTN_PICTURE_ROTATE": "Tourner",
"BTN_ADD_PICTURE": "Ajouter une photo",
"NOTIFICATION": {
"TITLE": "Nouvelle notification | {{'COMMON.APP_NAME'|translate}}",
"HAS_UNREAD": "Vous avez {{count}} notification{{count>0?'s':''}} non lue{{count>0?'s':''}}"
},
"NOTIFICATIONS": {
"TITLE": "Notifications",
"MARK_ALL_AS_READ": "Tout marquer comme lu",
"NO_RESULT": "Aucune notification",
"SHOW_ALL": "Voir tout",
"LOAD_NOTIFICATIONS_FAILED": "Erreur de chargement des notifications"
},
"REPORT_ABUSE": {
"TITLE": "Signaler un problème",
"SUB_TITLE": "Merci d'expliquer succintement le problème :",
"REASON_HELP": "J'explique le problème...",
"ASK_DELETE": "Demander la suppression ?",
"CONFIRM": {
"SENT": "Signalement envoyé. Merci !"
}
}
},
"MENU": {
"REGISTRY": "Pages",
"USER_PROFILE": "Mon profil",
"MESSAGES": "Messages",
"NOTIFICATIONS": "Notifications",
"INVITATIONS": "Invitations"
},
"ACCOUNT": {
"NEW": {
"ORGANIZATION_ACCOUNT": "Compte pour une organisation",
"ORGANIZATION_ACCOUNT_HELP": "Si vous représentez une entreprise, une association, etc.<br/>Aucun dividende universel ne sera créé par ce compte."
},
"EVENT": {
"MEMBER_WITHOUT_PROFILE": "Vous pouvez <a ui-sref=\"app.edit_profile\">saisir votre profil Cesium+</a> (optionnel) pour offrir une meilleure visibilité de votre compte. Ce profil sera stocké dans <b>un annuaire indépendant</b> de la monnaie, mais décentralisé."
},
"ERROR": {
"WS_CONNECTION_FAILED": "ğchange ne peut pas recevoir les notifications à cause d'une erreur technique (connexion au noeud de données ğchange).<br/><br/>Si le problème persiste, veuillez <b>choisir un autre noeud de données</b> dans les paramètres."
}
},
"WOT": {
"BTN_SUGGEST_CERTIFICATIONS_DOTS": "Suggérer des identités à certifier...",
"BTN_ASK_CERTIFICATIONS_DOTS": "Demander à des membres de me certifier...",
"BTN_ASK_CERTIFICATION": "Demander une certification",
"SUGGEST_CERTIFICATIONS_MODAL": {
"TITLE": "Suggérer des certifications",
"HELP": "Sélectionner vos suggestions"
},
"ASK_CERTIFICATIONS_MODAL": {
"TITLE": "Demander des certifications",
"HELP": "Sélectionner les destinataires"
},
"SEARCH": {
"DIVIDER_PROFILE": "Comptes",
"DIVIDER_PAGE": "Pages",
"DIVIDER_GROUP": "Groupes"
},
"VIEW": {
"STARS": "Niveau de confiance",
"STAR_HIT_COUNT": "{{total}} note{{total>1 ? 's' : ''}}",
"BTN_STAR_HELP": "Noter ce profil",
"BTN_STARS_REMOVE": "Supprimer ma note",
"BTN_REDO_STAR_HELP": "Actualiser votre note",
"BTN_FOLLOW": "Suivre l'activité de ce profil",
"BTN_STOP_FOLLOW": "Ne plus suivre ce profil"
}
},
"COMMENTS": {
"DIVIDER": "Commentaires",
"SHOW_MORE_COMMENTS": "Afficher les commentaires précédents",
"COMMENT_HELP": "Votre commentaire, question, etc.",
"COMMENT_HELP_REPLY_TO": "Votre réponse...",
"BTN_SEND": "Envoyer",
"POPOVER_SHARE_TITLE": "Message #{{number}}",
"REPLY": "Répondre",
"REPLY_TO": "Réponse à :",
"REPLY_TO_LINK": "En réponse à ",
"REPLY_TO_DELETED_COMMENT": "En réponse à un commentaire supprimé",
"REPLY_COUNT": "{{replyCount}} réponses",
"DELETED_COMMENT": "Commentaire supprimé",
"MODIFIED_ON": "modifié le {{time|formatDate}}",
"MODIFIED_PARENTHESIS": "(modifié ensuite)",
"ERROR": {
"FAILED_SAVE_COMMENT": "Erreur lors de la sauvegarde du commentaire",
"FAILED_REMOVE_COMMENT": "Erreur lors de la suppression du commentaire"
}
},
"MESSAGE": {
"REPLY_TITLE_PREFIX": "Rep: ",
"FORWARD_TITLE_PREFIX": "Tr: ",
"BTN_REPLY": "Répondre",
"BTN_COMPOSE": "Nouveau message",
"BTN_WRITE": "Ecrire",
"NO_MESSAGE_INBOX": "Aucun message reçu",
"NO_MESSAGE_OUTBOX": "Aucun message envoyé",
"NOTIFICATIONS": {
"TITLE": "Messages",
"MESSAGE_RECEIVED": "Vous avez <b>reçu un message</b><br/>de"
},
"LIST": {
"INBOX": "Boîte de réception",
"OUTBOX": "Messages envoyés",
"LAST_INBOX": "Nouveaux messages",
"LAST_OUTBOX": "Messages envoyés",
"BTN_LAST_MESSAGES": "Messages récents",
"TITLE": "Messages",
"SEARCH_HELP": "Recherche dans les messages",
"POPOVER_ACTIONS": {
"TITLE": "Options",
"DELETE_ALL": "Supprimer tous les messages"
}
},
"COMPOSE": {
"TITLE": "Nouveau message",
"TITLE_REPLY": "Répondre",
"SUB_TITLE": "Nouveau message",
"TO": "A",
"OBJECT": "Objet",
"OBJECT_HELP": "Objet",
"ENCRYPTED_HELP": "Veuillez noter que ce message sera chiffré avant envoi, afin que seul le destinataire puisse le lire, et qu'il soit assuré que vous soyez bien son auteur.",
"MESSAGE": "Message",
"MESSAGE_HELP": "Contenu du message",
"CONTENT_CONFIRMATION": "Le contenu du message est vide.<br/><br/>Voulez-vous néanmoins envoyer le message ?"
},
"VIEW": {
"TITLE": "Message",
"SENDER": "Envoyé par",
"RECIPIENT": "Envoyé à",
"NO_CONTENT": "Message vide",
"DELETE": "Supprimer le message"
},
"CONFIRM": {
"REMOVE": "Êtes-vous sûr de vouloir <b>supprimer ce message</b> ?<br/><br/>Cette opération est irréversible.",
"REMOVE_ALL" : "Êtes-vous sûr de vouloir <b>supprimer tous les messages</b> ?<br/><br/>Cette opération est irréversible.",
"MARK_ALL_AS_READ": "Êtes-vous sûr de vouloir <b>marquer tous les messages comme lu</b> ?",
"USER_HAS_NO_PROFILE": "Cette identité n'a aucun profil. Il se peut qu'elle n'utilise pas l'extension de gestion des messages privés, et <b>ne consultera donc pas votre message</b>.<br/><br/>Êtes-vous sûr de vouloir <b>continuer</b> malgré tout ?"
},
"INFO": {
"MESSAGE_REMOVED": "Message supprimé",
"All_MESSAGE_REMOVED": "Tous les messages ont été supprimés",
"MESSAGE_SENT": "Message envoyé"
},
"ERROR": {
"SEND_MSG_FAILED": "Erreur lors de l'envoi du message.",
"LOAD_MESSAGES_FAILED": "Erreur lors de la récupération des messages.",
"LOAD_MESSAGE_FAILED": "Erreur lors de la récupération du message.",
"MESSAGE_NOT_READABLE": "Lecture du message impossible.",
"USER_NOT_RECIPIENT": "Vous n'êtes pas le destinataire de ce message : déchiffrement impossible.",
"NOT_AUTHENTICATED_MESSAGE": "L'authenticité du message est douteuse ou son contenu est corrompu.",
"REMOVE_MESSAGE_FAILED": "Erreur de suppression du message",
"MESSAGE_CONTENT_TOO_LONG": "Valeur trop longue ({{maxLength}} caractères max).",
"MARK_AS_READ_FAILED": "Impossible de marquer le message comme 'lu'.",
"LOAD_NOTIFICATIONS_FAILED": "Erreur lors de la récupération des notifications de messages.",
"REMOVE_All_MESSAGES_FAILED": "Erreur lors de la suppression de tous les messages.",
"MARK_ALL_AS_READ_FAILED": "Erreur lors du marquage des messages comme lu.",
"RECIPIENT_IS_MANDATORY": "Le destinataire est obligatoire."
}
},
"REGISTRY": {
"CATEGORY": "Activité principale",
"GENERAL_DIVIDER": "Informations générales",
"LOCATION_DIVIDER": "Adresse",
"SOCIAL_NETWORKS_DIVIDER": "Réseaux sociaux et site web",
"TECHNICAL_DIVIDER": "Informations techniques",
"BTN_SHOW_WOT": "Personnes",
"BTN_SHOW_WOT_HELP": "Rechercher des personnes",
"BTN_SHOW_PAGES": "Pages",
"BTN_SHOW_PAGES_HELP": "Rechercher des pages",
"BTN_NEW": "Créer une page",
"MY_PAGES": "Mes pages",
"NO_PAGE": "Aucune page",
"SEARCH": {
"TITLE": "Pages",
"SEARCH_HELP": "Quoi, Qui : restaurant, Chez Marcel, ...",
"BTN_ADD": "Nouveau",
"BTN_LAST_RECORDS": "Pages récentes",
"BTN_ADVANCED_SEARCH": "Recherche avancée",
"BTN_OPTIONS": "Recherche avancée",
"TYPE": "Type de page",
"LOCATION_HELP": "Où : Code postal, Ville",
"RESULTS": "Résultats",
"RESULT_COUNT_LOCATION": "{{count}} résultat{{count>0?'s':''}}, près de {{location}}",
"RESULT_COUNT": "{{count}} résultat{{count>0?'s':''}}",
"LAST_RECORDS": "Pages récentes",
"LAST_RECORD_COUNT_LOCATION": "{{count}} page{{count>0?'s':''}} récente{{count>0?'s':''}}, près de {{location}}",
"LAST_RECORD_COUNT": "{{count}} page{{count>0?'s':''}} récente{{count>0?'s':''}}",
"POPOVER_FILTERS": {
"BTN_ADVANCED_SEARCH": "Options avancées ?"
}
},
"VIEW": {
"TITLE": "Annuaire",
"CATEGORY": "Activité principale :",
"LOCATION": "Adresse :",
"MENU_TITLE": "Options",
"POPOVER_SHARE_TITLE": "{{title}}",
"REMOVE_CONFIRMATION" : "Êtes-vous sûr de vouloir supprimer cette page ?<br/><br/>Cette opération est irréversible."
},
"TYPE": {
"TITLE": "Types",
"SELECT_TYPE": "Type de page :",
"ENUM": {
"SHOP": "Commerce local",
"COMPANY": "Entreprise",
"ASSOCIATION": "Association",
"INSTITUTION": "Institution"
}
},
"EDIT": {
"TITLE": "Edition",
"TITLE_NEW": "Nouvelle page",
"RECORD_TYPE":"Type de page",
"RECORD_TITLE": "Nom",
"RECORD_TITLE_HELP": "Nom",
"RECORD_DESCRIPTION": "Description",
"RECORD_DESCRIPTION_HELP": "Description de l'activité",
"RECORD_ADDRESS": "Rue",
"RECORD_ADDRESS_HELP": "Rue, bâtiment...",
"RECORD_CITY": "Ville",
"RECORD_CITY_HELP": "Ville",
"RECORD_SOCIAL_NETWORKS": "Réseaux sociaux et site web",
"RECORD_PUBKEY": "Clé publique",
"RECORD_PUBKEY_HELP": "Clé publique de réception des paiements"
},
"WALLET": {
"REGISTRY_DIVIDER": "Pages",
"REGISTRY_HELP": "Les pages référencent des activités acceptant la monnaie ou la favorisant : commerces, entreprises, associations, institutions."
},
"ERROR": {
"LOAD_CATEGORY_FAILED": "Erreur de chargement de la liste des activités",
"LOAD_RECORD_FAILED": "Erreur lors du chargement de la page",
"LOOKUP_RECORDS_FAILED": "Erreur lors de l'exécution de la recherche",
"REMOVE_RECORD_FAILED": "Erreur lors de la suppression de la page",
"SAVE_RECORD_FAILED": "Erreur lors de la sauvegarde",
"RECORD_NOT_EXISTS": "Page inexistante",
"GEO_LOCATION_NOT_FOUND": "Ville ou code postal non trouvé"
},
"INFO": {
"RECORD_REMOVED" : "Page supprimée",
"RECORD_SAVED": "Page sauvegardée"
}
},
"PROFILE": {
"PROFILE_DIVIDER": "Profil ğchange",
"PROFILE_DIVIDER_HELP": "Il s'agit de données annexes, stockées sur le réseau ğchange.",
"NO_PROFILE_DEFINED": "Aucun profil saisi",
"BTN_ADD": "Saisir mon profil",
"BTN_EDIT": "Editer mon profil",
"BTN_DELETE": "Supprimer mon profil",
"BTN_REORDER": "Réordonner",
"UID": "Pseudonyme",
"TITLE": "Nom, Prénom",
"TITLE_HELP": "Nom, Prénom",
"DESCRIPTION": "A propos de moi",
"DESCRIPTION_HELP": "A propos de moi...",
"SOCIAL_HELP": "http://...",
"GENERAL_DIVIDER": "Informations générales",
"SOCIAL_NETWORKS_DIVIDER": "Réseaux sociaux, sites web",
"STAR": "Niveau de confiance",
"TECHNICAL_DIVIDER": "Informations techniques",
"MODAL_AVATAR": {
"TITLE": "Photo de profil",
"SELECT_FILE_HELP": "Veuillez <b>choisir un fichier image</b>, en cliquant sur le bouton ci-dessous :",
"BTN_SELECT_FILE": "Choisir une photo",
"RESIZE_HELP": "<b>Recadrez l'image</b>, si besoin. Un clic maintenu sur l'image permet de la déplacer. Cliquez sur la zone en bas à gauche pour zoomer.",
"RESULT_HELP": "<b>Voici le résultat</b> tel que visible sur votre profil :"
},
"CONFIRM": {
"DELETE": "Êtes-vous sûr de vouloir <b>supprimer votre profil ğchange ?</b><br/><br/>Cette opération est irréversible."
},
"ERROR": {
"REMOVE_PROFILE_FAILED": "Erreur de suppression du profil",
"LOAD_PROFILE_FAILED": "Erreur de chargement du profil utilisateur",
"SAVE_PROFILE_FAILED": "Erreur lors de la sauvegarde",
"INVALID_SOCIAL_NETWORK_FORMAT": "Format non pris en compte : veuillez indiquer une adresse valide.<br/><br/>Exemples :<ul><li>- Une page Facebook (https://www.facebook.com/user)</li><li>- Une page web (http://www.monsite.fr)</li><li>- Une adresse email (joe@dalton.com)</li></ul>",
"IMAGE_RESIZE_FAILED": "Erreur lors du redimensionnement de l'image"
},
"INFO": {
"PROFILE_REMOVED": "Profil supprimé",
"PROFILE_SAVED": "Profil sauvegardé"
},
"HELP": {
"WARNING_PUBLIC_DATA": "Les informations renseignées dans votre profil <b>sont publiques</b> : visibles y compris par des personnes <b>non connectées</b>."
}
},
"LOCATION": {
"BTN_GEOLOC_ADDRESS": "Trouver mon adresse sur la carte",
"USE_GEO_POINT": "Géolocaliser (recommandé) ?",
"LOADING_LOCATION": "Recherche de l'adresse...",
"LOCATION_DIVIDER": "Adresse",
"ADDRESS": "Rue",
"ADDRESS_HELP": "Rue, complément d'adresse...",
"CITY": "Ville",
"CITY_HELP": "Code postal, Ville, Pays",
"DISTANCE": "Distance maximale autour de la ville",
"DISTANCE_UNIT": "km",
"DISTANCE_OPTION": "{{value}} {{'LOCATION.DISTANCE_UNIT'|translate}}",
"SEARCH_HELP": "Code postal, Ville",
"PROFILE_POSITION": "Position du profil",
"MODAL": {
"TITLE": "Recherche de l'adresse",
"SEARCH_HELP": "Ville, Code postal, Pays",
"ALTERNATIVE_RESULT_DIVIDER": "Résultats alternatifs pour <b>{{address}}</b> :",
"POSITION": "Lat/Lon : {{lat}}/{{lon}}"
},
"ERROR": {
"CITY_REQUIRED_IF_STREET": "Champ obligatoire (car une rue est saisie)",
"REQUIRED_FOR_LOCATION": "Champ obligatoire pour apparaître sur la carte",
"INVALID_FOR_LOCATION": "Adresse inconnue",
"GEO_LOCATION_FAILED": "Impossible de récupérer votre position. Veuillez utiliser le bouton de recherche.",
"ADDRESS_LOCATION_FAILED": "Impossible de récupérer la position à partir de l'adresse"
}
},
"SUBSCRIPTION": {
"SUBSCRIPTION_DIVIDER": "Services en ligne",
"SUBSCRIPTION_DIVIDER_HELP": "Les services en ligne offrent des services supplémentaires optionnels, délégués à un tiers.",
"BTN_ADD": "Ajouter un service",
"BTN_EDIT": "Gérer mes services",
"NO_SUBSCRIPTION": "Aucun service utilisé",
"SUBSCRIPTION_COUNT": "Services / Abonnements",
"EDIT": {
"TITLE": "Services en ligne",
"HELP_TEXT": "Gérez ici vos abonnements et autres services en ligne",
"PROVIDER": "Prestataire :"
},
"TYPE": {
"ENUM": {
"EMAIL": "Recevoir les notifications par email"
}
},
"CONFIRM": {
"DELETE_SUBSCRIPTION": "Êtes-vous sûr de vouloir <b>supprimer cet abonnement</b> ?"
},
"ERROR": {
"LOAD_SUBSCRIPTIONS_FAILED": "Erreur lors du chargement des services en ligne",
"ADD_SUBSCRIPTION_FAILED": "Erreur de l'envoi de l'abonnement",
"UPDATE_SUBSCRIPTION_FAILED": "Erreur de la mise à jour de l'abonnement",
"DELETE_SUBSCRIPTION_FAILED": "Erreur lors de la suppression de l'abonnement"
},
"MODAL_EMAIL": {
"TITLE": "Notification par email",
"HELP": "Remplissez ce formulaire pour <b>être notifié par email</b> des événements de votre compte.<br/>Votre adresse email sera chiffrée pour n'être visible que par le prestataire de service.",
"EMAIL_LABEL": "Votre email :",
"EMAIL_HELP": "jean.dupond@domaine.com",
"FREQUENCY_LABEL": "Fréquence des notifications :",
"FREQUENCY_DAILY": "Journalier",
"FREQUENCY_WEEKLY": "Hebdomadaire",
"PROVIDER": "Prestataire du service :"
}
},
"DOCUMENT": {
"HASH": "Hash : ",
"LOOKUP": {
"TITLE": "Recherche de documents",
"BTN_ACTIONS": "Actions",
"SEARCH_HELP": "issuer:AAA*, time:1508406169",
"LAST_DOCUMENTS_DOTS": "Derniers documents :",
"LAST_DOCUMENTS": "Derniers documents",
"SHOW_QUERY": "Voir la requête",
"HIDE_QUERY": "Masquer la requête",
"HEADER_TIME": "Date/Heure",
"HEADER_ISSUER": "Emetteur",
"HEADER_RECIPIENT": "Destinataire",
"READ": "Lu",
"DOCUMENT_TYPE": "Type",
"DOCUMENT_TITLE": "Titre",
"BTN_REMOVE": "Supprimer ce document",
"BTN_COMPACT": "Compacter",
"HAS_REGISTERED": "a créé ou modifié son profil",
"POPOVER_ACTIONS": {
"TITLE": "Actions",
"REMOVE_ALL": "Supprimer ces documents..."
},
"TYPE": {
"USER_PROFILE": "Profil",
"MARKET_RECORD": "Annonce",
"MARKET_COMMENT": "Commentaire sur une annonce",
"PAGE_RECORD": "Page",
"PAGE_COMMENT": "Commentaire sur une page",
"GROUP_RECORD": "Groupe",
"GROUP_COMMENT": "Commentaire sur un groupe"
}
},
"INFO": {
"REMOVED": "Document supprimé"
},
"CONFIRM": {
"REMOVE": "Êtes-vous sûr de vouloir <b>supprimer ce document</b> ?",
"REMOVE_ALL": "Êtes-vous sûr de vouloir <b>supprimer ces documents</b> ?"
},
"ERROR": {
"LOAD_DOCUMENTS_FAILED": "Erreur lors de la recherche de documents",
"REMOVE_FAILED": "Erreur lors de la suppression du document",
"REMOVE_ALL_FAILED": "Erreur lors de la suppression des documents"
}
},
"ES_SETTINGS": {
"PLUGIN_NAME": "Paramètres avancés",
"PLUGIN_NAME_HELP": "Filtrage des notifications, etc.",
"ENABLE_TOGGLE": "Activer l'extension ?",
"ENABLE_MESSAGE_TOGGLE": "Activer les messages privés ?",
"ENABLE_SETTINGS_TOGGLE": "Activer le stockage distant des paramètres ?",
"PEER": "Adresse du nœud de données",
"POPUP_PEER": {
"TITLE" : "Nœud de données",
"HELP" : "Saisissez l'adresse du nœud que vous voulez utiliser :",
"PEER_HELP": "serveur.domaine.com:port"
},
"NOTIFICATIONS": {
"DIVIDER": "Notifications",
"HELP_TEXT": "Activez les types de notifications que vous souhaitez recevoir :",
"ENABLE_TX_SENT": "Notifier les <b>paiements émis</b> ?",
"ENABLE_TX_RECEIVED": "Notifier les <b>paiements reçus</b> ?",
"ENABLE_CERT_SENT": "Notifier les <b>certifications émises</b> ?",
"ENABLE_CERT_RECEIVED": "Notifier les <b>certifications reçues</b> ?"
},
"CONFIRM": {
"ASK_ENABLE_TITLE": "Fonctionnalités optionnelles",
"ASK_ENABLE": "L'extension Cesium+ est <b>désactivée</b> dans vos paramètres, rendant inactives les fonctionnalités : <ul><li>&nbsp;&nbsp;<b><i class=\"icon ion-person\"></i> Profils Cesium+</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-android-notifications\"></i> Notifications</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-email\"></i> Messages privés</b>.<li>&nbsp;&nbsp;<b><i class=\"icon ion-location\"></i> Cartes, etc.</b>.</ul><br/><b>Souhaitez-vous ré-activer</b> l'extension ?"
}
},
"ES_WALLET": {
"ERROR": {
"RECIPIENT_IS_MANDATORY": "Un destinataire est obligatoire pour le chiffrement."
}
},
"ES_PEER": {
"NAME": "Nom",
"DOCUMENTS": "Documents",
"SOFTWARE": "Logiciel",
"DOCUMENT_COUNT": "Nombre de documents",
"EMAIL_SUBSCRIPTION_COUNT": "{{emailSubscription}} abonnés aux notifications par email"
},
"EVENT": {
"NODE_STARTED": "Votre noeud ES API <b>{{params[0]}}</b> est démarré",
"NODE_BMA_DOWN": "Le noeud <b>{{params[0]}}:{{params[1]}}</b> (utilisé par votre noeud ES API) est <b>injoignable</b>.",
"NODE_BMA_UP": "Le noeud <b>{{params[0]}}:{{params[1]}}</b> est à nouveau accessible.",
"MEMBER_JOIN": "Vous êtes maintenant <b>membre</b> de la monnaie <b>{{params[0]}}</b> !",
"MEMBER_LEAVE": "Vous n'êtes <b>plus membre</b> de la monnaie <b>{{params[0]}}</b> !",
"MEMBER_EXCLUDE": "Vous n'êtes <b>plus membre</b> de la monnaie <b>{{params[0]}}</b>, faute de non renouvellement ou par manque de certifications.",
"MEMBER_REVOKE": "La révocation de votre compte a été effectuée. Il ne pourra plus être un compte membre de la monnaie <b>{{params[0]}}</b>.",
"MEMBER_ACTIVE": "Votre renouvellement d'adhésion à la monnaie <b>{{params[0]}}</b> a été <b>pris en compte</b>.",
"TX_SENT": "Votre <b>paiement</b> à <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> a été effectué.",
"TX_SENT_MULTI": "Votre <b>paiement</b> à <b>{{params[1]}}</b> a été effectué.",
"TX_RECEIVED": "Vous avez <b>reçu un paiement</b> de <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"TX_RECEIVED_MULTI": "Vous avez <b>reçu un paiement</b> de <b>{{params[1]}}</b>.",
"CERT_SENT": "Votre <b>certification</b> à <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> a été effectuée.",
"CERT_RECEIVED": "Vous avez <b>reçu une certification</b> de <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"USER": {
"LIKE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> aime votre profil",
"FOLLOW_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> suit votre activité",
"STAR_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> vous a noté ({{params[3]}} <b class=\"ion-star\">)",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> vous demande une modération sur le profil : <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a signalé un profil à supprimer : <b>{{params[2]}}</b>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a signalé votre profil"
},
"PAGE": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a commenté votre page : <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a modifié son commentaire sur votre page : <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a répondu à votre commentaire sur la page : <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a modifié sa réponse à votre commentaire sur la page : <b>{{params[2]}}</b>",
"FOLLOW_NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a commenté la page : <b>{{params[2]}}</b>",
"FOLLOW_UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a modifié son commentaire sur la page : <b>{{params[2]}}</b>",
"FOLLOW_NEW": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a ajouté la page : <b>{{params[2]}}</b>",
"FOLLOW_UPDATE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a modifié la page : <b>{{params[2]}}</b>",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> vous demande une modération sur la page : <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a signalé une page à supprimer : <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a signalé votre page : <b>{{params[2]}}</b>"
}
},
"CONFIRM": {
"ES_USE_FALLBACK_NODE": "Nœud de données <b>{{old}}</b> injoignable ou adresse invalide.<br/><br/>Voulez-vous temporairement utiliser le nœud de données <b>{{new}}</b> ?"
},
"ERROR": {
"ES_CONNECTION_ERROR": "Nœud de données <b>{{server}}</b> injoignable ou adresse invalide.<br/><br/>Vérifiez votre connexion Internet, ou changer de nœud de données dans les <a class=\"positive\" ng-click=\"doQuickFix('settings')\">paramètres avancés</a>.",
"ES_MAX_UPLOAD_BODY_SIZE": "Le volume des données à envoyer dépasse la limite fixée par le serveur.<br/>Veuillez ré-essayer après avoir, par exemple, supprimer des photos."
}
}
);
$translateProvider.translations("nl-NL", {
"COMMON": {
"CATEGORY": "Categorie",
"CATEGORIES": "Categorieën",
"CATEGORY_SEARCH_HELP": "Zoeken",
"LAST_MODIFICATION_DATE": "Vernieuws op ",
"SUBMIT_BY": "Ingediend door",
"BTN_PUBLISH": "Publiceren",
"BTN_PICTURE_DELETE": "Wissen",
"BTN_PICTURE_FAVORISE": "Default",
"BTN_ADD_PICTURE": "Afbeelding toevoegen",
"NOTIFICATIONS": {
"TITLE": "Notificaties",
"MARK_ALL_AS_READ": "Markeer alles als gelezen",
"NO_RESULT": "Geen berichten",
"SHOW_ALL": "Toon alles",
"LOAD_NOTIFICATIONS_FAILED": "Kan berichten niet laden"
}
},
"MENU": {
"REGISTRY": "Ondernemingen",
"USER_PROFILE": "Mijn profiel",
"MESSAGES": "Berichten"
},
"ACCOUNT": {
"NEW": {
"ORGANIZATION_ACCOUNT": "Ondernemingsrekening",
"ORGANIZATION_ACCOUNT_HELP": "Als je een onderneming, vereniging etc. vertegenwoordigd.<br/>Deze rekening zal geen dividend créeren."
},
"EVENT": {
"MEMBER_WITHOUT_PROFILE": "Vul <a ui-sref=\"app.user_edit_profile\"je gebruikersprofiel</a> in om sneller een certificering te verkrijgen. Leden zullen een verfifieerbare identiteit eerder vertrouwen."
}
},
"COMMENTS": {
"DIVIDER": "Commentaren",
"SHOW_MORE_COMMENTS": "Toon eerder commentaren",
"COMMENT_HELP": "Jouw commentaar, vraag...",
"COMMENT_HELP_REPLY_TO": "Jouw antwoord...",
"BTN_SEND": "Verzenden",
"POPOVER_SHARE_TITLE": "Bericht #{{number}}",
"REPLY": "Antwoord",
"REPLY_TO": "Antwoorden op:",
"REPLY_TO_LINK": "In antwoord op ",
"REPLY_TO_DELETED_COMMENT": "In antwoord op een gewist bericht",
"REPLY_COUNT": "{{replyCount}} antwoorden",
"DELETED_COMMENT": "Bericht gewist"
},
"MESSAGE": {
"REPLY_TITLE_PREFIX": "Re: ",
"FORWARD_TITLE_PREFIX": "Fw: ",
"BTN_REPLY": "Antwoord",
"BTN_COMPOSE": "Nieuw bericht",
"BTN_WRITE": "Schrijven",
"NO_MESSAGE_INBOX": "Geen bericht ontvangen",
"NO_MESSAGE_OUTBOX": "Geen bericht verzonden",
"NOTIFICATIONS": {
"TITLE": "Berichten",
"MESSAGE_RECEIVED": "Je hebt een <b>bericht ontvangen</b><br/>van"
},
"LIST": {
"INBOX": "Inbox",
"OUTBOX": "Verzonden",
"TITLE": "Privé",
"POPOVER_ACTIONS": {
"TITLE": "Opties",
"DELETE_ALL": "Alle berichten wissen"
}
},
"COMPOSE": {
"TITLE": "Nieuw bericht",
"TITLE_REPLY": "Antwoord",
"SUB_TITLE": "Nieuw bericht",
"TO": "Aan",
"OBJECT": "Onderwerp",
"OBJECT_HELP": "Onderwerp",
"ENCRYPTED_HELP": "Please note this message will by encrypt before sending zodat alleen de ontvanger het kan lezen en zeker kan zijn dat jij de auteur bent.",
"MESSAGE": "Bericht",
"MESSAGE_HELP": "Berichtinhoud",
"CONTENT_CONFIRMATION": "Geen berichtinhoud.<br/><br/>Weet je zeker dat je dit bericht wil verzenden?"
},
"VIEW": {
"TITLE": "Bericht",
"SENDER": "Verzonden door",
"RECIPIENT": "Verzonden aan",
"NO_CONTENT": "Leeg bericht"
},
"CONFIRM": {
"REMOVE": "Weet je zeker dat je <b>dit bericht wil wissen</b>?<br/><br/>Dit kan niet ongedaan gemaakt worden.",
"REMOVE_ALL": "Weet je zeker dat je <b>alle berichten wil wissen</b>?<br/><br/>Dit kan niet ongedaan gemaakt worden.",
"MARK_ALL_AS_READ": "Weet je zeker dat je <b>alle berichten als gelezen wil markeren</b>?"
},
"INFO": {
"MESSAGE_REMOVED": "Bericht succesvol gewist",
"All_MESSAGE_REMOVED": "Berichten succesvol gewist",
"MESSAGE_SENT": "Bericht verzonden"
},
"ERROR": {
"SEND_MSG_FAILED": "Fout tijdens verzending.",
"LOAD_MESSAGES_FAILED": "Kan berichten niet laden.",
"LOAD_MESSAGE_FAILED": "Kan bericht niet laden.",
"MESSAGE_NOT_READABLE": "Kan bericht niet lezen.",
"USER_NOT_RECIPIENT": "Je bent niet de geadresseerde van dit bericht: het kan niet gelezen worden.",
"NOT_AUTHENTICATED_MESSAGE": "De authenticiteit van het bericht is onduidelijk of de inhoud is gecorrumpeerd.",
"REMOVE_MESSAGE_FAILED": "Kan bericht niet wissen.",
"MESSAGE_CONTENT_TOO_LONG": "Waarde te land (max {{maxLength}} characters).",
"MARK_AS_READ_FAILED": "Kan bericht niet als gelezen markeren.",
"LOAD_NOTIFICATIONS_FAILED": "Kan niet alle berichtnotificaties laden.",
"REMOVE_All_MESSAGES_FAILED": "Kan niet alle berichten wissen.",
"MARK_ALL_AS_READ_FAILED": "Kan berichten niet als gelezen markeren."
}
},
"REGISTRY": {
"CATEGORY": "Hoofdactiviteit",
"GENERAL_DIVIDER": "Basisinformatie",
"LOCATION_DIVIDER": "Adres",
"SOCIAL_NETWORKS_DIVIDER": "Sociale media en website",
"TECHNICAL_DIVIDER": "Technische informatie",
"BTN_NEW": "Toevoegen",
"SEARCH": {
"TITLE": "Bedrijfsregister",
"TITLE_SMALL_DEVICE": "Bedrijfsregister",
"SEARCH_HELP": "Wie, Wat: kapper, Lili's restaurant, ...",
"BTN_ADD": "Nieuw",
"BTN_OPTIONS": "Geavanceerd zoeken",
"TYPE": "Soort organisatie",
"LOCATION": "Locatie",
"LOCATION_HELP": "Plaats",
"LAST_RECORDS": "Nieuwste referenties:",
"RESULTS": "Resultaten:"
},
"VIEW": {
"TITLE": "Register",
"CATEGORY": "Hoofdactiviteit:",
"LOCATION": "Adres:",
"MENU_TITLE": "Opties",
"POPOVER_SHARE_TITLE": "{{title}}",
"REMOVE_CONFIRMATION" : "Weet je zeker dat je deze referentie wil verwijderen?<br/><br/>Dit kan niet ongedaan worden gemaakt."
},
"TYPE": {
"TITLE": "Nieuwe referentie",
"SELECT_TYPE": "Soort organizatie:",
"ENUM": {
"SHOP": "Locale winkel",
"COMPANY": "Onderneming",
"ASSOCIATION": "Stichting",
"INSTITUTION": "Instituut"
}
},
"EDIT": {
"TITLE": "Bewerk",
"TITLE_NEW": "Nieuwe referentie",
"RECORD_TYPE":"Soort organizatie",
"RECORD_TITLE": "Naam",
"RECORD_TITLE_HELP": "Naam",
"RECORD_DESCRIPTION": "Beschrijving",
"RECORD_DESCRIPTION_HELP": "Omschrijf activiteit",
"RECORD_ADDRESS": "Adres",
"RECORD_ADDRESS_HELP": "Adres: straat, gebouw...",
"RECORD_CITY": "Plaats",
"RECORD_CITY_HELP": "Plaats",
"RECORD_SOCIAL_NETWORKS": "Sociale media en website",
"RECORD_PUBKEY": "Publieke sleutel",
"RECORD_PUBKEY_HELP": "Publieke sleutel om betalingen te ontvangen"
},
"ERROR": {
"LOAD_CATEGORY_FAILED": "Laden hoofdactiveiten mislukt",
"LOAD_RECORD_FAILED": "Laden datasheet mislukt",
"LOOKUP_RECORDS_FAILED": "Opzoeken datasheets is mislukt.",
"REMOVE_RECORD_FAILED": "Verwijderen datasheet mislukt",
"SAVE_RECORD_FAILED": "Opslaan datasheet mislukt",
"RECORD_NOT_EXISTS": "Datasheet niet gevonden"
},
"INFO": {
"RECORD_REMOVED" : "Datasheet succesvol verwijderd"
}
},
"PROFILE": {
"UID": "Pseudoniem",
"TITLE": "Naam",
"TITLE_HELP": "Naam",
"DESCRIPTION": "Over mij",
"DESCRIPTION_HELP": "Over mij...",
"ADDRESS": "Adres",
"ADDRESS_HELP": "Adres (optioneel)",
"CITY": "Plaats",
"CITY_HELP": "Plaats (optioneel)",
"SOCIAL_HELP": "http://...",
"GENERAL_DIVIDER": "Algemene informatie",
"LOCATION_DIVIDER": "Localisatie",
"SOCIAL_NETWORKS_DIVIDER": "Sociale media en website",
"TECHNICAL_DIVIDER": "Technische informatie",
"ERROR": {
"LOAD_PROFILE_FAILED": "Kon gebruikersprofiel niet laden.",
"SAVE_PROFILE_FAILED": "Opslaan profiel mislukt",
"INVALID_SOCIAL_NETWORK_FORMAT": "Ongeldig formaat: vul een geldig internetadres in.<br/><br/>Voorbeelden:<ul><li>- Een Facebookpagina (https://www.facebook.com/user)</li><li>- Een webpagina (http://www.domain.com)</li><li>- Een emailadres (joe@dalton.com)</li></ul>",
"IMAGE_RESIZE_FAILED": "Fout tijdens afbeelding schalen"
},
"INFO": {
"PROFILE_SAVED": "Profiel opgeslagen"
},
"HELP": {
"WARNING_PUBLIC_DATA": "Let op, de informatie die hier is vastgelegd <b>is publiek</b>: zichtbaar ook voor <b>niet ingelogde gebruikers</b>."
}
},
"ES_SETTINGS": {
"PLUGIN_NAME": "Cesium+",
"ENABLE_TOGGLE": "Uitbreiding inschakelen?",
"ENABLE_MESSAGE_TOGGLE": "Berichten inschakelen?",
"ENABLE_SETTINGS_TOGGLE": "Globale opslag voor instellingen inschakelen?",
"PEER": "Adres dataknooppunt",
"POPUP_PEER": {
"TITLE" : "Dataknoop",
"HELP" : "Stel het te gebruiken adres in:",
"PEER_HELP": "server.domein.com:poort"
},
"NOTIFICATIONS": {
"DIVIDER": "Notificaties",
"HELP_TEXT": "Schakel het type notificatie dat je wil ontvangen in:",
"ENABLE_TX_SENT": "Bericht bij validatie van <b>verzonden betalingen</b>?",
"ENABLE_TX_RECEIVED": "Bericht bij validatie van <b>ontvangen betalingen</b>?",
"ENABLE_CERT_SENT": "Bericht bij validatie van <b>verzonden certificaties</b>?",
"ENABLE_CERT_RECEIVED": "Bericht bij validatie van <b>ontvangen certificaties</b>?"
},
"CONFIRM": {
"ASK_ENABLE_TITLE": "Nieuwe functies",
"ASK_ENABLE": "Er zijn nieuwe functies beschikbaar: <ul><li>&nbsp;&nbsp;<b><i class=\"icon ion-person\"></i> Profile Cesium+</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-android-notifcaitions\"></i> Notifications</b>;<li>&nbsp;&nbsp;<b><i class=\"icon ion-email\"></i> Privé berichten</b>.</ul><br/>Deze zijn <b>uitgeschakeld</b> in je instellingen.<br/><br/>Wil je deze functies <b>inschakelen</b>?"
}
},
"EVENT": {
"NODE_STARTED": "Je knoop ES API <b>{{params[0]}}</b> is UP",
"NODE_BMA_DOWN": "Knooppunt <b>{{params[0]}}:{{params[1]}}</b> (gebruikt door je ES API) is <b>onbereikbaar</b>.",
"NODE_BMA_UP": "Knooppunt <b>{{p0}}:{{params[1]}}</b> is weer onbereikbaar.",
"MEMBER_JOIN": "Je bent nu <b>lid</b> van valuta <b>{{params[0]}}</b>!",
"MEMBER_LEAVE": "Je bent <b>geen lid meer</b> van valuta <b>{{params[0]}}</b>!",
"MEMBER_ACTIVE": "Je lidmaatschap bij <b>{{params[0]}}</b> is met <b>succes verlengd</b>.",
"TX_SENT": "Je <b>betaling</b> aan <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> is uitgevoerd.",
"TX_SENT_MULTI": "Je <b>betaling</b> aan <b>{{params[1]}}</b> is uitgevoerd.",
"TX_RECEIVED": "Je hebt een <b>betaling ontvangen</b> van <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"TX_RECEIVED_MULTI": "Je hebt een <b>betaling ontvangen</b> van <b>{{params[1]}}</b>.",
"CERT_SENT": "Je <b>certificatie</b> van <span class=\"positive\" ><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> is uitgevoerd.",
"CERT_RECEIVED": "Je hebt een <b>certificatie ontvangen</b> van <span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span>.",
"REGISTRY": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> heeft gereageerd op jouw referentie: <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> heeft zijn/aar reactie op jouw referentie bewerkt: <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> hheeft gereageerd op jouw commentaar op referentie: <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> heeft zijn/haar reactie op jouw commentaar bewerkt, op referentie: <b>{{params[2]}}</b>"
}
},
"ERROR": {
}
}
);
$translateProvider.translations("en-GB", {
"MAP": {
"COMMON": {
"SEARCH_DOTS": "Search..."
},
"NETWORK": {
"LOOKUP": {
"BTN_MAP": "Peers map",
"BTN_MAP_HELP": "Open peers map"
},
"VIEW": {
"TITLE": "Peers map",
"LAYER": {
"MEMBER": "Member peers",
"MIRROR": "Mirror peers",
"OFFLINE": "Offline peers"
}
}
},
"WOT": {
"LOOKUP": {
"BTN_MAP": "Members map",
"BTN_MAP_HELP": "Open members map"
},
"VIEW": {
"TITLE": "Members map",
"LAYER": {
"MEMBER": "Members",
"PENDING": "Pending registrations",
"WALLET": "Simple wallets"
}
}
},
"PROFILE": {
"MARKER_HELP": "<b>Drag and drop</b> this marker to <b>update<br/>your position</b>, or use the buttons<br/>on top of the map."
},
"ERROR": {
"LOCALIZE_ME_FAILED": "Unable to retrieve your current position"
}
}
}
);
$translateProvider.translations("en", {
"MAP": {
"COMMON": {
"SEARCH_DOTS": "Search..."
},
"NETWORK": {
"LOOKUP": {
"BTN_MAP": "Peers map",
"BTN_MAP_HELP": "Open peers map"
},
"VIEW": {
"TITLE": "Peers map",
"LAYER": {
"MEMBER": "Member peers",
"MIRROR": "Mirror peers",
"OFFLINE": "Offline peers"
}
}
},
"WOT": {
"LOOKUP": {
"BTN_MAP": "Members map",
"BTN_MAP_HELP": "Open members map"
},
"VIEW": {
"TITLE": "Members map",
"LAYER": {
"MEMBER": "Members",
"PENDING": "Pending registrations",
"WALLET": "Simple wallets"
}
}
},
"PROFILE": {
"MARKER_HELP": "<b>Drag and drop</b> this marker to <b>update<br/>your position</b>, or use the buttons<br/>on top of the map."
},
"ERROR": {
"LOCALIZE_ME_FAILED": "Unable to retrieve your current position"
}
}
}
);
$translateProvider.translations("eo-EO", {
"MAP": {
"COMMON": {
"SEARCH_DOTS": "Traserĉi...",
"BTN_LOCALIZE_ME": "Lokalizi min"
},
"NETWORK": {
"LOOKUP": {
"BTN_MAP": "Mapo",
"BTN_MAP_HELP": "Malfermi la mapon pri nodoj"
},
"VIEW": {
"TITLE": "Mapo pri nodoj",
"LAYER": {
"MEMBER": "Membro-nodoj",
"MIRROR": "Spegul-nodoj",
"OFFLINE": "Nekonektitaj nodoj"
}
}
},
"WOT": {
"LOOKUP": {
"BTN_MAP": "Mapo",
"BTN_MAP_HELP": "Malfermi la mapon pri membroj"
},
"VIEW": {
"TITLE": "Mapo pri membroj",
"LAYER": {
"MEMBER": "Membroj",
"PENDING": "Aliĝoj atendantaj",
"WALLET": "Simplaj monujoj"
}
}
},
"PROFILE": {
"MARKER_HELP": "<b>Ŝovu-demetu</b> tiun ĉi markilon por <b>aktualigi<br/> vian lokon</b> sur la mapo, aŭ uzu la serĉo-butonon<br/>super la mapo."
},
"ERROR": {
"LOCALIZE_ME_FAILED": "Neeblas ricevi vian nunan lokon"
}
}
}
);
$translateProvider.translations("fr-FR", {
"MAP": {
"COMMON": {
"SEARCH_DOTS": "Rechercher...",
"BTN_LOCALIZE_ME": "Me localiser"
},
"NETWORK": {
"LOOKUP": {
"BTN_MAP": "Carte",
"BTN_MAP_HELP": "Ouvrir la carte des noeuds"
},
"VIEW": {
"TITLE": "Carte des noeuds",
"LAYER": {
"MEMBER": "Nœuds membre",
"MIRROR": "Nœuds miroir",
"OFFLINE": "Nœuds hors ligne"
}
}
},
"WOT": {
"LOOKUP": {
"BTN_MAP": "Carte",
"BTN_MAP_HELP": "Ouvrir la carte des membres"
},
"VIEW": {
"TITLE": "Carte des membres",
"LAYER": {
"MEMBER": "Membres",
"PENDING": "Inscriptions en attente",
"WALLET": "Simples portefeuilles"
}
}
},
"PROFILE": {
"MARKER_HELP": "<b>Glissez-déposez</b> ce marqueur pour <b>mettre<br/>à jour votre position</b>, ou utilisez les boutons<br/>au dessus de la carte."
},
"ERROR": {
"LOCALIZE_ME_FAILED": "Impossible de récupérer votre position actuelle"
}
}
}
);
$translateProvider.translations("en-GB", {
"NETWORK": {
"VIEW": {
"BTN_GRAPH": "Statistics"
}
},
"GRAPH": {
"COMMON": {
"LINEAR_SCALE" : "Linear scale",
"LOGARITHMIC_SCALE" : "Logarithmic scale",
"BTN_SHOW_STATS": "See statistics",
"BTN_SHOW_DETAILED_STATS": "Detailed statistics",
"RANGE_DURATION_DIVIDER": "Step unit:",
"RANGE_DURATION": {
"HOUR": "Group by <b>hour</b>",
"DAY": "Group by <b>day</b>",
"MONTH": "Group by <b>month</b>"
}
},
"PEER": {
"VIEW": {
"BLOCK_COUNT_LABEL": "Computed blocks count",
"BLOCK_COUNT": "{{count}} blocks",
"NO_BLOCK": "No block"
}
},
"DOC_STATS": {
"TITLE": "Data storage statistics",
"MARKET": {
"TITLE": "Number of Ads",
"AD": "Ads",
"COMMENT": "Comments"
},
"USER": {
"TITLE": "Number of documents linked to an account",
"USER_PROFILE": "User profiles",
"USER_SETTINGS": "Saved settings"
},
"MESSAGE": {
"TITLE": "Number of documents related to the communication",
"MESSAGE_INBOX": "Messages in inbox",
"MESSAGE_OUTBOX": "Messages in outbox",
"INVITATION_CERTIFICATION": "Invitations to certify"
},
"SOCIAL": {
"TITLE": "Number of page or group",
"PAGE_COMMENT": "Comments",
"PAGE_RECORD": "Pages",
"GROUP_RECORD": "Groups"
},
"SUBSCRIPTION": {
"TITLE": "Number of online subscriptions",
"EMAIL": "Email notifications"
},
"OTHER": {
"TITLE": "Other documents",
"HISTORY_DELETE": "Deletion of documents"
}
}
}
}
);
$translateProvider.translations("en", {
"NETWORK": {
"VIEW": {
"BTN_GRAPH": "Statistics"
}
},
"GRAPH": {
"COMMON": {
"LINEAR_SCALE" : "Linear scale",
"LOGARITHMIC_SCALE" : "Logarithmic scale",
"BTN_SHOW_STATS": "See statistics",
"BTN_SHOW_DETAILED_STATS": "Detailed statistics",
"RANGE_DURATION_DIVIDER": "Step unit:",
"RANGE_DURATION": {
"HOUR": "Group by <b>hour</b>",
"DAY": "Group by <b>day</b>",
"MONTH": "Group by <b>month</b>"
}
},
"PEER": {
"VIEW": {
"BLOCK_COUNT_LABEL": "Computed blocks count",
"BLOCK_COUNT": "{{count}} blocks",
"NO_BLOCK": "No block"
}
},
"DOC_STATS": {
"TITLE": "Data storage statistics",
"MARKET": {
"TITLE": "Number of Ads",
"AD": "Ads",
"COMMENT": "Comments"
},
"USER": {
"TITLE": "Number of documents linked to an account",
"USER_PROFILE": "User profiles",
"USER_SETTINGS": "Saved settings"
},
"MESSAGE": {
"TITLE": "Number of documents related to the communication",
"MESSAGE_INBOX": "Messages in inbox",
"MESSAGE_OUTBOX": "Messages in outbox",
"INVITATION_CERTIFICATION": "Invitations to certify"
},
"SOCIAL": {
"TITLE": "Number of page or group",
"PAGE_COMMENT": "Comments",
"PAGE_RECORD": "Pages",
"GROUP_RECORD": "Groups"
},
"SUBSCRIPTION": {
"TITLE": "Number of online subscriptions",
"EMAIL": "Email notifications"
},
"OTHER": {
"TITLE": "Other documents",
"HISTORY_DELETE": "Deletion of documents"
}
}
}
}
);
$translateProvider.translations("eo-EO", {
"NETWORK": {
"VIEW": {
"BTN_GRAPH": "Statistikoj"
}
},
"GRAPH": {
"COMMON": {
"LINEAR_SCALE" : "Lineara skalo",
"LOGARITHMIC_SCALE" : "Logaritma skalo",
"BTN_SHOW_STATS": "Vidi la statistikojn",
"BTN_SHOW_DETAILED_STATS": "Detalaj statistikoj",
"RANGE_DURATION_DIVIDER": "Tempo-unuo:",
"RANGE_DURATION": {
"HOUR": "Horo",
"DAY": "Tago",
"MONTH": "Monato"
}
},
"PEER": {
"VIEW": {
"BLOCK_COUNT_LABEL": "Nombro de blokoj kalkulitaj",
"BLOCK_COUNT": "{{count}} blokoj",
"NO_BLOCK": "Neniu bloko"
}
},
"DOC_STATS": {
"TITLE": "Statistikoj pri stokado",
"MARKET": {
"TITLE": "Nombro de anoncoj",
"AD": "Anoncoj",
"COMMENT": "Komentoj"
},
"MARKET_DELTA": {
"TITLE": "Variado de la nombro de anoncoj",
"AD": "Anoncoj",
"COMMENT": "Komentoj"
},
"USER": {
"TITLE": "Nombro de dokumentoj ligitaj al konto",
"USER_PROFILE": "Uzanto-profiloj",
"USER_SETTINGS": "Parametroj konservitaj"
},
"USER_DELTA": {
"TITLE": "Variado de la nombro de dokumentoj ligitaj al konto",
"USER_PROFILE": "Uzanto-profiloj",
"USER_SETTINGS": "Parametroj konservitaj"
},
"MESSAGE": {
"TITLE": "Nombro de dokumentoj ligitaj al komunikado",
"MESSAGE_INBOX": "Mesaĝoj en ricevujo",
"MESSAGE_OUTBOX": "Senditaj mesaĝoj konservitaj",
"INVITATION_CERTIFICATION": "Invitoj atestotaj"
},
"SOCIAL": {
"TITLE": "Nombro de paĝoj aŭ grupoj",
"PAGE_COMMENT": "Komentoj",
"PAGE_RECORD": "Paĝoj",
"GROUP_RECORD": "Grupoj"
},
"SUBSCRIPTION": {
"TITLE": "Nombro de abonoj",
"EMAIL": "Avizoj per retmesaĝoj"
},
"OTHER": {
"TITLE": "Aliaj dokumentoj",
"HISTORY_DELETE": "Forigoj de dokumentoj"
}
},
"SYNCHRO": {
"TITLE": "Statistikoj pri sinkronigoj",
"COUNT": {
"TITLE": "Kvanto sinkronigita",
"INSERTS": "Enmetoj",
"UPDATES": "Ĝisdatigoj",
"DELETES": "Forigoj"
},
"PEER": {
"TITLE": "Nodoj informpetitaj",
"ES_USER_API": "Nodoj pri datenoj de uzantoj",
"ES_SUBSCRIPTION_API": "Nodoj pri retaj servoj"
},
"PERFORMANCE": {
"TITLE": "Efikecoj pri efektiviĝo",
"DURATION": "Tempo por efektiviĝo (ms)"
}
}
}
}
);
$translateProvider.translations("es-ES", {
"NETWORK": {
"VIEW": {
"BTN_GRAPH": "Estadística"
}
},
"GRAPH": {
"COMMON": {
"LINEAR_SCALE" : "Escala lineal",
"LOGARITHMIC_SCALE" : "Escala logarítmica",
"BTN_SHOW_STATS": "Ver estadísticas",
"BTN_SHOW_DETAILED_STATS": "Estadísticas detalladas",
"RANGE_DURATION_DIVIDER": "Unidad de paso:",
"RANGE_DURATION": {
"HOUR": "Grupo por <b>hora</b>",
"DAY": "Grupo por <b>día</b>",
"MONTH": "Grupo por <b>mes</b>"
}
},
"PEER": {
"VIEW": {
"BLOCK_COUNT_LABEL": "Número de bloques calculados",
"BLOCK_COUNT": "{{count}} bloques",
"NO_BLOCK": "Ningún bloque"
}
}
}
}
);
$translateProvider.translations("fr-FR", {
"NETWORK": {
"VIEW": {
"BTN_GRAPH": "Statistiques"
}
},
"GRAPH": {
"COMMON": {
"LINEAR_SCALE" : "Echelle linéaire",
"LOGARITHMIC_SCALE" : "Echelle logarithmique",
"BTN_SHOW_STATS": "Voir les statistiques",
"BTN_SHOW_DETAILED_STATS": "Statistiques détaillées",
"RANGE_DURATION_DIVIDER": "Unité de temps :",
"RANGE_DURATION": {
"HOUR": "Heure",
"DAY": "Jour",
"MONTH": "Mois"
}
},
"PEER": {
"VIEW": {
"BLOCK_COUNT_LABEL": "Nombre de blocs calculés",
"BLOCK_COUNT": "{{count}} blocs",
"NO_BLOCK": "Aucun bloc"
}
},
"DOC_STATS": {
"TITLE": "Statistiques de stockage",
"MARKET": {
"TITLE": "Nombre d'annonces",
"AD": "Annonces",
"COMMENT": "Commentaires"
},
"MARKET_DELTA": {
"TITLE": "Variation du nombre d'annonces",
"AD": "Annonces",
"COMMENT": "Commentaires"
},
"USER": {
"TITLE": "Nombre de documents liés à un compte",
"USER_PROFILE": "Profils utilisateur",
"USER_SETTINGS": "Paramètres sauvegardés"
},
"USER_DELTA": {
"TITLE": "Variation du nombre de documents liés à un compte",
"USER_PROFILE": "Profils utilisateur",
"USER_SETTINGS": "Paramètres sauvegardés"
},
"MESSAGE": {
"TITLE": "Nombre de documents liés à la communication",
"MESSAGE_INBOX": "Messages en boîte de réception",
"MESSAGE_OUTBOX": "Messages envoyés sauvegardés",
"INVITATION_CERTIFICATION": "Invitations à certifier"
},
"SOCIAL": {
"TITLE": "Nombre de pages ou groupes",
"PAGE_COMMENT": "Commentaires",
"PAGE_RECORD": "Pages",
"GROUP_RECORD": "Groupes"
},
"SUBSCRIPTION": {
"TITLE": "Nombre d'abonnements",
"EMAIL": "Notifications emails"
},
"OTHER": {
"TITLE": "Autres documents",
"HISTORY_DELETE": "Suppressions de documents"
}
},
"SYNCHRO": {
"TITLE": "Statistiques de synchronisations",
"COUNT": {
"TITLE": "Volume synchronisé",
"INSERTS": "Insertions",
"UPDATES": "Mises à jour",
"DELETES": "Suppressions"
},
"PEER": {
"TITLE": "Noeuds requêtés",
"ES_USER_API": "Noeuds données utilisateurs",
"ES_SUBSCRIPTION_API": "Noeuds services en ligne"
},
"PERFORMANCE": {
"TITLE": "Performances d'exécution",
"DURATION": "Temps d'exécution (ms)"
}
}
}
}
);
$translateProvider.translations("it-IT", {
"NETWORK": {
"VIEW": {
"BTN_GRAPH": "Statistiche"
}
},
"GRAPH": {
"COMMON": {
"LINEAR_SCALE" : "Scala lineare",
"LOGARITHMIC_SCALE" : "Scala logaritmica",
"BTN_SHOW_STATS": "Vedere le statistiche",
"BTN_SHOW_DETAILED_STATS": "Statistiche dettagliate",
"RANGE_DURATION_DIVIDER": "Unità di tempo :",
"RANGE_DURATION": {
"HOUR": "Ora",
"DAY": "Giorno",
"MONTH": "Mese"
}
},
"PEER": {
"VIEW": {
"BLOCK_COUNT_LABEL": "Numero di blocchi calcolati",
"BLOCK_COUNT": "{{count}} blocchi",
"NO_BLOCK": "Nessun blocco"
}
},
"DOC_STATS": {
"TITLE": "Statistiche di stoccaggio",
"USER": {
"TITLE": "Numero di documenti legati ad un conto",
"USER_PROFILE": "Profili dell'utente",
"USER_SETTINGS": "Impostazioni salvate",
},
"MESSAGE": {
"TITLE": "Numero di documenti legati alla conversazione",
"MESSAGE_INBOX": "Messaggi in arrivo",
"MESSAGE_OUTBOX": "Messaggi inviati salvati",
"INVITATION_CERTIFICATION": "Invitazioni da certificare"
},
"SOCIAL": {
"TITLE": "Numero di pagine o gruppi",
"PAGE_COMMENT": "Commenti",
"PAGE_RECORD": "Pagine",
"GROUP_RECORD": "Gruppi",
},
"OTHER": {
"TITLE": "Altri documenti",
"HISTORY_DELETE": "Cronologia eliminazione documenti",
}
},
"SYNCHRO": {
"TITLE": "Statistiche di sincronizzazioni",
"COUNT": {
"TITLE": "Volume sincronizzato",
"INSERTS": "Inserimenti",
"UPDATES": "Aggiornamenti",
"DELETES": "Eliminazioni"
},
"PEER": {
"TITLE": "Nodi interrogati",
"ES_USER_API": "Nodi dati utenti",
"ES_SUBSCRIPTION_API": "Noeuds servizi online"
},
"PERFORMANCE": {
"TITLE": "Prestazioni (performance) di esecuzione",
"DURATION": "Tempo di esecuzione (ms)"
}
}
}
}
);
$translateProvider.translations("nl-NL", {
"NETWORK": {
"VIEW": {
"BTN_GRAPH": "Statistieken"
}
},
"GRAPH": {
"COMMON": {
"LINEAR_SCALE" : "Lineaire schaal",
"LOGARITHMIC_SCALE" : "Logaritmische schaal",
"BTN_SHOW_STATS": "Zie statistieken",
"BTN_SHOW_DETAILED_STATS": "Gedetailleerde statistieken",
"RANGE_DURATION_DIVIDER": "Stap eenheid:",
"RANGE_DURATION": {
"HOUR": "Groep per <b>uur</b>",
"DAY": "Groep per <b>dag</b>",
"MONTH": "Groep per <b>maand</b>"
}
}
}
}
);
$translateProvider.translations("en-GB", {
"MENU": {
"MARKET": "Ads",
"MY_RECORDS": "My ads"
},
"MARKET": {
"COMMON": {
"PRICE": "Price",
"BTN_NEW_AD": "New ad",
"SOLD": "Close ad",
"LAST_UPDATE": "Last update",
"AROUND_ME": "Around me"
},
"JOIN": {
"PROFILE": {
"WARNING": "You now have to complete your user profile.<br/><br/>This is <b>public information</b>, accessible to everyone.",
"TITLE": "Lastname, Firstname",
"TITLE_HELP": "Lastname, Firstname or pseudonym",
"DESCRIPTION": "Abut me",
"DESCRIPTION_HELP": "Say something about you..."
},
"SUBSCRIPTION": {
"EMAIL": "Email",
"EMAIL_HELP": "Email (optional)"
},
"LAST_SLIDE_CONGRATULATION": "You have entered all necessary information: Congratulations!<br/>You can now <b>send the creation request </b>.<br/><br/>For information, the public key below will identify your future account:",
"CONFIRMATION_WALLET_ACCOUNT": "<b class=\"assertive\">Warning:</b> The identifier and the password can not be changed.<br/><br/>You should <b>always remember it!</b><br/><br/><b>Are you sure</b> you want to continue with these credentials?"
},
"PROFILE": {
"DEFAULT_TITLE": "User {{pubkey|formatPubkey}}"
},
"LOGIN": {
"HELP": "Please fill your account credentials:",
"REMEMBER_ME": "Remember me?"
},
"EVENT_LOGIN": {
"TITLE": "Contact information",
"HELP": "Please indicate a <b>email or phone number</b>, so that we can contact you during the event:",
"EMAIL_OR_PHONE": "Email or phone number",
"EMAIL_OR_PHONE_HELP": "Email or phone number",
"REMEMBER_ME": "Remember me?",
"ERROR": {
"INVALID_USERNAME": "Email or phone number invalid"
}
},
"HOME": {
"BTN_NEW_AD": "Place an ad",
"BTN_SHOW_MARKET_OFFER": "Explore Ads",
"LOCATION_LABEL": "Find ads near by:",
"LOCATION_HELP": "City, Country",
"ERROR": {
"GEO_LOCATION_NOT_FOUND": "City or zip code not found"
}
},
"CATEGORY": {
"ALL": "All categories"
},
"SEARCH": {
"TITLE": "Market",
"CATEGORY": "Category: ",
"SEARCH_HELP": "What, Where: car, Columbia city, ...",
"BY": "by",
"BTN_ADD": "New",
"BTN_OPTIONS": "Advanced search",
"BTN_AROUND_ME": "Around me",
"GEO_DISTANCE": "Maximum distance around the city",
"GEO_DISTANCE_OPTION": "{{value}} km",
"SHOW_MORE": "Show more",
"SHOW_MORE_COUNT": "(current limit to {{limit}})",
"LOCATION": "Location",
"LOCATION_HELP": "City",
"RESULTS": "Results",
"RESULT_COUNT_LOCATION": "{{count}} result{{count>0?'s':''}}, near {{location}}",
"RESULT_COUNT": "{{count}} result{{count>0?'s':''}}",
"LAST_RECORDS": "Recent ads:",
"LAST_RECORD_COUNT_LOCATION": "{{count}} recent ad{{count>0?'s':''}}, near {{location}}",
"LAST_RECORD_COUNT": "{{count}} recent ad{{count>0?'s':''}}",
"BTN_LAST_RECORDS": "Recent ads",
"BTN_SHOW_CATEGORIES": "Show categories",
"BTN_OFFERS": "Offers",
"BTN_NEEDS": "Needs",
"SHOW_CLOSED_RECORD": "Display closed ads?",
"SHOW_OLD_RECORD": "Display old ads?",
"RECORD_STOCK": "Stock:"
},
"GALLERY": {
"TITLE": "Slideshow",
"BTN_START": "Start",
"BTN_CONTINUE": "Resume",
"BTN_PAUSE": "Pause",
"BTN_STOP": "Stop",
"SLIDE_DURATION": "Display time:",
"SLIDE_DURATION_OPTION": "{{value}} seconds"
},
"VIEW": {
"TITLE": "Ad",
"BTN_SOLD_AD": "Close the ad",
"BTN_SOLD": "Close",
"BTN_REOPEN": "Reopen the ad",
"BTN_WRITE_OFFER": "Write to the seller",
"BTN_WRITE_NEED": "Write to the applicant",
"BTN_FOLLOW": "Follow this ad",
"BTN_STOP_FOLLOW": "Stop following this ad",
"MENU_TITLE": "Options",
"RECORD_FEES_PARENTHESIS": "(fees)",
"RECORD_STOCK": "Available stock:",
"POPOVER_SHARE_TITLE": "Ad {{title}}",
"REMOVE_CONFIRMATION": "Are you sure you want to delete this ad?<br/><br/> This is irreversible.",
"SOLD_CONFIRMATION" : "<b>Are you sure</b> you want to close this ad?",
"REOPEN_CONFIRMATION" : "<b>Are you sure</b> you want to repoen this ad?",
"NEW_MESSAGE_TITLE": "About your ad \"{{title}}\"...",
"MORE_LIKE_THIS": "This might interest you:"
},
"WALLET": {
"DUNITER_PUBKEY": "Public key to receive payments",
"DUNITER_ACCOUNT": "Receipt of payments in {{currency|currencySymbol}}",
"DUNITER_ACCOUNT_HELP": "The public key (above) indicates the destination of the payments you will receive.",
"DUNITER_ACCOUNT_NO_PUBKEY_HELP": "No public account key has been entered. You will need to give it to prospective buyers.<br/>You can enter it at any time, <b>by editing your profile</b>."
},
"TYPE": {
"TITLE": "New ad",
"SELECT_TYPE": "Kind of ad:",
"OFFER": "Offer",
"OFFER_SHORT": "Offer",
"NEED": "Need",
"NEED_SHORT": "Need"
},
"LOCAL_SALE": {
"LOCATION": "Stand number",
"LOCATION_HELP": "Stand number: 1, 2, ...",
"LOCATION_PREFIX": "Stand #"
},
"EDIT": {
"TITLE": "Edit",
"TITLE_NEW": "New ad",
"RECORD_TITLE": "Title",
"RECORD_TITLE_HELP": "Title",
"RECORD_DESCRIPTION": "Description",
"RECORD_DESCRIPTION_HELP": "Description",
"RECORD_LOCATION": "Address",
"RECORD_LOCATION_HELP": "City, Country",
"RECORD_PRICE": "Price",
"RECORD_PRICE_HELP": "Price (optional)",
"RECORD_CURRENCY": "Currency",
"RECORD_FEES": "Fees",
"RECORD_FEES_HELP": "Fees (optional)",
"RECORD_STOCK": "Available stock",
"RECORD_STOCK_HELP": "Available stock"
},
"WOT": {
"VIEW": {
"STARS": "Trust level",
"STAR_HIT_COUNT": "{{total}} rate{{total>1 ? 's' : ''}}",
"BTN_RECORDS": "Ads",
"BTN_STAR_HELP": "Rate this profile",
"BTN_REDO_STAR_HELP": "Update your rate for this profile",
"DUNITER_PUBKEY": "Public key to receive payments",
"DUNITER_ACCOUNT": "Receipt of payments in {{currency|currencySymbol}}",
"DUNITER_ACCOUNT_HELP": "The public key (above) is the account to be used to pay for this user.",
"DUNITER_ACCOUNT_HELP_ASK_USER": "This user has not communicated their account public key. <a ng-click=\"showNewMessageModal()\">Contact him</a> to get it."
},
"ERROR": {
"FAILED_STAR_PROFILE": "Error sending your rate. Please try again later."
}
},
"ERROR": {
"INVALID_LOGIN_CREDENTIALS": "Invalid credentials.<br/>Please try again.",
"FAILED_SAVE_RECORD": "Saving ad failed",
"FAILED_UPDATE_RECORD": "Updating Ad failed",
"LOAD_CATEGORY_FAILED": "Loading categories failed",
"LOOKUP_RECORDS_FAILED": "Error while loading records.",
"LOAD_RECORD_FAILED": "Loading ad failed",
"REMOVE_RECORD_FAILED": "Deleting ad failed",
"SOLD_RECORD_FAILED": "Error while closing the ad",
"REOPEN_RECORD_FAILED": "Error while reopening the ad",
"FAILED_SAVE_COMMENT": "Saving comment failed",
"FAILED_REMOVE_COMMENT": "Deleting comment failed",
"RECORD_NOT_EXISTS": "Ad not found",
"RECORD_EXCEED_UPLOAD_SIZE": "It seems that your <b> ad is too big </ b> to be accepted by the data node.<br/><br/>You can delete <b>delete photos</b> again.",
"GEO_LOCATION_NOT_FOUND": "City or zip code not found"
},
"INFO": {
"RECORD_REMOVED": "Ad successfully deleted",
"RECORD_SOLD" : "Ad closed",
"RECORD_REOPEN" : "Ad reopen"
}
},
"EVENT": {
"MARKET": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has commented on your ad: <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his/her comment on your ad: <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has replied to your comment on the ad: <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his/her answer to your comment on the ad: <b>{{params[2]}}</b>",
"FOLLOW_NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has commented on the ad: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his/her comment on the ad: <b>{{params[2]}}</b>",
"FOLLOW_NEW": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> added the ad: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> updated the ad: <b>{{params[2]}}</b>",
"FOLLOW_CLOSE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> sold the ad: <b>{{params[2]}}</b>",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> asks for moderation on the ad: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> posted an ad to moderate: <b>{{params[2]}}</b>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span>reported abuse on your ad: <b>{{params[2]}}</b>",
"LIKE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> liked your ad: <b>{{params[2]}}</b>",
"FOLLOW_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> follows your ad: <b>{{params[2]}}</b>"
}
}
}
);
$translateProvider.translations("en", {
"MENU": {
"MARKET": "Ads",
"MY_RECORDS": "My ads"
},
"MARKET": {
"COMMON": {
"PRICE": "Price",
"BTN_NEW_AD": "New ad",
"SOLD": "Close ad",
"LAST_UPDATE": "Last update",
"AROUND_ME": "Around me"
},
"JOIN": {
"PROFILE": {
"WARNING": "You now have to complete your user profile.<br/><br/>This is <b>public information</b>, accessible to everyone.",
"TITLE": "Lastname, Firstname",
"TITLE_HELP": "Lastname, Firstname or pseudonym",
"DESCRIPTION": "Abut me",
"DESCRIPTION_HELP": "Say something about you..."
},
"SUBSCRIPTION": {
"EMAIL": "Email",
"EMAIL_HELP": "Email (optional)"
},
"LAST_SLIDE_CONGRATULATION": "You have entered all necessary information: Congratulations!<br/>You can now <b>send the creation request </b>.<br/><br/>For information, the public key below will identify your future account:",
"CONFIRMATION_WALLET_ACCOUNT": "<b class=\"assertive\">Warning:</b> The identifier and the password can not be changed.<br/><br/>You should <b>always remember it!</b><br/><br/><b>Are you sure</b> you want to continue with these credentials?"
},
"PROFILE": {
"DEFAULT_TITLE": "User {{pubkey|formatPubkey}}"
},
"LOGIN": {
"HELP": "Please fill your account credentials:",
"REMEMBER_ME": "Remember me?"
},
"EVENT_LOGIN": {
"TITLE": "Contact information",
"HELP": "Please indicate a <b>email or phone number</b>, so that we can contact you during the event:",
"EMAIL_OR_PHONE": "Email or phone number",
"EMAIL_OR_PHONE_HELP": "Email or phone number",
"REMEMBER_ME": "Remember me?",
"ERROR": {
"INVALID_USERNAME": "Email or phone number invalid"
}
},
"HOME": {
"BTN_NEW_AD": "Place an ad",
"BTN_SHOW_MARKET_OFFER": "Explore Ads",
"LOCATION_LABEL": "Find ads near by:",
"LOCATION_HELP": "City, Country",
"ERROR": {
"GEO_LOCATION_NOT_FOUND": "City or zip code not found"
}
},
"CATEGORY": {
"ALL": "All categories"
},
"SEARCH": {
"TITLE": "Market",
"CATEGORY": "Category: ",
"SEARCH_HELP": "What, Where: car, Columbia city, ...",
"BY": "by",
"BTN_ADD": "New",
"BTN_OPTIONS": "Advanced search",
"BTN_AROUND_ME": "Around me",
"GEO_DISTANCE": "Maximum distance around the city",
"GEO_DISTANCE_OPTION": "{{value}} km",
"SHOW_MORE": "Show more",
"SHOW_MORE_COUNT": "(current limit to {{limit}})",
"LOCATION": "Location",
"LOCATION_HELP": "City",
"RESULTS": "Results",
"RESULT_COUNT_LOCATION": "{{count}} result{{count>0?'s':''}}, near {{location}}",
"RESULT_COUNT": "{{count}} result{{count>0?'s':''}}",
"LAST_RECORDS": "Recent ads:",
"LAST_RECORD_COUNT_LOCATION": "{{count}} recent ad{{count>0?'s':''}}, near {{location}}",
"LAST_RECORD_COUNT": "{{count}} recent ad{{count>0?'s':''}}",
"BTN_LAST_RECORDS": "Recent ads",
"BTN_SHOW_CATEGORIES": "Show categories",
"BTN_OFFERS": "Offers",
"BTN_NEEDS": "Needs",
"SHOW_CLOSED_RECORD": "Display closed ads?",
"SHOW_OLD_RECORD": "Display old ads?",
"RECORD_STOCK": "Stock:"
},
"GALLERY": {
"TITLE": "Slideshow",
"BTN_START": "Start",
"BTN_CONTINUE": "Resume",
"BTN_PAUSE": "Pause",
"BTN_STOP": "Stop",
"SLIDE_DURATION": "Display time:",
"SLIDE_DURATION_OPTION": "{{value}} seconds"
},
"VIEW": {
"TITLE": "Ad",
"BTN_SOLD_AD": "Close the ad",
"BTN_SOLD": "Close",
"BTN_REOPEN": "Reopen the ad",
"BTN_WRITE_OFFER": "Write to the seller",
"BTN_WRITE_NEED": "Write to the applicant",
"BTN_FOLLOW": "Follow this ad",
"BTN_STOP_FOLLOW": "Stop following this ad",
"MENU_TITLE": "Options",
"RECORD_FEES_PARENTHESIS": "(fees)",
"RECORD_STOCK": "Available stock:",
"POPOVER_SHARE_TITLE": "Ad {{title}}",
"REMOVE_CONFIRMATION": "Are you sure you want to delete this ad?<br/><br/> This is irreversible.",
"SOLD_CONFIRMATION" : "<b>Are you sure</b> you want to close this ad?",
"REOPEN_CONFIRMATION" : "<b>Are you sure</b> you want to repoen this ad?",
"NEW_MESSAGE_TITLE": "About your ad \"{{title}}\"...",
"MORE_LIKE_THIS": "This might interest you:"
},
"WALLET": {
"DUNITER_PUBKEY": "Public key to receive payments",
"DUNITER_ACCOUNT": "Receipt of payments in {{currency|currencySymbol}}",
"DUNITER_ACCOUNT_HELP": "The public key (above) indicates the destination of the payments you will receive.",
"DUNITER_ACCOUNT_NO_PUBKEY_HELP": "No public account key has been entered. You will need to give it to prospective buyers.<br/>You can enter it at any time, <b>by editing your profile</b>."
},
"TYPE": {
"TITLE": "New ad",
"SELECT_TYPE": "Kind of ad:",
"OFFER": "Offer",
"OFFER_SHORT": "Offer",
"NEED": "Need",
"NEED_SHORT": "Need"
},
"LOCAL_SALE": {
"LOCATION": "Stand number",
"LOCATION_HELP": "Stand number: 1, 2, ...",
"LOCATION_PREFIX": "Stand #"
},
"EDIT": {
"TITLE": "Edit",
"TITLE_NEW": "New ad",
"RECORD_TITLE": "Title",
"RECORD_TITLE_HELP": "Title",
"RECORD_DESCRIPTION": "Description",
"RECORD_DESCRIPTION_HELP": "Description",
"RECORD_LOCATION": "Address",
"RECORD_LOCATION_HELP": "City, Country",
"RECORD_PRICE": "Price",
"RECORD_PRICE_HELP": "Price (optional)",
"RECORD_CURRENCY": "Currency",
"RECORD_FEES": "Fees",
"RECORD_FEES_HELP": "Fees (optional)",
"RECORD_STOCK": "Available stock",
"RECORD_STOCK_HELP": "Available stock"
},
"WOT": {
"VIEW": {
"BTN_RECORDS": "Ads",
"DUNITER_PUBKEY": "Public key to receive payments",
"DUNITER_ACCOUNT": "Receipt of payments in {{currency|currencySymbol}}",
"DUNITER_ACCOUNT_HELP": "The public key (above) is the account to be used to pay for this user.",
"DUNITER_ACCOUNT_HELP_ASK_USER": "This user has not communicated their account public key. <a ng-click=\"showNewMessageModal()\">Contact him</a> to get it."
},
"ERROR": {
"FAILED_STAR_PROFILE": "Error sending your rate. Please try again later."
}
},
"ERROR": {
"INVALID_LOGIN_CREDENTIALS": "Invalid credentials.<br/>Please try again.",
"FAILED_SAVE_RECORD": "Saving ad failed",
"FAILED_UPDATE_RECORD": "Updating Ad failed",
"LOAD_CATEGORY_FAILED": "Loading categories failed",
"LOOKUP_RECORDS_FAILED": "Error while loading records.",
"LOAD_RECORD_FAILED": "Loading ad failed",
"REMOVE_RECORD_FAILED": "Deleting ad failed",
"SOLD_RECORD_FAILED": "Error while closing the ad",
"REOPEN_RECORD_FAILED": "Error while reopening the ad",
"FAILED_SAVE_COMMENT": "Saving comment failed",
"FAILED_REMOVE_COMMENT": "Deleting comment failed",
"RECORD_NOT_EXISTS": "Ad not found",
"RECORD_EXCEED_UPLOAD_SIZE": "It seems that your <b> ad is too big </ b> to be accepted by the data node.<br/><br/>You can delete <b>delete photos</b> again.",
"GEO_LOCATION_NOT_FOUND": "City or zip code not found"
},
"INFO": {
"RECORD_REMOVED": "Ad successfully deleted",
"RECORD_SOLD" : "Ad closed",
"RECORD_REOPEN" : "Ad reopen"
}
},
"EVENT": {
"MARKET": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has commented on your ad: <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his/her comment on your ad: <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has replied to your comment on the ad: <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his/her answer to your comment on the ad: <b>{{params[2]}}</b>",
"FOLLOW_NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has commented on the ad: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> has modified his/her comment on the ad: <b>{{params[2]}}</b>",
"FOLLOW_NEW": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> added the ad: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> updated the ad: <b>{{params[2]}}</b>",
"FOLLOW_CLOSE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> sold the ad: <b>{{params[2]}}</b>",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> asks for moderation on the ad: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> posted an ad to moderate: <b>{{params[2]}}</b>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span>reported abuse on your ad: <b>{{params[2]}}</b>",
"LIKE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> liked your ad: <b>{{params[2]}}</b>",
"FOLLOW_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> follows your ad: <b>{{params[2]}}</b>"
}
}
}
);
$translateProvider.translations("eo-EO", {
"MENU": {
"MARKET": "Anoncoj",
"MY_RECORDS": "Miaj anoncoj"
},
"MARKET": {
"COMMON": {
"PRICE": "Prezo",
"BTN_NEW_AD": "Mi metas anoncon",
"SOLD": "Anonco fermita",
"LAST_UPDATE": "Lasta ĝisdatigo",
"AROUND_ME": "Ĉirkaŭ mi"
},
"JOIN": {
"PROFILE": {
"WARNING": "Nun restas al vi kompletigi vian uzant-profilon.<br/><br/>Temas pri <b>publikaj informoj</b>, alireblaj de ĉiuj.",
"TITLE": "Familia nomo, Persona nomo",
"TITLE_HELP": "Familia nomo, Persona nomo aŭ pseŭdonimo",
"DESCRIPTION": "Pri mi",
"DESCRIPTION_HELP": "Diru ion pri vi..."
},
"SUBSCRIPTION": {
"EMAIL": "Retadreso",
"EMAIL_HELP": "Retadreso (nedeviga)"
},
"LAST_SLIDE_CONGRATULATION": "Vi tajpis ĉiujn necesajn informojn: Gratulon!<br/>Vi nun povas <b>sendi la peton pri kreado</b> de konto.<br/><br/>Ne forgesu viajn identigilojn, vi ne povos ŝanĝi ilin!",
"CONFIRMATION_WALLET_ACCOUNT": "<b class=\"assertive\">Averto:</b> la identigilo kaj la pasvorto ne plu povos esti modifitaj.<br/><br/><b>Certiĝu, ke vi ĉiam rememoros ilin!</b><br/><br/><b>Ĉu vi certas</b>, ke vi volas daŭrigi per tiuj identigiloj?"
},
"PROFILE": {
"DEFAULT_TITLE": "Uzanto {{pubkey|formatPubkey}}"
},
"LOGIN": {
"HELP": "Bonvolu tajpi viajn identigilojn por konektiĝi:",
"REMEMBER_ME": "Memori min?"
},
"EVENT_LOGIN": {
"TITLE": "Kontakteblecoj",
"HELP": "Bonvolu indiki <b>retadreson aŭ poŝtelefon-numeron</b>, tiel ke la organizantoj povu kontakti vin dum la evento:",
"EMAIL_OR_PHONE": "Retadreso aŭ telefono",
"EMAIL_OR_PHONE_HELP": "Retadreso aŭ telefon-numero",
"REMEMBER_ME": "Memori min?",
"ERROR": {
"INVALID_USERNAME": "Retadreso aŭ telefon-numero nevalida"
}
},
"HOME": {
"BTN_NEW_AD": "Meti anoncon",
"BTN_SHOW_MARKET_OFFER": "Vidi la anoncojn",
"LOCATION_LABEL": "Serĉado de anoncoj laŭ urbo:",
"LOCATION_HELP": "Poŝt-kodo, Urbo",
"ERROR": {
"GEO_LOCATION_NOT_FOUND": "Urbo aŭ poŝt-kodo ne trovita"
}
},
"CATEGORY": {
"ALL": "Ĉiuj kategorioj"
},
"SEARCH": {
"TITLE": "Anoncoj",
"CATEGORY": "Kategorio: ",
"SEARCH_HELP": "Serĉado (biciklo, ŝuoj...)",
"BY": "de",
"BTN_ADD": "Nova",
"BTN_OPTIONS": "Detala serĉado",
"BTN_AROUND_ME": "Ĉirkaŭ mi",
"GEO_DISTANCE": "Maksimuma distanco ĉirkaŭ la urbo:",
"GEO_DISTANCE_OPTION": "{{value}} km",
"SHOW_MORE": "Afiŝi pli",
"SHOW_MORE_COUNT": "(nuna limo je {{limit}})",
"LOCATION": "Urbo",
"LOCATION_HELP": "Poŝt-kodo, Urbo",
"RESULTS": "Rezultoj",
"RESULT_COUNT_LOCATION": "{{count}} rezulto{{count>0?'j':''}}, proksime de {{location}}",
"RESULT_COUNT": "{{count}} rezulto{{count>0?'j':''}}",
"LAST_RECORDS": "Lastaj anoncoj",
"LAST_RECORD_COUNT_LOCATION": "{{count}} freŝdata{{count>0?'j':''}} anonco{{count>0?'j':''}}, proksime de {{location}}",
"LAST_RECORD_COUNT": "{{count}} freŝdata{{count>0?'j':''}} anonco{{count>0?'j':''}}",
"BTN_LAST_RECORDS": "Lastaj anoncoj",
"BTN_SHOW_CATEGORIES": "Trarigardi la kategoriojn",
"BTN_OFFERS": "Proponoj",
"BTN_NEEDS": "Petoj",
"SHOW_CLOSED_RECORD": "Afiŝi la fermitajn anoncojn?",
"SHOW_OLD_RECORD": "Afiŝi la malnovajn anoncojn?",
"RECORD_STOCK": "Stoko:"
},
"GALLERY": {
"TITLE": "Bildaro",
"BTN_START": "Komenci",
"BTN_CONTINUE": "Malpaŭzi",
"BTN_PAUSE": "Paŭzi",
"BTN_STOP": "Ĉesi",
"SLIDE_DURATION": "Afiŝo-daŭro:",
"SLIDE_DURATION_OPTION": "{{value}} sekundoj"
},
"VIEW": {
"TITLE": "Anonco",
"BTN_SOLD_AD": "Fermi la anoncon",
"BTN_SOLD": "Fermi",
"BTN_REOPEN": "Reaperigi la anoncon",
"BTN_WRITE_OFFER": "Skribi al la vendanto",
"BTN_WRITE_NEED": "Skribi al la petanto",
"BTN_FOLLOW": "Sekvi tiun ĉi anoncon",
"BTN_STOP_FOLLOW": "Ne plu sekvi tiun ĉi anoncon",
"MENU_TITLE": "Kromeblecoj",
"RECORD_FEES_PARENTHESIS": "(kostoj)",
"RECORD_STOCK": "Stoko disponebla:",
"POPOVER_SHARE_TITLE": "Anonco {{title}}",
"REMOVE_CONFIRMATION" : "Ĉu vi certas, ke vi volas forigi tiun ĉi anoncon?<br/><br/>Tiu ago estas neinversigebla.",
"SOLD_CONFIRMATION" : "<b>Ĉu vi certas</b>, ke vi volas fermi tiun ĉi anoncon?",
"REOPEN_CONFIRMATION" : "<b>Ĉu vi certas</b>, ke vi volas reaperigi tiun ĉi anoncon?",
"NEW_MESSAGE_TITLE": "Pri la anonco \"{{title}}\"...",
"MORE_LIKE_THIS": "Tio ĉi povus interesi vin:"
},
"WALLET": {
"DUNITER_PUBKEY": "Publika ŝlosilo por ricevi la pagojn",
"DUNITER_ACCOUNT": "Ricevo de la pagoj en {{currency|currencySymbol}}",
"DUNITER_ACCOUNT_HELP": "La publika ŝlosilo (ĉi-supre) indikas la celkonton de la pagoj, kiujn vi ricevos.",
"DUNITER_ACCOUNT_NO_PUBKEY_HELP": "Neniu publika ŝlosilo de konto estis sciigita. Vi devos doni ĝin al la eventualaj aĉetontoj.<br/>Vi povas tajpi ĝin iam ajn, <b>redaktante vian profilon</b>."
},
"TYPE": {
"TITLE": "Nova anonco",
"SELECT_TYPE": "Tipo de anonco:",
"OFFER": "Propono, Vendo",
"OFFER_SHORT": "Propono",
"NEED": "Peto, Serĉo",
"NEED_SHORT": "Peto"
},
"LOCAL_SALE": {
"LOCATION": "Numero de budo",
"LOCATION_HELP": "Numero de budo: 1, 2, ...",
"LOCATION_PREFIX": "Budo n°"
},
"EDIT": {
"TITLE": "Redaktado",
"TITLE_NEW": "Nova anonco",
"RECORD_TITLE": "Titolo",
"RECORD_TITLE_HELP": "Titolo: biciklo, libro...",
"RECORD_DESCRIPTION": "Priskribo",
"RECORD_DESCRIPTION_HELP": "Priskribo",
"RECORD_LOCATION": "Urbo",
"RECORD_LOCATION_HELP": "Poŝt-kodo, Urbo",
"RECORD_PRICE": "Prezo",
"RECORD_PRICE_HELP": "Prezo (nedeviga)",
"RECORD_CURRENCY": "Mono",
"RECORD_FEES": "Sendo-kostoj",
"RECORD_FEES_HELP": "Kostoj (nedeviga)",
"RECORD_STOCK": "Stoko disponebla",
"RECORD_STOCK_HELP": "Stoko disponebla"
},
"WOT": {
"VIEW": {
"BTN_RECORDS": "Anoncoj",
"DUNITER_PUBKEY": "Publika ŝlosilo por ricevi la pagojn",
"DUNITER_ACCOUNT": "Ricevo de la pagoj en {{currency|currencySymbol}}",
"DUNITER_ACCOUNT_HELP": "La publika ŝlosilo (ĉi-supre) rilatas al la konto uzota por pagi tiun ĉi anoncanton.",
"DUNITER_ACCOUNT_HELP_ASK_USER": "Tiu ĉi anoncanto ne sciigis la publikan ŝlosilon de sia konto. <a ng-click=\"showNewMessageModal()\">Kontaktu lin/ŝin</a> por ekhavi ĝin."
},
"ERROR": {
"FAILED_STAR_PROFILE": "Eraro dum la sendo de via noto. Bonvolu reprovi pli poste."
}
},
"ERROR": {
"INVALID_LOGIN_CREDENTIALS": "Identigilo aŭ pasvorto nevalida.<br/><br/>Kontrolu, ke ili ja rilatas al konto <b>kreita ĉe ğchange</b>.",
"FAILED_SAVE_RECORD": "Eraro dum la registrado de la anonco",
"FAILED_UPDATE_RECORD": "Eraro dum la ĝisdatigo de la anonco",
"LOAD_CATEGORY_FAILED": "Eraro dum la ekstarigo de la kategorioj",
"LOOKUP_RECORDS_FAILED": "Eraro dum la disvolviĝo de la serĉado",
"LOAD_RECORD_FAILED": "Eraro dum la ŝarĝado de la anonco",
"REMOVE_RECORD_FAILED": "Eraro dum la forigo de la anonco",
"SOLD_RECORD_FAILED": "Eraro dum la fermo de la anonco",
"REOPEN_RECORD_FAILED": "Eraro dum la reaperigo de la anonco",
"FAILED_SAVE_COMMENT": "Eraro dum la konservo de la komento",
"FAILED_REMOVE_COMMENT": "Eraro dum la forigo de la komento",
"RECORD_NOT_EXISTS": "Anonco neekzistanta",
"RECORD_EXCEED_UPLOAD_SIZE": "Ŝajnas, ke via anonco <b>estas tro ampleksa</b> por esti akceptata de la daten-nodo.<br/><br/>Vi povas ekzemple <b>forigi fotojn</b>, kaj poste provi denove.",
"GEO_LOCATION_NOT_FOUND": "Urbo aŭ post-kodo ne trovita"
},
"INFO": {
"RECORD_REMOVED" : "Anonco forigita",
"RECORD_SOLD" : "Anonco fermita",
"RECORD_REOPEN" : "Anonco reaperigita"
}
},
"EVENT": {
"MARKET": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> komentis vian anoncon: <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> modifis sian komenton ĉe via anonco: <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> respondis al via komento ĉe la anonco: <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> modifis la respondon al via komento ĉe la anonco: <b>{{params[2]}}</b>",
"FOLLOW_NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> komentis la anoncon: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> modifis sian komenton ĉe la anonco: <b>{{params[2]}}</b>",
"FOLLOW_NEW": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> aldonis la anoncon: <b>{{params[2]}}</b>",
"FOLLOW_UPDATE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> modifis la anoncon: <b>{{params[2]}}</b>",
"FOLLOW_CLOSE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> fermis la anoncon: <b>{{params[2]}}</b>",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> petas de vi moderigon ĉe la anonco: <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> atentigis pri anonco moderiginda: <b>{{params[2]}}</b>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> atentigis pri via anonco: <b>{{params[2]}}</b>",
"LIKE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> ŝatis vian anoncon: <b>{{params[2]}}</b>",
"FOLLOW_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> sekvas vian anoncon: <b>{{params[2]}}</b>"
}
}
}
);
$translateProvider.translations("es-ES", {
"MENU": {
"MARKET": "Anuncios",
"MY_RECORDS": "Mis anuncios"
},
"MARKET": {
"COMMON": {
"PRICE": "Precio",
"BTN_NEW_AD": "Presento un anuncio",
"SOLD": "El anuncio cerrada",
"AROUND_ME": "Alrededor de yo"
},
"HOME": {
"BTN_NEW_AD": "Poner un anuncio",
"BTN_SHOW_MARKET_OFFER": "Consultar los anuncios"
},
"CATEGORY": {
"ALL": "Todas las categorías"
},
"SEARCH": {
"TITLE": "Anuncios",
"CATEGORY": "Categorías: ",
"SEARCH_HELP": "Búsqueda (coche, libro...)",
"BY": "por",
"BTN_ADD": "Nuevo",
"BTN_OPTIONS": "Búsqueda avanzada",
"BTN_AROUND_ME": "Alrededor de yo",
"SHOW_MORE": "Visualizar más",
"SHOW_MORE_COUNT": "(límite actual a {{limit}})",
"LOCATION": "localización",
"LOCATION_HELP": "Ciudad",
"LAST_RECORDS": "últimos anuncios :",
"RESULTS": "Resultados :",
"BTN_LAST_RECORDS": "últimos anuncios",
"BTN_SHOW_CATEGORIES": "Recorrer las categorías",
"BTN_OFFERS": "Ofrecimientos",
"BTN_NEEDS": "Demandas",
"SHOW_CLOSED_RECORD": "Mostrar anuncios cerrados?",
"RECORD_STOCK": "Stock :"
},
"GALLERY": {
"TITLE": "Diapositivas",
"BTN_START": "Comienzo",
"BTN_CONTINUE": "Retomar",
"BTN_PAUSE": "Pausa",
"BTN_STOP": "Detener",
"SLIDE_DURATION": "Visualización de la hora :",
"SLIDE_DURATION_OPTION": "{{value}} segundo"
},
"VIEW": {
"TITLE": "Anuncio",
"BTN_SOLD_AD": "Vendido",
"BTN_SOLD": "Vendido",
"BTN_REOPEN": "Reabierto el anuncio",
"BTN_WRITE_OFFER": "Escribir al vendedor",
"BTN_WRITE_NEED": "Escribir al solicitante",
"MENU_TITLE": "Opciónes",
"RECORD_FEES_PARENTHESIS": "(gastos)",
"RECORD_STOCK": "Stock disponible :",
"POPOVER_SHARE_TITLE": "Anuncio {{title}}",
"REMOVE_CONFIRMATION" : "Está usted segura/o querer suprimir este anuncio ?<br/><br/>Esta operación es ireversible."
},
"TYPE": {
"TITLE": "Nuevo anuncio",
"SELECT_TYPE": "Tipo de anuncio :",
"OFFER": "Ofrecimiento, Venta",
"OFFER_SHORT": "Ofrecimiento",
"NEED": "Demanda, Búsqueda",
"NEED_SHORT": "Demanda"
},
"LOCAL_SALE": {
"LOCATION": "Número de stand",
"LOCATION_HELP": "Número de stand : 1, 2, ...",
"LOCATION_PREFIX": "Stand n°"
},
"EDIT": {
"TITLE": "Edición",
"TITLE_NEW": "Nuevo anuncio",
"RECORD_TITLE": "Título",
"RECORD_TITLE_HELP": "Título",
"RECORD_DESCRIPTION": "Descripción",
"RECORD_DESCRIPTION_HELP": "Descripción",
"RECORD_LOCATION": "Ciudad",
"RECORD_LOCATION_HELP": "Dirección, Ciudad",
"RECORD_PRICE": "Precio",
"RECORD_PRICE_HELP": "Precio (opcional)",
"RECORD_CURRENCY": "Moneda",
"RECORD_FEES": "Gastos de envío",
"RECORD_FEES_HELP": "Gastos de envío (opcional)",
"RECORD_STOCK": "Stock disponible",
"RECORD_STOCK_HELP": "Stock disponible"
},
"WOT": {
"VIEW": {
"BTN_RECORDS": "Anuncios"
}
},
"ERROR": {
"INVALID_LOGIN_CREDENTIALS": "De usuario o contraseña no válidos.<br/>Por favor, inténtelo de nuevo.",
"FAILED_SAVE_RECORD": "Fracaso durante el registro de el anuncio",
"FAILED_UPDATE_RECORD": "Fracaso durante la actualización de el anuncio",
"LOAD_CATEGORY_FAILED": "Erreur de actualización de las categorías",
"LOOKUP_RECORDS_FAILED": "Fracaso durante la ejecución de la búsqueda.",
"LOAD_RECORD_FAILED": "Fracaso durante la carga de el anuncio.",
"REMOVE_RECORD_FAILED": "Erreur de la supresión de el anuncio",
"SOLD_RECORD_FAILED": "Erreur de la cierre de el anuncio",
"REOPEN_RECORD_FAILED": "Erreur de la reapertura de el anuncio",
"FAILED_SAVE_COMMENT": "Fracaso durante el respaldo del comentario",
"FAILED_REMOVE_COMMENT": "Fracaso durante la supresión del comentario",
"RECORD_NOT_EXISTS": "Anuncio inexistente"
},
"INFO": {
"RECORD_REMOVED" : "Anuncio suprimido",
"RECORD_SOLD" : "Anuncio cerrada",
"RECORD_REOPEN" : "Anuncio reabrió"
}
},
"EVENT": {
"MARKET": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> ha comentado su anuncio : <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> ha modificado su comentario sobre su anuncio : <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> ha contestado a su comentario sobre el anuncio : <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> ha modificado la repuesta a su comentario sobre el anuncio : <b>{{params[2]}}</b>"
}
}
});
$translateProvider.translations("fr-FR", {
"MENU": {
"MARKET": "Annonces",
"MY_RECORDS": "Mes annonces"
},
"MARKET": {
"COMMON": {
"PRICE": "Prix",
"BTN_NEW_AD": "Je dépose une annonce",
"SOLD": "Annonce close",
"LAST_UPDATE": "Dernière mise à jour",
"AROUND_ME": "Autour de moi"
},
"JOIN": {
"PROFILE": {
"WARNING": "Il vous reste maintenant à compléter votre profil utilisateur.<br/><br/>Il s'agit <b>d'informations publiques</b>, accessibles par tous.",
"TITLE": "Nom, Prénom",
"TITLE_HELP": "Nom, Prénom ou pseudonyme",
"DESCRIPTION": "A propos de moi",
"DESCRIPTION_HELP": "Dites quelque chose à propos de vous..."
},
"SUBSCRIPTION": {
"EMAIL": "Email",
"EMAIL_HELP": "Email (optionnel)"
},
"LAST_SLIDE_CONGRATULATION": "Vous avez saisi toutes les informations nécessaires : Bravo !<br/>Vous pouvez maintenant <b>envoyer la demande de création</b> de compte.<br/><br/>N'oubliez pas vos identifiants, vous ne pourrez pas en changer !",
"CONFIRMATION_WALLET_ACCOUNT": "<b class=\"assertive\">Avertissement :</b> l'identifiant et le mot de passe ne pourront plus être modifiés.<br/><br/><b>Assurez-vous de toujours vous en rappeler !</b><br/><br/><b>Etes-vous sûr</b> de vouloir continuer avec ces identifiants ?"
},
"PROFILE": {
"DEFAULT_TITLE": "Utilisateur {{pubkey|formatPubkey}}"
},
"LOGIN": {
"HELP": "Veuillez saisir vos identifiants de connexion :",
"REMEMBER_ME": "Se souvenir de moi ?"
},
"EVENT_LOGIN": {
"TITLE": "Coordonnées",
"HELP": "Veuillez indiquer un <b>email ou numéro de téléphone</b> portable, afin que les organisateurs puissent vous contacter durant l'événement :",
"EMAIL_OR_PHONE": "Email ou téléphone",
"EMAIL_OR_PHONE_HELP": "Email ou numéro de téléphone",
"REMEMBER_ME": "Se souvenir de moi ?",
"ERROR": {
"INVALID_USERNAME": "Email ou numéro de téléphone non valide"
}
},
"HOME": {
"BTN_NEW_AD": "Déposer une annonce",
"BTN_SHOW_MARKET_OFFER": "Voir les annonces",
"LOCATION_LABEL": "Recherche d'annonces par ville :",
"LOCATION_HELP": "Code postal, Ville",
"ERROR": {
"GEO_LOCATION_NOT_FOUND": "Ville ou code postal non trouvé"
}
},
"CATEGORY": {
"ALL": "Toutes les catégories"
},
"SEARCH": {
"TITLE": "Annonces",
"CATEGORY": "Catégorie : ",
"SEARCH_HELP": "Recherche (vélo, rollers...)",
"BY": "par",
"BTN_ADD": "Nouveau",
"BTN_OPTIONS": "Recherche avancée",
"BTN_AROUND_ME": "Autour de moi",
"GEO_DISTANCE": "Distance maximale autour de la ville :",
"GEO_DISTANCE_OPTION": "{{value}} km",
"SHOW_MORE": "Afficher plus",
"SHOW_MORE_COUNT": "(limite actuelle à {{limit}})",
"LOCATION": "Ville",
"LOCATION_HELP": "Code postal, Ville",
"RESULTS": "Résultats",
"RESULT_COUNT_LOCATION": "{{count}} résultat{{count>0?'s':''}}, près de {{location}}",
"RESULT_COUNT": "{{count}} résultat{{count>0?'s':''}}",
"LAST_RECORDS": "Dernières annonces",
"LAST_RECORD_COUNT_LOCATION": "{{count}} annonce{{count>0?'s':''}} récente{{count>0?'s':''}}, près de {{location}}",
"LAST_RECORD_COUNT": "{{count}} annonce{{count>0?'s':''}} récente{{count>0?'s':''}}",
"BTN_LAST_RECORDS": "Dernières annonces",
"BTN_SHOW_CATEGORIES": "Parcourir les catégories",
"BTN_OFFERS": "Offres",
"BTN_NEEDS": "Demandes",
"SHOW_CLOSED_RECORD": "Afficher les annonces closes ?",
"SHOW_OLD_RECORD": "Afficher les anciennes annonces ?",
"RECORD_STOCK": "Stock :"
},
"GALLERY": {
"TITLE": "Diaporama",
"BTN_START": "Démarrer",
"BTN_CONTINUE": "Reprendre",
"BTN_PAUSE": "Pause",
"BTN_STOP": "Arrêter",
"SLIDE_DURATION": "Durée d'affichage :",
"SLIDE_DURATION_OPTION": "{{value}} secondes"
},
"VIEW": {
"TITLE": "Annonce",
"BTN_SOLD_AD": "Clore l'annonce",
"BTN_SOLD": "Clore",
"BTN_REOPEN": "Réouvrir l'annonce",
"BTN_WRITE_OFFER": "Ecrire au vendeur",
"BTN_WRITE_NEED": "Ecrire au demandeur",
"BTN_FOLLOW": "Suivre cette annonce",
"BTN_STOP_FOLLOW": "Ne plus suivre cette annonce",
"MENU_TITLE": "Options",
"RECORD_FEES_PARENTHESIS": "(frais)",
"RECORD_STOCK": "Stock disponible :",
"POPOVER_SHARE_TITLE": "Annonce {{title}}",
"REMOVE_CONFIRMATION" : "Êtes-vous sûr de vouloir supprimer cette annonce ?<br/><br/>Cette opération est irréversible.",
"SOLD_CONFIRMATION" : "<b>Êtes-vous sûr</b> de vouloir clore cette annonce ?",
"REOPEN_CONFIRMATION" : "<b>Êtes-vous sûr</b> de vouloir réouvrir cette annonce ?",
"NEW_MESSAGE_TITLE": "Au sujet de l'annonce \"{{title}}\"...",
"MORE_LIKE_THIS": "Ceci pourrait vous intéresser :"
},
"WALLET": {
"DUNITER_PUBKEY": "Clé publique de réception des paiements",
"DUNITER_ACCOUNT": "Réception des paiements en {{currency|currencySymbol}}",
"DUNITER_ACCOUNT_HELP": "La clef publique (ci-dessus) indique la destination des paiements que vous recevrez.",
"DUNITER_ACCOUNT_NO_PUBKEY_HELP": "Aucune clé publique de compte n'a été renseignée. Vous devrez la donner aux acheteurs éventuels.<br/>Vous pouvez la saisir à tout moment, <b>en éditant votre profil</b>."
},
"TYPE": {
"TITLE": "Nouvelle annonce",
"SELECT_TYPE": "Type d'annonce :",
"OFFER": "Offre, Vente",
"OFFER_SHORT": "Offre",
"NEED": "Demande, Recherche",
"NEED_SHORT": "Demande"
},
"LOCAL_SALE": {
"LOCATION": "Numéro du stand",
"LOCATION_HELP": "Numéro du stand : 1, 2, ...",
"LOCATION_PREFIX": "Stand n°"
},
"EDIT": {
"TITLE": "Edition",
"TITLE_NEW": "Nouvelle annonce",
"RECORD_TITLE": "Titre",
"RECORD_TITLE_HELP": "Titre : vélo, livre...",
"RECORD_DESCRIPTION": "Description",
"RECORD_DESCRIPTION_HELP": "Description",
"RECORD_LOCATION": "Ville",
"RECORD_LOCATION_HELP": "Code postal, Ville",
"RECORD_PRICE": "Prix",
"RECORD_PRICE_HELP": "Prix (optionnel)",
"RECORD_CURRENCY": "Monnaie",
"RECORD_FEES": "Frais d'envoi",
"RECORD_FEES_HELP": "Frais (optionnel)",
"RECORD_STOCK": "Stock disponible",
"RECORD_STOCK_HELP": "Stock disponible"
},
"WOT": {
"VIEW": {
"BTN_RECORDS": "Annonces",
"DUNITER_PUBKEY": "Clé publique de réception des paiements",
"DUNITER_ACCOUNT": "Réception des paiements en {{currency|currencySymbol}}",
"DUNITER_ACCOUNT_HELP": "La clef publique (ci-dessus) correspond au compte à utiliser pour payer cet utilisateur.",
"DUNITER_ACCOUNT_HELP_ASK_USER": "Cet utilisateur n'a pas communiqué sa clef publique de compte. <a ng-click=\"showNewMessageModal()\">Contactez-le</a> pour l'obtenir."
},
"ERROR": {
"FAILED_STAR_PROFILE": "Erreur lors de l'envoi de votre note. Veuillez réessayer ultérieurement."
}
},
"ERROR": {
"INVALID_LOGIN_CREDENTIALS": "Identifiant ou mot de passe invalide.<br/><br/>Vérifiez qu'ils correspondent bien à un compte <b>créé sur ğchange</b>.",
"FAILED_SAVE_RECORD": "Erreur lors de l'enregistrement de l'annonce",
"FAILED_UPDATE_RECORD": "Erreur lors de la mise à jour de l'annonce",
"LOAD_CATEGORY_FAILED": "Erreur d'initialisation des catégories",
"LOOKUP_RECORDS_FAILED": "Erreur lors de l'exécution de la recherche",
"LOAD_RECORD_FAILED": "Erreur lors du chargement de l'annonce",
"REMOVE_RECORD_FAILED": "Erreur de la suppression de l'annonce",
"SOLD_RECORD_FAILED": "Erreur lors de la fermeture de l'annonce",
"REOPEN_RECORD_FAILED": "Erreur lors de la réouverture de l'annonce",
"FAILED_SAVE_COMMENT": "Erreur lors de la sauvegarde du commentaire",
"FAILED_REMOVE_COMMENT": "Erreur lors de la suppression du commentaire",
"RECORD_NOT_EXISTS": "Annonce inexistante",
"RECORD_EXCEED_UPLOAD_SIZE": "Il semble que votre annonce <b>soit trop volumineuse</b> pour être acceptée par le noeud de données.<br/><br/>Vous pouvez par exemple <b>supprimer des photos</b>, puis essayer à nouveau.",
"GEO_LOCATION_NOT_FOUND": "Ville ou code postal non trouvé"
},
"INFO": {
"RECORD_REMOVED" : "Annonce supprimée",
"RECORD_SOLD" : "Annonce close",
"RECORD_REOPEN" : "Annonce réouverte"
}
},
"EVENT": {
"MARKET": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a commenté votre annonce : <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a modifié son commentaire sur votre annonce : <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a répondu à votre commentaire sur l'annonce : <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a modifié la réponse à votre commentaire sur l'annonce : <b>{{params[2]}}</b>",
"FOLLOW_NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a commenté l'annonce : <b>{{params[2]}}</b>",
"FOLLOW_UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a modifié son commentaire sur l'annonce : <b>{{params[2]}}</b>",
"FOLLOW_NEW": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a ajouté l'annonce : <b>{{params[2]}}</b>",
"FOLLOW_UPDATE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a modifié l'annonce : <b>{{params[2]}}</b>",
"FOLLOW_CLOSE": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a clôturé l'annonce : <b>{{params[2]}}</b>",
"MODERATION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> vous demande une modération sur l'annonce : <b>{{params[2]}}</b><br/><b class=\"dark ion-quote\"> </b><span class=\"text-italic\">{{params[3]}}</span>",
"DELETION_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a signalé une annonce à modérer : <b>{{params[2]}}</b>",
"ABUSE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a signalé votre annonce : <b>{{params[2]}}</b>",
"LIKE_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> a aimé votre annonce : <b>{{params[2]}}</b>",
"FOLLOW_RECEIVED": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||params[1]}}</span> suit votre annonce : <b>{{params[2]}}</b>"
}
}
}
);
$translateProvider.translations("nl-NL", {
"MENU": {
"MARKET": "Advertenties"
},
"MARKET": {
"COMMON": {
"PRICE": "Prijs",
"BTN_BUY": "Koop",
"BTN_BUY_DOTS": "Koop...",
"BTN_NEW_AD": "Nieuwe advertentie",
"AROUND_ME": "In mijn omgeving"
},
"SEARCH": {
"TITLE": "Markt",
"SEARCH_HELP": "Wat, waar: auto, Utrecht, ...",
"BTN_ADD": "Nieuw",
"BTN_OPTIONS": "Geavanceerd zoeken",
"BTN_AROUND_ME": "In mijn omgeving",
"SHOW_MORE": "Toon meer",
"SHOW_MORE_COUNT": "(huidige limiet op {{limit}})",
"LOCATION": "Locatie",
"LOCATION_HELP": "Plaats",
"LAST_RECORDS": "Nieuwste advertenties:",
"RESULTS": "Resultaat:",
"BTN_OFFERS": "Aangeboden",
"BTN_NEEDS": "Gezocht"
},
"VIEW": {
"TITLE": "Advertentie",
"MENU_TITLE": "Opties",
"POPOVER_SHARE_TITLE": "Advertentie {{title}}",
"REMOVE_CONFIRMATION" : "Weet je zeker dat je deze advertentie wil wissen?<br/><br/>Dit kan niet ongedaan worden gemaakt."
},
"TYPE": {
"TITLE": "Nieuwe advertentie",
"SELECT_TYPE": "Soort advertentie:",
"OFFER": "Aanbod",
"NEED": "Vraag"
},
"EDIT": {
"TITLE": "Bewerk",
"TITLE_NEW": "Nieuwe advertentie",
"RECORD_TITLE": "Titel",
"RECORD_TITLE_HELP": "Titel",
"RECORD_DESCRIPTION": "Beschrijving",
"RECORD_DESCRIPTION_HELP": "Beschrijving",
"RECORD_LOCATION": "Adres",
"RECORD_LOCATION_HELP": "Straat, Plaats",
"RECORD_PRICE": "Prijs",
"RECORD_PRICE_HELP": "Prijs (optioneel)",
"RECORD_CURRENCY": "Valuta"
},
"ERROR": {
"FAILED_SAVE_RECORD": "Advertentie opslaan mislukt",
"FAILED_UPDATE_RECORD": "Advertentie aanpassen mislukt",
"LOAD_CATEGORY_FAILED": "Categorieên laden mislukt",
"LOOKUP_RECORDS_FAILED": "Fout tijdens laden van advertenties.",
"LOAD_RECORD_FAILED": "Advertentie laden mislukt",
"REMOVE_RECORD_FAILED": "Advertentie wissen mislukt",
"FAILED_SAVE_COMMENT": "Commentaar opslaan mislukt",
"FAILED_REMOVE_COMMENT": "Commentaar wissen mislukt",
"RECORD_NOT_EXISTS": "Advertentie niet gevonden"
},
"INFO": {
"RECORD_REMOVED" : "Advertentie succesvol verwijderd"
}
},
"EVENT": {
"MARKET": {
"NEW_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> heeft gereageerd op jouw advertentie: <b>{{params[2]}}</b>",
"UPDATE_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> heeft zijn/aar reactie op jouw advertentie bewerkt: <b>{{params[2]}}</b>",
"NEW_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> heeft gereageerd op jouw commentaar op advertentie: <b>{{params[2]}}</b>",
"UPDATE_REPLY_COMMENT": "<span class=\"positive\"><i class=\"icon ion-person\"></i>&thinsp;{{name||uid||params[1]}}</span> heeft zijn/haar reactie op jouw commentaar bewerkt, op advertentie: <b>{{params[2]}}</b>"
}
}
}
);
}]);
angular.module('cesium.plugins.templates', []).run(['$templateCache', function($templateCache) {$templateCache.put('plugins/es/templates/menu_extend.html','\n<!-- Main section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'menu-discover\'">\n <ion-item menu-close ng-if="$root.settings.expertMode" class="item item-icon-left" active-link="active" active-link-path-prefix="#/app/network/data" ui-sref="app.es_network">\n <i class="icon ion-cloud"></i>\n <span translate>MENU.NETWORK</span>\n </ion-item>\n <a id="helptip-menu-btn-network"></a>\n\n</ng-if>\n\n<!-- Toolbar section -->\n<div ng-if=":state:enable && extensionPoint === \'nav-buttons-right\'" class="hidden-xs hidden-sm">\n\n <!-- messages -->\n <button class="button button-clear icon ion-email" ng-if="login" active-link="gray" active-link-path-prefix="#/app/user/message" ng-click="showMessagesPopover($event)">\n <span ng-if="$root.walletData.messages.unreadCount" class="badge badge-button badge-positive">{{walletData.messages.unreadCount}}</span>\n </button>\n\n <!-- notifications -->\n <button class="button button-clear icon ion-android-notifications" ng-if="login" active-link="gray" active-link-path-prefix="#/app/notifications" ng-click="showNotificationsPopover($event)">\n <span ng-if="walletData.notifications.unreadCount" class="badge badge-button badge-positive">{{walletData.notifications.unreadCount}}</span>\n </button>\n</div>\n\n<!-- User section -->\n<div ng-if=":state:enable && extensionPoint === \'menu-user\'" class="visible-xs visible-sm">\n\n <a menu-close class="item item-icon-left" active-link="active" active-link-path-prefix="#/app/user/message" ng-class="{\'item-menu-disable\': !login}" ng-click="loginAndGo(\'app.user_message\')">\n <i class="icon ion-email"></i>\n <span translate>MENU.MESSAGES</span>\n <span class="badge badge-positive" ng-if="walletData.messages.unreadCount">{{walletData.messages.unreadCount}}</span>\n </a>\n\n <a menu-close class="item item-icon-left" active-link="active" active-link-path-prefix="#/app/notifications" ng-class="{\'item-menu-disable\': !login}" ng-click="loginAndGo(\'app.view_notifications\')">\n <i class="icon ion-android-notifications"></i>\n <span translate>MENU.NOTIFICATIONS</span>\n <span class="badge badge-positive" ng-if="walletData.notifications.unreadCount">{{walletData.notifications.unreadCount}}</span>\n </a>\n\n</div>\n');
$templateCache.put('plugins/market/templates/menu_extend.html','\n<!-- Main section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'menu-main\'">\n\n <!-- view market -->\n <ion-item menu-close class="item item-icon-left" active-link="active" active-link-path-prefix="#/app/market" ui-sref="app.market_lookup">\n <i class="icon ion-speakerphone"></i>\n <span translate>MENU.MARKET</span>\n </ion-item>\n\n <!-- view pages -->\n <ion-item menu-close class="item item-icon-left" active-link="active" active-link-path-prefix="#/app/page" ui-sref="app.registry_lookup">\n <i class="icon ion-social-buffer"></i>\n <span translate>MENU.REGISTRY</span>\n </ion-item>\n\n <!-- view market gallery\n <ion-item menu-close class="item item-icon-left hidden-xs hidden-sm"\n active-link="active"\n active-link-path-prefix="#/app/gallery/market"\n ui-sref="app.market_gallery">\n <i class="icon ion-images"></i>\n <span translate>MARKET.GALLERY.TITLE</span>\n </ion-item>-->\n\n</ng-if>\n\n<div ng-if=":state:enable && extensionPoint === \'menu-user\'">\n <!-- wallet records -->\n <a menu-close class="item item-border item-icon-left" active-link="active" active-link-path-prefix="#/app/records/wallet" ng-click="loginAndGo(\'app.market_wallet_records\')" ng-class="{\'item-menu-disable\': !login}">\n <i class="icon ion-person" style="font-size: 19px; left: 13px"></i>\n <i class="icon-secondary ion-speakerphone" style="font-size: 16px; left: 32px; top: -6px"></i>\n <span translate>MENU.MY_RECORDS</span>\n </a>\n</div>\n');
$templateCache.put('plugins/es/templates/common/dropdown_locations.html','\n <!-- dropdown -->\n <ul class="item no-padding list dropdown-list" ng-if="locations" scroll="true">\n\n <div ng-if="!locations.length" class="item padding assertive">\n <span translate>COMMON.SEARCH_NO_RESULT</span>\n </div>\n\n <a ng-repeat="res in locations" class="item item-border-large item-text-wrap ink done in {{res.selected && \'active\' || \'\'}}" ng-class="::{\'item-divider\': !res.address, \'item-icon-left\': res.address}" ng-click="::res.address ? selectLocation(res) : false">\n\n <!-- if divider -->\n <h4 class="text-italic" ng-if="::!res.address" ng-bind-html="res.name"></h4>\n\n <!-- if divider -->\n <ng-if ng-if="::res.address">\n\n <i class="icon ion-location"></i>\n\n <h3 ng-if="res.address.road">\n {{::res.address.road}}\n </h3>\n <h3>\n <span ng-if="res.address.postcode">{{::res.address.postcode}}</span>\n {{::res.address.city||res.address.village}}\n <span class="gray">| {{::res.address.country}}</span>\n </h3>\n <h5 class="gray">\n {{\'LOCATION.MODAL.POSITION\'|translate:res }}\n </h5>\n </ng-if>\n\n </a>\n\n </ul>\n');
$templateCache.put('plugins/es/templates/common/edit_pictures.html','<div class="gallery" ng-controller="ESPicturesEditCtrl as ctrl">\n\n <!-- Picture list -->\n <div ng-repeat="picture in pictures" class="item card card-gallery stable-bg" ng-class="{\'in done\': picture.isnew}">\n <div>\n <h2 ng-if="picture.title">{{picture.title}}</h2>\n <img ng-src="{{picture.src}}">\n </div>\n <div class="item done in tabs tabs-secondary tabs-icon-left">\n <a class="tab-item stable-bg assertive" ng-click="removePicture($index)" title="{{\'COMMON.BTN_PICTURE_DELETE\' | translate}}"><i class="icon ion-trash-a"></i>{{\'COMMON.BTN_PICTURE_DELETE\'|translate}}</a>\n <a class="tab-item stable-bg dark" ng-click="rotatePicture($index)" title="{{\'COMMON.BTN_PICTURE_ROTATE\' | translate}}"><i class="icon ion-forward"></i>{{\'COMMON.BTN_PICTURE_ROTATE\'|translate}}</a>\n <a class="tab-item stable-bg" ng-click="favoritePicture($index)" ng-class="{\'gray\': $index !== 0, \'positive\': $index === 0}" title="{{\'COMMON.BTN_PICTURE_FAVORISE\' | translate}}"><i class="icon ion-star"></i>{{\'COMMON.BTN_PICTURE_FAVORISE\'|translate}}</a>\n </div>\n </div>\n\n <!-- Add picture button -->\n <div class="item card card-gallery card-gallery-new text-center padding ink" ng-click="selectNewPicture()">\n <i class="ion-image stable" style="font-size:150px"></i>\n <b class="ion-plus gray" style="font-size:80px; position:absolute; top:25px; right: 5px"></b>\n <p translate>COMMON.BTN_ADD_PICTURE</p>\n </div>\n\n <input type="file" id="pictureFile" accept="image/*" onchange="angular.element(this).scope().fileChanged(event)" style="visibility:hidden; position:absolute">\n</div>\n\n');
$templateCache.put('plugins/es/templates/common/edit_position.html','<div class="item item-divider" translate>LOCATION.LOCATION_DIVIDER</div>\n\n<!-- street -->\n<ion-item class="item-input item-floating-label item-button-right">\n <span class="input-label">{{\'LOCATION.ADDRESS\' | translate}}</span>\n <textarea placeholder="{{\'LOCATION.ADDRESS_HELP\' | translate}}" ng-model="formData.address" ng-model-options="{ debounce: 350 }" rows="4" cols="10">\n </textarea>\n</ion-item>\n\n<!-- city -->\n<div class="item item-input item-floating-label" ng-class="{\'item-input-error\': form.$submitted && form.geoPoint.$invalid}">\n <span class="input-label" translate>LOCATION.CITY</span>\n <input type="text" placeholder="{{\'LOCATION.CITY_HELP\'|translate}}" name="city" ng-model="formData.city" ng-model-options="{ updateOn: \'blur\' }" required-if="formData.address" ng-change="onCityChanged()">\n</div>\n<input type="hidden" name="geoPoint" ng-model="formData.geoPoint" required-if="formPosition.enable" geo-point>\n<div class="form-errors" ng-show="form.$submitted && form.city.$error" ng-messages="form.city.$error">\n <div class="form-error" ng-message="required">\n <span translate="LOCATION.ERROR.CITY_REQUIRED_IF_STREET"></span>\n </div>\n</div>\n<div class="form-errors" ng-show="form.$submitted && form.geoPoint.$error" ng-messages="form.geoPoint.$error">\n <div class="form-error" ng-message="required">\n <span translate="LOCATION.ERROR.REQUIRED_FOR_LOCATION" ng-if="!formData.city"></span>\n <span translate="LOCATION.ERROR.INVALID_FOR_LOCATION" ng-if="formData.city"></span>\n </div>\n <div class="form-error" ng-message="geoPoint">\n <span translate="LOCATION.ERROR.REQUIRED_FOR_LOCATION" ng-if="!formData.city"></span>\n <span translate="LOCATION.ERROR.INVALID_FOR_LOCATION" ng-if="formData.city"></span>\n </div>\n</div>\n\n\n<!-- Position (lat/lon) -->\n<div class="item row item-text-wrap no-padding">\n\n <div class="col no-padding">\n\n <!-- appear on map ? -->\n <ion-checkbox ng-if="options.position.showCheckbox" ng-model="formPosition.enable" ng-change="onUseGeopointChanged()" class="item item-border-large done in">\n <div class="item-content">\n <span translate>LOCATION.USE_GEO_POINT</span>\n <h4 class="gray" ng-if="formPosition.loading">\n <ion-spinner class="icon ion-spinner-small" icon="android"></ion-spinner>\n {{\'LOCATION.LOADING_LOCATION\'|translate}}\n </h4>\n </div>\n </ion-checkbox>\n </div>\n\n <div class="col col-10 no-padding" style="min-width: 60px">\n <div class="row text-center">\n\n <a class="button button-stable button-small-padding" title="{{\'LOCATION.BTN_GEOLOC_ADDRESS\'|translate}}" ng-disabled="!formPosition.enable" ng-click="openSearchLocationModal()">\n <i class="icon ion-home" style="left: 15px"></i>\n <b class="icon-secondary ion-search" style="top: -9px; left:32px; font-size: 18px"></b>\n </a>\n\n </div>\n </div>\n</div>\n\n\n<cs-extension-point name="after-position"></cs-extension-point>\n');
$templateCache.put('plugins/es/templates/common/edit_socials.html','\n <div class="item item-divider" translate>PROFILE.SOCIAL_NETWORKS_DIVIDER</div>\n\n <ion-item class="item-remove-animate item-icon-left" type="no-padding item-text-wrap" ng-if="formData.socials && formData.socials.length" ng-repeat="social in formData.socials | filter:filterFn track by social.url" id="social-{{social.url|formatSlug}}">\n <i class="icon ion-social-{{social.type}}" ng-class="{\'ion-bookmark\': social.type == \'other\', \'ion-link\': social.type == \'web\', \'ion-email\': social.type == \'email\', \'ion-iphone\': social.type == \'phone\'}"></i>\n <p ng-if="social.type && social.type != \'web\'">\n {{social.type}}\n <i class="ion-locked" ng-if="social.recipient"></i>\n </p>\n <h2>\n <a href="{{social.url}}" ng-if="social.type != \'email\' && social.type != \'phone\'" target="_blank">{{social.url}}</a>\n <a href="mailto:{{social.url}}" ng-if="social.type == \'email\'">{{social.url}}</a>\n <a href="tel:{{social.url}}" ng-if="social.type == \'phone\'">{{social.url}}</a>\n <a class="gray hidden-device" ng-if="!social.recipient" ng-click="formData.socials.splice($index, 1); dirty = true;">\n &nbsp;<b class="ion ion-trash-a"></b>&nbsp;\n </a>\n <a class="gray hidden-device" ng-if="!social.recipient" ng-click="editSocialNetwork($index)">\n &nbsp;<b class="ion ion-edit"></b>&nbsp;\n </a>\n </h2>\n <ion-option-button class="button-assertive" ng-if="!social.recipient" ng-click="formData.socials.splice($index, 1); dirty = true;">\n {{\'COMMON.BTN_DELETE\'|translate}}\n </ion-option-button>\n <ion-option-button class="button-info" ng-if="!social.recipient" ng-click="editSocialNetwork($index)">\n {{\'COMMON.BTN_EDIT\'|translate}}\n </ion-option-button>\n </ion-item>\n\n <div class="item item-complex item-input-inset">\n <label class="item-input-wrapper">\n <input type="text" style="width:100%" placeholder="{{\'PROFILE.SOCIAL_HELP\'|translate}}" id="socialUrl" on-return="addSocialNetwork();" ng-model="socialData.url">\n </label>\n <button class="button button-small hidden-xs" type="button" ng-click="addSocialNetwork()">\n {{\'COMMON.BTN_ADD\'|translate}}\n </button>\n <button class="button button-small button-icon icon ion-android-add visible-xs" type="button" ng-click="addSocialNetwork()">\n </button>\n </div>\n');
$templateCache.put('plugins/es/templates/common/item_comment.html','<ng-init ng-init="level = level + 1; hash=(comment.id|formatHash)">\n <a name="{{::hash}}"></a>\n\n <ion-item id="comment-{{::comment.id|formatHash}}" class="card card-comment card-avatar stable-900-bg item-text-wrap no-padding" ng-class="{\'in done\': comment.isnew, \'positive-100-bg\': (hash == anchor)}">\n\n <!-- Parent comment -->\n <div class="card-header padding-left" ng-if="comment.parent && !hideParent">\n <h5 class="gray underline">\n <ng-if ng-if="!comment.parent.issuer">\n {{\'COMMENTS.REPLY_TO_DELETED_COMMENT\'|translate}}\n </ng-if>\n <ng-if ng-if="comment.parent.issuer">\n <a ng-click="toggleExpandedParent(comment, $index)">\n {{\'COMMENTS.REPLY_TO_LINK\'|translate}}\n <ng-if ng-if="::comment.parent.name||comment.parent.uid">\n {{::comment.parent.name||comment.parent.uid}}\n </ng-if>\n <ng-if ng-if="::!comment.parent.name && !comment.parent.uid">\n <i class="ion-key"></i>\n {{::comment.parent.issuer|formatPubkey}}\n </ng-if>\n </a>\n <i ng-class="::{\'ion-arrow-down-b\': !comment.expandedParent[$index], \'ion-arrow-up-b\': comment.expandedParent[$index]}"></i>\n </ng-if>\n </h5>\n <div class="padding-left" ng-if="comment.expandedParent[$index]">\n <div class="card card-avatar card-avatar-small stable-bg item-text-wrap no-padding in done">\n <ng-include ng-init="comment = comment.parent" src="\'plugins/es/templates/common/item_comment_content.html\'">\n </ng-include>\n </div>\n </div>\n </div>\n\n <ng-include src="\'plugins/es/templates/common/item_comment_content.html\'"></ng-include>\n\n <div class="card-footer gray">\n <small class="underline">\n <a ng-click="share($event, comment, $index)" title="{{comment.creationTime | formatDate}}{{ (comment.creationTime != comment.time) ? (\' - \' + (\'COMMENTS.MODIFIED_ON\'|translate:comment)) : \'\'}}">{{comment.creationTime | formatFromNow}}\n <span ng-if="comment.time && comment.creationTime != comment.time" translate>COMMENTS.MODIFIED_PARENTHESIS</span>\n </a>\n\n <ng-if ng-if="comment.replyCount">\n | <a class="dark" ng-click="toggleExpandedReplies(comment, $index)">{{\'COMMENTS.REPLY_COUNT\'|translate:comment}}</a>\n <i ng-class="{\'ion-arrow-down-b\': !comment.showReplies, \'ion-arrow-up-b\': comment.showReplies}"></i>\n </ng-if>\n </small>\n\n <div class="pull-right">\n <a class="ion-android-share-alt" ng-click="share($event, comment)">\n </a>\n <a class="ion-edit" ng-if="isUserPubkey(comment.issuer)" ng-click="edit(comment)">\n </a>\n <a class="ion-trash-a" ng-if="isUserPubkey(comment.issuer)" ng-click="remove(comment, $index)">\n </a>\n <a class="ion-reply" ng-click="reply(comment)">\n {{::\'COMMENTS.REPLY\'|translate}}\n </a>\n </div>\n </div>\n </ion-item>\n\n <!-- replies -->\n <div ng-if="comment.expandedReplies[$index]" class="padding-left card-avatar-small expanded" ng-init="hideParent=true">\n <ng-include ng-repeat="comment in comment.replies track by comment.id" src="\'plugins/es/templates/common/item_comment.html\'">\n </ng-include>\n </div>\n\n</ng-init>\n');
$templateCache.put('plugins/es/templates/common/item_comment_content.html','\n <div class="item item-avatar done in">\n <span class="avatar avatar-member" ng-if="::!comment.avatar"></span>\n <span class="avatar" ng-if="::comment.avatar" style="background-image: url({{::comment.avatar.src}})"></span>\n\n <a class="pull-left" ui-sref="app.wot_identity({pubkey:comment.issuer, uid: comment.uid})">\n <span class="positive" ng-if="::comment.name||comment.uid">\n {{::comment.name||comment.uid}}\n </span>\n <span ng-if="::!comment.name && !comment.uid" class="gray">\n <i class="icon ion-key gray"></i>\n {{::comment.issuer|formatPubkey}}\n </span>\n </a>&nbsp;\n <span trust-as-html="comment.html"></span>\n </div>\n');
$templateCache.put('plugins/es/templates/common/item_location_search.html',' <!-- search text -->\n <div class="item no-padding">\n <div class="item-input item-button-right light-bg">\n <i class="icon ion-location placeholder-icon"></i>\n <input type="text" autocomplete="postal-code" placeholder="{{(options.location.help||\'LOCATION.SEARCH_HELP\')|translate}}" ng-model-options="{ debounce: 350 }" ng-model="search.location" ng-keydown="onKeydown($event)" ng-change="onLocationChanged()" ng-blur="hideDropdown()">\n\n <a class="button button-clear button-small button-stable gray ink no-padding" tabindex="-1" ng-click="showDistancePopover($event)">\n <span>{{\'COMMON.GEO_DISTANCE_OPTION\' | translate: {value: search.geoDistance} }}</span>\n &nbsp;<b class="ion-arrow-down-b" style="font-size: 12pt"></b>\n </a>\n\n </div>\n </div>\n\n <!-- dropdown -->\n <ng-include src="\'plugins/es/templates/common/dropdown_locations.html\'"></ng-include>\n\n');
$templateCache.put('plugins/es/templates/common/modal_category.html','<ion-modal-view class="modal-full-height">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n <h1 class="title">{{ (ctrl.title || \'COMMON.CATEGORIES\') | translate}}</h1>\n </ion-header-bar>\n\n <ion-content class="categoryModal">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list">\n <label class="item item-input">\n <i class="icon ion-search placeholder-icon"></i>\n <input type="text" placeholder="{{\'COMMON.CATEGORY_SEARCH_HELP\'|translate}}" ng-model="ctrl.searchText" ng-model-options="{ debounce: 350 }" ng-change="ctrl.doSearch()">\n </label>\n\n\n <div ng-repeat="cat in categories" class="item item-category item-text-wrap" ng-class="{\'item-divider\': !cat.parent}" ng-click="cat.parent ? closeModal(cat) : false">\n <h2 ng-bind-html="cat.name"></h2>\n </div>\n </div>\n </ion-content>\n</ion-modal-view>\n');
$templateCache.put('plugins/es/templates/common/modal_edit_avatar.html','<ion-modal-view>\n <ion-header-bar class="bar-positive">\n <button class="button button-clear visible-xs visible-sm" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n\n <h1 class="title" translate>PROFILE.MODAL_AVATAR.TITLE</h1>\n\n <button class="button button-clear icon-right visible-xs" ng-click="doCrop()" ng-disabled="formData.imageCropStep == 1" ng-if="formData.imageCropStep <= 2">\n <span translate>COMMON.BTN_NEXT</span>\n <i class="icon ion-ios-arrow-right"></i>\n </button>\n <button class="button button-clear icon-right visible-xs" ng-click="closeModal(formData.result)" ng-if="formData.imageCropStep == 3">\n <i class="icon ion-android-done"></i>\n </button>\n </ion-header-bar>\n\n <ion-content class="modal-avatar padding">\n\n\n <div ng-show="formData.imageCropStep == 1">\n <p translate>PROFILE.MODAL_AVATAR.SELECT_FILE_HELP</p>\n\n <!-- Add picture button -->\n <div class="item card text-center padding ink" ng-click="openFileSelector()">\n <i class="ion-image stable" style="font-size:150px"></i>\n <b class="ion-plus gray" style="position:relative; font-size:80px; top:-51px; right: 19px"></b>\n <p translate>PROFILE.MODAL_AVATAR.BTN_SELECT_FILE</p>\n </div>\n\n <input type="file" name="fileInput" accept="image/*" id="fileInput" onchange="angular.element(this).scope().fileChanged(event)" style="visibility:hidden; position:absolute">\n </div>\n\n <div ng-show="formData.imageCropStep == 2">\n <p translate>PROFILE.MODAL_AVATAR.RESIZE_HELP</p>\n\n <!-- <image-crop\n data-height="200" //shape\'s height\n data-width="150" //shape\'s width\n data-shape="square" //the shape.. square or circle\n data-step="imageCropStep"//scope variable that will contain the current step of the crop (1. Waiting for source image; 2. Image loaded, waiting for crop; 3. Crop done)\n src="imgSrc" //scope variable that will be the source image for the crop (may be a Blob or base64 string)\n data-result-blob="result" //scope variable that will contain the Blob information\n data-result="resultDataUrl" //scope variable that will contain the image\'s base64 string representation\n crop="initCrop" //scope variable that must be set to true when the image is ready to be cropped\n padding="250" //space, in pixels, rounding the shape\n max-size="1024" //max of the image, in pixels\n ></image-crop> -->\n\n <div class="item card text-center padding ink">\n <image-crop data-height="100" data-width="100" data-shape="circle" data-step="formData.imageCropStep" src="formData.imgSrc" data-result="formData.result" data-result-blob="formData.resultBlob" crop="formData.initCrop" padding="150" max-size="1024">\n </image-crop>\n </div>\n </div>\n\n <div ng-show="formData.imageCropStep == 3">\n <p translate>PROFILE.MODAL_AVATAR.RESULT_HELP</p>\n\n <div class="item card padding hero" style="height: 110px">\n <div class="content">\n <img class="avatar" ng-src="{{formData.result}}" style="height: 88px; width: 88px">\n </div>\n </div>\n </div>\n\n <!-- buttons bar -->\n <div class="padding hidden-xs text-right">\n <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>\n COMMON.BTN_CANCEL\n </button>\n <button class="button button-calm icon-right ion-chevron-right ink" ng-click="doCrop()" translate ng-disabled="formData.imageCropStep == 1" ng-if="formData.imageCropStep <= 2">\n COMMON.BTN_NEXT\n </button>\n <button class="button button-positive ink" ng-click="closeModal(formData.result)" translate ng-if="formData.imageCropStep == 3">\n COMMON.BTN_CONTINUE\n </button>\n </div>\n\n </ion-content>\n</ion-modal-view>\n');
$templateCache.put('plugins/es/templates/common/modal_location.html','<ion-modal-view class="modal-full-height modal-search-location">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n <h1 class="title" translate>LOCATION.MODAL.TITLE</h1>\n </ion-header-bar>\n\n <ion-content class="padding no-padding-xs" scroll="true">\n\n\n <!-- search text -->\n <div class="item item-input">\n <i class="icon ion-search placeholder-icon"></i>\n\n <input type="text" class="visible-xs visible-sm" placeholder="{{\'LOCATION.MODAL.SEARCH_HELP\'|translate}}" ng-model="search.text" ng-model-options="{ debounce: 650 }" ng-change="doSearch()">\n <input type="text" class="hidden-xs hidden-sm" placeholder="{{\'LOCATION.MODAL.SEARCH_HELP\'|translate}}" ng-model="search.text" on-return="doSearch()">\n </div>\n\n <div class="padding-top padding-xs" style="display: block; height: 60px">\n <div class="pull-left" ng-if="!search.loading && search.results">\n <h4 translate>COMMON.RESULTS_LIST</h4>\n </div>\n\n <div class="pull-right hidden-xs hidden-sm">\n <button class="button button-small button-stable ink" ng-click="doSearch()">\n {{\'COMMON.BTN_SEARCH\' | translate}}\n </button>\n </div>\n\n </div>\n\n <div class="center padding" ng-if="search.loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div ng-if="!search.loading && search.results && (!search.results.length || !search.results[0].address)" class="assertive padding">\n <span translate>COMMON.SEARCH_NO_RESULT</span>\n </div>\n\n <ion-list ng-if="!search.loading" class="padding-top {{::motion.ionListClass}}">\n <div ng-repeat="res in search.results" class="item item-border-large item-text-wrap ink" ng-class="::{\'item-divider\': !res.address, \'item-icon-left item-icon-right\': res.address}" ng-click="res.address ? closeModal(res) : false">\n\n <!-- if divider -->\n <h4 class="text-italic" ng-if="::!res.address" ng-bind-html="res.name"></h4>\n\n <!-- if divider -->\n <ng-if ng-if="::res.address">\n\n <i class="icon ion-location"></i>\n\n <h2 ng-if="res.address.road">\n {{::res.address.road}}\n </h2>\n <h3>\n <span ng-if="res.address.postcode">{{::res.address.postcode}}</span>\n {{::res.address.city||res.address.village}}\n <span class="gray">| {{::res.address.country}}</span>\n </h3>\n <h5 class="gray">\n {{\'LOCATION.MODAL.POSITION\'|translate:res }}\n </h5>\n\n <i class="icon ion-ios-arrow-right"></i>\n </ng-if>\n\n </div>\n </ion-list>\n </ion-content>\n\n <ion-footer-bar class="stable-bg padding-left padding-right block" ng-if="license">\n <div class="pull-right copyright">\n <span class="dark">\xA9 </span>\n <a class="positive" href="{{license.url}}" target="_blank">{{license.name}}</a>\n </div>\n </ion-footer-bar>\n</ion-modal-view>\n');
$templateCache.put('plugins/es/templates/common/popover_distances.html','<ion-popover-view class="popover-light popover-distance" style="height: {{33 + 4 * 53}}px">\n <ion-header-bar class="bar bar-header stable-bg">\n <div class="title" translate>COMMON.GEO_DISTANCE_SEARCH</div>\n </ion-header-bar>\n <ion-content scroll="true">\n <a class="item ink" ng-repeat="value in geoDistances" ng-click="selectDistance(value)">\n <b class="ion-checkmark" ng-if="search.geoDistance==value"></b>\n <b ng-if="search.geoDistance==value">{{\'COMMON.GEO_DISTANCE_OPTION\' | translate: {value: value} }}</b>\n <span ng-if="search.geoDistance!=value">{{\'COMMON.GEO_DISTANCE_OPTION\' | translate: {value: value} }}</span>\n </a>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/common/popover_profile_extend.html',' <!-- profile -->\n <button ng-if="enable" class="button button-positive button-small ink" ng-click="showEditUserProfile()">\n {{\'PROFILE.BTN_EDIT\' | translate}}\n </button>\n\n');
$templateCache.put('plugins/es/templates/common/popover_star.html','<ion-popover-view class="popover-star" style="height: {{likeData.stars.wasHit ? 90 : 50}}px">\n <ion-content scroll="false" class="padding-left padding-right">\n <h1>\n <a ng-repeat="level in [1,2,3,4,5]" ng-click="addStar(level)">\n <b class="dark ion-android-star" ng-if="level <= likeData.stars.level"></b>\n <b class="dark ion-android-star-half" ng-if="level > likeData.stars.level && level - 0.5 <= likeData.stars.level"></b>\n <b class="dark ion-android-star-outline" ng-if="level > likeData.stars.level && level - 0.5 > likeData.stars.level"></b>\n </a>\n </h1>\n <a ng-if="likeData.stars.wasHit" ng-click="removeStar(event)" translate>WOT.VIEW.BTN_STARS_REMOVE</a>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/common/popup_report_abuse.html','<form name="abuseForm" ng-submit="">\n <div class="list" ng-init="setAbuseForm(abuseForm)">\n\n <!-- reason -->\n <label class="item item-input" ng-class="{\'item-input-error\': abuseForm.$submitted && abuseForm.comment.$invalid}">\n <textarea class="padding" style="background-color: transparent" name="comment" type="text" placeholder="{{\'COMMON.REPORT_ABUSE.REASON_HELP\' | translate}}" rows="3" ng-model="abuseData.comment" ng-minlength="8" required></textarea>\n </label>\n <div class="form-errors" ng-if="abuseForm.$submitted && abuseForm.comment.$error" ng-messages="abuseForm.comment.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT"></span>\n </div>\n </div>\n\n <div class="item item-toggle item-text-wrap dark">\n <div class="input-label" translate>COMMON.REPORT_ABUSE.ASK_DELETE</div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="abuseData.delete">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n </div>\n</form>\n\n\n');
$templateCache.put('plugins/es/templates/common/view_comments.html','\n<form class="comments" ng-controller="ESCommentsCtrl" ng-submit="save()">\n\n <div class="item item-divider">\n <i class="icon ion-chatboxes"></i>\n <span translate>COMMENTS.DIVIDER</span>\n <span class="gray" ng-if="comments.total">({{comments.total}})</span>\n </div>\n\n <span class="item item-more-comments" ng-if="comments.hasMore">\n <small><a ng-click="showMore()" translate>COMMENTS.SHOW_MORE_COMMENTS</a></small>\n </span>\n\n <div class="padding-right">\n <ng-repeat ng-repeat="comment in comments.result track by comment.id" ng-include="\'plugins/es/templates/common/item_comment.html\'">\n </ng-repeat>\n </div>\n\n <div class="hidden-xs hidden-sm padding-right">\n <div class="card card-comment item item-input item-button-right">\n\n <!-- reply to comment-->\n <ng-if ng-if="formData.parent">\n <div class="padding card-header text-right pull-left" translate>COMMENTS.REPLY_TO</div><br>\n <div class="padding-left">\n <div class="card card-avatar card-avatar-small stable-900-bg item-text-wrap no-padding in done">\n <ng-include ng-if="formData.parent.message" ng-init="comment = formData.parent" src="\'plugins/es/templates/common/item_comment_content.html\'">\n </ng-include>\n <div class="item dark done in gray" ng-if="!formData.parent.message">\n {{::\'COMMENTS.DELETED_COMMENT\'|translate}}\n </div>\n <div class="card-footer text-right gray">\n <div class="pull-right">\n <a class="ion-close" ng-click="removeParentLink()">\n {{::\'COMMON.BTN_CANCEL\'|translate}}\n </a>\n </div>\n </div>\n </div>\n </div>\n </ng-if>\n <textarea class="padding" style="background-color: transparent" id="comment-form-textarea" rows="3" placeholder="{{formData.replyTo ? \'COMMENTS.COMMENT_HELP_REPLY_TO\' : \'COMMENTS.COMMENT_HELP\'|translate}}" ng-model="formData.message" ng-keypress="onKeypress($event)">\n </textarea>\n <div class="card-footer text-right">\n <button type="button" class="button button-small button-small-padding" ng-class="{\'button-positive\': formData.message.length}" ng-if="!formData.id" ng-click="save()" translate>\n COMMON.BTN_SEND\n </button>\n <!-- Edit buttons -->\n <ng-if ng-if="formData.id">\n <button type="button" class="button button-small button-small-padding" ng-click="cancel()" translate>\n COMMON.BTN_CANCEL\n </button>\n <button type="button" class="button button-small button-small-padding button-positive" ng-click="save()" translate>\n COMMON.BTN_SAVE\n </button>\n </ng-if>\n </div>\n </div>\n </div>\n\n <div class="visible-xs visible-sm" style="margin-bottom">\n <div class="block">\n <!-- reply to comment-->\n <div class="item item-input-inset done in" ng-if="formData.parent">\n <div class="padding text-right pull-left" translate>COMMENTS.REPLY_TO</div><br>\n <div class="padding-left expanded">\n <div class="card card-comment stable-900-bg item-text-wrap no-padding in done">\n <ng-include ng-if="::formData.parent.message" ng-init="comment = formData.parent" src="\'plugins/es/templates/common/item_comment_content.html\'">\n </ng-include>\n <span ng-if="::!formData.parent.message" translate>\n COMMENTS.DELETED_COMMENT\n </span>\n <div class="card-footer text-right gray">\n <div class="pull-right">\n <a class="ion-close" ng-click="removeParentLink()">\n {{::\'COMMON.BTN_CANCEL\'|translate}}\n </a>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="item item-input-inset">\n <div class="item-input-wrapper">\n <input type="text" id="comment-form-input" styl
$templateCache.put('plugins/es/templates/common/view_likes.html','\n<div class="likes">\n <!-- views -->\n <span class="gray" ng-if="likeData.views" title="{{\'COMMON.VIEWS_TEXT\'|translate: likeData.views }}">\n {{likeData.views.total ||\xA00}}\n <i class="icon ion-eye"></i>\n <ng-if ng-if="likeData.likes||likeData.dislikes">&nbsp;|&nbsp;</ng-if>\n </span>\n\n <!-- likes / dislikes -->\n <ng-if ng-if="likeData.likes||likeData.dislikes">\n <span ng-class="{\'gray\': !likeData.likes.wasHit, \'positive\': likeData.likes.wasHit}">\n <a title="{{\'COMMON.LIKES_TEXT\'|translate: likeData.likes }}" ng-click="!canEdit && toggleLike($event, {kind: \'like\'})">\n {{likeData.likes.total ||\xA00}}\n <i class="icon ion-heart"></i>\n </a>\n </span>\n <span ng-if="likeData.dislikes" ng-class="{\'gray\': !likeData.dislikes.wasHit, \'positive\': likeData.dislikes.wasHit}">\n <a title="{{\'COMMON.DISLIKES_TEXT\'|translate: likeData.dislikes }}" ng-click="!canEdit && toggleLike($event, {kind: \'dislike\'})">\n {{likeData.dislikes.total ||\xA00}}\n <i class="icon ion-heart-broken"></i>\n </a>\n </span>\n </ng-if>\n\n <!-- follow-->\n <span class="gray" ng-if="likeData.follows">&nbsp;|&nbsp;</span>\n <a ng-if="likeData.follows" ng-click="!canEdit && toggleLike($event, {kind: \'follow\'})">\n <span ng-class="{\'gray\': !likeData.follows.wasHit, \'positive\': likeData.follows.wasHit}" title="{{\'COMMON.FOLLOWS_TEXT\'|translate: follows }}">\n {{likeData.follows.total ||\xA00}}\n <i class="icon ion-android-people"></i>&nbsp;\n </span>\n <span ng-if="!canEdit" class="hidden-xs" ng-class="{\'assertive\': likeData.follows.wasHit, \'positive\': !likeData.follows.wasHit}">\n ({{likeData.follows.wasHit ? \'COMMON.BTN_STOP_FOLLOW\': \'COMMON.BTN_FOLLOW\' | translate }})\n </span>\n </a>\n <span class="gray" ng-if="likeData.abuses.total">&nbsp;|&nbsp;</span>\n <a ng-if="likeData.abuses.total && !likeData.abuses.wasHit" ng-click="!canEdit && reportAbuse($event)" title="{{\'COMMON.ABUSES_TEXT\'|translate: likeData.abuses }}">\n {{likeData.abuses.total ||\xA00}}\n <i class="icon ion-android-warning"></i>\n </a>\n <span ng-if="likeData.abuses.total && likeData.abuses.wasHit" class="assertive" title="{{\'COMMON.ABUSES_TEXT\'|translate: likeData.abuses }}">\n {{likeData.abuses.total ||\xA00}}\n <i class="icon ion-android-warning"></i>\n </span>\n</div>\n');
$templateCache.put('plugins/es/templates/common/view_pictures.html','<div ng-if="pictures && pictures.length>0" class="item gallery done in">\n <div ng-repeat="picture in pictures" class="item card card-gallery">\n <h2 ng-if="::picture.title">{{::picture.title}}</h2>\n <img ng-src="{{::picture.src}}">\n </div>\n</div>\n');
$templateCache.put('plugins/es/templates/document/item_document.html','<ion-item id="doc-{{::doc.id}}" class="item item-document item-icon-left ink item-text-wrap {{::ionItemClass}} no-padding-top no-padding-bottom" ng-click="selectDocument($event, doc)">\n\n <i class="icon ion-document stable" ng-if=":rebind:!doc.avatar"></i>\n <i class="avatar" ng-if=":rebind:doc.avatar" style="background-image: url(\'{{:rebind:doc.avatar.src}}\')"></i>\n\n <div class="row no-padding">\n\n <div class="col">\n <h3>\n <a ui-sref="app.wot_identity({pubkey: doc.pubkey, uid: doc.name})">\n <span class="positive" ng-if=":rebind:doc.name">\n <i class="ion-person"></i> {{:rebind:doc.name}}\n </span>\n </a>\n </h3>\n </div>\n\n <div class="col">\n <h3 class="dark">\n <i class="ion-locked" ng-if=":rebind:doc.nonce"></i>\n {{:rebind:doc.time|formatDate}}</h3>\n <h4 class="gray">{{:rebind:\'DOCUMENT.HASH\'|translate}} {{:rebind:doc.hash|formatHash}}</h4>\n </div>\n\n <div class="col col-50">\n <h4 class="gray">\n {{:rebind:\'DOCUMENT.LOOKUP.TYPE.\' + (doc.index + \'_\' + doc.type | uppercase) | translate}}\n </h4>\n <h4 ng-if="doc.type!=\'profile\'">\n {{:rebind:doc.title||doc.message|truncText: 150}}\n </h4>\n </div>\n\n\n\n <!--<div class="col">-->\n <!--<a-->\n <!--ng-if=":rebind:login && doc.pubkey==walletData.pubkey"-->\n <!--ng-click="remove($event, $index)"-->\n <!--class="gray pull-right"-->\n <!--title="{{\'DOCUMENT.LOOKUP.BTN_REMOVE\'|translate}}">-->\n <!--<i class="ion-trash-a"></i>-->\n <!--</a>-->\n <!--<h3 ng-if=":rebind:doc.recipient">-->\n <!--<a ui-sref="app.wot_identity({pubkey: doc.recipient.pubkey, uid: doc.recipient.name})">-->\n <!--<span class="gray">-->\n <!--<i class="ion-key"></i> {{:rebind:doc.recipient.pubkey|formatPubkey}}-->\n <!--</span>-->\n <!--<span class="positive" ng-if=":rebind:doc.recipient.name">-->\n <!--<i class="ion-person"></i> {{:rebind:doc.recipient.name}}-->\n <!--</span>-->\n <!--</a>-->\n <!--</h3>-->\n <!--<h4 class="gray" ng-if=":rebind:doc.read_signature">-->\n <!--<i class="ion-checkmark"></i>-->\n <!--<span translate>DOCUMENT.LOOKUP.READ</span>-->\n <!--</h4>-->\n\n <!--</div>-->\n\n </div>\n</ion-item>\n');
$templateCache.put('plugins/es/templates/document/item_document_comment.html','<ion-item id="doc-{{::doc.id}}" class="item item-document item-document-comment item-icon-left ink {{::ionItemClass}} no-padding-top no-padding-bottom" ng-class="{\'compacted\': compactMode}" ng-click="selectDocument($event, doc)">\n\n <i ng-show=":rebind:!compactMode" ng-if=":rebind:!doc.avatar" class="icon ion-ios-chatbubble-outline stable"></i>\n <i ng-show=":rebind:!compactMode" ng-if=":rebind:doc.avatar" class="avatar" style="background-image: url(\'{{:rebind:doc.avatar.src}}\')"></i>\n\n <div class="row no-padding">\n <div class="col">\n <h4>\n <i class="ion-ios-chatbubble-outline dark"></i>\n <span class="gray" ng-if=":rebind:doc.name">\n <i class="ion-person" ng-show=":rebind:!compactMode"></i>\n {{:rebind:doc.name}}:\n </span>\n <span class="dark">\n <i class="ion-quote" ng-if=":rebind:!compactMode"></i>\n {{:rebind:doc.message|truncText:50}}\n </span>\n </h4>\n <h4 class="gray"> <i class="ion-clock"></i> {{:rebind:doc.time|formatDate}}</h4>\n </div>\n\n <div class="col">\n <h3>\n <a ui-sref="app.wot_identity({pubkey: doc.pubkey, uid: doc.name})">\n\n </a>\n </h3>\n </div>\n\n <div class="col" ng-if=":rebind:!compactMode">\n <a ng-if=":rebind:login && doc.pubkey==walletData.pubkey" ng-click="remove($event, $index)" class="gray pull-right hidden-xs hidden-sm" title="{{\'DOCUMENT.LOOKUP.BTN_REMOVE\'|translate}}">\n <i class="ion-trash-a"></i>\n </a>\n </div>\n\n </div>\n</ion-item>\n');
$templateCache.put('plugins/es/templates/document/item_document_profile.html','<ion-item id="doc-{{::doc.id}}" class="item item-document item-icon-left ink {{::ionItemClass}} no-padding-top no-padding-bottom" ng-class="{\'compacted\': compactMode}" ng-click="selectDocument($event, doc)">\n\n <i ng-show=":rebind:!compactMode" ng-if=":rebind:doc.avatar" class="avatar" style="background-image: url({{:rebind:doc.avatar.src}})"></i>\n <i ng-show=":rebind:!compactMode" ng-if=":rebind:!doc.avatar" class="icon ion-person stable"></i>\n\n <div class="row no-padding">\n <div class="col">\n <h4 ng-if=":rebind:doc.title">\n <i class="ion-person gray"></i>\n <span class="dark">\n {{:rebind:doc.title}}\n </span>\n <span class="gray">\n {{:rebind:\'DOCUMENT.LOOKUP.HAS_REGISTERED\'|translate}}\n </span>\n </h4>\n <h4>\n <span class="dark" ng-if=":rebind:doc.city">\n <i class="ion-location"></i> {{:rebind:doc.city}}\n </span>\n <span class="gray">\n <i class="ion-clock"></i> {{:rebind:doc.time|formatDate}}\n </span>\n </h4>\n </div>\n\n <div class="col" ng-if=":rebind:!compactMode">\n <a ng-if=":rebind:login && doc.pubkey==walletData.pubkey" ng-click="remove($event, $index)" class="gray pull-right" title="{{\'DOCUMENT.LOOKUP.BTN_REMOVE\'|translate}}">\n <i class="ion-trash-a"></i>\n </a>\n </div>\n\n </div>\n</ion-item>\n');
$templateCache.put('plugins/es/templates/document/items_documents.html','\n<div class="item row row-header done in hidden-xs hidden-sm">\n\n <a class="no-padding dark col col-header" ng-if=":rebind:expertMode" ng-click="toggleSort(\'issuer\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'issuer\'"></cs-sort-icon>\n {{\'DOCUMENT.LOOKUP.HEADER_ISSUER\' | translate}}\n </a>\n <a class="no-padding dark col col-header" ng-if=":rebind:expertMode" ng-click="toggleSort(\'time\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'time\'"></cs-sort-icon>\n {{\'DOCUMENT.LOOKUP.HEADER_TIME\' | translate}}\n </a>\n <div class="no-padding dark col col-50 col-header" ng-if=":rebind:expertMode">\n <span class="gray">{{\'DOCUMENT.LOOKUP.DOCUMENT_TYPE\' | translate}} /</span> {{\'DOCUMENT.LOOKUP.DOCUMENT_TITLE\' | translate}}\n </div>\n</div>\n\n<div class="padding gray" ng-if=":rebind:!search.loading && !search.results.length" translate>\n COMMON.SEARCH_NO_RESULT\n</div>\n\n<!-- for each doc -->\n<ng-repeat ng-repeat="doc in :rebind:search.results track by doc.id" ng-switch on="doc.type">\n <div ng-switch-when="comment">\n <ng-include src="::\'plugins/es/templates/document/item_document_comment.html\'"></ng-include>\n </div>\n <div ng-switch-when="profile">\n <ng-include src="::\'plugins/es/templates/document/item_document_profile.html\'"></ng-include>\n </div>\n <div ng-switch-default>\n <ng-include src="::\'plugins/es/templates/document/item_document.html\'"></ng-include>\n </div>\n</ng-repeat>\n');
$templateCache.put('plugins/es/templates/document/lookup.html','<ion-view>\n <ion-nav-title>\n <span translate>DOCUMENT.LOOKUP.TITLE</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n\n <button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n\n </ion-nav-buttons>\n\n <ion-content class="padding no-padding-xs" scroll="true">\n\n <ng-include src="\'plugins/es/templates/document/lookup_form.html\'"></ng-include>\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/document/lookup_form.html','<div class="lookupForm">\n\n\n <div class="item no-padding">\n\n <!--<div class="button button-small button-text button-stable button-icon-event padding no-padding-right ink"\n ng-repeat="filter in search.filters" ng-if="filter">\n <span ng-bind-html="\'DOCUMENT.LOOKUP.TX_SEARCH_FILTER.\'+filter.type|translate:filter"></span>\n <i class="icon ion-close" ng-click="itemRemove($index)"></i>\n\n </div>-->\n\n <label class="item-input">\n <i class="icon ion-search placeholder-icon"></i>\n <input type="text" class="visible-xs visible-sm" placeholder="{{\'DOCUMENT.LOOKUP.SEARCH_HELP\'|translate}}" ng-model="search.text" ng-model-options="{ debounce: 650 }" ng-change="doSearchText()">\n <input type="text" class="hidden-xs hidden-sm" id="{{searchTextId}}" placeholder="{{\'DOCUMENT.LOOKUP.SEARCH_HELP\'|translate}}" ng-model="search.text" on-return="doSearchText()">\n <div class="helptip-anchor-center">\n <a id="{{helptipPrefix}}-search-text"></a>\n </div>\n\n </label>\n </div>\n\n\n <div class="padding-top padding-xs" style="display: block; height: 60px">\n <div class="pull-left">\n <h4 ng-if="search.last" translate>\n DOCUMENT.LOOKUP.LAST_DOCUMENTS\n </h4>\n <h4 ng-if="!search.last">\n {{\'COMMON.RESULTS_LIST\'|translate}}\n </h4>\n <h5 class="dark" ng-if="!search.loading && search.total">\n <span translate="COMMON.RESULTS_COUNT" translate-values="{count: search.total}"></span>\n <small class="gray" ng-if=":rebind:search.took && expertMode">\n - {{:rebind:\'COMMON.EXECUTION_TIME\'|translate: {duration: search.took} }}\n </small>\n <small class="gray" ng-if=":rebind:expertMode && search.filters && search.filters.length">\n - <a ng-click="toggleShowQuery()" ng-if="!showQuery">\n <span translate>DOCUMENT.LOOKUP.SHOW_QUERY</span>\n <i class="icon ion-arrow-down-b gray"></i>\n </a>\n <a ng-click="toggleShowQuery()" ng-if="showQuery">\n <span translate>DOCUMENT.LOOKUP.HIDE_QUERY</span>\n <i class="icon ion-arrow-up-b gray"></i>\n </a>\n </small>\n </h5>\n <h5 class="gray" ng-if="search.loading">\n <ion-spinner class="icon ion-spinner-small" icon="android"></ion-spinner>\n <span translate>COMMON.SEARCHING</span>\n <br>\n </h5>\n </div>\n\n <div class="pull-right hidden-xs hidden-sm">\n <a class="button button-text button-small ink" ng-if="login" ng-click="showActionsPopover($event)">\n {{\'DOCUMENT.LOOKUP.BTN_ACTIONS\' | translate}}\n <i class="icon ion-arrow-down-b"></i>\n </a>\n &nbsp;\n <button class="button button-small button-stable ink" ng-click="doSearchText()">\n {{\'COMMON.BTN_SEARCH\' | translate:search}}\n </button>\n </div>\n </div>\n\n <div class="item no-border no-padding" ng-if=":rebind:search.filters && search.filters.length && expertMode">\n <small class="no-padding no-margin" ng-if="showQuery">\n <span class="gray text-wrap dark">{{:rebind:search.query}}</span>\n </small>\n </div>\n\n <ion-list class="list" ng-class="::motion.ionListClass">\n\n <ng-include src="\'plugins/es/templates/document/items_documents.html\'"></ng-include>\n\n </ion-list>\n\n <ion-infinite-scroll ng-if="search.hasMore" spinner="android" on-infinite="showMore()" distance="1%">\n </ion-infinite-scroll>\n\n</div>');
$templateCache.put('plugins/es/templates/document/lookup_popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>DOCUMENT.LOOKUP.POPOVER_ACTIONS.TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n <a class="item item-icon-left assertive ink" ng-class="{\'gray\': !search.total}" ng-click="removeAll()">\n <i class="icon ion-trash-a"></i>\n {{\'DOCUMENT.LOOKUP.POPOVER_ACTIONS.REMOVE_ALL\' | translate}}\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/group/edit_group.html','<ion-view left-buttons="leftButtons">\n <ion-nav-title>\n <span class="visible-xs" ng-if="id" ng-bind-html="formData.title"></span>\n <span class="visible-xs" ng-if="!loading && !id" translate>GROUP.EDIT.TITLE_NEW</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear visible-xs visible-sm" ng-class="{\'ion-android-send\':!id, \'ion-android-done\': id}" ng-click="save()">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true">\n <div class="row no-padding">\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col">\n <!-- loading -->\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <form name="recordForm" novalidate="" ng-submit="save()">\n\n <!-- -->\n <div class="list" ng-class="motion.ionListClass" ng-init="setForm(recordForm)">\n\n <div class="item hidden-xs">\n <h1 ng-if="id" ng-bind-html="formData.title"></h1>\n <h1 ng-if="!id" translate>GROUP.EDIT.TITLE_NEW</h1>\n <h2 class="balanced" ng-if="!id">\n <i class="icon ion-android-people"></i>\n <i class="icon ion-android-lock" ng-if="formData.type==\'managed\'"></i>\n {{\'GROUP.TYPE.ENUM.\'+formData.type|upper|translate}}\n </h2>\n </div>\n <div class="item" ng-if="id">\n <h4 class="gray">\n <i class="icon ion-calendar"></i>\n {{\'COMMON.LAST_MODIFICATION_DATE\'|translate}}&nbsp;{{formData.time | formatDate}}\n </h4>\n <div class="badge badge-balanced badge-editable" ng-click="showRecordTypeModal()">\n {{\'GROUP.TYPE.ENUM.\'+formData.type|upper|translate}}\n </div>\n </div>\n\n <!-- pictures -->\n <ng-include src="\'plugins/es/templates/common/edit_pictures.html\'"></ng-include>\n\n <div class="item item-divider" translate>GROUP.GENERAL_DIVIDER</div>\n\n <!-- title -->\n <div class="item item-input item-floating-label" ng-class="{\'item-input-error\': form.$submitted && form.title.$invalid}">\n <span class="input-label" translate>GROUP.EDIT.RECORD_TITLE</span>\n <input type="text" placeholder="{{\'GROUP.EDIT.RECORD_TITLE_HELP\'|translate}}" name="title" id="group-record-title" ng-model="formData.title" ng-minlength="3" ng-required="true">\n </div>\n <div class="form-errors" ng-if="form.$submitted && form.title.$error" ng-messages="form.title.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT"></span>\n </div>\n </div>\n\n <!-- description -->\n <div class="item item-input item-floating-label">\n <span class="input-label" translate>GROUP.EDIT.RECORD_DESCRIPTION</span>\n <textarea placeholder="{{\'GROUP.EDIT.RECORD_DESCRIPTION_HELP\'|translate}}" ng-model="formData.description" rows="8" cols="10">\n </textarea>\n </div>\n\n <!-- social networks -->\n <ng-include src="\'plugins/es/templates/common/edit_socials.html\'"></ng-include>\n\n </div>\n\n <div class="padding hidden-xs hidden-sm text-right">\n <button class="button button-clear button-dark ink" ng-click="cancel()" type="button" translate>\n COMMON.BTN_CANCEL\n </button>\n <button class="button button-positive button-raised ink" type="submit" ng-if="!id" translate>\n
$templateCache.put('plugins/es/templates/group/item_group.html','<a name="group-{{:rebind:group.hash}}"></a>\n<ion-item id="group-{{:rebind:block.hash}}" class="item item-icon-left item-group {{ionItemClass}}" ng-click="select(group)">\n\n <i class="icon ion-cube stable" ng-if=":rebind:(!group.empty && !group.avatar)"></i>\n <i class="avatar" ng-if=":rebind:!group.empty && group.avatar" style="background-image: url(\'{{:rebind:block.avatar.src}}\')"></i>\n\n <div class="row no-padding">\n <div class="col">\n <h4 class="dark">\n <i class="ion-clock"></i>\n {{:rebind:group.creationTime|formatDate}}\n </h4>\n <h4>\n <!-- membersCount -->\n <i class="dark ion-person"></i>\n <span class="dark" ng-if=":rebind:group.membersCount">+{{:rebind:group.membersCount}}</span>\n </h4>\n </div>\n\n <div class="col col-33 positive hidden-md">\n <h4><i class="ion-person"></i> <span ng-bind-html=":rebind:group.title"></span></h4>\n </div>\n\n </div>\n</ion-item>\n');
$templateCache.put('plugins/es/templates/group/items_groups.html','\n\n<div class="item row row-header hidden-xs hidden-sm" ng-if="expertMode">\n\n <a class="no-padding dark col col-header" ng-click="toggleSort(\'medianTime\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'medianTime\'"></cs-sort-icon>\n {{\'GROUP.LOOKUP.HEADER_CREATION_TIME\' | translate}}\n </a>\n <a class="no-padding dark col col-header" ng-click="toggleSort(\'issuer\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'issuer\'"></cs-sort-icon>\n {{\'GROUP.LOOKUP.HEADER_ISSUER\' | translate}}\n </a>\n <div class="col col-20">&nbsp;\n </div>\n <a class="no-padding dark col col-20 col-header" ng-click="toggleSort(\'number\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'number\'"></cs-sort-icon>\n {{\'GROUP.LOOKUP.HEADER_NAME\' | translate}}\n </a>\n</div>\n\n<div class="padding gray" ng-if=":rebind:!search.loading && !search.results.length" translate>\n COMMON.SEARCH_NO_RESULT\n</div>\n\n<ng-repeat ng-repeat="group in :rebind:search.results" ng-include="\'plugins/es/templates/group/item_group.html\'">\n</ng-repeat>\n');
$templateCache.put('plugins/es/templates/group/list.html','<ion-list class="{{::motion.ionListClass}}">\n\n <ion-item ng-repeat="notification in search.results" class="item-border-large item-text-wrap ink item-avatar" ng-class="{\'unread\': !notification.read}" ng-click="select(notification)">\n\n <i ng-if="!notification.avatar" class="item-image icon {{::notification.avatarIcon}}"></i>\n <i ng-if="notification.avatar" class="item-image avatar" style="background-image: url({{::notification.avatar.src}})"></i>\n\n <h3 trust-as-html="notification.message | translate:notification"></h3>\n <h4>\n <i class="icon {{notification.icon}}"></i>&thinsp;<span class="dark">{{notification.time|formatFromNow}}</span>\n <span class="gray">| {{notification.time|formatDate}}</span>\n </h4>\n </ion-item>\n</ion-list>\n\n<ion-infinite-scroll ng-if="!search.loading && search.hasMore" spinner="android" on-infinite="showMore()" distance="1%">\n</ion-infinite-scroll>\n');
$templateCache.put('plugins/es/templates/group/lookup.html','<ion-view class="view-group">\n <ion-nav-title>\n <span translate>GROUP.LOOKUP.TITLE</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n\n <button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n\n </ion-nav-buttons>\n\n <ion-content class="padding no-padding-xs" scroll="true">\n <ng-include src="\'plugins/es/templates/group/lookup_form.html\'"></ng-include>\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/group/lookup_form.html','<div class="lookupForm">\n\n <button class="button button-small button-positive button-clear ink pull-right padding-right hidden-sm hidden-xs" ng-click="showNewRecordModal()">\n <i class="icon ion-plus"></i>\n {{\'GROUP.LOOKUP.BTN_NEW\' | translate}}\n </button>\n\n <!-- search text-->\n <label class="item item-input">\n <i class="icon ion-search placeholder-icon"></i>\n <input type="text" class="visible-xs visible-sm" placeholder="{{\'GROUP.LOOKUP.SEARCH_HELP\'|translate}}" ng-model="search.text" ng-model-options="{ debounce: 650 }" ng-change="doSearchText()">\n <input type="text" class="hidden-xs hidden-sm" id="{{searchTextId}}" placeholder="{{\'GROUP.LOOKUP.SEARCH_HELP\'|translate}}" ng-model="search.text" on-return="doSearchText()">\n <div class="helptip-anchor-center">\n <a id="helptip-group-search-text"></a>\n </div>\n\n </label>\n\n <div class="padding-top padding-xs" style="display: block; height: 60px">\n <div class="pull-left">\n <h4 ng-if="search.type==\'open\'" translate>\n GROUP.LOOKUP.OPEN_RESULTS_LIST\n </h4>\n <h4 ng-if="search.type==\'last\'" translate>\n GROUP.LOOKUP.LAST_RESULTS_LIST\n </h4>\n <h4 ng-if="search.type==\'managed\'" translate>\n GROUP.LOOKUP.MANAGED_RESULTS_LIST\n </h4>\n <h4 ng-if="search.type==\'text\'">\n {{\'COMMON.RESULTS_LIST\'|translate}}\n </h4>\n <h5 class="dark" ng-if="!search.loading && search.total">\n <span translate="COMMON.RESULTS_COUNT" translate-values="{count: search.total}"></span>\n <small class="gray" ng-if=":rebind:search.took && expertMode">\n - {{:rebind:\'COMMON.EXECUTION_TIME\'|translate: {duration: search.took} }}\n </small>\n </h5>\n <h5 class="gray" ng-if="search.loading">\n <ion-spinner class="icon ion-spinner-small" icon="android"></ion-spinner>\n <span translate>COMMON.SEARCHING</span>\n <br>\n </h5>\n </div>\n\n <div class="pull-right hidden-xs hidden-sm">\n <a ng-if="enableFilter" class="button button-text button-small ink icon ion-clock" ng-class="{\'button-text-positive\': search.type==\'last\'}" ng-click="doSearchLast()">\n {{\'GROUP.LOOKUP.BTN_LAST\' | translate}}\n </a>\n &nbsp;\n <button class="button button-small button-stable ink" ng-click="doSearchText()">\n {{\'COMMON.BTN_SEARCH\' | translate:search}}\n </button>\n </div>\n </div>\n\n <ion-list class="list {{ionListClass}}">\n\n <ng-include src="\'plugins/es/templates/group/items_groups.html\'"></ng-include>\n\n </ion-list>\n\n <ion-infinite-scroll ng-if="search.hasMore" spinner="android" on-infinite="showMore()" distance="1%">\n </ion-infinite-scroll>\n\n</div>');
$templateCache.put('plugins/es/templates/group/modal_record_type.html','<ion-modal-view>\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n <h1 class="title" translate>GROUP.TYPE.TITLE</h1>\n </ion-header-bar>\n\n <ion-content class="lookupForm padding">\n <h3 translate>GROUP.TYPE.SELECT_TYPE</h3>\n\n \t<div class="list">\n\n <!-- open group -->\n <div class="item item-complex card stable-bg item-icon-left ink" ng-click="closeModal(\'open\')">\n <div class="item-content item-text-wrap">\n <i class="item-image icon ion-android-people dark"></i>\n <h2 translate>GROUP.TYPE.OPEN_GROUP</h2>\n <h4 class="gray" translate>GROUP.TYPE.OPEN_GROUP_HELP</h4>\n </div>\n </div>\n\n <!-- managed group -->\n <div class="item item-complex card stable-bg item-icon-left ink" ng-click="closeModal(\'managed\')">\n <div class="item-content item-text-wrap">\n <i class="item-image icon ion-android-people dark"></i>\n <i class="icon-secondary ion-android-lock dark" style="left: 10px; top: -8px"></i>\n <h2 translate>GROUP.TYPE.MANAGED_GROUP</h2>\n <h4 class="gray" translate>GROUP.TYPE.MANAGED_GROUP_HELP</h4>\n </div>\n </div>\n\n </div>\n</ion-content>\n</ion-modal-view>\n');
$templateCache.put('plugins/es/templates/group/view_record.html','<ion-view left-buttons="leftButtons">\n <ion-nav-title>\n\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-bar button-icon button-clear visible-xs visible-sm" ng-click="edit()" ng-if="canEdit">\n <i class="icon ion-android-create"></i>\n </button>\n <button class="button button-bar button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true">\n <div class="positive-900-bg hero">\n <div class="content" ng-if="!loading">\n <i class="avatar cion-registry-{{formData.type}}" ng-if="!formData.thumbnail"></i>\n <i class="avatar" style="background-image: url({{::formData.thumbnail.src}})" ng-if="formData.thumbnail"></i>\n <h3 ng-bind-html="formData.title"></h3>\n <h4>&nbsp;</h4>\n </div>\n <h4 class="content light" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </h4>\n </div>\n\n <div class="row no-padding-xs">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n\n <div class="col list item-text-wrap no-padding-xs" ng-class="motion.ionListClass">\n\n <div class="item">\n <h2 class="gray">\n <a ng-if="formData.city" ui-sref="app.groups({location:formData.city})">\n <i class="icon ion-location"></i>\n <span ng-bind-html="formData.city"></span>\n </a>\n <span ng-if="formData.city && formData.type">&nbsp;|&nbsp;</span>\n <a ng-if="formData.type" ui-sref="app.groups({type:formData.type})">\n <i class="icon ion-flag"></i>\n {{\'GROUP.TYPE.ENUM.\'+formData.type|upper|translate}}\n </a>\n </h2>\n <h4>\n <i class="icon ion-clock" ng-if="formData.time"></i>\n <span translate>COMMON.SUBMIT_BY</span>\n <a ng-class="{\'positive\': issuer.uid, \'gray\': !issuer.uid}" ui-sref="app.wot_identity({pubkey:issuer.pubkey, uid: issuer.name||issuer.uid})">\n <ng-if ng-if="issuer.uid">\n <i class="icon ion-person"></i>\n {{::issuer.name||issuer.uid}}\n </ng-if>\n <span ng-if="!issuer.uid">\n <i class="icon ion-key"></i>\n {{issuer.pubkey|formatPubkey}}\n </span>\n </a>\n <span>\n {{formData.time|formatFromNow}}\n <h4 class="gray hidden-xs">|\n {{formData.time | formatDate}}\n </h4>\n </span>\n </h4>\n </div>\n\n <!-- Buttons bar-->\n <div class="item large-button-bar hidden-xs hidden-sm">\n <button class="button button-stable button-small-padding icon ion-android-share-alt" ng-click="showSharePopover($event)">\n </button>\n <button class="button button-calm ink-dark" ng-if="formData.pubkey && !isUserPubkey(formData.pubkey)" ng-click="showTransferModal({pubkey:formData.pubkey, uid: formData.title})">\n {{\'COMMON.BTN_SEND_MONEY\' | translate}}\n </button>\n <button class="button button-stable icon-left ink-dark" ng-if="canEdit" ng-click="delete()">\n <i class="icon ion-trash-a assertive"></i>\n <span class="assertive"> {{\'COMMON.BTN_DELETE\' | translate}}</span>\n </button>\n <button class="button button-calm icon-left ion-android-create ink" ng-if="canEdit" ng-click="edit()">\n {{\'COMMON.BTN_EDIT\' | translate}}\n </button>\n </div>\n\n <ion-item>\n <h2>\n <span class="text-keep-lines" ng-bind-html="formData.description"></span>\n </h2>\n </ion-item>\n\n <ion-item>\n <h4 ng-if="formData.address">\n <span class="gray" translate>REGISTRY.VIEW.LOCATION</span>\n <a class="positive" target="_bl
$templateCache.put('plugins/es/templates/join/modal_join_extend.html','<ng-if ng-if=":state:enable && extensionPoint === \'select-account-type\'">\n\n <!-- ornigzation wallet -->\n <div class="item item-complex card stable-bg item-icon-left item-icon-right ink" ng-class="{ activated: accountTypeMember != null && !accountTypeMember }" ng-click="selectAccountType(\'organization\')">\n <div class="item-content item-text-wrap">\n <i class="item-image icon dark cion-registry-association"></i>\n <h2 translate>ACCOUNT.NEW.ORGANIZATION_ACCOUNT</h2>\n <h4 class="gray" translate>ACCOUNT.NEW.ORGANIZATION_ACCOUNT_HELP</h4>\n <i class="icon dark ion-ios-arrow-right"></i>\n </div>\n </div>\n\n</ng-if>\n\n<!-- Add a slide -->\n<ng-if ng-if=":state:enable && extensionPoint === \'last-slide\'">\n\n <!-- STEP 6: organization type -->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n\n <p>TOTO</p>\n\n </ion-content>\n </ion-slide-page>\n\n</ng-if>\n');
$templateCache.put('plugins/es/templates/message/compose.html','<ion-view left-buttons="leftButtons" id="composeMessage">\n <ion-nav-title>\n <span class="visible-xs visible-sm" nf-if="!isReply" translate>MESSAGE.COMPOSE.TITLE</span>\n <span class="visible-xs visible-sm" nf-if="isReply" translate>MESSAGE.COMPOSE.TITLE_REPLY</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear icon ion-android-send visible-xs" ng-click="doSend()">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true">\n <div class="row">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n <div class="col">\n <h2 class="hidden-xs hidden-sm">\n {{\'MESSAGE.COMPOSE.SUB_TITLE\'|translate}}\n </h2>\n <h4 class="hidden-xs hidden-sm">&nbsp;</h4>\n <ng-include src="\'plugins/es/templates/message/compose_form.html\'"></ng-include>\n </div>\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n </div>\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/message/compose_form.html',' <form name="messageForm" novalidate="" ng-submit="doSend()">\n\n <div class="list no-margin" ng-init="setForm(messageForm)">\n\n <a class="item item-icon-right gray ink" ng-class="{\'item-input-error\': form.$submitted && !formData.destPub}" ng-click="showWotLookupModal()">\n <span class="gray" translate>MESSAGE.COMPOSE.TO</span>\n <span class="badge badge-royal" ng-if="destUid"><i class="ion-person"></i>&nbsp;{{destUid}}</span>&nbsp;\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n <div class="form-errors" ng-if="form.$submitted && !formData.destPub">\n <div class="form-error">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <div class="item item-text-wrap">\n <span class="gray" translate>TRANSFER.FROM</span>\n <span class="badge badge-balanced">\n <ion-spinner icon="android" ng-if="!$root.walletData.pubkey"></ion-spinner>\n <span ng-if="$root.walletData.pubkey && !$root.walletData.name">\n {{$root.walletData.pubkey| formatPubkey}}&nbsp;&nbsp;\n </span>\n <span ng-if="$root.walletData.pubkey && $root.walletData.name">\n {{$root.walletData.name}}\n </span>\n </span>\n </div>\n\n <!-- Object -->\n <div class="item item-input" ng-class="{\'item-input-error\': form.$submitted && form.title.$invalid}">\n <!--<span class="input-label">{{\'MESSAGE.COMPOSE.OBJECT\' | translate}}</span>-->\n <input type="text" placeholder="{{\'MESSAGE.COMPOSE.OBJECT_HELP\' | translate}}" name="title" ng-model="formData.title" ng-maxlength="256" required>\n \n </div>\n <div class="form-errors" ng-show="form.$submitted && form.title.$error" ng-messages="form.title.$error">\n <div class="form-error" ng-message="maxlength">\n <span translate="MESSAGE.ERROR.MESSAGE_CONTENT_TOO_LONG" translate-values="{maxLength: 256}"></span>\n </div>\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- Content -->\n <div class="item item-input item-floating-label" ng-class="{\'item-input-error\': form.$submitted && form.content.$invalid}">\n <span class="input-label">{{\'MESSAGE.COMPOSE.MESSAGE\' | translate}}</span>\n <textarea placeholder="{{\'MESSAGE.COMPOSE.MESSAGE_HELP\' | translate}}" name="content" ng-model="formData.content" rows="8" ng-maxlength="5000">\n </textarea>\n </div>\n <div class="form-errors" ng-show="form.$submitted && form.content.$error" ng-messages="form.content.$error">\n <div class="form-error" ng-message="maxlength">\n <span translate="MESSAGE.ERROR.MESSAGE_CONTENT_TOO_LONG" translate-values="{maxLength: 5000}"></span>\n </div>\n </div>\n\n </div>\n\n <div class="padding hidden-xs text-right">\n <button class="button button-clear button-dark ink" ng-click="cancel()" type="button" translate>COMMON.BTN_CANCEL</button>\n <button class="button button-positive ink" type="submit" translate>TRANSFER.BTN_SEND</button>\n </div>\n\n <!-- Encryption info -->\n <div class="list no-padding">\n <div class="item item-icon-left item-text-wrap">\n <i class="icon ion-ios-information-outline positive"></i>\n <h4 class="positive" translate>MESSAGE.COMPOSE.ENCRYPTED_HELP</h4>\n </div>\n </div>\n </form>\n\n');
$templateCache.put('plugins/es/templates/message/list.html','<ion-view left-buttons="leftButtons" class="view-messages">\n <ion-nav-title>\n <span translate>MESSAGE.LIST.TITLE</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="load()">\n </button>\n\n <button class="button button-icon button-clear visible-xs visible-sm" ng-click="showActionsPopover($event)">\n <i class="icon ion-android-more-vertical"></i>\n </button>\n </ion-nav-buttons>\n\n <ion-content class="padding no-padding-xs">\n <ion-refresher pulling-text="{{\'COMMON.BTN_REFRESH\' | translate}}" on-refresh="refresh(true)">\n </ion-refresher>\n\n <!-- Buttons bar-->\n <ion-list>\n <div class="item large-button-bar hidden-xs hidden-sm">\n\n <button class="button button-stable button-small-padding icon ion-loop" ng-click="load()">\n </button>\n\n <button class="button button-calm icon ion-compose" ng-click="showNewMessageModal()">\n {{\'MESSAGE.BTN_COMPOSE\' | translate}}\n </button>\n\n <button class="button button-stable icon-right ink" ng-click="showActionsPopover($event)">\n &nbsp; <i class="icon ion-android-more-vertical"></i>&nbsp;\n {{\'COMMON.BTN_OPTIONS\' | translate}}\n </button>\n </div>\n\n <div class="pull-right hidden-xs hidden-sm">\n <a class="button button-text button-small ink icon ion-archive" ng-class="{\'button-text-positive\': type==\'inbox\'}" ng-click="setType(\'inbox\')">\n {{\'MESSAGE.LIST.INBOX\' | translate}}\n </a>\n &nbsp;\n <a class="button button-text button-small ink icon ion-paper-airplane" ng-class="{\'button-text-positive\': type==\'outbox\'}" ng-click="setType(\'outbox\')" class="badge-balanced">\n {{\'MESSAGE.LIST.OUTBOX\' | translate}}\n </a>\n </div>\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n </ion-list>\n\n <ion-list class="{{::motion.ionListClass}}" can-swipe="$root.device.enable" ng-hide="loading">\n\n <div class="padding gray" ng-if="!messages.length">\n <span ng-if="type==\'inbox\'" translate>MESSAGE.NO_MESSAGE_INBOX</span>\n <span ng-if="type==\'outbox\'" translate>MESSAGE.NO_MESSAGE_OUTBOX</span>\n </div>\n\n <ion-item class="item item-border-large item-avatar item-icon-right ink" ng-repeat="msg in messages" ui-sref="app.user_view_message({type:type, id:msg.id})">\n\n <i ng-if="::!msg.avatar" class="item-image icon" ng-class="{\'ion-person\': msg.uid, \'ion-email\': !msg.uid}"></i>\n <i ng-if="::msg.avatar" class="item-image avatar" style="background-image: url({{::msg.avatar.src}})"></i>\n\n <h4 class="pull-right hidden-xs hidden-sm">\n <span class="dark"><i class="ion-clock"></i> {{::msg.time|formatFromNow}}</span>\n <span class="gray">| {{::msg.time|formatDate}}</span>\n </h4>\n <h4 class="pull-right visible-xs visible-sm dark"><i class="ion-clock"></i> {{::msg.time|formatFromNow}}</h4>\n <h3>\n <a class="positive" ng-if="::msg.name||msg.uid" ui-sref="app.wot_identity({pubkey:msg.issuer, uid:msg.name||msg.uid})">\n <i class="ion-person"></i>\n {{::msg.name||msg.uid}}\n </a>\n <a class="gray" ng-if="::!msg.name && !msg.uid" ui-sref="app.wot_identity({pubkey:msg.issuer})">\n <i class="ion-key"></i>\n {{::msg.issuer|formatPubkey}}\n </a>\n </h3>\n <h2 ng-class="{\'unread\': !msg.read}">{{::msg.title}}</h2>\n <p>{{::msg.summary||msg.content}}</p>\n <i class="icon ion-ios-arrow-right"></i>\n <ion-option-button class="button-stable" ng-click="showReplyModal($index)" translate>MESSAGE.BTN_REPLY</ion-option-button>\n <ion-option-button class="button-assertive" ng-click="delete($index)" translate>COMMON.BTN_DELETE</ion-option-button>\n\n </ion
$templateCache.put('plugins/es/templates/message/lookup_popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>MESSAGE.LIST.POPOVER_ACTIONS.TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n <a class="item item-icon-left ink" ng-class="{\'gray\': (type != \'inbox\' && !messages.length)}" ng-click="markAllAsRead()">\n <i class="icon ion-android-checkmark-circle"></i>\n {{\'COMMON.NOTIFICATIONS.MARK_ALL_AS_READ\' | translate}}\n </a>\n\n <a class="item item-icon-left assertive ink" ng-class="{\'gray\': !messages.length}" ng-click="deleteAll()">\n <i class="icon ion-trash-a"></i>\n {{\'MESSAGE.LIST.POPOVER_ACTIONS.DELETE_ALL\' | translate}}\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/message/modal_compose.html','<ion-modal-view id="composeMessage" class="modal-full-height">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear visible-xs" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n <h1 class="title" ng-if="!isReply" translate>MESSAGE.COMPOSE.TITLE</h1>\n <h1 class="title" ng-if="isReply" translate>MESSAGE.COMPOSE.TITLE_REPLY</h1>\n\n <button class="button button-icon button-clear icon ion-android-send visible-xs" ng-click="doSend()">\n </button>\n </ion-header-bar>\n\n <ion-content scroll="true">\n <ng-include src="\'plugins/es/templates/message/compose_form.html\'"></ng-include>\n </ion-content>\n</ion-modal-view>\n');
$templateCache.put('plugins/es/templates/message/popover_message.html','<ion-popover-view class="fit hidden-xs hidden-sm popover-notification" ng-controller="PopoverMessageCtrl">\n <ion-header-bar class="stable-bg block">\n <div class="title" translate>MESSAGE.NOTIFICATIONS.TITLE</div>\n\n <div class="pull-right">\n <a class="positive" ng-click="showNewMessageModal()" translate>MESSAGE.BTN_COMPOSE</a>\n </div>\n </ion-header-bar>\n <ion-content scroll="true">\n <div class="center" ng-if="search.loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n <div class="padding gray" ng-if="!search.loading && !search.results.length" translate>\n MESSAGE.NO_MESSAGE_INBOX\n </div>\n\n <ion-list>\n\n <ion-item ng-repeat="notification in search.results" class="item-border-large item-text-wrap ink item-avatar" ng-class="{\'unread\': !notification.read}" ng-click="select(notification)">\n\n <i ng-if="::!notification.avatar" class="item-image icon ion-email"></i>\n <i ng-if="::notification.avatar" class="item-image avatar" style="background-image: url({{::notification.avatar.src}})"></i>\n\n <h3>\n <span translate>MESSAGE.NOTIFICATIONS.MESSAGE_RECEIVED</span>\n <span class="positive" ng-if="::notification.name||notification.uid"><i class="ion-person"></i>&thinsp;{{::notification.name||notification.uid}}</span>\n <span class="gray" ng-if="::!notification.name && !notification.uid"><i class="ion-key"></i>&thinsp;{{::notification.issuer|formatPubkey}}</span>\n </h3>\n <h4>\n <i class="icon ion-archive balanced"></i>&thinsp;<span class="dark">{{notification.time|formatFromNow}}</span>\n <span class="gray">| {{::notification.time|formatDate}}</span>\n </h4>\n </ion-item>\n </ion-list>\n\n <ion-infinite-scroll ng-if="!search.loading && search.hasMore" spinner="android" on-infinite="showMore()" distance="1%">\n </ion-infinite-scroll>\n </ion-content>\n\n <ion-footer-bar class="stable-bg block">\n\n <!-- show all -->\n <div class="pull-right">\n <a class="positive" ui-sref="app.user_message" ng-click="closePopover()" translate>COMMON.NOTIFICATIONS.SHOW_ALL</a>\n </div>\n </ion-footer-bar>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/message/view_message.html','<ion-view left-buttons="leftButtons" class="view-message">\n <ion-nav-title>\n <span translate>MESSAGE.VIEW.TITLE</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n\n <button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true">\n\n <div class="row no-padding">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col no-padding">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list animate-fade-slide-in item-text-wrap">\n\n <!-- Buttons bar-->\n <div class="item large-button-bar hidden-xs hidden-sm">\n <button class="button button-stable icon-left ink-dark" ng-click="delete()">\n <i class="icon ion-trash-a assertive"></i>\n <span class="assertive"> {{\'COMMON.BTN_DELETE\' | translate}}</span>\n </button>\n <button class="button button-stable icon ion-reply" ng-click="showReplyModal()">\n {{\'MESSAGE.BTN_REPLY\' | translate}}\n </button>\n <!--<button class="button button-small button-stable icon ion-reply"\n ng-click="showForwardModal()">\n {{\'MESSAGE.BTN_FORWARD\' | translate}}\n </button>-->\n </div>\n\n <div class="item item-avatar" ng-class="{\'item-avatar\': formData.avatar}">\n\n <i ng-if="!formData.avatar" class="item-image ion-person"></i>\n <i ng-if="formData.avatar" class="item-image avatar" style="background-image: url({{::formData.avatar.src}})"></i>\n\n <h1 class="title hidden-xs hidden-sm" ng-bind-html="formData.title"></h1>\n <h4>\n {{type == \'inbox\' ? \'MESSAGE.VIEW.SENDER\': \'MESSAGE.VIEW.RECIPIENT\'|translate}}\n <a class="positive" ui-sref="app.wot_identity({pubkey: (type == \'inbox\') ? formData.issuer : formData.recipient, uid: formData.name||formData.uid})">\n <span ng-if="formData.name||formData.uid">\n <i class="ion-person"></i>\n {{formData.name||formData.uid}}\n </span>\n <span ng-if="!formData.name&&!formData.uid" class="gray">\n <i class="ion-key gray"></i>\n {{formData.issuer|formatPubkey}}\n </span>\n </a>\n <span class="hidden-xs hidden-sm">\n <i class="ion-clock"></i>\n {{formData.time|formatFromNow}}\n <span class="gray">|\n {{formData.time | formatDate}}\n </span>\n </span>\n </h4>\n <h5 class="gray visible-xs visible-sm">\n <i class="ion-clock"></i> {{formData.time | formatDate}}\n </h5>\n </div>\n\n <!-- content -->\n <ion-item class="visible-xs visible-sm">\n <h1 class="title" ng-bind-html="formData.title"></h1>\n </ion-item>\n\n <!-- content -->\n <ion-item>\n <p ng-bind-html="formData.html">\n </p>\n\n <div class="padding gray" ng-if="!formData.content" translate>\n MESSAGE.VIEW.NO_CONTENT\n </div>\n </ion-item>\n\n\n </div>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n </div>\n </ion-content>\n\n <button id="fab-view-message-reply" class="button button-fab button-fab-bottom-right button-calm icon ion-reply visible-xs visible-sm spin" ng-click="showReplyModal()">\n </button>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/message/view_popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left assertive ink" ng-click="delete()">\n <i class="icon ion-trash-a"></i>\n {{\'MESSAGE.VIEW.DELETE\' | translate}}\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/network/item_content_peer.html','\n <i class="icon ion-android-desktop" ng-class=":rebind:{\'balanced\': peer.online && peer.hasMainConsensusBlock, \'energized\': peer.online && peer.hasConsensusBlock, \'gray\': peer.online && !peer.hasConsensusBlock && !peer.hasMainConsensusBlock, \'stable\': !peer.online}" ng-if=":rebind:!peer.avatar"></i>\n <b class="icon-secondary ion-person" ng-if=":rebind:!peer.avatar" ng-class=":rebind:{\'balanced\': peer.online && peer.hasMainConsensusBlock, \'energized\': peer.online && peer.hasConsensusBlock, \'gray\': peer.online && !peer.hasConsensusBlock && !peer.hasMainConsensusBlock, \'stable\': !peer.online}" style="left: 26px; top: -3px"></b>\n <i class="avatar" ng-if=":rebind:peer.avatar" style="background-image: url(\'{{:rebind:peer.avatar.src}}\')"></i>\n <b class="icon-secondary assertive ion-close-circled" ng-if=":rebind:!peer.online" style="left: 37px; top: -10px"></b>\n\n <div class="row no-padding">\n <div class="col no-padding">\n <h3 class="dark">{{:rebind:peer.dns || peer.server}}</h3>\n <h4>\n <span class="gray" ng-if=":rebind:!peer.name">\n <i class="ion-key"></i> {{:rebind:peer.pubkey|formatPubkey}}\n </span>\n <span class="positive" ng-if=":rebind:peer.name">\n <i class="ion-person"></i> {{:rebind:peer.name}}\n </span>\n <span class="gray">{{:rebind:peer.dns && (\' | \' + peer.server) + (peer.ep.path||\'\') }}</span>\n </h4>\n </div>\n <div class="col col-15 no-padding text-center hidden-xs hidden-sm" ng-if="::expertMode">\n <div style="min-width: 50px; padding-top: 5px">\n <span ng-if=":rebind:peer.isSsl()" title="SSL">\n <i class="ion-locked"></i><small class="hidden-md"> SSL</small>\n </span>\n <span ng-if=":rebind:peer.hasEndpoint(\'ES_SUBSCRIPTION_API\')" title="{{\'ES_PEER.EMAIL_SUBSCRIPTION_COUNT\'|translate: peer.docCount }}">\n <i class="ion-email"></i> {{:rebind:peer.docCount.emailSubscription || \'?\'}}\n </span>\n </div>\n <div ng-if=":rebind:peer.isTor()">\n <i class="ion-bma-tor-api"></i>\n </div>\n <div ng-if=":rebind:peer.isWs2p()&&peer.isTor()" ng-click="showWs2pPopover($event, peer)">\n <i class="ion-bma-tor-api"></i>\n </div>\n </div>\n <div class="col col-20 no-padding text-center" ng-if="::!expertMode && search.type != \'offline\'">\n <div style="min-width: 50px; padding-top: 5px" ng-if=":rebind:peer.docCount.emailSubscription!==undefined">\n <span ng-if=":rebind:peer.hasEndpoint(\'ES_SUBSCRIPTION_API\')" title="{{\'ES_PEER.EMAIL_SUBSCRIPTION_COUNT\'|translate: peer.docCount }}">\n <i class="ion-email"></i> {{:rebind:peer.docCount.emailSubscription || \'?\'}}\n </span>\n </div>\n </div>\n <div class="col col-20 no-padding text-center" ng-if="::expertMode && search.type != \'offline\'">\n <h4 class="hidden-sm hidden-xs gray">\n {{:rebind:peer.software||\'?\'}}\n </h4>\n <h4 class="hidden-sm hidden-xs gray">{{:rebind: peer.version ? (\'v\'+peer.version) : \'\'}}</h4>\n </div>\n <div class="col col-20 no-padding text-center" id="{{$index === 0 ? helptipPrefix + \'-peer-0-block\' : \'\'}}">\n <span class="badge badge-stable">\n {{:rebind:peer.docCount.record !== undefined ? (peer.docCount.record|formatInteger) : \'?\'}}\n <span ng-if=":rebind:!expertMode && peer.docCount.record!==undefined">\n {{::\'ES_PEER.DOCUMENTS\'|translate|lowercase }}\n </span>\n </span>\n <span class="badge badge-secondary" ng-class=":rebind:{\'balanced\': peer.hasMainConsensusBlock, \'energized\': peer.hasConsensusBlock, \'ng-hide\': !peer.currentNumber }" ng-if="::expertMode">\n {{:rebind:\'BLOCKCHAIN.VIEW.TITLE\'|translate: {number:peer.currentNumber} }}\n </span>\n\n </div>\n </div>\n');
$templateCache.put('plugins/es/templates/network/items_peers.html','<div ng-class="::motion.ionListClass" class="no-padding">\n\n <div class="item item-text-wrap no-border done in gray no-padding-top no-padding-bottom inline text-italic" ng-if="::isHttps && expertMode">\n <small><i class="icon ion-alert-circled"></i> {{::\'NETWORK.INFO.ONLY_SSL_PEERS\'|translate}}</small>\n </div>\n\n <div class="item row row-header hidden-xs hidden-sm done in" ng-if="::expertMode">\n <a class="col col-header no-padding dark" ng-click="toggleSort(\'name\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'name\'"></cs-sort-icon>\n {{::\'ES_PEER.NAME\' | translate}} / {{::\'COMMON.PUBKEY\' | translate}}\n </a>\n <a class="no-padding dark hidden-md col col-15 col-header" ng-click="toggleSort(\'api\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'api\'"></cs-sort-icon>\n {{::\'PEER.API\' | translate}}\n </a>\n <a class="no-padding dark col col-20 col-header" ng-click="toggleSort(\'difficulty\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'software\'"></cs-sort-icon>\n {{::\'ES_PEER.SOFTWARE\' | translate}}\n </a>\n <a class="col col-20 col-header no-padding dark" ng-click="toggleSort(\'doc_count\')">\n <cs-sort-icon asc="search.asc" sort="search.sort" toggle="\'doc_count\'"></cs-sort-icon>\n {{::\'ES_PEER.DOCUMENTS\' | translate}}\n </a>\n </div>\n\n <div ng-repeat="peer in :rebind:search.results track by peer.id" class="item item-peer item-icon-left ink" ng-class="::ionItemClass" id="{{helptipPrefix}}-peer-{{$index}}" ng-click="selectPeer(peer)" ng-include="\'plugins/es/templates/network/item_content_peer.html\'">\n </div>\n\n</div>\n');
$templateCache.put('plugins/es/templates/network/lookup_popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>PEER.POPOVER_FILTER_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left item-icon-right ink" ng-click="toggleSearchType(\'member\')">\n <i class="icon ion-person"></i>\n {{\'PEER.MEMBERS\' | translate}}\n <i class="icon ion-ios-checkmark-empty" ng-show="search.type==\'member\'"></i>\n </a>\n\n <a class="item item-icon-left item-icon-right ink" ng-click="toggleSearchType(\'mirror\')">\n <i class="icon ion-radio-waves"></i>\n {{\'PEER.MIRRORS\' | translate}}\n <i class="icon ion-ios-checkmark-empty" ng-show="search.type==\'mirror\'"></i>\n </a>\n\n <a class="item item-icon-left item-icon-right ink" ng-click="toggleSearchType(\'offline\')">\n <i class="icon ion-eye-disabled"></i>\n {{\'PEER.OFFLINE\' | translate}}\n <i class="icon ion-ios-checkmark-empty" ng-show="search.type==\'offline\'"></i>\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/network/modal_network.html','<ion-modal-view id="nodes" class="modal-full-height" cache-view="false">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n <h1 class="title" translate>PEER.PEER_LIST</h1>\n <div class="buttons buttons-right header-item">\n <span class="secondary">\n <button class="button button-clear icon ion-loop button-clear" ng-click="refresh()">\n\n </button>\n <button class="button button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n </span>\n </div>\n </ion-header-bar>\n\n <ion-content>\n <div class="list">\n <div class="padding padding-xs" style="display: block; height: 60px">\n\n <div class="pull-left">\n <h4>\n <span ng-if="!enableFilter || !search.type" translate>PEER.ALL_PEERS</span>\n <span ng-if="!search.loading">({{search.results.length}})</span>\n </h4>\n </div>\n\n <div class="pull-right">\n <ion-spinner class="icon" icon="android" ng-if="search.loading"></ion-spinner>&nbsp;\n </div>\n </div>\n\n <ng-include src="\'plugins/es/templates/network/items_peers.html\'"></ng-include>\n\n\t </div>\n </ion-content>\n</ion-modal-view>\n');
$templateCache.put('plugins/es/templates/network/popover_endpoints.html','<ion-popover-view class="popover-endpoints popover-light" style="height: {{(titleKey?30:0)+((!items || items.length &lt;= 1) ? 55 : 3+items.length*52)}}px">\n <ion-header-bar class="bar bar-header stable-bg" ng-if="titleKey">\n <div class="title">\n {{titleKey | translate:titleValues }}\n </div>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list" ng-class="{\'has-header\': titleKey}">\n <div class="item item-text-wrap" ng-repeat="item in items">\n <div class="item-label" ng-if="item.label">{{item.label | translate}}</div>\n <div id="endpoint_{{$index}}" class="badge item-note dark">{{item.value}}\n </div>\n </div>\n </div></ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/network/popover_network.html','<ion-popover-view class="fit hidden-xs hidden-sm popover-notification popover-network" ng-controller="NetworkLookupPopoverCtrl">\n <ion-header-bar class="stable-bg block">\n <div class="title">\n {{\'MENU.NETWORK\'|translate}}\n <ion-spinner class="ion-spinner-small" icon="android" ng-if="search.loading"></ion-spinner>\n </div>\n\n <div class="pull-right">\n <a ng-class="{\'positive\': search.type==\'member\', \'dark\': search.type!==\'member\'}" ng-click="toggleSearchType(\'member\')" translate>PEER.MEMBERS</a>\n </div>\n </ion-header-bar>\n <ion-content scroll="true">\n <div class="list no-padding">\n <ng-include src="\'plugins/es/templates/network/items_peers.html\'"></ng-include>\n </div>\n </ion-content>\n\n <ion-footer-bar class="stable-bg block">\n <!-- settings -->\n <div class="pull-left">\n <a class="positive" ui-sref="app.settings" ng-click="closePopover()" translate>COMMON.NOTIFICATIONS.SETTINGS</a>\n </div>\n\n <!-- show all -->\n <div class="pull-right">\n <a class="positive" ui-sref="app.es_network" ng-click="closePopover()" translate>COMMON.NOTIFICATIONS.SHOW_ALL</a>\n </div>\n </ion-footer-bar>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/network/popover_peer_info.html','<ion-popover-view class="fit hidden-xs hidden-sm popover-notification popover-peer-info" ng-controller="PeerInfoPopoverCtrl">\n <ion-header-bar class="stable-bg block">\n <div class="title">\n {{\'PEER.VIEW.TITLE\'|translate}}\n </div>\n </ion-header-bar>\n <ion-content scroll="true">\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list no-padding" ng-if="!loading">\n\n <div class="item" ng-if=":rebind:formData.software">\n <i class="ion-outlet"></i>\n {{\'NETWORK.VIEW.SOFTWARE\'|translate}}\n <div class="badge" ng-class=":rebind:{\'badge-energized\': formData.isPreRelease, \'badge-assertive\': formData.hasNewRelease }">\n {{formData.software}} v{{:rebind:formData.version}}\n </div>\n <div class="gray badge badge-secondary" ng-if="formData.isPreRelease">\n <i class="ion-alert-circled"></i>\n <span ng-bind-html="\'NETWORK.VIEW.WARN_PRE_RELEASE\'|translate: formData.latestRelease"></span>\n </div>\n <div class="gray badge badge-secondary" ng-if="formData.hasNewRelease">\n <i class="ion-alert-circled"></i>\n <span ng-bind-html="\'NETWORK.VIEW.WARN_NEW_RELEASE\'|translate: formData.latestRelease"></span>\n </div>\n </div>\n\n <div class="item">\n <i class="ion-locked"></i>\n {{\'NETWORK.VIEW.ENDPOINTS.BMAS\'|translate}}\n <div class="badge badge-balanced" ng-if=":rebind:formData.useSsl" translate>COMMON.BTN_YES</div>\n <div class="badge badge-assertive" ng-if=":rebind:!formData.useSsl" translate>COMMON.BTN_NO</div>\n </div>\n\n <div class="item">\n <i class="ion-cube"></i>\n {{\'BLOCKCHAIN.VIEW.TITLE_CURRENT\'|translate}}\n <div class="badge badge-balanced">\n {{:rebind:formData.number | formatInteger}}\n </div>\n </div>\n\n <div class="item">\n <i class="ion-clock"></i>\n {{\'CURRENCY.VIEW.MEDIAN_TIME\'|translate}}\n <div class="badge dark">\n {{:rebind:formData.medianTime | medianDate}}\n </div>\n </div>\n\n <div class="item">\n <i class="ion-lock-combination"></i>\n {{\'CURRENCY.VIEW.POW_MIN\'|translate}}\n <div class="badge dark">\n {{:rebind:formData.powMin | formatInteger}}\n </div>\n </div>\n\n <!-- Allow extension here -->\n <cs-extension-point name="default"></cs-extension-point>\n\n </div>\n </ion-content>\n\n <ion-footer-bar class="stable-bg block">\n <!-- settings -->\n <div class="pull-left">\n <a class="positive" ui-sref="app.settings" ng-click="closePopover()" translate>MENU.SETTINGS</a>\n </div>\n\n <!-- show all -->\n <div class="pull-right">\n <a class="positive" ui-sref="app.view_es_peer" ng-click="closePopover()" translate>PEER.BTN_SHOW_PEER</a>\n </div>\n </ion-footer-bar>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/network/view_es_network.html','<ion-view>\n <ion-nav-title>\n <span translate>MENU.NETWORK</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="refresh()">\n </button>\n </ion-nav-buttons>\n\n\n <ion-content scroll="true" ng-init="enableFilter=true; ionItemClass=\'item-border-large\';">\n\n <div class="row responsive-sm responsive-md responsive-lg">\n <div class="col list col-border-right">\n <div class="padding padding-xs" style="display: block; height: 60px">\n <div class="pull-left">\n <h4>\n <span ng-if="enableFilter && !search.online" translate>PEER.OFFLINE_PEERS</span>\n <span ng-if="!enableFilter || search.online" translate>PEER.ALL_PEERS</span>\n <span ng-if="search.results.length">({{search.results.length}})</span>\n <ion-spinner ng-if="search.loading" class="icon ion-spinner-small" icon="android"></ion-spinner>\n </h4>\n </div>\n\n <div class="pull-right">\n\n <div class="pull-right" ng-if="enableFilter">\n\n <a class="button button-text button-small hidden-xs hidden-sm ink" ng-class="{\'button-text-positive\': !search.online, \'button-text-stable\': search.online}" ng-click="toggleOnline(!search.online)">\n <i class="icon ion-close-circled light-gray"></i>\n <span>{{\'PEER.OFFLINE\'|translate}}</span>\n </a>\n\n <!-- Allow extension here -->\n <cs-extension-point name="filter-buttons"></cs-extension-point>\n </div>\n </div>\n </div>\n\n <div id="helptip-network-peers" style="display: block"></div>\n\n <ng-include src="\'plugins/es/templates/network/items_peers.html\'"></ng-include>\n </div>\n\n <div class="col col-33" ng-controller="MkLastDocumentsCtrl">\n <div class="padding padding-xs" style="display: block">\n <h4 translate>DOCUMENT.LOOKUP.LAST_DOCUMENTS_DOTS</h4>\n\n <div class="pull-right hidden-xs hidden-sm">\n <a class="button button-text button-small ink" ng-class="{\'button-text-positive\': compactMode, \'button-text-stable\': !compactMode}" ng-click="toggleCompactMode()">\n <i class="icon ion-navicon"></i>\n <b class="icon-secondary ion-arrow-down-b" style="top: -8px; left: 5px; font-size: 8px"></b>\n <b class="icon-secondary ion-arrow-up-b" style="top: 4px; left: 5px; font-size: 8px"></b>\n <span>{{\'DOCUMENT.LOOKUP.BTN_COMPACT\'|translate}}</span>\n </a>\n\n <!-- Allow extension here -->\n <cs-extension-point name="buttons"></cs-extension-point>\n\n <a class="button button-text button-small ink" ui-sref="app.document_search({index: search.index, type: search.type})">\n <i class="icon ion-android-search"></i>\n <span>{{\'COMMON.BTN_SEARCH\'|translate}}</span>\n </a>\n\n </div>\n </div>\n\n <ng-include src="\'plugins/market/templates/document/list_documents.html\'"></ng-include>\n\n </div>\n </div>\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/network/view_es_peer.html','<ion-view>\n <ion-nav-title>\n <span translate>PEER.VIEW.TITLE</span>\n </ion-nav-title>\n\n <ion-content class="has-header" scroll="true">\n\n <div class="row no-padding">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n\n <div class="col list">\n\n <ion-item>\n <h1>\n <span translate>PEER.VIEW.TITLE</span>\n <span class="gray">\n {{node.host}}\n </span>\n </h1>\n <h2 class="gray">\n <i class="gray icon ion-android-globe"></i>\n {{node.ep.dns || node.server}}\n <span class="gray" ng-if="!loading && node.useSsl">\n <i class="gray ion-locked"></i> <small>SSL</small>\n </span>\n <span class="gray" ng-if="!loading && node.useTor">\n <i class="gray ion-bma-tor-api"></i>\n </span>\n </h2>\n\n <!-- node owner -->\n <h3>\n <span class="dark">\n <i class="icon ion-android-desktop"></i>\n {{\'PEER.VIEW.OWNER\'|translate}}\n </span>\n <a class="positive" ng-if="node.name" ui-sref="app.wot_identity({pubkey: node.pubkey, uid: node.name})">\n <i class="ion-person"></i> {{node.name}}\n </a>\n <span ng-if="!loading && !node.name">\n <a class="gray" ui-sref="app.wot_identity({pubkey: node.pubkey})">\n <i class="ion-key"></i>\n {{node.pubkey|formatPubkey}}\n </a>\n </span>\n </h3>\n\n <h3>\n <a ng-click="openRawPeering($event)">\n <i class="icon ion-share"></i> {{\'PEER.VIEW.SHOW_RAW_PEERING\'|translate}}\n </a>\n\n <span class="gray" ng-if="!isReachable"> | </span>\n <a ng-if="!isReachable" ng-click="openRawCurrentBlock($event)">\n <i class="icon ion-share"></i> <span translate>PEER.VIEW.SHOW_RAW_CURRENT_BLOCK</span>\n </a>\n </h3>\n </ion-item>\n\n\n <div class="item item-divider" translate>\n PEER.VIEW.GENERAL_DIVIDER\n </div>\n\n <ion-item class="item-icon-left item-text-wrap ink" copy-on-click="{{node.pubkey}}">\n <i class="icon ion-key"></i>\n <span translate>COMMON.PUBKEY</span>\n <h4 class="dark text-left">{{node.pubkey}}</h4>\n </ion-item>\n\n <ion-item class="item item-icon-left item-text-wrap ink" ng-if="isReachable">\n <i class="icon ion-cube"></i>\n <span translate>BLOCKCHAIN.VIEW.TITLE_CURRENT</span>\n <div class="badge badge-calm" ng-if="!loading">\n {{current.number|formatInteger}}\n </div>\n </ion-item>\n\n <ion-item class="item item-icon-left item-text-wrap" ng-if="isReachable">\n <i class="icon ion-document"></i>\n <span translate>ES_PEER.DOCUMENT_COUNT</span>\n <div class="badge badge-stable" ng-if="!loading">\n {{node.docCount|formatInteger}}\n </div>\n </ion-item>\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink" ng-if="isReachable" ui-sref="app.document_search(options.document)">\n <i class="icon ion-document" style="font-size: 25px"></i>\n <i class="icon-secondary ion-clock" style="font-size: 18px; left: 33px; top: -12px"></i>\n <span translate>DOCUMENT.LOOKUP.LAST_DOCUMENTS</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n\n <!-- Allow extension here -->\n <cs-extension-point name="general"></cs-extension-point>\n\n <div class="item item-divider" ng-hide="loading || !isReachable" translate>\n PEER.VIEW.KNOWN_PEERS\n </div>\n\n <ion-item class="item item-text-wrap no-border done in gray no-padding-top no-padding-bottom inline text-italic" ng-show="!loading && !isReachable">\n <small><i class="icon ion-alert-circled"></i> {{\
$templateCache.put('plugins/es/templates/notification/list_notification.html','<ion-list class="{{::motion.ionListClass}}">\n\n <ion-item ng-repeat="notification in search.results" class="item-border-large item-text-wrap ink item-avatar" ng-class="{\'unread\': !notification.read}" ng-click="select(notification)">\n\n <i ng-if="!notification.avatar" class="item-image icon {{::notification.avatarIcon}}"></i>\n <i ng-if="notification.avatar" class="item-image avatar" style="background-image: url({{::notification.avatar.src}})"></i>\n\n <h3 trust-as-html="notification.message | translate:notification"></h3>\n <h4>\n <i class="icon {{notification.icon}}"></i>&thinsp;<span class="dark">{{notification.time|formatFromNow}}</span>\n <span class="gray">| {{notification.time|formatDate}}</span>\n </h4>\n </ion-item>\n</ion-list>\n\n<ion-infinite-scroll ng-if="!search.loading && search.hasMore" spinner="android" on-infinite="showMore()" distance="1%">\n</ion-infinite-scroll>\n');
$templateCache.put('plugins/es/templates/notification/popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink" ng-disabled="!search.results.length" ng-click="markAllAsRead()">\n <i class="icon ion-android-checkmark-circle"></i>\n {{\'COMMON.NOTIFICATIONS.MARK_ALL_AS_READ\' | translate}}\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/notification/popover_notification.html','<ion-popover-view class="fit popover-notification" ng-controller="PopoverNotificationsCtrl">\n <ion-header-bar class="stable-bg block">\n <div class="title" translate>COMMON.NOTIFICATIONS.TITLE</div>\n\n <div class="pull-right">\n <a class="positive" ng-click="markAllAsRead()" translate>COMMON.NOTIFICATIONS.MARK_ALL_AS_READ</a>\n </div>\n </ion-header-bar>\n\n <ion-content scroll="true">\n <div class="center" ng-if="search.loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n <div class="padding gray" ng-if="!search.loading && !search.results.length" translate>\n COMMON.NOTIFICATIONS.NO_RESULT\n </div>\n\n <ng-include src="\'plugins/es/templates/notification/list_notification.html\'"></ng-include>\n\n </ion-content>\n\n <ion-footer-bar class="stable-bg block">\n <!-- settings\n <div class="pull-left">\n <a class="positive"\n ui-sref="app.es_settings"\n ng-click="closePopover()"\n translate>MENU.SETTINGS</a>\n </div> -->\n\n <!-- show all -->\n <div class="pull-right">\n <a class="positive" ui-sref="app.view_notifications" ng-click="closePopover()" translate>COMMON.NOTIFICATIONS.SHOW_ALL</a>\n </div>\n </ion-footer-bar>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/notification/view_notifications.html','<ion-view left-buttons="leftButtons" class="view-notification">\n <ion-nav-title>\n {{\'COMMON.NOTIFICATIONS.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="load()">\n </button>\n\n <button class="button button-icon button-clear visible-xs visible-sm" ng-click="showActionsPopover($event)">\n <i class="icon ion-android-more-vertical"></i>\n </button>\n </ion-nav-buttons>\n\n <ion-content class="padding no-padding-xs" scroll="true">\n\n <!-- Buttons bar-->\n <div class="hidden-xs hidden-sm padding text-center">\n\n <button class="button button-stable button-small-padding icon ion-loop ink" ng-click="load()">\n </button>\n\n <button class="button button-raised icon-left ion-checkmark ink" ng-click="markAllAsRead()">\n {{\'COMMON.NOTIFICATIONS.MARK_ALL_AS_READ\' | translate}}\n </button>\n </div>\n\n <div class="row no-padding">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col">\n\n <div class="center" ng-if="search.loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="padding gray" ng-if="!search.loading && !search.results.length" translate>\n COMMON.NOTIFICATIONS.NO_RESULT\n </div>\n\n <ng-include src="\'plugins/es/templates/notification/list_notification.html\'"></ng-include>\n\n\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n </div>\n\n </ion-content>\n\n</ion-view>\n');
$templateCache.put('plugins/es/templates/registry/edit_record.html','<ion-view left-buttons="leftButtons" class="view-page">\n <ion-nav-title>\n <span class="visible-xs" ng-if="id" ng-bind-html="formData.title"></span>\n <span class="visible-xs" ng-if="!loading && !id" translate>REGISTRY.EDIT.TITLE_NEW</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear visible-xs visible-sm" ng-class="{\'ion-android-send\':!id, \'ion-android-done\': id}" ng-click="save()">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true">\n\n <div class="hero">\n <div class="content">\n <i class="avatar" ng-class="avatarClass" ng-style="avatarStyle">\n <button class="button button-positive button-large button-clear flat icon ion-camera visible-xs visible-sm" style="display: inline-block" ng-click="showAvatarModal()"></button>\n <button ng-if="avatar.src" class="button button-positive button-large button-clear flat visible-xs visible-sm" style="display: inline-block; left: 85px; bottom:15px" ng-click="rotateAvatar()">\n <i class="icon-secondary ion-image" style="left: 24px; top: 3px; font-size: 24px"></i>\n <i class="icon-secondary ion-forward" style="left: 26px; top: -13px"></i>\n </button>\n <button class="button button-positive button-large button-clear icon ion-camera hidden-xs hidden-sm" ng-click="showAvatarModal()"></button>\n </i>\n <h3 class="dark">\n <span ng-if="!loading && formData.title">{{formData.title}}</span>\n <span ng-if="!loading && !id && !formData.title" translate>REGISTRY.EDIT.TITLE_NEW</span>\n </h3>\n <h4 class="dark">\n <ion-spinner ng-if="loading" icon="android"></ion-spinner>\n </h4>\n </div>\n </div>\n\n <div class="row no-padding">\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col no-padding-xs">\n\n <form name="recordForm" novalidate="" ng-submit="save()">\n <div class="list {{::motion.ionListClass}}" ng-init="setForm(recordForm)">\n <div class="item" ng-if="id">\n <h4 class="gray">\n <i class="icon ion-calendar"></i>\n {{\'COMMON.LAST_MODIFICATION_DATE\'|translate}}&nbsp;{{formData.time | formatDate}}\n </h4>\n <div class="badge badge-balanced badge-editable" ng-click="showRecordTypeModal()">\n {{\'REGISTRY.TYPE.ENUM.\'+formData.type|upper|translate}}\n </div>\n </div>\n\n <!-- pictures -->\n <ng-include src="\'plugins/es/templates/common/edit_pictures.html\'"></ng-include>\n\n <div class="item item-divider" translate>REGISTRY.GENERAL_DIVIDER</div>\n\n <!-- title -->\n <div class="item item-input item-floating-label" ng-class="{\'item-input-error\': form.$submitted && form.title.$invalid}">\n <span class="input-label" translate>REGISTRY.EDIT.RECORD_TITLE</span>\n <input type="text" placeholder="{{\'REGISTRY.EDIT.RECORD_TITLE_HELP\'|translate}}" name="title" id="registry-record-title" ng-model="formData.title" ng-minlength="3" ng-required="true">\n </div>\n <div class="form-errors" ng-if="form.$submitted && form.title.$error" ng-messages="form.title.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT"></span>\n </div>\n </div>\n\n <!-- description -->\n <div class="item item-input item-floating-label">\n <span class="input-label" translate>REGISTRY.EDIT.RECORD_DESCRIPTION</span>\n <textarea placeholder="{{\'REGISTRY.EDIT.RECORD_DESCRIPTION_HELP\'|transla
$templateCache.put('plugins/es/templates/registry/lookup.html','<ion-view left-buttons="leftButtons">\n <ion-nav-title>\n <span translate>REGISTRY.SEARCH.TITLE</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="doUpdate()">\n </button>\n <button class="button button-bar button-icon button-clear icon ion-android-funnel visible-xs visible-sm" ng-click="showFiltersPopover($event)">\n </button>\n </ion-nav-buttons>\n\n <ion-content class="lookupForm padding no-padding-xs">\n\n <ng-include src="::\'plugins/es/templates/registry/lookup_form.html\'"></ng-include>\n\n <ng-include src="::\'plugins/es/templates/registry/lookup_list.html\'"></ng-include>\n\n </ion-content>\n\n <button id="fab-add-registry-record" class="button button-fab button-fab-bottom-right button-assertive icon ion-plus hidden-md hidden-lg spin" ng-click="showNewPageModal()">\n </button>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/registry/lookup_form.html','\n<!-- selected location\n<a\n class="button button-small button-text button-stable button-icon-event stable-900-bg"\n style="margin-right: 10px;">\n &nbsp;<i class="icon ion-location"></i>\n {{search.location}}\n <i class="icon ion-close" ng-click="removeLocation2()">&nbsp;&nbsp;</i>\n</a>-->\n<form ng-submit="doSearch()">\n <div class="item no-padding">\n\n <div class="item-input light-bg">\n\n <div class="animate-show-hide ng-hide" ng-show="entered" ng-if="search.geoPoint || search.type || search.category">\n <!-- selected location -->\n <div ng-show="search.geoPoint" class="button button-small button-text button-stable button-icon-event stable-900-bg" style="margin-right: 10px">\n &nbsp;<i class="icon ion-location"></i>\n <span ng-bind-html="search.location"></span>\n <i class="icon ion-close" ng-click="removeLocation()">&nbsp;&nbsp;</i>\n </div>\n\n <!-- selected type -->\n <div ng-show="search.type" class="button button-small button-text button-stable button-icon-event stable-900-bg" style="margin-right: 10px">\n &nbsp;<i class="icon cion-page-{{search.type}}"></i>\n <span>{{\'REGISTRY.TYPE.ENUM.\'+search.type|uppercase|translate}}</span>\n <i class="icon ion-close" ng-click="removeType()">&nbsp;&nbsp;</i>\n </div>\n\n <!-- selected category -->\n <div ng-show="search.category.name" class="button button-small button-text button-stable button-icon-event stable-900-bg" style="margin-right: 10px">\n &nbsp;<i class="icon ion-flag"></i>\n <span>{{search.category.name|truncText:40}}</span>\n <i class="icon ion-close" ng-click="removeCategory()">&nbsp;&nbsp;</i>\n </div>\n </div>\n\n <i class="icon ion-search placeholder-icon"></i>\n <input type="text" class="visible-xs visible-sm" placeholder="{{\'REGISTRY.SEARCH.SEARCH_HELP\'|translate}}" ng-model="search.text" ng-model-options="{ debounce: 650 }" ng-change="doSearch()" on-return="doSearchText()" select-on-click>\n <input type="text" class="hidden-xs hidden-sm" placeholder="{{\'REGISTRY.SEARCH.SEARCH_HELP\'|translate}}" id="registrySearchText" ng-model="search.text" on-return="doSearchText()">\n </div>\n\n </div>\n\n <!-- location -->\n <ng-include src="::\'plugins/es/templates/common/item_location_search.html\'" ng-if="entered && options.location.show && (!search.geoPoint || smallscreen)" ng-controller="ESSearchPositionItemCtrl"></ng-include>\n\n <!-- options -->\n <ng-include src="::\'plugins/es/templates/registry/lookup_form_options.html\'"></ng-include>\n\n\n\n<div class="padding-top hidden-xs hidden-sm" style="display: block; height: 60px">\n <div class="pull-left">\n\n <a class="button button-text button-small ink" ng-class="{\'button-text-stable\': !search.advanced, \'button-text-positive\': search.advanced}" ng-click="search.advanced=!search.advanced">\n {{\'REGISTRY.SEARCH.BTN_ADVANCED_SEARCH\' | translate}}\n <i class="icon" ng-class="{\'ion-arrow-down-b\': !search.advanced, \'ion-arrow-up-b\': search.advanced}"></i>\n </a>\n\n &nbsp;\n\n </div>\n\n <div class="pull-right">\n\n <a ng-if="enableFilter" class="button button-text button-small ink" ng-class="{\'button-text-positive\': search.lastRecords}" ng-click="doGetLastRecords()">\n <i class="icon ion-clock"></i>\n {{\'REGISTRY.SEARCH.BTN_LAST_RECORDS\' | translate}}\n </a>\n &nbsp;\n\n <!-- Allow extension here -->\n <cs-extension-point name="filter-buttons"></cs-extension-point>\n\n &nbsp;\n\n <button class="button button-small button-stable ink" ng-click="doSearchText()">\n {{\'COMMON.BTN_SEARCH\' | translate}}\n </button>\n </div>\n</div>\n\n<div class="padding-xs" style="display: block; height: 60px">\n <div class="pull-left ng-hide" ng-show="!search.loading && search.results">\n <ng-if ng-if="search.lastRecords">\n <h4 translate>REGISTRY.SEARCH.LAST_RECORDS</h4>\n <small
$templateCache.put('plugins/es/templates/registry/lookup_form_options.html','\n <div class="item item-icon-left item-icon-right item-input stable-bg" ng-click="showRecordTypeModal($event)" ng-if="search.advanced && !search.type">\n <b class="icon-secondary ion-help gray" style="left:10px; top: -8px"></b>\n <b class="icon-secondary cion-page-association gray" style="left:14px; top: 2px"></b>\n <b class="icon-secondary cion-page-company gray" style="left:28px; top: -6px"></b>\n\n <span class="input-label item-icon-left-padding" translate>REGISTRY.SEARCH.TYPE</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </div>\n\n <div class="item item-icon-left item-icon-right item-input stable-bg" ng-click="showCategoryModal($event)" ng-if="search.advanced && !search.category">\n <i class="icon ion-flag gray"></i>\n <span class="input-label item-icon-left-padding" translate>REGISTRY.CATEGORY</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </div>\n\n <div class="item item-icon-left item-input item-select stable-bg" ng-if="search.advanced && options.location.show">\n <i class="icon ion-arrow-resize gray"></i>\n <span class="input-label item-icon-left-padding" translate>LOCATION.DISTANCE</span>\n <label>\n <select ng-model="search.geoDistance" class="col-border-left" ng-options="i as (geoDistanceLabels[i].labelKey | translate:geoDistanceLabels[i].labelParams ) for i in geoDistances track by i">\n </select>\n </label>\n </div>');
$templateCache.put('plugins/es/templates/registry/lookup_lg.html','<ion-view left-buttons="leftButtons" class="view-registry-search">\n <ion-nav-title>\n <span translate>REGISTRY.SEARCH.TITLE</span>\n </ion-nav-title>\n\n <ion-content class="lookupForm padding no-padding-xs stable-100-bg">\n\n <button class="button button-small button-positive button-clear ink pull-right padding-right hidden-sm hidden-xs" ng-click="showNewPageModal()">\n <i class="icon ion-plus"></i>\n {{\'REGISTRY.BTN_NEW\' | translate}}\n </button>\n\n <ng-include src="::\'plugins/es/templates/registry/lookup_form.html\'"></ng-include>\n\n <ng-include src="::\'plugins/es/templates/registry/lookup_list_lg.html\'"></ng-include>\n\n </ion-content>\n\n <button id="fab-add-registry-record" class="button button-fab button-fab-bottom-right button-assertive icon ion-plus hidden-md hidden-lg spin" ng-click="showNewPageModal()">\n </button>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/registry/lookup_list.html','\n<ion-list class="{{::motion.ionListClass}}" ng-if="!search.loading && search.results && search.results.length > 0">\n\n <div ng-repeat="item in search.results" class="item item-avatar item-icon-right item-border-large ink" ui-sref="app.view_page({id: item.id, title: item.urlTitle})">\n\n <i ng-if="::!item.avatar" class="item-image icon cion-page-{{::item.type}}"></i>\n <i ng-if="::item.avatar" class="item-image avatar" style="background-image: url({{::item.avatar.src}})"></i>\n\n <h2 ng-bind-html="::item.title"></h2>\n <h4>\n <span class="dark" ng-if="::item.city">\n <b class="ion-location"></b>\n <span ng-bind-html="::item.city"></span>\n </span>\n <span class="gray" ng-if="::item.distance">\n ({{::item.distance|formatDecimal}} {{::geoUnit}})\n </span>\n </h4>\n <h4 class="gray" ng-if="::item.time && search.lastRecords">\n <i class="ion-clock"></i>\n {{::item.time | formatFromNow}}\n </h4>\n <h4 class="gray" ng-if="!search.lastRecords">\n <i class="cion-page-{{::item.type}}"></i>\n <span ng-if="item.category">{{::item.category.name}}</span>\n <span ng-if="!item.category">{{::\'REGISTRY.TYPE.ENUM.\'+item.type|uppercase|translate}}</span>\n </h4>\n <i class="icon ion-ios-arrow-right"></i>\n </div>\n</ion-list>\n');
$templateCache.put('plugins/es/templates/registry/lookup_list_lg.html','\n\n<div class="list {{::motion.ionListClass}} light-bg" ng-if="!search.loading && search.results && search.results.length > 0">\n\n <a ng-repeat="item in search.results" class="item item-record item-border-large ink padding-xs" ui-sref="app.view_page({id: item.id, title: item.urlTitle})">\n\n <div class="row row-record">\n <div class="col item-text-wrap item-avatar-left-padding" ng-class="::{\'item-avatar\': item.avatar || item.type}">\n <i class="item-image icon cion-page-{{::item.type}}" ng-if="::!item.avatar"></i>\n <i class="item-image avatar" style="background-image: url({{::item.avatar.src}})" ng-if="::item.avatar"></i>\n <h2 ng-bind-html="::item.title"></h2>\n <h4>\n <span class="dark" ng-if="::item.city">\n <b class="ion-location"></b>\n <span ng-bind-html="::item.city"></span>\n </span>\n <span class="gray" ng-if="::item.distance">\n ({{::item.distance|formatDecimal}} {{::geoUnit}})\n </span>\n </h4>\n <h4>\n <span class="gray" ng-if="::item.time && search.lastRecords">\n <b class="ion-clock"></b>\n {{::item.time | formatFromNow}}\n </span>\n <span ng-if="::item.tags" class="dark">\n <ng-repeat ng-repeat="tag in ::item.tags">\n #<ng-bind-html ng-bind-html="::tag"></ng-bind-html>\n </ng-repeat>\n </span>\n </h4>\n <span ng-if="::item.picturesCount > 1" class="badge badge-balanced badge-picture-count">{{::item.picturesCount}}&nbsp;<i class="icon ion-camera"></i></span>\n </div>\n <div class="col col-20 hidden-xs hidden-sm">\n <h3 class="gray">\n <ng-if ng-if="::item.category">{{::item.category.name}}</ng-if>\n <ng-if ng-if="::!item.category">{{::\'REGISTRY.TYPE.ENUM.\'+item.type|uppercase|translate}}</ng-if>\n </h3>\n </div>\n <div class="col hidden-xs">\n <h4 class="text-wrap">\n <span class="visible-sm">\n <b class="ion-flag"></b>\n <ng-if ng-if="::item.category">{{::item.category.name|truncText:50}}</ng-if>\n <ng-if ng-if="::!item.category">{{::\'REGISTRY.TYPE.ENUM.\'+item.type|uppercase|translate}}</ng-if>\n </span>\n <span class="gray text-italic" ng-if="::item.description">\n <b class="ion-quote"></b>\n <span ng-bind-html="::item.description|truncText:500" ng-if="::item.description"></span>\n </span>\n </h4>\n </div>\n </div>\n\n </a>\n</div>\n\n<ion-infinite-scroll ng-if="!search.loading && search.hasMore" spinner="android" on-infinite="showMore()" distance="10%">\n</ion-infinite-scroll>\n');
$templateCache.put('plugins/es/templates/registry/lookup_popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <!-- new page -->\n <a class="item item-icon-left ink" ng-click="showNewPageModal();">\n <i class="icon ion-plus"></i>\n <span translate>REGISTRY.BTN_NEW</span>\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/registry/lookup_popover_filters.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_FILTER_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <!-- new page -->\n <a class="item item-icon-left ink" ng-click="doGetLastRecords()">\n <i class="icon ion-clock"></i>\n {{\'REGISTRY.SEARCH.BTN_LAST_RECORDS\' | translate}}\n </a>\n\n <!-- advanced options -->\n <a class="item item-icon-left ink" ng-click="toggleAdvanced();">\n <i class="icon ion-android-checkbox-outline-blank" ng-show="!search.advanced"></i>\n <i class="icon ion-android-checkbox-outline" ng-show="search.advanced"></i>\n <span translate>REGISTRY.SEARCH.POPOVER_FILTERS.BTN_ADVANCED_SEARCH</span>\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/registry/modal_record_type.html','<ion-modal-view>\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n <h1 class="title" translate>{{getParameters().title||\'REGISTRY.TYPE.TITLE\'|translate}}</h1>\n </ion-header-bar>\n\n <ion-content class="lookupForm">\n <div class="list padding">\n <h3 translate>REGISTRY.TYPE.SELECT_TYPE</h3>\n <button class="button button-block button-stable icon-left cion-page-shop" ng-click="closeModal(\'shop\')" translate>REGISTRY.TYPE.ENUM.SHOP</button>\n\n <button class="button button-block button-stable icon-left cion-page-association" ng-click="closeModal(\'association\')" translate>REGISTRY.TYPE.ENUM.ASSOCIATION</button>\n\n <button class="button button-block button-stable icon-left cion-page-company" ng-click="closeModal(\'company\')" translate>REGISTRY.TYPE.ENUM.COMPANY</button>\n\n <button class="button button-block button-stable icon-left cion-page-institution" ng-click="closeModal(\'institution\')" translate>REGISTRY.TYPE.ENUM.INSTITUTION</button>\n </div>\n </ion-content>\n</ion-modal-view>\n');
$templateCache.put('plugins/es/templates/registry/view_popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>REGISTRY.VIEW.MENU_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink" ng-click="showSharePopover($event)">\n <i class="icon ion-android-share-alt"></i>\n {{\'COMMON.BTN_SHARE\' | translate}}\n </a>\n\n <a class="item item-icon-left assertive ink" ng-if="canEdit" ng-click="delete()">\n <i class="icon ion-trash-a"></i>\n {{\'COMMON.BTN_DELETE\' | translate}}\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/es/templates/registry/view_record.html','<ion-view left-buttons="leftButtons" class="view-page">\n <ion-nav-title>\n\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-bar button-icon button-clear visible-xs visible-sm" ng-click="edit()" ng-if="canEdit">\n <i class="icon ion-android-create"></i>\n </button>\n <button class="button button-bar button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true" class="refresher-top-bg">\n\n <ion-refresher pulling-text="{{\'COMMON.BTN_REFRESH\' | translate}}" on-refresh="load()">\n </ion-refresher>\n\n <div class="hero">\n <div class="content" ng-if="!loading">\n <i class="avatar cion-page-{{formData.type}}" ng-if="!formData.avatar"></i>\n <i class="avatar" ng-style="{{avatarStyle}}" ng-if="formData.avatar"></i>\n <h3><span class="dark" ng-bind-html="formData.title"></span></h3>\n <h4>&nbsp;</h4>\n </div>\n <h4 class="content dark" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </h4>\n <h4 class="content gray hidden-xs hidden-sm" ng-if="formData.city">\n <i class="icon ion-location"></i>\n <span ng-bind-html="formData.city"></span>\n </h4>\n </div>\n\n <div class="row no-padding-xs">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n\n <div class="col list animate-fade-slide-in item-text-wrap no-padding-xs">\n\n <div class="item">\n <h2 class="gray">\n <a ng-if="formData.city" ui-sref="app.registry_lookup({location:formData.city})">\n <i class="icon ion-location"></i>\n <span ng-bind-html="formData.city"></span>\n </a>\n <span ng-if="formData.city && formData.type">&nbsp;|&nbsp;</span>\n <a ng-if="formData.type" ui-sref="app.registry_lookup({type:formData.type})">\n <i class="cion-page-{{formData.type}}"></i>\n {{\'REGISTRY.TYPE.ENUM.\'+formData.type|upper|translate}}\n </a>\n </h2>\n <h4>\n <i class="icon ion-clock" ng-if="formData.time"></i>\n <span translate>COMMON.SUBMIT_BY</span>\n <a ng-class="{\'positive\': issuer.uid, \'gray\': !issuer.uid}" ui-sref="app.wot_identity({pubkey:issuer.pubkey, uid: issuer.name||issuer.uid})">\n <ng-if ng-if="issuer.uid">\n <i class="icon ion-person"></i>\n {{::issuer.name||issuer.uid}}\n </ng-if>\n <span ng-if="!issuer.uid">\n <i class="icon ion-key"></i>\n {{issuer.pubkey|formatPubkey}}\n </span>\n </a>\n <span>\n {{formData.time|formatFromNow}}\n <h4 class="gray hidden-xs">|\n {{formData.time | formatDate}}\n </h4>\n </span>\n </h4>\n </div>\n\n <!-- Buttons bar-->\n <a id="registry-share-anchor-{{id}}"></a>\n <div class="item large-button-bar hidden-xs hidden-sm">\n <button class="button button-stable button-small-padding icon ion-android-share-alt" ng-click="showSharePopover($event)">\n </button>\n <!--<button class="button button-calm ink-dark"-->\n <!--ng-if="formData.pubkey && !isUserPubkey(formData.pubkey)"-->\n <!--ng-click="showTransferModal({pubkey:formData.pubkey, uid: formData.title})">-->\n <!--{{\'COMMON.BTN_SEND_MONEY\' | translate}}-->\n <!--</button>-->\n <button class="button button-stable icon-left ink-dark" ng-if="canEdit" ng-click="delete()">\n <i class="icon ion-trash-a assertive"></i>\n <span class="assertive"> {{\'COMMON.BTN_DELETE\' | translate}}</span>\n </button>\n <button class="button button-calm icon-left ion-android-create ink" ng-
$templateCache.put('plugins/es/templates/registry/view_wallet_pages.html','<ion-view left-buttons="leftButtons" class="view-notification">\n <ion-nav-title>\n {{\'REGISTRY.MY_PAGES\' | translate}}\n </ion-nav-title>\n\n <ion-content class="padding no-padding-xs" scroll="true">\n\n <ion-refresher pulling-text="{{:locale:\'COMMON.BTN_REFRESH\' | translate}}" on-refresh="doUpdate()">\n </ion-refresher>\n\n <!-- Buttons bar -->\n <div class="hidden-xs hidden-sm padding text-center">\n\n <button class="button button-stable button-small-padding icon ion-loop ink" ng-click="doUpdate()" title="{{\'COMMON.BTN_REFRESH\' | translate}}">\n </button>\n\n <button class="button button-calm icon-left ink" ng-click="showNewPageModal()">\n {{\'REGISTRY.BTN_NEW\' | translate}}\n </button>\n </div>\n\n <div class="center padding" ng-if="search.loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="center padding gray" ng-if="!search.loading && !search.results.length" translate>\n REGISTRY.NO_PAGE\n </div>\n\n <ng-include src="\'plugins/es/templates/registry/lookup_list.html\'"></ng-include>\n\n </ion-content>\n\n <button id="fab-wallet-add-registry-record" class="button button-fab button-fab-bottom-right button-assertive hidden-md hidden-lg spin" ng-click="showNewPageModal()">\n <i class="icon ion-plus"></i>\n </button>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/settings/plugin_settings.html','<ion-view left-buttons="leftButtons" class="settings">\n <ion-nav-title translate>ES_SETTINGS.PLUGIN_NAME</ion-nav-title>\n\n <ion-content scroll="true">\n\n <span class="item item-divider" translate>SETTINGS.NETWORK_SETTINGS</span>\n\n <div class="item ink" ng-click="formData.enable && changeEsNode()" ng-disabled="!formData.enable">\n <div class="input-label" ng-class="{\'gray\': !formData.enable}">\n {{\'ES_SETTINGS.PEER\' | translate}}\n </div>\n <span class="item-note" ng-class="{\'dark\': formData.enable}">{{getServer()}}</span>\n </div>\n\n <!--span class="item item-divider" translate>ES_SETTINGS.NOTIFICATIONS.DIVIDER</span>\n\n <span class="item gray item-text-wrap" translate>ES_SETTINGS.NOTIFICATIONS.HELP_TEXT</span>\n\n <div class="item item-toggle dark" >\n <div class="input-label" ng-class="{\'gray\': !formData.enable}" translate>ES_SETTINGS.NOTIFICATIONS.ENABLE_TX_SENT</div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.notifications.txSent" ng-disabled="!formData.enable">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n <div class="item item-toggle dark" >\n <div class="input-label" ng-class="{\'gray\': !formData.enable}" translate>ES_SETTINGS.NOTIFICATIONS.ENABLE_TX_RECEIVED</div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.notifications.txReceived" ng-disabled="!formData.enable">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n <div class="item item-toggle dark" >\n <div class="input-label" ng-class="{\'gray\': !formData.enable}" translate>ES_SETTINGS.NOTIFICATIONS.ENABLE_CERT_SENT</div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.notifications.certSent" ng-disabled="!formData.enable">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n <div class="item item-toggle dark" >\n <div class="input-label" ng-class="{\'gray\': !formData.enable}" translate>ES_SETTINGS.NOTIFICATIONS.ENABLE_CERT_RECEIVED</div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.notifications.certReceived" ng-disabled="!formData.enable">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div-->\n\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/settings/settings_extend.html','\n<!--<span class="item item-divider" translate>SETTINGS.PLUGINS_SETTINGS</span>-->\n\n<div class="item item-text-wrap ink item-icon-right" ui-sref="app.es_settings">\n <div class="input-label ng-binding" translate>ES_SETTINGS.PLUGIN_NAME</div>\n <!--<h4 class="gray" translate>ES_SETTINGS.PLUGIN_NAME_HELP</h4>-->\n <i class="gray icon ion-ios-arrow-right"></i>\n</div>\n');
$templateCache.put('plugins/es/templates/subscription/edit_subscriptions.html','<ion-view left-buttons="leftButtons" class="view-notification">\n <ion-nav-title>\n {{\'SUBSCRIPTION.EDIT.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content class="padding no-padding-xs" scroll="true">\n\n <ion-refresher pulling-text="{{:locale:\'COMMON.BTN_REFRESH\' | translate}}" on-refresh="load()">\n </ion-refresher>\n\n <!-- Buttons bar -->\n <div class="hidden-xs hidden-sm padding text-center">\n\n <button class="button button-stable button-small-padding icon ion-loop ink" ng-click="load()" title="{{\'COMMON.BTN_REFRESH\' | translate}}">\n </button>\n\n <button class="button button-calm ink" ng-click="addSubscription()">\n {{\'SUBSCRIPTION.BTN_ADD\' | translate}}\n </button>\n </div>\n\n <div class="center padding" ng-if="search.loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="center padding gray" ng-if="!search.loading && !search.results.length" translate>\n SUBSCRIPTION.NO_SUBSCRIPTION\n </div>\n\n <div class="row no-padding">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col list {{::motion.ionListClass}} item-border-large">\n\n <!-- emails -->\n <ng-repeat ng-repeat="subscriptions in search.results | filter: { type: \'email\' }" ng-include="\'plugins/es/templates/subscription/item_\' + subscriptions.type.toLowerCase() + \'_subscription.html\'">>\n </ng-repeat>\n\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n </div>\n\n </ion-content>\n\n <button id="fab-add-subscription-record" class="button button-fab button-fab-bottom-right button-assertive icon ion-plus hidden-md hidden-lg spin" ng-click="addSubscription()">\n </button>\n</ion-view>\n');
$templateCache.put('plugins/es/templates/subscription/item_email_subscription.html','<div class="item item-avatar">\n <i class="item-image icon ion-email"></i>\n <h3>\n {{\'SUBSCRIPTION.TYPE.ENUM.\' + subscriptions.type.toUpperCase() | translate}}\n </h3>\n <h4 class="gray">\n {{\'SUBSCRIPTION.EDIT.PROVIDER\'|translate}}\n <a ui-sref="app.wot_identity({pubkey: subscriptions.recipient, uid: subscriptions.uid})">\n <span ng-class="{\'positive\': subscriptions.uid, \'dark\': !subscriptions.uid}" ng-if="subscriptions.name||subscriptions.uid">\n <i class="ion-person" ng-if="subscriptions.uid"></i>\n {{subscriptions.name||subscriptions.uid}}\n </span>\n <span class="gray" ng-if="!subscriptions.uid">\n <i class="ion-key"></i>\n {{subscriptions.recipient | formatPubkey}}\n </span>\n </a>\n </h4>\n <div class="item-note text-right">\n <span ng-repeat="item in subscriptions.items">\n {{item.content.email}}\n <a class="ion-trash-a gray padding-left" ng-click="deleteSubscription(item)"></a>\n <a class="ion-edit gray padding-left" ng-click="editSubscription(item)"></a>\n <br>\n </span>\n </div>\n</div>\n');
$templateCache.put('plugins/es/templates/subscription/modal_email.html','\n<ion-modal-view id="composeMessage" class="modal-full-height">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear visible-xs" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n <h1 class="title" translate>SUBSCRIPTION.MODAL_EMAIL.TITLE</h1>\n\n <button class="button button-icon button-clear icon ion-android-send visible-xs" ng-click="doSubmit()">\n </button>\n </ion-header-bar>\n\n <ion-content scroll="true">\n\n <!-- Encryption info -->\n <div class="item item-icon-left item-text-wrap">\n <i class="icon ion-ios-information-outline positive"></i>\n <h4 class="positive" translate>SUBSCRIPTION.MODAL_EMAIL.HELP</h4>\n </div>\n\n <form name="subscriptionForm" novalidate="" ng-submit="doSubmit()">\n\n <div class="list" ng-init="setForm(subscriptionForm)">\n\n <!-- email -->\n <label class="item item-input" ng-class="{\'item-input-error\': form.$submitted && (form.email.$invalid || form.email.$error)}">\n <span class="input-label" translate>SUBSCRIPTION.MODAL_EMAIL.EMAIL_LABEL</span>\n <input name="email" type="text" placeholder="{{\'SUBSCRIPTION.MODAL_EMAIL.EMAIL_HELP\' | translate}}" ng-model="formData.content.email" ng-minlength="3" required email>\n </label>\n <div class="form-errors" ng-if="form.$submitted && (form.email.$invalid || form.email.$error)" ng-messages="form.email.$error">\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT"></span>\n </div>\n <div class="form-error" ng-message="email">\n <span translate="ERROR.FIELD_NOT_EMAIL"></span>\n </div>\n </div>\n\n <!-- Frequency -->\n <label class="item item-input item-select" ng-class="{\'item-input-error\': form.$submitted && !formData.content.frequency}">\n <span class="input-label" translate>SUBSCRIPTION.MODAL_EMAIL.FREQUENCY_LABEL</span>\n <select name="frequency" ng-model="formData.content.frequency" style="height: 46px;margin-top: 1px">\n <option value="weekly" translate>SUBSCRIPTION.MODAL_EMAIL.FREQUENCY_WEEKLY</option>\n <option value="daily" translate>SUBSCRIPTION.MODAL_EMAIL.FREQUENCY_DAILY</option>\n </select>\n </label>\n <div class="form-errors" ng-if="form.$submitted && !formData.content.frequency">\n <div class="form-error">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- Recipient (service provider) -->\n <a class="item item-input item-icon-right gray ink" ng-class="{\'item-input-error\': form.$submitted && !formData.recipient}" ng-click="showNetworkLookup()" style="height: 67px">\n <span class="input-label" translate>SUBSCRIPTION.MODAL_EMAIL.PROVIDER</span>\n <span class="badge animate-fade-in animate-show-hide ng-hide" ng-class="{\'badge-royal\': recipient.name, \'badge-stable\': !recipient.name}" ng-show="recipient && (recipient.name)">\n <i class="ion-person" ng-if="recipient.name"></i> {{recipient.name}}\n </span>\n <span class="badge badge-secondary animate-fade-in animate-show-hide ng-hide" ng-show="formData.recipient">\n <i class="ion-key"></i> {{formData.recipient | formatPubkey}}\n </span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n <div class="form-errors" ng-if="form.$submitted && !formData.recipient">\n <div class="form-error">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n </div>\n\n <div class="padding hidden-xs text-right">\n <button class="button button-clear button-dark ink" ng-click="cancel()" type="button" translate>COMMON.BTN_CANCEL\n </button>\n <button class="button button-positive ink" type="submit">\n {{\'COMMON.BTN_ADD\' | translate}}\n </button>\n </div>\n\n
$templateCache.put('plugins/es/templates/user/edit_profile.html','<ion-view left-buttons="leftButtons">\n <ion-nav-title>\n <!-- no title-->\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear ion-android-done visible-xs visible-sm" ng-click="submitAndSaveAndClose()">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true">\n\n <div class="positive-900-bg hero">\n <div class="content">\n <i class="avatar" ng-style="avatarStyle" ng-class="{\'avatar-wallet\': !loading && !avatar && walletData && !walletData.isMember, \'avatar-member\': !loading && !avatar && walletData.isMember}">\n <button class="button button-positive button-large button-clear flat icon ion-camera visible-xs visible-sm" style="display: inline-block" ng-click="showAvatarModal()"></button>\n <button ng-if="avatar.src" class="button button-positive button-large button-clear flat visible-xs visible-sm" style="display: inline-block; left: 85px; bottom:15px" ng-click="rotateAvatar()">\n <i class="icon-secondary ion-image" style="left: 24px; top: 3px; font-size: 24px"></i>\n <i class="icon-secondary ion-forward" style="left: 26px; top: -13px"></i>\n </button>\n <button class="button button-positive button-large button-clear icon ion-camera hidden-xs hidden-sm" ng-click="showAvatarModal()"></button>\n </i>\n <h3 class="light">\n <ng-if ng-if="!loading && !formData.title && walletData && walletData.isMember">{{walletData.uid}}</ng-if>\n <ng-if ng-if="!loading && !formData.title && walletData && !walletData.isMember">{{::walletData.pubkey | formatPubkey}}</ng-if>\n <ng-if ng-if="!loading && formData.title">{{formData.title}}</ng-if>\n </h3>\n <h4 class="light">\n <ion-spinner ng-if="loading" icon="android"></ion-spinner>\n </h4>\n </div>\n </div>\n\n\n <div class="row no-padding">\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n\n <div class="col">\n <form name="profileForm" novalidate="" ng-submit="saveAndClose()">\n\n <div class="list item-text-wrap {{::motion.ionListClass}}" ng-init="setForm(profileForm)">\n\n <!-- Public info -->\n <div class="item item-icon-left item-text-wrap">\n <i class="icon ion-ios-information-outline positive"></i>\n <h4 class="positive" translate>PROFILE.HELP.WARNING_PUBLIC_DATA</h4>\n </div>\n\n <div class="item item-divider">\n {{\'PROFILE.GENERAL_DIVIDER\' | translate}}\n </div>\n\n <!-- title -->\n <ion-item class="item-input item-floating-label item-button-right" ng-class="{\'item-input-error\': form.$submitted && form.title.$invalid}">\n <span class="input-label">{{\'PROFILE.TITLE\' | translate}}</span>\n <input type="text" name="title" placeholder="{{\'PROFILE.TITLE_HELP\' | translate}}" id="profile-name" ng-model="formData.title" ng-model-options="{ debounce: 350 }" ng-maxlength="50" required>\n </ion-item>\n <div class="form-errors" ng-show="form.$submitted && form.title.$error" ng-messages="form.title.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n <div class="form-error" ng-message="maxlength">\n <span translate="ERROR.FIELD_TOO_LONG_WITH_LENGTH" translate-values="{maxLength: 50}"></span>\n </div>\n </div>\n\n <!-- description -->\n <ion-item class="item-input item-floating-label item-button-right">\n <span class="input-label" style="width: 100%">{{\'PROFILE.DESCRIPTION\' | translate}}</span>\n <textarea placeholder="{{\'PROFILE.DESCRIPTION_HELP\' | translate}}" ng-model="formData.description" ng-model-options="{ debounce: 350 }" rows="4" cols="10">\n </textarea>\n
$templateCache.put('plugins/es/templates/user/items_profile.html','<div ng-if="!formData.profile && !formData.name" class="item gray" translate>PROFILE.NO_PROFILE_DEFINED</div>\n\n<!-- name -->\n<div class="item" ng-if="formData.name && showName">\n <span class="gray" translate>PROFILE.TITLE</span>\n <h3>{{formData.name}}</h3>\n</div>\n\n<!-- About me -->\n<div class="item item-text-wrap" ng-if="formData.profile.description">\n <span class="gray" translate>PROFILE.DESCRIPTION</span>\n <h3 trust-as-html="formData.profile.description"></h3>\n</div>\n\n<!-- Localisation -->\n<div class="item" ng-if="formData.profile.address || formData.profile.city" copy-on-click="{{formData.profile.address ? formData.profile.address + \'&#10;\' : \'\'}}{{formData.profile.city}}">\n <span class="gray" translate>LOCATION.LOCATION_DIVIDER</span>\n <h3>\n <span class="text-keep-lines" ng-if="formData.profile.address">{{formData.profile.address}}<br></span>\n {{formData.profile.city}}\n </h3>\n</div>\n\n<!-- Socials networks -->\n<div class="item" ng-if="formData.profile.socials && formData.profile.socials.length" ng-controller="ESSocialsViewCtrl">\n <span class="gray" translate>PROFILE.SOCIAL_NETWORKS_DIVIDER</span>\n <div class="list no-padding">\n <ion-item ng-repeat="social in formData.profile.socials | filter:filterFn track by social.url " id="social-{{::social.url|formatSlug}}" class="item-icon-left item-text-wrap no-padding-bottom ink" ng-click="openSocial($event, social)">\n <i class="icon ion-social-{{social.type}}" ng-class="{\'ion-bookmark\': social.type == \'other\', \'ion-link\': social.type == \'web\', \'ion-email\': social.type == \'email\', \'ion-iphone\': social.type == \'phone\'}"></i>\n <p ng-if="social.type && social.type != \'web\'">\n {{social.type}}\n <i class="ion-locked" ng-if="social.recipient"></i>\n </p>\n <h4>\n <a>{{::social.url}}</a>\n </h4>\n </ion-item>\n </div>\n</div>\n\n\n');
$templateCache.put('plugins/es/templates/wallet/view_wallet_extend.html','<ng-if ng-if=":state:enable && extensionPoint === \'hero\'">\n <!-- likes -->\n <h4 class="light">\n <small ng-include="\'plugins/es/templates/common/view_likes.html\'" ng-init="canEdit=true"></small>\n </h4>\n</ng-if>\n\n<ng-if ng-if=":state:enable && extensionPoint === \'after-general\'">\n\n <!-- profile -->\n <div class="item item-divider item-divider-top-border">\n <span ng-bind-html="\'PROFILE.PROFILE_DIVIDER\' | translate"></span>\n <a class="badge button button-text button-small button-small-padding" ui-sref="app.user_edit_profile">\n <i class="icon ion-edit"></i>\n <span ng-if="!formData.profile" translate>PROFILE.BTN_ADD</span>\n <span ng-if="formData.profile" translate>PROFILE.BTN_EDIT</span>\n </a>\n </div>\n\n <ng-include src="\'plugins/es/templates/user/items_profile.html\'" ng-init="showName=true"></ng-include>\n\n <!-- subscriptions -->\n <div class="item item-divider item-divider-top-border">\n <span>\n {{\'SUBSCRIPTION.SUBSCRIPTION_DIVIDER\' | translate}}\n <i style="font-size: 12pt; cursor: pointer" ng-click="showSubscriptionHelp=!showSubscriptionHelp" class="icon positive ion-ios-help-outline" title="{{\'SUBSCRIPTION.SUBSCRIPTION_DIVIDER_HELP\' | translate}}"></i>\n <span>\n\n <a class="badge button button-text button-small button-small-padding" ng-if="!formData.subscriptions.count" ui-sref="app.edit_subscriptions">\n <i class="icon ion-edit"></i>\n <span translate>SUBSCRIPTION.BTN_ADD</span>\n </a>\n </span></span></div>\n\n <div class="item item-text-wrap positive item-small-height" ng-show="showSubscriptionHelp">\n <small translate>SUBSCRIPTION.SUBSCRIPTION_DIVIDER_HELP</small>\n </div>\n\n <div ng-if="!formData.subscriptions.count" class="item gray" translate>SUBSCRIPTION.NO_SUBSCRIPTION</div>\n\n <a class="item item-icon-left item-text-wrap item-icon-right ink" ng-if="formData.subscriptions.count" ui-sref="app.edit_subscriptions">\n <i class="icon ion-gear-a"></i>\n <span translate>SUBSCRIPTION.SUBSCRIPTION_COUNT</span>\n <span class="badge badge-calm">{{formData.subscriptions.count}}</span>\n\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n\n <!-- page -->\n <div class="item item-divider item-divider-top-border">\n <span>\n {{\'REGISTRY.WALLET.REGISTRY_DIVIDER\' | translate}}\n <i style="font-size: 12pt; cursor: pointer" ng-click="showPagesHelp=!showPagesHelp" class="icon positive ion-ios-help-outline" title="{{\'REGISTRY.WALLET.REGISTRY_HELP\' | translate}}"></i>\n <span>\n\n <a class="badge button button-text button-small button-small-padding" ng-if="!formData.pages.count" ng-click="showNewPageModal($event)">\n <i class="icon ion-edit"></i>\n <span translate>REGISTRY.BTN_NEW</span>\n </a>\n </span></span></div>\n\n <div class="item item-text-wrap positive item-small-height" ng-show="showPagesHelp">\n <small translate>REGISTRY.WALLET.REGISTRY_HELP</small>\n </div>\n\n <div ng-if="!formData.pages.count" class="item gray" translate>REGISTRY.NO_PAGE</div>\n\n <a class="item item-icon-left item-text-wrap item-icon-right ink" ng-if="formData.pages.count" ui-sref="app.wallet_pages">\n <i class="icon ion-social-buffer"></i>\n <span translate>REGISTRY.MY_PAGES</span>\n <span class="badge badge-calm">{{formData.pages.count}}</span>\n\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n</ng-if>\n');
$templateCache.put('plugins/es/templates/wot/view_identity_extend.html','<!-- Hero -->\n<ng-if ng-if=":state:enable && extensionPoint === \'hero\'">\n <!-- likes -->\n <h4 class="light">\n <small ng-include="\'plugins/es/templates/common/view_likes.html\'"></small>\n </h4>\n</ng-if>\n\n<!-- Top fab buttons -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons-top-fab\'">\n <button id="fab-compose-{{:rebind:formData.pubkey}}" class="button button-fab button-fab-top-left button-fab-hero mini button-stable spin" style="left: 88px" ng-click="showNewMessageModal()">\n <i class="icon ion-compose"></i>\n </button>\n</ng-if>\n\n<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons\'">\n\n <!-- message -->\n <button class="button button-calm button-raised icon icon-left icon ion-compose ink" ng-click="showNewMessageModal()" title="{{\'MESSAGE.BTN_WRITE\' | translate}}" translate>\n MESSAGE.BTN_WRITE\n </button>\n <!-- Star -->\n <button class="button button-stable button-small-padding ink" ng-if="likeData.stars && !likeData.stars.wasHit" title="{{\'WOT.VIEW.BTN_STAR_HELP\'|translate: likeData.stars }}" ng-click="showStarPopover($event)">\n <i class="icon ion-android-star-outline"></i>\n </button>\n <button class="button button-stable button-small-padding ink" ng-if="likeData.stars.wasHit" title="{{\'WOT.VIEW.BTN_REDO_STAR_HELP\'|translate: likeData.stars }}" ng-click="showStarPopover($event)">\n <i class="icon" ng-class="{\'ion-android-star-half\': likeData.stars.level > 0 && likeData.stars.level <=3, \'ion-android-star\': likeData.stars.level > 3}"></i>\n <span>{{likeData.stars.level}}/5</span>\n </button>\n\n <!-- options -->\n <button class="button button-stable button-small-padding icon ion-android-more-vertical" ng-click="showActionsPopover($event)">\n </button>\n</ng-if>\n\n<!-- General section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'general\'">\n\n <!-- star level -->\n <div class="item item-icon-left item-text-wrap" ng-if="likeData.stars" ng-click="smallscreen && showStarPopover($event)">\n\n <i class="icon" ng-class="{\'ion-android-star-outline\': likeData.stars.levelAvg <= 2, \'ion-android-star-half\': likeData.stars.levelAvg > 2 && likeData.stars.levelAvg <= 3, \'ion-android-star energized\': likeData.stars.levelAvg > 3}"></i>\n\n <span translate>WOT.VIEW.STARS</span>\n <h4 class="dark">{{\'WOT.VIEW.STAR_HIT_COUNT\' | translate: likeData.stars }}</h4>\n\n <div class="badge" ng-if="likeData.stars.loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n <div class="badge" ng-if="!likeData.stars.loading">\n <span ng-repeat="value in [1,2,3,4,5]" ng-class="{\'energized\': likeData.stars.levelAvg > 3, \'assertive\': likeData.stars.levelAvg <= 2}">\n <b class="ion-android-star" ng-if="value <= likeData.stars.levelAvg"></b>\n <b class="ion-android-star-half" ng-if="value > likeData.stars.levelAvg && value - 0.5 <= likeData.stars.levelAvg"></b>\n <b class="ion-android-star-outline" ng-if="value > likeData.stars.levelAvg && value - 0.5 > likeData.stars.levelAvg"></b>\n </span>\n <small class="dark">({{likeData.stars.levelAvg}}/5)</small>\n </div>\n </div>\n</ng-if>\n\n<!-- After general section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'after-general\'">\n\n <span class="item item-divider item-divider-top-border" translate>PROFILE.PROFILE_DIVIDER</span>\n\n <div class="double-padding-x padding-bottom">\n <ng-include src="\'plugins/es/templates/user/items_profile.html\'" ng-init="showName=false;"></ng-include>\n </div>\n\n</ng-if>\n');
$templateCache.put('plugins/es/templates/wot/view_popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink visible-xs visible-sm" ng-click="showSharePopover($event)">\n <i class="icon ion-android-share-alt"></i>\n {{\'COMMON.BTN_SHARE\' | translate}}\n </a>\n\n <!--<a class="item item-icon-left assertive ink "\n ng-if="canEdit"\n ng-click="delete()">\n <i class="icon ion-trash-a"></i>\n {{\'COMMON.BTN_DELETE\' | translate}}\n </a>-->\n\n <!-- Follow -->\n <a class="item item-icon-left ink" ng-if="!canEdit && likeData.follows" ng-click="hideActionsPopover() && toggleLike($event, {kind: \'follow\'})">\n <i class="icon" ng-class="{\'ion-android-notifications-off\': likeData.follows.wasHit, \'ion-android-notifications\': !likeData.follows.wasHit}"></i>\n <b class="ion-plus icon-secondary" ng-if="!likeData.follows.wasHit" style="font-size: 16px; left: 38px; top: -7px"></b>\n {{(!likeData.follows.wasHit ? \'WOT.VIEW.BTN_FOLLOW\' : \'WOT.VIEW.BTN_STOP_FOLLOW\' )| translate}}\n </a>\n\n <!-- report abuse -->\n <a class="item item-icon-left ink" ng-if="!canEdit && likeData.abuses && !likeData.abuses.wasHit" ng-click="hideActionsPopover() && reportAbuse($event)">\n <i class="icon ion-android-warning"></i>\n {{\'COMMON.BTN_REPORT_ABUSE_DOTS\' | translate}}\n </a>\n <a class="item item-icon-left ink" ng-if="!canEdit && likeData.abuses && likeData.abuses.wasHit" ng-click="hideActionsPopover() && toggleLike($event, {kind: \'abuse\'})">\n <i class="icon ion-android-warning"></i>\n <b class="ion-close icon-secondary" style="font-size: 16px; left: 38px; top: -7px"></b>\n {{\'COMMON.BTN_REMOVE_REPORTED_ABUSE\' | translate}}\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/map/templates/user/edit_profile_extend.html','<div class="item no-padding {{ionItemClass}}" ng-if="formData.geoPoint && formData.geoPoint.lat && formData.geoPoint.lon">\n <leaflet height="250px" center="map.center" markers="map.markers" defaults="map.defaults">\n </leaflet>\n</div>\n');
$templateCache.put('plugins/graph/templates/account/graph_balance.html','\n <!-- button bar -->\n <div class="button-bar-inline" style="top: 33px; margin-top:-33px; position: relative">\n <button class="button button-stable button-clear no-padding-xs pull-right" ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <div class="padding-left padding-right">\n <canvas id="account-balance" class="chart-bar" height="{{height}}" width="{{width}}" chart-data="data" chart-dataset-override="datasetOverride" chart-colors="colors" chart-options="options" chart-labels="labels" chart-click="onChartClick">\n </canvas>\n </div>\n\n <ng-include src="\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n');
$templateCache.put('plugins/graph/templates/account/graph_certifications.html','\n <div class="padding-left padding-right">\n <canvas id="account-certifications" class="chart-bar" height="{{height}}" width="{{width}}" chart-data="data" chart-dataset-override="datasetOverride" chart-colors="colors" chart-options="options" chart-labels="labels" chart-click="onChartClick">\n </canvas>\n </div>\n');
$templateCache.put('plugins/graph/templates/account/graph_sum_tx.html','<div class="row responsive-sm" ng-if="!loading">\n\n <div class="col col-10 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col text-center">\n\n <!-- [NEW] TX input chart -->\n <p class="gray padding text-wrap" ng-if="inputChart.data.length" translate>GRAPH.ACCOUNT.INPUT_CHART_TITLE</p>\n <canvas id="chart-received-pie" class="chart-pie" chart-data="inputChart.data" chart-labels="inputChart.labels" chart-colors="inputChart.colors" chart-click="onInputChartClick">\n </canvas>\n\n </div>\n\n <div class="col col-10 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col text-center">\n\n <!-- [NEW] TX input chart -->\n <p class="gray padding text-wrap" ng-if="outputChart.data.length" translate>GRAPH.ACCOUNT.OUTPUT_CHART_TITLE</p>\n <canvas id="chart-sent-pie" class="chart-pie" chart-data="outputChart.data" chart-labels="outputChart.labels" chart-colors="outputChart.colors" chart-click="onOutputChartClick">\n </canvas>\n\n </div>\n\n <div class="col col-10 hidden-xs hidden-sm">&nbsp;</div>\n\n</div>\n');
$templateCache.put('plugins/graph/templates/account/view_stats.html','<ion-view left-buttons="leftButtons" cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.ACCOUNT.TITLE\' | translate}}{{id}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="no-padding">\n\n\n\n <div class="list">\n\n <!-- - - - - Balance - - - - -->\n <ng-controller ng-controller="GpAccountBalanceCtrl">\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item item-divider" ng-if="!loading">\n {{\'GRAPH.ACCOUNT.BALANCE_DIVIDER\'|translate}}\n <ion-spinner ng-if="loadingRange" class="ion-spinner-small" icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs" ng-if="!loading" ng-include="\'plugins/graph/templates/account/graph_balance.html\'" ng-init="setSize(350, 1000)">\n </div>\n </ng-controller>\n\n </div>\n\n <div class="item no-padding-xs" ng-include="\'plugins/graph/templates/account/graph_sum_tx.html\'" ng-controller="GpAccountSumTxCtrl">\n </div>\n\n\n </ion-content>\n\n</ion-view>\n');
$templateCache.put('plugins/graph/templates/common/graph_range_bar.html','\n <div class="range range-positive no-padding-left no-padding-right">\n <a class="button button-stable button-clear no-padding pull-left" ng-click="goPreviousRange($event)">\n <i class="icon ion-chevron-left"></i>\n </a>\n <input type="range" ng-model="formData.timePct" name="timePct" min="0" max="100" value="{{formData.timePct}}" ng-change="onRangeChanged();" ng-model-options="{ debounce: 250 }">\n <a class="button button-stable button-clear no-padding pull-right" ng-click="goNextRange($event)">\n <i class="icon ion-chevron-right"></i>\n </a>\n </div>\n');
$templateCache.put('plugins/graph/templates/common/popover_range_actions.html','<ion-popover-view class="has-header popover-graph-currency">\n <ion-header-bar>\n <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <!-- duration divider -->\n <div class="item item-divider">\n {{\'GRAPH.COMMON.RANGE_DURATION_DIVIDER\'|translate}}\n </div>\n\n <!-- duration: hour -->\n <a class="item item-icon-left ink" ng-click="setRangeDuration(\'hour\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'hour\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.HOUR\' | translate"></span>\n </a>\n\n <!-- duration: day -->\n <a class="item item-icon-left ink" ng-click="setRangeDuration(\'day\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'day\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.DAY\' | translate"></span>\n </a>\n\n <!-- duration: month -->\n <a class="item item-icon-left ink" ng-click="setRangeDuration(\'month\')">\n <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==\'month\'"></i>\n <span ng-bind-html="\'GRAPH.COMMON.RANGE_DURATION.MONTH\' | translate"></span>\n </a>\n\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/graph/templates/docstats/graph.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline" style="top: 33px; margin-top:-33px; position: relative">\n <button class="button button-stable button-clear no-padding-xs pull-right" ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="{{::chartIdPrefix}}{{chart.id}}" class="chart-line" height="{{height}}" width="{{width}}" chart-data="chart.data" chart-labels="labels" chart-dataset-override="chart.datasetOverride" chart-options="chart.options" chart-click="onChartClick">\n </canvas>\n\n <ng-include src="\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n');
$templateCache.put('plugins/graph/templates/docstats/view_stats.html','<ion-view left-buttons="leftButtons" cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.DOC_STATS.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding">\n\n <div class="list">\n\n <!-- Doc stat -->\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs" ng-if="!loading" ng-repeat="chart in charts" ng-include="\'plugins/graph/templates/docstats/graph.html\'" ng-init="setSize(250, 1000)">\n </div>\n\n </div>\n\n </ion-content>\n\n</ion-view>\n');
$templateCache.put('plugins/graph/templates/network/view_es_network_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'buttons\'">\n <a class="button button-text button-small ink" ui-sref="app.doc_stats_lg">\n <i class="icon ion-stats-bars"></i>\n <span>{{\'NETWORK.VIEW.BTN_GRAPH\'|translate}}</span>\n </a>\n</ng-if>\n');
$templateCache.put('plugins/graph/templates/network/view_es_peer_extend.html','<!-- Buttons section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'general\'">\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink" ng-if="isReachable" ui-sref="app.doc_stats_lg">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.DOC_STATS.TITLE</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n\n <a class="item item-icon-left item-icon-right item-text-wrap ink" ng-if="isReachable" ui-sref="app.doc_synchro_lg">\n <i class="icon ion-stats-bars"></i>\n <span translate>GRAPH.SYNCHRO.TITLE</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n</ng-if>\n\n');
$templateCache.put('plugins/graph/templates/synchro/graph.html','\n <!-- graphs button bar -->\n <div class="button-bar-inline" style="top: 33px; margin-top:-33px; position: relative">\n <button class="button button-stable button-clear no-padding-xs pull-right" ng-click="showActionsPopover($event)">\n <i class="icon ion-navicon-round"></i>\n </button>\n </div>\n\n <canvas id="synchro-chart-{{chart.id}}" class="chart-bar" height="{{height}}" width="{{width}}" chart-data="chart.data" chart-labels="labels" chart-dataset-override="chart.datasetOverride" chart-options="chart.options">\n </canvas>\n\n <ng-include src="\'plugins/graph/templates/common/graph_range_bar.html\'"></ng-include>\n');
$templateCache.put('plugins/graph/templates/synchro/view_stats.html','<ion-view left-buttons="leftButtons" cache-view="false">\n <ion-nav-title>\n {{\'GRAPH.SYNCHRO.TITLE\' | translate}}\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding">\n\n <div class="list">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="item no-padding-xs" ng-if="!loading" ng-repeat="chart in charts" ng-include="\'plugins/graph/templates/synchro/graph.html\'" ng-init="setSize(250, 1000)">\n </div>\n\n </div>\n\n </ion-content>\n\n</ion-view>\n');
$templateCache.put('plugins/market/templates/category/card_category_lg.html','\n <div class="item card stable-bg padding">\n <!-- header: parent category -->\n <div class="card-header">\n <h3 class="dark" ng-class="{\'bold\': cat.count}">\n <span ng-bind-html="cat.name"></span> <ng-if ng-if="cat.count">({{cat.count}})</ng-if>\n </h3>\n </div>\n\n <!-- children categories-->\n <div class="item-text-wrap">\n <span ng-repeat="cat in cat.children" class="padding-right">\n <a ng-class="{\'bold\': cat.count}" ng-click="onCategoryClick(cat)"><ng-bind-html ng-bind-html="cat.name"></ng-bind-html><ng-if ng-if="cat.count"> ({{cat.count}})</ng-if></a>\n </span>\n </div>\n </div>\n');
$templateCache.put('plugins/market/templates/category/list_categories_lg.html','\n <div class="list half {{::motion.ionListClass}}">\n <ng-repeat ng-repeat="cat in categories track by cat.id" ng-if="$index % 2 == 0" ng-include="\'plugins/market/templates/category/card_category_lg.html\'">\n </ng-repeat>\n </div>\n\n <div class="list half {{::motion.ionListClass}}">\n <ng-repeat ng-repeat="cat in categories track by cat.id" ng-if="$index % 2 == 1" ng-include="\'plugins/market/templates/category/card_category_lg.html\'">\n </ng-repeat>\n </div>\n');
$templateCache.put('plugins/market/templates/category/view_categories.html','<ion-view left-buttons="leftButtons" class="market view-record">\n <ion-nav-title>\n <!--<span translate>MARKET.VIEW.TITLE</span>-->\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="refresh()">\n </button>\n </ion-nav-buttons>\n\n <ion-content>\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list {{::motion.ionListClass}}" ng-if="!loading">\n\n <!-- all categories -->\n <a class="item item-border item-icon-left item-icon-right bold" ng-class="{\'bold\': totalCount}" ng-click="onCategoryClick()">\n <span translate>MARKET.CATEGORY.ALL</span> ({{totalCount}})\n <i class="icon ion-ios-arrow-right"></i>\n </a>\n\n <!-- loop on root categories -->\n <ng-repeat ng-repeat="cat in categories track by cat.id">\n <div class="item item-divider" ng-class="{\'bold\': cat.count}">\n <span ng-bind-html="cat.name"></span> <ng-if ng-if="cat.count">({{cat.count}})</ng-if>\n </div>\n\n <!-- children categories-->\n <a ng-repeat="cat in cat.children track by cat.id" class="item item-border item-icon-left item-icon-right" ng-class="{\'bold\': cat.count}" ng-click="onCategoryClick(cat)">\n <span ng-bind-html="cat.name"></span><span ng-if="cat.count"> ({{cat.count}})</span>\n <i class="icon ion-ios-arrow-right"></i>\n </a>\n </ng-repeat>\n </div>\n\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/market/templates/category/view_categories_lg.html','<ion-view left-buttons="leftButtons" class="market view-record">\n <ion-nav-title>\n <!--<span translate>MARKET.VIEW.TITLE</span>-->\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n\n <!--<button class="button button-bar button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm"\n ng-click="showActionsPopover($event)">\n </button>-->\n </ion-nav-buttons>\n\n <ion-content class="padding">\n\n <ng-include src="\'plugins/market/templates/category/list_categories_lg.html\'"></ng-include>\n\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/market/templates/document/item_document_comment.html','<ion-item id="doc-{{::doc.id}}" class="item item-document item-document-comment item-icon-left ink {{::ionItemClass}} no-padding-top no-padding-bottom" ng-class="{\'compacted\': compactMode}" ng-click="selectDocument($event, doc)">\n\n <i ng-show=":rebind:!compactMode" ng-if=":rebind:!doc.avatar" class="icon ion-ios-chatbubble-outline stable"></i>\n <i ng-show=":rebind:!compactMode" ng-if=":rebind:doc.avatar" class="avatar" style="background-image: url(\'{{:rebind:doc.avatar.src}}\')"></i>\n\n <div class="row no-padding">\n <div class="col">\n <h4>\n <i class="ion-ios-chatbubble-outline dark"></i>\n <span class="gray" ng-if=":rebind:doc.name">\n <i class="ion-person" ng-show=":rebind:!compactMode"></i>\n {{:rebind:doc.name}}:\n </span>\n <span class="dark">\n <i class="ion-quote" ng-if=":rebind:!compactMode"></i>\n {{:rebind:doc.message|truncText:50}}\n </span>\n </h4>\n <h4 class="gray"> <i class="ion-clock"></i> {{:rebind:doc.time|formatDate}}</h4>\n </div>\n\n <div class="col">\n <h3>\n <a ui-sref="app.wot_identity({pubkey: doc.pubkey, uid: doc.name})">\n\n </a>\n </h3>\n </div>\n\n <div class="col" ng-if=":rebind:!compactMode">\n <a ng-if=":rebind:login && doc.pubkey==walletData.pubkey" ng-click="remove($event, $index)" class="gray pull-right hidden-xs hidden-sm" title="{{\'DOCUMENT.LOOKUP.BTN_REMOVE\'|translate}}">\n <i class="ion-trash-a"></i>\n </a>\n </div>\n\n </div>\n</ion-item>\n');
$templateCache.put('plugins/market/templates/document/item_document_profile.html','<ion-item id="doc-{{::doc.id}}" class="item item-document item-icon-left ink {{::ionItemClass}} no-padding-top no-padding-bottom" ng-class="{\'compacted\': compactMode}" ng-click="selectDocument($event, doc)">\n\n <i ng-show=":rebind:!compactMode" ng-if=":rebind:doc.avatar" class="avatar" style="background-image: url({{:rebind:doc.avatar.src}})"></i>\n <i ng-show=":rebind:!compactMode" ng-if=":rebind:!doc.avatar" class="icon ion-person stable"></i>\n\n <div class="row no-padding">\n <div class="col">\n <h4 ng-if=":rebind:doc.title">\n <i class="ion-person gray"></i>\n <span class="dark">\n {{:rebind:doc.title}}\n </span>\n <span class="gray">\n {{:rebind:\'DOCUMENT.LOOKUP.HAS_REGISTERED\'|translate}}\n </span>\n </h4>\n <h4>\n <span class="dark" ng-if=":rebind:doc.city">\n <i class="ion-location"></i> {{:rebind:doc.city}}\n </span>\n <span class="gray">\n <i class="ion-clock"></i> {{:rebind:doc.time|formatDate}}\n </span>\n </h4>\n </div>\n\n <div class="col" ng-if=":rebind:!compactMode">\n <a ng-if=":rebind:login && doc.pubkey==walletData.pubkey" ng-click="remove($event, $index)" class="gray pull-right" title="{{\'DOCUMENT.LOOKUP.BTN_REMOVE\'|translate}}">\n <i class="ion-trash-a"></i>\n </a>\n </div>\n\n </div>\n</ion-item>\n');
$templateCache.put('plugins/market/templates/document/item_document_record.html','<ion-item id="doc-{{::doc.id}}" class="item item-document item-icon-left item-text-wrap ink {{::ionItemClass}} no-padding-top no-padding-bottom" ng-class="{\'positive-100-bg\': doc.updated}" ng-click="selectDocument($event, doc)">\n\n <i ng-if=":rebind:doc.thumbnail" class="avatar" style="background-image: url({{:rebind:doc.thumbnail.src}})"></i>\n <i ng-if=":rebind:!doc.thumbnail" class="icon ion-speakerphone stable"></i>\n\n <div class="row no-padding">\n <div class="col">\n <h3 ng-if="doc.title">\n <i class="ion-speakerphone dark"></i>\n {{:rebind:doc.title}}\n </h3>\n <h4>\n <span class="dark" ng-if=":rebind:doc.picturesCount > 1">\n <i class="ion-camera"></i> {{:rebind:doc.picturesCount}}\n </span>\n <span class="dark" ng-if=":rebind:doc.city">\n <i class="ion-location"></i> {{:rebind:doc.city}}\n </span>\n <span class="gray" ng-if=":rebind:doc.name">\n <i class="ion-person"></i> {{:rebind:doc.name}}\n </span>\n\n\n </h4>\n </div>\n\n <div class="col col-33">\n <small class="gray pull-right"><i class="ion-clock"></i> {{:rebind:doc.time|formatDate}}\n </small>\n <a ng-if=":rebind:login && doc.pubkey==walletData.pubkey" ng-click="remove($event, $index)" class="gray pull-right hidden-xs hidden-sm" title="{{\'DOCUMENT.LOOKUP.BTN_REMOVE\'|translate}}">\n <i class="ion-trash-a"></i>\n </a>\n </div>\n\n </div>\n</ion-item>\n');
$templateCache.put('plugins/market/templates/document/list_documents.html','\n<ion-list class="list" ng-class="::motion.ionListClass">\n\n <ng-repeat ng-repeat="doc in :rebind:search.results track by doc.id" ng-switch on="doc.type">\n <div ng-switch-when="record">\n <ng-include src="::\'plugins/market/templates/document/item_document_record.html\'"></ng-include>\n </div>\n <div ng-switch-when="comment">\n <ng-include src="::\'plugins/market/templates/document/item_document_comment.html\'"></ng-include>\n </div>\n <div ng-switch-when="profile">\n <ng-include src="::\'plugins/market/templates/document/item_document_profile.html\'"></ng-include>\n </div>\n <div ng-switch-default>\n <ng-include src="::\'plugins/es/templates/document/item_document.html\'"></ng-include>\n </div>\n </ng-repeat>\n\n</ion-list>\n\n<ion-infinite-scroll ng-if="!search.loading && search.hasMore" spinner="android" on-infinite="showMore()" distance="1%">\n</ion-infinite-scroll>\n');
$templateCache.put('plugins/market/templates/gallery/modal_slideshow.html','<ion-modal-view class="modal modal-pictures" on-swipe-down="closeModal()">\n\n <ion-header-bar class="transparent">\n <!-- start/stop buttons -->\n <a class="button button-icon button-small-padding pull-left icon ion-play hidden-xs hidden-sm ink" ng-class="{\'light\': !interval, \'gray\': interval}" title="{{\'MARKET.GALLERY.BTN_START\'|translate}}" ng-click="start()">\n </a>\n <a class="button button-icon button-small-padding pull-left icon ion-pause hidden-xs hidden-sm ink" ng-class="{\'light\': interval, \'gray\': !interval}" title="{{\'MARKET.GALLERY.BTN_PAUSE\'|translate}}" ng-click="stop()">\n </a>\n\n <h1 class="title balanced" ng-bind-html="activeCategory.name"></h1>\n\n <a class="button button-icon pbutton-small-padding pull-right light hidden-xs hidden-sm ink" ng-click="closeModal()">\n <i class="icon ion-close"></i>\n </a>\n </ion-header-bar>\n\n <ion-slide-box on-slide-changed="slideChanged(index)" active-slide="activeSlide" class="has-header">\n <ion-slide ng-repeat="record in activeCategory.pictures">\n\n\n <div class="image" ng-style="::{\'background-image\': record.src ? (\'url(\'+record.src+\')\') : \'\' }">\n <div class="item no-border item-text-wrap dark padding-left">\n <h1 class="item-text-wrap" ng-class="{\'col-75\': record.price && record.type=\'offer\' || record.type=\'need\'}">\n {{record.title}}\n </h1>\n <h3 ng-if="::record.city && record.stock\'">\n <i class="ion-location"></i> {{options.location.prefix|translate}} {{record.city}}\n </h3>\n\n <div class="badge badge-balanced badge-price" ng-if="::record.price && record.type=\'offer\'" ng-class="{\'sold\': !record.stock}">\n {{record.price|formatAmount:record }}\n </div>\n <div class="badge badge-energized badge-price" ng-if="record.type==\'need\'">\n <i class="cion-market-need"></i>\n <span translate>MARKET.TYPE.NEED_SHORT</span>\n </div>\n <span class="badge badge-assertive badge-secondary" ng-if="::!record.stock" translate>\n MARKET.COMMON.SOLD\n </span>\n </div>\n\n <div class="card card-description light" ng-if="::record.src && record.description && true">\n <div class="item item-text-wrap">\n <i class="ion-quote"></i>\n <span ng-bind-html="record.description"></span>\n </div>\n </div>\n </div>\n\n\n\n </ion-slide>\n </ion-slide-box>\n</ion-modal-view>');
$templateCache.put('plugins/market/templates/gallery/view_gallery.html','<ion-view left-buttons="leftButtons" class="market view-record">\n <ion-nav-title>\n <span translate>MARKET.GALLERY.TITLE</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <!--<button class="button button-bar button-icon button-clear visible-xs visible-sm" ng-click="edit()" ng-if="canEdit">\n <i class="icon ion-android-create"></i>\n </button>\n <button class="button button-bar button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm"\n ng-click="showActionsPopover($event)">\n </button>-->\n </ion-nav-buttons>\n\n <ion-content class="has-header">\n\n <div class="row responsive-sm">\n <div class="col col-20 list">\n <button class="item button button-block button-raised icon icon-left ion-play ink" title="{{!activeCategory ? \'MARKET.GALLERY.BTN_START\' : \'MARKET.GALLERY.BTN_CONTINUE\' | translate}}" ng-click="startSlideShow()">\n {{!activeCategory ? \'MARKET.GALLERY.BTN_START\' : \'MARKET.GALLERY.BTN_CONTINUE\' | translate}}\n </button>\n\n <button class="item button button-block button-raised icon icon-left ion-stop ink" title="{{\'MARKET.GALLERY.BTN_STOP\' | translate}}" ng-disabled="!activeCategory" ng-click="resetSlideShow()">\n {{\'MARKET.GALLERY.BTN_STOP\' | translate}}\n </button>\n </div>\n\n <div class="col list">\n <div class="item item-input item-select no-border">\n <div class="input-label">\n {{\'MARKET.GALLERY.SLIDE_DURATION\' | translate}}\n </div>\n <select ng-model="options.slideDuration" ng-options="i as (slideDurationLabels[i].labelKey | translate:slideDurationLabels[i].labelParams ) for i in slideDurations track by i">\n </select>\n </div>\n <div class="item item-toggle dark">\n <div class="input-label">\n {{\'MARKET.SEARCH.SHOW_CLOSED_RECORD\' | translate}}\n </div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="options.showClosed">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n </div>\n </div>\n\n <div class="list">\n <ng-repeat ng-repeat="cat in categories | filter:isLoadedCategory track by cat.id" ng-init="catIndex=$index">\n <div class="item item-divider">\n\n <span ng-bind-html="cat.name"></span> ({{cat.count}})\n </div>\n\n <a class="item item-list-detail">\n <ion-scroll direction="x">\n <img ng-repeat="picture in cat.pictures track by picture.id" ng-src="{{::picture.src}}" title="{{::picture.title}}{{::picture.price ? (\' (\' + (picture.price | formatAmount:picture) +\')\') : \'\' }}" ng-click="showPicturesModal(catIndex, $index, true)" class="image-list-thumb">\n </ion-scroll>\n </a>\n </ng-repeat>\n </div>\n </ion-content>\n</ion-view>');
$templateCache.put('plugins/market/templates/help/help.html','\n <a name="join"></a>\n <h2 translate>HELP.JOIN.SECTION</h2>\n\n <a name="join-salt"></a>\n <div class="row responsive-sm">\n <div class="col col-20 gray" translate>LOGIN.SALT</div>\n <div class="col" translate>HELP.JOIN.SALT</div>\n </div>\n\n <a name="join-password"></a>\n <div class="row responsive-sm">\n <div class="col col-20 gray" translate>LOGIN.PASSWORD</div>\n <div class="col" translate>HELP.JOIN.PASSWORD</div>\n </div>\n\n <a name="glossary"></a>\n <h2 translate>HELP.GLOSSARY.SECTION</h2>\n\n <a name="pubkey"></a>\n <div class="row responsive-sm">\n <div class="col col-20 gray" translate>COMMON.PUBKEY</div>\n <div class="col" translate>HELP.GLOSSARY.PUBKEY_DEF</div>\n </div>\n\n <a name="universal_dividend"></a>\n <div class="row responsive-sm">\n <div class="col col-20 gray" translate>COMMON.UNIVERSAL_DIVIDEND</div>\n <div class="col" translate>HELP.GLOSSARY.UNIVERSAL_DIVIDEND_DEF</div>\n </div>\n\n\n');
$templateCache.put('plugins/market/templates/help/modal_help.html','<ion-view class="modal slide-in-up ng-enter active ng-enter-active">\n\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CLOSE\n </button>\n\n <h1 class="title" translate>HELP.TITLE</h1>\n </ion-header-bar>\n\n <ion-content scroll="true" class="padding">\n\n <ng-include src="\'templates/help/help.html\'"></ng-include>\n\n <div class="padding hidden-xs text-center">\n <button class="button button-positive ink" type="submit" ng-click="closeModal()">\n {{\'COMMON.BTN_CLOSE\' | translate}}\n </button>\n </div>\n\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/market/templates/help/view_help.html','<ion-view left-buttons="leftButtons">\n <ion-nav-title>\n <span class="visible-xs visible-sm" translate>HELP.TITLE</span>\n </ion-nav-title>\n\n <ion-content scroll="true" class="padding">\n\n <h1 class="hidden-xs hidden-sm" translate>HELP.TITLE</h1>\n\n <ng-include src="\'templates/help/help.html\'"></ng-include>\n\n </ion-content>\n</ion-view>\n');
$templateCache.put('plugins/market/templates/home/home_extend.html','\n<ng-if ng-if="enable">\n\n <h2 ng-if="$root.config.plugins.market.homeMessage" ng-bind-html="$root.config.plugins.market.homeMessage"></h2>\n\n <div class="row no-padding center" ng-controller="ESSearchPositionItemCtrl">\n\n <div class="item item-divider text-left no-padding-left">\n <span translate>MARKET.HOME.LOCATION_LABEL</span>\n </div>\n\n <!-- location -->\n <div class="item no-padding item-icon-right" style="background-color: white">\n <div class="item-input item-input-search">\n <input type="text" class="visible-xs visible-sm" placeholder="{{(options.location.help||\'MARKET.HOME.LOCATION_HELP\')|translate}}" ng-model="search.location" ng-keydown="onKeydown($event)" ng-change="onLocationChanged()" ng-blur="hideDropdown()" ng-model-options="{ debounce: 650 }">\n <input type="text" id="searchLocationInput" class="hidden-xs hidden-sm" placeholder="{{(options.location.help||\'MARKET.HOME.LOCATION_HELP\')|translate}}" ng-model="search.location" ng-keydown="onKeydown($event)" ng-change="onLocationChanged()" ng-blur="hideDropdown()" ng-model-options="{ debounce: 350 }" on-return="doSearch()">\n </div>\n\n <a class="icon ion-search ink-dark" ng-click="doSearch()" title="{{\'MARKET.HOME.BTN_SHOW_MARKET_OFFER\'|translate}}"></a>\n </div>\n\n <!-- dropdown -->\n <ng-include src="\'plugins/es/templates/common/dropdown_locations.html\'"></ng-include>\n\n </div>\n\n <br class="hidden-xs hidden-sm">\n <br class="hidden-xs hidden-sm">\n <br>\n\n <button type="button" class="button button-block button-balanced button-raised icon icon-left ion-compose ink-dark" ng-click="showNewRecordModal()" translate>MARKET.HOME.BTN_NEW_AD</button>\n\n\n <br class="hidden-xs">\n\n</ng-if>\n\n');
$templateCache.put('plugins/market/templates/join/modal_join.html','<ion-modal-view class="modal-full-height">\n\n <ion-header-bar class="bar-positive">\n\n <button class="button button-clear visible-xs" ng-if="!slides.slider.activeIndex" ng-click="closeModal()" translate>COMMON.BTN_CANCEL\n </button>\n <button class="button button-icon button-clear icon ion-ios-arrow-back buttons header-item" ng-click="slidePrev()" ng-if="slides.slider.activeIndex">\n </button>\n <button class="button button-icon button-clear icon ion-ios-help-outline visible-xs" ng-if="!isLastSlide" ng-click="showHelpModal()"></button>\n\n <h1 class="title" translate>ACCOUNT.NEW.TITLE</h1>\n\n <button class="button button-clear icon-right visible-xs" ng-if="!isLastSlide" ng-click="doNext()">\n <span translate>COMMON.BTN_NEXT</span>\n <i class="icon ion-ios-arrow-right"></i>\n </button>\n <button class="button button-clear icon-right visible-xs" ng-if="isLastSlide" ng-click="doNewAccount()">\n <i class="icon ion-android-send"></i>\n </button>\n </ion-header-bar>\n\n\n <ion-slides options="slides.options" slider="slides.slider">\n\n <!-- STEP: salt -->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n <form name="saltForm" novalidate="" ng-submit="doNext(\'saltForm\')">\n\n <div class="list" ng-init="setForm(saltForm, \'saltForm\')">\n\n <div class="item item-text-wrap text-center padding hidden-xs">\n <a class="pull-right icon-help" ng-click="showHelpModal(\'join-salt\')"></a>\n <span translate>ACCOUNT.NEW.SALT_WARNING</span>\n </div>\n\n <!-- salt -->\n <div class="item item-input" ng-class="{ \'item-input-error\': saltForm.$submitted && saltForm.username.$invalid}">\n <span class="input-label" translate>LOGIN.SALT</span>\n <input name="username" type="text" placeholder="{{\'LOGIN.SALT_HELP\' | translate}}" autocomplete="off" ng-change="formDataChanged()" ng-model="formData.username" ng-minlength="8" required>\n </div>\n <div class="form-errors" ng-show="saltForm.$submitted && saltForm.username.$error" ng-messages="saltForm.username.$error">\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 8}"></span>\n </div>\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- confirm salt -->\n <div class="item item-input" ng-class="{ \'item-input-error\': saltForm.$submitted && saltForm.confirmSalt.$invalid}">\n <span class="input-label pull-right" translate>ACCOUNT.NEW.SALT_CONFIRM</span>\n <input name="confirmUsername" type="text" autocomplete="off" placeholder="{{\'ACCOUNT.NEW.SALT_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmUsername" compare-to="formData.username">\n </div>\n <div class="form-errors" ng-show="saltForm.$submitted && saltForm.confirmUsername.$error" ng-messages="saltForm.confirmUsername.$error">\n <div class="form-error" ng-message="compareTo">\n <span translate="ERROR.SALT_NOT_CONFIRMED"></span>\n </div>\n </div>\n\n <div class="padding hidden-xs text-right">\n <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL\n </button>\n <button class="button button-calm icon-right ion-chevron-right ink" type="submit" translate>\n COMMON.BTN_NEXT\n <i class="icon ion-arrow-right-a"></i>\n </button>\n </div>\n </div>\n </form>\n </ion-content>\n </ion-slide-page>\n\n <!-- STEP: password-
$templateCache.put('plugins/market/templates/login/modal_event_login.html','<ion-modal-view class="modal-full-height">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear visible-xs" ng-click="closeModal()" translate>COMMON.BTN_CANCEL\n </button>\n <h1 class="title" translate>MARKET.EVENT_LOGIN.TITLE</h1>\n <div class="buttons buttons-right">\n <span class="secondary-buttons">\n <button class="button button-positive button-icon button-clear icon ion-android-done visible-xs" ng-click="doLogin()">\n </button>\n </span></div>\n\n </ion-header-bar>\n\n <ion-content>\n <form name="loginForm" novalidate="" ng-submit="doLogin()">\n\n\n <div class="list" ng-init="setForm(loginForm)">\n\n <div class="item item-text-wrap" ng-bind-html="\'MARKET.EVENT_LOGIN.HELP\' | translate">\n </div>\n\n <!-- salt (=username, to enable browser login cache) -->\n <label class="item item-input" ng-class="{ \'item-input-error\': form.$submitted && form.username.$invalid}">\n <span class="input-label hidden-xs" translate>MARKET.EVENT_LOGIN.EMAIL_OR_PHONE</span>\n <input name="username" type="text" placeholder="{{\'MARKET.EVENT_LOGIN.EMAIL_OR_PHONE_HELP\' | translate}}" ng-model="formData.username" ng-model-options="{ debounce: 650 }" class="highlight-light" ng-pattern="usernamePattern" required>\n </label>\n <div class="form-errors" ng-show="form.$submitted && form.username.$error" ng-messages="form.username.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n <div class="form-error" ng-message="pattern">\n <span translate="MARKET.EVENT_LOGIN.ERROR.INVALID_USERNAME"></span>\n </div>\n </div>\n\n <!-- password\n <label class="item item-input"\n ng-class="{ \'item-input-error\': form.$submitted && form.password.$invalid}">\n <span class="input-label hidden-xs" translate>LOGIN.PASSWORD</span>\n <input name="password" type="password" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}"\n ng-model="formData.password"\n ng-model-options="{ debounce: 650 }"\n select-on-click\n required>\n </label>\n <div class="form-errors"\n ng-show="form.$submitted && form.password.$error"\n ng-messages="form.password.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>-->\n\n\n <!-- remember me -->\n <ion-checkbox ng-model="formData.rememberMe" class="item item-border-large ink">\n <div class="item-content" translate>MARKET.EVENT_LOGIN.REMEMBER_ME</div>\n </ion-checkbox>\n\n <!-- Show public key\n <div class="item item-button-right left">\n <span ng-if="formData.username && formData.password"\n class="input-label" translate>COMMON.PUBKEY</span>\n <a class="button button-light button-small ink animate-if"\n ng-click="showPubkey()"\n ng-if="showPubkeyButton"\n >\n {{\'COMMON.BTN_SHOW_PUBKEY\' | translate}}\n </a>\n <h3 class="gray text-no-wrap" ng-if="!computing">\n {{pubkey}}\n </h3>\n <h3 ng-if="computing">\n <ion-spinner icon="android"></ion-spinner>\n </h3>\n </div>-->\n\n </div>\n\n <div class="padding hidden-xs text-right">\n <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL\n </button>\n <button class="button button-positive ink" type="submit">\n {{\'COMMON.BTN_LOGIN\' | translate}}\n </button>\n </div>\n\n <!-- Register ?\n <div class="text-center no-padding">\n {{\'LOGIN.NO_ACCOUNT_QUESTION\'|translate}}\n <br class="vis
$templateCache.put('plugins/market/templates/login/modal_login.html','<ion-modal-view class="modal-full-height modal-login">\n <ion-header-bar class="bar-positive">\n <button class="button button-clear visible-xs" ng-click="closeModal()" translate>COMMON.BTN_CANCEL\n </button>\n <h1 class="title" ng-bind-html="\'LOGIN.TITLE\' | translate">\n </h1>\n <div class="buttons buttons-right">\n <span class="secondary-buttons visible-xs">\n <button class="button button-positive button-icon button-clear icon ion-android-done" style="color: #fff" ng-click="doLogin()">\n </button>\n </span></div>\n\n </ion-header-bar>\n\n <ion-content>\n <form name="loginForm" novalidate="" ng-submit="doLogin()">\n\n\n <div class="list" ng-init="setForm(loginForm)">\n\n <div class="item item-text-wrap" ng-bind-html="\'MARKET.LOGIN.HELP\' | translate">\n </div>\n\n <!-- salt (=username, to enable browser login cache) -->\n <label class="item item-input" ng-class="{ \'item-input-error\': form.$submitted && form.username.$invalid}">\n <span class="input-label hidden-xs" translate>LOGIN.SALT</span>\n <input name="username" type="text" placeholder="{{\'LOGIN.SALT_HELP\' | translate}}" ng-model="formData.username" autocomplete="off" ng-model-options="{ debounce: 650 }" class="highlight-light" required>\n </label>\n <div class="form-errors" ng-show="form.$submitted && form.username.$error" ng-messages="form.username.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- password -->\n <label class="item item-input" ng-class="{ \'item-input-error\': form.$submitted && form.password.$invalid}">\n <span class="input-label hidden-xs" translate>LOGIN.PASSWORD</span>\n <input name="password" type="password" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}" ng-model="formData.password" ng-model-options="{ debounce: 650 }" select-on-click required>\n </label>\n <div class="form-errors" ng-show="form.$submitted && form.password.$error" ng-messages="form.password.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n\n <!-- remember me -->\n <ion-checkbox ng-model="formData.rememberMe" class="item item-border-large ink">\n <div class="item-content" translate>MARKET.LOGIN.REMEMBER_ME</div>\n </ion-checkbox>\n\n <!-- Show public key\n <div class="item item-button-right left">\n <span ng-if="formData.username && formData.password"\n class="input-label" translate>COMMON.PUBKEY</span>\n <a class="button button-light button-small ink animate-if"\n ng-click="showPubkey()"\n ng-if="showPubkeyButton"\n >\n {{\'COMMON.BTN_SHOW_PUBKEY\' | translate}}\n </a>\n <h3 class="gray text-no-wrap" ng-if="!computing">\n {{pubkey}}\n </h3>\n <h3 ng-if="computing">\n <ion-spinner icon="android"></ion-spinner>\n </h3>\n </div>-->\n\n </div>\n\n <div class="padding hidden-xs text-right">\n <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate>COMMON.BTN_CANCEL\n </button>\n <button class="button button-positive ink" type="submit">\n {{\'COMMON.BTN_LOGIN\' | translate}}\n </button>\n </div>\n\n <!-- Register ? -->\n <div class="text-center no-padding">\n {{\'LOGIN.NO_ACCOUNT_QUESTION\'|translate}}\n <br class="visible-xs">\n <a ng-click="showJoinModal()" translate>\n LOGIN.CREATE_ACCOUNT\n </a>\n </div>\n\n <!--<div class="text-center no-padding">\n <a ng-click="showAccountSecurityModal()" translate>\n LOGIN.FORGOTTEN_ID\n </a>\n </div
$templateCache.put('plugins/market/templates/record/edit_record.html','<ion-view left-buttons="leftButtons" id="editMarket">\n <ion-nav-title>\n <span class="visible-xs" ng-if="id" ng-bind-html="formData.title"></span>\n <span class="visible-xs" ng-if="!loading && !id" translate>MARKET.EDIT.TITLE_NEW</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-clear button-icon icon visible-xs visible-sm" ng-class="{\'ion-android-send\':!id, \'ion-android-done\': id}" ng-click="save()">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true">\n\n <div class="row no-padding">\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n <div class="col">\n\n <form name="recordForm" novalidate="" ng-submit="save()">\n <div class="list {{::motion.ionListClass}}" ng-init="setForm(recordForm)">\n\n <div class="item hidden-xs item-text-wrap">\n <h1 ng-if="id" ng-bind-html="formData.title"></h1>\n <h1 ng-if="!id" translate>MARKET.EDIT.TITLE_NEW</h1>\n\n </div>\n <div class="item" ng-if="id||options.type.show">\n <h4 class="gray" ng-if="id">\n <i class="icon ion-calendar"></i>\n {{\'COMMON.LAST_MODIFICATION_DATE\'|translate}}&nbsp;{{formData.time | formatDate}}\n </h4>\n <div class="badge badge-balanced" ng-if="options.type.show" ng-class="{\'badge-editable\': options.type.canEdit}" ng-click="options.type.canEdit ? showRecordTypeModal() : \'\'">\n <span>{{\'MARKET.TYPE.\'+formData.type|upper|translate}}</span>\n </div>\n </div>\n\n <!-- pictures -->\n <ng-include src="\'plugins/es/templates/common/edit_pictures.html\'"></ng-include>\n\n <!-- category -->\n <a class="item item-icon-right ink item-border" ng-if="options.category.show" ng-class="{\'item-input-error\': form.$submitted && !formData.category.id}" ng-click="showCategoryModal()">\n <span class="item-label" translate>COMMON.CATEGORY</span>\n <span ng-if="!formData.category.id" class="item-note">{{::\'COMMON.CATEGORY_SELECT_HELP\'|translate}}</span>\n <span class="badge badge-royal" ng-bind-html="formData.category.name"></span>&nbsp;\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n <div class="form-errors" ng-show="form.$submitted && !formData.category.id">\n <div class="form-error">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- title -->\n <div class="item item-input item-floating-label" ng-class="{\'item-input-error\': form.$submitted && form.title.$invalid}">\n <span class="input-label" translate>MARKET.EDIT.RECORD_TITLE</span>\n <input type="text" placeholder="{{\'MARKET.EDIT.RECORD_TITLE_HELP\'|translate}}" name="title" id="market-record-title" ng-model="formData.title" ng-minlength="3" required>\n </div>\n <div class="form-errors" ng-show="form.$submitted && form.title.$invalid" ng-messages="form.title.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT"></span>\n </div>\n </div>\n\n <div class="item item-input item-floating-label" ng-if="options.description.show">\n <span class="input-label" translate>MARKET.EDIT.RECORD_DESCRIPTION</span>\n <textarea placeholder="{{\'MARKET.EDIT.RECORD_DESCRIPTION_HELP\'|translate}}" ng-model="formData.description" rows="8" cols="10"></textarea>\n </div>\n\n <!-- price -->\n
$templateCache.put('plugins/market/templates/record/modal_record_type.html','<ion-modal-view>\n <ion-header-bar class="bar-positive">\n <button class="button button-clear" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button>\n <h1 class="title" translate>MARKET.TYPE.TITLE</h1>\n </ion-header-bar>\n\n <ion-content class="lookupForm">\n \t<div class="list padding">\n <h3 translate>MARKET.TYPE.SELECT_TYPE</h3>\n <button class="button button-block button-stable icon icon-left cion-market-offer" ng-click="closeModal(\'offer\')" translate>MARKET.TYPE.OFFER</button>\n <button class="button button-block button-stable icon icon-left cion-market-need" ng-click="closeModal(\'need\')" translate>MARKET.TYPE.NEED</button>\n </div>\n</ion-content>\n</ion-modal-view>\n');
$templateCache.put('plugins/market/templates/record/popover_unit.html','<ion-popover-view class="popover-unit">\n <ion-content scroll="false">\n <div class="list">\n <a class="item item-icon-left" ng-class="{ \'selected\': !useRelative}" ng-click="setUseRelative(false)">\n <i class="icon" ng-class="{ \'ion-ios-checkmark-empty\': !useRelative}"></i>\n <i ng-bind-html="formData.currency | currencySymbol:false"></i>\n </a>\n <a class="item item-icon-left" ng-class="{ \'selected\': $parent.useRelative}" ng-click="setUseRelative(true)">\n <i class="icon" ng-class="{ \'ion-ios-checkmark-empty\': useRelative}"></i>\n <i ng-bind-html="formData.currency | currencySymbol:true"></i>\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/market/templates/record/view_popover_actions.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>MARKET.VIEW.MENU_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <a class="item item-icon-left ink visible-xs visible-sm" ng-click="showSharePopover($event)">\n <i class="icon ion-android-share-alt"></i>\n {{\'COMMON.BTN_SHARE\' | translate}}\n </a>\n\n <!--<a class="item item-icon-left assertive ink "\n ng-if="canEdit"\n ng-click="delete()">\n <i class="icon ion-trash-a"></i>\n {{\'COMMON.BTN_DELETE\' | translate}}\n </a>-->\n\n <a class="item item-icon-left ink visible-xs visible-sm" ng-if="canSold" ng-click="sold()">\n <i class="icon ion-minus-circled"></i>\n {{\'MARKET.VIEW.BTN_SOLD_AD\' | translate}}\n </a>\n\n <a class="item item-icon-left ink visible-xs visible-sm" ng-if="canReopen" ng-click="reopen()">\n <i class="icon ion-unlocked"></i>\n {{\'MARKET.VIEW.BTN_REOPEN\' | translate}}\n </a>\n\n <!-- Write to vendor -->\n <a class="item item-icon-left ink visible-xs visible-sm" ng-if="!canEdit" ng-click="showNewMessageModal()">\n <i class="icon ion-compose"></i>\n {{\'MARKET.VIEW.BTN_WRITE_\'+formData.type |upper|translate}}\n </a>\n\n <!-- Follow -->\n <a class="item item-icon-left ink" ng-if="!canEdit" ng-click="hideActionsPopover() && toggleLike($event, {kind: \'follow\'})">\n <i class="icon" ng-class="{\'ion-android-notifications-off\': likeData.follows.wasHit, \'ion-android-notifications\': !likeData.follows.wasHit}"></i>\n <b class="ion-plus icon-secondary" ng-if="!likeData.follows.wasHit" style="font-size: 16px; left: 38px; top: -7px"></b>\n {{(!likeData.follows.wasHit ? \'MARKET.VIEW.BTN_FOLLOW\' : \'MARKET.VIEW.BTN_STOP_FOLLOW\' )| translate}}\n </a>\n\n <!-- report abuse -->\n <a class="item item-icon-left ink" ng-if="!canEdit && !likeData.abuses.wasHit" ng-click="hideActionsPopover() && reportAbuse($event)">\n <i class="icon ion-android-warning"></i>\n<!-- <b class="ion-plus icon-secondary" style="font-size: 16px; left: 38px; top: -7px;"></b>-->\n {{\'COMMON.BTN_REPORT_ABUSE_DOTS\' | translate}}\n </a>\n <a class="item item-icon-left ink" ng-if="!canEdit && likeData.abuses.wasHit" ng-click="hideActionsPopover() && toggleLike($event, {kind: \'abuse\'})">\n <i class="icon ion-android-warning"></i>\n <b class="ion-close icon-secondary" style="font-size: 16px; left: 38px; top: -7px"></b>\n {{\'COMMON.BTN_REMOVE_REPORTED_ABUSE\' | translate}}\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/market/templates/record/view_record.html','<ion-view left-buttons="leftButtons" class="market view-record">\n <ion-nav-title>\n <span translate>MARKET.VIEW.TITLE</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-bar button-icon button-clear visible-xs visible-sm" ng-click="edit()" ng-if="canEdit">\n <i class="icon ion-android-create"></i>\n </button>\n <button class="button button-bar button-icon button-clear icon ion-android-more-vertical visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n </ion-nav-buttons>\n\n <ion-content class="grid">\n\n <div class="row no-padding">\n <div class="col col-15 hidden-xs hidden-sm hidden-md">&nbsp;</div>\n\n <div class="col col-main no-padding">\n\n <div class="center padding" ng-if="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="list {{::motion.ionListClass}} item-text-wrap no-padding-xs">\n\n <!-- desktop : title and location -->\n <div class="item item-text-wrap hidden-xs hidden-sm" ng-if="!smallscreen">\n\n <!-- title -->\n <h1 ng-bind-html="formData.title"></h1>\n\n <!-- location and category-->\n <h2 class="gray">\n <a class="positive" ng-if="formData.city" ui-sref="app.market_lookup({location:formData.city, lat: formData.geoPoint && formData.geoPoint.lat, lon:formData.geoPoint && formData.geoPoint.lon})">\n <i class="icon ion-location"></i>\n {{::options.location.prefix|translate}}<span ng-bind-html="::formData.city"></span>\n </a>\n <span ng-if="formData.city && formData.category.name">&nbsp;|&nbsp;</span>\n <a ng-if="formData.category.name" ui-sref="app.market_lookup({category:formData.category.id})">\n <i class="icon ion-flag"></i>\n <span ng-bind-html="::formData.category.name"></span>\n </a>\n </h2>\n <h4>\n <i class="icon ion-clock"></i>\n <span translate>COMMON.SUBMIT_BY</span>\n <a ui-sref="app.wot_identity({pubkey:issuer.pubkey, uid: issuer.uid})">\n <span ng-if="issuer.name||issuer.uid" class="positive">\n <i class="icon ion-person"></i>\n {{::issuer.name||issuer.uid}}\n </span>\n <span ng-if="!issuer.name && !issuer.uid" class="gray">\n <i class="icon ion-key"></i>\n {{::formData.issuer|formatPubkey}}\n </span>\n </a>\n <span>\n {{formData.creationTime|formatFromNow}}\n <span class="gray hidden-xs">|\n {{formData.creationTime | formatDate}}\n </span>\n </span>\n </h4>\n <h4 class="dark" ng-if="formData.time - formData.creationTime > 86400">\n <i class="icon ion-edit"></i>\n <span translate>MARKET.COMMON.LAST_UPDATE</span>\n <span>{{::formData.time | formatDate}}</span>\n </h4>\n <!-- likes -->\n <h4>\n <ng-include src="\'plugins/es/templates/common/view_likes.html\'"></ng-include>\n </h4>\n <div class="badge" ng-if="options.type.show" ng-class="{\'badge-energized\': formData.type == \'need\', \'badge-calm\': formData.type == \'offer\'}">\n <i class="cion-market-{{formData.type}}"></i>\n <span>{{\'MARKET.TYPE.\'+formData.type|upper|translate}}</span>\n </div>\n </div>\n\n <!-- mobile: title and location -->\n <div class="item item-text-wrap" ng-if="smallscreen">\n\n <!-- title -->\n <h1 ng-bind-html="formData.title"></h1>\n\n\n <h2 class="gray">\n <a class="positive" ng-if="formData.city" ui-sref="app.market_lookup({location:formData.location})">\n <i clas
$templateCache.put('plugins/market/templates/search/item_record.html','\n<div class="item no-padding">\n\n <a class="card card-record stable-bg ink" ng-click="showRecord($event, $index)">\n\n <div class="card-item item-text-wrap padding-right" ng-class="::{\'item-thumbnail-left\': rec.thumbnail, \'padding-left\': !rec.thumbnail}">\n <i class="item-image" ng-if="::rec.thumbnail" style="background-image: url(\'{{::rec.thumbnail.src}}\')"></i>\n <h2 class="padding-top" ng-bind-html="::rec.title | truncText:100"></h2>\n <h4 class="gray">\n <span class="positive" ng-if="::rec.location">\n <i class="icon ion-location"></i> {{::options.location.prefix|translate}}<span ng-bind-html="rec.location"></span>\n </span>\n <span ng-show="rec.time">\n <br ng-show="rec.location">\n <i class="icon ion-clock"></i> {{::rec.time | formatFromNow}}\n {{::\'MARKET.SEARCH.BY\'|translate}}\n <span class="dark">{{::rec.name || (rec.pubkey|formatPubkey)}}</span>\n </span>\n <span ng-if="rec.stock>1"><i class="icon ion-pie-graph"></i> {{::rec.stock}}</span>\n </h4>\n <div ng-if="rec.picturesCount > 1" class="badge badge-balanced badge-picture-count">{{::rec.picturesCount}}&nbsp;<i class="icon ion-camera"></i>\n </div>\n <div ng-if="rec.stock===0" class="badge badge-assertive">\n <small><i class="ion-close"></i>\n <span translate>MARKET.COMMON.SOLD</span></small>\n </div>\n\n </div>\n <div class="card-footer" style="height: 45px">\n <div class="badge badge-price badge-calm" ng-if="rec.type===\'offer\' && rec.price">\n <span ng-bind-html=":rebind:rec.price|formatAmount:{currency: rec.currency, useRelative: $root.settings.useRelative}"></span>\n </div>\n <div class="badge badge-calm" ng-if="rec.type==\'offer\' && !rec.price && options.type.show">\n <i class="cion-market-offer"></i>\n <span translate>MARKET.TYPE.OFFER_SHORT</span>\n </div>\n <div class="badge badge-energized" ng-if="rec.type==\'need\' && options.type.show">\n <i class="cion-market-need"></i>\n <span translate>MARKET.TYPE.NEED_SHORT</span>\n </div>\n </div>\n </a>\n\n</div>\n');
$templateCache.put('plugins/market/templates/search/list_records.html',' <!-- result label -->\n <div class="padding" style="display: block; height: 60px">\n <div class="pull-left ng-hide" ng-show="!search.loading">\n <ng-if ng-if="search.lastRecords">\n <h4 translate>MARKET.SEARCH.LAST_RECORDS</h4>\n <small class="gray no-padding" ng-if="search.total">\n <span ng-if="search.geoPoint && search.total">{{\'MARKET.SEARCH.LAST_RECORD_COUNT_LOCATION\'|translate:{count: search.total, location: search.location} }}</span>\n <span ng-if="!search.geoPoint && search.total">{{\'MARKET.SEARCH.LAST_RECORD_COUNT\'|translate:{count: search.total} }}</span>\n </small>\n </ng-if>\n\n <ng-if ng-if="!search.lastRecords">\n <h4 translate>MARKET.SEARCH.RESULTS</h4>\n <small class="gray no-padding" ng-if="search.total">\n <span ng-if="search.geoPoint && search.total">{{\'MARKET.SEARCH.RESULT_COUNT_LOCATION\'|translate:{count: search.total, location: search.location} }}</span>\n <span ng-if="!search.geoPoint && search.total">{{\'MARKET.SEARCH.RESULT_COUNT\'|translate:{count: search.total} }}</span>\n </small>\n </ng-if>\n\n </div>\n </div>\n\n <div class="center" ng-if="search.loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n\n <div class="padding assertive" ng-if="!search.loading && search.results.length===0 && search.advanced != null" translate>\n COMMON.SEARCH_NO_RESULT\n </div>\n\n <div class="list {{::motion.ionListClass}} no-padding" ng-if="!search.loading && search.results.length">\n\n <ng-include ng-repeat="rec in search.results" src="\'plugins/market/templates/search/item_record.html\'">\n </ng-include>\n </div>\n\n <ion-infinite-scroll ng-if="!search.loading && search.hasMore" spinner="android" on-infinite="showMore()" distance="1%">\n </ion-infinite-scroll>\n');
$templateCache.put('plugins/market/templates/search/list_records_lg.html','\n<div class="padding-xs" style="display: block; height: 60px">\n <div class="pull-left ng-hide" ng-show="!search.loading">\n <ng-if ng-if="search.lastRecords">\n <h4 translate>MARKET.SEARCH.LAST_RECORDS</h4>\n <small class="gray no-padding" ng-if="search.total">\n <span ng-if="search.geoPoint && search.total">{{\'MARKET.SEARCH.LAST_RECORD_COUNT_LOCATION\'|translate:{count: search.total, location: search.location} }}</span>\n <span ng-if="!search.geoPoint && search.total">{{\'MARKET.SEARCH.LAST_RECORD_COUNT\'|translate:{count: search.total} }}</span>\n </small>\n </ng-if>\n\n <ng-if ng-if="!search.lastRecords">\n <h4 translate>MARKET.SEARCH.RESULTS</h4>\n <small class="gray no-padding" ng-if="search.total">\n <span ng-if="search.geoPoint && search.total">{{\'MARKET.SEARCH.RESULT_COUNT_LOCATION\'|translate:{count: search.total, location: search.location} }}</span>\n <span ng-if="!search.geoPoint && search.total">{{\'MARKET.SEARCH.RESULT_COUNT\'|translate:{count: search.total} }}</span>\n </small>\n </ng-if>\n\n </div>\n</div>\n\n<div class="center" ng-if="search.loading">\n <ion-spinner icon="android"></ion-spinner>\n</div>\n\n<div class="padding assertive" ng-if="!search.loading && search.results.length===0 && search.advanced != null" translate>\n COMMON.SEARCH_NO_RESULT\n</div>\n\n<div class="list {{::motion.ionListClass}} light-bg" ng-if="!search.loading && search.results.length">\n\n <a ng-repeat="rec in search.results track by rec.id" class="item item-record item-border-large ink padding-xs" ui-sref="app.market_view_record({id: rec.id, title: rec.urlTitle})">\n\n <div class="row row-record">\n <div class="col item-text-wrap item-thumbnail-left">\n <i ng-if="::rec.thumbnail" class="item-image" style="background-image: url({{::rec.thumbnail.src}})"></i>\n <i class="item-image ion-speakerphone" ng-if="::!rec.thumbnail"></i>\n <h2 ng-bind-html="rec.title"></h2>\n <h4 class="positive" ng-if="rec.city">\n <i class="icon ion-location"></i>\n {{::options.location.prefix|translate}}<span ng-bind-html="::rec.city"></span>\n <span class="gray" ng-if="::rec.distance">\n ({{::rec.distance|formatDecimal}} {{::geoUnit}})\n </span>\n </h4>\n <h4 class="gray" ng-if="rec.creationTime">\n <i class="icon ion-clock"></i>\n {{::rec.creationTime | formatFromNow}}\n {{::\'MARKET.SEARCH.BY\'|translate}}\n <span class="dark">{{::rec.name || (rec.pubkey|formatPubkey)}}</span>\n </h4>\n <span ng-if="::rec.picturesCount > 1" class="badge badge-balanced badge-picture-count">{{::rec.picturesCount}}&nbsp;<i class="icon ion-camera"></i></span>\n </div>\n <div class="col col-20" style="max-width: 180px">\n <h3 class="gray" ng-if="::rec.category" ng-bind-html="::rec.category.name"></h3>\n <h5 ng-if="::rec.stock>1" class="gray hidden-xs hidden-sm"><i class="icon ion-pie-graph"></i> <span class="">{{::rec.stock}} <i class="ion-checkmark balanced"></i></span></h5>\n <div class="badge badge-price" ng-if="::rec.price" ng-class="{\'badge-calm\': rec.type==\'offer\', \'badge-energized\': rec.type==\'need\'}">\n <i class="cion-market-{{rec.type}}"></i>\n <span ng-bind-html=":rebind:rec.price|formatAmount:{currency: rec.currency, useRelative: $root.settings.useRelative}"></span>\n </div>\n <div class="badge badge-price" ng-if="::!search.type && !rec.price" ng-class="::{\'badge-calm\': rec.type==\'offer\', \'badge-energized\': rec.type==\'need\'}">\n <i class="cion-market-{{::rec.type}}"></i>\n {{::\'MARKET.TYPE.\'+rec.type+\'_SHORT\'|upper|translate}}\n </div>\n </div>\n <div class="col hidden-sm hidden-xs">\n <h4 class="gray text-wrap text-italic" ng-if="::!!rec.description">
$templateCache.put('plugins/market/templates/search/lookup.html','<ion-view left-buttons="leftButtons" class="market">\n <ion-nav-title>\n <span ng-if="entered && !search.category" translate>MARKET.SEARCH.TITLE</span>\n <span ng-if="search.category" ng-bind-html="search.category.name"></span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="doRefresh()">\n </button>\n <button class="button button-bar button-icon button-clear icon ion-android-funnel visible-xs visible-sm" ng-click="showActionsPopover($event)">\n </button>\n </ion-nav-buttons>\n\n <ion-content class="lookupForm" bind-notifier="{ rebind: $root.settings.useRelative }">\n\n <form ng-submit="doSearch()">\n\n <a ng-if="!search.category && options.category.show" class="item item-icon-right ink" ui-sref="app.market_categories">\n <span class="gray"> <i class="gray ion-android-funnel"></i> {{\'MARKET.SEARCH.BTN_SHOW_CATEGORIES\'|translate}}</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>\n\n <label class="item item-input">\n <i class="icon ion-search placeholder-icon"></i>\n <input type="text" placeholder="{{\'MARKET.SEARCH.SEARCH_HELP\'|translate}}" ng-model="search.text" ng-model-options="{ debounce: 650 }" ng-change="doSearch()">\n </label>\n\n <!-- location -->\n <ng-include src="\'plugins/es/templates/common/item_location_search.html\'" ng-controller="ESSearchPositionItemCtrl" ng-init=""></ng-include>\n\n <!-- options -->\n <ng-include src="::\'plugins/market/templates/search/lookup_options.html\'"></ng-include>\n </form>\n\n \n\n <!-- list of records -->\n <ng-include src="\'plugins/market/templates/search/list_records.html\'"></ng-include>\n\n </ion-content>\n\n <button id="fab-add-market-record" class="button button-fab button-fab-bottom-right button-assertive icon ion-plus spin" ng-click="showNewRecordModal()">\n </button>\n</ion-view>\n');
$templateCache.put('plugins/market/templates/search/lookup_actions_popover.html','<ion-popover-view class="fit has-header">\n <ion-header-bar>\n <h1 class="title" translate>MARKET.VIEW.MENU_TITLE</h1>\n </ion-header-bar>\n <ion-content scroll="false">\n <div class="list item-text-wrap">\n\n <!-- last record -->\n <a class="item item-icon-left ink" ng-click="doGetLastRecords()">\n <i class="icon ion-clock"></i>\n {{\'MARKET.SEARCH.BTN_LAST_RECORDS\' | translate}}\n </a>\n\n <!-- show closed Ads ? -->\n <a class="item item-icon-left ink" ng-click="toggleShowClosed();">\n <i class="icon ion-android-checkbox-outline-blank" ng-show="!search.showClosed"></i>\n <i class="icon ion-android-checkbox-outline" ng-show="search.showClosed"></i>\n <span translate>MARKET.SEARCH.SHOW_CLOSED_RECORD</span>\n </a>\n </div>\n </ion-content>\n</ion-popover-view>\n');
$templateCache.put('plugins/market/templates/search/lookup_lg.html','<ion-view left-buttons="leftButtons" class="market">\n <ion-nav-title>\n <span translate>MARKET.SEARCH.TITLE</span>\n </ion-nav-title>\n\n <ion-content class="lookupForm padding no-padding-xs stable-100-bg" bind-notifier="{ rebind:$root.settings.useRelative }">\n\n <div class="padding-top hidden-xs hidden-sm" style="display: block; height: 60px">\n\n <!-- show categories button -->\n <div class="pull-left">\n <a class="button button-text button-small ink" ng-if="options.category.show" ng-class="{\'button-text-positive\': search.showCategories, \'button-text-dark\': !search.showCategories}" ng-click="search.showCategories=!search.showCategories;">\n <span translate>MARKET.SEARCH.BTN_SHOW_CATEGORIES</span>\n <i class="icon ion-arrow-down-b"></i>\n </a>\n </div>\n\n <!-- new record button -->\n <div class="pull-right">\n <button class="button button-small button-positive button-clear ink padding-right" ng-click="showNewRecordModal()">\n <i class="icon ion-plus"></i>\n {{\'MARKET.COMMON.BTN_NEW_AD\' | translate}}\n </button>\n </div>\n </div>\n\n <!-- categories drop down -->\n <div class="list dropdown-list" ng-mouseleave="search.showCategories=false;" ng-show="search.showCategories" scroll="true" ng-controller="MkListCategoriesCtrl" ng-init="load()">\n <div class="text-center" ng-show="loading">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n <ng-include class="no-border no-padding" ng-show="!loading" src="\'plugins/market/templates/category/list_categories_lg.html\'">\n </ng-include>\n </div>\n\n <form ng-submit="doSearch()">\n\n <!-- search text -->\n <div class="item no-padding light-bg">\n\n <div class="item-input light-bg">\n <div class="animate-show-hide ng-hide" ng-show="entered">\n\n <!-- selected location -->\n <div ng-show="!search.loading && search.geoPoint && search.location" class="button button-small button-text button-stable button-icon-event stable-900-bg" style="margin-right: 10px">\n &nbsp;<i class="icon ion-location"></i>\n {{search.location.split(\',\')[0]}}\n <span ng-if="search.geoDistance">({{\'COMMON.GEO_DISTANCE_OPTION\' | translate: {value: search.geoDistance} }})</span>\n <i class="icon ion-close" ng-click="removeLocation()">&nbsp;&nbsp;</i>\n </div>\n\n <!-- selected category -->\n <div ng-show="search.category.name" class="button button-small button-text button-stable button-icon-event stable-900-bg" style="margin-right: 10px">\n &nbsp;<i class="icon ion-flag"></i>\n {{\'MARKET.SEARCH.CATEGORY\'|translate}}\n <span ng-bind-html="search.category.name"></span>\n <i class="icon ion-close" ng-click="removeCategory()">&nbsp;&nbsp;</i>\n </div>\n\n </div>\n\n <i class="icon ion-search placeholder-icon"></i>\n <input type="text" class="visible-xs visible-sm" placeholder="{{\'MARKET.SEARCH.SEARCH_HELP\'|translate}}" ng-model="search.text" ng-model-options="{ debounce: 650 }" ng-change="doSearch()">\n <input type="text" class="hidden-xs hidden-sm" placeholder="{{\'MARKET.SEARCH.SEARCH_HELP\'|translate}}" id="marketSearchText" ng-model="search.text" on-return="doSearch()">\n </div>\n\n </div>\n\n <!-- location -->\n <ng-include src="::\'plugins/es/templates/common/item_location_search.html\'" ng-if="entered && !search.geoPoint && options.location.show" ng-controller="ESSearchPositionItemCtrl"></ng-include>\n\n <!-- options -->\n <ng-include src="::\'plugins/market/templates/search/lookup_options.html\'"></ng-include>\n </form>\n\n <div ng-if="!search.loading && !search.category && options.category.show" class="padding-right visible-xs visible-sm" style="display: block; height: 35px">\n <a class="button button-text b
$templateCache.put('plugins/market/templates/search/lookup_options.html','\n\n <!-- Show closed ad ? -->\n <div ng-if="search.advanced" class="item item-icon-left item-input item-toggle stable-bg">\n <i class="icon ion-speakerphone gray"></i>\n <b class="icon-secondary ion-close-circled assertive" style="top:7px; left: 34px"></b>\n <span class="input-label item-icon-left-padding" ng-click="search.showClosed=!search.showClosed">\n {{\'MARKET.SEARCH.SHOW_CLOSED_RECORD\' | translate}}\n </span>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="search.showClosed">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n <!-- Show OLD ad ? -->\n <div ng-if="search.advanced" class="item item-icon-left item-input item-toggle stable-bg">\n <i class="icon ion-clock gray"></i>\n <b class="icon-secondary ion-close-circled assertive" style="top:7px; left: 34px"></b>\n <span class="input-label item-icon-left-padding" ng-click="search.showOld=!search.showOld">\n {{\'MARKET.SEARCH.SHOW_OLD_RECORD\' | translate}}\n </span>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="search.showOld">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n');
$templateCache.put('plugins/market/templates/wallet/view_wallet_extend.html','\n\n<ng-if ng-if=":state:enable && extensionPoint === \'general\'">\n\n\n <ion-item class="item-icon-left item-text-wrap ink" ng-if="formData.profile.pubkey" copy-on-click="{{:rebind:formData.profile.pubkey}}">\n <i class="icon ion-card"></i>\n <span translate>MARKET.WALLET.DUNITER_PUBKEY</span>\n <h4 id="pubkey" class="dark text-left">{{:rebind:formData.profile.pubkey}}</h4>\n </ion-item>\n\n <div class="item item-icon-left item-text-wrap">\n <i class="icon ion-ios-help-outline positive"></i>\n <span ng-bind-html="\'MARKET.WALLET.DUNITER_ACCOUNT\'|translate:{currency:$root.currency.name}"></span>\n\n <h4 ng-if="formData.profile.pubkey" class="gray" translate>MARKET.WALLET.DUNITER_ACCOUNT_HELP</h4>\n <h4 ng-if="!formData.profile.pubkey" class="gray" translate>MARKET.WALLET.DUNITER_ACCOUNT_NO_PUBKEY_HELP</h4>\n </div>\n\n <!-- star level -->\n <div class="item item-icon-left item-text-wrap" ng-if="likeData.stars">\n\n <i class="icon" ng-class="{\'ion-android-star-outline\': likeData.stars.levelAvg <= 2, \'ion-android-star-half\': likeData.stars.levelAvg > 2 && likeData.stars.levelAvg <= 3, \'ion-android-star energized\': likeData.stars.levelAvg > 3}"></i>\n <span translate>WOT.VIEW.STARS</span>\n <h4 class="dark">{{\'WOT.VIEW.STAR_HIT_COUNT\' | translate: likeData.stars }}</h4>\n\n <div class="badge">\n <span ng-repeat="value in [1,2,3,4,5]" ng-class="{\'energized\': likeData.stars.levelAvg > 3, \'assertive\': likeData.stars.levelAvg <= 2}">\n <b class="ion-android-star" ng-if="value <= likeData.stars.levelAvg"></b>\n <b class="ion-android-star-half" ng-if="value > likeData.stars.levelAvg && value - 0.5 <= likeData.stars.levelAvg"></b>\n <b class="ion-android-star-outline" ng-if="value > likeData.stars.levelAvg && value - 0.5 > likeData.stars.levelAvg"></b>\n </span>\n <small class="dark">({{likeData.stars.levelAvg}}/5)</small>\n </div>\n </div>\n\n <div class="visible-xs visible-sm">\n <div class="item item-divider item-divider-top-border">\n {{\'MENU.MARKET\' | translate}}\n </div>\n\n <!-- market records -->\n <a class="item item-icon-left item-icon-right" ui-sref="app.market_wallet_records">\n <i class="icon ion-speakerphone"></i>\n {{\'MENU.MY_RECORDS\' | translate}}\n <i class="icon ion-ios-arrow-right"></i>\n </a>\n </div>\n\n</ng-if>\n');
$templateCache.put('plugins/market/templates/wallet/view_wallet_records.html','<ion-view left-buttons="leftButtons" class="market">\n <ion-nav-title>\n <span class="visible-xs visible-sm" translate>MENU.MY_RECORDS</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <cs-extension-point name="nav-buttons"></cs-extension-point>\n\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="doUpdate()">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true" bind-notifier="{ rebind:settings.useRelative, locale:settings.locale.id}">\n\n <!-- Buttons bar-->\n <div class="hidden-xs hidden-sm padding text-center">\n\n <button class="button button-stable button-small-padding icon ion-loop ink" ng-click="doSearch()" title="{{\'COMMON.BTN_REFRESH\' | translate}}">\n </button>\n\n &nbsp;\n\n <button class="button button-calm ink" ng-click="showNewRecordModal()">\n {{\'MARKET.COMMON.BTN_NEW_AD\' | translate}}\n </button>\n </div>\n\n <!-- list of records -->\n <div class="lookupForm" ng-class="::{\'padding-horizontal\': !smallscreen}">\n <ng-include src="::smallscreen ? \'plugins/market/templates/search/list_records.html\' : \'plugins/market/templates/search/list_records_lg.html\'"></ng-include>\n </div>\n </ion-content>\n\n <button id="fab-wallet-add-market-record" class="button button-fab button-fab-bottom-right button-assertive hidden-md hidden-lg drop" ng-click="showNewRecordModal()">\n <i class="icon ion-plus"></i>\n </button>\n</ion-view>\n');
$templateCache.put('plugins/market/templates/wot/view_identity_extend.html','\n<!-- General section -->\n<ng-if ng-if=":state:enable && extensionPoint === \'general\'">\n\n <ion-item class="item-icon-left item-text-wrap ink" ng-if="formData.profile.pubkey" copy-on-click="{{:rebind:formData.profile.pubkey}}">\n <i class="icon ion-card"></i>\n <span translate>MARKET.WOT.VIEW.DUNITER_PUBKEY</span>\n <h4 id="pubkey" class="dark text-left">{{:rebind:formData.profile.pubkey}}</h4>\n </ion-item>\n\n <div class="item item-icon-left item-text-wrap item-wallet-event">\n <i class="icon ion-ios-help-outline positive"></i>\n <span ng-bind-html="\'MARKET.WOT.VIEW.DUNITER_ACCOUNT\'|translate:{currency:$root.currency.name}"></span>\n <h4 class="gray" ng-if="formData.profile.pubkey" translate>MARKET.WOT.VIEW.DUNITER_ACCOUNT_HELP</h4>\n <h4 ng-if="!formData.profile.pubkey" trust-as-html="\'MARKET.WOT.VIEW.DUNITER_ACCOUNT_HELP_ASK_USER\'|translate"></h4>\n </div>\n</ng-if>\n\n<ng-if ng-if=":state:enable && extensionPoint === \'after-general\'">\n\n <div class="item item-divider item-divider-top-border">\n {{\'MENU.MARKET\' | translate}}\n </div>\n\n <!-- market records -->\n <a class="item item-icon-left item-icon-right" ui-sref="app.market_identity_records({pubkey: formData.pubkey})">\n <i class="icon ion-speakerphone"></i>\n {{\'MARKET.WOT.VIEW.BTN_RECORDS\' | translate}}\n <i class="icon ion-ios-arrow-right"></i>\n </a>\n\n</ng-if>\n');
$templateCache.put('plugins/market/templates/wot/view_identity_records.html','<ion-view left-buttons="leftButtons" class="market">\n <ion-nav-title>\n <span class="visible-xs visible-sm" translate>MENU.MY_RECORDS</span>\n </ion-nav-title>\n\n <ion-nav-buttons side="secondary">\n <cs-extension-point name="nav-buttons"></cs-extension-point>\n\n <button class="button button-icon button-clear icon ion-loop visible-xs visible-sm" ng-click="doUpdate()">\n </button>\n </ion-nav-buttons>\n\n <ion-content scroll="true" bind-notifier="{ rebind:settings.useRelative, locale:settings.locale.id}">\n\n <!-- Buttons bar-->\n <div class="hidden-xs hidden-sm padding text-center">\n\n <button class="button button-stable button-small-padding icon ion-loop ink" ng-click="doSearch()" title="{{\'COMMON.BTN_REFRESH\' | translate}}">\n </button>\n\n </div>\n\n <!-- list of records -->\n <div class="lookupForm" ng-class="::{\'padding-horizontal\': !smallscreen}">\n <ng-include src="::smallscreen ? \'plugins/market/templates/search/list_records.html\' : \'plugins/market/templates/search/list_records_lg.html\'"></ng-include>\n </div>\n </ion-content>\n\n</ion-view>\n');}]);
angular.module('cesium.es.plugin', [
// Services
'cesium.es.services',
// Controllers
'cesium.es.app.controllers',
'cesium.es.common.controllers',
'cesium.es.settings.controllers',
'cesium.es.wot.controllers',
'cesium.es.wallet.controllers',
'cesium.es.profile.controllers',
'cesium.es.message.controllers',
'cesium.es.notification.controllers',
'cesium.es.registry.controllers',
'cesium.es.subscription.controllers',
'cesium.es.document.controllers',
'cesium.es.network.controllers'
])
;
function EsPeer(json) {
var that = this;
Object.keys(json).forEach(function (key) {
that[key] = json[key];
});
that.endpoints = that.endpoints || [];
}
EsPeer.prototype.regexp = {
API_REGEXP: /^([A-Z_]+)(?:[ ]+([a-z_][a-z0-9-_.ğĞ]*))?(?:[ ]+([0-9.]+))?(?:[ ]+([0-9a-f:]+))?(?:[ ]+([0-9]+))(?:\/[^\/]+)?$/,
LOCAL_IP_ADDRESS: /^127[.]0[.]0.|192[.]168[.]|10[.]0[.]0[.]|172[.]16[.]/
};
EsPeer.prototype.keyID = function () {
var ep = this.ep || this.getEP();
if (ep.useBma) {
return [this.pubkey || "Unknown", ep.dns, ep.ipv4, ep.ipv6, ep.port, ep.useSsl, ep.path].join('-');
}
return [this.pubkey || "Unknown", ep.ws2pid, ep.path].join('-');
};
EsPeer.prototype.copyValues = function(to) {
var obj = this;
["version", "currency", "pub", "endpoints", "hash", "status", "block", "signature"].forEach(function (key) {
to[key] = obj[key];
});
};
EsPeer.prototype.copyValuesFrom = function(from) {
var obj = this;
["version", "currency", "pub", "endpoints", "block", "signature"].forEach(function (key) {
obj[key] = from[key];
});
};
EsPeer.prototype.json = function() {
var obj = this;
var json = {};
["version", "currency", "endpoints", "status", "block", "signature"].forEach(function (key) {
json[key] = obj[key];
});
json.raw = this.raw && this.getRaw();
json.pubkey = this.pubkey;
return json;
};
EsPeer.prototype.getEP = function() {
if (this.ep) return this.ep;
var ep = null;
var epRegex = this.regexp.API_REGEXP;
this.endpoints.forEach(function(epStr){
var matches = !ep && epRegex.exec(epStr);
if (matches) {
ep = {
"api": matches[1] || '',
"dns": matches[2] || '',
"ipv4": matches[3] || '',
"ipv6": matches[4] || '',
"port": matches[5] || 80,
"path": matches[6] || '',
"useSsl": matches[5] == 443
};
}
});
return ep || {};
};
EsPeer.prototype.getEndpoints = function(regexp) {
if (!regexp) return this.endpoints;
if (typeof regexp === 'string') regexp = new RegExp('^' + regexp);
return this.endpoints.reduce(function(res, ep){
return ep.match(regexp) ? res.concat(ep) : res;
}, []);
};
EsPeer.prototype.hasEndpoint = function(endpoint){
var regExp = this.regexp[endpoint] || new RegExp('^' + endpoint);
var endpoints = this.getEndpoints(regExp);
return endpoints && endpoints.length > 0;
};
EsPeer.prototype.hasEsEndpoint = function() {
var endpoints = this.getEsEndpoints();
return endpoints && endpoints.length > 0;
};
EsPeer.prototype.getEsEndpoints = function() {
return this.getEndpoints(/^(ES_CORE_API|ES_USER_API|ES_SUBSCRIPTION_API|GCHANGE_API)/);
};
EsPeer.prototype.getDns = function() {
var ep = this.ep || this.getEP();
return ep.dns ? ep.dns : null;
};
EsPeer.prototype.getIPv4 = function() {
var ep = this.ep || this.getEP();
return ep.ipv4 ? ep.ipv4 : null;
};
EsPeer.prototype.getIPv6 = function() {
var ep = this.ep || this.getEP();
return ep.ipv6 ? ep.ipv6 : null;
};
EsPeer.prototype.getPort = function() {
var ep = this.ep || this.getEP();
return ep.port ? ep.port : null;
};
EsPeer.prototype.getHost = function() {
var ep = this.ep || this.getEP();
return ((ep.port == 443 || ep.useSsl) && ep.dns) ? ep.dns :
(this.hasValid4(ep) ? ep.ipv4 :
(ep.dns ? ep.dns :
(ep.ipv6 ? '[' + ep.ipv6 + ']' :'')));
};
EsPeer.prototype.getURL = function() {
var ep = this.ep || this.getEP();
var host = this.getHost();
var protocol = (ep.port == 443 || ep.useSsl) ? 'https' : 'http';
return protocol + '://' + host + (ep.port ? (':' + ep.port) : '');
};
EsPeer.prototype.getServer = function() {
var ep = this.ep || this.getEP();
var host = this.getHost();
return host + (host && ep.port ? (':' + ep.port) : '');
};
EsPeer.prototype.hasValid4 = function(ep) {
return ep.ipv4 &&
/* exclude private address - see https://fr.wikipedia.org/wiki/Adresse_IP */
!ep.ipv4.match(this.regexp.LOCAL_IP_ADDRESS) ?
true : false;
};
EsPeer.prototype.isReachable = function () {
return !!this.getServer();
};
EsPeer.prototype.isSsl = function() {
var ep = this.ep || this.getEP();
return ep.useSsl;
};
EsPeer.prototype.isTor = function() {
var ep = this.ep || this.getEP();
return ep.useTor;
};
EsPeer.prototype.isHttp = function() {
var ep = this.ep || this.getEP();
return !bma.useTor;
};
function EsNotification(json, markAsReadCallback) {
var messagePrefixes = {
'user': 'EVENT.USER.',
'page': 'EVENT.PAGE.',
// gchange market record
'market': 'EVENT.MARKET.'
};
var that = this;
// Avoid undefined errors
json = json || {};
that.id = json.id || ('' + Date.now()); // Keep id if exists, otherwise create it from timestamp
that.type = json.type && json.type.toLowerCase();
that.time = json.time;
that.hash = json.hash;
that.read = json.read_signature ? true : false;
that.message = json.reference && messagePrefixes[json.reference.index] ?
messagePrefixes[json.reference.index] + json.code :
'EVENT.' + json.code;
that.params = json.params;
if (markAsReadCallback && (typeof markAsReadCallback === "function") ) {
that.markAsReadCallback = markAsReadCallback;
}
function _formatHash(input) {
return input ? input.substr(0,4) + input.substr(input.length-4) : '';
}
that.markAsRead = function() {
if (that.markAsReadCallback) {
that.markAsReadCallback(that);
}
};
var pubkeys;
json.code = json.code || '';
// Membership
if (json.code.startsWith('MEMBER_')) {
that.avatarIcon = 'ion-person';
that.icon = 'ion-information-circled positive';
that.state = 'app.view_wallet';
that.medianTime = that.time;
}
// TX
else if (json.code.startsWith('TX_')) {
that.avatarIcon = 'ion-card';
that.icon = (json.code === 'TX_SENT') ? 'ion-paper-airplane dark' : 'ion-archive balanced';
that.medianTime = that.time;
pubkeys = json.params.length > 0 ? json.params[0] : null;
if (pubkeys && pubkeys.indexOf(',') == -1) {
that.pubkey = pubkeys;
}
that.state = 'app.view_wallet_tx';
that.stateParams = {refresh: true};
}
// Certifications
else if (json.code.startsWith('CERT_')) {
that.avatarIcon = (json.code === 'CERT_RECEIVED') ? 'ion-ribbon-b' : 'ion-ribbon-a';
that.icon = (json.code === 'CERT_RECEIVED') ? 'ion-ribbon-b balanced' : 'ion-ribbon-a gray';
that.pubkey = json.params.length > 0 ? json.params[0] : null;
that.medianTime = that.time;
that.state = 'app.wallet_cert';
that.stateParams = {
type: (json.code === 'CERT_RECEIVED') ? 'received' : 'given'
};
}
// Message
else if (json.code.startsWith('MESSAGE_')) {
that.avatarIcon = 'ion-email';
that.icon = 'ion-email dark';
pubkeys = json.params.length > 0 ? json.params[0] : null;
if (pubkeys && pubkeys.indexOf(',') === -1) {
that.pubkey = pubkeys;
}
that.id = json.reference.id; // Do not care about notification ID, because notification screen use message _id
}
// user profile record
else if (json.reference && json.reference.index === 'user' && json.reference.type === 'profile') {
that.pubkey = json.params.length > 0 ? json.params[0] : null;
that.state = 'app.wot_identity';
that.stateParams = {
pubkey: that.pubkey,
uid: json.params && json.params[3],
};
if (json.code.startsWith('LIKE_')) {
that.avatarIcon = 'ion-person';
that.icon = 'ion-ios-heart positive';
}
else if (json.code.startsWith('STAR_')) {
that.avatarIcon = 'ion-person';
that.icon = 'ion-star gray';
}
else if (json.code.startsWith('FOLLOW_')) {
that.avatarIcon = 'ion-person';
that.icon = 'ion-ios-people gray';
}
else if (json.code.startsWith('ABUSE_')) {
that.avatarIcon = 'ion-person';
that.icon = 'ion-android-warning assertive';
}
if (json.code.startsWith('MODERATION_')) {
that.state = 'app.wot_identity';
that.stateParams = {
pubkey: json.reference.id,
uid: json.params && json.params[3],
};
that.avatarIcon = 'ion-alert-circled';
that.icon = 'ion-alert-circled energized';
// If deletion has been asked, change the message
var level = json.params && json.params[4] || 0;
if (json.code === 'MODERATION_RECEIVED' && level == 5) {
that.message = 'EVENT.USER.DELETION_RECEIVED';
that.icon = 'ion-trash-a assertive';
}
}
else {
that.icon = 'ion-person dark';
that.state = 'app.view_wallet';
}
}
// page record
else if (json.reference && json.reference.index === 'page') {
that.pubkey = json.params.length > 0 ? json.params[0] : null;
that.avatarIcon = 'ion-social-buffer';
if (json.reference.anchor) {
that.icon = 'ion-ios-chatbubble-outline dark';
that.state = 'app.view_page_anchor';
that.stateParams = {
id: json.reference.id,
title: json.params[1],
anchor: _formatHash(json.reference.anchor)
};
}
else {
that.icon = 'ion-social-buffer dark';
that.state = 'app.view_page';
that.stateParams = {
id: json.reference.id,
title: json.params[1]
};
}
if (json.code.startsWith('LIKE_')) {
that.icon = 'ion-ios-heart positive';
that.state = 'app.wot_identity';
that.stateParams = {
pubkey: that.pubkey,
uid: json.params && json.params[3],
};
}
else if (json.code.startsWith('FOLLOW_')) {
that.avatarIcon = 'ion-person';
that.state = 'app.wot_identity';
that.stateParams = {
pubkey: that.pubkey,
uid: json.params && json.params[3],
};
}
else if (json.code.startsWith('ABUSE_')) {
that.icon = 'ion-alert-circled energized';
that.state = 'app.wot_identity';
that.stateParams = {
pubkey: that.pubkey,
uid: json.params && json.params[3],
};
}
else if (json.code.startsWith('MODERATION_')) {
that.avatarIcon = 'ion-alert-circled';
that.icon = 'ion-alert-circled energized';
// If deletion has been asked, change the message
var level = json.params && json.params[4] || 0;
if (json.code === 'MODERATION_RECEIVED' && level == 5) {
that.message = 'EVENT.PAGE.DELETION_RECEIVED';
that.icon = 'ion-trash-a assertive';
}
}
}
// market record
else if (json.reference && json.reference.index === 'market') {
that.avatarIcon = 'ion-speakerphone';
that.pubkey = json.params.length > 0 ? json.params[0] : null;
if (json.reference.anchor) {
that.icon = 'ion-ios-chatbubble-outline dark';
that.state = 'app.market_view_record_anchor';
that.stateParams = {
id: json.reference.id,
title: json.params[2],
anchor: _formatHash(json.reference.anchor)
};
}
else {
that.icon = 'ion-speakerphone dark';
that.state = 'app.market_view_record';
that.stateParams = {
id: json.reference.id,
title: json.params[2]};
}
if (json.code.startsWith('LIKE_')) {
that.icon = 'ion-ios-heart positive';
}
else if (json.code.startsWith('FOLLOW_')) {
that.avatarIcon = 'ion-person';
}
else if (json.code.startsWith('ABUSE_')) {
that.icon = 'ion-alert-circled energized';
}
else if (json.code.startsWith('MODERATION_')) {
that.avatarIcon = 'ion-alert-circled';
that.icon = 'ion-alert-circled energized';
// If deletion has been asked, change the message
if (json.code === 'MODERATION_RECEIVED' && level == 5) {
that.message = 'EVENT.MARKET.DELETION_RECEIVED';
that.icon = 'ion-trash-a assertive';
}
}
}
// info message
else if (json.type === 'INFO') {
that.avatarIcon = 'ion-information';
that.icon = 'ion-information-circled positive';
}
// warn message
else if (json.type === 'WARN') {
that.avatarIcon = 'ion-alert-circled';
that.icon = 'ion-alert-circled energized';
}
// error message
else if (json.type === 'ERROR') {
that.avatarIcon = 'ion-close';
that.icon = 'ion-close-circled assertive';
}
return that;
}
function Comment(id, json) {
var that = this;
that.id = id;
that.message = null; // set in copyFromJson()
that.html = null; // set in copyFromJson()
that.issuer = null; // set in copyFromJson()
that.time = null; // set in copyFromJson()
that.creationTime = null; // set in copyFromJson()
that.reply_to = null; // set in copyFromJson()
that.replyCount = 0;
that.parent = null;
that.replies = [];
that.onRemoveListeners = [];
that.copy = function(otherComment) {
// Mandatory fields
that.message = otherComment.message;
that.html = otherComment.html;
that.issuer = otherComment.issuer;
that.time = otherComment.time;
that.creationTime = otherComment.creationTime || that.time; // fill using time, for backward compatibility
// Optional fields
that.id = otherComment.id || that.id;
that.reply_to = otherComment.reply_to || that.reply_to;
that.uid = otherComment.uid || that.uid;
that.name = otherComment.name || that.name;
that.avatarStyle = otherComment.avatarStyle || that.avatarStyle;
if (otherComment.parent) {
that.parent = otherComment.parent;
}
if (otherComment.replies) that.setReplies(otherComment.replies);
};
that.copyFromJson = function(json) {
that.message = json.message;
that.issuer = json.issuer;
that.time = json.time;
that.creationTime = json.creationTime || that.time;
that.reply_to = json.reply_to;
};
that.addOnRemoveListener = function(listener) {
if (listener && (typeof listener === "function") ) {
that.onRemoveListeners.push(listener);
}
};
that.cleanAllListeners = function() {
that.onRemoveListeners = [];
};
that.setReplies = function(replies) {
that.removeAllReplies();
that.addReplies(replies);
};
that.addReplies = function(replies) {
if (!replies || !replies.length) return;
replies = replies.sort(function(cm1, cm2) {
return (cm1.time - cm2.time);
});
_.forEach(replies, function(reply) {
reply.parent = that;
that.replies.push(reply);
});
that.replyCount += replies.length;
};
that.containsReply = function(reply) {
return that.replies.indexOf(reply) != -1;
};
that.addReply = function(reply) {
that.replyCount += 1;
that.replies.push(reply);
that.replies = that.replies.sort(function(cm1, cm2) {
return (cm1.time - cm2.time);
});
reply.parent = that;
};
that.removeAllReplies = function() {
if (that.replyCount) {
var replies = that.replies.splice(0, that.replies.length);
that.replyCount = 0;
_.forEach(replies, function (reply) {
reply.remove();
});
}
};
that.removeReply = function(replyId) {
var index = _.findIndex(that.replies, {id: replyId});
if (index != -1) {
that.replyCount--;
var reply = that.replies.splice(index, 1)[0];
delete reply.parent;
}
};
that.remove = function() {
if (that.parent) {
that.parent.removeReply(that.id);
delete that.parent;
}
//that.removeAllReplies();
if (that.onRemoveListeners.length) {
_.forEach(that.onRemoveListeners, function(listener) {
listener(that);
});
that.issuer = null;
that.message = null;
that.cleanAllListeners();
}
};
// Init from json
if (json && typeof json === "object") {
that.copyFromJson(json);
}
}
angular.module('cesium.es.services', [
// Services
'cesium.es.http.services',
'cesium.es.comment.services',
'cesium.es.social.services',
'cesium.es.settings.services',
'cesium.es.crypto.services',
'cesium.es.profile.services',
'cesium.es.notification.services',
'cesium.es.message.services',
'cesium.es.modal.services',
'cesium.es.wallet.services',
'cesium.es.subscription.services',
'cesium.es.geo.services',
'cesium.es.document.services',
'cesium.es.registry.services',
'cesium.es.network.services'
])
;
angular.module('cesium.es.comment.services', ['ngResource', 'cesium.services',
'cesium.es.http.services', 'cesium.es.profile.services'])
.factory('esComment', ['$rootScope', '$q', 'UIUtils', 'BMA', 'esHttp', 'csWallet', 'csWot', function($rootScope, $q, UIUtils, BMA, esHttp, csWallet, csWot) {
'ngInject';
function EsComment(index) {
var
DEFAULT_SIZE = 20,
fields = {
commons: ["issuer", "creationTime", "time", "message", "reply_to"]
},
exports = {
index: index,
fields: {
commons: fields.commons
},
raw: {
search: esHttp.post('/'+index+'/comment/_search'),
remove: esHttp.record.remove(index, 'comment'),
wsChanges: esHttp.ws('/ws/_changes'),
add: new esHttp.record.post('/'+index+'/comment', {creationTime: true}),
update: new esHttp.record.post('/'+index+'/comment/:id/_update', {creationTime: true})
}
};
exports.raw.refreshTreeLinks = function(data) {
return exports.raw.addTreeLinks(data, true);
};
exports.raw.addTreeLinks = function(data, refresh) {
data = data || {};
data.result = data.result || [];
data.mapById = data.mapById || {};
var incompleteCommentIdByParentIds = {};
_.forEach(_.values(data.mapById), function(comment) {
if (comment.reply_to && !comment.parent) {
var parent = data.mapById[comment.reply_to];
if (!parent) {
parent = new Comment(comment.reply_to);
incompleteCommentIdByParentIds[parent.id] = comment.id;
data.mapById[parent.id] = parent;
}
if (!refresh || !parent.containsReply(comment)) {
parent.addReply(comment);
}
}
});
if (!_.size(incompleteCommentIdByParentIds)) {
var deferred = $q.defer();
deferred.resolve(data);
return deferred.promise;
}
var request = {
query : {
terms: {
_id: _.keys(incompleteCommentIdByParentIds)
}
},
sort : [
// Need desc, because of size+offset (will be sort in 'asc' order later)
{ "creationTime" : {"order" : "desc"}},
{ "time" : {"order" : "desc"}} // for backward compatibility
],
from: 0,
size: 1000,
_source: fields.commons
};
console.debug("[ES] [comment] Getting missing comments in tree");
return exports.raw.search(request)
.then(function(res){
if (!res.hits.total) {
console.error("[ES] [comment] Comments has invalid [reply_to]: " + _.values(incompleteCommentIdByParentIds).join(','));
return data;
}
_.forEach(res.hits.hits, function(hit) {
var comment = data.mapById[hit._id];
comment.copyFromJson(hit._source);
// Parse URL and hashtags
comment.html = esHttp.util.parseAsHtml(comment.message);
delete incompleteCommentIdByParentIds[comment.id];
});
if (_.size(incompleteCommentIdByParentIds)) {
console.error("Comments has invalid [reply_to]: " + _.values(incompleteCommentIdByParentIds).join(','));
}
return exports.raw.addTreeLinks(data); // recursive call
});
};
exports.raw.loadDataByRecordId = function(recordId, options) {
options = options || {};
options.from = options.from || 0;
options.size = options.size || DEFAULT_SIZE;
options.loadAvatar = angular.isDefined(options.loadAvatar) ? options.loadAvatar : true;
options.loadAvatarAllParent = angular.isDefined(options.loadAvatarAllParent) ? (options.loadAvatar && options.loadAvatarAllParent) : false;
if (options.size < 0) options.size = 1000; // all comments
var request = {
query : {
term: { record : recordId}
},
sort : [
// Need desc, because of size+offset (will be sort in 'asc' order later)
{ "creationTime" : {"order" : "desc"}},
{ "time" : {"order" : "desc"}} // for backward compatibility
],
from: options.from,
size: options.size,
_source: fields.commons
};
var data = {
total: 0,
mapById: {},
result: [],
pendings: {}
};
// Search comments
return exports.raw.search(request)
.then(function(res){
if (!res.hits.total) return data;
data.total = res.hits.total;
data.result = res.hits.hits.reduce(function (result, hit) {
var comment = new Comment(hit._id, hit._source);
// Parse URL and hashtags
comment.html = esHttp.util.parseAsHtml(comment.message);
// fill map by id
data.mapById[comment.id] = comment;
return result.concat(comment);
}, data.result);
// Add tree (parent/child) link
return exports.raw.addTreeLinks(data);
})
// Fill avatars (and uid)
.then(function() {
if (!options.loadAvatar) return;
if (options.loadAvatarAllParent) {
return csWot.extendAll(_.values(data.mapById), 'issuer');
}
return csWot.extendAll(data.result, 'issuer');
})
// Sort (creationTime asc)
.then(function() {
data.result = data.result.sort(function(cm1, cm2) {
return (cm1.creationTime - cm2.creationTime);
});
return data;
});
};
// Add listener to send deletion
exports.raw.createOnDeleteListener = function(data) {
return function(comment) {
var index = _.findIndex(data.result, {id: comment.id});
if (index === -1) return;
data.result.splice(index, 1);
delete data.mapById[comment.id];
// Send deletion request
if (csWallet.isUserPubkey(comment.issuer)) {
exports.raw.remove(comment.id, csWallet.data.keypair)
.catch(function(err){
console.error(err);
throw new Error('MARKET.ERROR.FAILED_REMOVE_COMMENT');
});
}
};
};
exports.raw.startListenChanges = function(recordId, data, scope) {
data = data || {};
data.result = data.result || [];
data.mapById = data.mapById || {};
data.pendings = data.pendings || {};
scope = scope||$rootScope;
// Add listener to send deletion
var onRemoveListener = exports.raw.createOnDeleteListener(data);
_.forEach(data.result, function(comment) {
comment.addOnRemoveListener(onRemoveListener);
});
// Open websocket
var now = Date.now();
console.info("[ES] [comment] Starting websocket to listen comments on [{0}/record/{1}]".format(index, recordId.substr(0,8)));
var wsChanges = esHttp.websocket.changes(index + '/comment');
return wsChanges.open()
// Listen changes
.then(function(){
console.debug("[ES] [comment] Websocket opened in {0} ms".format(Date.now() - now));
wsChanges.on(function(change) {
if (!change) return;
scope.$applyAsync(function() {
var comment = data.mapById[change._id];
if (change._operation === 'DELETE') {
if (comment) comment.remove();
}
else if (change._source && change._source.record === recordId) {
// update
if (comment) {
comment.copyFromJson(change._source);
// Parse URL and hashtags
comment.html = esHttp.util.parseAsHtml(comment.message);
exports.raw.refreshTreeLinks(data);
}
// create (if not in pending comment)
else if ((!data.pendings || !data.pendings[change._source.creationTime]) && change._source.issuer != csWallet.data.pubkey) {
comment = new Comment(change._id, change._source);
comment.addOnRemoveListener(onRemoveListener);
comment.isnew = true;
// Parse URL and hashtags
comment.html = esHttp.util.parseAsHtml(comment.message);
// fill map by id
data.mapById[change._id] = comment;
exports.raw.refreshTreeLinks(data)
// fill avatars (and uid)
.then(function() {
return csWot.extend(comment, 'issuer');
})
.then(function() {
data.result.push(comment);
});
}
else {
console.debug("Skip comment received by WS (already in pending)");
}
}
});
});
});
};
/**
* Save a comment (add or update)
* @param recordId
* @param data
* @param comment
* @returns {*}
*/
exports.raw.save = function(recordId, data, comment) {
data = data || {};
data.result = data.result || [];
data.mapById = data.mapById || {};
data.pendings = data.pendings || {};
// Preparing JSON to sent
var id = comment.id;
var json = {
creationTime: id ? comment.creationTime || comment.time/*for compat*/ : moment().utc().unix(),
message: comment.message,
record: recordId,
issuer: csWallet.data.pubkey
};
if (comment.reply_to || comment.parent) {
json.reply_to = comment.reply_to || comment.parent.id;
}
else {
json.reply_to = null; // force to null because ES ignore missing field, when updating
}
// Create or update the entity
var entity;
if (!id) {
entity = new Comment(null, json);
entity.addOnRemoveListener(exports.raw.createOnDeleteListener(data));
// copy additional wallet data
entity.uid = csWallet.data.uid;
entity.name = csWallet.data.name;
entity.avatar = csWallet.data.avatar;
entity.isnew = true;
if (comment.parent) {
comment.parent.addReply(entity);
}
data.result.push(entity);
}
else {
entity = data.mapById[id];
entity.copy(comment);
}
// Parse URL and hashtags
entity.html = esHttp.util.parseAsHtml(entity.message);
// Send add request
if (!id) {
data.pendings = data.pendings || {};
data.pendings[json.creationTime] = json;
return exports.raw.add(json)
.then(function (id) {
entity.id = id;
data.mapById[id] = entity;
delete data.pendings[json.creationTime];
return entity;
});
}
// Send update request
else {
return exports.raw.update(json, {id: id})
.then(function () {
return entity;
});
}
};
exports.raw.stopListenChanges = function(data) {
console.debug("[ES] [comment] Stopping websocket on comments");
_.forEach(data.result, function(comment) {
comment.cleanAllListeners();
});
// Close previous
exports.raw.wsChanges().close();
};
// Expose functions
exports.load = exports.raw.loadDataByRecordId;
exports.save = exports.raw.save;
exports.changes = {
start: exports.raw.startListenChanges,
stop: exports.raw.stopListenChanges
};
return exports;
}
return {
instance: EsComment
};
}])
;
angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.services', 'cesium.config'])
/**
* Elastic Search Http
*/
.factory('esHttp', ['$q', '$timeout', '$rootScope', '$state', '$sce', '$translate', '$window', '$filter', 'CryptoUtils', 'UIUtils', 'csHttp', 'csConfig', 'csSettings', 'csCache', 'BMA', 'csWallet', 'csPlatform', 'Api', function($q, $timeout, $rootScope, $state, $sce, $translate, $window, $filter,
CryptoUtils, UIUtils, csHttp, csConfig, csSettings, csCache, BMA, csWallet, csPlatform, Api) {
'ngInject';
// Allow to force SSL connection with port different from 443
var forceUseSsl = (csConfig.httpsMode === 'true' || csConfig.httpsMode === true || csConfig.httpsMode === 'force') ||
($window.location && $window.location.protocol === 'https:') ? true : false;
if (forceUseSsl) {
console.debug('[ES] [https] Enable SSL (forced by config or detected in URL)');
}
function EsHttp(host, port, useSsl, useCache) {
var
that = this,
cachePrefix = 'esHttp-',
constants = {
ES_USER_API: 'ES_USER_API',
ES_SUBSCRIPTION_API: 'ES_SUBSCRIPTION_API',
ES_USER_API_ENDPOINT: 'ES_USER_API( ([a-z_][a-z0-9-_.]*))?( ([0-9.]+))?( ([0-9a-f:]+))?( ([0-9]+))',
ANY_API_ENDPOINT: '([A-Z_]+)(?:[ ]+([a-z_][a-z0-9-_.ğĞ]*))?(?:[ ]+([0-9.]+))?(?:[ ]+([0-9a-f:]+))?(?:[ ]+([0-9]+))(?:\\/[^\\/]+)?',
MAX_UPLOAD_BODY_SIZE: csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.maxUploadBodySize || 2097152 /*=2M*/,
GCHANGE_API: 'GCHANGE_API',
like: {
KINDS: ['VIEW', 'LIKE', 'DISLIKE', 'FOLLOW', 'ABUSE', 'STAR']
}
},
regexp = {
IMAGE_SRC: exact('data:([A-Za-z//]+);base64,(.+)'),
URL: match('(www\\.|https?:\/\/(www\\.)?)[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)'),
HASH_TAG: match('(?:^|[\t\n\r\s ])#([0-9_-\\wḡĞǦğàáâãäåçèéêëìíîïðòóôõöùúûüýÿ]+)'),
USER_TAG: match('(?:^|[\t\n\r\s ])@('+BMA.constants.regexp.USER_ID+')'),
ES_USER_API_ENDPOINT: exact(constants.ES_USER_API_ENDPOINT),
API_ENDPOINT: exact(constants.ANY_API_ENDPOINT),
},
fallbackNodeIndex = 0,
listeners,
defaultSettingsNode,
truncUrlFilter = $filter('truncUrl');
that.data = {
isFallback: false
};
that.cache = _emptyCache();
that.api = new Api(this, "esHttp");
that.started = false;
that.init = init;
init(host, port, useSsl, useCache);
that.useCache = angular.isDefined(useCache) ? useCache : false; // need here because used in get() function
function init(host, port, useSsl, useCache) {
// Use settings as default
if (!host && csSettings.data) {
host = host || (csSettings.data.plugins && csSettings.data.plugins.es ? csSettings.data.plugins.es.host : null);
port = port || (host ? csSettings.data.plugins.es.port : null);
useSsl = angular.isDefined(useSsl) ? useSsl : (port == 443 || csSettings.data.plugins.es.useSsl || forceUseSsl);
}
that.alive = false;
that.host = host;
that.port = port || ((useSsl || forceUseSsl) ? 443 : 80);
that.useSsl = angular.isDefined(useSsl) ? useSsl : (that.port == 443 || forceUseSsl);
that.server = csHttp.getServer(host, port);
}
function isSameNodeAsSettings(data) {
data = data || csSettings.data;
if (!data.plugins || !data.plugins.es) return false;
var host = data.plugins.es.host;
var useSsl = data.plugins.es.port == 443 || data.plugins.es.useSsl || forceUseSsl;
var port = data.plugins.es.port || (useSsl ? 443 : 80);
return isSameNode(host, port, useSsl);
}
function isSameNode(host, port, useSsl) {
return (that.host === host) &&
(that.port === port) &&
(angular.isUndefined(useSsl) || useSsl == that.useSsl);
}
// Say if the ES node is a fallback node or the configured node
function isFallbackNode() {
return that.data.isFallback;
}
// Set fallback flag (e.g. called by ES settings, when resetting settings)
function setIsFallbackNode(isFallback) {
that.data.isFallback = isFallback;
}
function exact(regexpContent) {
return new RegExp('^' + regexpContent + '$');
}
function match(regexpContent) {
return new RegExp(regexpContent);
}
function _emptyCache() {
return {
getByPath: {},
postByPath: {},
wsByPath: {}
};
}
function onSettingsReset(data, deferred) {
deferred = deferred || $q.defer();
if (that.data.isFallback) {
// Force a restart
if (that.started) {
that.stop();
}
}
// Reset to default values
that.data.isFallback = false;
defaultSettingsNode = null;
deferred.resolve(data);
return deferred.promise;
}
that.cleanCache = function() {
console.debug('[ES] [http] Cleaning requests cache...');
_.keys(that.cache.wsByPath).forEach(function(key) {
var sock = that.cache.wsByPath[key];
sock.close();
});
that.cache = _emptyCache();
csCache.clear(cachePrefix);
};
that.copy = function(otherNode) {
if (that.started) that.stop();
that.init(otherNode.host, otherNode.port, otherNode.useSsl || otherNode.port == 443);
that.data.isTemporary = false; // reset temporary flag
return that.start(true /*skipInit*/);
};
// Get node time (UTC) FIXME: get it from the node
that.date = { now : csHttp.date.now };
that.byteCount = function (s) {
s = (typeof s == 'string') ? s : JSON.stringify(s);
return encodeURI(s).split(/%(?:u[0-9A-F]{2})?[0-9A-F]{2}|./).length - 1;
};
that.getUrl = function(path) {
return csHttp.getUrl(that.host, that.port, path, that.useSsl);
};
that.get = function (path, cacheTime) {
cacheTime = that.useCache && cacheTime;
var cacheKey = path + (cacheTime ? ('#'+cacheTime) : '');
var getRequestFn = function(params) {
if (!that.started) {
if (!that._startPromise) {
console.warn('[ES] [http] Trying to get [{0}] before start(). Waiting...'.format(path));
}
return that.ready().then(function(start) {
if (!start) return $q.reject('ERROR.ES_CONNECTION_ERROR');
return getRequestFn(params); // loop
});
}
var request = that.cache.getByPath[cacheKey];
if (!request) {
if (cacheTime) {
request = csHttp.getWithCache(that.host, that.port, path, that.useSsl, cacheTime, null, null, cachePrefix);
}
else {
request = csHttp.get(that.host, that.port, path, that.useSsl);
}
that.cache.getByPath[cacheKey] = request;
}
return request(params);
};
return getRequestFn;
};
that.post = function(path) {
var postRequest = function(obj, params) {
if (!that.started) {
if (!that._startPromise) {
console.error('[ES] [http] Trying to post [{0}] before start()...'.format(path));
}
return that.ready().then(function(start) {
if (!start) return $q.reject('ERROR.ES_CONNECTION_ERROR');
return postRequest(obj, params); // loop
});
}
var request = that.cache.postByPath[path];
if (!request) {
request = csHttp.post(that.host, that.port, path, that.useSsl);
that.cache.postByPath[path] = request;
}
return request(obj, params);
};
return postRequest;
};
that.ws = function(path) {
return function() {
var sock = that.cache.wsByPath[path];
if (!sock || sock.isClosed()) {
sock = csHttp.ws(that.host, that.port, path, that.useSsl);
// When close, remove from cache
sock.onclose = function() {
delete that.cache.wsByPath[path];
};
that.cache.wsByPath[path] = sock;
}
return sock;
};
};
that.wsChanges = function(source) {
var wsChanges = that.ws('/ws/_changes')();
if (!source) return wsChanges;
// If a source is given, send it just after connection open
var _inheritedOpen = wsChanges.open;
wsChanges.open = function() {
return _inheritedOpen.call(wsChanges).then(function(sock) {
if(sock) {
sock.send(source);
}
else {
console.warn('Trying to access ws changes, but no sock anymore... already open ?');
}
});
};
return wsChanges;
};
that.isAlive = function() {
return csHttp.get(that.host, that.port, '/node/summary', that.useSsl)()
.then(function(json) {
var software = json && json.duniter && json.duniter.software || 'unknown';
if (software === "gchange-pod" || software === "cesium-plus-pod") return true;
console.error("[ES] [http] Not a Gchange Pod, but a {0} node. Please check '/summary/node'".format(software));
return false;
})
.catch(function() {
return false;
});
};
// Alert user if node not reached - fix issue #
that.checkNodeAlive = function(alive) {
if (alive) {
setIsFallbackNode(!isSameNodeAsSettings());
return true;
}
if (angular.isUndefined(alive)) {
return that.isAlive().then(that.checkNodeAlive);
}
var settings = csSettings.data.plugins && csSettings.data.plugins.es || {};
// Remember the default node
defaultSettingsNode = defaultSettingsNode || {
host: settings.host,
port: settings.port
};
var fallbackNode = settings.fallbackNodes && fallbackNodeIndex < settings.fallbackNodes.length && settings.fallbackNodes[fallbackNodeIndex++];
if (!fallbackNode) {
$translate('ERROR.ES_CONNECTION_ERROR', {server: that.server})
.then(UIUtils.alert.info);
return false; // stop the loop
}
var newServer = csHttp.getServer(fallbackNode.host, fallbackNode.port);
UIUtils.loading.hide();
return $translate('CONFIRM.ES_USE_FALLBACK_NODE', {old: that.server, new: newServer})
.then(UIUtils.alert.confirm)
.then(function (confirm) {
if (!confirm) return false; // stop the loop
that.cleanCache();
that.init(fallbackNode.host, fallbackNode.port, fallbackNode.useSsl || fallbackNode.port == 443);
// check is alive then loop
return that.isAlive().then(that.checkNodeAlive);
});
};
that.isStarted = function() {
return that.started;
};
that.ready = function() {
if (that.started) return $q.when(true);
return that._startPromise || that.start();
};
that.start = function(skipInit) {
if (that._startPromise) return that._startPromise;
if (that.started) return $q.when(that.alive);
that._startPromise = csPlatform.ready()
.then(function() {
if (!skipInit) {
// Init with defaults settings
that.init();
}
})
.then(function() {
console.debug('[ES] [http] Starting on [{0}]{1}...'.format(
that.server,
(that.useSsl ? ' (SSL on)' : '')
));
var now = Date.now();
return that.checkNodeAlive()
.then(function(alive) {
that.alive = alive;
if (!alive) {
console.error('[ES] [http] Could not start [{0}]: node unreachable'.format(that.server));
that.started = true;
delete that._startPromise;
fallbackNodeIndex = 0; // reset the fallback node counter
return false;
}
// Add listeners
addListeners();
console.debug('[ES] [http] Started in '+(Date.now()-now)+'ms');
that.api.node.raise.start();
that.started = true;
delete that._startPromise;
fallbackNodeIndex = 0; // reset the fallback node counter
return true;
});
});
return that._startPromise;
};
that.stop = function() {
console.debug('[ES] [http] Stopping...');
removeListeners();
setIsFallbackNode(false); // will be re-computed during start phase
delete that._startPromise;
if (that.alive) {
that.cleanCache();
that.alive = false;
that.started = false;
that.api.node.raise.stop();
}
else {
that.started = false;
}
return $q.when();
};
that.restart = function() {
that.stop();
return $timeout(that.start, 200);
};
function parseTagsFromText(value, prefix) {
prefix = prefix || '#';
var reg = prefix === '@' ? regexp.USER_TAG : regexp.HASH_TAG;
var matches = value && reg.exec(value);
var tags = matches && [];
while(matches) {
var tag = matches[1];
if (!_.contains(tags, tag)) {
tags.push(tag);
}
value = value.substr(matches.index + matches[1].length + 1);
matches = value.length > 0 && reg.exec(value);
}
return tags;
}
function parseUrlsFromText(value) {
var matches = value && regexp.URL.exec(value);
var urls = matches && [];
while(matches) {
var url = matches[0];
if (!_.contains(urls, url)) {
urls.push(url);
}
value = value.substr(matches.index + matches[0].length + 1);
matches = value && regexp.URL.exec(value);
}
return urls;
}
function parseMarkdownTitlesFromText(value, prefix, suffix) {
prefix = prefix || '##';
var reg = match('(?:^|[\\r\\s])('+prefix+'([^#></]+)' + (suffix||'') + ')');
var matches = value && reg.exec(value);
var lines = matches && [];
var res = matches && [];
while(matches) {
var line = matches[1];
if (!_.contains(lines, line)) {
lines.push(line);
res.push({
line: line,
title: matches[2]
});
}
value = value.substr(matches.index + matches[1].length + 1);
matches = value.length > 0 && reg.exec(value);
}
return res;
}
function escape(text) {
if (!text) return text;
return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function parseAsHtml(text, options) {
var content = text ? escape(text.trim()) : undefined;
if (content) {
options = options || {};
options.tagState = options.tagState || 'app.wot_lookup';
options.uidState = options.uidState || 'app.wot_identity_uid';
if (options.newLine || !angular.isDefined(options.newLine)) {
content = content.replace(/\n/g, '<br>\n');
}
// Replace URL in description
var urls = parseUrlsFromText(content);
_.forEach(urls, function(url){
// Make sure protocol is defined
var href = (url.startsWith('http://') || url.startsWith('https://')) ? url : ('http://' + url);
// Redirect URL to the function 'openLink', to open a new window if need (e.g. desktop app)
var link = '<a on-tap=\"openLink($event, \'{0}\')\" href=\"{1}\" target="_blank">{2}</a>'.format(href, href, truncUrlFilter(url));
content = content.replace(url, link);
});
// Replace hashtags
var hashTags = parseTagsFromText(content);
_.forEach(hashTags, function(tag){
var link = '<a ui-sref=\"{0}({hash: \'{1}\'})\">#{2}</a>'.format(options.tagState, tag, tag);
content = content.replace('#'+tag, link);
});
// Replace user tags
var userTags = parseTagsFromText(content, '@');
_.forEach(userTags, function(tag){
var link = '<a ui-sref=\"{0}({uid: \'{1}\'})\">@{2}</a>'.format(options.uidState, tag, tag);
content = content.replace('@'+tag, link);
});
// Replace markdown titles
var titles = parseMarkdownTitlesFromText(content, '#+[ ]*', '<br>');
_.forEach(titles, function(matches){
var size = matches.line.lastIndexOf('#', 5)+1;
content = content.replace(matches.line, '<h{0}>{1}</h{2}>'.format(size, matches.title, size));
});
}
return content;
}
function fillRecordTags(record, fieldNames) {
fieldNames = fieldNames || ['title', 'description'];
record.tags = fieldNames.reduce(function(res, fieldName) {
var value = record[fieldName];
var tags = value && parseTagsFromText(value);
return tags ? res.concat(tags) : res;
}, []);
}
function findObjectInTree(obj, attrName) {
if (!obj) return;
if (obj[attrName]) return obj[attrName];
if (Array.isArray(obj)) {
return obj.reduce(function(res, item) {
return res ? res : findObjectInTree(item, attrName);
}, false);
}
else if (typeof obj == "object") {
return _.reduce(_.keys(obj), function (res, key) {
return res ? res : findObjectInTree(obj[key], attrName);
}, false);
}
}
function postRecord(path, options) {
options = options || {};
var postRequest = that.post(path);
return function(record, params) {
if (!csWallet.isLogin()) return $q.reject('Wallet must be login before sending record to ES node');
if (options.creationTime && !record.creationTime) {
record.creationTime = moment().utc().unix();
}
// Always update the time - fix Cesium #572
// Make sure time is always > previous (required by ES node)
var now = moment().utc().unix();
record.time = (!record.time || record.time < now) ? now : (record.time+1);
var keypair = csWallet.data.keypair;
var obj = angular.copy(record);
delete obj.signature;
delete obj.hash;
obj.issuer = csWallet.data.pubkey;
if (!obj.version) {
obj.version = 2;
}
// Fill tags
if (options.tagFields) {
fillRecordTags(obj, options.tagFields);
}
var str = JSON.stringify(obj);
return CryptoUtils.util.hash(str)
.then(function(hash) {
return CryptoUtils.sign(hash, keypair)
.then(function(signature) {
// Prepend hash+signature
str = '{"hash":"{0}","signature":"{1}",'.format(hash, signature) + str.substring(1);
// Send data
return postRequest(str, params)
.then(function (id){
// Clear cache
csCache.clear(cachePrefix);
return id;
})
.catch(function(err) {
var bodyLength = that.byteCount(obj);
if (bodyLength > constants.MAX_UPLOAD_BODY_SIZE) {
throw {message: 'ERROR.ES_MAX_UPLOAD_BODY_SIZE', length: bodyLength};
}
throw err;
});
});
});
};
}
function countRecords(index, type, cacheTime) {
var getRequest = that.get("/{0}/{1}/_search?size=0".format(index, type), cacheTime);
return function(params) {
return getRequest(params)
.then(function(res) {
return res && res.hits && res.hits.total;
});
};
}
function removeRecord(index, type) {
return function(id) {
if (!csWallet.isLogin()) return $q.reject('Wallet must be login before sending record to ES node');
var obj = {
version: 2,
index: index,
type: type,
id: id,
issuer: csWallet.data.pubkey,
time: moment().utc().unix()
};
var str = JSON.stringify(obj);
return CryptoUtils.util.hash(str)
.then(function(hash) {
return CryptoUtils.sign(hash, csWallet.data.keypair)
.then(function(signature) {
// Prepend hash+signature
str = '{"hash":"{0}","signature":"{1}",'.format(hash, signature) + str.substring(1);
// Send data
return that.post('/history/delete')(str)
.then(function (id) {
return id;
});
});
});
};
}
function addLike(index, type) {
var postRequest = postRecord('/{0}/{1}/:id/_like'.format(index, type));
return function(id, options) {
options = options || {};
options.kind = options.kind && options.kind.toUpperCase() || 'LIKE';
if (!csWallet.isLogin()) return $q.reject('Wallet must be login before sending record to ES node');
var obj = {
version: 2,
index: index,
type: type,
id: id,
kind: options.kind
};
if (options.comment) obj.comment = options.comment;
if (angular.isDefined(options.level)) obj.level = options.level;
return postRequest(obj);
};
}
function toggleLike(index, type) {
var getIdsRequest = getLikeIds(index, type);
var addRequest = addLike(index, type);
var removeRequest = removeRecord('like', 'record');
return function(id, options) {
options = options || {};
options.kind = options.kind || 'LIKE';
if (!csWallet.isLogin()) return $q.reject('Wallet must be login before sending record to ES node');
return getIdsRequest(id, {kind: options.kind, issuer: csWallet.data.pubkey})
.then(function(existingLikeIds) {
// User already like: so remove it
if (existingLikeIds && existingLikeIds.length) {
return $q.all(_.map(existingLikeIds, function(likeId) {
return removeRequest(likeId)
}))
// Return the deletion, as a delta
.then(function() {
return -1 * existingLikeIds.length;
});
}
// User not like, so add it
else {
return addRequest(id, options)
// Return the insertion, as a delta
.then(function() {
return +1;
});
}
});
}
}
function getLikeIds(index, type) {
var searchRequest = that.get('/like/record/_search?_source=false&q=:q');
var baseQueryString = 'index:{0} AND type:{1} AND id:'.format(index, type);
return function(id, options) {
options = options || {};
options.kind = options.kind || 'LIKE';
var queryString = baseQueryString + id;
if (options.kind) queryString += ' AND kind:' + options.kind.toUpperCase();
if (options.issuer) queryString += ' AND issuer:' + options.issuer;
return searchRequest({q: queryString})
.then(function(res) {
return (res && res.hits && res.hits.hits || []).map(function(hit) {
return hit._id;
});
});
}
}
function removeLike(index, type) {
var removeRequest = removeRecord('like', 'record');
return function(id) {
if (id) {
return removeRequest(id);
}
// Get the ID
else {
}
}
}
function countLikes(index, type) {
var searchRequest = that.post("/like/record/_search");
return function(id, options) {
options = options || {};
options.kind = options.kind && options.kind.toUpperCase() || 'LIKE';
// Get level (default to true when kind=star, otherwise false)
options.level = angular.isDefined(options.level) ? options.level : (options.kind === 'STAR');
var request = {
query: {
bool: {
filter: [
{term: {index: index}},
{term: {type: type}},
{term: {id: id}},
{term: {kind: options.kind.toUpperCase()}}
]
}
},
size: 0
};
// To known if the user already like, add 'should' on issuer, and limit to 1
if (options.issuer) {
request.query.bool.should = {term: {issuer: options.issuer}};
request.size = 1;
request._source = ["issuer"];
}
// Computre level AVG and issuer level
if (options.level) {
request.aggs = {
level_sum: {
sum: {field: "level"}
}
};
request._source = request._source || [];
request._source.push("level");
}
return searchRequest(request)
.then(function(res) {
var hits = res && res.hits;
// Check is issuer is return (because of size=1 and should filter)
var issuerHitIndex = hits && options.issuer ? _.findIndex(hits.hits, function(hit) {
return hit._source.issuer === options.issuer;
}) : -1;
var result = {
total: hits && hits.total || 0,
wasHit: issuerHitIndex !== -1 || false,
wasHitId: issuerHitIndex !== -1 && hits.hits[issuerHitIndex]._id || false
};
// Set level values (e.g. is kind=star)
if (options.level) {
result.level= issuerHitIndex !== -1 ? hits.hits[issuerHitIndex]._source.level : undefined;
result.levelSum = res.aggregations && res.aggregations.level_sum.value || 0;
// Compute the AVG (rounded at a precision of 0.5)
result.levelAvg = result.total && (Math.floor((result.levelSum / result.total + 0.5) * 10) / 10 - 0.5) || 0;
}
return result;
})
}
}
that.image = {};
function imageFromAttachment(attachment) {
if (!attachment || !attachment._content_type || !attachment._content || attachment._content.length === 0) {
return null;
}
var image = {
src: "data:" + attachment._content_type + ";base64," + attachment._content
};
if (attachment._title) {
image.title = attachment._title;
}
if (attachment._name) {
image.name = attachment._name;
}
return image;
}
function imageToAttachment(image) {
if (!image || !image.src) return null;
var match = regexp.IMAGE_SRC.exec(image.src);
if (!match) return null;
var attachment = {
_content_type: match[1],
_content: match[2]
};
if (image.title) {
attachment._title = image.title;
}
if (image.name) {
attachment._name = image.name;
}
return attachment;
}
/**
* This will create a image (src, title, name) using the _content is present, or computing a image URL to the ES node
* @param host
* @param port
* @param hit
* @param imageField
* @returns {{}}
*/
that.image.fromHit = function(hit, imageField) {
if (!hit || !hit._source) return;
var attachment = hit._source[imageField];
if (!attachment || !attachment._content_type || !attachment._content_type.startsWith("image/")) return;
var image = {};
// If full content: then use it directly
if (attachment._content) {
image.src = "data:" + attachment._content_type + ";base64," + attachment._content;
}
// Compute an url
else {
var extension = attachment._content_type.substr(6);
var path = [hit._index, hit._type, hit._id, '_image', imageField].join('/');
path = '/' + path + '.' + extension;
image.src = that.getUrl(path);
}
if (attachment._title) {
image.title = attachment._title;
}
if (attachment._name) {
image.name = attachment._name;
}
return image;
};
function parseEndPoint(endpoint) {
var matches = regexp.API_ENDPOINT.exec(endpoint);
if (!matches) return;
return {
"api": matches[1] || '',
"dns": matches[2] || '',
"ipv4": matches[3] || '',
"ipv6": matches[4] || '',
"port": matches[5] || 80,
"path": matches[6] || '',
"useSsl": matches[5] == 443
};
}
function emptyHit() {
return {
_id: null,
_index: null,
_type: null,
_version: null,
_source: {}
};
}
function addListeners() {
// Watch some service events
listeners = [
csSettings.api.data.on.reset($rootScope, onSettingsReset, that)
];
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
// Define events
that.api.registerEvent('node', 'start');
that.api.registerEvent('node', 'stop');
var exports = {
getServer: csHttp.getServer,
node: {
summary: that.get('/node/summary'),
parseEndPoint: parseEndPoint,
same: isSameNode,
sameAsSettings: isSameNodeAsSettings,
isFallback: isFallbackNode
},
websocket: {
changes: that.wsChanges,
block: that.ws('/ws/block'),
peer: that.ws('/ws/peer')
},
wot: {
member: {
uids : that.get('/wot/members')
}
},
network: {
peering: {
self: that.get('/network/peering')
},
peers: that.get('/network/peers')
},
record: {
post: postRecord,
remove: removeRecord,
count : countRecords
},
like: {
toggle: toggleLike,
add: addLike,
remove: removeLike,
count: countLikes
},
image: {
fromAttachment: imageFromAttachment,
toAttachment: imageToAttachment
},
hit: {
empty: emptyHit
},
util: {
parseTags: parseTagsFromText,
parseAsHtml: parseAsHtml,
findObjectInTree: findObjectInTree
},
cache: csHttp.cache,
constants: constants
};
exports.constants.regexp = regexp;
angular.merge(that, exports);
}
var service = new EsHttp(undefined, undefined, undefined, true);
service.instance = function(host, port, useSsl, useCache) {
return new EsHttp(host, port, useSsl, useCache);
};
service.lightInstance = function(host, port, useSsl, timeout) {
port = port || 80;
useSsl = angular.isDefined(useSsl) ? useSsl : (+port === 443);
function countHits(path, params) {
return csHttp.get(host, port, path)(params)
.then(function(res) {
return res && res.hits && res.hits.total;
});
}
function countRecords(index, type) {
return countHits("/{0}/{1}/_search?size=0".format(index, type));
}
function countSubscriptions(params) {
var queryString = _.keys(params||{}).reduce(function(res, key) {
return (res && (res + " AND ") || "") + key + ":" + params[key];
}, '');
return countHits("/subscription/record/_search?size=0&q=" + queryString);
}
return {
host: host,
port: port,
useSsl: useSsl,
node: {
summary: csHttp.getWithCache(host, port, '/node/summary', useSsl, csHttp.cache.LONG, false, timeout)
},
network: {
peering: {
self: csHttp.get(host, port, '/network/peering', useSsl, timeout)
},
peers: csHttp.get(host, port, '/network/peers', useSsl, timeout)
},
blockchain: {
current: csHttp.get(host, port, '/blockchain/current?_source=number,hash,medianTime', useSsl, timeout)
},
record: {
count: countRecords
},
subscription: {
count: countSubscriptions
}
};
};
return service;
}])
;
angular.module('cesium.es.settings.services', ['cesium.services', 'cesium.es.http.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('esSettings');
}
}])
.factory('esSettings', ['$rootScope', '$q', '$timeout', 'Api', 'esHttp', 'csConfig', 'csSettings', 'CryptoUtils', 'Device', 'UIUtils', 'csWallet', function($rootScope, $q, $timeout, Api, esHttp,
csConfig, csSettings, CryptoUtils, Device, UIUtils, csWallet) {
'ngInject';
var
SETTINGS_SAVE_SPEC = {
includes: ['locale', 'useRelative', 'useLocalStorage', 'expertMode', 'logoutIdle'],
excludes: ['newIssueVersion', 'timeout', 'cacheTimeMs', 'time', 'login', 'build'],
plugins: {
es: {
excludes: ['enable', 'host', 'port', 'useSsl', 'fallbackNodes', 'minVersion', 'document', 'maxUploadBodySize', 'defaultCountry'],
notifications: {
}
}
},
helptip: {
excludes: ['installDocUrl']
}
},
fixedSettings = {
plugins: {
es: {
minVersion: "1.2.0",
document: {
index: 'user',
type: 'profile'
}
}
}
},
defaultSettings = angular.merge({
plugins: {
es: {
askEnable: false,
notifications: {
readTime: true,
txSent: true,
txReceived: true,
certSent: true,
certReceived: true
},
invitations: {
readTime: true
},
defaultCountry: undefined,
enableGoogleApi: false,
googleApiKey: undefined,
wot: {
enableMixedSearch: true
},
geoDistance: '20km'
}
}
},
fixedSettings,
{plugins: {es: csConfig.plugins && csConfig.plugins.es || {}}}
),
that = this,
api = new Api('esSettings'),
previousRemoteData,
listeners,
ignoreSettingsChanged = false
;
that.api = api;
that.get = esHttp.get('/user/settings/:id');
that.add = esHttp.record.post('/user/settings');
that.update = esHttp.record.post('/user/settings/:id/_update');
that.isEnable = function() {
return csSettings.data.plugins &&
csSettings.data.plugins.es &&
csSettings.data.plugins.es.enable &&
!!csSettings.data.plugins.es.host;
};
that.setPluginSaveSpecs = function(pluginName, saveSpecs) {
if (pluginName && saveSpecs) {
SETTINGS_SAVE_SPEC.plugins[pluginName] = angular.copy(saveSpecs);
}
};
function copyUsingSpec(data, copySpec) {
var result = {};
// Add implicit includes
if (copySpec.includes) {
_.forEach(_.keys(copySpec), function(key) {
if (key !== "includes" && key !== "excludes") {
copySpec.includes.push(key);
}
});
}
_.forEach(_.keys(data), function(key) {
if ((!copySpec.includes || _.contains(copySpec.includes, key)) &&
(!copySpec.excludes || !_.contains(copySpec.excludes, key))) {
if (data[key] && (typeof data[key] == 'object') &&
copySpec[key] && (typeof copySpec[key] == 'object')) {
result[key] = copyUsingSpec(data[key], copySpec[key]); // Recursive call
}
else {
result[key] = data[key];
}
}
});
return result;
}
// Load settings
function loadSettings(pubkey, keypair) {
var now = Date.now();
return $q.all([
CryptoUtils.box.keypair.fromSignKeypair(keypair),
that.get({id: pubkey})
.catch(function(err){
if (err && err.ucode && err.ucode == 404) {
return null; // not found
}
else {
throw err;
}
})])
.then(function(res) {
var boxKeypair = res[0];
res = res[1];
if (!res || !res._source) {
return;
}
var record = res._source;
// Do not apply if same version
if (record.time === csSettings.data.time) {
console.debug('[ES] [settings] Loaded user settings in '+ (Date.now()-now) +'ms (no update need)');
return;
}
var nonce = CryptoUtils.util.decode_base58(record.nonce);
// Decrypt settings content
return CryptoUtils.box.open(record.content, nonce, boxKeypair.boxPk, boxKeypair.boxSk)
.then(function(json) {
var settings = JSON.parse(json || '{}');
settings.time = record.time;
console.debug('[ES] [settings] Loaded user settings in '+ (Date.now()-now) +'ms');
//console.debug(settings);
return settings;
})
// if error: skip stored content
.catch(function(err){
console.error('[ES] [settings] Could not read stored settings: ' + (err && err.message || 'decryption error'));
// make sure to remove time, to be able to save it again
delete csSettings.data.time;
return null;
});
});
}
function onSettingsReset(data, deferred) {
deferred = deferred || $q.defer();
angular.merge(data, defaultSettings);
deferred.resolve(data);
return deferred.promise;
}
function onWalletLogin(data, deferred) {
deferred = deferred || $q.defer();
if (!data || !data.pubkey || !data.keypair) {
deferred.resolve();
return deferred.promise;
}
// Waiting to load crypto libs
if (!CryptoUtils.isLoaded()) {
console.debug('[ES] [settings] Waiting crypto lib loading...');
return $timeout(function() {
return onWalletLogin(data, deferred);
}, 50);
}
console.debug('[ES] [settings] Loading user settings...');
// Load settings
loadSettings(data.pubkey, data.keypair)
.then(function(settings) {
if (!settings) return; // not found or up to date
angular.merge(csSettings.data, settings);
// Remember for comparison
previousRemoteData = settings;
console.debug('[ES] [settings] Successfully load settings from ES');
return storeSettingsLocally();
})
.then(function() {
deferred.resolve(data);
})
.catch(function(err){
deferred.reject(err);
});
return deferred.promise;
}
// Listen for settings changed
function onSettingsChanged(data) {
// avoid recursive call, because storeSettingsLocally() could emit event again
if (ignoreSettingsChanged) return;
var wasEnable = listeners && listeners.length > 0;
refreshState();
var isEnable = that.isEnable();
if (csWallet.isLogin()) {
if (!wasEnable && isEnable) {
onWalletLogin(csWallet.data);
}
else {
storeSettingsRemotely(data);
}
}
}
function storeSettingsLocally() {
if (ignoreSettingsChanged) return $q.when();
ignoreSettingsChanged = true;
return csSettings.store()
.then(function(){
ignoreSettingsChanged = false;
})
.catch(function(err) {
ignoreSettingsChanged = false;
throw err;
});
}
function storeSettingsRemotely(data) {
if (!csWallet.isLogin()) return $q.when();
var filteredData = copyUsingSpec(data, SETTINGS_SAVE_SPEC);
if (previousRemoteData && angular.equals(filteredData, previousRemoteData)) {
return $q.when();
}
// Waiting to load crypto libs
if (!CryptoUtils.isLoaded()) {
console.debug('[ES] [settings] Waiting crypto lib loading...');
return $timeout(function() {
return storeSettingsRemotely();
}, 50);
}
var time = esHttp.date.now();
console.debug('[ES] [settings] Saving user settings... at time ' + time);
return $q.all([
CryptoUtils.box.keypair.fromSignKeypair(csWallet.data.keypair),
CryptoUtils.util.random_nonce()
])
.then(function(res) {
var boxKeypair = res[0];
var nonce = res[1];
var record = {
issuer: csWallet.data.pubkey,
nonce: CryptoUtils.util.encode_base58(nonce),
time: time
};
//console.debug("Will store settings remotely: ", filteredData);
var json = JSON.stringify(filteredData);
return CryptoUtils.box.pack(json, nonce, boxKeypair.boxPk, boxKeypair.boxSk)
.then(function(cypherText) {
record.content = cypherText;
// create or update
return !data.time ?
that.add(record) :
that.update(record, {id: record.issuer});
});
})
.then(function() {
// Update settings version, then store (on local store only)
csSettings.data.time = time;
previousRemoteData = filteredData;
console.debug('[ES] [settings] Saved user settings in ' + (esHttp.date.now() - time) + 'ms');
return storeSettingsLocally();
})
.catch(function(err) {
console.error(err);
throw err;
})
;
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
// Extend csWallet.login()
listeners = [
csSettings.api.data.on.reset($rootScope, onSettingsReset, this),
csWallet.api.data.on.login($rootScope, onWalletLogin, this)
];
}
function refreshState() {
var enable = that.isEnable();
// Disable
if (!enable && listeners && listeners.length > 0) {
console.debug("[ES] [settings] Disable");
removeListeners();
// Force ES node to stop
return esHttp.stop()
.then(function() {
// Emit event
api.state.raise.changed(enable);
});
}
// Enable
else if (enable && (!listeners || listeners.length === 0)) {
return esHttp.start()
.then(function(started) {
if (!started) {
// TODO : alert user ?
console.error('[ES] node could not be started !!');
}
else {
console.debug("[ES] [settings] Enable");
addListeners();
if (csWallet.isLogin()) {
return onWalletLogin(csWallet.data)
.then(function() {
// Emit event
api.state.raise.changed(enable);
});
}
else {
// Emit event
api.state.raise.changed(enable);
}
}
});
}
}
api.registerEvent('state', 'changed');
csSettings.ready().then(function() {
csSettings.api.data.on.changed($rootScope, onSettingsChanged, this);
esHttp.api.node.on.stop($rootScope, function() {
previousRemoteData = null;
}, this);
return refreshState();
});
return that;
}]);
angular.module('cesium.es.social.services', ['cesium.es.crypto.services'])
.factory('SocialUtils', ['$filter', '$q', 'CryptoUtils', 'BMA', 'csWallet', 'esCrypto', function($filter, $q, CryptoUtils, BMA, csWallet, esCrypto) {
'ngInject';
function SocialUtils() {
var
regexp = {
URI: "([a-zAZ0-9]+)://[ a-zA-Z0-9-_:/;*?!^\\+=@&~#|<>%.]+",
EMAIL: "[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$",
PHONE: "[+]?[0-9. ]{9,15}",
socials: {
facebook: "https?://((fb.me)|((www.)?facebook.com))",
twitter: "https?://(www.)?twitter.com",
googleplus: "https?://plus.google.com(/u)?",
youtube: "https?://(www.)?youtube.com",
github: "https?://(www.)?github.com",
tumblr: "https?://(www.)?tumblr.com",
snapchat: "https?://(www.)?snapchat.com",
linkedin: "https?://(www.)?linkedin.com",
vimeo: "https?://(www.)?vimeo.com",
instagram: "https?://(www.)?instagram.com",
wordpress: "https?://([a-z]+)?wordpress.com",
diaspora: "https?://(www.)?((diaspora[-a-z]+)|(framasphere)).org",
duniter: "duniter://[a-zA-Z0-9-_:/;*?!^\\+=@&~#|<>%.]+",
bitcoin: "bitcoin://[a-zA-Z0-9-_:/;*?!^\\+=@&~#|<>%.]+",
curve25519: "curve25519://(" + BMA.constants.regexp.PUBKEY + "):([a-zA-Z0-9]+)@([a-zA-Z0-9-_:/;*?!^\\+=@&~#|<>%.]+)"
}
}
;
function exact(regexpContent) {
return new RegExp("^" + regexpContent + "$");
}
regexp.URI = exact(regexp.URI);
regexp.EMAIL = exact(regexp.EMAIL);
regexp.PHONE = exact(regexp.PHONE);
_.keys(regexp.socials).forEach(function(key){
regexp.socials[key] = exact(regexp.socials[key]);
});
function getTypeFromUrl(url){
var type;
if (regexp.URI.test(url)) {
var protocol = regexp.URI.exec(url)[1];
var urlToMatch = url;
if (protocol == 'http' || protocol == 'https') {
var slashPathIndex = url.indexOf('/', protocol.length + 3);
if (slashPathIndex > 0) {
urlToMatch = url.substring(0, slashPathIndex);
}
}
//console.log("match URI, try to match: " + urlToMatch);
_.keys(regexp.socials).forEach(function(key){
if (regexp.socials[key].test(urlToMatch)) {
type = key;
return false; // stop
}
});
if (!type) {
type = 'web';
}
}
else if (regexp.EMAIL.test(url)) {
type = 'email';
}
else if (regexp.PHONE.test(url)) {
type = 'phone';
}
if (!type) {
console.warn("[ES] [social] Unable to detect type of social URL: " + url);
}
return type;
}
function getFromUrl(url) {
url = url ? url.trim() : url;
if (url && url.length > 0) {
if (url.startsWith('www.')) {
url = 'http://' + url;
}
return {
type: getTypeFromUrl(url),
url: url
};
}
return;
}
function reduceArray(socials) {
if (!socials || !socials.length) return [];
var map = {};
socials.forEach(function(social) {
if (social.type == 'curve25519') {
delete social.issuer;
if (social.valid) {
angular.merge(social, getFromUrl(social.url));
}
}
else {
// Retrieve object from URL, to get the right type (e.g. if new regexp)
social = getFromUrl(social.url);
}
if (social) {
var id = $filter('formatSlug')(social.url);
map[id] = social;
}
});
return _.values(map);
}
function createSocialForEncryption(recipient, dataToEncrypt) {
return {
recipient: recipient,
type: 'curve25519',
url: dataToEncrypt
};
}
function openArray(socials, issuer, recipient) {
recipient = recipient || csWallet.data.pubkey;
// Waiting to load crypto libs
if (!CryptoUtils.isLoaded()) {
console.debug('[socials] Waiting crypto lib loading...');
return $timeout(function() {
return openArray(socials, issuer, recipient);
}, 100);
}
var socialsToDecrypt = _.filter(socials||[], function(social){
var matches = social.url && social.type == 'curve25519' && regexp.socials.curve25519.exec(social.url);
if (!matches) return false;
social.recipient = matches[1];
social.nonce = matches[2];
social.url = matches[3];
social.issuer = issuer;
social.valid = (social.recipient === recipient);
return social.valid;
});
if (!socialsToDecrypt.length) return $q.when(reduceArray(socials));
return esCrypto.box.open(socialsToDecrypt, undefined/*=wallet keypair*/, 'issuer', 'url')
.then(function() {
// return all socials (encrypted or not)
return reduceArray(socials);
});
}
function packArray(socials) {
// Waiting to load crypto libs
if (!CryptoUtils.isLoaded()) {
console.debug('[socials] Waiting crypto lib loading...');
return $timeout(function() {
return packArray(socials);
}, 100);
}
var socialsToEncrypt = _.filter(socials||[], function(social){
return social.type == 'curve25519' && social.url && social.recipient;
});
if (!socialsToEncrypt.length) return $q.when(socials);
return CryptoUtils.util.random_nonce()
.then(function(nonce) {
return $q.all(socialsToEncrypt.reduce(function(res, social) {
return res.concat(esCrypto.box.pack(social, undefined/*=wallet keypair*/, 'recipient', 'url', nonce));
}, []));
})
.then(function(res){
return res.reduce(function(res, social) {
return res.concat({
type: 'curve25519',
url: 'curve25519://{0}:{1}@{2}'.format(social.recipient, social.nonce, social.url)
});
}, []);
});
}
return {
get: getFromUrl,
reduce: reduceArray,
// Encryption
createForEncryption: createSocialForEncryption,
open: openArray,
pack: packArray
};
}
var service = SocialUtils();
service.instance = SocialUtils;
return service;
}])
;
angular.module('cesium.es.crypto.services', ['ngResource', 'cesium.services'])
.factory('esCrypto', ['$q', '$rootScope', 'CryptoUtils', function($q, $rootScope, CryptoUtils) {
'ngInject';
function getBoxKeypair(keypair) {
if (!keypair) {
throw new Error('Missing keypair');
}
if (keypair.boxPk && keypair.boxSk) {
return $q.when(keypair);
}
return $q.all([
CryptoUtils.box.keypair.skFromSignSk(keypair.signSk),
CryptoUtils.box.keypair.pkFromSignPk(keypair.signPk)
])
.then(function(res) {
return {
boxSk: res[0],
boxPk: res[1]
};
});
}
function packRecordFields(record, keypair, recipientFieldName, cypherFieldNames, nonce) {
recipientFieldName = recipientFieldName || 'recipient';
if (!record[recipientFieldName]) {
return $q.reject({message:'ES_WALLET.ERROR.RECIPIENT_IS_MANDATORY'});
}
cypherFieldNames = cypherFieldNames || 'content';
if (typeof cypherFieldNames == 'string') {
cypherFieldNames = [cypherFieldNames];
}
// Work on a copy, to keep the original record (as it could be use again - fix #382)
record = angular.copy(record);
// Get recipient
var recipientPk = CryptoUtils.util.decode_base58(record[recipientFieldName]);
return $q.all([
getBoxKeypair(keypair),
CryptoUtils.box.keypair.pkFromSignPk(recipientPk),
nonce ? $q.when(nonce) : CryptoUtils.util.random_nonce()
])
.then(function(res) {
//var senderSk = res[0];
var boxKeypair = res[0];
var senderSk = boxKeypair.boxSk;
var boxRecipientPk = res[1];
var nonce = res[2];
return $q.all(
cypherFieldNames.reduce(function(res, fieldName) {
if (!record[fieldName]) return res; // skip undefined fields
return res.concat(
CryptoUtils.box.pack(record[fieldName], nonce, boxRecipientPk, senderSk)
);
}, []))
.then(function(cypherTexts){
// Replace field values with cypher texts
var i = 0;
_.forEach(cypherFieldNames, function(cypherFieldName) {
if (!record[cypherFieldName]) {
// Force undefined fields to be present in object
// This is better for ES storage, that always works on lazy update mode
record[cypherFieldName] = null;
}
else {
record[cypherFieldName] = cypherTexts[i++];
}
});
// Set nonce
record.nonce = CryptoUtils.util.encode_base58(nonce);
return record;
});
});
}
function openRecordFields(records, keypair, issuerFieldName, cypherFieldNames) {
issuerFieldName = issuerFieldName || 'issuer';
cypherFieldNames = cypherFieldNames || 'content';
if (typeof cypherFieldNames == 'string') {
cypherFieldNames = [cypherFieldNames];
}
var now = new Date().getTime();
var issuerBoxPks = {}; // a map used as cache
var jobs = [getBoxKeypair(keypair)];
return $q.all(records.reduce(function(jobs, message) {
var issuer = message[issuerFieldName];
if (!issuer) {throw 'Record has no ' + issuerFieldName;}
if (issuerBoxPks[issuer]) return res;
return jobs.concat(
CryptoUtils.box.keypair.pkFromSignPk(CryptoUtils.util.decode_base58(issuer))
.then(function(issuerBoxPk) {
issuerBoxPks[issuer] = issuerBoxPk; // fill box pk cache
}));
}, jobs))
.then(function(res){
var boxKeypair = res[0];
return $q.all(records.reduce(function(jobs, record) {
var issuerBoxPk = issuerBoxPks[record[issuerFieldName]];
var nonce = CryptoUtils.util.decode_base58(record.nonce);
record.valid = true;
return jobs.concat(
cypherFieldNames.reduce(function(res, cypherFieldName) {
if (!record[cypherFieldName]) return res;
return res.concat(CryptoUtils.box.open(record[cypherFieldName], nonce, issuerBoxPk, boxKeypair.boxSk)
.then(function(text) {
record[cypherFieldName] = text;
})
.catch(function(err){
console.error(err);
console.warn('[ES] [crypto] a record may have invalid cypher ' + cypherFieldName);
record.valid = false;
}));
}, []));
}, []));
})
.then(function() {
console.debug('[ES] [crypto] All record decrypted in ' + (new Date().getTime() - now) + 'ms');
return records;
});
}
// exports
return {
box: {
getKeypair: getBoxKeypair,
pack: packRecordFields,
open: openRecordFields
}
};
}])
;
angular.module('cesium.es.profile.services', ['cesium.services', 'cesium.es.http.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('esProfile');
}
}])
.factory('esProfile', ['$rootScope', '$q', 'esHttp', 'SocialUtils', 'csWot', 'csWallet', 'csPlatform', 'esSettings', function($rootScope, $q, esHttp, SocialUtils, csWot, csWallet, csPlatform, esSettings) {
'ngInject';
var
that = this,
listeners;
that.raw = {
getFields: esHttp.get('/user/profile/:id?&_source_exclude=avatar._content&_source=:fields'),
get: esHttp.get('/user/profile/:id?&_source_exclude=avatar._content'),
getAll: esHttp.get('/user/profile/:id'),
search: esHttp.post('/user/profile/_search'),
mixedSearch: esHttp.post('/user,page/profile,record/_search'),
countLikes: esHttp.like.count('user', 'profile')
};
function getAvatarAndName(pubkey) {
return that.raw.getFields({id: pubkey, fields: 'title,avatar._content_type'})
.then(function(res) {
var profile;
if (res && res._source) {
// name
profile = {name: res._source.title};
// avatar
profile.avatar = esHttp.image.fromHit(res, 'avatar');
}
return profile;
})
.catch(function(err){
// no profile defined
if (err && err.ucode && err.ucode == 404) {
return null;
}
else {
throw err;
}
});
}
function getProfile(pubkey, options) {
options = options || {};
var get = options.raw ? that.raw.getAll : that.raw.get;
return get({id: pubkey})
.then(function(res) {
if (!res || !res.found || !res._source) return undefined;
var profile = {
name: res._source.title,
source: res._source
};
// Avoid too long name (workaround for #308)
if (profile.name && profile.name.length > 30) {
// now using truncText filter
// profile.name = profile.name.substr(0, 27) + '...';
}
// avatar
profile.avatar = esHttp.image.fromHit(res, 'avatar');
// description
if (!options.raw) {
profile.description = esHttp.util.parseAsHtml(profile.source.description);
}
// Social url must be unique in socials links - Workaround for issue #306:
if (profile.source.socials && profile.source.socials.length) {
profile.source.socials = _.uniq(profile.source.socials, false, function (social) {
return social.url;
});
}
if (!csWallet.isLogin()) {
// Exclude crypted socials
profile.source.socials = _.filter(profile.source.socials, function(social) {
return social.type != 'curve25519';
});
}
else {
// decrypt socials (if login)
return SocialUtils.open(profile.source.socials, pubkey)
.then(function(){
//console.log(profile.source.socials);
// Exclude invalid decrypted socials
//profile.source.socials = _.where(profile.source.socials, {valid: true});
return profile;
});
}
return profile;
})
.catch(function(err){
// no profile defined
if (err && err.ucode && err.ucode == 404) {
return null;
}
else {
throw err;
}
});
}
function fillAvatars(datas, pubkeyAtributeName) {
return onWotSearch(null, datas, pubkeyAtributeName);
}
function _fillSearchResultFromHit(data, hit, avatarFieldName) {
data.avatar = data.avatar || esHttp.image.fromHit(hit, avatarFieldName||'avatar');
// name (basic or highlighted)
data.name = hit._source.title;
// Avoid too long name (workaround for #308)
if (data.name && data.name.length > 30) {
// now using truncText filter
//data.name = data.name.substr(0, 27) + '...';
}
data.description = hit._source.description || data.description;
data.city = hit._source.city || data.city;
if (hit.highlight) {
if (hit.highlight.title) {
data.name = hit.highlight.title[0];
}
if (hit.highlight.tags) {
data.tags = hit.highlight.tags.reduce(function(res, tag){
return res.concat(tag.replace('<em>', '').replace('</em>', ''));
},[]);
}
}
}
function _fillSearchResultsFromHits(datas, res, dataByPubkey, pubkeyAtributeName) {
if (!res || !res.hits || !res.hits.total) return datas;
var indices = {};
dataByPubkey = dataByPubkey || {};
pubkeyAtributeName = pubkeyAtributeName || 'pubkey';
var values;
_.forEach(res.hits.hits, function (hit) {
var avatarFieldName = 'avatar';
// User profile
if (hit._index === "user") {
values = dataByPubkey && dataByPubkey[hit._id];
if (!values) {
var value = {};
value[pubkeyAtributeName] = hit._id;
values = [value];
datas.push(value);
}
}
// Page or group
else if (hit._index !== "user") {
if (!indices[hit._index]) {
indices[hit._index] = true;
// add a separator
datas.push({
id: 'divider-' + hit._index,
divider: true,
index: hit._index
});
}
var item = {
id: hit._index + '-' + hit._id, // unique id in list
index: hit._index,
templateUrl: 'plugins/es/templates/wot/lookup_item_{0}.html'.format(hit._index),
state: 'app.view_{0}'.format(hit._index),
stateParams: {id: hit._id, title: hit._source.title},
creationTime: hit._source.creationTime,
memberCount: hit._source.memberCount,
type: hit._source.type
};
values = [item];
datas.push(item);
avatarFieldName = 'thumbnail';
}
avatar = esHttp.image.fromHit(hit, avatarFieldName);
_.forEach(values, function (data) {
data.avatar = avatar;
_fillSearchResultFromHit(data, hit);
});
});
// Add divider on top
if (_.keys(indices).length) {
datas.splice(0, 0, {
id: 'divider-identities',
divider: true,
index: 'profile'
});
}
}
function search(options) {
return searchText(undefined, options);
}
function searchText(text, options) {
options = options || {};
var request = {
highlight: {fields : {title : {}, tags: {}}},
from: options.from || 0,
size: options.size || 100,
_source: options._source || ["title", "avatar._content_type", "time", "city"]
};
if (!text) {
delete request.highlight; // highlight not need
request.sort = {time: 'desc'};
}
else {
request.query = {};
request.query.bool = {
should: [
{match: {title: {
query: text,
boost: 2
}}},
{prefix: {title: text}}
]
};
var tags = text ? esHttp.util.parseTags(text) : undefined;
if (tags) {
request.query.bool.should.push({terms: {tags: tags}});
}
}
if (options.mixedSearch) {
console.debug("[ES] [profile] Mixed search: enable");
if (text) {
request.indices_boost = {
"user" : 100,
"page" : 1,
"group" : 0.01
};
}
request._source = request._source.concat(["description","creationTime", "membersCount", "type"]);
}
var search = options.mixedSearch ? that.raw.mixedSearch : that.raw.search;
return search(request)
.then(function(res) {
var result = [];
_fillSearchResultsFromHits(result, res);
return result;
});
}
function onWotSearch(text, datas, pubkeyAtributeName, deferred) {
deferred = deferred || $q.defer();
if (!text && (!datas || !datas.length)) {
deferred.resolve(datas);
return deferred.promise;
}
pubkeyAtributeName = pubkeyAtributeName || 'pubkey';
text = text ? text.toLowerCase().trim() : text;
var dataByPubkey;
var tags = text ? esHttp.util.parseTags(text) : undefined;
var request = {
query: {},
highlight: {fields : {title : {}, tags: {}}},
from: 0,
size: 100,
_source: ["title", "avatar._content_type"]
};
// TODO: uncomment
//var mixedSearch = text && esSettings.wot.isMixedSearchEnable();
var mixedSearch = false;
if (mixedSearch) {
request._source = request._source.concat(["description", "city", "creationTime", "membersCount", "type"]);
console.debug("[ES] [profile] Mixed search: enable");
}
if (datas.length > 0) {
// collect pubkeys and fill values map
dataByPubkey = {};
_.forEach(datas, function(data) {
var pubkey = data[pubkeyAtributeName];
if (pubkey) {
var values = dataByPubkey[pubkey];
if (!values) {
values = [data];
dataByPubkey[pubkey] = values;
}
else {
values.push(data);
}
}
});
var pubkeys = _.keys(dataByPubkey);
// Make sure all results will be return
request.size = (pubkeys.length <= request.size) ? request.size : pubkeys.length;
if (!text) {
delete request.highlight; // highlight not need
request.query.constant_score = {
filter: {
terms : {_id : pubkeys}
}
};
}
else {
request.query.constant_score = {
filter: {bool: {should: [
{terms : {_id : pubkeys}},
{bool: {
must: [
{match: {title: {query: text, boost: 2}}},
{prefix: {title: text}}
]}
}
]}}
};
if (tags) {
request.query.constant_score.filter.bool.should.push({terms: {tags: tags}});
}
}
}
else if (text){
request.query.bool = {
should: [
{match: {title: {
query: text,
boost: 2
}}},
{prefix: {title: text}}
]
};
if (tags) {
request.query.bool.should.push({terms: {tags: tags}});
}
}
else {
// nothing to search: stop here
deferred.resolve(datas);
return deferred.promise;
}
if (text && mixedSearch) {
request.indices_boost = {
"user" : 100,
"page" : 1,
"group" : 0.01
};
}
var search = mixedSearch ? that.raw.mixedSearch : that.raw.search;
search(request)
.then(function(res) {
_fillSearchResultsFromHits(datas, res, dataByPubkey, pubkeyAtributeName);
deferred.resolve(datas);
})
.catch(function(err){
if (err && err.ucode && err.ucode == 404) {
deferred.resolve(datas);
}
else {
deferred.reject(err);
}
});
return deferred.promise;
}
function onWotLoad(data, deferred) {
deferred = deferred || $q.defer();
if (!data || !data.pubkey) {
deferred.resolve();
return deferred.promise;
}
// Load full profile
getProfile(data.pubkey)
.then(function(profile) {
if (profile) {
data.name = profile.name;
data.avatar = profile.avatar;
data.profile = profile.source;
data.profile.description = profile.description;
}
deferred.resolve(data);
})
.catch(function(err) {
deferred.reject(data);
});
return deferred.promise;
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
// Extend csWot events
listeners = [
csWot.api.data.on.load($rootScope, onWotLoad, this),
csWot.api.data.on.search($rootScope, onWotSearch, this)
];
}
function refreshState() {
var enable = esHttp.alive;
if (!enable && listeners && listeners.length > 0) {
console.debug("[ES] [profile] Disable");
removeListeners();
}
else if (enable && (!listeners || listeners.length === 0)) {
console.debug("[ES] [profile] Enable");
addListeners();
}
}
// Default actions
csPlatform.ready().then(function() {
esHttp.api.node.on.start($rootScope, refreshState, this);
esHttp.api.node.on.stop($rootScope, refreshState, this);
return refreshState();
});
return {
search: search,
searchText: searchText,
getAvatarAndName: getAvatarAndName,
get: getProfile,
add: esHttp.record.post('/user/profile', {tagFields: ['title', 'description'], creationTime: true}),
update: esHttp.record.post('/user/profile/:id/_update', {tagFields: ['title', 'description'], creationTime: true}),
remove: esHttp.record.remove("user","profile"),
avatar: esHttp.get('/user/profile/:id?_source=avatar'),
fillAvatars: fillAvatars,
like: {
toggle: esHttp.like.toggle('user', 'profile'),
add: esHttp.like.add('user', 'profile'),
remove: esHttp.like.remove('user', 'profile'),
count: that.raw.countLikes
}
};
}])
;
angular.module('cesium.es.notification.services', ['cesium.services', 'cesium.es.http.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('esNotification');
}
}])
.factory('esNotification', ['$rootScope', '$q', '$timeout', '$translate', '$state', 'esHttp', 'csConfig', 'csSettings', 'csWallet', 'csWot', 'UIUtils', 'BMA', 'CryptoUtils', 'csPlatform', 'Api', function($rootScope, $q, $timeout, $translate, $state,
esHttp, csConfig, csSettings, csWallet, csWot, UIUtils, BMA, CryptoUtils, csPlatform, Api) {
'ngInject';
var
constants = {
MESSAGE_CODES: ['MESSAGE_RECEIVED'],
INVITATION_CODES: ['INVITATION_TO_CERTIFY'],
DEFAULT_LOAD_SIZE: 20
},
fields = {
commons: ["type", "code", "params", "reference", "recipient", "time", "hash", "read_signature"]
},
that = this,
listeners,
api = new Api(this, 'esNotification')
;
constants.EXCLUDED_CODES = constants.MESSAGE_CODES.concat(constants.INVITATION_CODES);
that.raw = {
postCount: esHttp.post('/user/event/_count'),
postSearch: esHttp.post('/user/event/_search'),
postReadById: esHttp.post('/user/event/:id/_read'),
ws: {
getUserEvent: esHttp.ws('/ws/event/user/:pubkey/:locale'),
getChanges: esHttp.ws('/ws/_changes')
}
};
// Create the filter query
function createFilterQuery(pubkey, options) {
options = options || {};
options.codes = options.codes || {};
options.codes.excludes = options.codes.excludes || constants.EXCLUDED_CODES;
var query = {
bool: {
must: [
{term: {recipient: pubkey}}
]
}
};
// Includes codes
if (options.codes && options.codes.includes) {
query.bool.must.push({terms: { code: options.codes.includes}});
}
else {
// Excludes codes
var excludesCodes = [];
if (!csSettings.getByPath('plugins.es.notifications.txSent', false)) {
excludesCodes.push('TX_SENT');
}
if (!csSettings.getByPath('plugins.es.notifications.txReceived', true)) {
excludesCodes.push('TX_RECEIVED');
}
if (!csSettings.getByPath('plugins.es.notifications.certSent', false)) {
excludesCodes.push('CERT_SENT');
}
if (!csSettings.getByPath('plugins.es.notifications.certReceived', true)) {
excludesCodes.push('CERT_RECEIVED');
}
if (options.codes.excludes) {
_.forEach(options.codes.excludes, function(code) {
excludesCodes.push(code);
});
}
if (excludesCodes.length) {
query.bool.must_not = {terms: { code: excludesCodes}};
}
}
// Filter on time
if (options.readTime) {
query.bool.must.push({range: {time: {gt: options.readTime}}});
}
return query;
}
// Load unread notifications count
function loadUnreadNotificationsCount(pubkey, options) {
var request = {
query: createFilterQuery(pubkey, options)
};
// Filter unread only
request.query.bool.must.push({missing: { field : "read_signature" }});
return that.raw.postCount(request)
.then(function(res) {
return res.count;
});
}
// Load user notifications
function loadNotifications(pubkey, options) {
options = options || {};
options.from = options.from || 0;
options.size = options.size || constants.DEFAULT_LOAD_SIZE;
var request = {
query: createFilterQuery(pubkey, options),
sort : [
{ "time" : {"order" : "desc"}}
],
from: options.from,
size: options.size,
_source: fields.commons
};
return that.raw.postSearch(request)
.then(function(res) {
if (!res.hits || !res.hits.total) return [];
var notifications = res.hits.hits.reduce(function(res, hit) {
var item = new EsNotification(hit._source, markNotificationAsRead);
item.id = hit._id;
return res.concat(item);
}, []);
return csWot.extendAll(notifications);
});
}
function onNewUserEvent(event) {
if (!event || !csWallet.isLogin()) return;
// If notification is an invitation
if (_.contains(constants.INVITATION_CODES, event.code)) {
api.event.raise.newInvitation(event);
return;
}
// If notification is a message
if (_.contains(constants.MESSAGE_CODES, event.code)) {
api.event.raise.newMessage(event);
return;
}
var notification = new EsNotification(event, markNotificationAsRead);
notification.id = event.id || notification.id;
// Extend the notification entity
return csWot.extendAll([notification])
.then(function() {
if (!$rootScope.$$phase) {
$rootScope.$apply(function() {
addNewNotification(notification);
});
}
else {
addNewNotification(notification);
}
})
.then(function() {
return emitEsNotification(notification);
});
}
function addNewNotification(notification) {
csWallet.data.notifications = csWallet.data.notifications || {};
csWallet.data.notifications.unreadCount++;
api.data.raise.new(notification);
return notification;
}
function htmlToPlaintext(text) {
return text ? String(text).replace(/<[^>]*>/gm, '').replace(/&[^;]+;/gm, '') : '';
}
function emitEsNotification(notification, title) {
// If it's okay let's create a notification
$q.all([
$translate(title||'COMMON.NOTIFICATION.TITLE'),
$translate(notification.message, notification)
])
.then(function(res) {
var title = htmlToPlaintext(res[0]);
var body = htmlToPlaintext(res[1]);
var icon = notification.avatar && notification.avatar.src || './img/logo.png';
emitHtml5Notification(title, {
body: body,
icon: icon,
lang: $translate.use(),
tag: notification.id,
onclick: function() {
$rootScope.$applyAsync(function() {
if (typeof notification.markAsRead === "function") {
notification.markAsRead();
}
if (notification.state) {
$state.go(notification.state, notification.stateParams);
}
});
}
});
});
}
function emitHtml5Notification(title, options) {
// Let's check if the browser supports notifications
if (!("Notification" in window)) return;
// Let's check whether notification permissions have already been granted
if (Notification.permission === "granted") {
// If it's okay let's create a notification
var browserNotification = new Notification(title, options);
browserNotification.onclick = options.onclick || browserNotification.onclick;
}
// Otherwise, we need to ask the user for permission
else if (Notification.permission !== "denied") {
Notification.requestPermission(function (permission) {
// If the user accepts, let's create a notification
if (permission === "granted") {
emitHtml5Notification(title, options); // recursive call
}
});
}
}
// Mark a notification as read
function markNotificationAsRead(notification) {
if (notification.read || !notification.id) return; // avoid multi call
// Should never append (fix in Duniter4j issue #12)
if (!notification.id) {
console.error('[ES] [notification] Could not mark as read: no \'id\' found!', notification);
return;
}
notification.read = true;
CryptoUtils.sign(notification.hash, csWallet.data.keypair)
.then(function(signature){
return that.raw.postReadById(signature, {id:notification.id});
})
.catch(function(err) {
console.error('[ES] [notification] Error while trying to mark event as read.', err);
});
}
function onWalletReset(data) {
data.notifications = data.notifications || {};
data.notifications.unreadCount = null;
// Stop listening notification
that.raw.ws.getUserEvent().close();
}
function onWalletLogin(data, deferred) {
deferred = deferred || $q.defer();
if (!data || !data.pubkey || !data.keypair) {
deferred.resolve();
return deferred.promise;
}
console.debug('[ES] [notification] Loading count...');
var now = new Date().getTime();
// Load unread notifications count
loadUnreadNotificationsCount(
data.pubkey, {
readTime: csSettings.data.wallet ? csSettings.data.wallet.notificationReadTime : 0,
excludeCodes: constants.EXCLUDED_CODES
})
.then(function(unreadCount) {
data.notifications = data.notifications || {};
data.notifications.unreadCount = unreadCount;
// Emit HTML5 notification
if (unreadCount > 0) {
$timeout(function() {
emitEsNotification({
message: 'COMMON.NOTIFICATION.HAS_UNREAD',
count: unreadCount,
state: 'app.view_notifications'
}, 'COMMON.APP_NAME');
}, 500);
}
console.debug('[ES] [notification] Loaded count (' + unreadCount + ') in '+(new Date().getTime()-now)+'ms');
deferred.resolve(data);
})
.catch(function(err){
deferred.reject(err);
})
// Listen new events
.then(function(){
console.debug('[ES] [notification] Starting listen user event...');
var userEventWs = that.raw.ws.getUserEvent();
listeners.push(userEventWs.close);
return userEventWs.on(onNewUserEvent,
{pubkey: data.pubkey, locale: csSettings.data.locale.id}
)
.catch(function(err) {
console.error('[ES] [notification] Unable to listen user event', err);
// TODO : send a event to csHttp instead ?
// And display such connectivity errors in UI
UIUtils.alert.error('ACCOUNT.ERROR.WS_CONNECTION_FAILED');
});
});
return deferred.promise;
}
function addListeners() {
// Listen some events
listeners = [
csWallet.api.data.on.login($rootScope, onWalletLogin, this),
csWallet.api.data.on.init($rootScope, onWalletReset, this),
csWallet.api.data.on.reset($rootScope, onWalletReset, this)
];
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function refreshState() {
var enable = esHttp.alive;
if (!enable && listeners && listeners.length > 0) {
console.debug("[ES] [notification] Disable");
removeListeners();
if (csWallet.isLogin()) {
onWalletReset(csWallet.data);
}
}
else if (enable && (!listeners || listeners.length === 0)) {
console.debug("[ES] [notification] Enable");
addListeners();
if (csWallet.isLogin()) {
return onWalletLogin(csWallet.data);
}
}
}
// Register extension points
api.registerEvent('data', 'new');
api.registerEvent('event', 'newInvitation');
api.registerEvent('event', 'newMessage');
// Default actions
csPlatform.ready().then(function() {
esHttp.api.node.on.start($rootScope, refreshState, this);
esHttp.api.node.on.stop($rootScope, refreshState, this);
return refreshState();
});
// Exports
that.load = loadNotifications;
that.unreadCount = loadUnreadNotificationsCount;
that.html5 = {
emit: emitHtml5Notification
};
that.api = api;
that.websocket = {
event: that.raw.ws.getUserEvent,
change: that.raw.ws.getChanges
};
that.constants = constants;
return that;
}])
;
angular.module('cesium.es.message.services', ['ngResource', 'cesium.services', 'cesium.crypto.services', 'cesium.wot.services',
'cesium.es.http.services', 'cesium.es.wallet.services', 'cesium.es.notification.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('esMessage');
}
}])
.factory('esMessage', ['$q', '$rootScope', '$timeout', 'UIUtils', 'Api', 'CryptoUtils', 'csPlatform', 'csConfig', 'csSettings', 'esHttp', 'csWallet', 'esWallet', 'csWot', 'esNotification', function($q, $rootScope, $timeout, UIUtils, Api, CryptoUtils,
csPlatform, csConfig, csSettings, esHttp, csWallet, esWallet, csWot, esNotification) {
'ngInject';
var
constants = {
DEFAULT_LOAD_SIZE: 10
},
fields = {
commons: ["issuer", "recipient", "title", "content", "time", "nonce", "read_signature"],
notifications: ["issuer", "time", "hash", "read_signature"]
},
raw = {
postSearch: esHttp.post('/message/inbox/_search'),
postSearchByType: esHttp.post('/message/:type/_search'),
getByTypeAndId : esHttp.get('/message/:type/:id'),
postReadById: esHttp.post('/message/inbox/:id/_read')
},
listeners,
api = new Api(this, 'esMessage');
function onWalletInit(data) {
data.messages = data.messages || {};
data.messages.unreadCount = null;
}
function onWalletReset(data) {
if (data.messages) {
delete data.messages;
}
}
function onWalletLogin(data, deferred) {
deferred = deferred || $q.defer();
if (!data || !data.pubkey) {
deferred.resolve();
return deferred.promise;
}
console.debug('[ES] [message] Loading count...');
var now = new Date().getTime();
// Count unread messages
countUnreadMessages(data.pubkey)
.then(function(unreadCount){
data.messages = data.messages || {};
data.messages.unreadCount = unreadCount;
console.debug('[ES] [message] Loaded count (' + unreadCount + ') in '+(new Date().getTime()-now)+'ms');
deferred.resolve(data);
})
.catch(function(err){
console.error('Error chile counting message: ' + (err.message ? err.message : err));
deferred.resolve(data);
});
return deferred.promise;
}
function countUnreadMessages(pubkey) {
pubkey = pubkey || (csWallet.isLogin() ? csWallet.data.pubkey : pubkey);
if (!pubkey) {
throw new Error('no pubkey, and user not connected.');
}
var request = {
query: {
bool: {
must: [
{term: {recipient: pubkey}},
{missing: { field : "read_signature" }}
]
}
}
};
return esHttp.post('/message/inbox/_count')(request)
.then(function(res) {
return res.count;
});
}
// Listen message changes
function onNewMessageEvent(event) {
console.debug("[ES] [message] detected new message (from notification service)");
var notification = new EsNotification(event);
notification.issuer = notification.pubkey;
delete notification.pubkey;
csWot.extend(notification, 'issuer')
.then(function() {
csWallet.data.messages = csWallet.data.messages || {};
csWallet.data.messages.unreadCount++;
// Raise event
api.data.raise.new(notification);
});
}
function sendMessage(message) {
return csWallet.getKeypair()
.then(function(keypair) {
return doSendMessage(message, keypair)
.then(function(res){
var outbox = (csSettings.data.plugins.es.message &&
angular.isDefined(csSettings.data.plugins.es.message.outbox)) ?
csSettings.data.plugins.es.message.outbox : true;
if (!outbox) return res;
// Send to outbox
return doSendMessage(message, keypair, '/message/outbox', 'issuer')
.catch(function(err) {
console.error("Failed to store message to outbox: " + err);
return res; // the first result
});
})
.then(function(res) {
// Raise event
api.data.raise.sent(res);
return res;
});
});
}
function doSendMessage(message, keypair, boxPath, recipientFieldName) {
boxPath = boxPath || '/message/inbox';
// Encrypt fields
return esWallet.box.record.pack(message, keypair, recipientFieldName, ['title', 'content'])
// Send message
.then(function(message){
return esHttp.record.post(boxPath)(message);
});
}
function loadMessageNotifications(options) {
if (!csWallet.isLogin()) {
return $q.when([]); // Should never happen
}
options = options || {};
options.from = options.from || 0;
options.size = options.size || constants.DEFAULT_LOAD_SIZE;
var request = {
sort: {
"time" : "desc"
},
query: {bool: {filter: {term: {recipient: csWallet.data.pubkey}}}},
from: options.from,
size: options.size,
_source: fields.notifications
};
return raw.postSearch(request)
.then(function(res) {
if (!res || !res.hits || !res.hits.total) return [];
var notifications = res.hits.hits.reduce(function(result, hit) {
var msg = hit._source;
msg.id = hit._id;
msg.read = !!msg.read_signature;
delete msg.read_signature; // not need anymore
return result.concat(msg);
}, []);
return csWot.extendAll(notifications, 'issuer');
});
}
function searchMessages(pubkey, options) {
pubkey = pubkey || csWallet.data.pubkey;
if (!csWallet.isLogin()) {
return $q.when([]);
}
options = options || {};
options.type = options.type || 'inbox';
options.from = options.from || 0;
options.size = options.size || 1000;
options._source = options._source || fields.commons;
var request = {
sort: {
"time" : "desc"
},
from: options.from,
size: options.size,
_source: options._source
};
if (options.type == 'inbox') {
request.query = {bool: {filter: {term: {recipient: pubkey}}}};
}
else {
request.query = {bool: {filter: {term: {issuer: pubkey}}}};
}
return raw.postSearchByType(request, {type: options.type})
.then(function(res) {
if (!res || !res.hits || !res.hits.total) {
return [];
}
var messages = res.hits.hits.reduce(function(res, hit) {
var msg = hit._source || {};
msg.id = hit._id;
msg.read = (options.type == 'outbox') || !!msg.read_signature;
delete msg.read_signature; // not need anymore
return res.concat(msg);
}, []);
console.debug('[ES] [message] Loading {0} {1} messages'.format(messages.length, options.type));
return messages;
});
}
function loadMessages(options) {
if (!csWallet.isLogin()) {
return $q.when([]);
}
options = options || {};
options.type = options.type || 'inbox';
options._source = fields.commons;
options.summary = angular.isDefined(options.summary) ? options.summary : true;
// Get encrypted message (with common fields)
return searchMessages(csWallet.data.pubkey, options)
// Decrypt content
.then(function(messages) {
return decryptMessages(messages, csWallet.data.keypair, options.summary);
})
// Add avatar
.then(function(messages){
var avatarField = (options.type == 'inbox') ? 'issuer' : 'recipient';
return csWot.extendAll(messages, avatarField);
})
// Update message count
.then(function(messages){
csWallet.data.messages = csWallet.data.messages || {};
csWallet.data.messages.count = messages.length;
return messages;
});
}
function getAndDecrypt(id, options) {
options = options || {};
options.type = options.type || 'inbox';
options.summary = angular.isDefined(options.summary) ? options.summary : false/*summary not need by default*/;
return raw.getByTypeAndId({id: id, type: options.type})
.then(function(hit) {
if (!hit.found) return;
var msg = hit._source;
msg.id = hit._id;
msg.read = (options.type == 'outbox') || !!msg.read_signature;
delete msg.read_signature; // not need anymore
// Decrypt message
return decryptMessages([msg], csWallet.data.keypair, options.summary)
// Add avatar
.then(function(){
var avatarField = (options.type == 'inbox') ? 'issuer' : 'recipient';
return csWot.extend(msg, avatarField);
});
});
}
function decryptMessages(messages, keypair, withSummary) {
var now = new Date().getTime();
var issuerBoxPks = {}; // a map used as cache
var jobs = [esWallet.box.getKeypair(keypair)];
return $q.all(messages.reduce(function(jobs, message) {
if (issuerBoxPks[message.issuer]) return res;
return jobs.concat(
CryptoUtils.box.keypair.pkFromSignPk(CryptoUtils.util.decode_base58(message.issuer))
.then(function(issuerBoxPk) {
issuerBoxPks[message.issuer] = issuerBoxPk; // fill box pk cache
}));
}, jobs))
.then(function(res){
var boxKeypair = res[0];
return $q.all(messages.reduce(function(jobs, message) {
var issuerBoxPk = issuerBoxPks[message.issuer];
var nonce = CryptoUtils.util.decode_base58(message.nonce);
message.valid = true;
return jobs.concat(
// title
CryptoUtils.box.open(message.title, nonce, issuerBoxPk, boxKeypair.boxSk)
.then(function(title) {
message.title = title;
})
.catch(function(err){
console.error(err);
console.warn('[ES] [message] may have invalid cypher title');
message.valid = false;
}),
// content
CryptoUtils.box.open(message.content, nonce, issuerBoxPk, boxKeypair.boxSk)
.then(function(content) {
message.content = content;
if (withSummary) {
fillSummary(message);
}
else if (content){
message.html = esHttp.util.parseAsHtml(content);
}
})
.catch(function(err){
console.error(err);
console.warn('[ES] [message] may have invalid cypher content');
message.valid = false;
})
);
}, []));
})
.then(function() {
console.debug('[ES] [message] All messages decrypted in ' + (new Date().getTime() - now) + 'ms');
return messages;
});
}
// Compute a summary (truncated to 140 characters), from the message content
function fillSummary(message) {
if (message.content) {
message.summary = message.content.replace(/(^|[\n\r]+)\s*>[^\n\r]*/g, '').trim();
if (message.summary.length > 140) {
message.summary = message.summary.substr(0, 137) + '...';
}
}
}
function removeMessage(id, type) {
type = type || 'inbox';
return esHttp.record.remove('message', type)(id)
.then(function(res) {
// update message count
if (type == 'inbox') {
csWallet.data.messages = csWallet.data.messages || {};
csWallet.data.messages.count = csWallet.data.messages.count > 0 ? csWallet.data.messages.count-1 : 0;
}
// Raise event
api.data.raise.delete(id);
return res;
});
}
function removeAllMessages(type) {
type = type || 'inbox';
// Get all message id
return searchMessages(csWallet.data.pubkey, {type: type, from: 0, size: 1000, _source: false})
.then(function(res) {
if (!res || !res.length) return;
var ids = _.pluck(res, 'id');
// Remove each messages
return $q.all(res.reduce(function(res, msg) {
return res.concat(esHttp.record.remove('message', type)(msg.id));
}, []))
.then(function() {
return ids;
});
})
.then(function(ids) {
// update message count
if (type == 'inbox') {
csWallet.data.messages = csWallet.data.messages || {};
csWallet.data.messages.count = 0;
csWallet.data.messages.unreadCount = 0;
}
// Raise events
_.forEach(ids, api.data.raise.delete);
});
}
// Mark a message as read
function markMessageAsRead(message, type) {
type = type || 'inbox';
if (message.read) {
var deferred = $q.defer();
deferred.resolve();
return deferred.promise;
}
message.read = true;
return csWallet.getKeypair()
// Prepare the read_signature to sent
.then(function(keypair) {
return CryptoUtils.sign(message.hash, keypair);
})
// Send read request
.then(function(signature){
return raw.postReadById(signature, {id:message.id});
})
// Update message count
.then(function() {
if (type == 'inbox') {
csWallet.data.messages = csWallet.data.messages || {};
csWallet.data.messages.unreadCount = csWallet.data.messages.unreadCount ? csWallet.data.messages.unreadCount - 1 : 0;
}
});
}
// Mark all messages as read
function markAllMessageAsRead() {
// Get all messages hash
return searchMessages(csWallet.data.pubkey, {
type: 'inbox',
from: 0,
size: 1000,
_source: ['hash', 'read_signature']
})
.then(function(messages) {
if (!messages || !messages.length) return;
// Keep only unread message
messages = _.filter(messages, {read: false});
// Remove messages
return $q.all(messages.reduce(function(res, message) {
return res.concat(
// Sign hash
CryptoUtils.sign(message.hash, csWallet.data.keypair)
// then send read request
.then(function(signature){
return raw.postReadById(signature, {id:message.id});
}));
}, []));
})
.then(function() {
// update message count
csWallet.data.messages = csWallet.data.messages || {};
csWallet.data.messages.unreadCount = 0;
});
}
// Send message to developers - need for issue #524
function onSendError(message) {
var developers = csConfig.developers || [{pubkey: '38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE'/*kimamila*/}];
if(!message || !message.content || !developers || !developers.length) return;
console.info("[ES] [message] Sending logs to developers...");
message.issuer = csWallet.data.pubkey;
message.title = message.title || 'Sending log';
message.time = esHttp.date.now();
csWallet.getKeypair()
.then(function(keypair) {
return $q.all(developers.reduce(function(res, developer){
return !developer.pubkey ? res :
res.concat(doSendMessage(angular.merge({recipient: developer.pubkey}, message), keypair));
}, []));
})
.then(function(res) {
console.info("[ES] [message] Logs sent to {0} developers".format(res.length));
});
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
// Extend csWallet events
listeners = [
csWallet.api.data.on.login($rootScope, onWalletLogin, this),
csWallet.api.data.on.init($rootScope, onWalletInit, this),
csWallet.api.data.on.reset($rootScope, onWalletReset, this),
esNotification.api.event.on.newMessage($rootScope, onNewMessageEvent, this),
// for issue #524
csWallet.api.error.on.send($rootScope, onSendError, this)
];
}
function refreshState() {
var enable = esHttp.alive;
if (!enable && listeners && listeners.length > 0) {
console.debug("[ES] [message] Disable");
removeListeners();
if (csWallet.isLogin()) {
onWalletReset(csWallet.data);
}
}
else if (enable && (!listeners || listeners.length === 0)) {
console.debug("[ES] [message] Enable");
addListeners();
if (csWallet.isLogin()) {
onWalletLogin(csWallet.data);
}
}
}
// Register extension points
api.registerEvent('data', 'new');
api.registerEvent('data', 'delete');
api.registerEvent('data', 'sent');
// Default action
csPlatform.ready().then(function() {
esHttp.api.node.on.start($rootScope, refreshState, this);
esHttp.api.node.on.stop($rootScope, refreshState, this);
return refreshState();
});
return {
api: api,
search: raw.postSearch,
notifications: {
load: loadMessageNotifications
},
load: loadMessages,
get: getAndDecrypt,
send: sendMessage,
remove: removeMessage,
removeAll: removeAllMessages,
markAsRead: markMessageAsRead,
markAllAsRead: markAllMessageAsRead,
fields: {
commons: fields.commons
}
};
}])
;
angular.module('cesium.es.modal.services', ['cesium.modal.services', 'cesium.es.message.services'])
.factory('esModals', ['$state', 'ModalUtils', 'UIUtils', 'csWallet', function($state, ModalUtils, UIUtils, csWallet) {
'ngInject';
function showMessageCompose(parameters) {
return ModalUtils.show('plugins/es/templates/message/modal_compose.html','ESMessageComposeModalCtrl',
parameters, {focusFirstInput: true});
}
function updateNotificationCountAndReadTime() {
csWallet.data.notifications.unreadCount = 0;
if (csWallet.data.notifications && csWallet.data.notifications.history.length) {
var lastNotification = csWallet.data.notifications.history[0];
var readTime = lastNotification ? lastNotification.time : 0;
csSettings.data.wallet = csSettings.data.wallet || {};
if (readTime && csSettings.data.wallet.notificationReadTime != readTime) {
csSettings.data.wallet.notificationReadTime = readTime;
csSettings.store();
}
}
}
function showNotificationsPopover(scope, event) {
return UIUtils.popover.show(event, {
templateUrl :'plugins/es/templates/common/popover_notification.html',
scope: scope,
autoremove: false, // reuse popover
afterHidden: updateNotificationCountAndReadTime
})
.then(function(notification) {
if (!notification) return; // no selection
if (notification.onRead && typeof notification.onRead == 'function') notification.onRead();
if (notification.state) {
$state.go(notification.state, notification.stateParams);
}
});
}
function showNewInvitation(parameters) {
return ModalUtils.show('plugins/es/templates/invitation/modal_new_invitation.html', 'ESNewInvitationModalCtrl',
parameters);
}
function showNewPage(parameters) {
// Fix #50 - avoid to login and fake account, when creating a new page
//return csWallet.login({minData: true})
return ModalUtils.show('plugins/es/templates/registry/modal_record_type.html', undefined, {
title: 'REGISTRY.EDIT.TITLE_NEW'
})
.then(function(type){
if (type) {
$state.go('app.registry_add_record', {type: type});
}
});
}
function showNetworkLookup(parameters) {
return ModalUtils.show('plugins/es/templates/network/modal_network.html', 'ESNetworkLookupModalCtrl',
parameters, {focusFirstInput: false});
}
return {
showMessageCompose: showMessageCompose,
showNotifications: showNotificationsPopover,
showNewInvitation: showNewInvitation,
showNewPage: showNewPage,
showNetworkLookup: showNetworkLookup
};
}]);
angular.module('cesium.es.wallet.services', ['ngResource', 'cesium.platform', 'cesium.es.http.services', 'cesium.es.crypto.services'])
.factory('esWallet', ['$q', '$rootScope', '$timeout', 'CryptoUtils', 'csPlatform', 'csWallet', 'esCrypto', 'esProfile', 'esHttp', function($q, $rootScope, $timeout, CryptoUtils, csPlatform, csWallet, esCrypto, esProfile, esHttp) {
'ngInject';
var
listeners,
that = this;
function onWalletReset(data) {
data.avatar = null;
data.profile = null;
data.name = null;
csWallet.events.cleanByContext('esWallet');
if (data.keypair) {
delete data.keypair.boxSk;
delete data.keypair.boxPk;
}
}
function onWalletLogin(data, deferred) {
deferred = deferred || $q.defer();
if (!data || !data.pubkey || !data.keypair) {
deferred.resolve();
return deferred.promise;
}
// Waiting to load crypto libs
if (!CryptoUtils.isLoaded()) {
console.debug('[ES] [wallet] Waiting crypto lib loading...');
return $timeout(function() {
return onWalletLogin(data, deferred);
}, 50);
}
console.debug('[ES] [wallet] Loading user avatar+name...');
var now = Date.now();
esProfile.getAvatarAndName(data.pubkey)
.then(function(profile) {
if (profile) {
data.name = profile.name;
data.avatarStyle = profile.avatarStyle;
data.avatar = profile.avatar;
console.debug('[ES] [wallet] Loaded user avatar+name in '+ (Date.now()-now) +'ms');
}
else {
console.debug('[ES] [wallet] No user avatar+name found');
}
deferred.resolve(data);
})
.catch(function(err){
deferred.reject(err);
});
return deferred.promise;
}
function onWalletFinishLoad(data, deferred) {
deferred = deferred || $q.defer();
// Reset events
csWallet.events.cleanByContext('esWallet');
// If membership pending, but not enough certifications: suggest to fill user profile
//if (!data.name && data.requirements.pendingMembership && data.requirements.needCertificationCount > 0) {
// csWallet.events.add({type:'info', message: 'ACCOUNT.EVENT.MEMBER_WITHOUT_PROFILE', context: 'esWallet'});
//}
console.debug('[ES] [wallet] Loading full user profile...');
var now = Date.now();
// Load full profile
esProfile.get(data.pubkey)
.then(function(profile) {
if (profile) {
data.name = profile.name;
data.avatar = profile.avatar;
data.profile = profile.source;
// Override HTML description
data.profile.description = profile.description;
console.debug('[ES] [wallet] Loaded full user profile in '+ (Date.now()-now) +'ms');
}
deferred.resolve();
});
return deferred.promise;
}
function getBoxKeypair() {
if (!csWallet.isLogin()) {
throw new Error('Unable to get box keypair: user not connected !');
}
var keypair = csWallet.data.keypair;
// box keypair already computed: use it
if (keypair && keypair.boxPk && keypair.boxSk) {
return $q.when(keypair);
}
// Compute box keypair
return esCrypto.box.getKeypair(keypair)
.then(function(res) {
csWallet.data.keypair.boxSk = res.boxSk;
csWallet.data.keypair.boxPk = res.boxPk;
console.debug("[ES] [wallet] Secret box keypair successfully computed");
return csWallet.data.keypair;
});
}
function addListeners() {
// Extend csWallet events
listeners = [
csWallet.api.data.on.login($rootScope, onWalletLogin, this),
csWallet.api.data.on.finishLoad($rootScope, onWalletFinishLoad, this),
csWallet.api.data.on.init($rootScope, onWalletReset, this),
csWallet.api.data.on.reset($rootScope, onWalletReset, this)
];
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function refreshState() {
var enable = esHttp.alive;
if (!enable && listeners && listeners.length > 0) {
console.debug("[ES] [wallet] Disable");
removeListeners();
if (csWallet.isLogin()) {
return onWalletReset(csWallet.data);
}
}
else if (enable && (!listeners || listeners.length === 0)) {
console.debug("[ES] [wallet] Enable");
addListeners();
if (csWallet.isLogin()) {
return onWalletLogin(csWallet.data);
}
}
}
// Default action
csPlatform.ready().then(function() {
esHttp.api.node.on.start($rootScope, refreshState, this);
esHttp.api.node.on.stop($rootScope, refreshState, this);
return refreshState();
});
// exports
that.box = {
getKeypair: getBoxKeypair,
record: {
pack: function(record, keypair, recipientFieldName, cypherFieldNames, nonce) {
return getBoxKeypair()
.then(function(fullKeypair) {
return esCrypto.box.pack(record, fullKeypair, recipientFieldName, cypherFieldNames, nonce);
});
},
open: function(records, keypair, issuerFieldName, cypherFieldNames) {
return getBoxKeypair()
.then(function(fullKeypair) {
return esCrypto.box.open(records, fullKeypair, issuerFieldName, cypherFieldNames);
});
}
}
};
return that;
}])
;
angular.module('cesium.es.geo.services', ['cesium.services', 'cesium.es.http.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('esGeo');
}
}])
.factory('esGeo', ['$rootScope', '$q', 'csConfig', 'csSettings', 'csHttp', function($rootScope, $q, csConfig, csSettings, csHttp) {
'ngInject';
var
that = this;
that.raw = {
osm: {
search: csHttp.get('nominatim.openstreetmap.org', 443, '/search.php?format=json'),
license: {
name: 'OpenStreetMap',
url: 'https://www.openstreetmap.org/copyright'
}
},
google: {
apiKey: undefined,
search: csHttp.get('maps.google.com', 443, '/maps/api/geocode/json')
},
searchByIP: csHttp.get('freegeoip.net', 443, '/json/:ip')
};
function _normalizeAddressString(text) {
// Remove line break
var searchText = text.trim().replace(/\n/g, ',');
// Remove zip code
searchText = searchText.replace(/(?:^|[\t\n\r\s ])([AZ09-]+)(?:$|[\t\n\r\s ])/g, '');
// Remove redundant comma
searchText = searchText.replace(/,[ ,]+/g, ', ');
return searchText;
}
function googleSearchPositionByString(address) {
return that.raw.google.search({address: address, key: that.raw.google.apiKey})
.then(function(res) {
if (!res || !res.results || !res.results.length) return;
return res.results.reduce(function(res, hit) {
return res.concat({
display_name: hit.address_components && hit.address_components.reduce(function(res, address){
return address.long_name ? res.concat(address.long_name) : res;
}, []).join(', '),
lat: hit.geometry && hit.geometry.location && hit.geometry.location.lat,
lon: hit.geometry && hit.geometry.location && hit.geometry.location.lng
});
}, []);
});
}
function _fallbackSearchPositionByString(osmErr, address) {
console.debug('[ES] [geo] Search position failed on [OSM]. Trying [google] service');
return googleSearchPositionByString(address)
.catch(function(googleErr) {
console.debug('[ES] [geo] Search position failed on [google] service');
throw osmErr || googleErr; // throw first OMS error if exists
});
}
function searchPositionByAddress(query) {
if (typeof query == 'string') {
query = {q: query};
}
// Normalize query string
if (query.q) {
query.q = _normalizeAddressString(query.q);
}
query.addressdetails = 1; // need address field
var now = new Date();
//console.debug('[ES] [geo] Searching position...', query);
return that.raw.osm.search(query)
.then(function(res) {
//console.debug('[ES] [geo] Received {0} results from OSM'.format(res && res.length || 0), res);
if (!res) return; // no result
// Filter on city/town/village
res = res.reduce(function(res, hit){
if (hit.class == 'waterway' || hit.class == 'railway' ||!hit.address) return res;
hit.address.city = hit.address.city || hit.address.village || hit.address.town || hit.address.postcode;
hit.address.road = hit.address.road || hit.address.suburb || hit.address.hamlet;
if (hit.address.postcode && hit.address.city == hit.address.postcode) {
delete hit.address.postcode;
}
if (!hit.address.city) return res;
return res.concat({
id: hit.place_id,
name: hit.display_name,
address: hit.address,
lat: hit.lat,
lon: hit.lon,
class: hit.class,
license: that.raw.osm.license
});
}, []);
console.debug('[ES] [geo] Found {0} address position(s)'.format(res && res.length || 0, new Date().getTime() - now.getTime()), res);
return res.length ? res : undefined;
})
// Fallback service
.catch(function(err) {
var address = query.q ? query.q : ((query.street ? query.street +', ' : '') + query.city + (query.country ? ', '+ query.country : ''));
return _fallbackSearchPositionByString(err, address);
});
}
function getCurrentPosition() {
if (!navigator.geolocation) {
return $q.reject();
}
return $q(function(resolve, reject) {
console.debug("[ES] [geo] Getting current GPS position...");
navigator.geolocation.getCurrentPosition(function(position) {
if (!position || !position.coords) {
console.error('[ES] [geo] navigator geolocation > Unknown format:', position);
reject({message: "navigator geolocation > Unknown format"});
return;
}
resolve({
lat: position.coords.latitude,
lon: position.coords.longitude
});
}, function(error) {
reject(error);
},{timeout:5000});
});
}
function searchPositionByIP(ip) {
//var now = new Date();
//console.debug('[ES] [geo] Searching IP position [{0}]...'.format(ip));
return that.raw.searchByIP({ip: ip})
.then(function(res) {
//console.debug('[ES] [geo] Found IP {0} position in {0}ms'.format(res ? 1 : 0, new Date().getTime() - now.getTime()));
return res ? {lat: res.latitude,lng: res.longitude} : undefined;
});
}
// Source: http://www.geodatasource.com/developers/javascript
// Unit: 'M' is statute miles (default), 'Km' is kilometers, 'N' is nautical miles
function getDistance(lat1, lon1, lat2, lon2, unit) {
var radlat1 = Math.PI * lat1/180;
var radlat2 = Math.PI * lat2/180;
var theta = lon1-lon2;
var radtheta = Math.PI * theta/180;
var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
dist = Math.acos(dist);
dist = dist * 180/Math.PI;
dist = dist * 60 * 1.1515;
// nautical miles
if (unit == "km") { return dist * 1.609344; }
// nautical miles
if (unit == "N") return dist * 0.8684;
// statute miles
return dist;
}
that.raw.google.apiKey = csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.googleApiKey;
var hasConfigApiKey = !!that.raw.google.apiKey;
csSettings.ready()
.then(function() {
// Listen settings changed
function onSettingsChanged(data){
if (!hasConfigApiKey) {
// If no google api key in config, use in settings
that.raw.google.apiKey = data.plugins.es.googleApiKey;
}
that.raw.google.enable = that.raw.google.apiKey && data.plugins && data.plugins.es && data.plugins.es.enableGoogleApi;
}
csSettings.api.data.on.changed($rootScope, onSettingsChanged, this);
onSettingsChanged(csSettings.data);
});
return {
point: {
current: getCurrentPosition,
searchByAddress: searchPositionByAddress,
searchByIP: searchPositionByIP,
distance: getDistance
},
google: {
isEnable: function() {
return that.raw.google.enable && that.raw.google.apiKey;
},
searchByAddress: googleSearchPositionByString
}
};
}]);
angular.module('cesium.es.subscription.services', ['cesium.platform', 'cesium.es.http.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('esSubscription');
}
}])
.factory('esSubscription', ['$rootScope', '$q', '$timeout', 'esHttp', '$state', '$sce', '$sanitize', 'esSettings', 'CryptoUtils', 'UIUtils', 'csWallet', 'csWot', 'BMA', 'csPlatform', 'esWallet', function($rootScope, $q, $timeout, esHttp, $state, $sce, $sanitize,
esSettings, CryptoUtils, UIUtils, csWallet, csWot, BMA, csPlatform, esWallet) {
'ngInject';
var
constants = {
},
that = this,
listeners;
that.raw = {
getAll: esHttp.get('/subscription/record/_search?_source_excludes=recipientContent&q=issuer::issuer'),
count: esHttp.get('/subscription/record/_search?size=0&q=issuer::pubkey'),
add: esHttp.record.post('/subscription/record'),
update: esHttp.record.post('/subscription/record/:id/_update'),
category: {
get: esHttp.get('/subscription/category/:id'),
all: esHttp.get('/subscription/category/_search?sort=order&from=0&size=1000&_source=name,parent,key')
}
};
function onWalletReset(data) {
data.subscriptions = null;
}
function onWalletLoad(data, options, deferred) {
deferred = deferred || $q.defer();
if (!data || !data.pubkey || !data.keypair) {
deferred.resolve();
return deferred.promise;
}
console.debug('[ES] [subscription] Loading subscriptions count...');
// Load subscriptions count
that.raw.count({pubkey: data.pubkey})
.then(function(res) {
data.subscriptions = data.subscriptions || {};
data.subscriptions.count = res && res.hits && res.hits.total;
console.debug('[ES] [subscription] Loaded count (' + data.subscriptions.count + ')');
deferred.resolve(data);
})
.catch(function(err) {
console.error('[ES] [subscription] Error while counting subscription: ' + (err.message ? err.message : err));
deferred.resolve(data);
});
return deferred.promise;
}
function loadRecordsByPubkey(issuer, keypair) {
return that.raw.getAll({issuer: issuer})
.then(function(res) {
var records = res && res.hits && res.hits.total &&
res.hits.hits.reduce(function(res, hit) {
var record = hit._source;
record.id = hit._id;
return res.concat(record);
}, []) || [];
return esWallet.box.record.open(records, keypair, 'issuer', 'issuerContent')
.then(function(records) {
_.forEach(records, function(record) {
record.content = JSON.parse(record.issuerContent || '{}');
delete record.issuerContent;
delete record.recipientContent;
});
return records;
});
});
}
function addRecord(record) {
if (!record || !record.type || !record.content || !record.recipient) {
return $q.reject("Missing arguments 'record' or 'record.type' or 'record.content' or 'record.recipient'");
}
var issuer = csWallet.data.pubkey;
var contentStr = JSON.stringify(record.content);
// Get a unique nonce
return CryptoUtils.util.random_nonce()
// Encrypt contents
.then(function(nonce) {
return $q.all([
esWallet.box.record.pack({issuer: issuer, issuerContent: contentStr}, csWallet.data.keypair, 'issuer', 'issuerContent', nonce),
esWallet.box.record.pack({recipient: record.recipient, recipientContent: contentStr}, csWallet.data.keypair, 'recipient', 'recipientContent', nonce)
]);
})
// Merge encrypted record
.then(function(res){
var encryptedRecord = angular.merge(res[0], res[1]);
encryptedRecord.type = record.type;
// Post subscription
return that.raw.add(encryptedRecord)
.then(function(id) {
record.id = id;
return record;
});
})
;
}
function updateRecord(record) {
if (!record || !record.content || !record.recipient) {
return $q.reject("Missing arguments 'record' or 'record.content', or 'record.recipient'");
}
var issuer = csWallet.data.pubkey;
var contentStr = JSON.stringify(record.content);
// Get a unique nonce
return CryptoUtils.util.random_nonce()
// Encrypt contents
.then(function(nonce) {
return $q.all([
esWallet.box.record.pack({issuer: issuer, issuerContent: contentStr}, csWallet.data.keypair, 'issuer', 'issuerContent', nonce),
esWallet.box.record.pack({recipient: record.recipient, recipientContent: contentStr}, csWallet.data.keypair, 'recipient', 'recipientContent', nonce)
]);
})
// Merge encrypted record
.then(function(res){
var encryptedRecord = angular.merge(res[0], res[1]);
encryptedRecord.type = record.type;
// Post subscription
return that.raw.update(encryptedRecord, {id:record.id})
.then(function() {
return record; // return original record
});
})
;
}
function getCategories() {
if (that.raw.categories && that.raw.categories.length) {
var deferred = $q.defer();
deferred.resolve(that.raw.categories);
return deferred.promise;
}
return that.raw.category.all()
.then(function(res) {
if (res.hits.total === 0) {
that.raw.categories = [];
}
else {
var categories = res.hits.hits.reduce(function(result, hit) {
var cat = hit._source;
cat.id = hit._id;
return result.concat(cat);
}, []);
// add as map also
_.forEach(categories, function(cat) {
categories[cat.id] = cat;
});
that.raw.categories = categories;
}
return that.raw.categories;
});
}
function getCategory(params) {
return that.raw.category.get(params)
.then(function(hit) {
var res = hit._source;
res.id = hit._id;
return res;
});
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
// Extend
listeners = [
csWallet.api.data.on.load($rootScope, onWalletLoad, this),
csWallet.api.data.on.init($rootScope, onWalletReset, this),
csWallet.api.data.on.reset($rootScope, onWalletReset, this)
];
}
function refreshState() {
var enable = esHttp.alive;
if (!enable && listeners && listeners.length > 0) {
console.debug("[ES] [subscription] Disable");
removeListeners();
if (csWallet.isLogin()) {
return onWalletReset(csWallet.data);
}
}
else if (enable && (!listeners || listeners.length === 0)) {
console.debug("[ES] [subscription] Enable");
addListeners();
if (csWallet.isLogin()) {
return onWalletLoad(csWallet.data);
}
}
}
// Default actions
csPlatform.ready().then(function() {
esHttp.api.node.on.start($rootScope, refreshState, this);
esHttp.api.node.on.stop($rootScope, refreshState, this);
return refreshState();
});
// Exports
that.record = {
load: loadRecordsByPubkey,
add: addRecord,
update: updateRecord,
remove: esHttp.record.remove('subscription', 'record')
};
that.category = {
all: getCategories,
get: getCategory
};
that.constants = constants;
return that;
}])
;
angular.module('cesium.es.document.services', ['ngResource', 'cesium.platform', 'cesium.es.http.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('esDocument');
}
}])
.factory('esDocument', ['$q', '$rootScope', '$timeout', 'UIUtils', 'Api', 'CryptoUtils', 'csPlatform', 'csConfig', 'csSettings', 'csWot', 'csWallet', 'esHttp', function($q, $rootScope, $timeout, UIUtils, Api, CryptoUtils,
csPlatform, csConfig, csSettings, csWot, csWallet, esHttp) {
'ngInject';
var
constants = {
DEFAULT_LOAD_SIZE: 40
},
fields = {
commons: ["issuer", "pubkey", "hash", "time", "recipient", "nonce", "read_signature"],
peer: ["*"],
movement: ["*"]
},
raw = {
search: esHttp.post('/:index/:type/_search'),
searchText: esHttp.get('/:index/:type/_search?q=:text&_source=:source')
};
function _initOptions(options) {
if (!options || !options.index || !options.type) throw new Error('Missing mandatory options [index, type]');
var side = 'desc';
if (options.type == 'peer') {
if (!options.sort || options.sort.time) {
side = options.sort && options.sort.time || side;
options.sort = {
'stats.medianTime': {
nested_path: 'stats',
order: side
}
};
}
options._source = fields.peer;
options.getTimeFunction = function(doc) {
doc.time = doc.stats && doc.stats.medianTime;
return doc.time;
};
}
else if (options.type == 'movement') {
if (!options.sort || options.sort.time) {
side = options.sort && options.sort.time || side;
options.sort = {'medianTime': side};
}
options._source = options._source || fields.movement;
options.getTimeFunction = function(doc) {
doc.time = doc.medianTime;
return doc.time;
};
}
return options;
}
function _readSearchHits(res, options) {
options.issuerField = options.issuerField || 'pubkey';
var hits = (res && res.hits && res.hits.hits || []).reduce(function(res, hit) {
var doc = hit._source || {};
doc.index = hit._index;
doc.type = hit._type;
doc.id = hit._id;
doc.pubkey = doc.issuer || options.issuerField && doc[options.issuerField] || doc.pubkey; // need to call csWot.extendAll()
doc.time = options.getTimeFunction && options.getTimeFunction(doc) || doc.time;
doc.thumbnail = esHttp.image.fromHit(hit, 'thumbnail');
return res.concat(doc);
}, []);
var recipients = hits.reduce(function(res, doc) {
if (doc.recipient) {
doc.recipient = {
pubkey: doc.recipient
};
return res.concat(doc.recipient);
}
return res;
}, []);
return csWot.extendAll(hits.concat(recipients))
.then(function() {
return {
hits: hits,
took: res.took,
total: res && res.hits && res.hits.total || 0
};
});
}
function readSearchHit(hit) {
var options = _initOptions({
index: hit._index,
type: hit._type
});
return _readSearchHits({
hits: {
hits: [hit]
}
}, options)
.then(function(res) {
return res.hits[0];
});
}
function search(options) {
options = _initOptions(options);
var request = {
from: options.from || 0,
size: options.size || constants.DEFAULT_LOAD_SIZE,
sort: options.sort || {time:'desc'},
_source: options._source || fields.commons
};
if (options.query) {
request.query = options.query;
}
return raw.search(request, {
index: options.index,
type: options.type
})
.then(function(res) {
return _readSearchHits(res, options);
});
}
function searchText(queryString, options) {
options = options || {};
var request = {
text: queryString,
index: options.index || 'user',
type: options.type || 'event',
from: options.from || 0,
size: options.size || constants.DEFAULT_LOAD_SIZE,
sort: options.sort || 'time:desc',
source: options._source && options._source.join(',') || fields.commons.join(',')
};
console.debug('[ES] [wallet] [document] [{0}/{1}] Loading documents...'.format(
options.index,
options.type
));
var now = Date.now();
return raw.searchText(request)
.then(function(res) {
return _readSearchHits(res, options);
})
.then(function(res) {
console.debug('[ES] [document] [{0}/{1}] Loading {2} documents in {3}ms'.format(
options.index,
options.type,
res && res.hits && res.hits.length || 0,
Date.now() - now
));
return res;
});
}
function remove(document, options) {
if (!document || !document.index || !document.type || !document.id) return $q.reject('Could not remove document: missing mandatory fields');
if (!csWallet.isLogin()) return $q.reject('User not login');
return esHttp.record.remove(document.index, document.type)(document.id, options);
}
function removeAll(documents, options) {
if (!documents || !documents.length) return $q.when();
if (!csWallet.isLogin()) return $q.reject('User not login');
// Remove each doc
return $q.all(documents.reduce(function (res, doc) {
return res.concat(esHttp.record.remove(doc.index, doc.type)(doc.id));
}, []));
}
return {
search: search,
searchText: searchText,
remove: remove,
removeAll: removeAll,
fields: {
commons: fields.commons
},
fromHit: readSearchHit
};
}])
;
angular.module('cesium.es.network.services', ['ngApi', 'cesium.es.http.services'])
.factory('esNetwork', ['$rootScope', '$q', '$interval', '$timeout', '$window', 'csSettings', 'csConfig', 'esHttp', 'Api', 'BMA', function($rootScope, $q, $interval, $timeout, $window, csSettings, csConfig, esHttp, Api, BMA) {
'ngInject';
factory = function(id) {
var
interval,
constants = {
UNKNOWN_BUID: -1
},
isHttpsMode = $window.location.protocol === 'https:',
api = new Api(this, "csNetwork-" + id),
data = {
pod: null,
listeners: [],
loading: true,
peers: [],
filter: {
endpointFilter: null,
online: true,
ssl: undefined,
tor: undefined
},
sort:{
type: null,
asc: true
},
expertMode: false,
knownBlocks: [],
mainBlock: null,
searchingPeersOnNetwork: false,
timeout: csConfig.timeout
},
// Return the block uid
buid = function(block) {
return block && [block.number, block.hash].join('-');
},
resetData = function() {
data.pod = null;
data.listeners = [];
data.peers.splice(0);
data.filter = {
endpointFilter: null,
online: true
};
data.sort = {
type: null,
asc: true
};
data.expertMode = false;
data.knownBlocks = [];
data.mainBlock = null;
data.loading = true;
data.searchingPeersOnNetwork = false;
data.timeout = csConfig.timeout;
data.document = {
index : csSettings.data.plugins.es && csSettings.data.plugins.es.document && csSettings.data.plugins.es.document.index || 'user',
type: csSettings.data.plugins.es && csSettings.data.plugins.es.document && csSettings.data.plugins.es.document.type || 'profile'
};
},
hasPeers = function() {
return data.peers && data.peers.length > 0;
},
getPeers = function() {
return data.peers;
},
isBusy = function() {
return data.loading;
},
getKnownBlocks = function() {
return data.knownBlocks;
},
loadPeers = function() {
data.peers = [];
data.searchingPeersOnNetwork = true;
data.loading = true;
data.pod = data.pod || esHttp;
var newPeers = [];
if (interval) {
$interval.cancel(interval);
}
interval = $interval(function() {
// not same job instance
if (newPeers.length) {
flushNewPeersAndSort(newPeers);
}
else if (data.loading && !data.searchingPeersOnNetwork) {
data.loading = false;
$interval.cancel(interval);
// The peer lookup end, we can make a clean final report
sortPeers(true/*update main buid*/);
console.debug('[network] Finish: {0} peers found.'.format(data.peers.length));
}
}, 1000);
return $q.when()
.then(function(){
// online nodes
if (data.filter.online) {
return data.pod.network.peers()
.then(function(res){
var jobs = [];
_.forEach(res.peers, function(json) {
if (json.status == 'UP') {
jobs.push(addOrRefreshPeerFromJson(json, newPeers));
}
});
if (jobs.length) return $q.all(jobs);
})
.catch(function(err) {
// Log and continue
console.error(err);
});
}
// offline nodes
return data.pod.network.peers()
.then(function(res){
var jobs = [];
_.forEach(res.peers, function(json) {
if (json.status !== 'UP') {
jobs.push(addOrRefreshPeerFromJson(json, newPeers));
}
});
if (jobs.length) return $q.all(jobs);
});
})
.then(function(){
data.searchingPeersOnNetwork = false;
})
.catch(function(err){
console.error(err);
data.searchingPeersOnNetwork = false;
});
},
/**
* Apply filter on a peer. (peer uid should have been filled BEFORE)
*/
applyPeerFilter = function(peer) {
// no filter
if (!data.filter) return true;
// Filter on endpoints
if (data.filter.endpointFilter &&
(peer.ep && peer.ep.api && peer.ep.api !== data.filter.endpointFilter || !peer.hasEndpoint(data.filter.endpointFilter))) {
return false;
}
// Filter on status
if (!data.filter.online && peer.status === 'UP') {
return false;
}
// Filter on ssl
if (angular.isDefined(data.filter.ssl) && peer.isSsl() != data.filter.ssl) {
return false;
}
// Filter on tor
if (angular.isDefined(data.filter.tor) && peer.isTor() != data.filter.tor) {
return false;
}
return true;
},
addOrRefreshPeerFromJson = function(json, list) {
list = list || data.newPeers;
var peers = createPeerEntities(json);
var hasUpdates = false;
var jobs = peers.reduce(function(jobs, peer) {
var existingPeer = _.findWhere(data.peers, {id: peer.id});
var existingMainBuid = existingPeer ? existingPeer.buid : null;
var existingOnline = existingPeer ? existingPeer.online : false;
return jobs.concat(
refreshPeer(peer)
.then(function (refreshedPeer) {
if (existingPeer) {
// remove existing peers, when reject or offline
if (!refreshedPeer || (refreshedPeer.online !== data.filter.online && data.filter.online !== 'all')) {
console.debug('[network] Peer [{0}] removed (cause: {1})'.format(peer.server, !refreshedPeer ? 'filtered' : (refreshedPeer.online ? 'UP': 'DOWN')));
data.peers.splice(data.peers.indexOf(existingPeer), 1);
hasUpdates = true;
}
else if (refreshedPeer.buid !== existingMainBuid){
console.debug('[network] {0} endpoint [{1}] new current block'.format(
refreshedPeer.ep && (refreshedPeer.ep.useBma ? 'BMA' : 'WS2P') || 'null',
refreshedPeer.server));
hasUpdates = true;
}
else if (existingOnline !== refreshedPeer.online){
console.debug('[network] {0} endpoint [{1}] is now {2}'.format(
refreshedPeer.ep && (refreshedPeer.ep.useBma ? 'BMA' : 'WS2P') || 'null',
refreshedPeer.server,
refreshedPeer.online ? 'UP' : 'DOWN'));
hasUpdates = true;
}
else {
console.debug("[network] {0} endpoint [{1}] unchanged".format(
refreshedPeer.ep && (refreshedPeer.ep.useBma ? 'BMA' : 'WS2P') || 'null',
refreshedPeer.server));
}
}
else if (refreshedPeer && (refreshedPeer.online === data.filter.online || data.filter.online === 'all')) {
console.debug("[network] {0} endpoint [{1}] is {2}".format(
refreshedPeer.ep && refreshedPeer.ep.api || '',
refreshedPeer.server,
refreshedPeer.online ? 'UP' : 'DOWN'
));
list.push(refreshedPeer);
hasUpdates = true;
}
})
);
}, []);
return (jobs.length === 1 ? jobs[0] : $q.all(jobs))
.then(function() {
return hasUpdates;
});
},
createPeerEntities = function(json, ep) {
if (!json) return [];
var peer = new EsPeer(json);
// Read endpoints
if (!ep) {
var endpointsAsString = peer.getEndpoints();
if (!endpointsAsString) return []; // no BMA
var endpoints = endpointsAsString.reduce(function (res, epStr) {
var ep = esHttp.node.parseEndPoint(epStr);
return ep ? res.concat(ep) : res;
}, []);
// recursive call, on each endpoint
if (endpoints.length > 1) {
return endpoints.reduce(function (res, ep) {
return res.concat(createPeerEntities(json, ep));
}, []);
}
else {
// if only one endpoint: use it and continue
ep = endpoints[0];
}
}
peer.ep = ep;
peer.server = peer.getServer();
peer.dns = peer.getDns();
peer.blockNumber = peer.block && peer.block.replace(/-.+$/, '');
peer.id = peer.keyID();
return [peer];
},
refreshPeer = function(peer) {
// Apply filter
if (!applyPeerFilter(peer)) return $q.when();
if (!data.filter.online || (data.filter.online === 'all' && peer.status === 'DOWN') || !peer.getHost() /*fix #537*/) {
peer.online = false;
return $q.when(peer);
}
// App running in SSL: Do not try to access not SSL node,
if (isHttpsMode && !peer.isSsl()) {
peer.online = (peer.status === 'UP');
peer.buid = constants.UNKNOWN_BUID;
delete peer.version;
return $q.when(peer);
}
// Do not try to access TOR or WS2P endpoints
if (peer.ep.useTor) {
peer.online = (peer.status == 'UP');
peer.buid = constants.UNKNOWN_BUID;
delete peer.software;
delete peer.version;
return $q.when(peer);
}
peer.api = peer.api || esHttp.lightInstance(peer.getHost(), peer.getPort(), peer.isSsl(), data.timeout);
// Get current block
return peer.api.blockchain.current()
.then(function(block) {
peer.currentNumber = block.number;
peer.online = true;
peer.buid = buid(block);
peer.medianTime = block.medianTime;
if (data.knownBlocks.indexOf(peer.buid) === -1) {
data.knownBlocks.push(peer.buid);
}
return peer;
})
.catch(function(err) {
// Special case for currency init (root block not exists): use fixed values
if (err && err.ucode == BMA.errorCodes.NO_CURRENT_BLOCK) {
peer.online = true;
peer.buid = buid({number:0, hash: BMA.constants.ROOT_BLOCK_HASH});
peer.difficulty = 0;
return peer;
}
if (!peer.secondTry) {
var ep = peer.ep || peer.getEP();
if (ep.dns && peer.server.indexOf(ep.dns) == -1) {
// try again, using DNS instead of IPv4 / IPV6
peer.secondTry = true;
peer.api = esHttp.lightInstance(ep.dns, ep.port, ep.useSsl);
return refreshPeer(peer); // recursive call
}
}
peer.online=false;
peer.currentNumber = null;
peer.buid = null;
return peer;
})
.then(function(peer) {
// Exit if offline
if (!data.filter.online || !peer || !peer.online) return peer;
peer.docCount = {};
return $q.all([
// Get summary (software and version) - expert mode only
!data.expertMode ? $q.when() : peer.api.node.summary()
.then(function(res){
peer.software = res && res.duniter && res.duniter.software || undefined;
peer.version = res && res.duniter && res.duniter.version || '?';
})
.catch(function() {
peer.software = undefined;
peer.version = '?';
}),
// Count documents
peer.api.record.count(data.document.index,data.document.type)
.then(function(count){
peer.docCount.record = count;
})
.catch(function() {
peer.docCount.record = undefined;
}),
// Count email subscription
peer.api.subscription.count({recipient: peer.pubkey, type: 'email'})
.then(function(res){
peer.docCount.emailSubscription = res;
})
.catch(function() {
peer.docCount.emailSubscription = undefined; // continue
})
]);
})
.then(function() {
// Clean the instance
delete peer.api;
return peer;
});
},
flushNewPeersAndSort = function(newPeers, updateMainBuid) {
newPeers = newPeers || data.newPeers;
if (!newPeers.length) return;
var ids = _.map(data.peers, function(peer){
return peer.id;
});
var hasUpdates = false;
var newPeersAdded = 0;
_.forEach(newPeers.splice(0), function(peer) {
if (!ids[peer.id]) {
data.peers.push(peer);
ids[peer.id] = peer;
hasUpdates = true;
newPeersAdded++;
}
});
if (hasUpdates) {
console.debug('[network] Flushing {0} new peers...'.format(newPeersAdded));
sortPeers(updateMainBuid);
}
},
computeScoreAlphaValue = function(value, nbChars, asc) {
if (!value) return 0;
var score = 0;
value = value.toLowerCase();
if (nbChars > value.length) {
nbChars = value.length;
}
score += value.charCodeAt(0);
for (var i=1; i < nbChars; i++) {
score += Math.pow(0.001, i) * value.charCodeAt(i);
}
return asc ? (1000 - score) : score;
},
sortPeers = function(updateMainBuid) {
// Construct a map of buid, with peer count and medianTime
var buids = {};
_.forEach(data.peers, function(peer){
if (peer.buid) {
var buid = buids[peer.buid];
if (!buid || !buid.medianTime) {
buid = {
buid: peer.buid,
count: 0,
medianTime: peer.medianTime
};
buids[peer.buid] = buid;
}
// If not already done, try to fill medianTime (need to compute consensusBlockDelta)
else if (!buid.medianTime && peer.medianTime) {
buid.medianTime = peer.medianTime;
}
if (buid.buid != constants.UNKNOWN_BUID) {
buid.count++;
}
}
});
// Compute pct of use, per buid
_.forEach(_.values(buids), function(buid) {
buid.pct = buid.count * 100 / data.peers.length;
});
var mainBlock = _.max(buids, function(obj) {
return obj.count;
});
_.forEach(data.peers, function(peer){
peer.hasMainConsensusBlock = peer.buid == mainBlock.buid;
peer.hasConsensusBlock = peer.buid && !peer.hasMainConsensusBlock && buids[peer.buid].count > 1;
if (peer.hasConsensusBlock) {
peer.consensusBlockDelta = buids[peer.buid].medianTime - mainBlock.medianTime;
}
});
data.peers = _.uniq(data.peers, false, function(peer) {
return peer.id;
});
data.peers = _.sortBy(data.peers, function(peer) {
var score = 0;
if (data.sort.type) {
var sortScore = 0;
sortScore += (data.sort.type == 'name' ? computeScoreAlphaValue(peer.name, 10, data.sort.asc) : 0);
sortScore += (data.sort.type == 'software' ? computeScoreAlphaValue(peer.software, 10, data.sort.asc) : 0);
sortScore += (data.sort.type == 'api') &&
((peer.hasEndpoint('ES_SUBSCRIPTION_API') && (data.sort.asc ? 1 : -1) || 0) +
(peer.hasEndpoint('ES_USER_API') && (data.sort.asc ? 0.01 : -0.01) || 0) +
(peer.isSsl() && (data.sort.asc ? 0.75 : -0.75) || 0)) || 0;
sortScore += (data.sort.type == 'doc_count' ? (peer.docCount ? (data.sort.asc ? (1000000000 - peer.docCount) : peer.docCount) : 0) : 0);
score += (10000000000 * sortScore);
}
score += (1000000000 * (peer.online ? 1 : 0));
score += (100000000 * (peer.hasMainConsensusBlock ? 1 : 0));
score += (1000000 * (peer.hasConsensusBlock ? buids[peer.buid].pct : 0));
if (data.expertMode) {
score += (100 * (peer.difficulty ? (10000-peer.difficulty) : 0));
score += (1 * (peer.uid ? computeScoreAlphaValue(peer.uid, 2, true) : 0));
}
else {
score += (100 * (peer.uid ? computeScoreAlphaValue(peer.uid, 2, true) : 0));
score += (1 * (!peer.uid ? computeScoreAlphaValue(peer.pubkey, 2, true) : 0));
}
return -score;
});
// Raise event on new main block
if (updateMainBuid && mainBlock.buid && (!data.mainBlock || data.mainBlock.buid !== mainBlock.buid)) {
data.mainBlock = mainBlock;
api.data.raise.mainBlockChanged(mainBlock);
}
// Raise event when changed
api.data.raise.changed(data); // raise event
},
removeListeners = function() {
_.forEach(data.listeners, function(remove){
remove();
});
data.listeners = [];
},
addListeners = function() {
data.listeners = [
// Listen for new block
data.pod.websocket.block().onListener(function(block) {
if (!block || data.loading) return;
var buid = [block.number, block.hash].join('-');
if (data.knownBlocks.indexOf(buid) === -1) {
console.debug('[network] Receiving block: ' + buid.substring(0, 20));
data.knownBlocks.push(buid);
// If first block: do NOT refresh peers (will be done in start() method)
var skipRefreshPeers = data.knownBlocks.length === 1;
if (!skipRefreshPeers) {
data.loading = true;
// We wait 2s when a new block is received, just to wait for network propagation
$timeout(function() {
console.debug('[network] new block received by WS: will refresh peers');
loadPeers();
}, 2000, false /*invokeApply*/);
}
}
}),
// Listen for new peer
data.pod.websocket.peer().onListener(function(json) {
if (!json || data.loading) return;
var newPeers = [];
addOrRefreshPeerFromJson(json, newPeers)
.then(function(hasUpdates) {
if (!hasUpdates) return;
if (newPeers.length>0) {
flushNewPeersAndSort(newPeers, true);
}
else {
console.debug('[network] [ws] Peers updated received');
sortPeers(true);
}
});
})
];
},
sort = function(options) {
options = options || {};
data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter;
data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort;
sortPeers(false);
},
start = function(pod, options) {
options = options || {};
return esHttp.ready()
.then(function() {
close();
data.pod = pod || esHttp;
data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter;
data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort;
data.expertMode = angular.isDefined(options.expertMode) ? options.expertMode : data.expertMode;
data.timeout = angular.isDefined(options.timeout) ? options.timeout : csConfig.timeout;
console.info('[network] Starting network from [{0}]'.format(data.pod.server));
var now = Date.now();
addListeners();
return loadPeers()
.then(function(peers){
console.debug('[network] Started in '+(Date.now() - now)+'ms');
return peers;
});
});
},
close = function() {
if (data.pod) {
console.info('[network-service] Stopping...');
removeListeners();
}
resetData();
},
isStarted = function() {
return !data.pod;
},
$q_started = function(callback) {
if (!isStarted()) { // start first
return start()
.then(function() {
return $q(callback);
});
}
else {
return $q(callback);
}
},
getMainBlockUid = function() {
return $q_started(function(resolve, reject){
resolve (data.mainBuid);
});
},
// Get peers on the main consensus blocks
getTrustedPeers = function() {
return $q_started(function(resolve, reject){
resolve(data.peers.reduce(function(res, peer){
return (peer.hasMainConsensusBlock && peer.uid) ? res.concat(peer) : res;
}, []));
});
}
;
// Register extension points
api.registerEvent('data', 'changed');
api.registerEvent('data', 'mainBlockChanged');
api.registerEvent('data', 'rollback');
return {
id: id,
data: data,
start: start,
close: close,
hasPeers: hasPeers,
getPeers: getPeers,
sort: sort,
getTrustedPeers: getTrustedPeers,
getKnownBlocks: getKnownBlocks,
getMainBlockUid: getMainBlockUid,
loadPeers: loadPeers,
isBusy: isBusy,
// api extension
api: api
};
};
var service = factory('default');
service.instance = factory;
return service;
}]);
ESPicturesEditController.$inject = ['$scope', 'UIUtils', '$q', 'Device'];
ESSocialsEditController.$inject = ['$scope', '$focus', '$filter', 'UIUtils', 'SocialUtils'];
ESSocialsViewController.$inject = ['$scope'];
ESCommentsController.$inject = ['$scope', '$filter', '$state', '$focus', '$timeout', '$anchorScroll', 'UIUtils'];
ESCategoryModalController.$inject = ['$scope', 'UIUtils', '$timeout', 'parameters'];
ESAvatarModalController.$inject = ['$scope'];
ESPositionEditController.$inject = ['$scope', 'csConfig', 'esGeo', 'ModalUtils'];
ESLookupPositionController.$inject = ['$scope', '$q', 'csConfig', 'esGeo', 'ModalUtils'];
ESSearchPositionItemController.$inject = ['$scope', '$timeout', 'UIUtils', 'ModalUtils', 'csConfig', 'esGeo'];
ESSearchPositionModalController.$inject = ['$scope', '$q', '$translate', 'esGeo', 'parameters'];
ESLikesController.$inject = ['$scope', '$q', '$timeout', '$translate', '$ionicPopup', 'UIUtils', 'csWallet', 'esHttp'];angular.module('cesium.es.common.controllers', ['ngResource', 'cesium.es.services'])
.controller('ESPicturesEditCtrl', ESPicturesEditController)
.controller('ESPicturesEditCtrl', ESPicturesEditController)
.controller('ESSocialsEditCtrl', ESSocialsEditController)
.controller('ESSocialsViewCtrl', ESSocialsViewController)
.controller('ESCommentsCtrl', ESCommentsController)
.controller('ESCategoryModalCtrl', ESCategoryModalController)
.controller('ESAvatarModalCtrl', ESAvatarModalController)
.controller('ESPositionEditCtrl', ESPositionEditController)
.controller('ESLookupPositionCtrl', ESLookupPositionController)
.controller('ESSearchPositionItemCtrl', ESSearchPositionItemController)
.controller('ESSearchPositionModalCtrl', ESSearchPositionModalController)
.controller('ESLikesCtrl', ESLikesController)
;
function ESPicturesEditController($scope, UIUtils, $q, Device) {
'ngInject';
$scope.selectNewPicture = function(inputSelector) {
if (Device.enable){
$scope.openPicturePopup();
}
else {
var fileInput = angular.element(document.querySelector(inputSelector||'#pictureFile'));
if (fileInput && fileInput.length > 0) {
fileInput[0].click();
}
}
};
$scope.openPicturePopup = function() {
Device.camera.getPicture()
.then(function(imageData) {
$scope.pictures.push({
src: "data:image/png;base64," + imageData,
isnew: true // use to prevent visibility hidden (if animation)
});
})
.catch(UIUtils.onError('ERROR.TAKE_PICTURE_FAILED'));
};
$scope.fileChanged = function(event) {
if (!event.target.files || !event.target.files.length) return;
UIUtils.loading.show();
var file = event.target.files[0];
return UIUtils.image.resizeFile(file)
.then(function(imageData) {
$scope.pictures.push({
src: imageData,
isnew: true // use to prevent visibility hidden (if animation)
});
event.target.value = ""; // reset input[type=file]
UIUtils.loading.hide(100);
})
.catch(function(err) {
console.error(err);
event.target.value = ""; // reset input[type=file]
UIUtils.loading.hide();
});
};
$scope.removePicture = function(index){
$scope.pictures.splice(index, 1);
};
$scope.favoritePicture = function(index){
if (index > 0) {
var item = $scope.pictures[index];
$scope.pictures.splice(index, 1);
$scope.pictures.splice(0, 0, item);
}
};
$scope.rotatePicture = function(index){
var item = $scope.pictures[index];
UIUtils.image.rotateSrc(item.src)
.then(function(dataURL){
item.src = dataURL;
});
};
}
function ESCategoryModalController($scope, UIUtils, $timeout, parameters) {
'ngInject';
$scope.loading = true;
$scope.allCategories = [];
$scope.categories = [];
this.searchText = '';
// modal title
this.title = parameters && parameters.title;
$scope.afterLoad = function(result) {
$scope.categories = result;
$scope.allCategories = result;
$scope.loading = false;
$timeout(function() {
UIUtils.ink();
}, 10);
};
this.doSearch = function() {
var searchText = this.searchText.toLowerCase().trim();
if (searchText.length > 1) {
$scope.loading = true;
$scope.categories = $scope.allCategories.reduce(function(result, cat) {
if (cat.parent && cat.name.toLowerCase().search(searchText) != -1) {
return result.concat(cat);
}
return result;
}, []);
$scope.loading = false;
}
else {
$scope.categories = $scope.allCategories;
}
};
// load categories
if (parameters && parameters.categories) {
$scope.afterLoad(parameters.categories);
}
else if (parameters && parameters.load) {
parameters.load()
.then(function(res){
$scope.afterLoad(res);
});
}
}
function ESCommentsController($scope, $filter, $state, $focus, $timeout, $anchorScroll, UIUtils) {
'ngInject';
$scope.loading = true;
$scope.defaultCommentSize = 5;
$scope.formData = {};
$scope.comments = {};
$scope.$on('$recordView.enter', function(e, state) {
// First enter
if ($scope.loading) {
$scope.anchor = state && state.stateParams.anchor;
}
// second call (when using cached view)
else if ($scope.id) {
$scope.load($scope.id, {animate: false});
}
});
$scope.$on('$recordView.load', function(event, id, service) {
$scope.id = id || $scope.id;
$scope.service = service.comment || $scope.service;
console.debug("[ES] [comment] Will use {" + $scope.service.index + "} service");
if ($scope.id) {
$scope.load($scope.id)
.then(function() {
return $timeout(function() {
// Scroll to anchor
$scope.scrollToAnchor();
}, 500);
});
}
});
$scope.load = function(id, options) {
options = options || {};
options.from = options.from || 0;
// If anchor has been defined, load all comments
options.size = options.size || ($scope.anchor && -1/*all*/);
options.size = options.size || $scope.defaultCommentSize;
options.animate = angular.isDefined(options.animate) ? options.animate : true;
options.loadAvatarAllParent = angular.isDefined(options.loadAvatarAllParent) ? options.loadAvatarAllParent : true;
$scope.loading = true;
return $scope.service.load(id, options)
.then(function(data) {
if (!options.animate && data.result.length) {
_.forEach(data.result, function(cmt) {
cmt.isnew = true;
});
}
$scope.comments = data;
$scope.comments.hasMore = (data.total > data.result.length);
$scope.loading = false;
$scope.service.changes.start(id, data, $scope);
// Set Motion
$scope.motion.show({
selector: '.comments .item',
ink: false
});
});
};
$scope.$on('$recordView.beforeLeave', function(){
if ($scope.comments) {
if (!$scope.service) {
console.error('[comment] Comment controller has no service ! Unable to listen changes...');
return;
}
$scope.service.changes.stop($scope.comments);
}
});
$scope.scrollToAnchor = function() {
if (!$scope.anchor) return;
var elemList = document.getElementsByName($scope.anchor);
// Waiting for the element
if (!elemList || !elemList.length) {
return $timeout($scope.scrollToAnchor, 500);
}
// If many, remove all anchor except the last one
for (var i = 0; i<elemList.length-1; i++) {
angular.element(elemList[i]).remove();
}
// Scroll to the anchor
$anchorScroll($scope.anchor);
// Remove the anchor. This will the CSS class 'positive-100-bg' on the comment
$timeout(function () {
$scope.anchor = null;
}, 1500);
};
$scope.showMore = function(){
var from = 0;
var size = -1;
$scope.load($scope.id, {from: from, size: size, loadAvatarAllParent: false})
.then(function() {
// Set Motion
$scope.motion.show({
selector: '.card-avatar'
});
});
};
$scope.onKeypress = function(event) {
// If Ctrl + Enter: submit
if (event && event.charCode == 10 && event.ctrlKey) {
$scope.save();
event.preventDefault();
}
};
$scope.save = function() {
if (!$scope.formData.message || !$scope.formData.message.length) return;
$scope.loadWallet({minData: true, auth: true})
.then(function() {
UIUtils.loading.hide();
var comment = $scope.formData;
$scope.formData = {};
$scope.focusNewComment();
return $scope.service.save($scope.id, $scope.comments, comment);
})
.then(function() {
$scope.comments.total++;
})
.catch(UIUtils.onError('COMMENTS.ERROR.FAILED_SAVE_COMMENT'));
};
$scope.share = function(event, comment) {
var params = angular.copy($state.params);
var stateUrl;
if (params.anchor) {
params.anchor= $filter('formatHash')(comment.id);
stateUrl = $state.href($state.current.name, params, {absolute: true});
}
else {
stateUrl = $state.href($state.current.name, params, {absolute: true}) + '/' + $filter('formatHash')(comment.id);
}
var index = _.findIndex($scope.comments.result, {id: comment.id});
var url = stateUrl + '?u=' + (comment.uid||$filter('formatPubkey')(comment.issuer));
UIUtils.popover.show(event, {
templateUrl: 'templates/common/popover_share.html',
scope: $scope,
bindings: {
titleKey: 'COMMENTS.POPOVER_SHARE_TITLE',
titleValues: {number: index ? index + 1 : 1},
date: comment.creationTime,
value: url,
postUrl: stateUrl,
postMessage: comment.message
},
autoselect: '.popover-share input'
});
};
$scope.edit = function(comment) {
var newComment = new Comment();
newComment.copy(comment);
$scope.formData = newComment;
};
$scope.remove = function(comment) {
if (!comment) {return;}
comment.remove();
$scope.comments.total--;
};
$scope.reply = function(parent) {
if (!parent || !parent.id) {return;}
$scope.formData = {
parent: parent
};
$scope.focusNewComment(true);
};
$scope.cancel = function() {
$scope.formData = {};
$scope.focusNewComment();
};
$scope.focusNewComment = function(forceIfSmall) {
if (!UIUtils.screen.isSmall()) {
$focus('comment-form-textarea');
}
else {
if (forceIfSmall) $focus('comment-form-input');
}
};
$scope.removeParentLink = function() {
delete $scope.formData.parent;
delete $scope.formData.reply_to;
$scope.focusNewComment();
};
$scope.toggleExpandedReplies = function(comment, index) {
comment.expandedReplies = comment.expandedReplies || {};
comment.expandedReplies[index] = !comment.expandedReplies[index];
};
$scope.toggleExpandedParent = function(comment, index) {
comment.expandedParent = comment.expandedParent || {};
comment.expandedParent[index] = !comment.expandedParent[index];
};
}
function ESSocialsEditController($scope, $focus, $filter, UIUtils, SocialUtils) {
'ngInject';
$scope.socialData = {
url: null,
reorder: false
};
$scope.addSocialNetwork = function() {
if (!$scope.socialData.url || $scope.socialData.url.trim().length === 0) {
return;
}
$scope.formData.socials = $scope.formData.socials || [];
var url = $scope.socialData.url.trim();
var exists = _.findWhere($scope.formData.socials, {url: url});
if (exists) { // duplicate entry
$scope.socialData.url = '';
return;
}
var social = SocialUtils.get(url);
if (!social) {
UIUtils.alert.error('PROFILE.ERROR.INVALID_SOCIAL_NETWORK_FORMAT');
$focus('socialUrl');
return; // stop here
}
$scope.formData.socials.push(social);
$scope.socialData.url = '';
// Set Motion
$scope.motion.show({
selector: '#social-' + $filter('formatSlug')(social.url),
startVelocity: 10000
});
};
$scope.editSocialNetwork = function(index) {
var social = $scope.formData.socials[index];
$scope.formData.socials.splice(index, 1);
$scope.socialData.url = social.url;
$focus('socialUrl');
};
$scope.reorderSocialNetwork = function(social, fromIndex, toIndex) {
if (!social || fromIndex == toIndex) return; // no changes
$scope.formData.socials.splice(fromIndex, 1);
$scope.formData.socials.splice(toIndex, 0, social);
};
$scope.filterFn = function(social) {
return !social.recipient || social.valid;
};
}
function ESSocialsViewController($scope) {
'ngInject';
$scope.openSocial = function(event, social) {
event.stopPropagation();
return $scope.openLink(event, social.url, {
type: social.type
});
};
$scope.filterFn = function(social) {
return !social.recipient || social.valid;
};
}
function ESAvatarModalController($scope) {
$scope.formData = {
initCrop: false,
imageCropStep: 0,
imgSrc: undefined,
result: undefined,
resultBlob: undefined
};
$scope.openFileSelector = function() {
var fileInput = angular.element(document.querySelector('.modal-avatar #fileInput'));
if (fileInput && fileInput.length > 0) {
fileInput[0].click();
}
};
$scope.fileChanged = function(e) {
var files = e.target.files;
var fileReader = new FileReader();
fileReader.readAsDataURL(files[0]);
fileReader.onload = function(e) {
var res = this.result;
$scope.$applyAsync(function() {
$scope.formData.imgSrc = res;
});
};
};
$scope.doNext = function() {
if ($scope.formData.imageCropStep == 2) {
$scope.doCrop();
}
else if ($scope.formData.imageCropStep == 3) {
$scope.closeModal($scope.formData.result);
}
};
$scope.doCrop = function() {
$scope.formData.initCrop = true;
};
$scope.clear = function() {
$scope.formData = {
initCrop: false,
imageCropStep: 1,
imgSrc: undefined,
result: undefined,
resultBlob: undefined
};
};
}
function ESPositionEditController($scope, csConfig, esGeo, ModalUtils) {
'ngInject';
// The default country used for address localisation
var defaultCountry = csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.defaultCountry;
var loadingCurrentPosition = false;
$scope.options = $scope.options || {};
$scope.options.position = $scope.options.position || {
showCheckbox: true,
required: false
};
$scope.formPosition = {
loading: false,
enable: angular.isDefined($scope.options.position.required) ? $scope.options.position.required : false
};
$scope.tryToLocalize = function() {
if ($scope.formPosition.loading || loadingCurrentPosition) return;
var searchText = $scope.getAddressToSearch();
// No address, so try to localize by device
if (!searchText) {
loadingCurrentPosition = true;
return esGeo.point.current()
.then($scope.updateGeoPoint)
.then(function() {
loadingCurrentPosition = false;
})
.catch(function(err) {
console.error(err); // Silent
loadingCurrentPosition = false;
});
}
$scope.formPosition.loading = true;
return esGeo.point.searchByAddress(searchText)
.then(function(res) {
if (res && res.length == 1) {
return $scope.updateGeoPoint(res[0]);
}
return $scope.openSearchLocationModal({
text: searchText,
results: res||[],
forceFallback: !res || !res.length // force fallback search first
});
})
.then(function() {
$scope.formPosition.loading = false;
})
.catch(function(err) {
console.error(err); // Silent
$scope.formPosition.loading = false;
});
};
$scope.onCityChanged = function() {
if ($scope.loading) return;
if ($scope.formPosition.enable) {
if ($scope.formData.geoPoint) {
// Invalidate the position
$scope.formData.geoPoint.lat = undefined;
$scope.formData.geoPoint.lon = undefined;
}
return $scope.tryToLocalize();
}
};
$scope.onUseGeopointChanged = function() {
if ($scope.loading) return;
if (!$scope.formPosition.enable) {
if ($scope.formData.geoPoint) {
$scope.formData.geoPoint.lat = undefined;
$scope.formData.geoPoint.lon = undefined;
$scope.dirty = true;
}
}
else {
$scope.tryToLocalize();
}
};
$scope.onGeopointChanged = function() {
if ($scope.loading) {
$scope.formPosition.enable = $scope.formData.geoPoint && !!$scope.formData.geoPoint.lat && !!$scope.formData.geoPoint.lon;
}
};
$scope.$watch('formData.geoPoint', $scope.onGeopointChanged);
$scope.getAddressToSearch = function() {
return $scope.formData.address && $scope.formData.city ?
[$scope.formData.address.trim(), $scope.formData.city.trim()].join(', ') :
$scope.formData.city || $scope.formData.address || $scope.formData.location ;
};
$scope.updateGeoPoint = function(res) {
// user cancel
if (!res || !res.lat || !res.lon) {
// nothing to do
return;
}
$scope.dirty = true;
$scope.formData.geoPoint = $scope.formData.geoPoint || {};
$scope.formData.geoPoint.lat = parseFloat(res.lat);
$scope.formData.geoPoint.lon = parseFloat(res.lon);
if (res.address && res.address.city) {
var cityParts = [res.address.city];
if (res.address.postcode) {
cityParts.push(res.address.postcode);
}
if (res.address.country != defaultCountry) {
cityParts.push(res.address.country);
}
$scope.formData.city = cityParts.join(', ');
}
};
/* -- modal -- */
$scope.openSearchLocationModal = function(options) {
options = options || {};
var parameters = {
text: options.text || $scope.getAddressToSearch(),
results: options.results,
fallbackText: options.fallbackText || $scope.formData.city,
forceFallback: angular.isDefined(options.forceFallback) ? options.forceFallback : undefined
};
return ModalUtils.show(
'plugins/es/templates/common/modal_location.html',
'ESSearchPositionModalCtrl',
parameters,
{
focusFirstInput: true
//,scope: $scope
}
)
.then($scope.updateGeoPoint);
};
}
function ESLookupPositionController($scope, $q, csConfig, esGeo, ModalUtils) {
'ngInject';
// The default country used for address localisation
var defaultCountry = csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.defaultCountry;
var loadingPosition = false;
$scope.geoDistanceLabels = [5,10,20,50,100,250,500].reduce(function(res, distance){
res[distance] = {
labelKey: 'LOCATION.DISTANCE_OPTION',
labelParams: {value: distance}
};
return res;
}, {});
$scope.geoDistances = _.keys($scope.geoDistanceLabels);
$scope.searchPosition = function(searchText) {
if (loadingPosition) return $q.when();
loadingPosition = true;
// No address, so try to localize by device
var promise = !searchText ?
esGeo.point.current() :
esGeo.point.searchByAddress(searchText)
.then(function(res) {
if (res && res.length == 1) {
res[0].exact = true;
return res[0];
}
return $scope.openSearchLocationModal({
text: searchText,
results: res||[],
forceFallback: !res || !res.length // force fallback search first
})
.then(function(res) {
// Compute point name
if (res && res.address && res.address.city) {
var cityParts = [res.address.city];
if (res.address.postcode) {
cityParts.push(res.address.postcode);
}
if (res.address.country != defaultCountry) {
cityParts.push(res.address.country);
}
res.shortName = cityParts.join(', ');
}
return res;
});
});
promise
.then(function(res) {
loadingPosition = false;
// user cancel
if (!res || !res.lat || !res.lon) return;
return {
lat: parseFloat(res.lat),
lon: parseFloat(res.lon),
name: res.shortName,
exact: res.exact
};
})
.catch(function(err) {
console.error(err); // Silent
loadingPosition = false;
});
return promise;
};
/* -- modal -- */
$scope.openSearchLocationModal = function(options) {
options = options || {};
var parameters = {
text: options.text || $scope.getAddressToSearch(),
results: options.results,
fallbackText: options.fallbackText || $scope.search.location,
forceFallback: angular.isDefined(options.forceFallback) ? options.forceFallback : undefined
};
return ModalUtils.show(
'plugins/es/templates/common/modal_location.html',
'ESSearchPositionModalCtrl',
parameters,
{
focusFirstInput: true
//,scope: $scope
}
);
};
}
function ESSearchPositionItemController($scope, $timeout, UIUtils, ModalUtils, csConfig, esGeo) {
'ngInject';
// The default country used for address localisation
var defaultCountry = csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.defaultCountry;
var loadingPosition = false;
var minLength = 3;
$scope.locations = undefined;
$scope.selectLocationIndex = -1;
$scope.onKeydown = function(e) {
switch(e.keyCode)
{
case 27://Esc
$scope.hideDropdown();
break;
case 13://Enter
if($scope.locations && $scope.locations.length)
$scope.onEnter();
break;
case 38://Up
$scope.onArrowUpOrDown(-1);
e.preventDefault();
break;
case 40://Down
$scope.onArrowUpOrDown(1);
e.preventDefault();
break;
case 8://Backspace
case 45://Insert
case 46://Delete
break;
case 37://Left
case 39://Right
case 16://Shift
case 17://Ctrl
case 35://End
case 36://Home
break;
default://All keys
$scope.showDropdown();
}
};
$scope.onEnter = function() {
if ($scope.selectLocationIndex > -1) {
$scope.selectLocation($scope.locations[$scope.selectLocationIndex]);
}
else {
$scope.selectLocation($scope.locations[0]);
}
};
$scope.onArrowUpOrDown = function(velocity) {
if (!$scope.locations) return;
$scope.selectLocationIndex+=velocity;
if ($scope.selectLocationIndex >= $scope.locations.length) {
$scope.selectLocationIndex = 0;
}
if ($scope.selectLocationIndex < 0) {
$scope.selectLocationIndex = $scope.locations.length-1;
}
_.forEach($scope.locations||[], function(item, index) {
item.selected = (index == $scope.selectLocationIndex);
});
// TODO: scroll to item ?
};
$scope.onLocationChanged = function() {
if (loadingPosition || $scope.search.loading) return;
$scope.search.geoPoint = undefined; // reset geo point
$scope.showDropdown();
};
$scope.showDropdown = function() {
var text = $scope.search.location && $scope.search.location.trim();
if (!text || text.length < minLength) {
return $scope.hideDropdown(true/*force, if still loading*/);
}
// Compute a request id, to apply response only if current request
var requestId = ($scope.requestId && $scope.requestId + 1) || 1;
$scope.requestId = requestId;
loadingPosition = true;
// Execute the given query
return esGeo.point.searchByAddress(text)
.then(function(res) {
if ($scope.requestId != requestId) return; // Skip apply if not same request:
loadingPosition = false;
$scope.locations = res||[];
$scope.license = res && res.length && res[0].license;
})
.catch(function(err) {
$scope.hideDropdown();
throw err;
});
};
$scope.hideDropdown = function(force) {
// force, even if still loading
if (force) {
$scope.locations = undefined;
$scope.selectLocationIndex = -1;
$scope.license = undefined;
loadingPosition = false;
return;
}
return $timeout(function() {
if (loadingPosition) return;
$scope.locations = undefined;
$scope.license = undefined;
loadingPosition = false;
}, 500);
};
$scope.selectLocation = function(res, exactMatch) {
loadingPosition = true; // avoid event
if (res) {
// Update position
$scope.search.geoPoint = $scope.search.geoPoint || {};
$scope.search.geoPoint.lat = parseFloat(res.lat);
$scope.search.geoPoint.lon = parseFloat(res.lon);
if (exactMatch) {
$scope.search.geoPoint.exact = true;
}
else {
// Update location name
if (res && res.address && res.address.city) {
var cityParts = [res.address.city];
if (res.address.postcode) {
cityParts.push(res.address.postcode);
}
if (res.address.country != defaultCountry) {
cityParts.push(res.address.country);
}
$scope.search.location = cityParts.join(', ');
}
}
}
$scope.hideDropdown(true);
};
/* -- modal -- */
$scope.openSearchLocationModal = function(options) {
options = options || {
text: $scope.search.location
};
var parameters = {
text: options.text || $scope.search.location
};
return ModalUtils.show(
'plugins/es/templates/common/modal_location.html',
'ESSearchPositionModalCtrl',
parameters,
{
focusFirstInput: true
}
)
.then($scope.selectLocation);
};
/* -- popover -- */
$scope.showDistancePopover = function(event) {
UIUtils.popover.show(event, {
templateUrl: 'plugins/es/templates/common/popover_distances.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.actionsPopover = popover;
}
});
};
$scope.selectDistance = function(value) {
$scope.search.geoDistance = value;
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
}
function ESSearchPositionModalController($scope, $q, $translate, esGeo, parameters) {
'ngInject';
$scope.search = {
text: parameters.text || '',
fallbackText: parameters.fallbackText || undefined,
forceFallback: angular.isDefined(parameters.forceFallback) ? parameters.forceFallback : false,
loading: false,
results: parameters.results || undefined
};
$scope.$on('modal.shown', function() {
// Load search
$scope.doSearch(true/*first search*/);
});
$scope.doSearch = function(firstSearch) {
var text = $scope.search.text && $scope.search.text.trim();
if (!text) {
return $q.when(); // nothing to search
}
$scope.search.loading = true;
// Compute alternative query text
var fallbackText = firstSearch && $scope.search.fallbackText && $scope.search.fallbackText.trim();
fallbackText = fallbackText && fallbackText != text ? fallbackText : undefined;
// Execute the given query
return ((firstSearch && $scope.search.forceFallback && $scope.search.results) ?
$q.when($scope.search.results) :
esGeo.point.searchByAddress(text)
)
.then(function(res) {
if (res && res.length || !fallbackText) return res;
// Fallback search
return $q.all([
$translate('LOCATION.MODAL.ALTERNATIVE_RESULT_DIVIDER', {address: fallbackText}),
esGeo.point.searchByAddress(fallbackText)
])
.then(function (res) {
var dividerText = res[0];
res = res[1];
if (!res || !res.length) return res;
return [{name: dividerText}].concat(res);
});
})
.then(function(res) {
$scope.search.loading = false;
$scope.search.results = res||[];
$scope.license = res && res.length && res[0].license;
})
.catch(function(err) {
$scope.search.loading = false;
$scope.search.results = [];
$scope.license = undefined;
throw err;
})
;
};
}
function ESLikesController($scope, $q, $timeout, $translate, $ionicPopup, UIUtils, csWallet, esHttp) {
'ngInject';
$scope.entered = false;
$scope.abuseData = {};
$scope.abuseLevels = [
{value: 1, label: 'LOW'},
{value: 2, label: 'LOW'}
];
$scope.staring = false;
$scope.options = $scope.options || {};
$scope.options.like = $scope.options.like || {
kinds: esHttp.constants.like.KINDS,
index: undefined,
type: undefined,
id: undefined
};
$scope.$on('$recordView.enter', function(e, state) {
// First enter
if (!$scope.entered) {
$scope.entered = false;
// Nothing to do: main controller will trigger '$recordView.load' event
}
// second call (e.g. if cache)
else if ($scope.id) {
$scope.loadLikes($scope.id);
}
});
$scope.$on('$recordView.load', function(event, id) {
$scope.id = id || $scope.id;
if ($scope.id) {
$scope.loadLikes($scope.id);
}
});
// Init Like service
$scope.initLikes = function() {
if (!$scope.likeData) {
throw Error("Missing 'likeData' in scope. Cannot load likes counter");
}
if (!$scope.options.like.service) {
if (!$scope.options.like.index || !$scope.options.like.type) {
throw Error("Missing 'options.like.index' or 'options.like.type' in scope. Cannot load likes counter");
}
$scope.options.like.service = {
count: esHttp.like.count($scope.options.like.index, $scope.options.like.type),
add: esHttp.like.add($scope.options.like.index, $scope.options.like.type),
remove: esHttp.like.remove($scope.options.like.index, $scope.options.like.type),
toggle: esHttp.like.toggle($scope.options.like.index, $scope.options.like.type)
};
}
if (!$scope.options.like.kinds) {
// Get scope's kinds (e.g. defined in the parent scope)
$scope.options.like.kinds = _.filter(esHttp.constants.like.KINDS, function (kind) {
var key = kind.toLowerCase() + 's';
return angular.isDefined($scope.likeData[key]);
});
}
};
$scope.loadLikes = function(id) {
if ($scope.likeData.loading) return;// Skip
id = id || $scope.likeData.id;
$scope.initLikes();
var kinds = $scope.options.like.kinds || [];
if (!kinds.length) return; // skip
$scope.likeData.loading = true;
var now = Date.now();
console.debug("[ES] Loading counter of {0}... ({1})".format(id.substring(0,8), kinds));
return $q.all(_.map(kinds, function(kind) {
var key = kind.toLowerCase() + 's';
return $scope.options.like.service.count(id, {issuer: csWallet.isLogin() ? csWallet.data.pubkey : undefined, kind: kind})
.then(function (res) {
// Store result to scope
if ($scope.likeData[key]) {
angular.merge($scope.likeData[key], res);
}
});
}))
.then(function () {
$scope.likeData.id = id;
console.debug("[ES] Loading counter of {0} [OK] in {1}ms".format(id.substring(0,8), Date.now()-now));
if (_.contains(kinds, 'VIEW') && !$scope.canEdit) {
$scope.markAsView();
}
// Publish to parent scope (to be able to use it in action popover's buttons)
if ($scope.$parent) {
console.debug("[ES] [likes] Adding data and functions to parent scope");
$scope.$parent.toggleLike = $scope.toggleLike;
$scope.$parent.reportAbuse = $scope.reportAbuse;
}
$scope.likeData.loading = false;
})
.catch(function (err) {
console.error(err && err.message || err);
$scope.likeData.loading = false;
});
};
$scope.markAsView = function() {
if (!$scope.likeData || !$scope.likeData.views || $scope.likeData.views.wasHit) return; // Already view
var canEdit = $scope.canEdit || $scope.formData && csWallet.isUserPubkey($scope.formData.issuer);
if (canEdit) return; // User is the record's issuer: skip
var timer = $timeout(function() {
if (csWallet.isLogin()) {
$scope.options.like.service.add($scope.likeData.id, {kind: 'view'}).then(function() {
$scope.likeData.views.total = ($scope.likeData.views.total||0) + 1;
});
}
timer = null;
}, 3000);
$scope.$on("$destroy", function() {
if (timer) $timeout.cancel(timer);
});
};
$scope.toggleLike = function(event, options) {
$scope.initLikes();
if (!$scope.likeData.id) throw Error("Missing 'likeData.id' in scope. Cannot apply toggle");
options = options || {};
options.kind = options.kind && options.kind.toUpperCase() || 'LIKE';
var key = options.kind.toLowerCase() + 's';
$scope.likeData[key] = $scope.likeData[key] || {};
// Avoid too many call
if ($scope.likeData[key].loading === true || $scope.likeData.loading) {
event.preventDefault();
return $q.reject();
}
// Like/dislike should be inversed
if (options.kind === 'LIKE' && $scope.dislikes && $scope.dislikes.wasHit) {
return $scope.toggleLike(event, {kind: 'dislike'})
.then(function() {
$scope.toggleLike(event, options);
})
}
else if (options.kind === 'DISLIKE' && $scope.likes && $scope.likes.wasHit) {
return $scope.toggleLike(event, {kind: 'LIKE'})
.then(function() {
$scope.toggleLike(event, options);
})
}
$scope.likeData[key].loading = true;
// Make sure user is log in
return (csWallet.isLogin() ? $q.when() : $scope.loadWallet({minData: true}))
.then(function() {
// Apply like
return $scope.options.like.service.toggle($scope.likeData.id, options);
})
.then(function(delta) {
UIUtils.loading.hide();
if (delta !== 0) {
$scope.likeData[key].total = ($scope.likeData[key].total || 0) + delta;
$scope.likeData[key].wasHit = delta > 0;
}
$timeout(function() {
$scope.likeData[key].loading = false;
}, 1000);
})
.catch(function(err) {
console.error(err);
$scope.likeData[key].loading = false;
UIUtils.loading.hide();
event.preventDefault();
});
};
$scope.setAbuseForm = function(form) {
$scope.abuseForm = form;
};
$scope.showAbuseCommentPopover = function(event) {
return $translate(['COMMON.REPORT_ABUSE.TITLE', 'COMMON.REPORT_ABUSE.SUB_TITLE','COMMON.BTN_SEND', 'COMMON.BTN_CANCEL'])
.then(function(translations) {
UIUtils.loading.hide();
return $ionicPopup.show({
templateUrl: 'plugins/es/templates/common/popup_report_abuse.html',
title: translations['COMMON.REPORT_ABUSE.TITLE'],
subTitle: translations['COMMON.REPORT_ABUSE.SUB_TITLE'],
cssClass: 'popup-report-abuse',
scope: $scope,
buttons: [
{
text: translations['COMMON.BTN_CANCEL'],
type: 'button-stable button-clear gray'
},
{
text: translations['COMMON.BTN_SEND'],
type: 'button button-positive ink',
onTap: function(e) {
$scope.abuseForm.$submitted=true;
if(!$scope.abuseForm.$valid || !$scope.abuseData.comment) {
//don't allow the user to close unless he enters a uid
e.preventDefault();
} else {
return $scope.abuseData;
}
}
}
]
});
})
.then(function(res) {
$scope.abuseData = {};
if (!res || !res.comment) { // user cancel
UIUtils.loading.hide();
return undefined;
}
return res;
});
};
$scope.reportAbuse = function(event, options) {
if ($scope.likeData && $scope.likeData.abuses && $scope.likeData.abuses.wasHit) return; // Abuse already reported
options = options || {};
if (!options.comment) {
return (csWallet.isLogin() ? $q.when() : $scope.loadData({minData: true}))
// Ask a comment
.then(function() {
return $scope.showAbuseCommentPopover(event);
})
// Loop, with options.comment filled
.then(function(res) {
if (!res || !res.comment) return; // Empty comment: skip
options.comment = res.comment;
options.level = res.level || (res.delete && 5) || undefined;
return $scope.reportAbuse(event, options) // Loop, with the comment
});
}
// Send abuse report
options.kind = 'ABUSE';
return $scope.toggleLike(event, options)
.then(function() {
UIUtils.toast.show('COMMON.REPORT_ABUSE.CONFIRM.SENT')
})
};
$scope.addStar = function(level) {
if ($scope.starsPopover) {
return $scope.starsPopover.hide()
.then(function() {
$scope.starsPopover = null;
$scope.addStar(level); // Loop
})
}
if ($scope.likeData.loading || !$scope.likeData.stars || $scope.likeData.stars.loading) return; // Avoid multiple call
if (!csWallet.isLogin()) {
return $scope.loadWallet({minData: true})
.then(function(walletData) {
if (!walletData) return; // skip
UIUtils.loading.show();
// Reload the counter, to known if user already has
return $scope.options.like.service.count($scope.likeData.id, {issuer: walletData.pubkey, kind: 'STAR'})
.then(function(stars) {
angular.merge($scope.stars, stars);
$scope.addStar(level); // Loop
})
})
.catch(function(err) {
if (err === 'CANCELLED') return; // User cancelled
// Refresh current like
});
}
$scope.likeData.stars.loading = true;
var stars = angular.merge(
{total: 0, levelAvg: 0, levelSum: 0, level: 0, wasHit: false, wasHitId: undefined},
$scope.likeData.stars);
var successFunction = function() {
stars.wasHit = true;
stars.level = level;
// Compute AVG (round to near 0.5)
stars.levelAvg = Math.floor((stars.levelSum / stars.total + 0.5) * 10) / 10 - 0.5;
// Update the star level
angular.merge($scope.likeData.stars, stars);
UIUtils.loading.hide();
};
// Already hit: remove previous star, before inserted a new one
if (stars.wasHitId) {
console.debug("[ES] Deleting previous star level... " + stars.wasHitId);
return $scope.options.like.service.remove(stars.wasHitId)
.catch(function(err) {
// Not found, so continue
if (err && err.ucode === 404) return;
else throw err;
})
.then(function() {
console.debug("[ES] Deleting previous star level [OK]");
stars.levelSum = stars.levelSum - stars.level + level;
successFunction();
// Add the star (after a delay, to make sure deletion has been executed)
return $timeout(function() {
console.debug("[ES] Sending new star level...");
return $scope.options.like.service.add($scope.likeData.id, {kind: 'star', level: level || 1});
}, 2000);
})
.then(function(newHitId) {
stars.wasHitId = newHitId;
console.debug("[ES] Star level successfully sent... " + newHitId);
UIUtils.loading.hide();
return $timeout(function() {
$scope.likeData.stars.loading = false;
}, 1000);
})
.catch(function(err) {
console.error(err && err.message || err);
$scope.likeData.stars.loading = false;
UIUtils.onError('MARKET.WOT.ERROR.FAILED_STAR_PROFILE')(err);
// Reload, to force refresh state
$scope.loadLikes();
});
}
return $scope.options.like.service.add($scope.likeData.id, {kind: 'star', level: level || 1})
.then(function(newHitId) {
stars.levelSum += level;
stars.wasHitId = newHitId;
stars.total += 1;
successFunction();
console.debug("[ES] Star level successfully sent... " + newHitId);
$scope.likeData.stars.loading = false;
UIUtils.loading.hide();
})
.catch(function(err) {
console.error(err && err.message || err);
$scope.likeData.stars.loading = false;
UIUtils.onError('MARKET.WOT.ERROR.FAILED_STAR_PROFILE')(err);
});
};
$scope.removeStar = function(event) {
if ($scope.starsPopover) $scope.starsPopover.hide();
if ($scope.likeData.loading) return; // Skip
$scope.likeData.stars.level = undefined;
$scope.toggleLike(event, {kind: 'star'})
.then(function() {
return $timeout(function() {
$scope.loadLikes(); // refresh
}, 1000);
});
};
$scope.showStarPopover = function(event) {
$scope.initLikes();
if ($scope.likeData.stars.loading) return; // Avoid multiple call
if (angular.isUndefined($scope.likeData.stars.level)) {
$scope.likeData.stars.level = 0;
}
UIUtils.popover.show(event, {
templateUrl: 'plugins/es/templates/common/popover_star.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.starsPopover = popover;
}
})
};
csWallet.api.data.on.reset($scope, function() {
_.forEach($scope.options.like.kinds||[], function(kind) {
var key = kind.toLowerCase() + 's';
if ($scope.likeData[key]) {
$scope.likeData[key].wasHit = false;
$scope.likeData[key].wasHitId = undefined;
$scope.likeData[key].level = undefined;
}
})
}, this);
}
ESExtensionController.$inject = ['$scope', 'esSettings', 'PluginService'];
ESJoinController.$inject = ['$scope', 'esSettings', 'PluginService'];
ESMenuExtendController.$inject = ['$scope', '$state', 'PluginService', 'esSettings', 'UIUtils'];
ESProfilePopoverExtendController.$inject = ['$scope', '$state', 'csSettings', 'csWallet'];angular.module('cesium.es.app.controllers', ['ngResource', 'cesium.es.services'])
// Configure menu items
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Menu extension points
PluginServiceProvider.extendState('app', {
points: {
'nav-buttons-right': {
templateUrl: "plugins/es/templates/menu_extend.html",
controller: "ESMenuExtendCtrl"
},
'menu-discover': {
templateUrl: "plugins/es/templates/menu_extend.html",
controller: "ESMenuExtendCtrl"
},
'menu-user': {
templateUrl: "plugins/es/templates/menu_extend.html",
controller: "ESMenuExtendCtrl"
}
}
});
// Profile popover extension points
PluginServiceProvider.extendState('app', {
points: {
'profile-popover-user': {
templateUrl: "plugins/es/templates/common/popover_profile_extend.html",
controller: "ESProfilePopoverExtendCtrl"
}
}
});
}
}])
.controller('ESExtensionCtrl', ESExtensionController)
.controller('ESJoinCtrl', ESJoinController)
.controller('ESMenuExtendCtrl', ESMenuExtendController)
.controller('ESProfilePopoverExtendCtrl', ESProfilePopoverExtendController)
;
/**
* Generic controller, that enable/disable depending on esSettings enable/disable
*/
function ESExtensionController($scope, esSettings, PluginService) {
'ngInject';
$scope.extensionPoint = PluginService.extensions.points.current.get();
$scope.enable = esSettings.isEnable();
esSettings.api.state.on.changed($scope, function(enable) {
$scope.enable = enable;
$scope.$broadcast('$$rebind::state');
});
}
/**
* Control new account wizard extend view
*/
function ESJoinController($scope, esSettings, PluginService) {
'ngInject';
$scope.extensionPoint = PluginService.extensions.points.current.get();
$scope.enable = esSettings.isEnable();
esSettings.api.state.on.changed($scope, function(enable) {
$scope.enable = enable;
});
}
/**
* Control menu extension
*/
function ESMenuExtendController($scope, $state, PluginService, esSettings, UIUtils) {
'ngInject';
$scope.extensionPoint = PluginService.extensions.points.current.get();
$scope.enable = esSettings.isEnable();
$scope.showRegistryLookupView = function() {
$state.go(UIUtils.screen.isSmall() ? 'app.registry_lookup': 'app.registry_lookup_lg');
};
$scope.showNotificationsPopover = function(event) {
return UIUtils.popover.show(event, {
templateUrl :'plugins/es/templates/notification/popover_notification.html',
scope: $scope,
autoremove: false // reuse popover
});
};
$scope.showMessagesPopover = function(event) {
return UIUtils.popover.show(event, {
templateUrl :'plugins/es/templates/message/popover_message.html',
scope: $scope,
autoremove: false // reuse popover
});
};
$scope.showInvitationsPopover = function(event) {
return UIUtils.popover.show(event, {
templateUrl :'plugins/es/templates/invitation/popover_invitation.html',
scope: $scope,
autoremove: false // reuse popover
});
};
esSettings.api.state.on.changed($scope, function(enable) {
$scope.enable = enable;
});
}
/**
* Control profile popover extension
*/
function ESProfilePopoverExtendController($scope, $state, csSettings, csWallet) {
'ngInject';
$scope.updateView = function() {
$scope.enable = csWallet.isLogin() && (
(csSettings.data.plugins && csSettings.data.plugins.es) ?
csSettings.data.plugins.es.enable :
!!csSettings.data.plugins.host);
};
$scope.showEditUserProfile = function() {
$scope.closeProfilePopover();
$state.go('app.user_edit_profile');
};
csSettings.api.data.on.changed($scope, $scope.updateView);
csSettings.api.data.on.ready($scope, $scope.updateView);
csWallet.api.data.on.login($scope, function(data, deferred){
deferred = deferred || $q.defer();
$scope.updateView();
deferred.resolve();
return deferred.promise;
});
csWallet.api.data.on.logout($scope, $scope.updateView);
$scope.updateView();
}
ESPluginSettingsController.$inject = ['$scope', '$window', '$q', '$translate', '$ionicPopup', 'UIUtils', 'Modals', 'csHttp', 'csConfig', 'csSettings', 'esHttp', 'esSettings', 'esModals'];angular.module('cesium.es.settings.controllers', ['cesium.es.services'])
// Configure menu items
.config(['PluginServiceProvider', '$stateProvider', 'csConfig', function(PluginServiceProvider, $stateProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Extend settings via extension points
PluginServiceProvider.extendState('app.settings', {
points: {
'plugins': {
templateUrl: "plugins/es/templates/settings/settings_extend.html",
controller: "ESExtensionCtrl"
}
}
});
$stateProvider
.state('app.es_settings', {
url: "/settings/es",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/settings/plugin_settings.html",
controller: 'ESPluginSettingsCtrl'
}
}
});
}
}])
.controller('ESPluginSettingsCtrl', ESPluginSettingsController)
;
/*
* Settings extend controller
*/
function ESPluginSettingsController ($scope, $window, $q, $translate, $ionicPopup,
UIUtils, Modals, csHttp, csConfig, csSettings, esHttp, esSettings, esModals) {
'ngInject';
$scope.hasWindowNotification = !!("Notification" in window);
$scope.formData = {};
$scope.popupData = {}; // need for the node popup
$scope.loading = true;
$scope.enter= function(e, state) {
$scope.load();
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.load = function(keepEnableState) {
$scope.loading = true;
var wasEnable = $scope.formData.enable;
$scope.formData = csSettings.data.plugins && csSettings.data.plugins.es ?
angular.copy(csSettings.data.plugins.es) : {
enable: false,
host: undefined,
port: undefined
};
if (keepEnableState && wasEnable) {
$scope.formData.enable = wasEnable;
}
$scope.isFallbackNode = $scope.formData.enable && esHttp.node.isFallback();
$scope.server = $scope.getServer(esHttp);
$scope.loading = false;
};
esSettings.api.state.on.changed($scope, function(enable) {
$scope.load(true);
});
$scope.setPopupForm = function(popupForm) {
$scope.popupForm = popupForm;
};
// Change ESnode
$scope.changeEsNode= function(node) {
node = node || {
host: $scope.formData.host,
port: $scope.formData.port && $scope.formData.port != 80 && $scope.formData.port != 443 ? $scope.formData.port : undefined,
useSsl: angular.isDefined($scope.formData.useSsl) ?
$scope.formData.useSsl :
($scope.formData.port == 443)
};
$scope.showNodePopup(node)
.then(function(newNode) {
if (newNode.host == $scope.formData.host &&
newNode.port == $scope.formData.port &&
newNode.useSsl == $scope.formData.useSsl) {
UIUtils.loading.hide();
return; // same node = nothing to do
}
UIUtils.loading.show();
var newEsNode = esHttp.instance(newNode.host, newNode.port, newNode.useSsl);
return newEsNode.isAlive() // ping the node
.then(function(alive) {
if (!alive) {
UIUtils.loading.hide();
return UIUtils.alert.error('ERROR.INVALID_NODE_SUMMARY')
.then(function(){
$scope.changeEsNode(newNode); // loop
});
}
$scope.formData.host = newEsNode.host;
$scope.formData.port = newEsNode.port;
$scope.formData.useSsl = newEsNode.useSsl;
return esHttp.copy(newEsNode);
})
.then(function() {
$scope.server = $scope.getServer(esHttp);
$scope.isFallbackNode = false;
UIUtils.loading.hide();
});
});
};
// Show node popup
$scope.showNodePopup = function(node) {
return $q(function(resolve, reject) {
var parts = [node.host];
if (node.port && node.port != 80) {
parts.push(node.port);
}
$scope.popupData.newNode = parts.join(':');
$scope.popupData.useSsl = angular.isDefined(node.useSsl) ? node.useSsl : (node.port == 443);
if (!!$scope.popupForm) {
$scope.popupForm.$setPristine();
}
$translate(['ES_SETTINGS.POPUP_PEER.TITLE', 'ES_SETTINGS.POPUP_PEER.HELP', 'COMMON.BTN_OK', 'COMMON.BTN_CANCEL'])
.then(function (translations) {
// Choose UID popup
$ionicPopup.show({
templateUrl: 'templates/settings/popup_node.html',
title: translations['ES_SETTINGS.POPUP_PEER.TITLE'],
subTitle: translations['ES_SETTINGS.POPUP_PEER.HELP'],
scope: $scope,
buttons: [
{ text: translations['COMMON.BTN_CANCEL'] },
{
text: translations['COMMON.BTN_OK'],
type: 'button-positive',
onTap: function(e) {
$scope.popupForm.$submitted=true;
if(!$scope.popupForm.$valid || !$scope.popupForm.newNode) {
//don't allow the user to close unless he enters a node
e.preventDefault();
} else {
return {
server: $scope.popupData.newNode,
useSsl: $scope.popupData.useSsl
};
}
}
}
]
})
.then(function(res) {
if (!res) { // user cancel
UIUtils.loading.hide();
return;
}
var parts = res.server.split(':');
parts[1] = parts[1] ? parts[1] : (res.useSsl ? 443 : 80);
resolve({
host: parts[0],
port: parts[1],
useSsl: res.useSsl
});
});
});
});
};
$scope.showNodeList = function() {
// Check if need a filter on SSL node
var forceUseSsl = (csConfig.httpsMode === 'true' || csConfig.httpsMode === true || csConfig.httpsMode === 'force') ||
($window.location && $window.location.protocol === 'https:') ? true : false;
$ionicPopup._popupStack[0].responseDeferred.promise.close();
return esModals.showNetworkLookup({
enableFilter: true,
endpoint: esHttp.constants.ES_GCHANGE_API,
ssl: forceUseSsl ? true: undefined
})
.then(function (peer) {
if (!peer) return;
var esEps = peer.getEndpoints().reduce(function(res, ep){
var esEp = esHttp.node.parseEndPoint(ep);
return esEp ? res.concat(esEp) : res;
}, []);
if (!esEps.length) return;
var ep = esEps[0];
return {
host: (ep.dns ? ep.dns :
(peer.hasValid4(ep) ? ep.ipv4 : ep.ipv6)),
port: ep.port || 80,
useSsl: ep.useSsl || ep.port == 443
};
})
.then(function(newEsNode) {
$scope.changeEsNode(newEsNode); // get back to node popup
});
};
$scope.onFormChanged = function() {
if ($scope.loading) return;
$scope.formData.notifications = $scope.formData.notifications || {};
if ($scope.hasWindowNotification &&
$scope.formData.notifications.emitHtml5 !== (window.Notification.permission === "granted")){
window.Notification.requestPermission(function (permission) {
// If the user accepts, let's create a notification
$scope.formData.notifications.emitHtml5 = (permission === "granted"); // revert to false if permission not granted
$scope.onFormChanged(); // Loop
});
return;
}
$scope.loading = true;
csSettings.data.plugins = csSettings.data.plugins || {};
csSettings.data.plugins.es = csSettings.data.plugins.es ?
angular.merge(csSettings.data.plugins.es, $scope.formData) :
$scope.formData;
// Fix old settings (unused)
delete csSettings.data.plugins.es.newNode;
csSettings.store()
.then(function() {
$scope.loading = false;
});
};
$scope.$watch('formData', $scope.onFormChanged, true);
$scope.getServer = function(node) {
node = node || $scope.formData;
if (!node.host) return undefined;
return csHttp.getServer(node.host, node.port);
};
}
ESWalletViewController.$inject = ['$scope', '$controller', '$state', 'csWallet', 'esModals'];
ESWalletLikesController.$inject = ['$scope', '$controller', 'UIUtils', 'esHttp', 'esProfile'];angular.module('cesium.es.wallet.controllers', ['cesium.es.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
PluginServiceProvider.extendState('app.view_wallet', {
points: {
'hero': {
templateUrl: "plugins/es/templates/wallet/view_wallet_extend.html",
controller: 'ESWalletLikesCtrl'
},
'after-general': {
templateUrl: "plugins/es/templates/wallet/view_wallet_extend.html",
controller: 'ESWalletViewCtrl'
}
}
});
}
}])
.controller('ESWalletViewCtrl', ESWalletViewController)
.controller('ESWalletLikesCtrl', ESWalletLikesController)
;
function ESWalletViewController($scope, $controller, $state, csWallet, esModals) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESExtensionCtrl', {$scope: $scope}));
/* -- modals -- */
$scope.showNewPageModal = function(event) {
return esModals.showNewPage(event);
};
}
function ESWalletLikesController($scope, $controller, UIUtils, esHttp, esProfile) {
'ngInject';
$scope.options = $scope.options || {};
$scope.options.like = $scope.options.like || {
index: 'user',
type: 'profile',
service: esProfile.like
};
$scope.canEdit = true; // Avoid to change like counter itself
// Initialize the super class and extend it.
angular.extend(this, $controller('ESLikesCtrl', {$scope: $scope}));
// Initialize the super class and extend it.
angular.extend(this, $controller('ESExtensionCtrl', {$scope: $scope}));
// Load likes, when profile loaded
$scope.$watch('formData.pubkey', function(pubkey) {
if (pubkey) {
$scope.loadLikes(pubkey);
}
});
}
ESWotIdentityViewController.$inject = ['$scope', '$controller', '$ionicPopover', 'UIUtils', 'csWallet', 'esHttp', 'esProfile', 'esModals'];angular.module('cesium.es.wot.controllers', ['cesium.es.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
PluginServiceProvider
.extendStates(['app.wot_identity', 'app.wot_identity_uid'], {
points: {
'hero': {
templateUrl: "plugins/es/templates/wot/view_identity_extend.html",
controller: 'ESWotIdentityViewCtrl'
},
'general': {
templateUrl: "plugins/es/templates/wot/view_identity_extend.html",
controller: 'ESWotIdentityViewCtrl'
},
'after-general': {
templateUrl: "plugins/es/templates/wot/view_identity_extend.html",
controller: 'ESWotIdentityViewCtrl'
},
'buttons': {
templateUrl: "plugins/es/templates/wot/view_identity_extend.html",
controller: 'ESWotIdentityViewCtrl'
},
'buttons-top-fab': {
templateUrl: "plugins/es/templates/wot/view_identity_extend.html",
controller: 'ESWotIdentityViewCtrl'
},
}
})
;
}
}])
.controller('ESWotIdentityViewCtrl', ESWotIdentityViewController)
;
function ESWotIdentityViewController($scope, $controller, $ionicPopover, UIUtils, csWallet,
esHttp, esProfile, esModals) {
'ngInject';
$scope.options = $scope.options || {};
$scope.options.like = $scope.options.like || {
kinds: esHttp.constants.like.KINDS,
index: 'user',
type: 'profile',
service: esProfile.like
};
$scope.smallscreen = angular.isDefined($scope.smallscreen) ? $scope.smallscreen : UIUtils.screen.isSmall();
// Initialize the super class and extend it.
angular.extend(this, $controller('ESLikesCtrl', {$scope: $scope}));
// Initialize the super class and extend it.
angular.extend(this, $controller('ESExtensionCtrl', {$scope: $scope}));
/* -- modals -- */
$scope.showNewMessageModal = function(confirm) {
return $scope.loadWallet({minData: true})
.then(function() {
UIUtils.loading.hide();
// Ask confirmation, if user has no Cesium+ profil
if (!confirm && !$scope.formData.profile) {
return UIUtils.alert.confirm('MESSAGE.CONFIRM.USER_HAS_NO_PROFILE')
.then(function (confirm) {
// Recursive call (with confirm flag)
if (confirm) return true;
});
}
return true;
})
// Open modal
.then(function(confirm) {
if (!confirm) return false;
return esModals.showMessageCompose({
destPub: $scope.formData.pubkey,
destUid: $scope.formData.name||$scope.formData.uid
})
.then(function(sent) {
if (sent) UIUtils.toast.show('MESSAGE.INFO.MESSAGE_SENT');
});
});
};
/* -- likes -- */
// Load likes, when profile loaded
$scope.$watch('formData.pubkey', function(pubkey) {
if (pubkey) {
$scope.loadLikes(pubkey);
}
});
/* -- Popover -- */
$scope.showActionsPopover = function (event) {
UIUtils.popover.show(event, {
templateUrl: 'plugins/es/templates/wot/view_popover_actions.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.actionsPopover = popover;
}
});
};
$scope.hideActionsPopover = function () {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
$scope.actionsPopover = null;
}
return true;
};
if ($scope.extensionPoint === 'buttons-top-fab') {
// Show fab button, when parent execute motions
$scope.$on('$csExtension.motion', function(event) {
var canCompose = !!$scope.formData.profile;
if (canCompose) {
$scope.showFab('fab-compose-' + $scope.formData.pubkey);
}
});
}
// TODO : for DEV only
/*$timeout(function() {
if ($scope.extensionPoint != 'buttons') return;
$scope.showSuggestCertificationModal();
}, 1000);*/
}
ESViewEditProfileController.$inject = ['$scope', '$rootScope', '$q', '$timeout', '$state', '$focus', '$translate', '$controller', '$ionicHistory', 'UIUtils', 'BMA', 'esHttp', 'esProfile', 'ModalUtils', 'Device'];angular.module('cesium.es.profile.controllers', ['cesium.es.services'])
.config(['$stateProvider', function($stateProvider) {
$stateProvider.state('app.user_edit_profile', {
cache: false,
url: "/wallet/profile/edit",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/user/edit_profile.html",
controller: 'ESViewEditProfileCtrl'
}
},
data: {
auth: true
}
});
}])
.controller('ESViewEditProfileCtrl', ESViewEditProfileController)
;
function ESViewEditProfileController($scope, $rootScope, $q, $timeout, $state, $focus, $translate, $controller, $ionicHistory,
UIUtils, BMA, esHttp, esProfile, ModalUtils, Device) {
'ngInject';
// Initialize the super class and extend it.
// Not need, because already call inside the template
//angular.extend(this, $controller('ESPositionEditCtrl', {$scope: $scope}));
$scope.formData = {
title: null,
description: null,
socials: [],
geoPoint: {}
};
$scope.loading = true;
$scope.dirty = false;
$scope.walletData = null;
$scope.avatar = null;
$scope.existing = false;
$scope.socialData = {
url: null
};
$scope.pubkeyPattern = BMA.regexp.PUBKEY;
$scope.$on('$ionicView.enter', function(e) {
$scope.loadWallet()
.then(function(walletData) {
return $scope.load(walletData);
})
.catch(function(err){
if (err == 'CANCELLED') {
return $scope.close()
.then(UIUtils.loading.hide);
}
UIUtils.onError('PROFILE.ERROR.LOAD_PROFILE_FAILED')(err);
});
});
$scope.$on('$stateChangeStart', function (event, next, nextParams, fromState) {
if ($scope.dirty && !$scope.saving) {
// stop the change state action
event.preventDefault();
if (!$scope.loading) {
$scope.loading = true;
return UIUtils.alert.confirm('CONFIRM.SAVE_BEFORE_LEAVE',
'CONFIRM.SAVE_BEFORE_LEAVE_TITLE', {
cancelText: 'COMMON.BTN_NO',
okText: 'COMMON.BTN_YES_SAVE'
})
.then(function(confirmSave) {
$scope.loading = false;
if (confirmSave) {
$scope.form.$submitted=true;
return $scope.save(false/*silent*/, true/*haswait debounce*/)
.then(function(saved){
if (saved) {
$scope.dirty = false;
}
return saved; // change state only if not error
});
}
else {
$scope.dirty = false;
return true; // ok, change state
}
})
.then(function(confirmGo) {
if (confirmGo) {
// continue to the order state
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go(next.name, nextParams);
}
})
.catch(function(err) {
// Silent
});
}
}
});
$scope.load = function(walletData) {
$scope.loading = true; // to avoid the call of doSave()
return esProfile.get(walletData.pubkey, {
raw: true
})
.then(function(profile) {
if (profile) {
$scope.avatar = esHttp.image.fromAttachment(profile.source.avatar);
$scope.existing = true;
$scope.updateView(walletData, profile.source);
}
else {
$scope.avatar = undefined;
$scope.existing = false;
$scope.updateView(walletData, {});
}
// removeIf(device)
$focus('profile-name');
// endRemoveIf(device)
})
.catch(function(err){
UIUtils.loading.hide(10);
UIUtils.onError('PROFILE.ERROR.LOAD_PROFILE_FAILED')(err);
});
};
$scope.setForm = function(form) {
$scope.form = form;
};
$scope.updateView = function(wallet, profile) {
$scope.walletData = wallet;
$scope.formData = profile;
if (profile.avatar) {
$scope.avatarStyle={'background-image':'url("'+$scope.avatar.src+'")'};
}
$scope.formData.geoPoint = $scope.formData.geoPoint || {};
$scope.motion.show();
UIUtils.loading.hide();
// Update loading - done with a delay, to avoid trigger onFormDataChanged()
$timeout(function() {
$scope.loading = false;
}, 1000);
};
$scope.onFormDataChanged = function() {
if ($scope.loading) return;
$scope.dirty = true;
};
$scope.$watch('formData', $scope.onFormDataChanged, true);
$scope.save = function(silent, hasWaitDebounce) {
if(!$scope.form.$valid || !$rootScope.walletData || $scope.saving) {
return $q.reject();
}
if (!hasWaitDebounce) {
console.debug('[ES] [profile] Waiting debounce end, before saving...');
return $timeout(function() {
return $scope.save(silent, true);
}, 650);
}
$scope.saving = true;
console.debug('[ES] [profile] Saving user profile...');
var onError = function(message) {
return function(err) {
$scope.saving = false;
UIUtils.onError(message)(err);
};
};
var updateWallet = function(formData) {
if (formData) {
$scope.walletData.name = formData.title;
if ($scope.avatar) {
$scope.walletData.avatar = $scope.avatar;
}
else {
delete $scope.walletData.avatar;
}
$scope.walletData.profile = angular.copy(formData);
$scope.walletData.profile.description = esHttp.util.parseAsHtml(formData.description);
}
};
var showSuccessToast = function() {
if (!silent) {
return $translate('PROFILE.INFO.PROFILE_SAVED')
.then(function(message){
UIUtils.toast.show(message);
});
}
};
var doFinishSave = function(formData) {
// Social url must be unique in socials links - Fix #306:
if (formData.socials && formData.socials.length) {
formData.socials = _.uniq(formData.socials, false, function(social) {
return social.url;
});
}
// Workaround for old data
if (formData.position) {
delete formData.position;
}
if (formData.geoPoint && formData.geoPoint.lat && formData.geoPoint.lon) {
formData.geoPoint.lat = parseFloat(formData.geoPoint.lat);
formData.geoPoint.lon = parseFloat(formData.geoPoint.lon);
}
else{
formData.geoPoint = null;
}
if (!$scope.existing) {
return esProfile.add(formData)
.then(function() {
console.info("[ES] [profile] successfully created.");
$scope.existing = true;
$scope.saving = false;
$scope.dirty = false;
updateWallet(formData);
showSuccessToast();
return true;
})
.catch(onError('PROFILE.ERROR.SAVE_PROFILE_FAILED'));
}
else {
return esProfile.update(formData, {id: $rootScope.walletData.pubkey})
.then(function() {
console.info("[ES] Profile successfully updated.");
$scope.saving = false;
$scope.dirty = false;
updateWallet(formData);
showSuccessToast();
return true;
})
.catch(onError('PROFILE.ERROR.SAVE_PROFILE_FAILED'));
}
}; // end of doFinishSave
if ($scope.avatar && $scope.avatar.src) {
return UIUtils.image.resizeSrc($scope.avatar.src, true) // resize to thumbnail
.then(function(imageSrc) {
$scope.formData.avatar = esHttp.image.toAttachment({src: imageSrc});
return doFinishSave($scope.formData);
});
}
else {
delete $scope.formData.avatar;
return doFinishSave($scope.formData);
}
};
$scope.saveAndClose = function() {
return $scope.save()
.then(function(saved) {
if (saved) $scope.close();
});
};
$scope.submitAndSaveAndClose = function() {
$scope.form.$submitted=true;
$scope.saveAndClose();
};
$scope.cancel = function() {
$scope.dirty = false; // force not saved
$scope.close();
};
$scope.close = function() {
return $state.go('app.view_wallet', {refresh: true});
};
$scope.showAvatarModal = function() {
if (Device.camera.enable) {
return Device.camera.getPicture()
.then(function(imageData) {
if (!imageData) return;
$scope.avatar = {src: "data:image/png;base64," + imageData};
$scope.avatarStyle={'background-image':'url("'+imageData+'")'};
$scope.dirty = true;
})
.catch(UIUtils.onError('ERROR.TAKE_PICTURE_FAILED'));
}
else {
return ModalUtils.show('plugins/es/templates/common/modal_edit_avatar.html','ESAvatarModalCtrl',
{})
.then(function(imageData) {
if (!imageData) return;
$scope.avatar = {src: imageData};
$scope.avatarStyle={'background-image':'url("'+imageData+'")'};
$scope.dirty = true;
});
}
};
$scope.rotateAvatar = function(){
if (!$scope.avatar || !$scope.avatar.src || $scope.rotating) return;
$scope.rotating = true;
return UIUtils.image.rotateSrc($scope.avatar.src)
.then(function(imageData){
$scope.avatar.src = imageData;
$scope.avatarStyle={'background-image':'url("'+imageData+'")'};
$scope.dirty = true;
$scope.rotating = false;
})
.catch(function(err) {
console.error(err);
$scope.rotating = false;
});
};
}
ESMessageListController.$inject = ['$scope', '$state', '$translate', '$ionicHistory', '$ionicPopover', '$timeout', 'esModals', 'UIUtils', 'csWallet', 'esMessage'];
ESMessageComposeController.$inject = ['$scope', '$controller', 'UIUtils'];
ESMessageComposeModalController.$inject = ['$scope', 'Modals', 'UIUtils', 'csWallet', 'esHttp', 'esMessage', 'parameters'];
ESMessageViewController.$inject = ['$scope', '$state', '$timeout', '$translate', '$ionicHistory', '$ionicPopover', 'UIUtils', 'esModals', 'esMessage'];
PopoverMessageController.$inject = ['$scope', 'UIUtils', '$state', '$timeout', 'csWallet', 'esHttp', 'esMessage', 'esModals'];angular.module('cesium.es.message.controllers', ['cesium.es.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.user_message', {
url: "/user/message?type",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/message/list.html",
controller: 'ESMessageListCtrl'
}
}
})
.state('app.user_new_message', {
cache: false,
url: "/user/message/new?pubkey&uid&title&content",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/message/compose.html",
controller: 'ESMessageComposeCtrl'
}
}
})
.state('app.user_view_message', {
cache: false,
url: "/user/message/view/:type/:id",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/message/view_message.html",
controller: 'ESMessageViewCtrl'
}
}
})
;
}])
.controller('ESMessageListCtrl', ESMessageListController)
.controller('ESMessageComposeCtrl', ESMessageComposeController)
.controller('ESMessageComposeModalCtrl', ESMessageComposeModalController)
.controller('ESMessageViewCtrl', ESMessageViewController)
.controller('PopoverMessageCtrl', PopoverMessageController)
;
function ESMessageListController($scope, $state, $translate, $ionicHistory, $ionicPopover, $timeout,
esModals, UIUtils, csWallet, esMessage) {
'ngInject';
$scope.loading = true;
$scope.messages = [];
$scope.$on('$ionicView.enter', function(e, state) {
$scope.loadWallet({minData: true})
.then(function() {
if (!$scope.entered) {
$scope.entered = true;
$scope.type = state.stateParams && state.stateParams.type || 'inbox';
$scope.load();
}
$scope.showFab('fab-add-message-record');
})
.catch(function(err) {
if ('CANCELLED' === err) {
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go('app.home');
}
});
});
$scope.refresh = function(silent) {
return $scope.load(undefined, undefined, silent);
};
$scope.load = function(size, offset, silent) {
var options = {};
options.from = offset || 0;
options.size = size || 20;
options.type = $scope.type;
$scope.loading = !silent;
return esMessage.load(options)
.then(function(messages) {
$scope.messages = messages;
UIUtils.loading.hide();
$scope.loading = false;
if (messages.length > 0) {
$scope.motion.show({selector: '.view-messages .list .item'});
}
})
.catch(function(err) {
UIUtils.onError('MESSAGE.ERROR.LOAD_MESSAGES_FAILED')(err);
$scope.messages = [];
$scope.loading = false;
});
};
$scope.setType = function(type) {
$scope.type = type;
$scope.load();
};
$scope.markAllAsRead = function() {
$scope.hideActionsPopover();
if (!$scope.messages || !$scope.messages.length) return;
UIUtils.alert.confirm('MESSAGE.CONFIRM.MARK_ALL_AS_READ')
.then(function(confirm) {
if (confirm) {
esMessage.markAllAsRead()
.then(function () {
_.forEach($scope.messages, function(msg){
msg.read = true;
});
})
.catch(UIUtils.onError('MESSAGE.ERROR.MARK_ALL_AS_READ_FAILED'));
}
});
};
$scope.delete = function(index) {
var message = $scope.messages[index];
if (!message) return;
UIUtils.alert.confirm('MESSAGE.CONFIRM.REMOVE')
.then(function(confirm) {
if (confirm) {
esMessage.remove(message.id, $scope.type)
.then(function () {
$scope.messages.splice(index,1); // remove from messages array
UIUtils.toast.show('MESSAGE.INFO.MESSAGE_REMOVED');
})
.catch(UIUtils.onError('MESSAGE.ERROR.REMOVE_MESSAGE_FAILED'));
}
});
};
$scope.deleteAll = function() {
$scope.hideActionsPopover();
if (!$scope.messages || !$scope.messages.length) return;
UIUtils.alert.confirm('MESSAGE.CONFIRM.REMOVE_ALL')
.then(function(confirm) {
if (confirm) {
esMessage.removeAll($scope.type)
.then(function () {
$scope.messages.splice(0,$scope.messages.length); // reset array
UIUtils.toast.show('MESSAGE.INFO.All_MESSAGE_REMOVED');
})
.catch(UIUtils.onError('MESSAGE.ERROR.REMOVE_All_MESSAGES_FAILED'));
}
});
};
/* -- Modals -- */
$scope.showNewMessageModal = function(parameters) {
return $scope.loadWallet({minData: true})
.then(function() {
UIUtils.loading.hide();
return esModals.showMessageCompose(parameters)
.then(function(id) {
if (id) UIUtils.toast.show('MESSAGE.INFO.MESSAGE_SENT');
});
});
};
$scope.showReplyModal = function(index) {
var message = $scope.messages[index];
if (!message) return;
$translate('MESSAGE.REPLY_TITLE_PREFIX')
.then(function (prefix) {
var content = message.content ? message.content.replace(/^/g, ' > ') : null;
content = content ? content.replace(/\n/g, '\n > ') : null;
content = content ? content +'\n' : null;
return esModals.showMessageCompose({
destPub: message.issuer,
destUid: message.name,
title: prefix + message.title,
content: content,
isReply: true
});
})
.then(function(sent) {
if (sent) UIUtils.toast.show('MESSAGE.INFO.MESSAGE_SENT');
});
};
/* -- Popover -- */
$scope.showActionsPopover = function(event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('plugins/es/templates/message/lookup_popover_actions.html', {
scope: $scope
}).then(function(popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
/* -- watch events (delete, received, sent) -- */
// Message deletion
$scope.onMessageDelete = function(id) {
var index = _.findIndex($scope.messages, function(msg) {
return msg.id == id;
});
if (index) {
$scope.messages.splice(index,1); // remove from messages array
}
};
esMessage.api.data.on.delete($scope, $scope.onMessageDelete);
// Watch user sent message
$scope.onNewOutboxMessage = function(id) {
if ($scope.type != 'outbox') return;
// Add message sent to list
$scope.loading = true;
return $timeout(function() {
// Load the message sent
return esMessage.get(id, {type: $scope.type, summary: true});
}, 500 /*waiting ES propagation*/)
.then(function(msg) {
$scope.messages.splice(0,0,msg);
$scope.loading = false;
$scope.motion.show({selector: '.view-messages .list .item'});
})
.catch(function() {
$scope.loading = false;
});
};
esMessage.api.data.on.sent($scope, $scope.onNewOutboxMessage);
// Watch received message
$scope.onNewInboxMessage = function(notification) {
if ($scope.type != 'inbox' || !$scope.entered) return;
// Add message sent to list
$scope.loading = true;
// Load the the message
return esMessage.get(notification.id, {type: $scope.type, summary: true})
.then(function(msg) {
$scope.messages.splice(0,0,msg);
$scope.loading = false;
$scope.motion.show({selector: '.view-messages .list .item'});
})
.catch(function() {
$scope.loading = false;
});
};
esMessage.api.data.on.new($scope, $scope.onNewInboxMessage);
// Watch unauth
$scope.onUnauth = function() {
// Reset all data
$scope.messages = undefined;
$scope.loading = false;
$scope.entered = false;
};
csWallet.api.data.on.unauth($scope, $scope.onUnauth);
// for DEV only
/*$timeout(function() {
$scope.showNewMessageModal();
}, 900);
*/
}
function ESMessageComposeController($scope, $controller, UIUtils) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESMessageComposeModalCtrl', {$scope: $scope, parameters: {}}));
$scope.$on('$ionicView.enter', function(e, state) {
if (state.stateParams) {
if (state.stateParams.pubkey) {
$scope.formData.destPub = state.stateParams.pubkey;
if (state.stateParams.name) {
$scope.destUid = state.stateParams.name;
$scope.destPub = '';
}
else {
$scope.destUid = '';
$scope.destPub = $scope.formData.destPub;
}
}
if (state.stateParams.title) {
$scope.formData.title = state.stateParams.title;
}
if (state.stateParams.content) {
$scope.formData.content = state.stateParams.content;
}
}
$scope.loadWallet({minData: true})
.then(function() {
UIUtils.loading.hide();
})
.catch(function(err){
if (err === 'CANCELLED') {
$scope.showHome();
}
});
});
$scope.cancel = function() {
$scope.showHome();
};
$scope.setForm = function(form) {
$scope.form = form;
};
$scope.closeModal = function() {
$scope.showHome();
};
}
function ESMessageComposeModalController($scope, Modals, UIUtils, csWallet, esHttp, esMessage, parameters) {
'ngInject';
$scope.formData = {
title: parameters ? parameters.title : null,
content: parameters ? parameters.content : null,
destPub: parameters ? parameters.destPub : null
};
$scope.destUid = parameters ? parameters.destUid : null;
$scope.destPub = (parameters && !parameters.destUid) ? parameters.destPub : null;
$scope.isResponse = parameters ? parameters.isResponse : false;
$scope.doSend = function(forceNoContent) {
$scope.form.$submitted=true;
if(!$scope.form.$valid /*|| !$scope.formData.destPub*/) {
return;
}
// Ask user confirmation if no content
if (!forceNoContent && (!$scope.formData.content || !$scope.formData.content.trim().length)) {
return UIUtils.alert.confirm('MESSAGE.COMPOSE.CONTENT_CONFIRMATION')
.then(function(confirm) {
if (confirm) {
$scope.doSend(true);
}
});
}
UIUtils.loading.show();
var data = {
issuer: csWallet.data.pubkey,
recipient: $scope.formData.destPub,
title: $scope.formData.title,
content: $scope.formData.content,
time: esHttp.date.now()
};
esMessage.send(data)
.then(function(id) {
$scope.id=id;
UIUtils.loading.hide();
$scope.closeModal(id);
})
.catch(UIUtils.onError('MESSAGE.ERROR.SEND_MSG_FAILED'));
};
/* -- Modals -- */
$scope.showWotLookupModal = function() {
Modals.showWotLookup()
.then(function(result){
if (result) {
if (result.name) {
$scope.destUid = result.name;
$scope.destPub = '';
}
else {
$scope.destUid = '';
$scope.destPub = result.pubkey;
}
$scope.formData.destPub = result.pubkey;
// TODO focus on title field
//$focus('');
}
});
};
$scope.cancel = function() {
$scope.closeModal();
};
// TODO : for DEV only
/*$timeout(function() {
$scope.formData.destPub = 'G2CBgZBPLe6FSFUgpx2Jf1Aqsgta6iib3vmDRA1yLiqU';
$scope.formData.title = 'test';
$scope.formData.content = 'test';
$scope.destPub = $scope.formData.destPub;
$timeout(function() {
//$scope.doSend();
}, 800);
}, 100);
*/
}
function ESMessageViewController($scope, $state, $timeout, $translate, $ionicHistory, $ionicPopover,
UIUtils, esModals, esMessage) {
'ngInject';
$scope.formData = {};
$scope.id = null;
$scope.loading = true;
$scope.$on('$ionicView.enter', function (e, state) {
if (state.stateParams && state.stateParams.id) { // Load by id
if ($scope.loading) { // prevent reload if same id
$scope.type = state.stateParams.type || 'inbox';
$scope.load(state.stateParams.id, $scope.type)
.then(function(message) {
$scope.loading = false;
UIUtils.loading.hide();
if (!message) return;
$scope.id = message.id;
$scope.formData = message;
$scope.canDelete = true;
$scope.motion.show({selector: '.view-message .list .item'});
// Mark as read
if (!message.read) {
$timeout(function() {
// Message has NOT changed
if ($scope.id === message.id) {
esMessage.markAsRead(message, $scope.type)
.then(function() {
console.debug("[message] marked as read");
})
.catch(UIUtils.onError('MESSAGE.ERROR.MARK_AS_READ_FAILED'));
}
}, 2000); // 2s
}
});
}
$scope.showFab('fab-view-message-reply');
}
else {
$state.go('app.user_message');
}
});
$scope.load = function(id, type) {
type = type || 'inbox';
return $scope.loadWallet({minData: true})
.then(function() {
return esMessage.get(id, {type: type});
})
.catch(UIUtils.onError('MESSAGE.ERROR.LOAD_MESSAGE_FAILED'))
.then(function(message) {
if (!message.valid) {
return UIUtils.alert.error(!$scope.isUserPubkey(message.recipient) ? 'MESSAGE.ERROR.USER_NOT_RECIPIENT' : 'MESSAGE.ERROR.NOT_AUTHENTICATED_MESSAGE',
'MESSAGE.ERROR.MESSAGE_NOT_READABLE')
.then(function () {
$state.go('app.user_message', {type: type});
});
}
return message;
});
};
$scope.delete = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
UIUtils.alert.confirm('MESSAGE.CONFIRM.REMOVE')
.then(function(confirm) {
if (confirm) {
return esMessage.remove($scope.id, $scope.type)
.then(function () {
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go('app.user_message', {type: $scope.type});
UIUtils.toast.show('MESSAGE.INFO.MESSAGE_REMOVED');
})
.catch(UIUtils.onError('MESSAGE.ERROR.REMOVE_MESSAGE_FAILED'));
}
});
};
/* -- Popover -- */
$scope.showActionsPopover = function(event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('plugins/es/templates/message/view_popover_actions.html', {
scope: $scope
}).then(function(popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
/* -- Modals -- */
$scope.showReplyModal = function() {
var recipientField = ($scope.type == 'inbox') ? 'issuer' : 'recipient';
$translate('MESSAGE.REPLY_TITLE_PREFIX')
.then(function (prefix) {
var content = $scope.formData.content ? $scope.formData.content.replace(/^/g, ' > ') : null;
content = content ? content.replace(/\n/g, '\n > ') : null;
content = content ? content +'\n' : null;
return esModals.showMessageCompose({
destPub: $scope.formData[recipientField],
destUid: $scope.formData.name||$scope.formData.uid,
title: prefix + $scope.formData.title,
content: content,
isReply: true
});
})
.then(function(sent) {
if (sent) {
UIUtils.toast.show('MESSAGE.INFO.MESSAGE_SENT')
.then(function() {
$ionicHistory.goBack();
});
}
})
;
};
}
function PopoverMessageController($scope, UIUtils, $state, $timeout, csWallet, esHttp, esMessage, esModals) {
'ngInject';
var defaultSearchLimit = 40;
$scope.search = {
loading : true,
results: null,
hasMore : false,
loadingMore : false,
limit: defaultSearchLimit
};
$scope.$on('popover.shown', function() {
if ($scope.search.loading) {
$scope.load();
}
});
$scope.load = function(from, size) {
var options = {};
options.from = from || 0;
options.size = size || defaultSearchLimit;
return esMessage.notifications.load(options)
.then(function(notifications) {
if (!from) {
$scope.search.results = notifications;
}
else {
$scope.search.results = $scope.search.results.concat(notifications);
}
$scope.search.loading = false;
$scope.search.hasMore = ($scope.search.results && $scope.search.results.length >= $scope.search.limit);
$scope.updateView();
})
.catch(function(err) {
$scope.search.loading = false;
if (!from) {
$scope.search.results = [];
}
$scope.search.hasMore = false;
UIUtils.onError('MESSAGE.ERROR.LOAD_NOTIFICATIONS_FAILED')(err);
});
};
$scope.updateView = function() {
if ($scope.motion && $scope.search.results && $scope.search.results.length) {
$scope.motion.show({selector: '.popover-notification .item'});
}
};
$scope.showMore = function() {
$scope.search.limit = $scope.search.limit || defaultSearchLimit;
$scope.search.limit = $scope.search.limit * 2;
if ($scope.search.limit < defaultSearchLimit) {
$scope.search.limit = defaultSearchLimit;
}
$scope.search.loadingMore = true;
$scope.load(
$scope.search.results.length, // from
$scope.search.limit)
.then(function() {
$scope.search.loadingMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
// Listen notifications changes
$scope.onNewMessageNotification = function(notification) {
if ($scope.search.loading || $scope.search.loadingMore) return;
$scope.search.results.splice(0,0,notification);
$scope.updateView();
};
$scope.select = function(notification) {
if (!notification.read) notification.read = true;
$state.go('app.user_view_message', {id: notification.id, type: 'inbox'});
$scope.closePopover(notification);
};
$scope.resetData = function() {
if ($scope.search.loading) return;
console.debug("[ES] [messages] Resetting data (settings or account may have changed)");
$scope.search.hasMore = false;
$scope.search.results = [];
$scope.search.loading = true;
delete $scope.search.limit;
};
/* -- Modals -- */
$scope.showNewMessageModal = function(parameters) {
$scope.closePopover();
$timeout(function() {
esModals.showMessageCompose(parameters)
.then(function(id) {
if (id) UIUtils.toast.show('MESSAGE.INFO.MESSAGE_SENT');
});
}, 500); // Timeout need, to avoid freeze
};
/* -- listeners -- */
csWallet.api.data.on.logout($scope, $scope.resetData);
esHttp.api.node.on.stop($scope, $scope.resetData);
esHttp.api.node.on.start($scope, $scope.load);
esMessage.api.data.on.new($scope, $scope.onNewMessageNotification);
}
NotificationsController.$inject = ['$scope', '$rootScope', '$ionicPopover', '$state', '$timeout', 'UIUtils', 'esHttp', 'csSettings', 'csWallet', 'esNotification'];
PopoverNotificationsController.$inject = ['$scope', '$timeout', '$controller', 'UIUtils', '$state'];
angular.module('cesium.es.notification.controllers', ['cesium.es.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.view_notifications', {
url: "/notifications",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/notification/view_notifications.html",
controller: 'NotificationsCtrl'
}
}
})
;
}])
.controller('NotificationsCtrl', NotificationsController)
.controller('PopoverNotificationsCtrl', PopoverNotificationsController)
;
function NotificationsController($scope, $rootScope, $ionicPopover, $state, $timeout, UIUtils, esHttp, csSettings, csWallet, esNotification) {
'ngInject';
var defaultSearchLimit = 40;
$scope.search = {
loading : true,
results: null,
hasMore : false,
loadingMore : false,
limit: defaultSearchLimit,
options: {
codes: {
excludes: esNotification.constants.EXCLUDED_CODES
}
}
};
$scope.$on('$ionicView.enter', function() {
if ($scope.search.loading) {
$scope.load();
// Reset unread counter
$timeout(function() {
$scope.resetUnreadCount();
}, 1000);
}
});
$scope.load = function(from, size) {
var options = angular.copy($scope.search.options);
options.from = options.from || from || 0;
options.size = options.size || size || defaultSearchLimit;
$scope.search.loading = true;
return esNotification.load(csWallet.data.pubkey, options)
.then(function(res) {
if (!from) {
$scope.search.results = res || [];
}
else if (res){
$scope.search.results = $scope.search.results.concat(res);
}
$scope.search.loading = false;
$scope.search.hasMore = $scope.search.results.length >= $scope.search.limit;
$scope.updateView();
})
.catch(function(err) {
$scope.search.loading = false;
if (!from) {
$scope.search.results = [];
}
$scope.search.hasMore = false;
UIUtils.onError('COMMON.NOTIFICATIONS.LOAD_NOTIFICATIONS_FAILED')(err);
});
};
$scope.updateView = function() {
if ($scope.motion && $scope.motion.ionListClass && $scope.search.results.length) {
$scope.motion.show({selector: '.view-notification .item'});
}
};
$scope.markAllAsRead = function() {
$scope.hideActionsPopover();
if (!$scope.search.results.length) return;
UIUtils.loading.show()
.then(function() {
$rootScope.walletData.notifications.unreadCount = 0;
var lastNotification = $scope.search.results[0];
$rootScope.walletData.notifications.readTime = lastNotification ? lastNotification.time : 0;
_.forEach($scope.search.results, function (item) {
if (item.markAsRead && typeof item.markAsRead == 'function') item.markAsRead();
});
return UIUtils.loading.hide();
});
};
$scope.resetUnreadCount = function() {
if (!csWallet.data.notifications.unreadCount || !$scope.search.results || !$scope.search.results.length) return;
csWallet.data.notifications.unreadCount = 0;
var lastNotification = $scope.search.results[0];
var readTime = lastNotification.time ? lastNotification.time : 0;
if (readTime && (!csSettings.data.wallet || csSettings.data.wallet.notificationReadTime != readTime)) {
csSettings.data.wallet = csSettings.data.wallet || {};
csSettings.data.wallet.notificationReadTime = readTime;
csSettings.store();
}
};
$scope.select = function(item) {
if (item.markAsRead && typeof item.markAsRead == 'function') item.markAsRead();
if (item.state) {
$state.go(item.state, item.stateParams);
}
};
$scope.showMore = function() {
$scope.search.limit = $scope.search.limit || defaultSearchLimit;
$scope.search.limit += defaultSearchLimit;
if ($scope.search.limit < defaultSearchLimit) {
$scope.search.limit = defaultSearchLimit;
}
$scope.search.loadingMore = true;
$scope.load(
$scope.search.results.length, // from
$scope.search.limit)
.then(function() {
$scope.search.loadingMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
// Listen notifications changes
$scope.onNewNotification = function(notification) {
if ($scope.search.loading || $scope.search.loadingMore) return;
// Retrieve insertion index
var nextIndex = _.findIndex($scope.search.results, function(n) {
return notification.time > n.time;
});
if (nextIndex < 0) nextIndex = 0;
// Update the array
$scope.search.results.splice(nextIndex,0,notification);
$scope.updateView();
};
$scope.resetData = function() {
if ($scope.search.loading) return;
console.debug("[ES] [notifications] Resetting data (settings or account may have changed)");
$scope.search.hasMore = false;
$scope.search.results = [];
$scope.search.loading = true;
delete $scope.search.limit;
};
/* -- Popover -- */
$scope.showActionsPopover = function(event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('plugins/es/templates/notification/popover_actions.html', {
scope: $scope
}).then(function(popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
/* -- listeners -- */
csWallet.api.data.on.logout($scope, $scope.resetData);
esHttp.api.node.on.stop($scope, $scope.resetData);
esHttp.api.node.on.start($scope, $scope.load);
esNotification.api.data.on.new($scope, $scope.onNewNotification);
}
function PopoverNotificationsController($scope, $timeout, $controller, UIUtils, $state) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('NotificationsCtrl', {$scope: $scope}));
// Disable list motion
$scope.motion = null;
$scope.$on('popover.shown', function() {
if ($scope.search.loading) {
$scope.load();
}
});
$scope.updateView = function() {
if (!$scope.search.results.length) return;
// Set Ink
$timeout(function() {
UIUtils.ink({selector: '.popover-notification .item.ink'});
}, 100);
};
$scope.$on('popover.hidden', $scope.resetUnreadCount);
$scope.select = function(notification) {
if (!notification) return; // no selection
if (notification.markAsRead && typeof notification.markAsRead == 'function') notification.markAsRead();
if (notification.state) {
$state.go(notification.state, notification.stateParams);
}
$scope.closePopover(notification);
};
}
ViewSubscriptionsController.$inject = ['$scope', '$q', '$ionicHistory', 'csWot', 'csWallet', 'UIUtils', 'ModalUtils', 'esSubscription'];
ModalEmailSubscriptionsController.$inject = ['$scope', 'Modals', 'csSettings', 'esHttp', 'csWot', 'esModals', 'parameters'];angular.module('cesium.es.subscription.controllers', ['cesium.es.services'])
.config(['$stateProvider', function($stateProvider) {
$stateProvider
.state('app.edit_subscriptions', {
cache: false,
url: "/wallet/subscriptions",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/subscription/edit_subscriptions.html",
controller: 'ViewSubscriptionsCtrl'
}
},
data: {
auth: true,
minData: true
}
})
.state('app.edit_subscriptions_by_id', {
cache: false,
url: "/wallets/:id/subscriptions",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/subscription/edit_subscriptions.html",
controller: 'ViewSubscriptionsCtrl'
}
},
data: {
login: true,
minData: true
}
});
}])
.controller('ViewSubscriptionsCtrl', ViewSubscriptionsController)
.controller('ModalEmailSubscriptionsCtrl', ModalEmailSubscriptionsController)
;
function ViewSubscriptionsController($scope, $q, $ionicHistory, csWot, csWallet, UIUtils, ModalUtils, esSubscription) {
'ngInject';
$scope.loading = true;
$scope.popupData = {}; // need for the node popup
$scope.search = {
results: [],
loading: true
};
$scope.emailFrequencies = [
{id: "daily", label: "daily"},
{id: "weekly", label: "weekly"}
];
var wallet;
$scope.enter = function(e, state) {
// First load
if ($scope.loading) {
wallet = (state.stateParams && state.stateParams.id) ? csWallet.children.get(state.stateParams.id) : csWallet;
if (!wallet) {
UIUtils.alert.error('ERROR.UNKNOWN_WALLET_ID');
return $scope.showHome();
}
$scope.loadWallet({
wallet: wallet,
auth: true,
minData: true
})
.then(function() {
UIUtils.loading.hide();
return $scope.load();
})
.then(function() {
$scope.showFab('fab-add-subscription-record');
})
.catch(function(err){
if (err === 'CANCELLED') {
UIUtils.loading.hide(10);
$scope.loading=true; // reset for force reload next time
$ionicHistory.goBack();
return;
}
UIUtils.onError('SUBSCRIPTION.ERROR.LOAD_SUBSCRIPTIONS_FAILED')(err);
});
}
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.load = function() {
$scope.loading = true; // to avoid the call of doSave()
return esSubscription.record.load(wallet.data.pubkey, wallet.data.keypair)
.then(function(results) {
// Group by type
var groups = _.groupBy((results || []), function (record) {
return [record.type, record.recipient].join('|');
});
return _.keys(groups).reduce(function (res, key) {
var parts = key.split('|');
return res.concat({
type: parts[0],
recipient: parts[1],
items: groups[key]
});
}, []);
})
.then(function(results) {
return csWot.extendAll(results, 'recipient');
})
// Display result
.then($scope.updateView)
.catch(function(err){
UIUtils.loading.hide(10);
if (err && err.ucode == 404) {
$scope.updateView([]);
$scope.existing = false;
}
else {
UIUtils.onError('PROFILE.ERROR.LOAD_PROFILE_FAILED')(err);
}
});
};
$scope.updateView = function(results) {
if (results) {
$scope.search.results = results;
}
if ($scope.search.results && $scope.search.results.length) {
$scope.motion.show();
}
$scope.search.loading = false;
};
$scope.addSubscription = function() {
var type;
$scope.showCategoryModal()
.then(function(cat) {
if (!cat) return;
type = cat.id;
// get subscription parameters
if (type === 'email') {
return $scope.showEmailModal();
}
else {
UIUtils.alert.notImplemented();
}
})
.then(function(record) {
if (!record) return;
UIUtils.loading.show();
esSubscription.record.add(record, wallet)
.then($scope.addToUI)
.then(function() {
wallet.data.subscriptions = wallet.data.subscriptions || {count: 0};
wallet.data.subscriptions.count++;
UIUtils.loading.hide();
$scope.updateView();
})
.catch(UIUtils.onError('SUBSCRIPTION.ERROR.ADD_SUBSCRIPTION_FAILED'));
});
};
$scope.editSubscription = function(record) {
// get subscription parameters
var promise;
var oldRecord = angular.copy(record);
if (record.type === 'email') {
promise = $scope.showEmailModal(record);
}
if (!promise) return;
return promise
.then(function(res) {
if (!res) return;
UIUtils.loading.show();
record.id = oldRecord.id;
return esSubscription.record.update(record, wallet)
.then(function() {
// If recipient change, update in results
if (oldRecord.type !== record.type ||
oldRecord.recipient !== record.recipient) {
$scope.removeFromUI(oldRecord);
return $scope.addToUI(record);
}
})
.then(function() {
UIUtils.loading.hide();
$scope.updateView();
})
.catch(UIUtils.onError('SUBSCRIPTION.ERROR.UPDATE_SUBSCRIPTION_FAILED'));
});
};
$scope.deleteSubscription = function(record, confirm) {
if (!record || !record.id) return;
if (!confirm) {
return UIUtils.alert.confirm('SUBSCRIPTION.CONFIRM.DELETE_SUBSCRIPTION')
.then(function(confirm) {
if (confirm) return $scope.deleteSubscription(record, confirm);
});
}
UIUtils.loading.show();
esSubscription.record.remove(record.id, {wallet: wallet})
.then(function() {
wallet.data.subscriptions = wallet.data.subscriptions || {count: 1};
wallet.data.subscriptions.count--;
$scope.removeFromUI(record);
UIUtils.loading.hide();
})
.catch(UIUtils.onError('SUBSCRIPTION.ERROR.DELETE_SUBSCRIPTION_FAILED'));
};
$scope.removeFromUI = function(record) {
var subscriptions = _.findWhere($scope.search.results, {type: record.type, recipient: record.recipient});
var index = _.findIndex(subscriptions.items, record);
if (index >= 0) {
subscriptions.items.splice(index, 1);
}
if (!subscriptions.items.length) {
index = _.findIndex($scope.search.results, subscriptions);
$scope.search.results.splice(index, 1);
}
};
$scope.addToUI = function(record) {
$scope.search.results = $scope.search.results || [];
var subscriptions = _.findWhere($scope.search.results,
{type: record.type, recipient: record.recipient});
if (!subscriptions) {
subscriptions = {type: record.type, recipient: record.recipient, items: []};
return csWot.extendAll([subscriptions], 'recipient')
.then(function(){
subscriptions.items.push(record);
$scope.search.results.push(subscriptions);
return record;
});
}
subscriptions.items.push(record);
return $q.when(record);
};
/* -- modals -- */
$scope.showCategoryModal = function() {
// load categories
return esSubscription.category.all()
.then(function(categories){
return ModalUtils.show('plugins/es/templates/common/modal_category.html', 'ESCategoryModalCtrl as ctrl',
{categories : categories},
{focusFirstInput: true}
);
})
.then(function(cat){
if (cat && cat.parent) {
return cat;
}
});
};
$scope.showEmailModal = function(parameters) {
return ModalUtils.show('plugins/es/templates/subscription/modal_email.html','ModalEmailSubscriptionsCtrl',
parameters, {focusFirstInput: true});
};
}
function ModalEmailSubscriptionsController($scope, Modals, csSettings, esHttp, csWot, esModals, parameters) {
'ngInject';
$scope.frequencies = [
{id: "daily", label: "daily"},
{id: "weekly", label: "weekly"}
];
$scope.formData = parameters || {};
$scope.formData.content = $scope.formData.content || {};
$scope.formData.content.frequency = $scope.formData.content.frequency || $scope.frequencies[0].id; // set to first value
$scope.recipient = {};
$scope.$on('modal.shown', function() {
// Load recipient (uid, name, avatar...)
if ($scope.formData.recipient) {
$scope.recipient = {pubkey: $scope.formData.recipient};
return csWot.extendAll([$scope.recipient]);
}
else {
return esHttp.network.peering.self()
.then(function(res){
if (!res) return;
$scope.formData.recipient = res.pubkey;
$scope.recipient = {pubkey: $scope.formData.recipient};
return csWot.extendAll([$scope.recipient]);
});
}
});
// Submit
$scope.doSubmit = function() {
$scope.form.$submitted = true;
if (!$scope.form.$valid || !$scope.formData.content.email || !$scope.formData.content.frequency) return;
var record = {
type: 'email',
recipient: $scope.formData.recipient,
content: {
email: $scope.formData.content.email,
locale: csSettings.data.locale.id,
frequency: $scope.formData.content.frequency
}
};
$scope.closeModal(record);
};
$scope.cancel = function() {
$scope.closeModal();
};
if (!!$scope.subscriptionForm) {
$scope.subscriptionForm.$setPristine();
}
$scope.showNetworkLookup = function() {
return esModals.showNetworkLookup({
enableFilter: true,
endpointFilter: esHttp.constants.ES_SUBSCRIPTION_API
})
.then(function (peer) {
if (peer) {
$scope.recipient = peer;
$scope.formData.recipient = peer.pubkey;
}
});
};
}
ESDocumentLookupController.$inject = ['$scope', '$ionicPopover', '$location', '$timeout', 'csSettings', 'csWallet', 'UIUtils', 'esHttp', 'esDocument'];
ESLastDocumentsController.$inject = ['$scope', '$controller', '$timeout', '$state'];angular.module('cesium.es.document.controllers', ['cesium.es.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.document_search', {
url: "/data/search/:index/:type?q",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/document/lookup.html",
controller: 'ESDocumentLookupCtrl'
}
},
data: {
silentLocationChange: true
}
})
;
}])
.controller('ESDocumentLookupCtrl', ESDocumentLookupController)
.controller('ESLastDocumentsCtrl', ESLastDocumentsController)
;
function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout,
csSettings, csWallet, UIUtils, esHttp, esDocument) {
'ngInject';
$scope.search = $scope.search || {
loading: true,
hasMore: false,
text: undefined,
index: 'invitation',
type: 'certification',
results: [],
sort: 'time',
asc: false,
loadingMore: false
};
$scope.entered = false;
$scope.searchTextId = 'documentSearchText';
$scope.ionItemClass = 'item-border-large';
$scope.defaultSizeLimit = $scope.defaultSizeLimit || (UIUtils.screen.isSmall() ? 50 : 100);
$scope.helptipPrefix = 'helptip-document';
$scope.compactMode = angular.isDefined($scope.compactMode) ? $scope.compactMode : true;
$scope._source = $scope._source || ["issuer", "hash", "time", "creationTime", "title", "message"];
/**
* Enter into the view
* @param e
* @param state
*/
$scope.enter = function(e, state) {
if (!$scope.entered) {
$scope.entered = true;
$scope.search.index = state.stateParams && state.stateParams.index || $scope.search.index;
$scope.search.type = state.stateParams && state.stateParams.type || $scope.search.type;
$scope.search.text = state.stateParams && state.stateParams.q || $scope.search.text;
$scope.search.last = !$scope.search.text;
$scope.load();
}
$scope.expertMode = angular.isDefined($scope.expertMode) ? $scope.expertMode : !UIUtils.screen.isSmall() && csSettings.data.expertMode;
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.computeOptions = function(offset, size) {
var options = {
index: $scope.search.index,
type: $scope.search.type,
from: offset || 0,
size: size || $scope.defaultSizeLimit
};
// add sort
if ($scope.search.sort) {
options.sort = {};
options.sort[$scope.search.sort] = (!$scope.search.asc ? "desc" : "asc");
}
else { // default sort
options.sort = {time:'desc'};
}
// Included fields
options._source = options._source || $scope._source;
return options;
};
$scope.load = function(offset, size, silent) {
if ($scope.search.error) return;
var options = $scope.computeOptions(offset, size);
$scope.search.loading = !silent;
var searchFn = $scope.search.last ?
esDocument.search(options) :
esDocument.searchText($scope.search.text||'', options);
return searchFn
.then(function(res) {
if (!offset) {
$scope.search.results = res.hits;
$scope.search.took = res.took;
}
else {
$scope.search.results = $scope.search.results.concat(res.hits);
}
$scope.search.total = res.total;
UIUtils.loading.hide();
$scope.search.loading = false;
$scope.search.hasMore = res.hits && res.hits.length > 0 && res.total > $scope.search.results.length;
$scope.updateView();
})
.catch(function(err) {
$scope.search.results = [];
$scope.search.loading = false;
$scope.search.error = true;
$scope.search.hasMore = false;
UIUtils.onError('DOCUMENT.ERROR.LOAD_DOCUMENTS_FAILED')(err)
.then(function() {
$scope.search.error = false;
});
});
};
$scope.updateView = function() {
if ($scope.motion && $scope.search.results && $scope.search.results.length) {
$scope.motion.show({selector: '.list .item.item-document'});
}
$scope.$broadcast('$$rebind::rebind'); // notify binder
};
$scope.doSearchText = function() {
$scope.search.last = $scope.search.text ? false : true;
return $scope.load()
.then(function() {
// Update location href
$location.search({q: $scope.search.text}).replace();
});
};
$scope.doSearchLast = function() {
$scope.search.last = true;
$scope.search.text = undefined;
return $scope.load();
};
$scope.removeAll = function() {
$scope.hideActionsPopover();
if (!$scope.search.results || !$scope.search.results.length) return;
return UIUtils.alert.confirm('DOCUMENT.CONFIRM.REMOVE_ALL')
.then(function(confirm) {
if (!confirm) return;
UIUtils.loading.show();
return esDocument.removeAll($scope.search.results)
.then(function() {
$scope.search.loading = false;
return $timeout(function() {
UIUtils.toast.show('DOCUMENT.INFO.REMOVED'); // toast
return $scope.load();
}, 1000 /*waiting propagation*/);
})
.catch(UIUtils.onError('DOCUMENT.ERROR.REMOVE_ALL_FAILED'));
});
};
$scope.remove = function($event, index) {
var doc = $scope.search.results[index];
if (!doc || $event.defaultPrevented) return;
$event.stopPropagation();
UIUtils.alert.confirm('DOCUMENT.CONFIRM.REMOVE')
.then(function(confirm) {
if (!confirm) return;
return esDocument.remove(doc)
.then(function () {
$scope.search.results.splice(index,1); // remove from messages array
$scope.$broadcast('$$rebind::rebind'); // notify binder
UIUtils.toast.show('DOCUMENT.INFO.REMOVED'); // toast
})
.catch(UIUtils.onError('MESSAGE.ERROR.REMOVE_FAILED'));
});
};
$scope.selectDocument = function(event, doc) {
console.debug("Selected document: ", doc, esHttp);
var url = esHttp.getUrl('/{0}/{1}/_search?pretty&q=_id:{2}'.format(doc.index, doc.type, doc.id));
return $scope.openLink(event, url);
};
$scope.toggleCompactMode = function() {
$scope.compactMode = !$scope.compactMode;
$scope.updateView();
// Workaround to re-initialized the <ion-infinite-loop>
if (!$scope.search.hasMore && $scope.search.results.length && $scope.search.type == 'last') {
$timeout(function() {
$scope.search.hasMore = true;
}, 500);
}
};
$scope.toggleSort = function(sort){
if ($scope.search.sort === sort && !$scope.search.asc) {
$scope.search.asc = undefined;
$scope.search.sort = undefined;
}
else {
$scope.search.asc = ($scope.search.sort === sort) ? !$scope.search.asc : true;
$scope.search.sort = sort;
}
$scope.load();
};
$scope.showMore = function() {
if ($scope.search.loading) return;
$scope.search.loadingMore = true;
$scope.load(
$scope.search.results.length, // from
$scope.defaultSizeLimit,
true/*silent*/)
.then(function() {
$scope.search.loadingMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
$scope.startListenChanges = function() {
var now = Date.now();
var source = $scope.search.index + '/' + $scope.search.type;
var wsChanges = esHttp.websocket.changes(source);
return wsChanges.open()
.then(function(){
console.debug("[ES] [document] Websocket opened in {0} ms".format(Date.now()- now));
wsChanges.on(function(change) {
if (!$scope.search.last || !change) return; // ignore
esDocument.fromHit(change)
.then(function(doc) {
if (change._operation === 'DELETE') {
$scope.onDeleteDocument(doc);
}
else {
$scope.onNewDocument(doc);
}
});
});
});
};
$scope.onNewDocument = function(document) {
if (!$scope.search.last || $scope.search.loading) return; // skip
console.debug("[ES] [document] Detected new document: ", document);
var index = _.findIndex($scope.search.results, {id: document.id, index: document.index, type: document.type});
if (index < 0) {
$scope.search.total++;
$scope.search.results.splice(0, 0, document);
}
else {
document.updated = true;
$timeout(function() {
document.updated = false;
}, 2000);
$scope.search.results.splice(index, 1, document);
}
$scope.updateView();
};
$scope.onDeleteDocument = function(document) {
if (!$scope.search.last || $scope.search.loading) return; // skip
$timeout(function() {
var index = _.findIndex($scope.search.results, {id: document.id, index: document.index, type: document.type});
if (index < 0) return; // skip if not found
console.debug("[ES] [document] Detected document deletion: ", document);
$scope.search.results.splice(index, 1);
$scope.search.total--;
$scope.updateView();
}, 750);
};
/* -- Modals -- */
/* -- Popover -- */
$scope.showActionsPopover = function(event) {
UIUtils.popover.show(event, {
templateUrl: 'plugins/es/templates/document/lookup_popover_actions.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.actionsPopover = popover;
}
});
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
$scope.actionsPopover = null;
}
};
/* -- watch events -- */
$scope.resetData = function() {
if ($scope.search.loading) return;
console.debug("[ES] [document] Resetting data (settings or account may have changed)");
// Reset all data
$scope.search.results = [];
$scope.search.loading = false;
$scope.search.total = undefined;
$scope.search.loadingMore = false;
$scope.entered = false;
delete $scope.search.limit;
};
csWallet.api.data.on.logout($scope, $scope.resetData);
// for DEV only
/*$timeout(function() {
// launch default action fo DEV
}, 900);
*/
}
function ESLastDocumentsController($scope, $controller, $timeout, $state) {
'ngInject';
$scope.search = {
loading: true,
hasMore: true,
text: undefined,
index: 'user,page,group', type: 'profile,record,comment',
results: undefined,
sort: 'time',
asc: false
};
$scope.expertMode = false;
$scope.defaultSizeLimit = 20;
$scope._source = ["issuer", "hash", "time", "creationTime", "title", "avatar._content_type", "city", "message", "record"];
// Initialize the super class and extend it.
angular.extend(this, $controller('ESDocumentLookupCtrl', {$scope: $scope}));
$scope.$on('$ionicParentView.enter', $scope.enter);
$scope.selectDocument = function(event, doc) {
if (!doc || !event || event.defaultPrevented) return;
event.stopPropagation();
if (doc.index === "user" && doc.type === "profile") {
$state.go('app.wot_identity', {pubkey: doc.pubkey, uid: doc.name});
}
else if (doc.index === "page" && doc.type === "record") {
$state.go('app.view_page', {title: doc.title, id: doc.id});
}
else if (doc.index === "page" && doc.type === "comment") {
var anchor = $filter('formatHash')(doc.id);
$state.go('app.view_page_anchor', {title: doc.title, id: doc.record, anchor: anchor});
}
else if (doc.index === "group" && doc.type === "record") {
$state.go('app.view_group', {title: doc.title, id: doc.id});
}
else if (doc.index === "group" && doc.type === "comment") {
var anchor = $filter('formatHash')(doc.id);
$state.go('app.view_group_anchor', {title: doc.title, id: doc.record, anchor: anchor});
}
else {
console.warn("Click on this kind of document not implement yet!", doc)
}
};
// Override parent function computeOptions
var inheritedComputeOptions = $scope.computeOptions;
$scope.computeOptions = function(offset, size){
// Cal inherited function
var options = inheritedComputeOptions(offset, size);
if (!options.sort || options.sort.time) {
var side = options.sort && options.sort.time || side;
options.sort = [
//{'creationTime': side},
{'time': side}
];
}
options._source = options._source || $scope._source;
options.getTimeFunction = function(doc) {
doc.time = doc.creationTime || doc.time;
return doc.time;
};
return options;
};
// Listen for changes
$timeout(function() {
$scope.startListenChanges();
}, 1000);
}
ESNetworkLookupController.$inject = ['$scope', '$state', '$location', '$ionicPopover', '$window', '$translate', 'esHttp', 'UIUtils', 'csConfig', 'csSettings', 'csCurrency', 'esNetwork', 'csWot'];
ESNetworkLookupModalController.$inject = ['$scope', '$controller', 'parameters'];
ESPeerViewController.$inject = ['$scope', '$q', '$window', '$state', 'UIUtils', 'csWot', 'esHttp', 'csHttp', 'csSettings'];
ESNetworkLookupPopoverController.$inject = ['$scope', '$controller'];
ESPeerInfoPopoverController.$inject = ['$scope', '$q', 'csSettings', 'csCurrency', 'csHttp', 'esHttp'];
angular.module('cesium.es.network.controllers', ['cesium.es.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.es_network', {
url: "/network/data?online&expert",
cache: false, // fix #766
views: {
'menuContent': {
templateUrl: "plugins/es/templates/network/view_es_network.html",
controller: 'ESNetworkLookupCtrl'
}
},
data: {
silentLocationChange: true
}
})
.state('app.view_es_peer', {
url: "/network/data/peer/:server?ssl&tor",
cache: false,
views: {
'menuContent': {
templateUrl: "plugins/es/templates/network/view_es_peer.html",
controller: 'ESPeerViewCtrl'
}
},
data: {
preferHttp: true // avoid HTTPS if config has httpsMode=clever
}
});
}])
.controller('ESNetworkLookupCtrl', ESNetworkLookupController)
.controller('ESNetworkLookupModalCtrl', ESNetworkLookupModalController)
.controller('ESPeerViewCtrl', ESPeerViewController)
.controller('ESNetworkLookupPopoverCtrl', ESNetworkLookupPopoverController)
.controller('ESPeerInfoPopoverCtrl', ESPeerInfoPopoverController)
;
function ESNetworkLookupController($scope, $state, $location, $ionicPopover, $window, $translate,
esHttp, UIUtils, csConfig, csSettings, csCurrency, esNetwork, csWot) {
'ngInject';
$scope.networkStarted = false;
$scope.ionItemClass = '';
$scope.expertMode = csSettings.data.expertMode && !UIUtils.screen.isSmall();
$scope.isHttps = ($window.location.protocol === 'https:');
$scope.search = {
text: '',
loading: true,
online: true,
results: [],
endpointFilter: esHttp.constants.GCHANGE_API,
sort : undefined,
asc: true
};
$scope.listeners = [];
$scope.helptipPrefix = 'helptip-network';
$scope.enableLocationHref = true; // can be overrided by sub-controller (e.g. popup)
$scope.removeListeners = function() {
if ($scope.listeners.length) {
console.debug("[ES] [network] Closing listeners");
_.forEach($scope.listeners, function(remove){
remove();
});
$scope.listeners = [];
}
};
/**
* Enter in view
*/
$scope.enter = function(e, state) {
if ($scope.networkStarted) return;
$scope.networkStarted = true;
$scope.search.loading = true;
csCurrency.get()
.then(function (currency) {
if (currency) {
$scope.node = !esHttp.node.same(currency.node.host, currency.node.port) ?
esHttp.instance(currency.node.host, currency.node.port) : esHttp;
if (state && state.stateParams) {
if (state.stateParams.online == 'true') {
$scope.search.online = true;
}
if (state.stateParams.expert) {
$scope.expertMode = (state.stateParams.expert == 'true');
}
}
$scope.load();
}
})
.catch(function(err) {
UIUtils.onError('ERROR.GET_CURRENCY_FAILED')(err);
$scope.networkStarted = false;
});
};
$scope.$on('$ionicParentView.enter', $scope.enter);
/**
* Leave the view
*/
$scope.leave = function() {
if (!$scope.networkStarted) return;
$scope.removeListeners();
esNetwork.close();
$scope.networkStarted = false;
$scope.search.loading = true;
};
$scope.$on('$ionicView.beforeLeave', $scope.leave);
$scope.$on('$ionicParentView.beforeLeave', $scope.leave);
$scope.$on('$destroy', $scope.leave);
$scope.computeOptions = function() {
var options = {
filter: {
member: (!$scope.search.type || $scope.search.type === 'member'),
mirror: (!$scope.search.type || $scope.search.type === 'mirror'),
endpointFilter : (angular.isDefined($scope.search.endpointFilter) ? $scope.search.endpointFilter : null),
online: $scope.search.online && true
},
sort: {
type : $scope.search.sort,
asc : $scope.search.asc
},
expertMode: $scope.expertMode,
// larger timeout when on expert mode
timeout: csConfig.timeout && ($scope.expertMode ? (csConfig.timeout / 10) : (csConfig.timeout / 100))
};
return options;
};
$scope.load = function() {
if ($scope.search.loading){
esNetwork.start($scope.node, $scope.computeOptions());
// Catch event on new peers
$scope.refreshing = false;
$scope.listeners.push(
esNetwork.api.data.on.changed($scope, function(data){
if (!$scope.refreshing) {
$scope.refreshing = true;
csWot.extendAll(data.peers)
.then(function() {
// Avoid to refresh if view has been leaving
if ($scope.networkStarted) {
$scope.updateView(data);
}
$scope.refreshing = false;
});
}
}));
}
};
$scope.updateView = function(data) {
console.debug("[peers] Updating UI");
$scope.search.results = data.peers;
$scope.search.memberPeersCount = data.memberPeersCount;
// Always tru if network not started (e.g. after leave+renter the view)
$scope.search.loading = !$scope.networkStarted || esNetwork.isBusy();
if ($scope.motion && $scope.search.results && $scope.search.results.length > 0) {
$scope.motion.show({selector: '.item-peer'});
}
if (!$scope.loading) {
$scope.$broadcast('$$rebind::' + 'rebind'); // force data binding
}
};
$scope.refresh = function() {
// Network
$scope.search.loading = true;
esNetwork.loadPeers();
};
$scope.sort = function() {
$scope.search.loading = true;
$scope.refreshing = true;
esNetwork.sort($scope.computeOptions());
$scope.updateView(esNetwork.data);
};
$scope.toggleOnline = function(online){
$scope.hideActionsPopover();
$scope.search.online = (online !== false);
esNetwork.close();
$scope.search.loading = true;
$scope.load();
// Update location href
if ($scope.enableLocationHref) {
$location.search({online: $scope.search.online}).replace();
}
};
$scope.toggleSearchEndpoint = function(endpoint){
$scope.hideActionsPopover();
if ($scope.search.endpointFilter === endpoint || endpoint === null) {
$scope.search.endpointFilter = null;
}
else {
$scope.search.endpointFilter = endpoint;
}
$scope.sort();
};
$scope.toggleSort = function(sort){
if ($scope.search.sort === sort && !$scope.search.asc) {
$scope.search.asc = undefined;
$scope.search.sort = undefined;
}
else {
$scope.search.asc = ($scope.search.sort === sort) ? !$scope.search.asc : true;
$scope.search.sort = sort;
}
$scope.sort();
};
$scope.selectPeer = function(peer) {
// Skip offline
if (!peer.online ) return;
var stateParams = {server: peer.getServer()};
if (peer.isSsl()) {
stateParams.ssl = true;
}
if (peer.isTor()) {
stateParams.tor = true;
}
$state.go('app.view_es_peer', stateParams);
};
$scope.$on('csView.action.refresh', function(event, context) {
if (context === 'peers') {
$scope.refresh();
}
});
$scope.$on('csView.action.showActionsPopover', function(event, clickEvent) {
$scope.showActionsPopover(clickEvent);
});
/* -- popover -- */
$scope.showActionsPopover = function(event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('templates/network/lookup_popover_actions.html', {
scope: $scope
}).then(function(popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
$scope.showEndpointsPopover = function($event, peer, endpointFilter) {
var endpoints = peer.getEndpoints(endpointFilter);
endpoints = (endpoints||[]).reduce(function(res, ep) {
var bma = esHttp.node.parseEndPoint(ep);
return res.concat({
label: 'NETWORK.VIEW.NODE_ADDRESS',
value: peer.getServer() + (bma.path||'')
});
}, []);
if (!endpoints.length) return;
UIUtils.popover.show($event, {
templateUrl: 'templates/network/popover_endpoints.html',
bindings: {
titleKey: 'NETWORK.VIEW.ENDPOINTS.' + endpointFilter,
items: endpoints
}
});
$event.stopPropagation();
};
$scope.showWs2pPopover = function($event, peer) {
$event.stopPropagation();
return $translate('NETWORK.VIEW.PRIVATE_ACCESS')
.then(function(privateAccessMessage) {
UIUtils.popover.show($event, {
templateUrl: 'templates/network/popover_endpoints.html',
bindings: {
titleKey: 'NETWORK.VIEW.ENDPOINTS.WS2P',
valueKey: 'NETWORK.VIEW.NODE_ADDRESS',
items: [
{
label: 'NETWORK.VIEW.NODE_ADDRESS',
value: !peer.bma.private ? (peer.getServer() + (peer.bma.path||'')) : privateAccessMessage
},
{
label: 'NETWORK.VIEW.WS2PID',
value: peer.bma.ws2pid
},
{
label: 'NETWORK.VIEW.POW_PREFIX',
value: peer.powPrefix
}]
}
});
});
};
}
function ESNetworkLookupModalController($scope, $controller, parameters) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESNetworkLookupCtrl', {$scope: $scope}));
// Read parameters
parameters = parameters || {};
$scope.enableFilter = angular.isDefined(parameters.enableFilter) ? parameters.enableFilter : true;
$scope.search.type = angular.isDefined(parameters.type) ? parameters.type : $scope.search.type;
$scope.search.endpointFilter = angular.isDefined(parameters.endpointFilter) ? parameters.endpointFilter : $scope.search.endpointFilter;
$scope.expertMode = angular.isDefined(parameters.expertMode) ? parameters.expertMode : $scope.expertMode;
$scope.ionItemClass = parameters.ionItemClass || 'item-border-large';
$scope.enableLocationHref = false;
$scope.helptipPrefix = '';
$scope.selectPeer = function(peer) {
$scope.closeModal(peer);
};
$scope.$on('modal.hidden', function(){
$scope.leave();
});
// Disable this unsed method - called by load()
$scope.showHelpTip = function() {};
// Enter the modal
$scope.enter();
}
function ESNetworkLookupPopoverController($scope, $controller) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('NetworkLookupCtrl', {$scope: $scope}));
// Read parameters
var parameters = parameters || {};
$scope.enableFilter = angular.isDefined(parameters.enableFilter) ? parameters.enableFilter : true;
$scope.search.type = angular.isDefined(parameters.type) ? parameters.type : $scope.search.type;
$scope.search.endpointFilter = angular.isDefined(parameters.endpointFilter) ? parameters.endpointFilter : $scope.search.endpointFilter;
$scope.expertMode = angular.isDefined(parameters.expertMode) ? parameters.expertMode : $scope.expertMode;
$scope.ionItemClass = parameters.ionItemClass || 'item-border-large';
$scope.helptipPrefix = '';
$scope.selectPeer = function(peer) {
$scope.closePopover(peer);
};
$scope.$on('popover.hidden', function(){
$scope.leave();
});
// Disable this unsed method - called by load()
$scope.showHelpTip = function() {};
// Enter the popover
$scope.enter();
}
function ESPeerInfoPopoverController($scope, $q, csSettings, csCurrency, csHttp, esHttp) {
'ngInject';
$scope.loading = true;
$scope.formData = {};
$scope.load = function() {
$scope.loading = true;
$scope.formData = {};
return $q.all([
// get current block
csCurrency.blockchain.current()
.then(function(block) {
$scope.formData.number = block.number;
$scope.formData.medianTime = block.medianTime;
$scope.formData.powMin = block.powMin;
$scope.formData.useSsl = esHttp.useSsl;
})
.catch(function() {
delete $scope.formData.number;
delete $scope.formData.medianTime;
delete $scope.formData.powMin;
delete $scope.formData.useSsl;
// continue
}),
// Get node current version
esHttp.node.summary()
.then(function(res){
$scope.formData.version = res && res.duniter && res.duniter.version;
$scope.formData.software = res && res.duniter && res.duniter.software;
})
.catch(function() {
delete $scope.formData.version;
delete $scope.formData.software;
// continue
}),
// Get duniter latest version
esHttp.version.latest()
.then(function(latestRelease){
$scope.formData.latestRelease = latestRelease;
})
.catch(function() {
delete $scope.formData.latestRelease;
// continue
})
])
.then(function() {
// Compare, to check if newer
if ($scope.formData.latestRelease && $scope.formData.software == 'duniter') {
var compare = csHttp.version.compare($scope.formData.version, $scope.formData.latestRelease.version);
$scope.formData.isPreRelease = compare > 0;
$scope.formData.hasNewRelease = compare < 0;
}
else {
$scope.formData.isPreRelease = false;
$scope.formData.hasNewRelease = false;
}
$scope.loading = false;
$scope.$broadcast('$$rebind::' + 'rebind'); // force data binding
});
};
// Update UI on new block
csCurrency.api.data.on.newBlock($scope, function(block) {
if ($scope.loading) return;
console.debug("[peer info] Received new block. Reload content");
$scope.load();
});
// Update UI on settings changed
csSettings.api.data.on.changed($scope, function(data) {
if ($scope.loading) return;
console.debug("[peer info] Peer settings changed. Reload content");
$scope.load();
});
// Load data when enter
$scope.load();
}
function ESPeerViewController($scope, $q, $window, $state, UIUtils, csWot, esHttp, csHttp, csSettings) {
'ngInject';
$scope.node = {};
$scope.loading = true;
$scope.isHttps = ($window.location.protocol === 'https:');
$scope.isReachable = true;
$scope.options = {
document: {
index : csSettings.data.plugins.es && csSettings.data.plugins.es.document && csSettings.data.plugins.es.document.index || 'user',
type: csSettings.data.plugins.es && csSettings.data.plugins.es.document && csSettings.data.plugins.es.document.type || 'profile'
}
};
$scope.$on('$ionicView.beforeEnter', function (event, viewData) {
// Enable back button (workaround need for navigation outside tabs - https://stackoverflow.com/a/35064602)
viewData.enableBack = UIUtils.screen.isSmall() ? true : viewData.enableBack;
});
$scope.$on('$ionicView.enter', function(e, state) {
var isDefaultNode = !state.stateParams || !state.stateParams.server;
var server = state.stateParams && state.stateParams.server || esHttp.server;
var useSsl = state.stateParams && state.stateParams.ssl == "true" || (isDefaultNode ? esHttp.useSsl : false);
var useTor = state.stateParams.tor == "true" || (isDefaultNode ? esHttp.useTor : false);
return $scope.load(server, useSsl, useTor)
.then(function() {
return $scope.$broadcast('$csExtension.enter', e, state);
})
.then(function(){
$scope.loading = false;
});
});
$scope.load = function(server, useSsl, useTor) {
var node = {
server: server,
host: server,
useSsl: useSsl,
useTor: useTor
};
var serverParts = server.split(':');
if (serverParts.length == 2) {
node.host = serverParts[0];
node.port = serverParts[1];
}
node.url = csHttp.getUrl(node.host, node.port, undefined/*path*/, node.useSsl);
angular.merge($scope.node,
useTor ?
// For TOR, use a web2tor to access the endpoint
esHttp.lightInstance(node.host + ".to", 443, 443, true/*ssl*/, 60000 /*long timeout*/) :
esHttp.lightInstance(node.host, node.port, node.useSsl),
node);
$scope.isReachable = !$scope.isHttps || useSsl;
if (!$scope.isReachable) {
// Get node from the default node
return esHttp.network.peers()
.then(function(res) {
// find the current peer
var peers = (res && res.peers || []).reduce(function(res, json) {
var peer = new EsPeer(json);
if (!peer.hasEndpoint('GCHANGE_API')) return res;
var ep = esHttp.node.parseEndPoint(peer.getEndpoints('GCHANGE_API')[0]);
if((ep.dns == node.host || ep.ipv4 == node.host || ep.ipv6 == node.host) && (
ep.port == node.port)) {
peer.ep = ep;
return res.concat(peer);
}
return res;
}, []);
var peer = peers.length && peers[0];
// Current node found
if (peer) {
$scope.node.pubkey = peer.pubkey;
$scope.node.currency = peer.currency;
return csWot.extend($scope.node);
}
else {
console.warn('Could not get peer from /network/peers');
}
});
}
return $q.all([
// Get node peer info
$scope.node.network.peering.self()
.then(function(json) {
$scope.node.pubkey = json.pubkey;
$scope.node.currency = json.currency;
}),
// Get node doc count
$scope.node.record.count($scope.options.document.index, $scope.options.document.type)
.then(function(count) {
$scope.node.docCount = count;
}),
// Get known peers
$scope.node.network.peers()
.then(function(json) {
var peers = json.peers.reduce(function (res, p) {
var peer = new EsPeer(p);
if (!peer.hasEndpoint('GCHANGE_API')) return res;
peer.online = p.status === 'UP';
peer.blockNumber = peer.block.replace(/-.+$/, '');
peer.ep = esHttp.node.parseEndPoint(peer.getEndpoints('GCHANGE_API')[0]);
peer.dns = peer.getDns();
peer.id = peer.keyID();
peer.server = peer.getServer();
return res.concat(peer);
}, []);
// Extend (add uid+name+avatar)
return csWot.extendAll([$scope.node].concat(peers))
.then(function() {
// Final sort
$scope.peers = _.sortBy(peers, function(p) {
var score = 1;
score += 10000 * (p.online ? 1 : 0);
score += 1000 * (p.hasMainConsensusBlock ? 1 : 0);
score += 100 * (p.name ? 1 : 0);
return -score;
});
$scope.motion.show({selector: '.item-peer'});
});
}),
// Get current block
$scope.node.blockchain.current()
.then(function(json) {
$scope.current = json;
})
])
.catch(UIUtils.onError(useTor ? "PEER.VIEW.ERROR.LOADING_TOR_NODE_ERROR" : "PEER.VIEW.ERROR.LOADING_NODE_ERROR"));
};
$scope.selectPeer = function(peer) {
// Skip offline
if (!peer.online ) return;
var stateParams = {server: peer.getServer()};
if (peer.isSsl()) {
stateParams.ssl = true;
}
if (peer.isTor()) {
stateParams.tor = true;
}
$state.go('app.view_es_peer', stateParams);
};
/* -- manage link to raw document -- */
$scope.openRawPeering = function(event) {
return $scope.openLink(event, $scope.node.url + '/network/peering?pretty');
};
$scope.openRawCurrentBlock = function(event) {
return $scope.openLink(event, $scope.node.url + '/network/peering?pretty');
};
}
angular.module('cesium.es.registry.services', ['ngResource', 'cesium.services', 'cesium.es.http.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('esRegistry');
}
}])
.factory('esRegistry', ['$rootScope', '$q', 'csPlatform', 'csSettings', 'csWallet', 'csWot', 'esHttp', 'esComment', 'esGeo', function($rootScope, $q, csPlatform, csSettings, csWallet, csWot, esHttp, esComment, esGeo) {
'ngInject';
var
fields = {
commons: ["title", "description", "issuer", "time", "address", "city", "creationTime", "avatar._content_type",
"picturesCount", "type", "category", "socials", "pubkey",
"geoPoint"
]
},
that = this,
listeners;
that.raw = {
count: esHttp.get('/page/record/_search?size=0&q=issuer::pubkey'),
searchText: esHttp.get('/page/record/_search?q=:search'),
search: esHttp.post('/page/record/_search'),
get: esHttp.get('/page/record/:id'),
getCommons: esHttp.get('/page/record/:id?_source=' + fields.commons.join(',')),
category: {
get: esHttp.get('/page/category/:id'),
all: esHttp.get('/page/category/_search?sort=order&from=0&size=1000&_source=name,parent')
}
};
function onWalletReset(data) {
data.pages = null;
}
function onWalletLogin(data, deferred) {
deferred = deferred || $q.defer();
if (!data || !data.pubkey || !data.keypair) {
deferred.resolve();
return deferred.promise;
}
console.debug('[ES] [registry] Loading pages count...');
// Load subscriptions count
that.raw.count({pubkey: data.pubkey})
.then(function(res) {
data.pages = data.pages || {};
data.pages.count = res && res.hits && res.hits.total;
console.debug('[ES] [registry] Loaded pages count (' + data.pages.count + ')');
deferred.resolve(data);
})
.catch(function(err) {
console.error('[ES] [registry] Error while counting page: ' + (err.message ? err.message : err));
deferred.resolve(data);
});
return deferred.promise;
}
function getCategories() {
if (that.raw.categories && that.raw.categories.length) {
var deferred = $q.defer();
deferred.resolve(that.raw.categories);
return deferred.promise;
}
return that.raw.category.all()
.then(function(res) {
if (res.hits.total === 0) {
that.raw.categories = [];
}
else {
var categories = res.hits.hits.reduce(function(result, hit) {
var cat = hit._source;
cat.id = hit._id;
return result.concat(cat);
}, []);
// add as map also
_.forEach(categories, function(cat) {
categories[cat.id] = cat;
});
that.raw.categories = categories;
}
return that.raw.categories;
});
}
function getCategory(params) {
return that.raw.category.get(params)
.then(function(hit) {
var res = hit._source;
res.id = hit._id;
return res;
});
}
function readRecordFromHit(hit, categories) {
if (!hit) return;
var record = hit._source;
if (record.category && record.category.id) {
record.category = categories[record.category.id];
}
if (hit.highlight) {
if (hit.highlight.title) {
record.title = hit.highlight.title[0];
}
if (hit.highlight.description) {
record.description = hit.highlight.description[0];
}
if (hit.highlight.location) {
record.location = hit.highlight.location[0];
}
if (hit.highlight.tags) {
record.tags = hit.highlight.tags.reduce(function(res, tag){
return res.concat(tag.replace('<em>', '').replace('</em>', ''));
},[]);
}
}
// avatar
record.avatar = esHttp.image.fromHit(hit, 'avatar');
// pictures
if (hit._source.pictures && hit._source.pictures.reduce) {
record.pictures = hit._source.pictures.reduce(function(res, pic) {
return res.concat(esHttp.image.fromAttachment(pic.file));
}, []);
}
return record;
}
function search(request) {
request = request || {};
request.from = request.from || 0;
request.size = request.size || 20;
request._source = request._source || fields.commons;
request.highlight = request.highlight || {
fields : {
title : {},
description : {}
}
};
return $q.all([
// load categories
getCategories(),
// Do search
that.raw.search(request)
])
.then(function(res) {
var categories = res[0];
res = res[1];
if (!res || !res.hits || !res.hits.total) {
return {
total: 0,
hits: []
};
}
// Get geo_distance filter
var geoDistanceObj = esHttp.util.findObjectInTree(request.query, 'geo_distance');
var geoPoint = geoDistanceObj && geoDistanceObj.geoPoint;
var geoDistanceUnit = geoDistanceObj && geoDistanceObj.distance && geoDistanceObj.distance.replace(new RegExp("[0-9 ]+", "gm"), '');
var hits = res.hits.hits.reduce(function(result, hit) {
var record = readRecordFromHit(hit, categories);
record.id = hit._id;
// Add distance to point
if (geoPoint && record.geoPoint && geoDistanceUnit) {
record.distance = esGeo.point.distance(
geoPoint.lat, geoPoint.lon,
record.geoPoint.lat, record.geoPoint.lon,
geoDistanceUnit
);
}
return result.concat(record);
}, []);
return {
total: res.hits.total,
hits: hits
};
});
}
function loadData(id, options) {
options = options || {};
options.raw = angular.isDefined(options.raw) ? options.raw : false;
options.fecthPictures = angular.isDefined(options.fetchPictures) ? options.fetchPictures : options.raw;
return $q.all([
// load categories
getCategories(),
// Do get source
options.fecthPictures ?
that.raw.get({id: id}) :
that.raw.getCommons({id: id})
])
.then(function(res) {
var categories = res[0];
var hit = res[1];
var record = readRecordFromHit(hit, categories);
// parse description as Html
if (!options.raw) {
record.description = esHttp.util.parseAsHtml(record.description, {
tagState: 'app.registry_lookup'
});
}
// Load issuer (avatar, name, uid, etc.)
return csWot.extend({pubkey: record.issuer})
.then(function(issuer) {
return {
id: hit._id,
issuer: issuer,
record: record
};
});
});
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
// Extend
listeners = [
csWallet.api.data.on.login($rootScope, onWalletLogin, this),
csWallet.api.data.on.init($rootScope, onWalletReset, this),
csWallet.api.data.on.reset($rootScope, onWalletReset, this)
];
}
function refreshState() {
var enable = esHttp.alive;
if (!enable && listeners && listeners.length > 0) {
console.debug("[ES] [subscription] Disable");
removeListeners();
if (csWallet.isLogin()) {
return onWalletReset(csWallet.data);
}
}
else if (enable && (!listeners || listeners.length === 0)) {
console.debug("[ES] [subscription] Enable");
addListeners();
if (csWallet.isLogin()) {
return onWalletLogin(csWallet.data);
}
}
}
// Default actions
csPlatform.ready().then(function() {
esHttp.api.node.on.start($rootScope, refreshState, this);
esHttp.api.node.on.stop($rootScope, refreshState, this);
return refreshState();
});
that.category = {
all: getCategories,
get: getCategory
};
that.record = {
search: search,
load: loadData,
add: esHttp.record.post('/page/record', {tagFields: ['title', 'description'], creationTime: true}),
update: esHttp.record.post('/page/record/:id/_update', {tagFields: ['title', 'description']}),
remove: esHttp.record.remove('page', 'record'),
fields: {
commons: fields.commons
},
picture: {
all: esHttp.get('/page/record/:id?_source=pictures')
},
comment: esComment.instance('page')
};
that.currency = {
all: esHttp.get('/currency/record/_search?_source=currencyName,peers.host,peers.port'),
get: esHttp.get('/currency/record/:id/_source')
};
return that;
}])
;
ESRegistryLookupController.$inject = ['$scope', '$focus', '$timeout', '$filter', '$controller', '$location', '$translate', '$ionicPopover', 'Device', 'UIUtils', 'ModalUtils', 'BMA', 'csSettings', 'csWallet', 'esModals', 'esRegistry', 'esHttp'];
ESWalletPagesController.$inject = ['$scope', '$controller', '$timeout', 'UIUtils', 'csWallet'];
ESRegistryRecordViewController.$inject = ['$scope', '$rootScope', '$state', '$q', '$timeout', '$ionicPopover', '$ionicHistory', '$translate', '$anchorScroll', 'csConfig', 'csWallet', 'esRegistry', 'UIUtils', 'esHttp'];
ESRegistryRecordEditController.$inject = ['$scope', '$timeout', '$state', '$q', '$ionicHistory', '$focus', '$translate', '$controller', 'Device', 'UIUtils', 'ModalUtils', 'csWallet', 'esHttp', 'esRegistry'];angular.module('cesium.es.registry.controllers', ['cesium.es.services', 'cesium.es.common.controllers'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.registry_lookup', {
url: "/page?q&type&hash&category&location&issuer&reload&lat&lon&d&last",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/registry/lookup.html",
controller: 'ESRegistryLookupCtrl'
}
},
data: {
large: 'app.registry_lookup_lg',
silentLocationChange: true
}
})
.state('app.registry_lookup_lg', {
url: "/wot/page/lg?q&type&hash&category&location&issuer&reload&lat&lon&d&last",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/registry/lookup_lg.html",
controller: 'ESRegistryLookupCtrl'
}
},
data: {
silentLocationChange: true
}
})
.state('app.wallet_pages', {
url: "/wallet/pages?refresh",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/registry/view_wallet_pages.html",
controller: 'ESWalletPagesCtrl'
}
},
data: {
login: true,
minData: true
}
})
.state('app.view_page', {
url: "/page/view/:id/:title?refresh",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/registry/view_record.html",
controller: 'ESRegistryRecordViewCtrl'
}
}
})
.state('app.view_page_anchor', {
url: "/page/view/:id/:title/:anchor",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/registry/view_record.html",
controller: 'ESRegistryRecordViewCtrl'
}
}
})
.state('app.registry_add_record', {
cache: false,
url: "/page/add/:type",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/registry/edit_record.html",
controller: 'ESRegistryRecordEditCtrl'
}
},
data: {
auth: true,
minData: true
}
})
.state('app.registry_edit_record', {
cache: false,
url: "/page/edit/:id/:title",
views: {
'menuContent': {
templateUrl: "plugins/es/templates/registry/edit_record.html",
controller: 'ESRegistryRecordEditCtrl'
}
},
data: {
auth: true,
minData: true
}
})
;
}])
.controller('ESRegistryLookupCtrl', ESRegistryLookupController)
.controller('ESWalletPagesCtrl', ESWalletPagesController)
.controller('ESRegistryRecordViewCtrl', ESRegistryRecordViewController)
.controller('ESRegistryRecordEditCtrl', ESRegistryRecordEditController)
;
function ESRegistryLookupController($scope, $focus, $timeout, $filter, $controller, $location, $translate, $ionicPopover,
Device, UIUtils, ModalUtils, BMA, csSettings, csWallet, esModals, esRegistry, esHttp) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESLookupPositionCtrl', {$scope: $scope}));
var defaultSearchLimit = 10;
$scope.search = {
text: '',
results: [],
loading: true,
lastRecords: true,
type: null,
category: null,
location: null,
advanced: null,
issuer: null,
geoDistance: !isNaN(csSettings.data.plugins.es.geoDistance) ? csSettings.data.plugins.es.geoDistance : 20
};
$scope.searchTextId = 'registrySearchText';
$scope.enableFilter = true;
$scope.smallscreen = angular.isDefined($scope.smallscreen) ? $scope.smallscreen : UIUtils.screen.isSmall();
$scope.options = angular.merge($scope.options||{}, {
location: {
show: true,
help: 'REGISTRY.SEARCH.LOCATION_HELP'
}
});
$scope.enter = function(e, state) {
if (!$scope.entered || !$scope.search.results || $scope.search.results.length === 0) {
// Resolve distance unit
if (!$scope.geoUnit) {
return $translate('LOCATION.DISTANCE_UNIT')
.then(function(unit) {
$scope.geoUnit = unit;
return $scope.enter(e, state); // Loop
});
}
var finishEntered = function() {
// removeIf(device)
// Focus on search text (only if NOT device, to avoid keyboard opening)
if ($scope.searchTextId) {
$focus($scope.searchTextId);
}
// endRemoveIf(device)
$scope.entered = true;
$scope.doSearch();
};
// Search by text
if (state.stateParams && state.stateParams.q && (typeof state.stateParams.q == 'string')) {
$scope.search.text=state.stateParams.q;
}
if (state.stateParams && state.stateParams.hash) { // hash tag parameter
$scope.search.text = '#' + state.stateParams.hash;
}
// Search on location
if (state.stateParams && state.stateParams.location) {
$scope.search.location = state.stateParams.location;
if (state.stateParams.lat && state.stateParams.lon) {
$scope.search.geoPoint = {
lat: parseFloat(state.stateParams.lat),
lon: parseFloat(state.stateParams.lon)
};
}
if (state.stateParams.d) {
$scope.search.geoDistance = state.stateParams.d;
}
}
else {
var defaultSearch = csSettings.data.plugins.es.registry && csSettings.data.plugins.es.registry.defaultSearch;
// Apply defaults from settings
if (defaultSearch) {
if (defaultSearch.location){
angular.merge($scope.search, csSettings.data.plugins.es.registry.defaultSearch);
}
else {
defaultSearch = undefined; // invalid
}
}
// First time calling this view: apply profile location (if loaded)
if (!defaultSearch && csWallet.isLogin() && csWallet.data.profile) {
if (!csWallet.isDataLoaded()) {
UIUtils.loading.show();
return csWallet.loadData()
.then(function() {
UIUtils.loading.hide();
return $scope.enter(e,state); // loop
});
}
$scope.search.geoPoint = csWallet.data.profile.geoPoint;
$scope.search.location = csWallet.data.profile.city||(csWallet.data.profile.geoPoint ? $translate.instant('LOCATION.PROFILE_POSITION') : undefined);
}
}
// Search on type
if (state.stateParams && (state.stateParams.type || state.stateParams.last)) {
if (state.stateParams.last || state.stateParams.type == 'last') {
$scope.search.lastRecords = true;
$scope.search.type = undefined;
}
else {
$scope.search.type = state.stateParams.type;
}
}
else {
$scope.search.lastRecords = false;
}
// Search on issuer
if (state.stateParams && state.stateParams.issuer) {
$scope.search.issuer = state.stateParams.issuer;
}
// Search on category
if (state.stateParams && state.stateParams.category) {
esRegistry.category.get({id: state.stateParams.category})
.then(function(cat) {
$scope.search.category = cat;
finishEntered();
})
.catch(UIUtils.onError("REGISTRY.ERROR.LOAD_CATEGORY_FAILED"));
}
else {
finishEntered();
}
}
$scope.showFab('fab-add-registry-record');
};
$scope.$on('$ionicView.enter', function(e, state) {
// WARN: do not set by reference
// because it can be overrided by sub controller
return $scope.enter(e, state);
});
// Store some search options as settings defaults
$scope.leave = function() {
var dirty = false;
csSettings.data.plugins.es.registry = csSettings.data.plugins.es.registry || {};
csSettings.data.plugins.es.registry.defaultSearch = csSettings.data.plugins.es.registry.defaultSearch || {};
// Check if location changed
var location = $scope.search.location && $scope.search.location.trim();
var oldLocation = csSettings.data.plugins.es.registry.defaultSearch.location;
if (!oldLocation || (oldLocation !== location)) {
csSettings.data.plugins.es.registry.defaultSearch = {
location: location,
geoPoint: location && $scope.search.geoPoint ? angular.copy($scope.search.geoPoint) : undefined
};
dirty = true;
}
// Check if distance changed
var odlDistance = csSettings.data.plugins.es.geoDistance;
if (!odlDistance || odlDistance !== $scope.search.geoDistance) {
csSettings.data.plugins.es.geoDistance = $scope.search.geoDistance;
dirty = true;
}
// execute with a delay, for better UI perf
if (dirty) {
$timeout(function() {
csSettings.store();
});
}
};
$scope.$on('$ionicView.leave', function() {
// WARN: do not set by reference
// because it can be overrided by sub controller
return $scope.leave();
});
$scope.onGeoPointChanged = function() {
if ($scope.search.loading) return;
if ($scope.search.geoPoint && $scope.search.geoPoint.lat && $scope.search.geoPoint.lon && !$scope.search.geoPoint.exact) {
$scope.doSearch();
$scope.updateLocationHref();
}
};
$scope.$watch('search.geoPoint', $scope.onGeoPointChanged, true);
$scope.resolveLocationPosition = function() {
if ($scope.search.loadingPosition) return;
$scope.search.loadingPosition = true;
return $scope.searchPosition($scope.search.location)
.then(function(res) {
if (!res) {
$scope.search.loading = false;
$scope.search.results = undefined;
$scope.search.total = 0;
$scope.search.loadingPosition = false;
$scope.search.geoPoint = undefined;
throw 'CANCELLED';
}
$scope.search.geoPoint = res;
if (res.shortName && !res.exact) {
$scope.search.location = res.shortName;
}
$scope.search.loadingPosition = false;
});
};
$scope.doGetLastRecords = function(from) {
$scope.hidePopovers();
$scope.search.text = undefined;
return $scope.doSearch(from);
};
$scope.doSearchText = function() {
$scope.doSearch();
};
$scope.doSearch = function(from) {
$scope.search.loading = !from;
// Resolve location position
if ($scope.search.location && $scope.search.location.length >= 3 && !$scope.search.geoPoint) {
return $scope.resolveLocationPosition()
.then(function() {
return $scope.doSearch(from); // Loop
});
}
var text = $scope.search.text && $scope.search.text.trim() || '';
$scope.search.lastRecords = !text || !text.length;
var matches = [];
var filters = [];
if (text && text.length) {
// pubkey : use a special 'term', because of 'non indexed' field
if (BMA.regexp.PUBKEY.test(text /*case sensitive*/)) {
filters.push({term : { pubkey: text}});
}
else {
text = text.toLowerCase();
var tags = text ? esHttp.util.parseTags(text) : undefined;
var matchFields = ["title", "description", "city", "address"];
matches.push({multi_match : { query: text,
fields: matchFields,
type: "phrase_prefix"
}});
matches.push({match : { title: {query: text, boost: 2}}});
matches.push({prefix: {title: text}});
matches.push({match : { description: text}});
matches.push({
nested: {
path: "category",
query: {
bool: {
filter: {
match: { "category.name": text}
}
}
}
}
});
if (tags && tags.length) {
filters.push({terms: {tags: tags}});
}
}
}
// issuer: use only on filter
else if ($scope.search.issuer) {
filters.push({term : { issuer: $scope.search.issuer}});
}
if ($scope.search.type) {
filters.push({term: { type: $scope.search.type}});
}
if ($scope.search.category) {
filters.push({
nested: {
path: "category",
query: {
bool: {
filter: {
term: { "category.id": $scope.search.category.id}
}
}
}
}
});
}
var location = $scope.search.location && $scope.search.location.trim().toLowerCase();
if ($scope.search.geoPoint && $scope.search.geoPoint.lat && $scope.search.geoPoint.lon) {
// match location OR geo distance
if (location && location.length) {
var locationCity = location.split(',')[0];
filters.push({
or : [
// No position defined
{
and: [
{not: {exists: { field : "geoPoint" }}},
{match_phrase: { city: locationCity }}
]
},
// Has position
{geo_distance: {
distance: $scope.search.geoDistance + $scope.geoUnit,
geoPoint: {
lat: $scope.search.geoPoint.lat,
lon: $scope.search.geoPoint.lon
}
}}
]
});
}
else {
filters.push(
{geo_distance: {
distance: $scope.search.geoDistance + $scope.geoUnit,
geoPoint: {
lat: $scope.search.geoPoint.lat,
lon: $scope.search.geoPoint.lon
}
}});
}
}
var request = {
highlight: {fields : {title : {}, description: {}, tags: {}}},
from: from
};
if (matches.length > 0) {
request.query = request.query || {bool: {}};
request.query.bool.should = matches;
// Exclude result with score=0
request.query.bool.minimum_should_match = 1;
}
if (filters.length > 0) {
request.query = request.query || {bool: {}};
request.query.bool.filter = filters;
}
if ($scope.search.lastRecords) {
request.sort = {creationTime : "desc"};
}
var queryId = ($scope.queryId && $scope.queryId + 1) || 0;
$scope.queryId = queryId;
var isSameRequest = function() {
return $scope.queryId == queryId;
};
// Update href location
$scope.updateLocationHref();
// Execute the request
return $scope.doRequest(request, isSameRequest);
};
$scope.doRequest = function(options, isSameRequestFn) {
options = options || {};
options.from = options.from || 0;
options.size = options.size || defaultSearchLimit;
if (options.size < defaultSearchLimit) options.size = defaultSearchLimit;
$scope.search.loading = (options.from === 0);
return esRegistry.record.search(options)
.then(function(res) {
if (isSameRequestFn && !isSameRequestFn()) return; // Skip apply if not same request:
if (!res || !res.hits || !res.hits.length) {
$scope.search.results = (options.from > 0) ? $scope.search.results : [];
$scope.search.total = (options.from > 0) ? $scope.search.total : 0;
$scope.search.loading = false;
$scope.search.hasMore = false;
return;
}
var formatSlug = $filter('formatSlug');
_.forEach(res.hits, function(record) {
// Compute title for url
record.urlTitle = formatSlug(record.title);
});
// Replace results, or append if 'show more' clicked
if (!options.from) {
$scope.search.results = res.hits;
$scope.search.total = res.total;
}
else {
$scope.search.results = $scope.search.results.concat(res.hits);
}
$scope.search.hasMore = $scope.search.results.length < res.total;
$scope.search.loading = false;
$scope.motion.show({selector: '.list .item', ink: true});
})
.catch(function(err) {
$scope.search.loading = false;
$scope.search.results = (options.from > 0) ? $scope.search.results : [];
$scope.search.total = (options.from > 0) ? $scope.search.total : 0;
$scope.search.hasMore = false;
UIUtils.onError('REGISTRY.ERROR.LOOKUP_RECORDS_FAILED')(err);
});
};
$scope.showMore= function() {
var from = $scope.search.results ? $scope.search.results.length : 0;
$scope.search.loadingMore = true;
var searchFunction = ($scope.search.lastRecords) ?
$scope.doGetLastRecords :
$scope.doSearch;
return searchFunction(from)
.then(function() {
$scope.search.loadingMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
})
.catch(function(err) {
console.error(err);
$scope.search.loadingMore = false;
$scope.search.hasMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
$scope.removeType = function() {
$scope.search.type = null;
$scope.doSearch();
$scope.updateLocationHref();
};
$scope.removeCategory = function() {
$scope.search.category = null;
$scope.category = null;
$scope.doSearch();
$scope.updateLocationHref();
};
$scope.removeLocation = function() {
$scope.search.location = null;
$scope.search.geoPoint = null;
$scope.doSearch();
$scope.updateLocationHref();
};
// Update location href
$scope.updateLocationHref = function(from) {
// removeIf(device)
// Skip when "show more"
if (from) return;
$timeout(function() {
var text = $scope.search.text && $scope.search.text.trim();
var location = $scope.search.location && $scope.search.location.trim();
var stateParams = {
location: location && location.length ? location : undefined,
category: $scope.search.category ? $scope.search.category.id : undefined,
last: $scope.search.lastRecords ? true : undefined,
type: $scope.search.type ? $scope.search.type : undefined,
lat: $scope.search.geoPoint && $scope.search.geoPoint.lat || undefined,
lon: $scope.search.geoPoint && $scope.search.geoPoint.lon || undefined,
d: $scope.search.geoPoint && $scope.search.geoDistance || undefined
};
if (text && text.match(/^#\w+$/)) {
stateParams.hash = text.substr(1);
}
else if (text && text.length){
stateParams.q = text;
}
$location.search(stateParams).replace();
});
// endRemoveIf(device)
};
$scope.onToggleAdvanced = function() {
if ($scope.search.entered && !$scope.search.lastRecords) {
$scope.doSearch();
$scope.updateLocationHref();
}
};
$scope.$watch('search.advanced', $scope.onToggleAdvanced, true);
$scope.toggleAdvanced = function() {
$scope.search.advanced = !$scope.search.advanced;
$timeout($scope.hidePopovers, 200);
};
/* -- modals -- */
$scope.showRecordTypeModal = function(event) {
$scope.hidePopovers();
$timeout(function() {
if (event.isDefaultPrevented()) return;
ModalUtils.show('plugins/es/templates/registry/modal_record_type.html')
.then(function(type){
if (type) {
$scope.search.type = type;
$scope.doSearch();
$scope.updateLocationHref();
}
});
}, 350); // use timeout to allow event to be prevented in removeType()
};
$scope.showCategoryModal = function(event) {
$timeout(function() {
if (event.isDefaultPrevented()) return;
// load categories
esRegistry.category.all()
.then(function (categories) {
// open modal
return ModalUtils.show('plugins/es/templates/common/modal_category.html', 'ESCategoryModalCtrl as ctrl',
{categories: categories}, {focusFirstInput: true});
})
.then(function (cat) {
if (cat && cat.parent) {
$scope.search.category = cat;
$scope.doSearch();
$scope.updateLocationHref();
}
});
}, 350); // use timeout to allow event to be prevented in removeCategory()
};
$scope.showNewPageModal = function() {
$scope.hidePopovers();
return $scope.loadWallet()
.then(function(walletData) {
UIUtils.loading.hide();
if (walletData) {
return esModals.showNewPage();
}
});
};
/* -- popovers -- */
$scope.showActionsPopover = function(event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('plugins/es/templates/registry/lookup_popover_actions.html', {
scope: $scope
}).then(function(popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
$scope.showFiltersPopover = function(event) {
if (!$scope.filtersPopover) {
$ionicPopover.fromTemplateUrl('plugins/es/templates/registry/lookup_popover_filters.html', {
scope: $scope
}).then(function(popover) {
$scope.filtersPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.filtersPopover.remove();
});
$scope.filtersPopover.show(event);
});
}
else {
$scope.filtersPopover.show(event);
}
};
$scope.hideFiltersPopover = function() {
if ($scope.filtersPopover) {
$scope.filtersPopover.hide();
}
};
$scope.hidePopovers = function() {
$scope.hideActionsPopover();
$scope.hideFiltersPopover();
};
// TODO: remove auto add account when done
/* $timeout(function() {
$scope.search.text='lavenier';
$scope.doSearch();
}, 400);
*/
}
function ESWalletPagesController($scope, $controller, $timeout, UIUtils, csWallet) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESRegistryLookupCtrl', {$scope: $scope}));
$scope.searchTextId = undefined; // avoid focus
// Override the default enter
$scope.enter = function(e, state) {
if (!$scope.entered) {
return $scope.loadWallet({minData: true})
.then(function(walletData) {
UIUtils.loading.hide();
$scope.search.issuer = walletData.pubkey;
$scope.search.advanced = true;
$timeout($scope.doSearch, 100);
$scope.showFab('fab-wallet-add-registry-record');
});
}
else {
// Asking refresh
if (state.stateParams && state.stateParams.refresh) {
return $timeout($scope.doSearch, 2000 /*waiting for propagation, if deletion*/);
}
}
};
$scope.doUpdate = function() {
if (!csWallet.isLogin()) return;
$scope.search.issuer = csWallet.data.pubkey;
$scope.search.advanced = true;
return $scope.doSearch();
};
}
function ESRegistryRecordViewController($scope, $rootScope, $state, $q, $timeout, $ionicPopover, $ionicHistory, $translate,
$anchorScroll, csConfig, csWallet, esRegistry, UIUtils, esHttp) {
'ngInject';
$scope.formData = {};
$scope.id = null;
$scope.category = {};
$scope.pictures = [];
$scope.canEdit = false;
$scope.loading = true;
$scope.motion = UIUtils.motion.fadeSlideIn;
$scope.$on('$ionicView.beforeEnter', function (event, viewData) {
// Enable back button (workaround need for navigation outside tabs - https://stackoverflow.com/a/35064602)
viewData.enableBack = UIUtils.screen.isSmall() ? true : viewData.enableBack;
});
$scope.$on('$ionicView.enter', function(e, state) {
if (state.stateParams && state.stateParams.id) { // Load by id
if ($scope.loading || state.stateParams.refresh) { // prevent reload if same id (if not forced)
$scope.load(state.stateParams.id, state.stateParams.anchor);
}
$scope.$broadcast('$recordView.enter', state);
}
else {
$state.go('app.registry_lookup');
}
});
$scope.$on('$ionicView.beforeLeave', function(event, args){
$scope.$broadcast('$recordView.beforeLeave', args);
});
$scope.load = function(id, anchor) {
id = id || $scope.id;
$scope.loading = true;
return $q.all([
esRegistry.record.load(id)
.then(function (data) {
$scope.id= data.id;
$scope.formData = data.record;
//console.debug('Loading record', $scope.formData);
$scope.canEdit = csWallet.isUserPubkey($scope.formData.issuer);
$scope.issuer = data.issuer;
// avatar
$scope.avatar = $scope.formData.avatar;
$scope.avatarStyle= $scope.formData.avatar && {'background-image':'url("'+$scope.avatar.src+'")'};
UIUtils.loading.hide();
$scope.loading = false;
// Set Motion (only direct children, to exclude .lazy-load children)
$scope.motion.show({selector: '.list > .item, .list > ng-if > .item'});
})
.catch(function(err) {
// Retry (ES could have error)
if (!$scope.secondTry) {
$scope.secondTry = true;
$q(function() {
$scope.load(id);
}, 100);
}
else {
$scope.loading = false;
if (err && err.ucode === 404) {
UIUtils.toast.show('REGISTRY.ERROR.RECORD_NOT_EXISTS');
$state.go('app.registry_lookup');
}
else {
UIUtils.onError('REGISTRY.ERROR.LOAD_RECORD_FAILED')(err);
}
}
}),
// Load pictures
esRegistry.record.picture.all({id: id})
.then(function(hit) {
$scope.pictures = hit._source.pictures && hit._source.pictures.reduce(function(res, pic) {
return res.concat(esHttp.image.fromAttachment(pic.file));
}, []);
// Set Motion
if ($scope.pictures.length > 0) {
$scope.motion.show({
selector: '.lazy-load .item.card-gallery',
startVelocity: 3000
});
}
})
.catch(function() {
$scope.pictures = [];
}),
// Load other data (from child controller)
$timeout(function() {
return $scope.$broadcast('$recordView.load', id, esRegistry.record);
})
])
.then(function() {
// Display items in technical parts
$scope.motion.show({
selector: '.lazy-load .item',
startVelocity: 3000
});
// scroll (if comment anchor)
if (anchor) $timeout(function() {
$anchorScroll(anchor);
}, 1000);
});
};
// Edit click
$scope.edit = function() {
UIUtils.loading.show();
$state.go('app.registry_edit_record', {id: $scope.id});
};
$scope.delete = function() {
$scope.hideActionsPopover();
// translate
var translations;
$translate(['REGISTRY.VIEW.REMOVE_CONFIRMATION', 'REGISTRY.INFO.RECORD_REMOVED'])
.then(function(res) {
translations = res;
return UIUtils.alert.confirm(res['REGISTRY.VIEW.REMOVE_CONFIRMATION']);
})
.then(function(confirm) {
if (confirm) {
esRegistry.record.remove($scope.id)
.then(function () {
if (csWallet.data.pages && csWallet.data.pages.count) {
csWallet.data.pages.count--;
}
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go('app.wallet_pages', {refresh: true});
UIUtils.toast.show(translations['REGISTRY.INFO.RECORD_REMOVED']);
})
.catch(UIUtils.onError('REGISTRY.ERROR.REMOVE_RECORD_FAILED'));
}
});
};
/* -- modals & popover -- */
$scope.showActionsPopover = function(event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('plugins/es/templates/registry/view_popover_actions.html', {
scope: $scope
}).then(function(popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function() {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
$scope.showSharePopover = function(event) {
$scope.hideActionsPopover();
var title = $scope.formData.title;
// Use pod share URL - see issue #69
var url = esHttp.getUrl('/page/record/' + $scope.id + '/_share');
// Override default position, is small screen - fix #545
if (UIUtils.screen.isSmall()) {
event = angular.element(document.querySelector('#registry-share-anchor-'+$scope.id)) || event;
}
UIUtils.popover.share(event, {
bindings: {
url: url,
titleKey: 'REGISTRY.VIEW.POPOVER_SHARE_TITLE',
titleValues: {title: title},
time: $scope.formData.time,
postMessage: title
}
});
};
}
function ESRegistryRecordEditController($scope, $timeout, $state, $q, $ionicHistory, $focus, $translate, $controller,
Device, UIUtils, ModalUtils, csWallet, esHttp, esRegistry) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESPositionEditCtrl', {$scope: $scope}));
$scope.formData = {
title: null,
description: null,
socials: [],
geoPoint: null
};
$scope.loading = true;
$scope.dirty = false;
$scope.walletData = null;
$scope.id = null;
$scope.avatar = null;
$scope.pictures = [];
$scope.setForm = function(form) {
$scope.form = form;
};
$scope.$on('$ionicView.enter', function(e, state) {
$scope.loadWallet({minData: true})
.then(function(walletData) {
$scope.walletData = walletData;
if (state.stateParams && state.stateParams.id) { // Load by id
$scope.load(state.stateParams.id);
}
else {
if (state.stateParams && state.stateParams.type) {
$scope.updateView({
record: {
type: state.stateParams.type
}
});
}
}
// removeIf(device)
$focus('registry-record-title');
// endRemoveIf(device)
});
});
$scope.$on('$stateChangeStart', function (event, next, nextParams, fromState) {
if ($scope.dirty && !$scope.saving) {
// stop the change state action
event.preventDefault();
if (!$scope.loading) {
$scope.loading = true;
return UIUtils.alert.confirm('CONFIRM.SAVE_BEFORE_LEAVE',
'CONFIRM.SAVE_BEFORE_LEAVE_TITLE', {
cancelText: 'COMMON.BTN_NO',
okText: 'COMMON.BTN_YES_SAVE'
})
.then(function(confirmSave) {
$scope.loading = false;
if (confirmSave) {
$scope.form.$submitted=true;
return $scope.save(false/*silent*/, true/*haswait debounce*/)
.then(function(saved){
if (saved) {
$scope.dirty = false;
}
return saved; // change state only if not error
});
}
else {
$scope.dirty = false;
return true; // ok, change state
}
})
.then(function(confirmGo) {
if (confirmGo) {
// continue to the order state
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go(next.name, nextParams);
}
})
.catch(function(err) {
// Silent
});
}
}
});
$scope.load = function(id) {
$scope.loading = true;
esRegistry.record.load(id, {
raw: true
})
.then(function (data) {
if (data && data.record) {
$scope.updateView(data);
}
else {
$scope.updateView({record: {}});
}
})
.catch(function(err) {
UIUtils.loading.hide(10);
$scope.loading = false;
UIUtils.onError('REGISTRY.ERROR.LOAD_RECORD_FAILED')(err);
});
};
$scope.updateView = function(data) {
$scope.formData = data.record || {};
$scope.id= data.id;
// avatar
$scope.avatar = $scope.formData.avatar;
if ($scope.avatar) {
$scope.avatarStyle = $scope.avatar && {'background-image':'url("'+$scope.avatar.src+'")'};
$scope.avatarClass = {};
}
else {
$scope.avatarStyle = undefined;
$scope.avatarClass = {};
$scope.avatarClass['cion-page-' + $scope.formData.type] = !$scope.avatar;
}
// pictures
$scope.pictures = data.record && data.record.pictures || [];
delete data.record.pictures; // remove, as already stored in $scope.pictures
$scope.motion.show({
selector: '.animate-ripple .item, .card-gallery',
startVelocity: 3000
});
UIUtils.loading.hide();
// Update loading - done with a delay, to avoid trigger onFormDataChanged()
$timeout(function() {
$scope.loading = false;
}, 1000);
};
$scope.onFormDataChanged = function() {
if ($scope.loading) return;
$scope.dirty = true;
};
$scope.$watch('formData', $scope.onFormDataChanged, true);
$scope.needCategory = function() {
return $scope.formData.type && ($scope.formData.type=='company' || $scope.formData.type=='shop');
};
$scope.save = function(silent, hasWaitDebounce) {
$scope.form.$submitted=true;
if($scope.saving || // avoid multiple save
!$scope.form.$valid ||
(($scope.formData.type === 'shop' || $scope.formData.type === 'company') && (!$scope.formData.category || !$scope.formData.category.id))) {
return $q.reject();
}
if (!hasWaitDebounce) {
console.debug('[ES] [page] Waiting debounce end, before saving...');
return $timeout(function() {
return $scope.save(silent, true);
}, 650);
}
$scope.saving = true;
console.debug('[ES] [page] Saving record...');
var showSuccessToast = function() {
if (!silent) {
return $translate('REGISTRY.INFO.RECORD_SAVED')
.then(function(message){
UIUtils.toast.show(message);
});
}
};
var promise = $q.when();
return promise
.then(function(){
var json = $scope.formData;
if (!$scope.needCategory()) {
delete json.category;
}
json.time = esHttp.date.now();
// geo point
if (json.geoPoint && json.geoPoint.lat && json.geoPoint.lon) {
json.geoPoint.lat = parseFloat(json.geoPoint.lat);
json.geoPoint.lon = parseFloat(json.geoPoint.lon);
}
else{
json.geoPoint = null;
}
// Social url must be unique in socials links - Fix #306:
if (json.socials && json.socials.length) {
json.socials = _.uniq(json.socials, false, function(social) {
return social.url;
});
}
// Pictures
json.picturesCount = $scope.pictures.length;
if (json.picturesCount > 0) {
json.pictures = $scope.pictures.reduce(function (res, pic) {
return res.concat({file: esHttp.image.toAttachment(pic)});
}, []);
}
else {
json.pictures = [];
}
// Avatar
if ($scope.avatar && $scope.avatar.src) {
return UIUtils.image.resizeSrc($scope.avatar.src, true) // resize to avatar
.then(function(imageSrc) {
json.avatar = esHttp.image.toAttachment({src: imageSrc});
return json;
});
}
else {
// Workaround to allow content deletion, because of a bug in the ES attachment-mapper:
// get error (in ES node) : MapperParsingException[No content is provided.] - AttachmentMapper.parse(AttachmentMapper.java:471
json.avatar = {
_content: '',
_content_type: ''
};
return json;
}
})
.then(function(json){
// Create
if (!$scope.id) {
return esRegistry.record.add(json);
}
// Update
return esRegistry.record.update(json, {id: $scope.id});
})
.then(function(id) {
console.info("[ES] [page] Record successfully saved.");
if (!$scope.id && csWallet.data.pages && csWallet.data.pages.count) {
csWallet.data.pages.count++;
}
$scope.id = $scope.id || id;
$scope.saving = false;
$scope.dirty = false;
showSuccessToast();
$ionicHistory.clearCache($ionicHistory.currentView().stateId); // clear current view
$ionicHistory.nextViewOptions({historyRoot: true});
return $state.go('app.view_page', {id: $scope.id, refresh: true});
})
.catch(function(err) {
$scope.saving = false;
UIUtils.onError('REGISTRY.ERROR.SAVE_RECORD_FAILED')(err);
});
};
$scope.openPicturePopup = function() {
Device.camera.getPicture()
.then(function(imageData) {
if (imageData) {
$scope.pictures.push({src: "data:image/png;base64," + imageData});
}
})
.catch(UIUtils.onError('ERROR.TAKE_PICTURE_FAILED'));
};
$scope.rotateAvatar = function(){
if (!$scope.avatar || !$scope.avatar.src || $scope.rotating) return;
$scope.rotating = true;
return UIUtils.image.rotateSrc($scope.avatar.src)
.then(function(imageData){
$scope.avatar.src = imageData;
$scope.avatarStyle={'background-image':'url("'+imageData+'")'};
$scope.dirty = true;
$scope.rotating = false;
})
.catch(function(err) {
console.error(err);
$scope.rotating = false;
});
};
$scope.fileChanged = function(event) {
UIUtils.loading.show();
return $q(function(resolve, reject) {
var file = event.target.files[0];
UIUtils.image.resizeFile(file)
.then(function(imageData) {
$scope.pictures.push({src: imageData});
UIUtils.loading.hide();
resolve();
});
});
};
$scope.removePicture = function(index){
$scope.pictures.splice(index, 1);
};
$scope.favoritePicture = function(index){
if (index > 0) {
var item = $scope.pictures[index];
$scope.pictures.splice(index, 1);
$scope.pictures.splice(0, 0, item);
}
};
$scope.cancel = function() {
$ionicHistory.goBack();
};
/* -- modals -- */
$scope.showAvatarModal = function() {
if (Device.camera.enable) {
return Device.camera.getPicture()
.then(function(imageData) {
if (!imageData) return;
$scope.avatar = {src: "data:image/png;base64," + imageData};
$scope.avatarStyle={'background-image':'url("'+imageData+'")'};
$scope.dirty = true;
$scope.avatarClass = {};
})
.catch(UIUtils.onError('ERROR.TAKE_PICTURE_FAILED'));
}
else {
return ModalUtils.show('plugins/es/templates/common/modal_edit_avatar.html','ESAvatarModalCtrl',
{})
.then(function(imageData) {
if (!imageData) return;
$scope.avatar = {src: imageData};
$scope.avatarStyle={'background-image':'url("'+imageData+'")'};
$scope.dirty = true;
$scope.avatarClass = {};
});
}
};
$scope.showRecordTypeModal = function() {
ModalUtils.show('plugins/es/templates/registry/modal_record_type.html')
.then(function(type){
if (type) {
$scope.formData.type = type;
if (!$scope.avatar) {
$scope.avatarClass['cion-page-' + type] = true;
}
}
});
};
$scope.showCategoryModal = function(parameters) {
// load categories
esRegistry.category.all()
.then(function(categories){
// open modal
return ModalUtils.show('plugins/es/templates/common/modal_category.html', 'ESCategoryModalCtrl as ctrl',
{categories: categories}, {focusFirstInput: true});
})
.then(function(cat){
if (cat && cat.parent) {
$scope.formData.category = cat;
}
});
};
}
angular.module('cesium.market.services', [
// removeIf(device)
'cesium.market.converse.services',
// endRemoveIf(device)
'cesium.market.modal.services',
'cesium.market.record.services',
'cesium.market.wallet.services',
'cesium.market.settings.services'
]);
angular.module('cesium.market.plugin', [
'cesium.market.app.controllers',
'cesium.market.join.controllers',
'cesium.market.login.controllers',
'cesium.market.search.controllers',
'cesium.market.record.controllers',
'cesium.market.wallet.controllers',
'cesium.market.category.controllers',
'cesium.market.wot.controllers',
'cesium.market.document.controllers',
// Services
'cesium.market.services'
])
.run(['csConfig', 'Modals', 'mkModals', function(csConfig, Modals, mkModals) {
if (csConfig.plugins && csConfig.plugins.market && csConfig.plugins.market.enable) {
console.debug("[plugin] [market] Override login and join modals");
Modals.showLogin = mkModals.showLogin;
Modals.showJoin = mkModals.showJoin;
}
}]);
angular.module('cesium.market.modal.services', ['cesium.modal.services'])
.factory('mkModals', ['ModalUtils', function(ModalUtils) {
'ngInject';
function showJoinModal(parameters) {
return ModalUtils.show('plugins/market/templates/join/modal_join.html', 'MkJoinModalCtrl', parameters);
}
function showHelpModal(parameters) {
return ModalUtils.show('plugins/market/templates/help/modal_help.html', 'HelpModalCtrl', parameters);
}
function showLoginModal(parameters) {
return ModalUtils.show('plugins/market/templates/login/modal_login.html', 'MarketLoginModalCtrl', parameters);
}
function showEventLoginModal(parameters) {
return ModalUtils.show('plugins/market/templates/login/modal_event_login.html', 'MarketEventLoginModalCtrl', parameters);
}
return {
showHelp: showHelpModal,
showJoin: showJoinModal,
showLogin: showLoginModal,
showEventLogin: showEventLoginModal
};
}]);
angular.module('cesium.market.record.services', ['ngResource', 'cesium.services', 'cesium.es.services', 'cesium.market.settings.services'])
.factory('mkRecord', ['$q', 'csSettings', 'BMA', 'csConfig', 'esHttp', 'esComment', 'esGeo', 'csWot', 'csCurrency', 'mkSettings', function($q, csSettings, BMA, csConfig, esHttp, esComment, esGeo, csWot, csCurrency, mkSettings) {
'ngInject';
function MkRecord() {
var
fields = {
commons: ["category", "title", "description", "issuer", "time", "creationTime", "location", "address", "city", "price",
"unit", "currency", "thumbnail._content_type", "picturesCount", "type", "stock", "fees", "feesCurrency",
"geoPoint"]
},
exports = {
_internal: {}
},
filters = {
localSale: {
excludes: [
'cat2', // Voitures
'cat3', // Motos
'cat4', // Caravaning
'cat5', // Utilitaires
'cat7', // Nautisme
'cat28', // Animaux
'cat71', // Emploi
'cat8', // Immobilier
'cat66', // Vacances
'cat56', // Matériel professionnel
'cat31', // Services
'cat48' // Vins &amp; Gastronomie
]
}
};
exports._internal.record= {
postSearch: esHttp.post('/market/record/_search')
};
exports._internal.category= {
get: esHttp.get('/market/category/:id'),
all: esHttp.get('/market/category/_search?sort=order&size=1000&_source=name,parent'),
search: esHttp.post('/market/category/_search')
};
function _findAttributeInObjectTree(obj, attrName) {
if (!obj) return;
if (obj[attrName]) return obj[attrName];
if (Array.isArray(obj)) {
return obj.reduce(function(res, item) {
return res ? res : _findAttributeInObjectTree(item, attrName);
}, false)
}
if (typeof obj == "object") {
return _.reduce(_.keys(obj), function (res, key) {
return res ? res : _findAttributeInObjectTree(obj[key], attrName);
}, false);
}
}
function getCategories() {
if (exports._internal.categories && exports._internal.categories.length) {
var deferred = $q.defer();
deferred.resolve(exports._internal.categories);
return deferred.promise;
}
return exports._internal.category.all()
.then(function(res) {
if (res.hits.total === 0) {
exports._internal.categories = [];
}
else {
var categories = res.hits.hits.reduce(function(result, hit) {
var cat = hit._source;
cat.id = hit._id;
return result.concat(cat);
}, []);
// add as map also
_.forEach(categories, function(cat) {
categories[cat.id] = cat;
});
exports._internal.categories = categories;
}
return exports._internal.categories;
});
}
function getFilteredCategories(options) {
options = options || {};
options.filter = angular.isDefined(options.filter) ? options.filter : undefined;
var cachedResult = exports._internal.filteredCategories && exports._internal.filteredCategories[options.filter];
if (cachedResult && cachedResult.length) {
var deferred = $q.defer();
deferred.resolve(cachedResult);
return deferred.promise;
}
// Prepare filter exclude function
var excludes = options.filter && filters[options.filter] && filters[options.filter].excludes;
var isExclude = excludes && function(value) {
return _.contains(excludes, value);
};
return exports._internal.category.all()
.then(function(res) {
// no result
if (res.hits.total === 0) return [];
var categories = res.hits.hits.reduce(function(result, hit) {
var cat = hit._source;
cat.id = hit._id;
return (isExclude &&
((cat.parent && isExclude(cat.parent)) || isExclude(cat.id))) ?
result :
result.concat(cat);
}, []);
// add as map also
_.forEach(categories, function(cat) {
categories[cat.id] = cat;
});
exports._internal.filteredCategories = exports._internal.filteredCategories || {};
exports._internal.filteredCategories[options.type] = categories;
return categories;
});
}
function getCategory(params) {
return exports._internal.category.get(params)
.then(function(hit) {
var res = hit._source;
res.id = hit._id;
return res;
});
}
function getCategoriesStats(options) {
options = options || {};
// Make sure to have currency
if (!options.currencies) {
return mkSettings.currencies()
.then(function (currencies) {
options.currencies = currencies;
return getCategoriesStats(options); // loop
});
}
var request = {
size: 0,
aggs: {
category: {
nested: {
path: 'category'
},
aggs: {
by_id: {
terms: {
field: 'category.id',
size: 1000
}
}
}
}
}
};
var filters = [];
var matches = [];
if (options.withStock) {
filters.push({range: {stock: {gt: 0}}});
}
if (!options.withOld) {
var minTime = options.minTime ? options.minTime : Date.now() / 1000 - 24 * 365 * 60 * 60; // last year
// Round to hour, to be able to use cache
minTime = Math.floor(minTime / 60 / 60 ) * 60 * 60;
filters.push({range: {time: {gte: minTime}}});
}
if (options.currencies && options.currencies.length) {
filters.push({terms: {currency: options.currencies}});
}
// Add query to request
if (matches.length || filters.length) {
request.query = {bool: {}};
if (matches.length) {
request.query.bool.should = matches;
}
if (filters.length) {
request.query.bool.filter = filters;
}
}
var params = {
request_cache: angular.isDefined(options.cache) ? options.cache : true // enable by default
};
return $q.all([
getFilteredCategories(options),
exports._internal.record.postSearch(request, params)
]).then(function(res) {
var categories = res[0];
res = res[1];
var buckets = (res.aggregations.category && res.aggregations.category.by_id && res.aggregations.category.by_id.buckets || [])
var countById = {};
buckets.forEach(function(bucket){
var cat = categories[bucket.key];
if (cat){
countById[bucket.key] = bucket.doc_count;
if (cat.parent) {
countById[cat.parent] = (countById[cat.parent] || 0) + bucket.doc_count;
}
}
});
return categories.reduce(function(res, cat) {
return res.concat(angular.merge({
count: countById[cat.id] || 0
}, cat))
}, []);
})
.then(function(res) {
//var parents = _.filter(res, function(cat) {return !cat.parent;});
var catByParent = _.groupBy(res, function(cat) {return cat.parent || 'roots';});
_.forEach(catByParent.roots, function(parent) {
parent.children = catByParent[parent.id];
});
// group by parent category
return catByParent.roots;
})
.catch(function(err) {
console.error(err);
});
}
function readRecordFromHit(hit, categories, currentUD, options) {
options = options || {};
var record = hit._source;
if (record.category && record.category.id) {
record.category = categories[record.category.id];
}
if (record.price && options.convertPrice && currentUD) {
if (record.unit==='UD') {
record.price = record.price * currentUD;
}
}
if (record.fees && options.convertPrice && currentUD && (!record.feesCurrency || record.feesCurrency === record.currency)) {
if (record.unit==='UD') {
record.fees = record.fees * currentUD;
}
}
if (options.html && hit.highlight) {
if (hit.highlight.title) {
record.title = hit.highlight.title[0];
}
if (hit.highlight.description) {
record.description = hit.highlight.description[0];
}
else {
record.description = esHttp.util.parseAsHtml(record.description, {
tagState: 'app.market_lookup'
})
}
if (hit.highlight.location) {
record.location = hit.highlight.location[0];
}
if (hit.highlight.city) {
record.city = hit.highlight.city[0];
}
if (record.category && hit.highlight["category.name"]) {
record.category.name = hit.highlight["category.name"][0];
}
}
else if (options.html) {
// description
record.description = esHttp.util.parseAsHtml(record.description, {
tagState: 'app.market_lookup'
});
}
// thumbnail
record.thumbnail = esHttp.image.fromHit(hit, 'thumbnail');
// pictures
if (hit._source.pictures && hit._source.pictures.reduce) {
record.pictures = hit._source.pictures.reduce(function(res, pic) {
return pic && pic.file ? res.concat(esHttp.image.fromAttachment(pic.file)) : res;
}, []);
}
// backward compat (before gchange v0.6)
if (record.location && !record.city) {
record.city = record.location;
}
return record;
}
exports._internal.searchText = esHttp.get('/market/record/_search?q=:search');
exports._internal.search = esHttp.post('/market/record/_search');
exports._internal.get = esHttp.get('/market/record/:id');
exports._internal.getCommons = esHttp.get('/market/record/:id?_source=' + fields.commons.join(','));
function search(request) {
request = request || {};
request.from = request.from || 0;
request.size = request.size || 20;
request._source = request._source || fields.commons;
request.highlight = request.highlight || {
fields : {
title : {},
description : {},
"category.name" : {},
tags: {}
}
};
return $q.all([
// load categories
exports.category.all(),
// Get current UD
csCurrency.currentUD()
.then(function (currentUD) {
return currentUD;
})
.catch(function(err) {
console.error(err);
return 1;
}),
// Do search
exports._internal.search(request)
])
.then(function(res) {
var categories = res[0];
var currentUD = res[1];
res = res[2];
if (!res || !res.hits || !res.hits.total) {
return {
total: 0,
hits: []
};
}
// Get the geoPoint from the 'geo_distance' filter
var geoDistanceObj = esHttp.util.findObjectInTree(request.query, 'geo_distance');
var geoPoint = geoDistanceObj && geoDistanceObj.geoPoint;
var geoDistanceUnit = geoDistanceObj && geoDistanceObj.distance && geoDistanceObj.distance.replace(new RegExp("[0-9 ]+", "gm"), '');
var hits = res.hits.hits.reduce(function(result, hit) {
var record = readRecordFromHit(hit, categories, currentUD, {convertPrice: true, html: true});
record.id = hit._id;
// Add distance to point
if (geoPoint && record.geoPoint && geoDistanceUnit) {
record.distance = esGeo.point.distance(
geoPoint.lat, geoPoint.lon,
record.geoPoint.lat, record.geoPoint.lon,
geoDistanceUnit
);
}
return result.concat(record);
}, []);
return {
total: res.hits.total,
hits: hits
};
});
}
function loadData(id, options) {
options = options || {};
options.fetchPictures = angular.isDefined(options.fetchPictures) ? options.fetchPictures : true;
options.convertPrice = angular.isDefined(options.convertPrice) ? options.convertPrice : false;
return $q.all([
// load categories
exports.category.all(),
// Get current UD
csCurrency.currentUD()
.catch(function(err) {
console.error('Could not get current UD', err);
return 1;
}),
// Do get source
options.fetchPictures ?
exports._internal.get({id: id}) :
exports._internal.getCommons({id: id})
])
.then(function(res) {
var categories = res[0];
var currentUD = res[1];
var hit = res[2];
var record = readRecordFromHit(hit, categories, currentUD, options);
// Load issuer (avatar, name, uid, etc.)
return csWot.extend({pubkey: record.issuer})
.then(function(issuer) {
var data = {
id: hit._id,
issuer: issuer,
record: record
};
// Make sure currency if present (fix old data)
if (record.price && !record.currency) {
return mkSettings.currencies()
.then(function(currencies) {
record.currency = currencies && currencies[0];
return data;
});
}
return data;
});
});
}
function setStockToRecord(id, stock) {
return exports._internal.get({id: id})
.then(function(res) {
if (!res || !res._source) return;
var record = res._source;
record.stock = stock||0;
record.id = id;
return exports.record.update(record, {id: id});
});
}
function searchPictures(options) {
options = options || {};
var request = {
from: options.from||0,
size: options.size||20,
_source: options._source || ["category", "title", "price", "unit", "currency", "city", "pictures", "stock", "unitbase", "description", "type"]
};
var matches = [];
var filters = [];
if (options.category) {
filters.push({
nested: {
path: "category",
query: {
bool: {
filter: {
term: { "category.id": options.category}
}
}
}
}
});
}
if (options.categories) {
filters.push({
nested: {
path: "category",
query: {
bool: {
filter: {
terms: { "category.id": options.categories}
}
}
}
}
});
}
if (options.withStock) {
filters.push({range: {stock: {gt: 0}}});
}
if (!options.withOld) {
var minTime = options.minTime ? options.minTime : Date.now() / 1000 - 24 * 365 * 60 * 60; // last year
// Round to hour, to be able to use cache
minTime = Math.floor(minTime / 60 / 60 ) * 60 * 60;
filters.push({range: {time: {gte: minTime}}});
}
if (options.currencies && options.currencies.length) {
filters.push({terms: {currency: options.currencies}});
}
// Add query to request
if (matches.length || filters.length) {
request.query = {bool: {}};
if (matches.length) {
request.query.bool.should = matches;
}
if (filters.length) {
request.query.bool.filter = filters;
}
}
return exports.record.search(request)
.then(function(res) {
// Filter, to keep only record with pictures
return (res.hits || []).reduce(function(res, record) {
if (!record.pictures || !record.pictures.length) return res;
// Replace thumbnail with the first picture (full quality)
angular.merge(record, record.pictures[0]);
delete record.pictures;
delete record.thumbnail;
return res.concat(record);
}, []);
});
}
function searchMoreLikeThis(id, options) {
options = options || {};
var size = options.size||6;
var request = {
from: options.from||0,
size: size * 2,
_source: options._source || fields.commons,
query: {
more_like_this : {
fields : ["title", "category.name", "type", "city"],
like : [
{
"_index" : "market",
"_type" : "record",
"_id" : id
}
],
"min_term_freq" : 1,
"max_query_terms" : 12
}
}
};
if (options.type || options.category || options.city) {
var doc = {};
if (options.type) doc.type = options.type;
if (options.category) doc.category = {id: options.category};
if (options.city) doc.city = options.city;
request.query.more_like_this.like.push(
{
"_index" : "market",
"_type" : "record",
"doc" : doc
});
}
var minTime = (Date.now() / 1000) - 60 * 60 * 24 * 365; // last year
var oldHits = [];
var processHits = function(categories, currentUD, size, res) {
if (!res || !res.hits || !res.hits.total) {
return {
total: 0,
hits: []
};
}
var hits = res.hits.hits.reduce(function(res, hit, index) {
if (index >= size) return res; // Skip (already has enought ad)
var record = readRecordFromHit(hit, categories, currentUD, {convertPrice: true, html: true});
record.id = hit._id;
// Exclude if closed
if (record.stock === 0) return res;
// Exclude if too old
if ((record.time || record.creationTime) < minTime) {
oldHits.push(record);
return res;
}
return res.concat(record);
}, []);
if (hits.length < size) {
var missingSize = size - hits.length;
if (request.from < res.hits.total) {
request.from += size;
request.size = missingSize;
return exports._internal.search(request)
.then(function (more) {
return processHits(categories, currentUD, missingSize, more);
})
.then(function (more) {
return {
total: res.hits.total,
hits: hits.concat(more.hits || [])
};
});
}
else if (oldHits.length > 0){
if (oldHits.length > missingSize) {
oldHits.splice(missingSize);
}
hits = hits.concat(oldHits);
}
}
return {
total: res.hits.total,
hits: hits
};
};
return $q.all([
// load categories
exports.category.all(),
// Get current UD
csCurrency.currentUD()
.catch(function(err) {
console.error('Could not get current UD', err);
return 1;
}),
// Search request
exports._internal.search(request)
])
.then(function(res) {
var categories = res[0];
var currentUD = res[1];
res = res[2];
return processHits(categories, currentUD, size, res);
})
.then(function(res) {
return csWot.extendAll(res.hits, 'issuer')
.then(function(_) {
return res;
});
});
}
exports.category = {
all: getCategories,
filtered: getFilteredCategories,
get: getCategory,
searchText: esHttp.get('/market/category/_search?q=:search'),
search: esHttp.post('/market/category/_search'),
stats: getCategoriesStats
};
exports.record = {
search: search,
load: loadData,
setStock: setStockToRecord,
pictures: searchPictures,
add: esHttp.record.post('/market/record'),
update: esHttp.record.post('/market/record/:id/_update'),
remove: esHttp.record.remove('market', 'record'),
moreLikeThis : searchMoreLikeThis,
fields: {
commons: fields.commons
},
picture: {
all: esHttp.get('/market/record/:id?_source=pictures')
},
comment: esComment.instance('market'),
like: {
add: esHttp.like.add('market', 'record'),
remove: esHttp.like.remove('market', 'record'),
toggle: esHttp.like.toggle('market', 'record'),
count: esHttp.like.count('market', 'record')
}
};
return exports;
}
return MkRecord();
}])
;
angular.module('cesium.market.wallet.services', ['cesium.es.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.market;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('mkWallet');
}
}])
.factory('mkWallet', ['$rootScope', '$q', '$timeout', 'esHttp', '$state', '$sce', '$sanitize', '$translate', 'UIUtils', 'csSettings', 'csWallet', 'csWot', 'BMA', 'Device', 'SocialUtils', 'CryptoUtils', 'esWallet', 'esProfile', 'esSubscription', function($rootScope, $q, $timeout, esHttp, $state, $sce, $sanitize, $translate,
UIUtils, csSettings, csWallet, csWot, BMA, Device,
SocialUtils, CryptoUtils, esWallet, esProfile, esSubscription) {
'ngInject';
var
defaultProfile,
defaultSubscription,
that = this,
listeners;
function onWalletReset(data) {
data.profile = undefined;
data.name = undefined;
defaultProfile = undefined;
defaultSubscription = undefined;
}
function onWalletLoginCheck(data, deferred) {
deferred = deferred || $q.defer();
if (!data || !data.pubkey || !data.keypair) {
deferred.resolve();
return deferred.promise;
}
// Default user name
if (data.name) {
deferred.resolve(data);
return deferred.promise;
}
var now = Date.now();
console.debug("[market] [wallet] Checking user profile...");
// Check if profile exists
esProfile.get(data.pubkey)
.then(function(profile) {
// Profile exists: use it !
if (profile) {
data.name = profile.name;
data.avatar = profile.avatar;
data.profile = profile.source;
data.profile.description = profile.description;
return; // Continue
}
// Invalid credentials (no user profile found)
// AND no default profile to create a new one
if (!defaultProfile) {
UIUtils.alert.error('MARKET.ERROR.INVALID_LOGIN_CREDENTIALS');
deferred.reject('RETRY');
return deferred.promise;
}
// Save the new user profile
return registerNewProfile(data);
})
.then(function() {
return registerNewSubscription(data);
})
.then(function() {
console.info('[market] [wallet] Checked user profile in {0}ms'.format(Date.now() - now));
deferred.resolve(data);
})
.catch(function(err) {
deferred.reject(err);
});
return deferred.promise;
}
function registerNewProfile(data) {
if (!defaultProfile) return;
var now = Date.now();
console.debug("[market] [wallet] Saving user profile...");
// Profile not exists, but it exists a default profile (from the join controller)
data.profile = data.profile || {};
angular.merge(data.profile, defaultProfile);
return esWallet.box.getKeypair()
.then(function(keypair) {
return $q.all([
$translate('MARKET.PROFILE.DEFAULT_TITLE', {pubkey: data.pubkey}),
// Encrypt socials
SocialUtils.pack(angular.copy(data.profile.socials||[]), keypair)
])
})
.then(function(res) {
var title = res[0];
var encryptedSocials = res[1];
data.name = data.profile.title || title;
data.profile.title = data.name;
data.profile.issuer = data.pubkey;
// Add encrypted socials into a profile copy, then save it
var copiedProfile = angular.copy(data.profile);
copiedProfile.socials = encryptedSocials;
// Save the profile
return esProfile.add(copiedProfile)
})
.then(function() {
// clean default profile
defaultProfile = undefined;
console.info('[market] [wallet] Saving user profile in {0}ms'.format(Date.now() - now));
})
.catch(function(err) {
// clean default profile
defaultProfile = undefined;
console.error('[market] [wallet] Error while saving new profile', err);
throw err;
});
}
function registerNewSubscription(data) {
if (!defaultSubscription) return;
// Find the ES node pubkey (from its peer document)
return esHttp.network.peering.self()
.then(function(res) {
if (!res || !res.pubkey) return; // no pubkey: exit
var record = angular.merge({
type: 'email',
recipient: res.pubkey,
content: {
locale: csSettings.data.locale.id,
frequency: 'daily'
}
}, defaultSubscription);
if (record.type === 'email' && !record.content.email) {
console.warn("Missing email attribute (subscription content). Cannot subscribe!");
return;
}
return esSubscription.record.add(record, csWallet)
})
.then(function() {
data.subscriptions = data.subscriptions || {count: 0};
data.subscriptions.count++;
defaultSubscription = undefined;
})
.catch(function(err) {
defaultSubscription = undefined;
console.error('[market] [wallet] Error while saving new subscription', err);
throw err;
});
}
function onWalletFinishLoad(data, deferred) {
deferred = deferred || $q.defer();
// TODO: Load record count
//console.debug('[market] [user] Loading user record count...');
// var now = new Date().getTime();
deferred.resolve();
return deferred.promise;
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
// Extend csWallet and csWot events
listeners = [
csWallet.api.data.on.loginCheck($rootScope, onWalletLoginCheck, this),
csWallet.api.data.on.finishLoad($rootScope, onWalletFinishLoad, this),
csWallet.api.data.on.init($rootScope, onWalletReset, this),
csWallet.api.data.on.reset($rootScope, onWalletReset, this)
];
}
function refreshState() {
var enable = esHttp.alive;
if (!enable && listeners && listeners.length > 0) {
console.debug("[ES] [user] Disable");
removeListeners();
if (csWallet.isLogin()) {
return onWalletReset(csWallet.data);
}
}
else if (enable && (!listeners || listeners.length === 0)) {
console.debug("[ES] [user] Enable");
addListeners();
if (csWallet.isLogin()) {
return onWalletLoginCheck(csWallet.data);
}
}
}
// Default actions
Device.ready().then(function() {
esHttp.api.node.on.start($rootScope, refreshState, this);
esHttp.api.node.on.stop($rootScope, refreshState, this);
return refreshState();
});
that.setDefaultProfile = function(profile) {
defaultProfile = angular.copy(profile);
};
that.setDefaultSubscription = function(subscription) {
defaultSubscription = angular.copy(subscription);
};
return that;
}])
;
angular.module('cesium.market.settings.services', ['cesium.services', 'cesium.es.http.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('mkSettings');
}
}])
.factory('mkSettings', ['$rootScope', '$q', '$timeout', '$ionicHistory', 'Api', 'esHttp', 'csConfig', 'csSettings', 'esSettings', 'csCurrency', function($rootScope, $q, $timeout, $ionicHistory, Api, esHttp,
csConfig, csSettings, esSettings, csCurrency) {
'ngInject';
var
SETTINGS_SAVE_SPEC = {
includes: ['geoDistance'],
excludes: ['enable', 'homeMessage', 'defaultTags', 'defaultAdminPubkeys', 'record'],
cesiumApi: {}
},
defaultSettings = angular.merge({
plugins: {
market: {
enable: true,
geoDistance: "100km",
cesiumApi: {
enable: true,
baseUrl: "https://g1.duniter.fr/api"
}
},
converse: {
jid : "anonymous.duniter.org",
bosh_service_url: "https://chat.duniter.org/http-bind/",
auto_join_rooms : [
"gchange@muc.duniter.org"
]
}
}
},
// Market plugin
{plugins: {market: csConfig.plugins && csConfig.plugins.market || {}}},
// Converse plugin
{plugins: {converse: csConfig.plugins && csConfig.plugins.converse || {}}}
),
that = this,
readyDeferred = $q.defer(),
listeners,
ignoreSettingsChanged = false
;
// Define settings to save remotely
esSettings.setPluginSaveSpecs('market', SETTINGS_SAVE_SPEC);
that.raw = {
currencies: undefined
};
that.isEnable = function(data) {
data = data || csSettings.data;
return data.plugins && data.plugins.es && data.plugins.es.enable &&
data.plugins.market && data.plugins.market.enable;
};
that.currencies = function() {
if (readyDeferred) {
return readyDeferred.promise.then(function() {
return that.raw.currencies;
});
}
return $q.when(that.raw.currencies);
};
function _initCurrencies(data, deferred) {
deferred = deferred || $q.defer();
if (that.enable) {
that.raw.currencies = data.plugins.market.currencies;
if (!that.raw.currencies && data.plugins.market.defaultCurrency) {
that.raw.currencies = [data.plugins.market.defaultCurrency];
console.debug('[market] [settings] Currencies: ', that.raw.currencies);
if (deferred) deferred.resolve(that.raw.currencies);
}
else {
return csCurrency.get()
.then(function(currency) {
that.raw.currencies = [currency.name];
console.debug('[market] [settings] Currencies: ', that.raw.currencies);
if (deferred) deferred.resolve(that.raw.currencies);
})
.catch(function(err) {
if (deferred) {
deferred.reject(err);
}
else {
throw err;
}
});
}
}
else {
that.raw.currencies = [];
if (deferred) deferred.resolve(that.raw.currencies);
}
return deferred.promise;
}
function _compareVersion(version1, version2) {
var parts = version1 && version1.split('.');
var version1 = parts && parts.length == 3 ? {
major: parseInt(parts[0]),
minor: parseInt(parts[1]),
build: parseInt(parts[2])
}: {};
parts = version2 && version2.split('.');
var version2 = parts && parts.length == 3 ? {
major: parseInt(parts[0]),
minor: parseInt(parts[1]),
build: parseInt(parts[2])
} : {};
// check major
if (version1.major != version2.major) {
return version1.major < version2.major ? -1 : 1;
}
// check minor
if (version1.minor != version2.minor) {
return version1.minor < version2.minor ? -1 : 1;
}
// check build
if (version1.build != version2.build) {
return version1.build < version2.build ? -1 : 1;
}
return 0; // equals
}
function onSettingsReset(data, deferred) {
deferred = deferred || $q.defer();
data.plugins = data.plugins || {};
// reset plugin settings, then restore defaults
data.plugins.market = {};
angular.merge(data, defaultSettings);
deferred.resolve(data);
return deferred.promise;
}
// Listen for settings changed
function onSettingsChanged(data) {
// Workaround (version < 0.5.0) : remove older settings
var isVersionPrevious_0_5_0 = _compareVersion(data.version, '0.5.0') <= 0;
if (isVersionPrevious_0_5_0 && data.plugins && data.plugins.market) {
console.info('[market] [settings] Detected version previous <= 0.5.0 - restoring default settings...');
delete data.login;
data.plugins.market = angular.copy(defaultSettings.plugins.market);
}
data.plugins.es.document = data.plugins.es.document || {};
data.plugins.es.document.index = 'user,page,group,market';
data.plugins.es.document.type = 'profile,record,comment';
// Init currencies
_initCurrencies(data);
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
// Listening some events
listeners = [
csSettings.api.data.on.reset($rootScope, onSettingsReset, this),
csSettings.api.data.on.changed($rootScope, onSettingsChanged, this)
];
}
that.ready = function() {
if (!readyDeferred) return $q.when();
return readyDeferred.promise;
};
esSettings.api.state.on.changed($rootScope, function(enable) {
enable = enable && that.isEnable();
if (enable === that.enable) return; // nothing changed
that.enable = enable;
if (enable) {
console.debug('[market] [settings] Enable');
addListeners();
}
else {
console.debug('[market] [settings] Disable');
removeListeners();
}
// Init currencies
onSettingsChanged(csSettings.data);
if (readyDeferred) {
readyDeferred.resolve();
readyDeferred = null;
}
});
return that;
}]);
angular.module('cesium.market.converse.services', ['cesium.es.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.market;
if (enable) {
// Will force to load this service
PluginServiceProvider.registerEagerLoadingService('mkConverse');
}
}])
.factory('mkConverse', ['$rootScope', '$q', '$timeout', '$translate', 'esHttp', 'UIUtils', 'csConfig', 'csWallet', 'Device', 'csSettings', function($rootScope, $q, $timeout, $translate, esHttp, UIUtils, csConfig, csWallet, Device, csSettings) {
'ngInject';
var
defaultProfile,
that = this,
listeners,
initialized = false;
function onWalletReset(data) {
data.xmpp = null;
defaultProfile = undefined;
}
function textToNickname(text) {
return text ? String(text).replace(/[^a-zA-Z0-9]+/gm, '') : '';
}
function onWalletLogin(data, deferred) {
if (!data.name) {
// Wait for profile load
$timeout(function() {
return onWalletLogin(data); // recursive loop
}, 1000);
}
else {
if (!initialized) {
initialized = true;
var isEnable = csConfig.plugins && csConfig.plugins.converse && csConfig.plugins.converse.enable;
if (!isEnable) {
console.debug("[market] [converse] Disabled by config (property 'plugins.converse.enable')");
initialized = true;
}
else if (UIUtils.screen.isSmall()) {
console.debug("[market] [converse] Disabled on small screen");
initialized = true;
}
else {
var nickname = data.name ? textToNickname(data.name) : data.pubkey.substring(0, 8);
var now = new Date().getTime();
console.debug("[market] [converse] Starting Chatroom with username {" + nickname + "}...");
// Register plugin
converse.plugins.add('gchange-plugin', {
initialize: function () {
var _converse = this._converse;
$q.all([
_converse.api.waitUntil('chatBoxesFetched'),
_converse.api.waitUntil('roomsPanelRendered')
]).then(function () {
console.debug("[market] [converse] Chatroom started in " + (new Date().getTime() - now) + "ms");
});
}
});
var options = angular.merge({
"allow_muc_invitations": false,
"auto_login": true,
"allow_logout": true,
"authentication": "anonymous",
"jid": "anonymous.duniter.org",
"auto_away": 300,
"auto_join_on_invite": true,
"auto_reconnect": true,
"minimized": true,
"auto_join_rooms": [
"gchange@muc.duniter.org"
],
"blacklisted_plugins": [
"converse-mam",
"converse-otr",
"converse-register",
"converse-vcard"
],
"whitelisted_plugins": [
"gchange-plugin"
],
"bosh_service_url": "https://chat.duniter.org/http-bind/",
"allow_registration": false,
"show_send_button": false,
"muc_show_join_leave": false,
"notification_icon": "img/logo.png",
"i18n": $translate.use()
}, csSettings.data.plugins && csSettings.data.plugins.converse || {});
options.auto_join_rooms = _.map(options.auto_join_rooms || [], function (room) {
if (typeof room === "string") {
return {
jid: room,
nick: nickname
}
}
room.nick = nickname;
// Minimized by default
room.minimized = true;
return room;
});
// Run initialization
converse.initialize(options)
.catch(console.error);
}
}
// Already init
else {
// TODO:: close previous dialog and reconnect with the username
}
}
return deferred ? deferred.resolve() && deferred.promise : $q.when();
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function addListeners() {
// Extend csWallet events
listeners = [
csWallet.api.data.on.login($rootScope, onWalletLogin, this),
csWallet.api.data.on.init($rootScope, onWalletReset, this),
csWallet.api.data.on.reset($rootScope, onWalletReset, this)
];
}
function refreshState() {
var enable = esHttp.alive;
if (!enable && listeners && listeners.length > 0) {
console.debug("[market] [converse] Disable");
removeListeners();
if (csWallet.isLogin()) {
return onWalletReset(csWallet.data);
}
}
else if (enable && (!listeners || listeners.length === 0)) {
console.debug("[market] [converse] Enable");
addListeners();
if (csWallet.isLogin()) {
return onWalletLogin(csWallet.data);
}
}
}
// Default actions
Device.ready().then(function() {
esHttp.api.node.on.start($rootScope, refreshState, this);
esHttp.api.node.on.stop($rootScope, refreshState, this);
return refreshState();
});
that.setDefaultProfile = function(profile) {
defaultProfile = angular.copy(profile);
};
return that;
}])
;
MarketMenuExtendController.$inject = ['$scope', 'esSettings', 'PluginService'];
MarketHomeExtendController.$inject = ['$scope', '$rootScope', '$state', '$controller', '$focus', '$timeout', '$translate', 'ModalUtils', 'UIUtils', 'csConfig', 'esSettings', 'mkModals'];angular.module('cesium.market.app.controllers', ['ngResource', 'cesium.es.services', 'cesium.market.modal.services'])
// Configure menu items
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// Menu extension points
PluginServiceProvider.extendState('app', {
points: {
'menu-main': {
templateUrl: "plugins/market/templates/menu_extend.html",
controller: "MarketMenuExtendCtrl"
},
'menu-user': {
templateUrl: "plugins/market/templates/menu_extend.html",
controller: "MarketMenuExtendCtrl"
}
}
});
// Home extension points
PluginServiceProvider.extendState('app.home', {
points: {
'buttons': {
templateUrl: "plugins/market/templates/home/home_extend.html",
controller: "MarketHomeExtendCtrl"
}
}
});
}
}])
.controller('MarketMenuExtendCtrl', MarketMenuExtendController)
.controller('MarketHomeExtendCtrl', MarketHomeExtendController)
;
/**
* Control menu extension
*/
function MarketMenuExtendController($scope, esSettings, PluginService) {
'ngInject';
$scope.extensionPoint = PluginService.extensions.points.current.get();
$scope.enable = esSettings.isEnable();
esSettings.api.state.on.changed($scope, function(enable) {
$scope.enable = enable;
});
}
/**
* Control home extension
*/
function MarketHomeExtendController($scope, $rootScope, $state, $controller, $focus, $timeout, $translate,
ModalUtils, UIUtils, csConfig, esSettings, mkModals) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESLookupPositionCtrl', {$scope: $scope}));
$scope.enable = esSettings.isEnable();
$scope.search = {
location: undefined
};
// Screen options
$scope.options = $scope.options || angular.merge({
type: {
show: true
},
location: {
show: true,
prefix : undefined
}
}, csConfig.plugins && csConfig.plugins.market && csConfig.plugins.market.record || {});
esSettings.api.state.on.changed($scope, function(enable) {
$scope.enable = enable;
});
$scope.onGeoPointChanged = function() {
if ($scope.search.loading) return;
if ($scope.search.geoPoint && $scope.search.geoPoint.lat && $scope.search.geoPoint.lon && !$scope.search.geoPoint.exact) {
$scope.doSearch();
}
};
$scope.$watch('search.geoPoint', $scope.onGeoPointChanged, true);
$scope.resolveLocationPosition = function() {
if ($scope.search.loadingPosition) return;
$scope.search.loadingPosition = true;
return $scope.searchPosition($scope.search.location)
.then(function(res) {
if (!res) {
$scope.search.loadingPosition = false;
$scope.search.geoPoint = undefined;
throw 'CANCELLED';
}
$scope.search.geoPoint = res;
if (res.shortName && !res.exact) {
$scope.search.location = res.shortName;
}
$scope.search.loadingPosition = false;
});
};
$scope.doSearch = function(locationName) {
// Resolve location position
if (!$scope.search.geoPoint) {
return $scope.searchPosition($scope.search.location)
.then(function(res) {
if (res) {
$scope.search.geoPoint = res;
// No location = Around me
if (!$scope.search.location) {
$scope.search.geoPoint.exact= true;
return $translate("MARKET.COMMON.AROUND_ME")
.then(function(locationName) {
return $scope.doSearch(locationName); // Loop
})
}
return $scope.doSearch(); // Loop
}
})
.catch(function(err) {
console.error(err);
return $state.go('app.market_lookup');
});
}
var locationShortName = locationName || $scope.search.location && $scope.search.location.split(', ')[0];
if (locationShortName && $scope.search.geoPoint) {
$rootScope.geoPoints = $rootScope.geoPoints || {};
$rootScope.geoPoints[locationShortName] = $scope.search.geoPoint;
var stateParams = {
lat: $scope.search.geoPoint && $scope.search.geoPoint.lat,
lon: $scope.search.geoPoint && $scope.search.geoPoint.lon,
location: locationShortName
};
return UIUtils.screen.isSmall() ?
$state.go('app.market_lookup', stateParams) :
$state.go('app.market_lookup_lg', stateParams);
}
else {
$scope.search.geoPoint = undefined;
}
};
$scope.showNewRecordModal = function() {
return $scope.loadWallet({minData: true})
.then(function() {
return UIUtils.loading.hide();
}).then(function() {
if (!$scope.options.type.show && $scope.options.type.default) {
return $scope.options.type.default;
}
return ModalUtils.show('plugins/market/templates/record/modal_record_type.html');
})
.then(function(type){
if (type) {
$state.go('app.market_add_record', {type: type});
}
});
};
// Override default join and help modal (in the parent scope)
$scope.$parent.showJoinModal = mkModals.showJoin;
$scope.$parent.showLoginModal = mkModals.showLogin;
$scope.$parent.showHelpModal = mkModals.showHelp;
// removeIf(device)
// Focus on search text (only if NOT device, to avoid keyboard opening)
if (!UIUtils.screen.isSmall()) {
$timeout(function() {
$focus('searchLocationInput');
}, 500);
}
// endRemoveIf(device)
}
angular.module('cesium.market.join.controllers', ['cesium.services', 'cesium.market.services'])
.controller('MkJoinModalCtrl', ['$scope', '$timeout', '$state', 'UIUtils', 'CryptoUtils', 'csSettings', 'csWallet', 'csCurrency', 'mkWallet', 'mkModals', function ($scope, $timeout, $state, UIUtils, CryptoUtils, csSettings, csWallet, csCurrency, mkWallet, mkModals) {
'ngInject';
var EMAIL_REGEX = '^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$';
$scope.emailPattern = EMAIL_REGEX;
$scope.formData = {
pseudo: '',
description: undefined,
email: undefined
};
$scope.slides = {
slider: null,
options: {
loop: false,
effect: 'slide',
speed: 500
}
};
$scope.isLastSlide = false;
$scope.showUsername = false;
$scope.showPassword = false;
$scope.smallscreen = UIUtils.screen.isSmall();
$scope.enter = function() {
csCurrency.get().then(function(currency) {
$scope.currency = currency;
})
};
$scope.$on('modal.shown', $scope.enter);
$scope.slidePrev = function() {
$scope.slides.slider.unlockSwipes();
$scope.slides.slider.slidePrev();
$scope.slides.slider.lockSwipes();
$scope.isLastSlide = false;
};
$scope.slideNext = function() {
$scope.slides.slider.unlockSwipes();
$scope.slides.slider.slideNext();
$scope.slides.slider.lockSwipes();
$scope.isLastSlide = $scope.slides.slider.activeIndex === ($scope.slides.slider.slides.length-1);
};
$scope.showAccountPubkey = function() {
if ($scope.formData.pubkey) return; // not changed
$scope.formData.computing=true;
CryptoUtils.scryptKeypair($scope.formData.username, $scope.formData.password)
.then(function(keypair) {
$scope.formData.pubkey = CryptoUtils.util.encode_base58(keypair.signPk);
$scope.formData.computing=false;
})
.catch(function(err) {
$scope.formData.computing=false;
console.error('>>>>>>>' , err);
UIUtils.alert.error('ERROR.CRYPTO_UNKNOWN_ERROR');
});
};
$scope.formDataChanged = function() {
$scope.formData.computing=false;
$scope.formData.pubkey=null;
};
$scope.doNext = function(formName) {
if (!formName) {
switch($scope.slides.slider.activeIndex) {
case 0:
formName = 'saltForm';
break;
case 1:
formName = 'passwordForm';
break;
case 2:
formName = 'profileForm';
break;
}
}
if (formName) {
$scope[formName].$submitted=true;
if(!$scope[formName].$valid) {
return;
}
if (formName === 'passwordForm' || formName === 'pseudoForm') {
$scope.slideNext();
$scope.showAccountPubkey();
}
else {
$scope.slideNext();
}
}
};
$scope.doNewAccount = function(confirm) {
if (!confirm) {
return UIUtils.alert.confirm('MARKET.JOIN.CONFIRMATION_WALLET_ACCOUNT')
.then(function(confirm) {
if (confirm) {
$scope.doNewAccount(true);
}
});
}
UIUtils.loading.show();
// Fill a default profile
mkWallet.setDefaultProfile({
title: $scope.formData.title,
description: $scope.formData.description
});
// Fill the default subscription
if ($scope.formData.email) {
mkWallet.setDefaultSubscription({
type: 'email',
//recipient: '', TODO: allow to select a email provider
content: {
email: $scope.formData.email
}
});
}
// do not alert use if wallet is empty
csSettings.data.wallet = csSettings.data.wallet || {};
csSettings.data.wallet.alertIfUnusedWallet = false;
// Apply login (will call profile creation)
return csWallet.login($scope.formData.username, $scope.formData.password)
.catch(UIUtils.onError('ERROR.CRYPTO_UNKNOWN_ERROR'))
// Close the join current
.then($scope.closeModal)
// Redirect to wallet
.then(function() {
return $state.go('app.view_wallet');
});
};
$scope.showHelpModal = function(helpAnchor) {
if (!helpAnchor) {
helpAnchor = $scope.slides.slider.activeIndex == 1 ?
'join-salt' : ( $scope.slides.slider.activeIndex == 2 ?
'join-password' : undefined);
}
return mkModals.showHelp({anchor: helpAnchor});
};
// DEV only: remove auto add account when done
/*$timeout(function() {
$scope.formData.username="azertypoi";
$scope.formData.confirmUsername=$scope.formData.username;
$scope.formData.password="azertypoi";
$scope.formData.confirmPassword=$scope.formData.password;
$scope.formData.pseudo="azertypoi";
$scope.doNext();
$scope.doNext();
//$scope.form = {$valid:true};
}, 1000);
*/
}]);
MarketEventLoginModalController.$inject = ['$scope', '$controller', '$q', 'csConfig', 'csWallet', 'mkWallet'];
MarketLoginModalController.$inject = ['$scope', '$controller'];
angular.module('cesium.market.login.controllers', ['cesium.services'])
.controller('MarketEventLoginModalCtrl', MarketEventLoginModalController)
.controller('MarketLoginModalCtrl', MarketLoginModalController)
;
function MarketLoginModalController($scope, $controller) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('LoginModalCtrl', {$scope: $scope}));
}
// use this for local sale event
function MarketEventLoginModalController($scope, $controller, $q, csConfig, csWallet, mkWallet) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('LoginModalCtrl', {$scope: $scope}));
var EMAIL_REGEX = '^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$';
var PHONE_NUMBER_REGEX = '^[0-9]{9,10}$';
$scope.usernamePattern = EMAIL_REGEX + '|' + PHONE_NUMBER_REGEX;
$scope.onWalletLogin = function(data, deferred) {
deferred = deferred || $q.defer();
// Give the username to mkUser service,
// to store it inside the user profile
var adminPubkeys = csConfig.plugins && csConfig.plugins.market && csConfig.plugins.market.defaultAdminPubkeys;
if (adminPubkeys && adminPubkeys.length) {
console.error("[market] [login] Storing username into user profile socials");
var isEmail = new RegExp(EMAIL_REGEX).test($scope.formData.username);
// Add username into socials (with encryption - only admin pubkeys we be able to read it)
var social = {
url: $scope.formData.username,
type: isEmail ? 'email' : 'phone'
};
// Add social for the user itself
var socials = [angular.merge({recipient: data.pubkey}, social)];
// Add social for admins
var socials = (adminPubkeys||[]).reduce(function(res, pubkey) {
return res.concat(angular.merge({recipient: pubkey}, social)) ;
}, socials);
// Fill a default profile
mkWallet.setDefaultProfile({
socials: socials
});
}
deferred.resolve(data);
return deferred.promise;
};
csWallet.api.data.on.login($scope, $scope.onWalletLogin, this);
}
MkLookupAbstractController.$inject = ['$scope', '$state', '$filter', '$q', '$location', '$translate', '$controller', '$timeout', 'UIUtils', 'esHttp', 'ModalUtils', 'csConfig', 'csSettings', 'mkRecord', 'BMA', 'mkSettings', 'esProfile'];
MkLookupController.$inject = ['$scope', '$rootScope', '$controller', '$focus', '$timeout', '$ionicPopover', 'mkRecord', 'csSettings'];
MkViewGalleryController.$inject = ['$scope', 'csConfig', '$q', '$ionicScrollDelegate', '$ionicSlideBoxDelegate', '$ionicModal', '$interval', 'mkRecord'];angular.module('cesium.market.search.controllers', ['cesium.market.record.services', 'cesium.es.services', 'cesium.es.common.controllers'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.market_lookup', {
url: "/market?q&category&location&reload&type&hash&lat&lon&last&old",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/search/lookup.html",
controller: 'MkLookupCtrl'
}
},
data: {
large: 'app.market_lookup_lg',
silentLocationChange: true
}
})
.state('app.market_lookup_lg', {
url: "/market/lg?q&category&location&reload&type&hash&closed&lat&lon&last&old",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/search/lookup_lg.html",
controller: 'MkLookupCtrl'
}
},
data: {
silentLocationChange: true
}
})
.state('app.market_gallery', {
cache: true,
url: "/gallery/market",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/gallery/view_gallery.html",
controller: 'MkViewGalleryCtrl'
}
}
});
}])
.controller('MkLookupAbstractCtrl', MkLookupAbstractController)
.controller('MkLookupCtrl', MkLookupController)
.controller('MkViewGalleryCtrl', MkViewGalleryController)
;
function MkLookupAbstractController($scope, $state, $filter, $q, $location, $translate, $controller, $timeout,
UIUtils, esHttp, ModalUtils, csConfig, csSettings, mkRecord, BMA, mkSettings, esProfile) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESLookupPositionCtrl', {$scope: $scope}));
var defaultSearchLimit = 10;
$scope.search = {
text: '',
type: null,
lastRecords: true,
results: [],
loading: true,
category: null,
location: null,
geoPoint: null,
options: null,
loadingMore: false,
showClosed: false,
showOld: false,
geoDistance: !isNaN(csSettings.data.plugins.es.geoDistance) ? csSettings.data.plugins.es.geoDistance : 20,
sortAttribute: null,
sortDirection: 'desc'
};
// Screen options
$scope.options = $scope.options || angular.merge({
type: {
show: true
},
category: {
show: true
},
description: {
show: true
},
location: {
show: true,
prefix : undefined
},
fees: {
show: true
}
}, csConfig.plugins && csConfig.plugins.market && csConfig.plugins.market.record || {});
$scope.$watch('search.showClosed', function() {
$scope.options.showClosed = $scope.search.showClosed;
}, true);
$scope.$watch('search.showOld', function() {
$scope.options.showOld = $scope.search.showOld;
}, true);
$scope.init = function() {
return $q.all([
// Init currency
mkSettings.currencies()
.then(function(currencies) {
$scope.currencies = currencies;
}),
// Resolve distance unit
$translate('LOCATION.DISTANCE_UNIT')
.then(function(unit) {
$scope.geoUnit = unit;
})
])
.then(function() {
$timeout(function() {
// Set Ink
UIUtils.ink({selector: '.item'});
//$scope.showHelpTip();
}, 200);
});
};
$scope.toggleAdType = function(type) {
if (type === $scope.search.type) {
$scope.search.type = undefined;
}
else {
$scope.search.type = type;
}
if ($scope.search.lastRecords) {
$scope.doGetLastRecords();
}
else {
$scope.doSearch();
}
};
$scope.doSearch = function(from) {
$scope.search.loading = !from;
$scope.search.lastRecords = false;
if (!$scope.search.advanced) {
$scope.search.advanced = false;
}
if ($scope.search.location && !$scope.search.geoPoint) {
return $scope.searchPosition($scope.search.location)
.then(function(res) {
if (!res) {
$scope.search.loading = false;
return UIUtils.alert.error('MARKET.ERROR.GEO_LOCATION_NOT_FOUND');
}
//console.debug('[market] search by location results:', res);
$scope.search.geoPoint = res;
$scope.search.location = res.name && res.name.split(',')[0] || $scope.search.location;
return $scope.doSearch(from);
});
}
var text = $scope.search.text.trim();
var matches = [];
var filters = [];
var tags = text ? esHttp.util.parseTags(text) : undefined;
if (text.length > 1) {
// pubkey : use a special 'term', because of 'non indexed' field
if (BMA.regexp.PUBKEY.test(text /*case sensitive*/)) {
matches = [];
filters.push({term : { issuer: text}});
}
else {
text = text.toLowerCase();
var matchFields = ["title", "description"];
matches.push({multi_match : { query: text,
fields: matchFields,
type: "phrase_prefix"
}});
matches.push({match: {title: {query: text, boost: 2}}});
matches.push({prefix: {title: text}});
matches.push({match: {description: text}});
matches.push({
nested: {
path: "category",
query: {
bool: {
filter: {
match: { "category.name": text}
}
}
}
}
});
}
}
if ($scope.search.category) {
filters.push({
nested: {
path: "category",
query: {
bool: {
filter: {
term: { "category.id": $scope.search.category.id}
}
}
}
}
});
}
if (tags) {
filters.push({terms: {tags: tags}});
}
if (!matches.length && !filters.length) {
return $scope.doGetLastRecords();
}
var stateParams = {};
var location = $scope.search.location && $scope.search.location.trim().toLowerCase();
if ($scope.search.geoPoint && $scope.search.geoPoint.lat && $scope.search.geoPoint.lon) {
// match location OR geo distance
if (location && location.length) {
var locationCity = location.split(',')[0];
filters.push({
or : [
// No position defined
{
and: [
{not: {exists: { field : "geoPoint" }}},
{multi_match: {
query: locationCity,
fields : [ "city^3", "location" ]
}}
]
},
// Has position
{geo_distance: {
distance: $scope.search.geoDistance + $scope.geoUnit,
geoPoint: {
lat: $scope.search.geoPoint.lat,
lon: $scope.search.geoPoint.lon
}
}}
]
});
stateParams.location = $scope.search.location.trim();
}
else {
filters.push(
{geo_distance: {
distance: $scope.search.geoDistance + $scope.geoUnit,
geoPoint: {
lat: $scope.search.geoPoint.lat,
lon: $scope.search.geoPoint.lon
}
}});
}
stateParams.lat=$scope.search.geoPoint.lat;
stateParams.lon=$scope.search.geoPoint.lon;
}
if ($scope.search.showClosed) {
stateParams.closed = true;
}
else {
filters.push({range: {stock: {gt: 0}}});
}
if ($scope.search.showOld) {
stateParams.old = true;
}
else {
var minTime = (Date.now() / 1000) - 60 * 60 * 24 * 365;
filters.push({range: {time: {gt: minTime}}});
}
if ($scope.search.type) {
filters.push({term: {type: $scope.search.type}});
stateParams.type = $scope.search.type;
}
// filter on currency
if ($scope.currencies) {
filters.push({terms: {currency: $scope.currencies}});
}
stateParams.q = $scope.search.text;
var query = {bool: {}};
if (matches.length > 0) {
query.bool.should = matches;
// Exclude result with score=0
query.bool.minimum_should_match = 1;
}
if (filters.length > 0) {
query.bool.filter = filters;
}
// Update location href
if (!from) {
$location.search(stateParams).replace();
}
var request = {query: query, from: from};
if ($scope.search.sortAttribute) {
request.sort = request.sort || {};
request.sort[$scope.search.sortAttribute] = $scope.search.sortDirection == "asc" ? "asc" : "desc";
}
return $scope.doRequest(request);
};
$scope.doGetLastRecords = function(from) {
$scope.hideActionsPopover();
$scope.search.lastRecords = true;
var options = {
sort: {
"creationTime" : "desc"
},
from: from
};
var filters = [];
var matches = [];
// Filter on NOT closed
if (!$scope.search.showClosed) {
filters.push({range: {stock: {gt: 0}}});
}
// Filter on NOT too old
if (!$scope.search.showOld) {
var minTime = (Date.now() / 1000) - 60 * 60 * 24 * 365;
filters.push({range: {time: {gt: minTime}}});
}
// filter on type
if ($scope.search.type) {
filters.push({term: {type: $scope.search.type}});
}
// filter on currencies
if ($scope.currencies) {
filters.push({terms: {currency: $scope.currencies}});
}
// Category
if ($scope.search.category) {
filters.push({
nested: {
path: "category",
query: {
bool: {
filter: {
term: { "category.id": $scope.search.category.id}
}
}
}
}
});
}
var location = $scope.search.location && $scope.search.location.trim().toLowerCase();
if ($scope.search.geoPoint && $scope.search.geoPoint.lat && $scope.search.geoPoint.lon) {
// match location OR geo distance
if (location && location.length) {
var locationCity = location.split(',')[0];
filters.push({
or : [
// No position defined
{
and: [
{not: {exists: { field : "geoPoint" }}},
{multi_match: {
query: locationCity,
fields : [ "city^3", "location" ]
}}
]
},
// Has position
{geo_distance: {
distance: $scope.search.geoDistance + $scope.geoUnit,
geoPoint: {
lat: $scope.search.geoPoint.lat,
lon: $scope.search.geoPoint.lon
}
}}
]
});
}
// match geo distance
else {
filters.push(
{geo_distance: {
distance: $scope.search.geoDistance + $scope.geoUnit,
geoPoint: {
lat: $scope.search.geoPoint.lat,
lon: $scope.search.geoPoint.lon
}
}});
}
}
if (matches.length) {
options.query = {bool: {}};
options.query.bool.should = matches;
options.query.bool.minimum_should_match = 1;
}
if (filters.length) {
options.query = options.query || {bool: {}};
options.query.bool.filter = filters;
}
// Update location href
if (!from) {
$location.search({
last: true,
category: $scope.search.category && $scope.search.category.id,
type: $scope.search.type,
location: $scope.search.location,
lat: $scope.search.geoPoint && $scope.search.geoPoint.lat,
lon: $scope.search.geoPoint && $scope.search.geoPoint.lon
}).replace();
}
return $scope.doRequest(options);
};
$scope.doRefresh = function() {
var searchFunction = ($scope.search.lastRecords) ?
$scope.doGetLastRecords :
$scope.doSearch;
return searchFunction();
};
$scope.showMore = function() {
var from = $scope.search.results ? $scope.search.results.length : 0;
$scope.search.loadingMore = true;
var searchFunction = ($scope.search.lastRecords) ?
$scope.doGetLastRecords :
$scope.doSearch;
return searchFunction(from)
.then(function() {
$scope.search.loadingMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
})
.catch(function(err) {
console.error(err);
$scope.search.loadingMore = false;
$scope.search.hasMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
$scope.doRequest = function(options) {
options = options || {};
options.from = options.from || 0;
options.size = options.size || defaultSearchLimit;
if (options.size < defaultSearchLimit) options.size = defaultSearchLimit;
$scope.search.loading = (options.from === 0);
return mkRecord.record.search(options)
.then(function(res) {
if (!res || !res.hits || !res.hits.length) {
$scope.search.results = (options.from > 0) ? $scope.search.results : [];
$scope.search.total = (options.from > 0) ? $scope.search.total : 0;
$scope.search.hasMore = false;
$scope.search.loading = false;
return;
}
// Filter on type (workaround if filter on term 'type' not working)
var formatSlug = $filter('formatSlug');
_.forEach(res.hits, function(record) {
// Compute title for url
record.urlTitle = formatSlug(record.title);
});
// Load avatar and name
return esProfile.fillAvatars(res.hits, 'issuer')
.then(function(hits) {
// Replace results, or concat if offset
if (!options.from) {
$scope.search.results = hits;
$scope.search.total = res.total;
}
else {
$scope.search.results = $scope.search.results.concat(hits);
}
$scope.search.hasMore = $scope.search.results.length < $scope.search.total;
});
})
.then(function() {
$scope.search.loading = false;
// motion
$scope.motion.show();
})
.catch(function(err) {
$scope.search.loading = false;
$scope.search.results = (options.from > 0) ? $scope.search.results : [];
$scope.search.total = (options.from > 0) ? $scope.search.total : 0;
$scope.search.hasMore = false;
UIUtils.onError('MARKET.ERROR.LOOKUP_RECORDS_FAILED')(err);
});
};
/* -- modals -- */
$scope.showCategoryModal = function() {
// load categories
return mkRecord.category.all()
.then(function(categories){
return ModalUtils.show('plugins/es/templates/common/modal_category.html', 'ESCategoryModalCtrl as ctrl',
{categories : categories},
{focusFirstInput: true}
);
})
.then(function(cat){
if (cat && cat.parent) {
$scope.search.category = cat;
$scope.doSearch();
}
});
};
$scope.showNewRecordModal = function() {
return $scope.loadWallet({minData: true})
.then(function() {
return UIUtils.loading.hide();
}).then(function() {
if (!$scope.options.type.show && $scope.options.type.default) {
return $scope.options.type.default;
}
return ModalUtils.show('plugins/market/templates/record/modal_record_type.html');
})
.then(function(type){
if (type) {
$state.go('app.market_add_record', {type: type});
}
});
};
$scope.showRecord = function(event, index) {
if (event.defaultPrevented) return;
var item = $scope.search.results[index];
if (item) {
$state.go('app.market_view_record', {
id: item.id,
title: item.title
});
}
};
}
function MkLookupController($scope, $rootScope, $controller, $focus, $timeout, $ionicPopover, mkRecord, csSettings) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('MkLookupAbstractCtrl', {$scope: $scope}));
$scope.enter = function(e, state) {
if (!$scope.entered || !$scope.search.results || $scope.search.results.length === 0) {
var showAdvanced = false;
if (state.stateParams) {
// Search by text
if (state.stateParams.q) { // Query parameter
$scope.search.text = state.stateParams.q;
$scope.search.lastRecords = false;
}
else if (state.stateParams.last){
$scope.search.lastRecords = true;
}
// Search on type
if (state.stateParams.type) {
$scope.search.type = state.stateParams.type;
}
// Search on location
if (state.stateParams.location) {
$scope.search.location = state.stateParams.location;
}
// Geo point
if (state.stateParams.lat && state.stateParams.lon) {
$scope.search.geoPoint = {
lat: parseFloat(state.stateParams.lat),
lon: parseFloat(state.stateParams.lon)
};
}
else if (state.stateParams.location) {
// Try to get geoPoint from root scope
$scope.search.geoPoint = $rootScope.geoPoints && $rootScope.geoPoints[state.stateParams.location] || null;
}
else {
var defaultSearch = csSettings.data.plugins.es.market && csSettings.data.plugins.es.market.defaultSearch;
// Apply defaults from settings
if (defaultSearch && defaultSearch.location) {
angular.merge($scope.search, defaultSearch);
}
}
// Search on hash tag
if (state.stateParams.hash) {
if ($scope.search.text) {
$scope.search.text = '#' + state.stateParams.hash + ' ' + $scope.search.text;
}
else {
$scope.search.text = '#' + state.stateParams.hash;
}
}
// Show closed ads
if (angular.isDefined(state.stateParams.closed)) {
$scope.search.showClosed = true;
showAdvanced = true;
}
// Show old ads
if (angular.isDefined(state.stateParams.old)) {
$scope.search.showOld = true;
showAdvanced = true;
}
}
// Search on category
if (state.stateParams && (state.stateParams.category || state.stateParams.cat)) {
mkRecord.category.get({id: state.stateParams.category || state.stateParams.cat})
.then(function (cat) {
$scope.search.category = cat;
return $scope.init();
})
.then(function() {
return $scope.finishEnter(showAdvanced);
});
}
else {
$scope.init()
.then(function() {
return $scope.finishEnter(showAdvanced);
});
}
}
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.finishEnter = function(isAdvanced) {
$scope.search.advanced = isAdvanced ? true : $scope.search.advanced; // keep null if first call
if (!$scope.search.lastRecords) {
$scope.doSearch()
.then(function() {
$scope.showFab('fab-add-market-record');
});
}
else { // By default : get last records
$scope.doGetLastRecords()
.then(function() {
$scope.showFab('fab-add-market-record');
});
}
// removeIf(device)
// Focus on search text (only if NOT device, to avoid keyboard opening)
$focus('marketSearchText');
// endRemoveIf(device)
$scope.entered = true;
};
// Store some search options as settings defaults
$scope.updateSettings = function() {
var dirty = false;
csSettings.data.plugins.es.market = csSettings.data.plugins.es.market || {};
csSettings.data.plugins.es.market.defaultSearch = csSettings.data.plugins.es.market.defaultSearch || {};
// Check if location changed
var location = $scope.search.location && $scope.search.location.trim();
var oldLocation = csSettings.data.plugins.es.market.defaultSearch.location;
if (!oldLocation || (oldLocation !== location)) {
csSettings.data.plugins.es.market.defaultSearch = {
location: location,
geoPoint: location && $scope.search.geoPoint ? angular.copy($scope.search.geoPoint) : undefined
};
dirty = true;
}
// Check if distance changed
var odlDistance = csSettings.data.plugins.es.geoDistance;
if (!odlDistance || odlDistance !== $scope.search.geoDistance) {
csSettings.data.plugins.es.geoDistance = $scope.search.geoDistance;
dirty = true;
}
// execute with a delay, for better UI perf
if (dirty) {
$timeout(function() {
csSettings.store();
});
}
};
// Store some search options as settings defaults
$scope.leave = function() {
$scope.updateSettings();
};
$scope.$on('$ionicView.leave', function() {
// WARN: do not set by reference
// because it can be overrided by sub controller
return $scope.leave();
});
/* -- manage events -- */
$scope.onGeoPointChanged = function() {
if ($scope.search.loading) return;
if ($scope.search.geoPoint && $scope.search.geoPoint.lat && $scope.search.geoPoint.lon && !$scope.search.geoPoint.exact) {
$scope.doSearch();
}
};
$scope.$watch('search.geoPoint', $scope.onGeoPointChanged, true);
$scope.onLocationChanged = function() {
if ($scope.search.loading) return;
if (!$scope.search.location) {
$scope.removeLocation();
}
};
$scope.$watch('search.location', $scope.onLocationChanged, true);
$scope.onToggleShowClosedAdChanged = function(value) {
if ($scope.search.loading || !$scope.entered) return;
// Refresh results
$scope.doRefresh();
};
$scope.$watch('search.showClosed', $scope.onToggleShowClosedAdChanged, true);
$scope.onGeoDistanceChanged = function() {
if ($scope.search.loading || !$scope.entered) return;
if ($scope.search.location) {
// Refresh results
$scope.doRefresh();
}
};
$scope.$watch('search.geoDistance', $scope.onGeoDistanceChanged, true);
$scope.onCategoryClick = function(cat) {
if (cat && cat.parent) {
$scope.search.category = cat;
$scope.options.category.show = true;
$scope.search.showCategories=false; // hide categories
$scope.doSearch();
}
};
$scope.removeCategory = function() {
$scope.search.category = null;
$scope.category = null;
$scope.doSearch();
};
$scope.removeLocation = function() {
$scope.search.location = null;
$scope.search.geoPoint = null;
$scope.updateSettings();
$scope.doSearch();
};
/* -- modals & popover -- */
$scope.showActionsPopover = function (event) {
if (!$scope.actionsPopover) {
$ionicPopover.fromTemplateUrl('plugins/market/templates/search/lookup_actions_popover.html', {
scope: $scope
}).then(function (popover) {
$scope.actionsPopover = popover;
//Cleanup the popover when we're done with it!
$scope.$on('$destroy', function () {
$scope.actionsPopover.remove();
});
$scope.actionsPopover.show(event);
});
}
else {
$scope.actionsPopover.show(event);
}
};
$scope.hideActionsPopover = function () {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
}
};
$scope.toggleShowClosed = function() {
$scope.hideActionsPopover();
$scope.search.showClosed = !$scope.search.showClosed;
};
}
function MkViewGalleryController($scope, csConfig, $q, $ionicScrollDelegate, $ionicSlideBoxDelegate, $ionicModal, $interval, mkRecord) {
// Initialize the super class and extend it.
$scope.zoomMin = 1;
$scope.categories = [];
$scope.pictures = [];
$scope.activeSlide = 0;
$scope.activeCategory = null;
$scope.activeCategoryIndex = 0;
$scope.started = false;
$scope.options = $scope.options || angular.merge({
category: {
filter: undefined
},
slideDuration: 5000, // 5 sec
showClosed: false
}, csConfig.plugins && csConfig.plugins.market && csConfig.plugins.market.record || {});
$scope.slideDurationLabels = {
3000: {
labelKey: 'MARKET.GALLERY.SLIDE_DURATION_OPTION',
labelParams: {value: 3}
},
5000: {
labelKey: 'MARKET.GALLERY.SLIDE_DURATION_OPTION',
labelParams: {value: 5}
},
10000: {
labelKey: 'MARKET.GALLERY.SLIDE_DURATION_OPTION',
labelParams: {value: 10}
},
15000: {
labelKey: 'MARKET.GALLERY.SLIDE_DURATION_OPTION',
labelParams: {value: 15}
},
20000: {
labelKey: 'MARKET.GALLERY.SLIDE_DURATION_OPTION',
labelParams: {value: 20}
}
};
$scope.slideDurations = _.keys($scope.slideDurationLabels);
$scope.resetSlideShow = function() {
delete $scope.activeCategory;
delete $scope.activeCategoryIndex;
delete $scope.activeSlide;
delete $scope.categories;
};
$scope.startSlideShow = function(options) {
// Already load: continue
if ($scope.activeCategory && $scope.activeCategory.pictures && $scope.activeCategory.pictures.length) {
return $scope.showPicturesModal($scope.activeCategoryIndex,$scope.activeSlide);
}
options = options || {};
options.filter = options.filter || ($scope.options && $scope.options.category && $scope.options.category.filter);
options.withStock = (!$scope.options || !$scope.options.showClosed);
options.withOld = $scope.options && $scope.options.showOld;
$scope.stop();
$scope.loading = true;
delete $scope.activeCategory;
delete $scope.activeCategoryIndex;
delete $scope.activeSlide;
return mkRecord.category.stats(options)
.then(function(res) {
// Exclude empty categories
$scope.categories = _.filter(res, function(cat) {
return cat.count > 0 && cat.children && cat.children.length;
});
// Increment category
return $scope.nextCategory();
})
.then(function() {
$scope.loading = false;
})
.catch(function(err) {
console.error(err);
$scope.loading = false;
})
.then(function() {
if ($scope.categories && $scope.categories.length) {
return $scope.showPicturesModal(0,0);
}
});
};
$scope.showPicturesModal = function(catIndex, picIndex, pause) {
$scope.activeCategoryIndex = catIndex;
$scope.activeSlide = picIndex;
$scope.activeCategory = $scope.categories[catIndex];
if ($scope.modal) {
$ionicSlideBoxDelegate.slide(picIndex);
$ionicSlideBoxDelegate.update();
return $scope.modal.show()
.then(function() {
if (!pause) {
$scope.start();
}
});
}
return $ionicModal.fromTemplateUrl('plugins/market/templates/gallery/modal_slideshow.html',
{
scope: $scope
})
.then(function(modal) {
$scope.modal = modal;
$scope.modal.scope.closeModal = $scope.hidePictureModal;
// Cleanup the modal when we're done with it!
$scope.$on('$destroy', function() {
if ($scope.modal) {
$scope.modal.remove();
delete $scope.modal;
}
});
return $scope.modal.show()
.then(function() {
if (!pause) {
$scope.start();
}
});
});
};
$scope.hidePictureModal = function() {
$scope.stop();
if ($scope.modal && $scope.modal.isShown()) {
return $scope.modal.hide();
}
return $q.when();
};
$scope.start = function() {
if ($scope.interval) {
$interval.cancel($scope.interval);
}
console.debug('[market] [gallery] Start slideshow');
$scope.interval = $interval(function() {
$scope.nextSlide();
}, $scope.options.slideDuration);
};
$scope.stop = function() {
if ($scope.interval) {
console.debug('[market] [gallery] Stop slideshow');
$interval.cancel($scope.interval);
delete $scope.interval;
}
};
/* -- manage slide box slider-- */
$scope.nextCategory = function(started) {
if (!$scope.categories || !$scope.categories.length) return $q.when();
var started = started || !!$scope.interval;
// Make sure sure to stop slideshow
if (started && $scope.modal.isShown()) {
return $scope.hidePictureModal()
.then(function(){
return $scope.nextCategory(started);
});
}
$scope.activeCategoryIndex = $scope.loading ? 0 : $scope.activeCategoryIndex+1;
// End of slideshow: restart (reload all)
if ($scope.activeCategoryIndex === $scope.categories.length) {
$scope.resetSlideShow();
if (started) {
return $scope.startSlideShow();
}
return $q.when()
}
var category = $scope.categories[$scope.activeCategoryIndex];
// Load pictures
return mkRecord.record.pictures({
categories: _.pluck(category.children, 'id'),
size: 1000,
withStock: (!$scope.options || !$scope.options.showClosed)
})
.then(function(pictures) {
category.pictures = pictures;
if (started) {
return $scope.showPicturesModal($scope.activeCategoryIndex,0);
}
});
};
$scope.nextSlide = function() {
// If end of category pictures
if (!$scope.activeCategory || !$scope.activeCategory.pictures || !$scope.activeCategory.pictures.length || $ionicSlideBoxDelegate.currentIndex() == $scope.activeCategory.pictures.length-1) {
$scope.nextCategory();
}
else {
$ionicSlideBoxDelegate.next();
}
};
$scope.updateSlideStatus = function(slide) {
var zoomFactor = $ionicScrollDelegate.$getByHandle('scrollHandle' + slide).getScrollPosition().zoom;
if (zoomFactor == $scope.zoomMin) {
$ionicSlideBoxDelegate.enableSlide(true);
} else {
$ionicSlideBoxDelegate.enableSlide(false);
}
};
$scope.isLoadedCategory = function(cat) {
return cat.pictures && cat.pictures.length>0;
};
$scope.slideChanged = function(index) {
$scope.activeSlide = index;
}
}
MkRecordViewController.$inject = ['$scope', '$rootScope', '$anchorScroll', '$ionicPopover', '$state', '$ionicHistory', '$q', '$controller', '$timeout', '$filter', '$translate', 'UIUtils', 'Modals', 'csConfig', 'csCurrency', 'csWallet', 'esModals', 'esProfile', 'esHttp', 'mkRecord'];
MkRecordEditController.$inject = ['$scope', '$rootScope', '$q', '$state', '$ionicPopover', '$timeout', 'mkRecord', '$ionicHistory', '$focus', '$controller', 'UIUtils', 'ModalUtils', 'csConfig', 'esHttp', 'csSettings', 'csCurrency', 'mkSettings'];angular.module('cesium.market.record.controllers', ['cesium.market.record.services', 'cesium.es.services', 'cesium.es.common.controllers'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.market_view_record', {
url: "/market/view/:id/:title?refresh",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/record/view_record.html",
controller: 'MkRecordViewCtrl'
}
}
})
.state('app.market_view_record_anchor', {
url: "/market/view/:id/:title/:anchor?refresh",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/record/view_record.html",
controller: 'MkRecordViewCtrl'
}
}
})
.state('app.market_add_record', {
cache: false,
url: "/market/add/:type",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/record/edit_record.html",
controller: 'MkRecordEditCtrl'
}
}
})
.state('app.market_edit_record', {
cache: false,
url: "/market/edit/:id/:title",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/record/edit_record.html",
controller: 'MkRecordEditCtrl'
}
}
});
}])
.controller('MkRecordViewCtrl', MkRecordViewController)
.controller('MkRecordEditCtrl', MkRecordEditController)
;
function MkRecordViewController($scope, $rootScope, $anchorScroll, $ionicPopover, $state, $ionicHistory, $q, $controller,
$timeout, $filter, $translate, UIUtils, Modals, csConfig, csCurrency, csWallet,
esModals, esProfile, esHttp, mkRecord) {
'ngInject';
$scope.formData = {};
$scope.id = null;
$scope.category = {};
$scope.pictures = [];
$scope.canEdit = false;
$scope.maxCommentSize = 10;
$scope.loading = true;
$scope.motion = UIUtils.motion.fadeSlideInRight;
$scope.smallscreen = UIUtils.screen.isSmall();
$scope.moreAdMotion = UIUtils.motion.default;
$scope.smallpictures = false;
// Screen options
$scope.options = $scope.options || angular.merge({
type: {
show: true
},
category: {
show: true
},
description: {
show: true
},
location: {
show: true,
prefix : undefined
},
like: {
kinds: ['VIEW', 'LIKE', 'FOLLOW', 'ABUSE'],
index: 'market',
type: 'record',
service: mkRecord.record.like
}
}, csConfig.plugins && csConfig.plugins.market && csConfig.plugins.market.record || {});
$scope.likeData = {
views: {},
likes: {},
follows: {},
abuses: {}
};
$scope.search = {
type: null,
results: [],
total: 0,
loading: true
};
// Initialize the super class and extend it.
angular.extend(this, $controller('ESLikesCtrl', {$scope: $scope}));
$scope.enter = function (e, state) {
if (state.stateParams && state.stateParams.id) { // Load by id
if ($scope.loading || state.stateParams.refresh) {
$scope.load(state.stateParams.id);
}
else {
// prevent reload if same id (and if not forced)
UIUtils.loading.hide();
$scope.updateButtons();
}
// Notify child controllers
$scope.$broadcast('$recordView.enter', state);
}
else {
$state.go('app.market_lookup');
}
};
$scope.$on('$ionicView.enter', $scope.enter);
$scope.$on('$ionicView.beforeLeave', function (event, args) {
$scope.$broadcast('$recordView.beforeLeave', args);
});
$scope.load = function (id) {
$scope.loading = true;
$scope.formData = {};
var promise = mkRecord.record.load(id, {
fetchPictures: false,// lazy load for pictures
convertPrice: true, // convert to user unit
html: true // convert into HTML (title, description: tags, <br/> ...)
})
.then(function (data) {
$scope.formData = data.record;
$scope.formData.feesCurrency = data.record.feesCurrency || data.record.currency;
delete $scope.formData.useRelative;
$scope.id = data.id;
$scope.issuer = data.issuer;
// Load issuer stars
$scope.loadIssuerStars($scope.issuer.pubkey);
// Load more ads (if not mobile)
if (!$scope.smallscreen) {
$scope.loadMoreLikeThis(data);
}
$scope.updateView();
UIUtils.loading.hide();
$scope.loading = false;
})
.catch(function (err) {
if (!$scope.secondTry) {
$scope.secondTry = true;
$q(function () {
$scope.load(id); // loop once
}, 100);
}
else {
$scope.loading = false;
UIUtils.loading.hide();
if (err && err.ucode === 404) {
UIUtils.toast.show('MARKET.ERROR.RECORD_NOT_EXISTS');
$state.go('app.market_lookup');
}
else {
UIUtils.onError('MARKET.ERROR.LOAD_RECORD_FAILED')(err);
}
}
});
// Continue loading other data
$timeout(function () {
// Load pictures
$scope.loadPictures(id);
// Load other data (e.g. comments - see child controller)
$scope.$broadcast('$recordView.load', id, mkRecord.record);
});
return promise;
};
$scope.loadPictures = function(id) {
id = id || $scope.id;
return mkRecord.record.picture.all({id: id})
.then(function (hit) {
if (hit._source.pictures) {
$scope.pictures = hit._source.pictures.reduce(function (res, pic) {
return res.concat(esHttp.image.fromAttachment(pic.file));
}, []);
if ($scope.pictures.length) {
// Set Motion
$scope.motion.show({
selector: '.lazy-load .item.card-gallery',
ink: false
});
}
}
})
.catch(function () {
$scope.pictures = [];
});
};
$scope.loadMoreLikeThis = function(data) {
// Load more like this
return mkRecord.record.moreLikeThis(data.id, {
category: data.record.category.id,
type: data.record.type,
city: data.record.city
})
.then(function(res) {
if (!res || !res.total) return; // skip
$scope.search.results = res.hits;
$scope.search.total = res.total;
$scope.search.loading = false;
$scope.motion.show({
selector: '.list.list-more-record .item',
ink: true
});
});
};
// Load issuer stars
$scope.loadIssuerStars = function(pubkey) {
if (this.canEdit || csWallet.isLogin() && csWallet.isUserPubkey(pubkey)) return; // Skip
esProfile.like.count(pubkey, {kind: 'star', issuer: csWallet.isLogin() ? csWallet.data.pubkey : undefined})
.then(function(stars) {
$scope.issuer.stars = stars;
});
};
$scope.updateView = function() {
$scope.updateButtons();
// Set Motion (only direct children, to exclude .lazy-load children)
$scope.motion.show({
selector: '.list > .item',
ink: true
});
if (!$scope.canEdit) {
$scope.showFab('fab-like-market-record-' + $scope.id);
}
};
$scope.updateButtons = function() {
$scope.canEdit = $scope.formData && csWallet.isUserPubkey($scope.formData.issuer);
$scope.canSold = $scope.canEdit && $scope.formData.stock > 0;
$scope.canReopen = $scope.canEdit && $scope.formData.stock === 0;
if ($scope.canReopen) {
$scope.canEdit = false;
}
};
$scope.markAsView = function() {
if (!$scope.likeData || !$scope.likeData.views || $scope.likeData.views.wasHit) return; // Already view
var canEdit = $scope.canEdit || csWallet.isUserPubkey($scope.formData.issuer);
if (canEdit) return; // User is the record's issuer: skip
var timer = $timeout(function() {
if (csWallet.isLogin()) {
$scope.options.like.service.add($scope.id, {kind: 'view'}).then(function() {
$scope.likeData.views.total = ($scope.likeData.views.total||0) + 1;
});
}
timer = null;
}, 3000);
$scope.$on("$destroy", function() {
if (timer) $timeout.cancel(timer);
});
};
$scope.refreshConvertedPrice = function () {
$scope.loading = true; // force reloading if settings changed (e.g. unit price)
};
$scope.$watch('$root.settings.useRelative', $scope.refreshConvertedPrice, true);
$scope.edit = function () {
$state.go('app.market_edit_record', {id: $scope.id, title: $filter('formatSlug')($scope.formData.title)});
$scope.loading = true;
};
$scope.delete = function () {
$scope.hideActionsPopover();
UIUtils.alert.confirm('MARKET.VIEW.REMOVE_CONFIRMATION')
.then(function (confirm) {
if (confirm) {
mkRecord.record.remove($scope.id)
.then(function () {
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go('app.market_lookup');
UIUtils.toast.show('MARKET.INFO.RECORD_REMOVED');
})
.catch(UIUtils.onError('MARKET.ERROR.REMOVE_RECORD_FAILED'));
}
});
};
$scope.sold = function () {
$scope.hideActionsPopover();
UIUtils.alert.confirm('MARKET.VIEW.SOLD_CONFIRMATION')
.then(function (confirm) {
if (confirm) {
UIUtils.loading.show();
return mkRecord.record.setStock($scope.id, 0)
.then(function () {
// Update some fields (if view still in cache)
$scope.canSold = false;
$scope.canReopen = true;
$scope.canEdit = false;
})
.catch(UIUtils.onError('MARKET.ERROR.SOLD_RECORD_FAILED'))
.then(function() {
$ionicHistory.nextViewOptions({
disableBack: true,
disableAnimate: false,
historyRoot: true
});
$timeout(function(){
UIUtils.toast.show('MARKET.INFO.RECORD_SOLD');
}, 500);
$state.go('app.market_lookup');
});
}
});
};
$scope.reopen = function () {
$scope.hideActionsPopover();
UIUtils.alert.confirm('MARKET.VIEW.REOPEN_CONFIRMATION')
.then(function (confirm) {
if (confirm) {
return UIUtils.loading.show()
.then(function() {
return mkRecord.record.setStock($scope.id, 1)
.then(function () {
// Update some fields (if view still in cache)
$scope.canSold = true;
$scope.canReopen = false;
$scope.canEdit = true;
return UIUtils.loading.hide();
})
.then(function() {
UIUtils.toast.show('MARKET.INFO.RECORD_REOPEN');
})
.catch(UIUtils.onError('MARKET.ERROR.REOPEN_RECORD_FAILED'));
});
}
});
};
/* -- modals & popover -- */
$scope.showActionsPopover = function (event) {
UIUtils.popover.show(event, {
templateUrl: 'plugins/market/templates/record/view_popover_actions.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.actionsPopover = popover;
}
});
};
$scope.hideActionsPopover = function () {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
$scope.actionsPopover = null;
}
return true;
};
$scope.showSharePopover = function(event) {
$scope.hideActionsPopover();
var title = $scope.formData.title;
// Use pod share URL - see issue #69
var url = esHttp.getUrl('/market/record/' + $scope.id + '/_share');
// Override default position, is small screen - fix #25
if (UIUtils.screen.isSmall()) {
event = angular.element(document.querySelector('#record-share-anchor-' + $scope.id)) || event;
}
UIUtils.popover.share(event, {
bindings: {
url: url,
titleKey: 'MARKET.VIEW.POPOVER_SHARE_TITLE',
titleValues: {title: title},
time: $scope.formData.time,
postMessage: title,
postImage: $scope.pictures.length > 0 ? $scope.pictures[0] : null
}
});
};
$scope.showNewMessageModal = function() {
return $q.all([
$translate('MARKET.VIEW.NEW_MESSAGE_TITLE', $scope.formData),
$scope.loadWallet({minData: true})
])
.then(function(res) {
var title = res[0];
UIUtils.loading.hide();
return esModals.showMessageCompose({
title: title,
destPub: $scope.issuer.pubkey,
destUid: $scope.issuer.name || $scope.issuer.uid
});
})
.then(function(send) {
if (send) UIUtils.toast.show('MESSAGE.INFO.MESSAGE_SENT');
});
};
$scope.buy = function () {
$scope.hideActionsPopover();
return $scope.loadWallet()
.then(function (walletData) {
UIUtils.loading.hide();
if (walletData) {
return Modals.showTransfer({
pubkey: $scope.issuer.pubkey,
uid: $scope.issuer.name || $scope.issuer.uid,
amount: $scope.formData.price
}
)
.then(function (result) {
if (result) {
return UIUtils.toast.show('INFO.TRANSFER_SENT');
}
});
}
});
};
$scope.showRecord = function(event, index) {
if (event.defaultPrevented) return;
var item = $scope.search.results[index];
if (item) {
$state.go('app.market_view_record', {
id: item.id,
title: item.title
});
}
};
/* -- context aware-- */
// When wallet login/logout -> update buttons
function onWalletChange(data, deferred) {
deferred = deferred || $q.defer();
$scope.updateButtons();
$scope.loadLikes();
deferred.resolve();
return deferred.promise;
}
csWallet.api.data.on.login($scope, onWalletChange, this);
csWallet.api.data.on.logout($scope, onWalletChange, this);
}
function MkRecordEditController($scope, $rootScope, $q, $state, $ionicPopover, $timeout, mkRecord, $ionicHistory, $focus, $controller,
UIUtils, ModalUtils, csConfig, esHttp, csSettings, csCurrency, mkSettings) {
'ngInject';
// Screen options
$scope.options = $scope.options || angular.merge({
recordType: {
show: true,
canEdit: true
},
category: {
show: true,
filter: undefined
},
description: {
show: true
},
location: {
show: true,
required: true
},
position: {
showCheckbox: true,
required: true
},
unit: {
canEdit: true
},
login: {
type: "full"
}
}, csConfig.plugins && csConfig.plugins.market && csConfig.plugins.market.record || {});
// Initialize the super class and extend it.
angular.extend(this, $controller('ESPositionEditCtrl', {$scope: $scope}));
$scope.formData = {
price: null,
category: {},
geoPoint: null,
useRelative: csSettings.data.useRelative
};
$scope.id = null;
$scope.pictures = [];
$scope.loading = true;
//console.debug("[market] Screen options: ", $scope.options);
$scope.motion = UIUtils.motion.ripple;
$scope.setForm = function(form) {
$scope.form = form;
};
$scope.$on('$ionicView.enter', function(e, state) {
return $q.all([
mkSettings.currencies(),
// Load wallet
$scope.loadWallet({
minData: true
})
])
.then(function(res) {
$scope.currencies = res[0];
if (state.stateParams && state.stateParams.id) { // Load by id
$scope.load(state.stateParams.id);
}
else {
// New record
if (state.stateParams && state.stateParams.type) {
$scope.formData.type = state.stateParams.type;
}
$scope.formData.type = $scope.formData.type || ($scope.options.type && $scope.options.type.default) || 'offer'; // default: offer
$scope.formData.currency = $scope.currencies && $scope.currencies[0]; // use the first one, if any
$scope.loading = false;
UIUtils.loading.hide();
$scope.motion.show();
}
// Focus on title
if ($scope.options.focus && !UIUtils.screen.isSmall()) {
$focus('market-record-title');
}
})
.catch(function(err){
if (err === 'CANCELLED') {
$scope.motion.hide();
$scope.showHome();
}
});
});
/* -- popover -- */
$scope.showUnitPopover = function($event) {
UIUtils.popover.show($event, {
templateUrl: 'templates/wallet/popover_unit.html',
scope: $scope,
autoremove: true
})
.then(function(useRelative) {
$scope.formData.useRelative = useRelative;
});
};
$scope.cancel = function() {
$scope.closeModal();
};
$scope.load = function(id) {
UIUtils.loading.show();
return mkRecord.record.load(id, {
fetchPictures: true,
convertPrice: false // keep original price
})
.then(function(data) {
angular.merge($scope.formData, data.record);
$scope.formData.useRelative = (data.record.unit === 'UD');
if (!$scope.formData.useRelative) {
// add 2 decimals in quantitative mode
$scope.formData.price = $scope.formData.price ? $scope.formData.price / 100 : undefined;
$scope.formData.fees = $scope.formData.fees ? $scope.formData.fees / 100 : undefined;
}
// Set default currency (need by HELP texts)
if (!$scope.formData.currency) {
$scope.formData.currency = $scope.currency;
}
// Convert old record format
if (!$scope.formData.city && $scope.formData.location) {
$scope.formData.city = $scope.formData.location;
}
if ($scope.formData.location) {
$scope.formData.location = null;
}
$scope.id = data.id;
$scope.pictures = data.record.pictures || [];
delete $scope.formData.pictures; // duplicated with $scope.pictures
$scope.dirty = false;
$scope.motion.show({
selector: '.animate-ripple .item, .card-gallery'
});
UIUtils.loading.hide();
// Update loading - done with a delay, to avoid trigger onFormDataChanged()
$timeout(function() {
$scope.loading = false;
}, 1000);
})
.catch(UIUtils.onError('MARKET.ERROR.LOAD_RECORD_FAILED'));
};
$scope.save = function(silent, hasWaitDebounce) {
$scope.form.$submitted=true;
if($scope.saving || // avoid multiple save
!$scope.form.$valid || !$scope.formData.category.id) {
return $q.reject();
}
if (!hasWaitDebounce) {
console.debug('[ES] [market] Waiting debounce end, before saving...');
return $timeout(function() {
return $scope.save(silent, true);
}, 650);
}
$scope.saving = true;
console.debug('[ES] [market] Saving record...');
return UIUtils.loading.show({delay: 0})
// Preparing json (pictures + resizing thumbnail)
.then(function() {
var json = angular.copy($scope.formData);
delete json.useRelative;
var unit = $scope.formData.useRelative ? 'UD' : 'unit';
// prepare price
if (angular.isDefined(json.price) && json.price != null) { // warn: could be =0
if (typeof json.price == "string") {
json.price = parseFloat(json.price.replace(new RegExp('[.,]'), '.')); // fix #124
}
json.unit = unit;
if (unit === 'unit') {
json.price = json.price * 100;
}
if (!json.currency) {
json.currency = $scope.currency;
}
}
else {
// do not use 'undefined', but 'null' - fix #26
json.unit = null;
json.price = null;
// for now, allow set the currency, to make sure search request get Ad without price
if (!json.currency) {
json.currency = $scope.currency;
}
}
// prepare fees
if (json.fees) {
if (typeof json.fees == "string") {
json.fees = parseFloat(json.fees.replace(new RegExp('[.,]'), '.')); // fix #124
}
if (unit === 'unit') {
json.fees = json.fees * 100;
}
if (!json.feesCurrency) {
json.feesCurrency = json.currency || $scope.currency;
}
json.unit = json.unit || unit; // force unit to be set
}
else {
// do not use 'undefined', but 'null' - fix #26
json.fees = null;
json.feesCurrency = null;
}
json.time = esHttp.date.now();
// geo point
if (json.geoPoint && json.geoPoint.lat && json.geoPoint.lon) {
json.geoPoint.lat = parseFloat(json.geoPoint.lat);
json.geoPoint.lon = parseFloat(json.geoPoint.lon);
}
else{
json.geoPoint = null;
}
// Location is deprecated: force to null
if (angular.isDefined(json.location)) {
json.location = null;
}
json.picturesCount = $scope.pictures.length;
if (json.picturesCount) {
// Resize thumbnail
return UIUtils.image.resizeSrc($scope.pictures[0].src, true)
.then(function(thumbnailSrc) {
// First image = the thumbnail
json.thumbnail = esHttp.image.toAttachment({src: thumbnailSrc});
// Then = all pictures
json.pictures = $scope.pictures.reduce(function(res, picture) {
return res.concat({
file: esHttp.image.toAttachment({src: picture.src})
});
}, []);
return json;
});
}
else {
if ($scope.formData.thumbnail) {
// Workaround to allow content deletion, because of a bug in the ES attachment-mapper:
// get error (in ES node) : MapperParsingException[No content is provided.] - AttachmentMapper.parse(AttachmentMapper.java:471
json.thumbnail = {
_content: '',
_content_type: ''
};
}
json.pictures = [];
return json;
}
})
// Send data (create or update)
.then(function(json) {
if (!$scope.id) {
json.creationTime = esHttp.date.now();
// By default: stock always > 1 when created
json.stock = angular.isDefined(json.stock) ? json.stock : 1;
return mkRecord.record.add(json);
}
else {
return mkRecord.record.update(json, {id: $scope.id});
}
})
// Redirect to record view
.then(function(id) {
var isNew = !$scope.id;
$scope.id = $scope.id || id;
$scope.saving = false;
$scope.dirty = false;
// Has back history: go back then reload the view record page
if (!!$ionicHistory.backView()) {
var offState = $rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){
event.preventDefault();
$state.go('app.market_view_record', {id: $scope.id}, {location: "replace", reload: true});
offState(); // remove added listener
});
$ionicHistory.goBack(isNew ? -1 : -2);
}
// No back view: can occur when reloading the edit page
else {
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go('app.market_view_record', {id: $scope.id});
}
})
.catch(function(err) {
$scope.saving = false;
// Replace with a specific message
if (err && err.message === 'ES_HTTP.ERROR.MAX_UPLOAD_BODY_SIZE') {
err.message = 'MARKET.ERROR.RECORD_EXCEED_UPLOAD_SIZE';
}
UIUtils.onError('MARKET.ERROR.FAILED_SAVE_RECORD')(err);
});
};
$scope.openCurrencyLookup = function() {
alert('Not implemented yet. Please submit an issue if occur again.');
};
$scope.cancel = function() {
$scope.dirty = false; // force not saved
$ionicHistory.goBack();
};
$scope.$on('$stateChangeStart', function (event, next, nextParams, fromState) {
if (!$scope.dirty || $scope.saving) return;
// stop the change state action
event.preventDefault();
if ($scope.loading) return;
return UIUtils.alert.confirm('CONFIRM.SAVE_BEFORE_LEAVE',
'CONFIRM.SAVE_BEFORE_LEAVE_TITLE', {
cancelText: 'COMMON.BTN_NO',
okText: 'COMMON.BTN_YES_SAVE'
})
.then(function(confirmSave) {
if (confirmSave) {
return $scope.save();
}
})
.then(function() {
$scope.dirty = false;
$ionicHistory.nextViewOptions({
historyRoot: true
});
$state.go(next.name, nextParams);
UIUtils.loading.hide();
});
});
$scope.onFormDataChanged = function() {
if ($scope.loading) return;
$scope.dirty = true;
};
$scope.$watch('formData', $scope.onFormDataChanged, true);
/* -- modals -- */
$scope.showRecordTypeModal = function() {
ModalUtils.show('plugins/market/templates/record/modal_record_type.html')
.then(function(type){
if (type) {
$scope.formData.type = type;
}
});
};
$scope.showCategoryModal = function() {
// load categories
var getCategories;
if ($scope.options && $scope.options.category && $scope.options.category.filter) {
getCategories = mkRecord.category.filtered({filter: $scope.options.category.filter});
}
else {
getCategories = mkRecord.category.all();
}
getCategories
.then(function(categories){
return ModalUtils.show('plugins/es/templates/common/modal_category.html', 'ESCategoryModalCtrl as ctrl',
{categories : categories},
{focusFirstInput: true}
);
})
.then(function(cat){
if (cat && cat.parent) {
$scope.formData.category = cat;
}
});
};
}
MarketWalletRecordsController.$inject = ['$scope', '$controller', 'UIUtils'];angular.module('cesium.market.wallet.controllers', ['cesium.es.services'])
.config(['$stateProvider', 'PluginServiceProvider', 'csConfig', function($stateProvider, PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
PluginServiceProvider.extendState('app.view_wallet', {
points: {
'general': {
templateUrl: "plugins/market/templates/wallet/view_wallet_extend.html",
controller: 'ESExtensionCtrl'
}
}
})
;
}
$stateProvider
.state('app.market_wallet_records', {
url: "/records/wallet",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/wallet/view_wallet_records.html",
controller: 'MkWalletRecordsCtrl'
}
}
})
}])
.controller('MkWalletRecordsCtrl', MarketWalletRecordsController)
;
function MarketWalletRecordsController($scope, $controller, UIUtils) {
// Initialize the super class and extend it.
angular.extend(this, $controller('MkLookupAbstractCtrl', {$scope: $scope}));
$scope.search.showClosed = true;
$scope.smallscreen = UIUtils.screen.isSmall();
$scope.enter = function(e, state) {
$scope.loadWallet()
.then(function(walletData) {
$scope.search.text = walletData.pubkey;
$scope.search.lastRecords=false;
$scope.search.sortAttribute="creationTime";
$scope.search.sortDirection="desc";
if (!$scope.entered || !$scope.search.results || $scope.search.results.length === 0) {
$scope.doSearch()
.then(function() {
$scope.showFab('fab-wallet-add-market-record');
});
}
})
.catch(function(err){
if (err == 'CANCELLED') {
return $scope.showHome();
}
console.error(err);
});
$scope.entered = true;
};
$scope.$on('$ionicView.enter', $scope.enter);
}
MkListCategoriesController.$inject = ['$scope', 'UIUtils', 'csConfig', 'mkRecord'];
MkViewCategoriesController.$inject = ['$scope', '$controller', '$state'];angular.module('cesium.market.category.controllers', ['cesium.market.record.services', 'cesium.services'])
.config(['$stateProvider', function($stateProvider) {
'ngInject';
$stateProvider
.state('app.market_categories', {
url: "/market/categories",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/category/view_categories.html",
controller: 'MkViewCategoriesCtrl'
}
},
data: {
large: 'app.market_categories_lg'
}
})
.state('app.market_categories_lg', {
url: "/market/categories/lg",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/category/view_categories_lg.html",
controller: 'MkViewCategoriesCtrl'
}
}
})
;
}])
.controller('MkListCategoriesCtrl', MkListCategoriesController)
.controller('MkViewCategoriesCtrl', MkViewCategoriesController)
;
function MkListCategoriesController($scope, UIUtils, csConfig, mkRecord) {
'ngInject';
$scope.loading = true;
$scope.motion = UIUtils.motion.ripple;
// Screen options
$scope.options = $scope.options || angular.merge({
category: {
filter: undefined
},
showClosed: false
}, csConfig.plugins && csConfig.plugins.market && csConfig.plugins.market.record || {});
$scope.load = function(options) {
$scope.loading = true;
options = options || {};
options.filter = options.filter || ($scope.options && $scope.options.category && $scope.options.category.filter);
options.withStock = (!$scope.options || !$scope.options.showClosed);
return mkRecord.category.stats(options)
.then(function(res) {
$scope.categories = res;
$scope.totalCount = $scope.categories.reduce(function(res, cat) {
return res + cat.count;
}, 0);
$scope.loading = false;
$scope.motion.show && $scope.motion.show();
});
};
$scope.refresh = function() {
if ($scope.loading) return;
// Load data
return $scope.load();
};
$scope.$watch('options.showClosed', $scope.refresh, true);
}
function MkViewCategoriesController($scope, $controller, $state) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('MkListCategoriesCtrl', {$scope: $scope}));
// When view enter: load data
$scope.enter = function(e, state) {
// Load data
return $scope.load()
.then(function() {
$scope.loading = false;
if (!$scope.entered) {
$scope.motion.show();
}
$scope.entered = true;
});
};
$scope.$on('$ionicView.enter',$scope.enter);
$scope.onCategoryClick = function(cat) {
return $state.go('app.market_lookup', {category: cat && cat.id});
}
}
MkIdentityRecordsController.$inject = ['$scope', '$controller', 'UIUtils'];angular.module('cesium.market.wot.controllers', ['cesium.es.services'])
.config(['$stateProvider', 'PluginServiceProvider', 'csConfig', function($stateProvider, PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
PluginServiceProvider.extendStates(['app.wot_identity', 'app.wot_identity_uid'], {
points: {
'general': {
templateUrl: "plugins/market/templates/wot/view_identity_extend.html",
controller: 'ESExtensionCtrl'
},
'after-general': {
templateUrl: "plugins/market/templates/wot/view_identity_extend.html",
controller: 'ESExtensionCtrl'
}
}
})
;
}
$stateProvider
.state('app.market_identity_records', {
url: "/market/records/:pubkey",
views: {
'menuContent': {
templateUrl: "plugins/market/templates/wot/view_identity_records.html",
controller: 'MkIdentityRecordsCtrl'
}
}
})
}])
.controller('MkIdentityRecordsCtrl', MkIdentityRecordsController)
;
function MkIdentityRecordsController($scope, $controller, UIUtils) {
// Initialize the super class and extend it.
angular.extend(this, $controller('MkLookupAbstractCtrl', {$scope: $scope}));
$scope.smallscreen = UIUtils.screen.isSmall();
$scope.enter = function(e, state) {
$scope.pubkey = state && state.stateParams && state.stateParams.pubkey;
if (!$scope.pubkey) return $scope.showHome();
$scope.search.text = $scope.pubkey;
$scope.search.lastRecords=false;
$scope.doSearch();
$scope.entered = true;
};
$scope.$on('$ionicView.enter', $scope.enter);
}
MkLastDocumentsController.$inject = ['$scope', '$controller', '$timeout', '$state', '$filter'];angular.module('cesium.market.document.controllers', ['cesium.es.services'])
.controller('MkLastDocumentsCtrl', MkLastDocumentsController)
;
function MkLastDocumentsController($scope, $controller, $timeout, $state, $filter) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('ESLastDocumentsCtrl', {$scope: $scope}));
$scope.search.index = 'user,page,group,market';
$scope.search.type = 'profile,record,comment';
$scope._source = ["issuer", "hash", "time", "creationTime", "title", "price", "unit", "currency", "picturesCount", "thumbnail._content_type", "city", "message", "record"];
$scope.inheritedSelectDocument = $scope.selectDocument;
$scope.selectDocument = function(event, doc) {
// Call super function
if (doc.index !== "market") {
$scope.inheritedSelectDocument(event, doc);
return;
}
// Manage click on a market document
if (!doc || !event || event.defaultPrevented) return;
event.stopPropagation();
if (doc.index === "market" && doc.type === "record") {
$state.go('app.market_view_record', {id: doc.id, title: doc.title});
}
else if (doc.index === "market" && doc.type === "comment") {
var anchor = $filter('formatHash')(doc.id);
$state.go('app.market_view_record_anchor', {id: doc.record, anchor: anchor});
}
};
}
angular.module('cesium.graph.plugin', [
// Services
'cesium.graph.services',
// Controllers
'cesium.graph.common.controllers',
'cesium.graph.docstats.controllers',
'cesium.graph.synchro.controllers',
'cesium.graph.network.controllers'
])
;
angular.module('cesium.graph.services', [
// Services
'cesium.graph.color.services',
'cesium.graph.data.services'
])
;
angular.module('cesium.graph.data.services', ['cesium.es.http.services'])
.factory('gpData', ['$rootScope', '$q', '$timeout', 'esHttp', 'BMA', 'csCache', function($rootScope, $q, $timeout, esHttp, BMA, csCache) {
'ngInject';
var
currencyCache = csCache.get('gpData-currency-', csCache.constants.SHORT),
exports = {
node: {},
wot: {},
blockchain: {},
docstat: {},
synchro: {
execution: {}
},
raw: {
block: {
search: esHttp.post('/:currency/block/_search')
},
blockstat: {
search: esHttp.post('/:currency/blockstat/_search')
},
movement: {
search: esHttp.post('/:currency/movement/_search')
},
user: {
event: esHttp.post('/user/event/_search?pretty')
},
docstat: {
search: esHttp.post('/document/stats/_search')
},
synchro: {
search: esHttp.post('/:currency/synchro/_search')
}
},
regex: {
}
};
function _powBase(amount, base) {
return base <= 0 ? amount : amount * Math.pow(10, base);
}
function _initRangeOptions(options) {
options = options || {};
options.maxRangeSize = options.maxRangeSize || 30;
options.defaultTotalRangeCount = options.defaultTotalRangeCount || options.maxRangeSize*2;
options.rangeDuration = options.rangeDuration || 'day';
options.endTime = options.endTime || moment().utc().add(1, options.rangeDuration).unix();
options.startTime = options.startTime ||
moment.unix(options.endTime).utc().subtract(options.defaultTotalRangeCount, options.rangeDuration).unix();
// Make to sure startTime is never before the currency starts - fix #483
if (options.firstBlockTime && options.startTime < options.firstBlockTime) {
options.startTime = options.firstBlockTime;
}
return options;
}
/**
* Graph: "statictics on ES documents"
* @param currency
* @returns {*}
*/
exports.docstat.get = function(options) {
options = _initRangeOptions(options);
var jobs = [];
var from = moment.unix(options.startTime).utc().startOf(options.rangeDuration);
var to = moment.unix(options.endTime).utc().startOf(options.rangeDuration);
var ranges = [];
var processSearchResult = function (res) {
var aggs = res.aggregations;
return (aggs.range && aggs.range.buckets || []).reduce(function (res, agg) {
var item = {
from: agg.from,
to: agg.to
};
_.forEach(agg.index && agg.index.buckets || [], function (agg) {
var index = agg.key;
_.forEach(agg.type && agg.type.buckets || [], function (agg) {
var key = (index + '_' + agg.key);
item[key] = agg.max.value;
if (!indices[key]) indices[key] = true;
});
});
return res.concat(item);
}, []);
};
while(from.isBefore(to)) {
ranges.push({
from: from.unix(),
to: from.add(1, options.rangeDuration).unix()
});
// Flush if max range count, or just before loop condition end (fix #483)
var flush = (ranges.length === options.maxRangeSize) || !from.isBefore(to);
if (flush) {
var request = {
size: 0,
aggs: {
range: {
range: {
field: "time",
ranges: ranges
},
aggs: {
index : {
terms: {
field: "index",
size: 0
},
aggs: {
type: {
terms: {
field: "type",
size: 0
},
aggs: {
max: {
max: {
field : "count"
}
}
}
}
}
}
}
}
}
};
// prepare next loop
ranges = [];
var indices = {};
var params = {
request_cache: angular.isDefined(options.cache) ? options.cache : true // enable by default
};
if (jobs.length === 10) {
console.error('Too many parallel jobs!');
from = moment.unix(options.endTime).utc(); // stop while
}
else {
jobs.push(
exports.raw.docstat.search(request, params)
.then(processSearchResult)
);
}
}
} // loop
return $q.all(jobs)
.then(function(res) {
res = res.reduce(function(res, hits){
if (!hits || !hits.length) return res;
return res.concat(hits);
}, []);
res = _.sortBy(res, 'from');
return _.keys(indices).reduce(function(series, index) {
series[index] = _.pluck(res, index);
return series;
}, {
times: _.pluck(res, 'from')
});
});
};
/**
* Graph: "statictics on ES documents"
* @param currency
* @returns {*}
*/
exports.synchro.execution.get = function(options) {
options = _initRangeOptions(options);
var jobs = [];
var from = moment.unix(options.startTime).utc().startOf(options.rangeDuration);
var to = moment.unix(options.endTime).utc().startOf(options.rangeDuration);
var ranges = [];
var processSearchResult = function (res) {
var aggs = res.aggregations;
return (aggs.range && aggs.range.buckets || []).reduce(function (res, agg) {
var item = {
from: agg.from,
to: agg.to,
inserts: agg.result.inserts.value,
updates: agg.result.inserts.value,
deletes: agg.result.deletes.value,
duration: agg.duration.value
};
_.forEach(agg.api && agg.api.buckets || [], function (api) {
item[api.key] = api.peer_count && api.peer_count.value || 0;
if (!apis[api.key]) apis[api.key] = true;
});
return res.concat(item);
}, []);
};
while(from.isBefore(to)) {
ranges.push({
from: from.unix(),
to: from.add(1, options.rangeDuration).unix()
});
// Flush if max range count, or just before loop condition end (fix #483)
var flush = (ranges.length === options.maxRangeSize) || !from.isBefore(to);
if (flush) {
var request = {
size: 0,
aggs: {
range: {
range: {
field: "time",
ranges: ranges
},
aggs: {
api: {
terms: {
field: "api",
size: 0
},
aggs: {
peer_count: {
cardinality: {
field: "peer"
}
}
}
},
duration: {
sum: {
field: "executionTime"
}
},
result: {
nested: {
path: "result"
},
aggs: {
inserts : {
sum: {
field : "result.inserts"
}
},
updates : {
sum: {
field : "result.updates"
}
},
deletes : {
sum: {
field : "result.deletes"
}
}
}
}
}
}
}
};
// prepare next loop
ranges = [];
var apis = {};
if (jobs.length === 10) {
console.error('Too many parallel jobs!');
from = moment.unix(options.endTime).utc(); // stop while
}
else {
jobs.push(
exports.raw.synchro.search(request, {currency: options.currency})
.then(processSearchResult)
);
}
}
} // loop
return $q.all(jobs)
.then(function(res) {
res = res.reduce(function(res, hits){
if (!hits || !hits.length) return res;
return res.concat(hits);
}, []);
res = _.sortBy(res, 'from');
var series = {
times: _.pluck(res, 'from'),
inserts: _.pluck(res, 'inserts'),
updates: _.pluck(res, 'updates'),
deletes: _.pluck(res, 'deletes'),
duration: _.pluck(res, 'duration')
};
_.keys(apis).forEach(function(api) {
series[api] = _.pluck(res, api);
});
return series;
});
};
return exports;
}])
;
angular.module('cesium.graph.color.services', [])
.factory('gpColor', function() {
'ngInject';
var
constants = {
css2Rgb: {
'white': [255, 255, 255],
'assertive': [239, 71, 58], // ok
'calm': [17, 193, 243], // ok
'positive': [56, 126, 245], // ok
'balanced': [51, 205, 95], // ok
'energized': [255, 201, 0], // ok
'royal': [136, 106, 234], // ok
'gray': [150, 150, 150], // ok
'stable': [248, 248, 248] // ok
}
},
exports = {
scale: {}
};
/**
* Compute colors scale
* @param count
* @param opacity
* @param startColor
* @param startState
* @returns {Array}
*/
exports.scale.custom = function (count, opacity, startColor, startState) {
function _state2side(state) {
switch (state) {
case 0:
return 0;
case 1:
return -1;
case 2:
return 0;
case 3:
return 1;
}
}
// From [0,1]
opacity = opacity>0 && opacity|| 0.55;
var defaultStateSize = Math.round(count / 2.5/*=4 states max*/);
// Start color [r,v,b]
var color = startColor && startColor.length == 3 ? angular.copy(startColor) : [255, 0, 0]; // Red
// Colors state: 0=keep, 1=decrease, 2=keep, 3=increase
var states = startState && startState.length == 3 ? angular.copy(startState) : [0, 2, 3]; // R=keep, V=keep, B=increase
var steps = startColor ? [
Math.round(255 / defaultStateSize),
Math.round(255 / defaultStateSize),
Math.round(255 / defaultStateSize)
] : [
Math.round((color[0] - 50) / defaultStateSize),
Math.round((255 - color[1]) / defaultStateSize),
Math.round((255 - color[2]) / defaultStateSize)
];
// Compute start sides (1=increase, 0=flat, -1=decrease)
var sides = [
_state2side(states[0]),
_state2side(states[1]),
_state2side(states[2])];
// Use to detect when need to change a 'flat' state (when state = 0 or 2)
var stateCounters = [0, 0, 0];
var result = [];
for (var i = 0; i < count; i++) {
for (var j = 0; j < 3; j++) {
color[j] += sides[j] * steps[j];
stateCounters[j]++;
// color has reach a limit
if (((color[j] <= 0 || color[j] >= 255) && sides[j] !== 0) ||
(sides[j] === 0 && stateCounters[j] == defaultStateSize)) {
// Max sure not overflow limit
if (color[j] <= 0) {
color[j] = 0;
}
else if (color[j] >= 255) {
color[j] = 255;
}
// Go to the next state, in [0..3]
states[j] = (states[j] + 1) % 4;
// Update side from this new state
sides[j] = _state2side(states[j]);
// Reset state counter
stateCounters[j] = 0;
}
}
// Add the color to result
result.push('rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + opacity + ')');
}
return result;
};
exports.scale.default = function () {
return exports.scale.custom(25);
};
/**
* Create a array with the given color
**/
exports.scale.fix = function (length, color) {
return Array.apply(null, Array(length||25))
.map(String.prototype.valueOf, color||exports.rgba.calm(0.5));
};
// Create a function to generate a rgba string, from
exports.rgba = _.mapObject(constants.css2Rgb, function(rgbArray){
var prefix = 'rgba(' + rgbArray.join(',') + ',';
return function(opacity){
if (!opacity || opacity < 0) {
return 'rgb(' + rgbArray.join(',') + ')';
}
return prefix + opacity + ')';
};
});
exports.rgba.translucent = function() {
return 'rgb(0,0,0,0)';
};
exports.constants = constants;
return exports;
})
;
GpCurrencyAbstractController.$inject = ['$scope', '$filter', '$ionicPopover', '$ionicHistory', '$state', 'csSettings', 'csCurrency', 'UIUtils'];
angular.module('cesium.graph.common.controllers', ['cesium.services'])
.controller('GpCurrencyAbstractCtrl', GpCurrencyAbstractController)
;
function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHistory, $state, csSettings, csCurrency, UIUtils) {
'ngInject';
$scope.loading = true;
$scope.formData = $scope.formData || {
useRelative: csSettings.data.useRelative,
timePct: 100,
rangeDuration: 'day',
firstBlockTime: 0,
scale: 'linear',
hide: [],
beginAtZero: true
};
$scope.formData.useRelative = false; /*angular.isDefined($scope.formData.useRelative) ?
$scope.formData.useRelative : csSettings.data.useRelative;*/
$scope.scale = 'linear';
$scope.height = undefined;
$scope.width = undefined;
$scope.maintainAspectRatio = true;
$scope.times = [];
function _truncDate(time) {
return moment.unix(time).utc().startOf($scope.formData.rangeDuration).unix();
}
$scope.enter = function (e, state) {
if ($scope.loading) {
if (state && state.stateParams) {
// remember state, to be able to refresh location
$scope.stateName = state && state.stateName;
$scope.stateParams = angular.copy(state && state.stateParams||{});
if (!$scope.formData.currency && state && state.stateParams && state.stateParams.currency) { // Currency parameter
$scope.formData.currency = state.stateParams.currency;
}
if (state.stateParams.t) {
$scope.formData.timePct = state.stateParams.t;
}
else if (state.stateParams.timePct) {
$scope.formData.timePct = state.stateParams.timePct;
}
if (state.stateParams.stepUnit) {
$scope.formData.rangeDuration = state.stateParams.stepUnit;
}
if (state.stateParams.scale) {
$scope.formData.scale = state.stateParams.scale;
}
// Allow to hide some dataset
if (state.stateParams.hide) {
$scope.formData.hide = state.stateParams.hide.split(',').reduce(function(res, index){
return res.concat(parseInt(index));
}, []);
}
}
// Should be override by subclasses
$scope.init(e, state);
// Make sure there is currency, or load it not
if (!$scope.formData.currency) {
return csCurrency.get()
.then(function (currency) {
$scope.formData.currency = currency ? currency.name : null;
$scope.formData.firstBlockTime = currency ? _truncDate(currency.firstBlockTime) : 0;
if (!$scope.formData.firstBlockTime){
console.warn('[graph] currency.firstBlockTime not loaded ! Should have been loaded by currrency service!');
}
$scope.formData.currencyAge = _truncDate(moment().utc().unix()) - $scope.formData.firstBlockTime;
return $scope.enter(e, state);
});
}
$scope.load() // Should be override by subclasses
.then(function () {
// Update scale
$scope.setScale($scope.formData.scale);
// Hide some dataset by index (read from state params)
$scope.updateHiddenDataset();
$scope.loading = false;
});
}
};
$scope.$on('$csExtension.enter', $scope.enter);
$scope.$on('$ionicParentView.enter', $scope.enter);
$scope.updateLocation = function() {
if (!$scope.stateName) return;
$ionicHistory.nextViewOptions({
disableAnimate: true,
disableBack: true,
historyRoot: true
});
$scope.stateParams = $scope.stateParams || {};
$scope.stateParams.t = ($scope.formData.timePct >= 0 && $scope.formData.timePct < 100) ? $scope.formData.timePct : undefined;
$scope.stateParams.stepUnit = $scope.formData.rangeDuration != 'day' ? $scope.formData.rangeDuration : undefined;
$scope.stateParams.hide = $scope.formData.hide && $scope.formData.hide.length ? $scope.formData.hide.join(',') : undefined;
$scope.stateParams.scale = $scope.formData.scale != 'linear' ?$scope.formData.scale : undefined;
$state.go($scope.stateName, $scope.stateParams, {
reload: false,
inherit: true,
notify: false}
);
};
// Allow to fixe size, form a template (e.g. in a 'ng-init' tag)
$scope.setSize = function(height, width, maintainAspectRatio) {
$scope.height = height;
$scope.width = width;
$scope.maintainAspectRatio = angular.isDefined(maintainAspectRatio) ? maintainAspectRatio : $scope.maintainAspectRatio;
};
// When parent view execute a refresh action
$scope.$on('csView.action.refresh', function(event, context) {
if (!context || context == 'currency') {
return $scope.load();
}
});
$scope.init = function(e, state) {
// Should be override by subclasses
};
$scope.load = function() {
// Should be override by subclasses
};
$scope.toggleScale = function() {
$scope.setScale($scope.formData.scale === 'linear' ? 'logarithmic' : 'linear');
$scope.updateLocation();
};
$scope.setScale = function(scale) {
$scope.hideActionsPopover();
$scope.formData.scale = scale;
if (!$scope.options || !$scope.options.scales || !$scope.options.scales.yAxes) return;
var format = $filter('formatInteger');
_.forEach($scope.options.scales.yAxes, function(yAxe, index) {
yAxe.type = scale;
yAxe.ticks = yAxe.ticks || {};
if (scale == 'linear') {
yAxe.ticks.beginAtZero = angular.isDefined($scope.formData.beginAtZero) ? $scope.formData.beginAtZero : true;
delete yAxe.ticks.min;
yAxe.ticks.callback = function(value) {
return format(value);
};
}
else {
//yAxe.ticks.min = 0;
delete yAxe.ticks.beginAtZero;
delete yAxe.ticks.callback;
yAxe.ticks.callback = function(value, index) {
if (!value) return;
if (Math.log10(value)%1 === 0 || Math.log10(value/3)%1 === 0) {
return format(value);
}
return '';
};
}
});
};
$scope.setRangeDuration = function(rangeDuration) {
$scope.hideActionsPopover();
if ($scope.formData && rangeDuration == $scope.formData.rangeDuration) return;
$scope.formData.rangeDuration = rangeDuration;
// Restore default values
delete $scope.formData.startTime;
delete $scope.formData.endTime;
delete $scope.formData.rangeDurationSec;
//$scope.formData.timePct = 100;
// Reload data
$scope.load();
// Update location
$scope.updateLocation();
};
$scope.updateHiddenDataset = function(datasetOverride) {
datasetOverride = datasetOverride || $scope.datasetOverride || {};
_.forEach($scope.formData.hide||[], function(index) {
if (!datasetOverride[index]) return; // skip invalid index
// Hide the dataset (stroke the legend)
datasetOverride[index].hidden = true;
// If this dataset has an yAxis, hide it (if not used elsewhere)
var yAxisID = datasetOverride[index].yAxisID;
var yAxe = yAxisID && $scope.options && $scope.options.scales && _.findWhere($scope.options.scales.yAxes||[], {id: yAxisID});
if (yAxisID && yAxe) {
var yAxisDatasetCount = _.filter(datasetOverride, function(dataset) {
return dataset.yAxisID === yAxisID;
}).length;
if (yAxisDatasetCount == 1) {
yAxe.display = false;
}
}
});
};
$scope.onLegendClick = function(e, legendItem) {
var index = legendItem.datasetIndex;
var ci = this.chart;
var meta = ci.getDatasetMeta(index);
// Hide/show the dataset
meta.hidden = meta.hidden === null? !ci.data.datasets[index].hidden : null;
// Update yAxis display (if used by only ONE dataset)
if (ci.config && ci.config.data && ci.config.data.datasets) {
var yAxisDatasetCount = _.filter(ci.config.data.datasets, function(dataset) {
return dataset.yAxisID && dataset.yAxisID === meta.yAxisID;
}).length;
if (yAxisDatasetCount === 1) {
ci.scales[meta.yAxisID].options.display = !(meta.hidden === true);
}
}
// We hid a dataset ... rerender the chart
ci.update();
// Update window location
$scope.formData.hide = $scope.formData.hide||[];
$scope.formData.hide = meta.hidden ?
_.union($scope.formData.hide, [index]) :
_.difference($scope.formData.hide, [index]);
$scope.updateLocation();
};
$scope.goPreviousRange = function() {
if ($scope.loadingRange) return;
$scope.loadingRange = true;
$scope.formData.startTime -= $scope.times.length * $scope.formData.rangeDurationSec;
if ($scope.formData.startTime < $scope.formData.firstBlockTime) {
$scope.formData.startTime = $scope.formData.firstBlockTime;
}
$scope.formData.endTime = $scope.formData.startTime + $scope.times.length * $scope.formData.rangeDurationSec;
// Reload data
$scope.load().then(function(){
// Update location
$scope.updateLocation();
$scope.loadingRange = false;
});
};
$scope.goNextRange = function() {
if ($scope.loadingRange) return;
$scope.loadingRange = true;
$scope.formData.startTime += $scope.times.length * $scope.formData.rangeDurationSec;
if ($scope.formData.startTime > $scope.formData.firstBlockTime + $scope.formData.currencyAge - $scope.formData.timeWindow) {
$scope.formData.startTime = $scope.formData.firstBlockTime + $scope.formData.currencyAge - $scope.formData.timeWindow;
}
$scope.formData.endTime = $scope.formData.startTime + $scope.times.length * $scope.formData.rangeDurationSec;
// Reload data
$scope.load().then(function(){
// Update location
$scope.updateLocation();
$scope.loadingRange = false;
});
};
$scope.onRangeChanged = function() {
if ($scope.loadingRange) return;
$scope.loadingRange = true;
$scope.formData.startTime = $scope.formData.firstBlockTime + (parseFloat($scope.formData.timePct) / 100) * ($scope.formData.currencyAge - $scope.formData.timeWindow) ;
$scope.formData.endTime = $scope.formData.startTime + $scope.times.length * $scope.formData.rangeDurationSec;
// Reload data
$scope.load().then(function(){
// Update location
$scope.updateLocation();
$scope.loadingRange = false;
});
};
$scope.updateRange = function(startTime, endTime, updateTimePct) {
updateTimePct = angular.isDefined(updateTimePct) ? updateTimePct : true;
$scope.formData.startTime = startTime;
$scope.formData.endTime = endTime;
$scope.formData.timeWindow = $scope.formData.timeWindow || $scope.formData.endTime - $scope.formData.startTime;
$scope.formData.rangeDurationSec = $scope.formData.rangeDurationSec || $scope.formData.timeWindow / ($scope.times.length-1);
if (updateTimePct) {
$scope.formData.timePct = Math.ceil(($scope.formData.startTime - $scope.formData.firstBlockTime) * 100 /
($scope.formData.currencyAge - $scope.formData.timeWindow));
}
};
/* -- Popover -- */
$scope.showActionsPopover = function(event) {
UIUtils.popover.show(event, {
templateUrl: 'plugins/graph/templates/common/popover_range_actions.html',
scope: $scope,
autoremove: true,
afterShow: function(popover) {
$scope.actionsPopover = popover;
}
});
};
$scope.hideActionsPopover = function() {
if ($scope.actionsPopover) {
$scope.actionsPopover.hide();
$scope.actionsPopover = null;
}
};
}
GpDocStatsController.$inject = ['$scope', '$state', '$controller', '$q', '$translate', 'gpColor', 'gpData', '$filter'];
angular.module('cesium.graph.docstats.controllers', ['chart.js', 'cesium.graph.services', 'cesium.graph.common.controllers'])
.config(['$stateProvider', 'PluginServiceProvider', 'csConfig', function($stateProvider, PluginServiceProvider, csConfig) {
'ngInject';
$stateProvider
.state('app.doc_stats_lg', {
url: "/data/stats?stepUnit&t&hide&scale",
views: {
'menuContent': {
templateUrl: "plugins/graph/templates/docstats/view_stats.html",
controller: 'GpDocStatsCtrl'
}
}
});
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// TODO: add buttons to link with doc stats
}
}])
.controller('GpDocStatsCtrl', GpDocStatsController)
;
function GpDocStatsController($scope, $state, $controller, $q, $translate, gpColor, gpData, $filter) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('GpCurrencyAbstractCtrl', {$scope: $scope}));
$scope.formData.rangeDuration = 'month';
$scope.displayRightAxis = true;
$scope.hiddenDatasets = [];
$scope.chartIdPrefix = 'docstats-chart-';
$scope.charts = [
// Market Ads (offer, need)
{
id: 'market',
title: 'GRAPH.DOC_STATS.MARKET.TITLE',
series: [
{
key: 'market_record',
label: 'GRAPH.DOC_STATS.MARKET.AD',
color: gpColor.rgba.royal(),
pointHoverBackgroundColor: gpColor.rgba.royal(),
clickState: {
name: 'app.document_search',
params: {index:'market', type: 'record'}
}
},
{
key: 'market_comment',
label: 'GRAPH.DOC_STATS.MARKET.COMMENT',
color: gpColor.rgba.gray(0.5),
pointHoverBackgroundColor: gpColor.rgba.gray(),
clickState: {
name: 'app.document_search',
params: {index:'market', type: 'comment'}
}
}
]
},
// Market delta
{
id: 'user_delta',
title: 'GRAPH.DOC_STATS.MARKET_DELTA.TITLE',
series: [
{
key: 'market_record_delta',
label: 'GRAPH.DOC_STATS.MARKET_DELTA.AD',
type: 'line',
yAxisID: 'y-axis-delta',
color: gpColor.rgba.royal(),
pointHoverBackgroundColor: gpColor.rgba.royal()
},
{
key: 'market_comment_delta',
label: 'GRAPH.DOC_STATS.MARKET_DELTA.COMMENT',
type: 'line',
yAxisID: 'y-axis-delta',
color: gpColor.rgba.gray(0.5),
pointHoverBackgroundColor: gpColor.rgba.gray()
}
]
},
// User count
{
id: 'user',
title: 'GRAPH.DOC_STATS.USER.TITLE',
series: [
{
key: 'user_profile',
label: 'GRAPH.DOC_STATS.USER.USER_PROFILE',
color: gpColor.rgba.royal(0.7),
pointHoverBackgroundColor: gpColor.rgba.royal(),
clickState: {
name: 'app.document_search',
params: {index:'user', type: 'profile'}
}
},
{
key: 'user_settings',
label: 'GRAPH.DOC_STATS.USER.USER_SETTINGS',
color: gpColor.rgba.gray(0.5),
pointHoverBackgroundColor: gpColor.rgba.gray(),
clickState: {
name: 'app.document_search',
params: {index:'user', type: 'settings'}
}
}
]
},
// User delta
{
id: 'user_delta',
title: 'GRAPH.DOC_STATS.USER_DELTA.TITLE',
series: [
{
key: 'user_profile_delta',
label: 'GRAPH.DOC_STATS.USER_DELTA.USER_PROFILE',
type: 'line',
yAxisID: 'y-axis-delta',
color: gpColor.rgba.royal(),
pointHoverBackgroundColor: gpColor.rgba.royal()
},
{
key: 'user_settings_delta',
label: 'GRAPH.DOC_STATS.USER_DELTA.USER_SETTINGS',
type: 'line',
yAxisID: 'y-axis-delta',
color: gpColor.rgba.gray(0.5),
pointHoverBackgroundColor: gpColor.rgba.gray()
}
]
},
// Message & Co.
{
id: 'message',
title: 'GRAPH.DOC_STATS.MESSAGE.TITLE',
series: [
{
key: 'message_inbox',
label: 'GRAPH.DOC_STATS.MESSAGE.MESSAGE_INBOX',
color: gpColor.rgba.royal(),
pointHoverBackgroundColor: gpColor.rgba.royal(),
clickState: {
name: 'app.document_search',
params: {index:'message', type: 'inbox'}
}
},
{
key: 'message_outbox',
label: 'GRAPH.DOC_STATS.MESSAGE.MESSAGE_OUTBOX',
color: gpColor.rgba.calm(),
pointHoverBackgroundColor: gpColor.rgba.calm(),
clickState: {
name: 'app.document_search',
params: {index:'message', type: 'outbox'}
}
}
]
},
// Social Page & group
{
id: 'social',
title: 'GRAPH.DOC_STATS.SOCIAL.TITLE',
series: [
{
key: 'page_record',
label: 'GRAPH.DOC_STATS.SOCIAL.PAGE_RECORD',
color: gpColor.rgba.royal(),
pointHoverBackgroundColor: gpColor.rgba.royal(),
clickState: {
name: 'app.document_search',
params: {index:'page', type: 'record'}
}
},
{
key: 'group_record',
label: 'GRAPH.DOC_STATS.SOCIAL.GROUP_RECORD',
color: gpColor.rgba.calm(),
pointHoverBackgroundColor: gpColor.rgba.calm(),
clickState: {
name: 'app.document_search',
params: {index:'group', type: 'record'}
}
},
{
key: 'page_comment',
label: 'GRAPH.DOC_STATS.SOCIAL.PAGE_COMMENT',
color: gpColor.rgba.gray(0.5),
pointHoverBackgroundColor: gpColor.rgba.gray(),
clickState: {
name: 'app.document_search',
params: {index:'page', type: 'comment'}
}
}
]
},
// Subscriptions (email, ...)
{
id: 'subscription',
title: 'GRAPH.DOC_STATS.SUBSCRIPTION.TITLE',
series: [
{
key: 'subscription_record',
label: 'GRAPH.DOC_STATS.SUBSCRIPTION.EMAIL',
color: gpColor.rgba.royal(),
pointHoverBackgroundColor: gpColor.rgba.royal(),
clickState: {
name: 'app.document_search',
params: {index:'subscription', type: 'record'}
}
}
]
},
// Other: deletion, doc, etc.
{
id: 'other',
title: 'GRAPH.DOC_STATS.OTHER.TITLE',
series: [
{
key: 'history_delete',
label: 'GRAPH.DOC_STATS.OTHER.HISTORY_DELETE',
color: gpColor.rgba.gray(0.5),
pointHoverBackgroundColor: gpColor.rgba.gray(),
clickState: {
name: 'app.document_search',
params: {index:'history', type: 'delete'}
}
}
]
}
];
var formatInteger = $filter('formatInteger');
$scope.defaultChartOptions = {
responsive: true,
maintainAspectRatio: $scope.maintainAspectRatio,
title: {
display: true
},
legend: {
display: true,
onClick: $scope.onLegendClick
},
scales: {
xAxes: [{
stacked: true
}],
yAxes: [
{
id: 'y-axis',
stacked: true
},
{
id: 'y-axis-delta',
stacked: false
},
{
id: 'y-axis-delta-right',
stacked: false,
display: $scope.displayRightAxis,
position: 'right',
gridLines: {
drawOnChartArea: false
}
}
]
},
tooltips: {
enabled: true,
mode: 'index',
callbacks: {
label: function(tooltipItems, data) {
return data.datasets[tooltipItems.datasetIndex].label +
': ' + formatInteger(tooltipItems.yLabel);
}
}
}
};
$scope.init = function(e, state) {
if (state && state.stateParams) {
// Manage URL parameters
}
};
$scope.load = function(updateTimePct) {
return $q.all([
// Get i18n keys (chart title, series labels, date patterns)
$translate($scope.charts.reduce(function(res, chart) {
return res.concat(chart.series.reduce(function(res, serie) {
return res.concat(serie.label);
}, [chart.title]));
}, [
'COMMON.DATE_PATTERN',
'COMMON.DATE_SHORT_PATTERN',
'COMMON.DATE_MONTH_YEAR_PATTERN'
])),
// get Data
gpData.docstat.get($scope.formData)
])
.then(function(result) {
var translations = result[0];
var datePatterns = {
hour: translations['COMMON.DATE_PATTERN'],
day: translations['COMMON.DATE_SHORT_PATTERN'],
month: translations['COMMON.DATE_MONTH_YEAR_PATTERN']
};
result = result[1];
if (!result || !result.times) return; // no data
$scope.times = result.times;
//console.debug(result);
// Labels
var labelPattern = datePatterns[$scope.formData.rangeDuration];
$scope.labels = _.map(result.times, function(time) {
return moment.unix(time).local().format(labelPattern);
});
// Update range options with received values
$scope.updateRange(result.times[0], result.times[result.times.length-1], updateTimePct);
$scope.setScale($scope.scale);
// For each chart
_.forEach($scope.charts, function(chart){
// Prepare chart data
var usedYAxisIDs = {};
chart.data = _.map(chart.series, function(serie) {
usedYAxisIDs[serie.yAxisID||'y-axis'] = true;
// If 'delta' serie, then compute delta from the source serie
if (serie.key.endsWith('_delta')) {
var key = serie.key.substring(0, serie.key.length - '_delta'.length);
return getDeltaFromValueArray(result[key]) || [];
}
return result[serie.key]||[];
});
// Options (with title)
chart.options = angular.copy($scope.defaultChartOptions);
chart.options.title.text = translations[chart.title];
// Remove unused yAxis
chart.options.scales.yAxes = chart.options.scales.yAxes.reduce(function(res, yAxe){
return usedYAxisIDs[yAxe.id] ? res.concat(yAxe) : res;
}, []);
// Series datasets
chart.datasetOverride = _.map(chart.series, function(serie) {
return {
yAxisID: serie.yAxisID || 'y-axis',
type: serie.type || 'line',
label: translations[serie.label],
fill: serie.type !== 'line',
borderWidth: 2,
pointRadius: serie.type !== 'line' ? 0 : 2,
pointHitRadius: 4,
pointHoverRadius: 3,
borderColor: serie.color,
backgroundColor: serie.color,
pointBackgroundColor: serie.color,
pointBorderColor: serie.color,
pointHoverBackgroundColor: serie.pointHoverBackgroundColor||serie.color,
pointHoverBorderColor: serie.pointHoverBorderColor||gpColor.rgba.white()
};
});
});
});
};
$scope.onChartClick = function(data, e, item) {
if (!item) return;
var chart = _.find($scope.charts , function(chart) {
return ($scope.chartIdPrefix + chart.id) == item._chart.canvas.id;
});
var serie = chart.series[item._datasetIndex];
if (serie && serie.clickState && serie.clickState.name) {
var stateParams = serie.clickState.params ? angular.copy(serie.clickState.params) : {};
// Compute query
var from = $scope.times[item._index];
var to = moment.unix(from).utc().add(1, $scope.formData.rangeDuration).unix();
stateParams.q = 'time:>={0} AND time:<{1}'.format(from, to);
return $state.go(serie.clickState.name, stateParams);
}
else {
console.debug('Click on item index={0} on range [{1},{2}]'.format(item._index, from, to));
}
};
function getDeltaFromValueArray(values) {
if (!values) return undefined;
var previousValue;
return _.map(values, function(value) {
var newValue = (value !== undefined && previousValue !== undefined) ? (value - (previousValue || value)) : undefined;
previousValue = value;
return newValue;
});
}
}
GpSynchroController.$inject = ['$scope', '$controller', '$q', '$translate', 'gpColor', 'gpData', '$filter'];
angular.module('cesium.graph.synchro.controllers', ['chart.js', 'cesium.graph.services', 'cesium.graph.common.controllers'])
.config(['$stateProvider', 'PluginServiceProvider', 'csConfig', function($stateProvider, PluginServiceProvider, csConfig) {
'ngInject';
$stateProvider
.state('app.doc_synchro_lg', {
url: "/data/synchro?stepUnit&t&hide&scale",
views: {
'menuContent': {
templateUrl: "plugins/graph/templates/synchro/view_stats.html",
controller: "GpSynchroCtrl"
}
}
});
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
// TODO: add buttons to link with doc stats
}
}])
.controller('GpSynchroCtrl', GpSynchroController)
;
function GpSynchroController($scope, $controller, $q, $translate, gpColor, gpData, $filter) {
'ngInject';
// Initialize the super class and extend it.
angular.extend(this, $controller('GpCurrencyAbstractCtrl', {$scope: $scope}));
$scope.formData.rangeDuration = 'month';
$scope.hiddenDatasets = [];
$scope.charts = [
// Execution: number of doc
{
id: 'count',
title: 'GRAPH.SYNCHRO.COUNT.TITLE',
series: [
{
key: 'inserts',
type: 'bar',
label: 'GRAPH.SYNCHRO.COUNT.INSERTS',
color: gpColor.rgba.royal(),
pointHoverBackgroundColor: gpColor.rgba.royal()
},
{
key: 'updates',
type: 'bar',
label: 'GRAPH.SYNCHRO.COUNT.UPDATES',
color: gpColor.rgba.calm(),
pointHoverBackgroundColor: gpColor.rgba.calm()
},
{
key: 'deletes',
type: 'bar',
label: 'GRAPH.SYNCHRO.COUNT.DELETES',
color: gpColor.rgba.assertive(0.5),
pointHoverBackgroundColor: gpColor.rgba.assertive()
}
]
},
// Execution: number of peers
{
id: 'peer',
title: 'GRAPH.SYNCHRO.PEER.TITLE',
series: [
{
key: 'ES_USER_API',
label: 'GRAPH.SYNCHRO.PEER.ES_USER_API',
color: gpColor.rgba.royal(),
pointHoverBackgroundColor: gpColor.rgba.royal()
},
{
key: 'ES_SUBSCRIPTION_API',
label: 'GRAPH.SYNCHRO.PEER.ES_SUBSCRIPTION_API',
color: gpColor.rgba.gray(0.5),
pointHoverBackgroundColor: gpColor.rgba.gray()
}
]
},
// Execution: number of peers
{
id: 'performance',
title: 'GRAPH.SYNCHRO.PERFORMANCE.TITLE',
series: [
{
key: 'duration',
type: 'bar',
label: 'GRAPH.SYNCHRO.PERFORMANCE.DURATION',
color: gpColor.rgba.gray(0.5),
pointHoverBackgroundColor: gpColor.rgba.gray()
}
]
}
];
var formatInteger = $filter('formatInteger');
$scope.defaultChartOptions = {
responsive: true,
maintainAspectRatio: $scope.maintainAspectRatio,
title: {
display: true
},
legend: {
display: true,
onClick: $scope.onLegendClick
},
scales: {
xAxes: [{
stacked: true
}],
yAxes: [
{
stacked: true,
id: 'y-axis'
}
]
},
tooltips: {
enabled: true,
mode: 'index',
callbacks: {
label: function(tooltipItems, data) {
return data.datasets[tooltipItems.datasetIndex].label +
': ' + formatInteger(tooltipItems.yLabel);
}
}
}
};
$scope.init = function(e, state) {
if (state && state.stateParams) {
// Manage URL parameters
}
};
$scope.load = function(updateTimePct) {
return $q.all([
// Get i18n keys (chart title, series labels, date patterns)
$translate($scope.charts.reduce(function(res, chart) {
return res.concat(chart.series.reduce(function(res, serie) {
return res.concat(serie.label);
}, [chart.title]));
}, [
'COMMON.DATE_PATTERN',
'COMMON.DATE_SHORT_PATTERN',
'COMMON.DATE_MONTH_YEAR_PATTERN'
])),
// get Data
gpData.synchro.execution.get($scope.formData)
])
.then(function(result) {
var translations = result[0];
var datePatterns = {
hour: translations['COMMON.DATE_PATTERN'],
day: translations['COMMON.DATE_SHORT_PATTERN'],
month: translations['COMMON.DATE_MONTH_YEAR_PATTERN']
};
result = result[1];
if (!result || !result.times) return; // no data
$scope.times = result.times;
// Labels
var labelPattern = datePatterns[$scope.formData.rangeDuration];
$scope.labels = result.times.reduce(function(res, time) {
return res.concat(moment.unix(time).local().format(labelPattern));
}, []);
// Update range options with received values
$scope.updateRange(result.times[0], result.times[result.times.length-1], updateTimePct);
$scope.setScale($scope.scale);
// For each chart
_.forEach($scope.charts, function(chart){
// Data
chart.data = [];
_.forEach(chart.series, function(serie){
chart.data.push(result[serie.key]||[]);
});
// Options (with title)
chart.options = angular.copy($scope.defaultChartOptions);
chart.options.title.text = translations[chart.title];
// Series datasets
chart.datasetOverride = chart.series.reduce(function(res, serie) {
return res.concat({
yAxisID: 'y-axis',
type: serie.type || 'line',
label: translations[serie.label],
fill: true,
borderWidth: 2,
pointRadius: 0,
pointHitRadius: 4,
pointHoverRadius: 3,
borderColor: serie.color,
backgroundColor: serie.color,
pointBackgroundColor: serie.color,
pointBorderColor: serie.color,
pointHoverBackgroundColor: serie.pointHoverBackgroundColor||serie.color,
pointHoverBorderColor: serie.pointHoverBorderColor||gpColor.rgba.white()
});
}, []);
});
});
};
}
GpNetworkViewExtendController.$inject = ['$scope', 'PluginService', 'esSettings'];
GpPeerViewExtendController.$inject = ['$scope', '$timeout', 'PluginService', 'esSettings', 'csCurrency', 'gpData'];
angular.module('cesium.graph.network.controllers', ['chart.js', 'cesium.graph.services'])
.config(['$stateProvider', 'PluginServiceProvider', 'csConfig', function($stateProvider, PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
PluginServiceProvider
.extendState('app.es_network', {
points: {
'buttons': {
templateUrl: "plugins/graph/templates/network/view_es_network_extend.html",
controller: 'GpNetworkViewExtendCtrl'
}
}
})
.extendState('app.view_es_peer', {
points: {
'general': {
templateUrl: "plugins/graph/templates/network/view_es_peer_extend.html",
controller: 'ESExtensionCtrl'
}
}
})
;
}
}])
.controller('GpNetworkViewExtendCtrl', GpNetworkViewExtendController)
.controller('GpPeerViewExtendCtrl', GpPeerViewExtendController)
;
function GpNetworkViewExtendController($scope, PluginService, esSettings) {
'ngInject';
$scope.extensionPoint = PluginService.extensions.points.current.get();
$scope.enable = esSettings.isEnable();
esSettings.api.state.on.changed($scope, function(enable) {
$scope.enable = enable;
});
}
function GpPeerViewExtendController($scope, $timeout, PluginService, esSettings, csCurrency, gpData) {
'ngInject';
$scope.extensionPoint = PluginService.extensions.points.current.get();
$scope.enable = esSettings.isEnable();
$scope.loading = true;
$scope.node = $scope.node || {};
esSettings.api.state.on.changed($scope, function(enable) {
$scope.enable = enable;
});
/**
* Enter into the view
* @param e
* @param state
*/
$scope.enter = function(e, state) {
if (!$scope.node.currency && state && state.stateParams && state.stateParams.currency) { // Currency parameter
$scope.node.currency = state.stateParams.currency;
}
// Make sure there is currency, or load if not
if (!$scope.node.currency) {
return csCurrency.get()
.then(function(currency) {
$scope.node.currency = currency ? currency.name : null;
return $scope.enter(e, state);
});
}
// Make sure there is pubkey, or wait for parent load to be finished
if (!$scope.node.pubkey) {
return $timeout(function () {
return $scope.enter(e, state);
}, 500);
}
// load
return $scope.load();
};
$scope.$on('$csExtension.enter', $scope.enter);
$scope.load = function() {
if (!$scope.node.currency && !$scope.node.pubkey) return;
console.info("[Graph] [peer] Loading blocks count for [{0}]".format($scope.node.pubkey.substr(0, 8)));
return gpData.node.blockCount($scope.node.currency, $scope.node.pubkey)
.then(function(count) {
$scope.blockCount = count;
$scope.loading = false;
});
};
}
angular.module('cesium.map.plugin', [
'ui-leaflet',
// Services
'cesium.map.services',
// Controllers
'cesium.map.user.controllers'
])
// Configure plugin
.config(function() {
'ngInject';
// Define icon prefix for AwesomeMarker (a Leaflet plugin)
L.AwesomeMarkers.Icon.prototype.options.prefix = 'ion';
});
angular.module('cesium.map.services', [
// Services
'cesium.map.utils.services'
])
;
angular.module('cesium.map.utils.services', ['cesium.services', 'ui-leaflet'])
.factory('MapUtils', ['$timeout', '$q', '$translate', 'leafletData', 'csConfig', 'csSettings', 'esGeo', 'UIUtils', 'leafletHelpers', function($timeout, $q, $translate, leafletData, csConfig, csSettings, esGeo, UIUtils, leafletHelpers) {
'ngInject';
var
googleApiKey = csConfig.plugins && csConfig.plugins.es && csConfig.plugins.es.googleApiKey;
constants = {
locations: {
FRANCE: {
lat: 46.5588603, lng: 4.229736328124999, zoom: 6
}
},
LOCALIZE_ZOOM: 14
};
constants.DEFAULT_CENTER = csSettings.data && csSettings.data.plugins && csSettings.data.plugins.map && csSettings.data.plugins.map.center || constants.locations.FRANCE;
function initMap(options){
options = angular.merge({
center: angular.copy(constants.DEFAULT_CENTER),
defaults: {
scrollWheelZoom: true
},
layers: {
baselayers: {
osm: {
name: 'OpenStreetMap',
type: 'xyz',
url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
layerOptions: {
subdomains: ["a", "b", "c"],
attribution: "&copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>",
continuousWorld: true
}
},
cycle: {
name: "Google map",
type: "xyz",
url: 'http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key='+googleApiKey,
layerOptions: {
subdomains: ['mt0','mt1','mt2','mt3'],
attribution: "&copy; <a href=\"http://google.com/copyright\">Google</a>",
continuousWorld: true
}
}
}
},
controls: {
custom: []
}
}, options || {});
// Translate overlays name, if any
var overlaysNames;
if (options.layers.overlays) {
overlaysNames = _.keys(options.layers.overlays).reduce(function (res, key) {
return res.concat(options.layers.overlays[key].name);
}, []);
$translate(overlaysNames).then(function (translations) {
// Translate overlay names
_.keys(options.layers.overlays || {}).forEach(function (key) {
options.layers.overlays[key].name = translations[options.layers.overlays[key].name];
});
});
}
return options;
}
function updateMapCenter(map, center) {
if (isSameCenter(center, map)) return $q.when();
return $timeout(function () {
map.invalidateSize();
map._resetView(center, center.zoom, true);
}, 300);
}
function getCenter(options) {
if (!options) return;
var center;
if (options.lat) {
center = {};
center.lat = parseFloat(options.lat);
}
if (options.lng || options.lon) {
center = center || {};
center.lng = parseFloat(options.lng || options.lon);
}
if (options.zoom) {
center = center || {};
center.zoom = parseFloat(options.zoom);
}
if (!center) return;
// If missing some properties, complete with defaults
if (!leafletHelpers.isValidCenter(center)) {
center = angular.merge({}, constants.DEFAULT_CENTER, center);
}
return center;
}
function isSameCenter(center, map) {
return leafletHelpers.isSameCenterOnMap(center, map);
}
function isDefaultCenter(centerModel) {
var mapCenter = constants.DEFAULT_CENTER;
if (centerModel.lat && centerModel.lng && mapCenter.lat.toFixed(4) === centerModel.lat.toFixed(4) && mapCenter.lng.toFixed(4) === centerModel.lng.toFixed(4) && mapCenter.zoom === centerModel.zoom) {
return true;
}
return false;
}
// Create a default serach control, with default options
function initSearchControl(options) {
options = options || {};
options.initial = angular.isDefined(options.initial) ? options.initial : false;
options.marker = angular.isDefined(options.marker) ? options.marker : false;
options.propertyName = angular.isDefined(options.propertyName) ? options.propertyName : 'title';
options.position = angular.isDefined(options.position) ? options.position : 'topleft';
options.zoom = angular.isDefined(options.zoom) ? options.zoom : constants.LOCALIZE_ZOOM;
options.markerLocation = angular.isDefined(options.markerLocation) ? options.markerLocation : true;
var translatePromise = $translate(['MAP.COMMON.SEARCH_DOTS', 'COMMON.SEARCH_NO_RESULT']);
return {
// Simulate an addTo function, but wait for end of translations job
addTo: function (map) {
translatePromise.then(function (translations) {
L.control.search(angular.merge(options, {
textPlaceholder: translations['MAP.COMMON.SEARCH_DOTS'],
textErr: translations['COMMON.SEARCH_NO_RESULT']
})).addTo(map);
});
}
};
}
function initLocalizeMeControl() {
return L.easyButton('icon ion-android-locate', function(btn, map){
return esGeo.point.current()
.then(function(res) {
map.invalidateSize();
map._resetView({
lat: res.lat,
lng: res.lon
}, constants.LOCALIZE_ZOOM, true);
})
.catch(UIUtils.onError('MAP.ERROR.LOCALIZE_ME_FAILED'));
});
}
return {
map: initMap,
center: {
get: getCenter,
isSame: isSameCenter,
isDefault: isDefaultCenter
},
updateCenter: updateMapCenter,
control: {
search: initSearchControl,
localizeMe: initLocalizeMeControl
},
constants: constants
};
}]);
angular.module('cesium.map.user.controllers', ['cesium.services', 'cesium.map.services'])
.config(['PluginServiceProvider', 'csConfig', function(PluginServiceProvider, csConfig) {
'ngInject';
var enable = csConfig.plugins && csConfig.plugins.es;
if (enable) {
PluginServiceProvider
.extendState('app.user_edit_profile', {
points: {
'after-position': {
templateUrl: 'plugins/map/templates/user/edit_profile_extend.html',
controller: 'MapEditProfileViewCtrl'
}
}
});
}
}])
// [NEW] Manage events from the page #/app/wot/map
.controller('MapEditProfileViewCtrl', ['$scope', '$timeout', '$q', 'MapUtils', '$translate', function($scope, $timeout, $q, MapUtils, $translate) {
'ngInject';
var listeners = [];
$scope.mapId = 'map-user-profile-' + $scope.$id;
$scope.map = MapUtils.map({
markers: {},
center: {
zoom: 13
},
defaults: {
tileLayerOptions: {
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}
}
});
$scope.loading = true;
$scope.enter = function(e, state) {
// Wait parent controller load the profile
if (!$scope.formData || !$scope.formData.title) {
return $timeout($scope.enter, 500);
}
$scope.loading = true;
return $scope.load();
};
$scope.$on('$csExtension.enter', $scope.enter);
$scope.$on('$ionicParentView.enter', $scope.enter);
$scope.load = function() {
// no position define: remove existing listener
if (!$scope.formData.geoPoint || !$scope.formData.geoPoint.lat || !$scope.formData.geoPoint.lon) {
_.forEach(listeners, function(listener){
listener(); // unlisten
});
listeners = [];
delete $scope.map.markers.geoPoint;
$scope.loading = false;
return $q.when();
}
// If no marker exists on map: create it
if (!$scope.map.markers.geoPoint) {
return $translate('MAP.PROFILE.MARKER_HELP')
.then(function(helpText) {
$scope.map.markers.geoPoint = {
message: helpText,
lat: parseFloat($scope.formData.geoPoint.lat),
lng: parseFloat($scope.formData.geoPoint.lon),
draggable: true,
focus: true
};
angular.extend($scope.map.center, {
lat: $scope.map.markers.geoPoint.lat,
lng: $scope.map.markers.geoPoint.lng
});
// Listening changes
var listener = $scope.$watch('map.markers.geoPoint', function() {
if ($scope.loading) return;
if ($scope.map.markers.geoPoint && $scope.map.markers.geoPoint.lat && $scope.map.markers.geoPoint.lng) {
$scope.formData.geoPoint = $scope.formData.geoPoint || {};
$scope.formData.geoPoint.lat = $scope.map.markers.geoPoint.lat;
$scope.formData.geoPoint.lon = $scope.map.markers.geoPoint.lng;
}
}, true);
listeners.push(listener);
// Make sure map appear, if shown later
if (!$scope.ionItemClass) {
$scope.ionItemClass = 'done in';
}
$scope.loading = false;
});
}
// Marker exists: update lat/lon
else {
$scope.map.markers.geoPoint.lat = $scope.formData.geoPoint.lat;
$scope.map.markers.geoPoint.lng = $scope.formData.geoPoint.lon;
}
};
$scope.$watch('formData.geoPoint', function() {
if ($scope.loading) return;
$scope.load();
}, true);
}]);
// Ionic Starter App
// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
// 'starter.controllers' is found in controllers.js
angular.module('gchange', ['ionic', 'ionic-material', 'ngMessages', 'pascalprecht.translate',
'ngApi', 'angular-cache', 'angular.screenmatch', 'angular.bind.notifier', 'ImageCropper', 'ngFileSaver', 'ngIdle',
'FBAngular', // = angular-fullscreen
'cesium.plugins',
'cesium.filters', 'cesium.config', 'cesium.platform', 'cesium.controllers', 'cesium.templates', 'cesium.translations', 'cesium.components', 'cesium.directives'
])
// Override the automatic sync between location URL and state
// (see watch event $locationChangeSuccess in the run() function bellow)
.config(['$urlRouterProvider', function ($urlRouterProvider) {
'ngInject';
$urlRouterProvider.deferIntercept();
}])
.run(['$rootScope', '$translate', '$state', '$window', 'ionicReady', '$urlRouter', 'Device', 'UIUtils', '$ionicConfig', 'PluginService', 'csPlatform', function($rootScope, $translate, $state, $window, ionicReady, $urlRouter, Device, UIUtils, $ionicConfig, PluginService,
csPlatform) {
'ngInject';
// Must be done before any other $stateChangeStart listeners
csPlatform.disableChangeState();
var preventStateChange = false; // usefull to avoid duplicate login, when a first page with auth
$rootScope.$on('$stateChangeStart', function (event, next, nextParams, fromState) {
if (event.defaultPrevented) return;
var skip = !next.data || $rootScope.tour || event.currentScope.tour; // disabled for help tour
if (skip) return;
if (preventStateChange) {
event.preventDefault();
return;
}
// removeIf(android)
// removeIf(ios)
// -- Automatic redirection to large state (if define) (keep this code for platforms web and ubuntu build)
if (next.data.large && !UIUtils.screen.isSmall()) {
event.preventDefault();
$state.go(next.data.large, nextParams);
return;
}
// endRemoveIf(ios)
// endRemoveIf(android)
});
// Prevent $urlRouter's default handler from firing (don't sync ui router)
$rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
if ($state.current.data && $state.current.data.silentLocationChange === true) {
// Skipping propagation, because same URL, and state configured with 'silentLocationChange' options
var sameUrl = oldUrl && (oldUrl.split('?')[0] === newUrl.split('?')[0]);
if (sameUrl) event.preventDefault();
}
});
// Configures $urlRouter's listener *after* the previous listener
$urlRouter.listen();
// Start plugins eager services
PluginService.start();
ionicReady().then(function() {
if (ionic.Platform.isIOS()) {
if(window.StatusBar) {
// fix font color not white on iOS 11+
StatusBar.styleLightContent();
}
}
});
}])
;
window.ionic.Platform.ready(function() {
angular.bootstrap(document, ['gchange']);
});
angular.module('cesium.components', [])
.component('csAvatar', {
bindings: {
avatar: '<',
icon: '@'
},
template:
'<i ng-if="!$ctrl.avatar" class="item-image icon {{$ctrl.icon}}"></i>' +
'<i ng-if="$ctrl.avatar" class="item-image avatar" style="background-image: url({{::$ctrl.avatar.src}})"></i>'
})
.component('csBadgeCertification', {
bindings: {
requirements: '=',
parameters: '<',
csId: '@'
},
templateUrl: 'templates/common/badge_certification_count.html'
})
.component('csBadgeGivenCertification', {
bindings: {
identity: '=',
parameters: '<',
csId: '@'
},
templateUrl: 'templates/common/badge_given_certification_count.html'
})
.component('csSortIcon', {
bindings: {
asc: '=',
sort: '=',
toggle: '<'
},
template:
'<i class="ion-chevron-up" ng-class="{gray: !$ctrl.asc || $ctrl.sort != $ctrl.toggle}" style="position: relative;left : 5px; top:-5px; font-size: 9px;"></i>' +
'<i class="ion-chevron-down" ng-class="{gray : $ctrl.asc || $ctrl.sort != $ctrl.toggle}" style="position: relative; left: -2.6px; top: 3px; font-size: 9px;"></i>'
})
.component('csRemovableSelectionItem', {
transclude: true,
controller: function(){
this.$onInit = function(){
console.log("$onInit called: ", this);
};
this.remove = function(){
console.log("remove called: ", this);
};
},
template:
'<div >' +
' <ng-transclude></ng-transclude>' +
' <i class="icon ion-close" ng-click="$ctrl.remove();"></i>' +
'</div>'
})
;
angular.module('cesium.directives', [])
// Add new compare-to directive (need for form validation)
.directive("compareTo", function() {
return {
require: "?ngModel",
link: function(scope, element, attributes, ngModel) {
if (ngModel && attributes.compareTo) {
ngModel.$validators.compareTo = function(modelValue) {
return modelValue == scope.$eval(attributes.compareTo);
};
scope.$watch(attributes.compareTo, function() {
ngModel.$validate();
});
}
}
};
})
// Add new different-to directive (need for form validation)
.directive("differentTo", function() {
return {
require: "?ngModel",
link: function(scope, element, attributes, ngModel) {
if (ngModel && attributes.differentTo) {
ngModel.$validators.differentTo = function(modelValue) {
return modelValue != scope.$eval(attributes.differentTo);
};
scope.$watch(attributes.differentTo, function() {
ngModel.$validate();
});
}
}
};
})
.directive('numberFloat', function() {
var NUMBER_REGEXP = new RegExp('^[0-9]+([.,][0-9]+)?$');
return {
require: '?ngModel',
link: function(scope, element, attributes, ngModel) {
if (ngModel) {
ngModel.$validators.numberFloat = function(value) {
return ngModel.$isEmpty(value) || NUMBER_REGEXP.test(value);
};
}
}
};
})
.directive('numberInt', function() {
var INT_REGEXP = new RegExp('^[0-9]+$');
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
if (ngModel) {
ngModel.$validators.numberInt = function (value) {
return ngModel.$isEmpty(value) || INT_REGEXP.test(value);
};
}
}
};
})
.directive('email', function() {
var EMAIL_REGEXP = new RegExp('^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$');
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
if (ngModel) {
ngModel.$validators.email = function (value) {
return ngModel.$isEmpty(value) || EMAIL_REGEXP.test(value);
};
}
}
};
})
.directive('requiredIf', function() {
return {
require: '?ngModel',
link: function(scope, element, attributes, ngModel) {
if (ngModel && attributes.requiredIf) {
ngModel.$validators.required = function(value) {
return !(scope.$eval(attributes.requiredIf)) || !ngModel.$isEmpty(value);
};
scope.$watch(attributes.requiredIf, function() {
ngModel.$validate();
});
}
}
};
})
.directive('geoPoint', function() {
return {
require: '?ngModel',
link: function(scope, element, attributes, ngModel) {
if (ngModel) {
ngModel.$validators.geoPoint = function(value) {
return ngModel.$isEmpty(value) ||
// twice are defined
(angular.isDefined(value.lat) && angular.isDefined(value.lon)) ||
// or twice are NOT defined (=empty object - can be useful to override data in ES node)
(angular.isUndefined(value.lat) && angular.isUndefined(value.lon));
};
}
}
};
})
// Add a copy-on-click directive
.directive('copyOnClick', ['$window', '$document', 'Device', 'UIUtils', function ($window, $document, Device, UIUtils) {
'ngInject';
return {
restrict: 'A',
link: function (scope, element, attrs) {
var showCopyPopover = function (event) {
var value = attrs.copyOnClick;
if (value && Device.clipboard.enable) {
// copy to clipboard
Device.clipboard.copy(value)
.then(function(){
UIUtils.toast.show('INFO.COPY_TO_CLIPBOARD_DONE');
})
.catch(UIUtils.onError('ERROR.COPY_CLIPBOARD'));
}
else if (value) {
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
},
autoselect: '.popover-copy ' + (rows <= 1 ? 'input' : 'textarea')
});
}
};
element.bind('click', showCopyPopover);
element.bind('hold', showCopyPopover);
}
};
}])
// Add a select-on-click directive
.directive('selectOnClick', ['$window', function ($window) {
'ngInject';
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.bind('click', function () {
if ($window.getSelection && !$window.getSelection().toString() && this.value) {
this.setSelectionRange(0, this.value.length);
}
});
}
};
}])
.directive('activeLink', ['$location', function ($location) {
'ngInject';
return {
restrict: 'A',
link: function(scope, element, attrs, controller) {
var clazz = attrs.activeLink;
var path;
if (attrs.activeLinkPathPrefix) {
path = attrs.activeLinkPathPrefix.substring(1); //hack because path does not return including hashbang
scope.location = $location;
scope.$watch('location.path()', function (newPath) {
if (newPath && newPath.indexOf(path) === 0) {
element.addClass(clazz);
} else {
element.removeClass(clazz);
}
});
}
else if (attrs.href) {
path = attrs.href.substring(1); //hack because path does not return including hashbang
scope.location = $location;
scope.$watch('location.path()', function (newPath) {
if (newPath && newPath == path) {
element.addClass(clazz);
} else {
element.removeClass(clazz);
}
});
}
}
};
}])
// All this does is allow the message
// to be sent when you tap return
.directive('input', ['$timeout', function($timeout) {
return {
restrict: 'E',
scope: {
'returnClose': '=',
'onReturn': '&',
'onFocus': '&',
'onBlur': '&'
},
link: function(scope, element, attr) {
element.bind('focus', function(e) {
if (scope.onFocus) {
$timeout(function() {
scope.onFocus();
});
}
});
element.bind('blur', function(e) {
if (scope.onBlur) {
$timeout(function() {
scope.onBlur();
});
}
});
element.bind('keydown', function(e) {
if (e.which == 13) {
if (scope.returnClose) element[0].blur();
if (scope.onReturn) {
$timeout(function() {
scope.onReturn();
});
}
}
});
}
};
}])
.directive('trustAsHtml', ['$sce', '$compile', '$parse', function($sce, $compile, $parse){
return {
restrict: 'A',
compile: function (tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.trustAsHtml);
var ngBindHtmlWatch = $parse(tAttrs.trustAsHtml, function getStringValue(value) {
return (value || '').toString();
});
$compile.$$addBindingClass(tElement);
return function ngBindHtmlLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.trustAsHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
// we re-evaluate the expr because we want a TrustedValueHolderType
// for $sce, not a string
element.html($sce.getTrustedHtml($sce.trustAsHtml(ngBindHtmlGetter(scope))) || '');
$compile(element.contents())(scope);
});
};
}
};
}])
/**
* Close the current modal
*/
.directive('modalClose', ['$ionicHistory', '$timeout', function($ionicHistory, $timeout) {
return {
restrict: 'AC',
link: function($scope, $element) {
$element.bind('click', function() {
if ($scope.closeModal) {
$ionicHistory.nextViewOptions({
historyRoot: true,
disableAnimate: true,
expire: 300
});
// if no transition in 300ms, reset nextViewOptions
// the expire should take care of it, but will be cancelled in some
// cases. This directive is an exception to the rules of history.js
$timeout( function() {
$ionicHistory.nextViewOptions({
historyRoot: false,
disableAnimate: false
});
}, 300);
$scope.closeModal();
}
});
}
};
}])
/**
* Plugin extension point (see services/plugin-services.js)
*/
.directive('csExtensionPoint', ['$state', '$compile', '$controller', '$templateCache', 'PluginService', function ($state, $compile, $controller, $templateCache, PluginService) {
var getTemplate = function(extensionPoint) {
var template = extensionPoint.templateUrl ? $templateCache.get(extensionPoint.templateUrl) : extensionPoint.template;
if (!template) {
console.error('[plugin] Could not found template for extension :' + (extensionPoint.templateUrl ? extensionPoint.templateUrl : extensionPoint.template));
return '';
}
if (extensionPoint.controller) {
template = '<ng-controller ng-controller="'+extensionPoint.controller+'">' + template + '</div>';
}
return template;
};
var compiler = function(tElement, tAttributes) {
if (angular.isDefined(tAttributes.name)) {
var extensionPoints = PluginService.extensions.points.getActivesByName(tAttributes.name);
if (extensionPoints.length > 0) {
tElement.html("");
_.forEach(extensionPoints, function(extensionPoint){
tElement.append(getTemplate(extensionPoint));
});
}
}
return {
pre: function(scope, iElement, iAttrs){
PluginService.extensions.points.current.set(iAttrs.name);
},
post: function(){
PluginService.extensions.points.current.set();
}
};
};
return {
restrict: "E",
compile: compiler,
scope: {
content:'='
}
};
}])
.directive('onReadFile', ['$parse', function ($parse) {
return {
restrict: 'A',
scope: false,
link: function(scope, element, attrs) {
var fn = $parse(attrs.onReadFile);
element.on('change', function(onChangeEvent) {
var reader = new FileReader();
var fileData = {
name: this.files[0].name,
size: this.files[0].size,
type: this.files[0].type
};
reader.onload = function(onLoadEvent) {
scope.$applyAsync(function() {
fn(scope, {
file: {
fileContent: onLoadEvent.target.result,
fileData : fileData}
});
});
};
reader.readAsText((onChangeEvent.srcElement || onChangeEvent.target).files[0]);
});
}
};
}])
.directive("dropzone", ['$parse', function($parse) {
return {
restrict: 'A',
scope: false,
link: function(scope, elem, attrs) {
var fn = $parse(attrs.dropzone);
elem.bind('dragover', function (e) {
e.stopPropagation();
e.preventDefault();
});
elem.bind('dragenter', function(e) {
e.stopPropagation();
e.preventDefault();
});
elem.bind('dragleave', function(e) {
e.stopPropagation();
e.preventDefault();
});
elem.bind('drop', function(e) {
e.stopPropagation();
e.preventDefault();
var fileData = {
name: e.dataTransfer.files[0].name,
size: e.dataTransfer.files[0].size,
type: e.dataTransfer.files[0].type
};
var reader = new FileReader();
reader.onload = function(onLoadEvent) {
scope.$apply(function () {
fn(scope, {
file: {
fileContent: onLoadEvent.target.result,
fileData : fileData}
});
});
};
reader.readAsText(e.dataTransfer.files[0]);
});
}
};
}])
;
// Cesium filters
angular.module('cesium.filters', ['cesium.config', 'cesium.platform', 'pascalprecht.translate', 'cesium.translations'
])
.service('filterTranslations', ['$rootScope', 'csPlatform', 'csSettings', '$translate', function($rootScope, csPlatform, csSettings, $translate) {
'ngInject';
var
started = false,
startPromise,
that = this;
// Update some translations, when locale changed
function onLocaleChange() {
console.debug('[filter] Loading translations for locale [{0}]'.format($translate.use()));
return $translate(['COMMON.DATE_PATTERN', 'COMMON.DATE_SHORT_PATTERN', 'COMMON.UD'])
.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_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_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.UD = translations['COMMON.UD'];
if (that.UD === 'COMMON.UD') {
that.UD = 'UD';
}
});
}
that.ready = function() {
if (started) return $q.when(data);
return startPromise || that.start();
};
that.start = function() {
startPromise = csPlatform.ready()
.then(onLocaleChange)
.then(function() {
started = true;
csSettings.api.locale.on.changed($rootScope, onLocaleChange, this);
});
return startPromise;
};
// Default action
that.start();
return that;
}])
.filter('formatInteger', function() {
return function(input) {
return !input ? '0' : (input < 10000000 ? numeral(input).format('0,0') : numeral(input).format('0,0.000 a'));
};
})
.filter('formatAmount', ['csConfig', 'csSettings', 'csCurrency', '$filter', function(csConfig, csSettings, csCurrency, $filter) {
var minValue = 1 / Math.pow(10, csConfig.decimalCount || 2);
var format = '0,0.0' + Array(csConfig.decimalCount || 2).join('0');
var currencySymbol = $filter('currencySymbol');
function formatRelative(input, options) {
var currentUD = options && options.currentUD ? options.currentUD : csCurrency.data.currentUD;
if (!currentUD) {
console.warn("formatAmount: currentUD not defined");
return;
}
var amount = input / currentUD;
if (Math.abs(amount) < minValue && input !== 0) {
amount = '~ 0';
}
else {
amount = numeral(amount).format(format);
}
if (options && options.currency) {
return amount + ' ' + currencySymbol(options.currency, true);
}
return amount;
}
function formatQuantitative(input, options) {
var amount = numeral(input/100).format((input > -1000000000 && input < 1000000000) ? '0,0.00' : '0,0.000 a');
if (options && options.currency) {
return amount + ' ' + currencySymbol(options.currency, false);
}
return amount;
}
return function(input, options) {
if (input === undefined) return;
return (options && angular.isDefined(options.useRelative) ? options.useRelative : csSettings.data.useRelative) ?
formatRelative(input, options) :
formatQuantitative(input, options);
};
}])
.filter('formatAmountNoHtml', ['csConfig', 'csSettings', 'csCurrency', '$filter', function(csConfig, csSettings, csCurrency, $filter) {
var minValue = 1 / Math.pow(10, csConfig.decimalCount || 2);
var format = '0,0.0' + Array(csConfig.decimalCount || 2).join('0');
var currencySymbol = $filter('currencySymbolNoHtml');
function formatRelative(input, options) {
var currentUD = options && options.currentUD ? options.currentUD : csCurrency.data.currentUD;
if (!currentUD) {
console.warn("formatAmount: currentUD not defined");
return;
}
var amount = input / currentUD;
if (Math.abs(amount) < minValue && input !== 0) {
amount = '~ 0';
}
else {
amount = numeral(amount).format(format);
}
if (options && options.currency) {
return amount + ' ' + currencySymbol(options.currency, true);
}
return amount;
}
function formatQuantitative(input, options) {
var amount = numeral(input/100).format((input > -1000000000 && input < 1000000000) ? '0,0.00' : '0,0.000 a');
if (options && options.currency) {
return amount + ' ' + currencySymbol(options.currency, false);
}
return amount;
}
return function(input, options) {
if (input === undefined) return;
return (options && angular.isDefined(options.useRelative) ? options.useRelative : csSettings.data.useRelative) ?
formatRelative(input, options) :
formatQuantitative(input, options);
};
}])
.filter('currencySymbol', ['filterTranslations', '$filter', 'csSettings', function(filterTranslations, $filter, csSettings) {
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);
};
}])
.filter('currencySymbolNoHtml', ['filterTranslations', '$filter', 'csSettings', function(filterTranslations, $filter, csSettings) {
return function(input, useRelative) {
if (!input) return '';
return (angular.isDefined(useRelative) ? useRelative : csSettings.data.useRelative) ?
(filterTranslations.UD + ' ' + $filter('abbreviate')(input)) :
$filter('abbreviate')(input);
};
}])
.filter('formatDecimal', ['csConfig', 'csCurrency', function(csConfig, csCurrency) {
var minValue = 1 / Math.pow(10, csConfig.decimalCount || 2);
var format = '0,0.0' + Array(csConfig.decimalCount || 2).join('0');
return function(input) {
if (input === undefined) return '0';
if (input === Infinity || input === -Infinity) {
console.warn("formatDecimal: division by zero ? (is currentUD defined ?) = " + csCurrency.data.currentUD);
return 'error';
}
if (Math.abs(input) < minValue) return '~ 0';
return numeral(input/*-0.00005*/).format(format);
};
}])
.filter('formatNumeral', function() {
return function(input, pattern) {
if (input === undefined) return '0';
// for DEBUG only
//if (isNaN(input)) {
// return 'NaN';
//}
if (Math.abs(input) < 0.0001) return '~ 0';
return numeral(input).format(pattern);
};
})
.filter('formatDate', ['filterTranslations', function(filterTranslations) {
return function(input) {
return input ? moment.unix(parseInt(input)).local().format(filterTranslations.DATE_PATTERN || 'YYYY-MM-DD HH:mm') : '';
};
}])
.filter('formatDateShort', ['filterTranslations', function(filterTranslations) {
return function(input) {
return input ? moment.unix(parseInt(input)).local().format(filterTranslations.DATE_SHORT_PATTERN || 'YYYY-MM-DD') : '';
};
}])
.filter('formatDateMonth', ['filterTranslations', function(filterTranslations) {
return function(input) {
return input ? moment.unix(parseInt(input)).local().format(filterTranslations.DATE_MONTH_YEAR_PATTERN || 'MMM YY') : '';
};
}])
.filter('formatDateForFile', ['filterTranslations', function(filterTranslations) {
return function(input) {
return input ? moment.unix(parseInt(input)).local().format(filterTranslations.DATE_FILE_PATTERN || 'YYYY-MM-DD') : '';
};
}])
.filter('formatTime', function() {
return function(input) {
return input ? moment.unix(parseInt(input)).local().format('HH:mm') : '';
};
})
.filter('formatFromNow', function() {
return function(input) {
return input ? moment.unix(parseInt(input)).fromNow() : '';
};
})
.filter('formatDurationTo', function() {
return function(input) {
return input ? moment.unix(moment().utc().unix() + parseInt(input)).fromNow() : '';
};
})
.filter('formatDuration', function() {
return function(input) {
return input ? moment(0).from(moment.unix(parseInt(input)), true) : '';
};
})
// 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')
) : '';
};
})
.filter('formatPeriod', function() {
return function(input) {
if (!input) {return null;}
var duration = moment(0).from(moment.unix(parseInt(input)), true);
return duration.split(' ').slice(-1)[0]; // keep only last words (e.g. remove "un" "a"...)
};
})
.filter('formatFromNowShort', function() {
return function(input) {
return input ? moment.unix(parseInt(input)).fromNow(true) : '';
};
})
.filter('capitalize', function() {
return function(input) {
if (!input) return '';
input = input.toLowerCase();
return input.substring(0,1).toUpperCase()+input.substring(1);
};
})
.filter('abbreviate', function() {
var _cache = {};
return function(input) {
var currency = input || '';
if (_cache[currency]) return _cache[currency];
if (currency.length > 3) {
var unit = '', sepChars = ['-', '_', ' '];
for (var i = 0; i < currency.length; i++) {
var c = currency[i];
if (i === 0) {
unit = (c === 'g' || c === 'G') ? 'Ğ' : c ;
}
else if (i > 0 && sepChars.indexOf(currency[i-1]) != -1) {
unit += c;
}
}
currency = unit.toUpperCase();
}
else {
currency = currency.toUpperCase();
if (currency.charAt(0) === 'G') {
currency = 'Ğ' + (currency.length > 1 ? currency.substr(1) : '');
}
}
_cache[input] = currency;
return currency;
};
})
.filter('upper', function() {
return function(input) {
if (!input) return '';
return input.toUpperCase();
};
})
.filter('formatPubkey', function() {
return function(input) {
return input ? input.substr(0,8) : '';
};
})
.filter('formatHash', function() {
return function(input) {
return input ? input.substr(0,4) + input.substr(input.length-4) : '';
};
})
.filter('formatCategory', function() {
return function(input) {
return input && input.length > 28 ? input.substr(0,25)+'...' : input;
};
})
// Convert to user friendly URL (e.g. "Like - This" -> "like-this")
.filter('formatSlug', function() {
return function(input) {
return input ? encodeURIComponent(input
.toLowerCase()
.replace(/<[^>]+>/g,'') // Remove tag (like HTML tag)
.replace(/[^\w ]+/g,'')
.replace(/ +/g,'-'))
: '';
};
})
// Convert a URI into parameter (e.g. "http://hos/path" -> "http%3A%2F%2Fhost%2Fpath")
.filter('formatEncodeURI', function() {
return function(input) {
return input ? encodeURIComponent(input): '';
};
})
.filter('truncText', function() {
return function(input, size) {
size = size || 500;
return !input || input.length <= size ? input : (input.substr(0, size) + '...');
};
})
.filter('truncUrl', function() {
return function(input, size) {
size = size || 25;
var startIndex = input && (input.startsWith('http://') ? 7 : (input.startsWith('https://') ? 8 : 0)) || 0;
startIndex = input.startsWith('www.', startIndex) ? startIndex + 4 : startIndex;
return !input || (input.length-startIndex) <= size ? input.substr(startIndex) : (input.substr(startIndex, size) + '...');
};
})
.filter('trustAsHtml', ['$sce', function($sce) {
return function(html) {
return $sce.trustAsHtml(html);
};
}])
;
angular.module('cesium.platform', ['cesium.config', 'cesium.services'])
// Translation i18n
.config(['$translateProvider', 'csConfig', function ($translateProvider, csConfig) {
'ngInject';
$translateProvider
.uniformLanguageTag('bcp47')
.determinePreferredLanguage()
// Cela fait bugger les placeholder (pb d'affichage des accents en FR)
//.useSanitizeValueStrategy('sanitize')
.useSanitizeValueStrategy(null)
.fallbackLanguage([csConfig.fallbackLanguage ? csConfig.fallbackLanguage : 'en'])
.useLoaderCache(true);
}])
.config(['$httpProvider', 'csConfig', function($httpProvider, csConfig) {
'ngInject';
// Set default timeout
$httpProvider.defaults.timeout = !!csConfig.timeout ? csConfig.timeout : 300000 /* default timeout */;
//Enable cross domain calls
$httpProvider.defaults.useXDomain = true;
//Remove the header used to identify ajax call that would prevent CORS from working
delete $httpProvider.defaults.headers.common['X-Requested-With'];
}])
.config(['$compileProvider', 'csConfig', function($compileProvider, csConfig) {
'ngInject';
$compileProvider.debugInfoEnabled(!!csConfig.debug);
}])
.config(['$animateProvider', function($animateProvider) {
'ngInject';
$animateProvider.classNameFilter( /\banimate-/ );
}])
// Configure cache (used by HTTP requests) default max age
.config(['CacheFactoryProvider', 'csConfig', function (CacheFactoryProvider, csConfig) {
'ngInject';
angular.extend(CacheFactoryProvider.defaults, { maxAge: csConfig.cacheTimeMs || 60 * 5000 /*5min*/});
}])
// Configure screen size detection
.config(['screenmatchConfigProvider', function(screenmatchConfigProvider) {
'ngInject';
screenmatchConfigProvider.config.rules = 'bootstrap';
}])
.config(['$ionicConfigProvider', function($ionicConfigProvider) {
'ngInject';
// JS scrolling need for iOs (see http://blog.ionic.io/native-scrolling-in-ionic-a-tale-in-rhyme/)
var enableJsScrolling = ionic.Platform.isIOS();
$ionicConfigProvider.scrolling.jsScrolling(enableJsScrolling);
// Configure the view cache
$ionicConfigProvider.views.maxCache(5);
}])
.config(['IdleProvider', 'csConfig', function(IdleProvider, csConfig) {
'ngInject';
IdleProvider.idle(csConfig.logoutIdle||10*60/*10min*/);
IdleProvider.timeout(csConfig.logoutTimeout||15); // display warning during 15s
}])
.factory('$exceptionHandler', ['$log', function($log) {
'ngInject';
return function(exception, cause) {
if (cause) $log.error(exception, cause);
else $log.error(exception);
};
}])
.factory('csPlatform', ['ionicReady', '$rootScope', '$q', '$state', '$translate', '$timeout', 'UIUtils', 'BMA', 'Device', 'csHttp', 'csConfig', 'csCache', 'csSettings', 'csCurrency', 'csWallet', function (ionicReady, $rootScope, $q, $state, $translate, $timeout, UIUtils,
BMA, Device, csHttp, csConfig, csCache, csSettings, csCurrency, csWallet) {
'ngInject';
var
fallbackNodeIndex = 0,
defaultSettingsNode,
started = false,
startPromise,
listeners,
removeChangeStateListener;
function disableChangeState() {
if (removeChangeStateListener) return; // make sure to call this once
var remove = $rootScope.$on('$stateChangeStart', function (event, next, nextParams, fromState) {
if (!event.defaultPrevented && next.name !== 'app.home' && next.name !== 'app.settings') {
event.preventDefault();
if (startPromise) {
startPromise.then(function() {
$state.go(next.name, nextParams);
});
}
else {
UIUtils.loading.hide();
}
}
});
// store remove listener function
removeChangeStateListener = remove;
}
function enableChangeState() {
if (removeChangeStateListener) removeChangeStateListener();
removeChangeStateListener = null;
}
// Alert user if node not reached - fix issue #
function checkBmaNodeAlive(alive) {
if (alive) return true;
// Remember the default node
defaultSettingsNode = defaultSettingsNode || csSettings.data.node;
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) : '');
// 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)
}
// 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)
})
.then(function(res) {
if (!res) return checkBmaNodeAlive(); // Loop
// 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;
}
else if (BMA.useSsl == false && (fallbackNode.useSsl || fallbackNode.port==443)) {
messageParam.new += ' (SSL)';
}
}
return $translate('CONFIRM.USE_FALLBACK_NODE', messageParam)
.then(function(msg) {
return UIUtils.alert.confirm(msg);
})
.then(function (confirm) {
if (!confirm) return;
// Only change BMA node in settings
csSettings.data.node = fallbackNode;
// Add a marker, for UI
csSettings.data.node.temporary = true;
csHttp.cache.clear();
// loop
return BMA.copy(fallbackNode)
.then(checkBmaNodeAlive);
});
});
}
function isStarted() {
return started;
}
function getLatestRelease() {
var latestRelease = csSettings.data.latestReleaseUrl && csHttp.uri.parse(csSettings.data.latestReleaseUrl);
if (latestRelease) {
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 {
version: json.name,
url: json.html_url,
isNewer: (csHttp.version.compare(csConfig.version, json.name) < 0)
};
}
})
.catch(function(err) {
// silent (just log it)
console.error('[platform] Failed to get Cesium latest version', err);
})
;
}
return $q.when();
}
function addListeners() {
listeners = [
// Listen if node changed
BMA.api.node.on.restart($rootScope, restart, this)
];
}
function removeListeners() {
_.forEach(listeners, function(remove){
remove();
});
listeners = [];
}
function ready() {
if (started) return $q.when();
return startPromise || start();
}
function restart() {
console.debug('[platform] restarting csPlatform');
return stop()
.then(function () {
return $timeout(start, 200);
});
}
function start() {
// Avoid change state
disableChangeState();
// We use 'ionicReady()' instead of '$ionicPlatform.ready()', because this one is callable many times
startPromise = ionicReady()
.then($q.all([
// Load device
Device.ready(),
// Start settings
csSettings.ready()
]))
// Load BMA
.then(function(){
return BMA.ready().then(checkBmaNodeAlive);
})
// Load currency
.then(csCurrency.ready)
// Trying to restore wallet
.then(csWallet.ready)
.then(function(){
enableChangeState();
addListeners();
startPromise = null;
started = true;
})
.catch(function(err) {
startPromise = null;
started = false;
if($state.current.name !== $rootScope.errorState) {
$state.go($rootScope.errorState, {error: 'peer'});
}
throw err;
});
return startPromise;
}
function stop() {
if (!started) return $q.when();
removeListeners();
csWallet.stop();
csCurrency.stop();
BMA.stop();
return $timeout(function() {
enableChangeState();
started = false;
startPromise = null;
}, 500);
}
return {
disableChangeState: disableChangeState,
isStarted: isStarted,
ready: ready,
restart: restart,
start: start,
stop: stop,
version: {
latest: getLatestRelease
}
};
}])
.run(['$rootScope', '$state', '$window', '$urlRouter', 'ionicReady', '$ionicPlatform', '$ionicHistory', 'Device', 'UIUtils', '$ionicConfig', 'PluginService', 'csPlatform', 'csWallet', 'csSettings', 'csConfig', 'csCurrency', function($rootScope, $state, $window, $urlRouter, ionicReady, $ionicPlatform, $ionicHistory,
Device, UIUtils, $ionicConfig, PluginService, csPlatform, csWallet, csSettings, csConfig, csCurrency) {
'ngInject';
// Allow access to service data, from HTML templates
$rootScope.config = csConfig;
$rootScope.settings = csSettings.data;
$rootScope.currency = csCurrency.data;
$rootScope.walletData = csWallet.data;
$rootScope.device = Device;
$rootScope.errorState = 'app.home';
$rootScope.smallscreen = UIUtils.screen.isSmall();
// Compute the root path
var hashIndex = $window.location.href.indexOf('#');
$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:') {
$rootScope.$on('$stateChangeStart', function (event, next, nextParams, fromState) {
var path = 'https' + $rootScope.rootPath.substr(4) + $state.href(next, nextParams);
if (csConfig.httpsModeDebug) {
console.debug('[app] [httpsMode] --- Should redirect to: ' + path);
// continue
}
else {
$window.location.href = path;
}
});
}
// endRemoveIf(device)
// We use 'ionicReady()' instead of '$ionicPlatform.ready()', because this one is callable many times
ionicReady().then(function() {
// Keyboard
if (Device.keyboard.enable) {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs)
Device.keyboard.hideKeyboardAccessoryBar(true);
// iOS: do not push header up when opening keyboard
// (see http://ionicframework.com/docs/api/page/keyboard/)
if (ionic.Platform.isIOS()) {
Device.keyboard.disableScroll(true);
}
}
// 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 + ']');
UIUtils.setEffects(false);
}
// Status bar style
if (window.StatusBar) {
console.debug("[app] Status bar plugin enable");
}
// Get latest release
csPlatform.version.latest()
.then(function(release) {
if (release && release.isNewer) {
console.info('[app] New release detected [{0}]'.format(release.version));
$rootScope.newRelease = release;
}
else {
console.info('[app] Current version [{0}] is the latest release'.format(csConfig.version));
}
});
// Prevent BACK button to exit without confirmation
$ionicPlatform.registerBackButtonAction(function(event) {
if ($ionicHistory.backView()) {
return $ionicHistory.goBack();
}
event.preventDefault();
return UIUtils.alert.confirm('CONFIRM.EXIT_APP')
.then(function (confirm) {
if (!confirm) return; // user cancelled
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, '');
};
}
// 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();
};
}
// 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;
});
};
}