astroport/www/LOVELand/gchange/dist_js/gchange.js

38046 lines
1.7 MiB
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"></cs-extension-point>\n\n\n <!-- USER Section -->\n <div class="item item-divider"></div>\n\n\n <a menu-close class="item item-icon-left" active-link="active" active-link-path-prefix="#/app/wallet" ng-click="loginAndGo(\'app.view_wallet\')" ng-class="{\'item-menu-disable\': !login}">\n <i class="icon ion-person"></i>\n {{:locale:\'MENU.ACCOUNT\'|translate}}\n </a>\n <a id="helptip-menu-btn-account"></a>\n\n <!-- Allow extension here -->\n <cs-extension-point name="menu-user"></cs-extension-point>\n\n <a menu-close class="item item-icon-left visible-xs visible-sm" active-link="active" active-link-path-prefix="#/app/settings" ui-sref="app.settings">\n <i class="icon ion-android-settings"></i>\n {{:locale:\'MENU.SETTINGS\'|translate}}\n </a>\n <a id="helptip-menu-btn-settings"></a>\n\n </ion-list>\n\n </ion-content>\n\n <!-- removeIf(device) -->\n <ion-footer-bar class="bar-stable footer hidden-xs hidden-sm">\n <a class="pull-left icon-help" menu-toggle="left" title="{{:locale:\'HOME.BTN_HELP\'|translate}}" ui-sref="app.help"></a>\n\n <a class="title gray" ng-click="showAboutModal()" title="{{:locale:\'HOME.BTN_ABOUT\'|translate}}">\n <!-- version -->\n <span title="{{:locale:\'HOME.BTN_ABOUT\'|translate}}" ng-class="{\'assertive\': $root.newRelease}">\n <!-- warning icon, if new version available -->\n <i ng-if="$root.newRelease" class="ion-alert-circled assertive"></i>\n\n {{:locale:\'COMMON.APP_VERSION\'|translate:{version: config.version} }}\n </span>\n |\n <!-- about -->\n <span translate>HOME.BTN_ABOUT</span>\n </a>\n </ion-footer-bar>\n <!-- endRemoveIf(device) -->\n </ion-side-menu>\n\n\n</ion-side-menus>\n');
$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 class="toggle toggle-royal">\n <input type="checkbox" ng-model="showUsername">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\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-->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n <form name="passwordForm" novalidate="" ng-submit="doNext(\'passwordForm\')">\n\n <div class="item item-text-wrap text-center padding hidden-xs">\n <a class="pull-right icon-help" ng-click="showHelpModal(\'join-password\')"></a>\n <span translate>ACCOUNT.NEW.PASSWORD_WARNING</span>\n </div>\n\n <div class="list" ng-init="setForm(passwordForm, \'passwordForm\')">\n\n <!-- password -->\n <div class="item item-input" ng-class="{ \'item-input-error\': passwordForm.$submitted && passwordForm.password.$invalid}">\n <span class="input-label" translate>LOGIN.PASSWORD</span>\n <input ng-if="!showPassword" name="password" type="password" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}" ng-model="formData.password" ng-change="formDataChanged()" ng-minlength="6" required>\n <input ng-if="showPassword" name="text" type="text" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}" ng-model="formData.password" ng-change="formDataChanged()" ng-minlength="6" required>\n </div>\n <div class="form-errors" ng-show="passwordForm.$submitted && passwordForm.password.$error" ng-messages="passwordForm.password.$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 password -->\n <div class="item item-input" ng-class="{ \'item-input-error\': passwordForm.$submitted && passwordForm.confirmPassword.$invalid}">\n <span class="input-label" translate>ACCOUNT.NEW.PASSWORD_CONFIRM</span>\n <input ng-if="!showPassword" name="confirmPassword" type="password" placeholder="{{\'ACCOUNT.NEW.PASSWORD_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmPassword" compare-to="formData.password">\n <input ng-if="showPassword" name="confirmPassword" type="text" placeholder="{{\'ACCOUNT.NEW.PASSWORD_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmPassword" compare-to="formData.password">\n </div>\n <div class="form-errors" ng-show="passwordForm.$submitted && passwordForm.confirmPassword.$error" ng-messages="passwordForm.confirmPassword.$error">\n <div class="form-error" ng-message="compareTo">\n <span translate="ERROR.PASSWORD_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 class="toggle toggle-royal">\n <input type="checkbox" ng-model="showPassword">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\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 </button>\n </div>\n\n <div class="padding hidden-xs">\n </div>\n </form>\n </ion-content>\n </ion-slide-page>\n\n <!-- STEP 5: pseudo-->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n <form name="pseudoForm" novalidate="" ng-submit="doNext(\'pseudoForm\')">\n\n <div class="item item-text-wrap text-center padding hidden-xs">\n <span translate>PROFILE.JOIN.TITLE_WARNING</span>\n </div>\n\n <div class="list" ng-init="setForm(pseudoForm, \'pseudoForm\')">\n\n <!-- pseudo -->\n <div class="item item-input" ng-class="{\'item-input-error\': pseudoForm.$submitted && pseudoForm.pseudo.$invalid}">\n <span class="input-label" translate>PROFILE.TITLE</span>\n <input name="pseudo" type="text" placeholder="{{\'PROFILE.TITLE_HELP\' | translate}}" ng-model="formData.pseudo" ng-minlength="4" ng-maxlength="100" required>\n </div>\n <div class="form-errors" ng-show="pseudoForm.$submitted && pseudoForm.pseudo.$error" ng-messages="pseudoForm.pseudo.$error">\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 3}"></span>\n </div>\n <div class="form-error" ng-message="maxlength">\n <span translate="ERROR.FIELD_TOO_LONG_WITH_LENGTH" translate-values="{maxLength: 100}"></span>\n </div>\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></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 </button>\n </div>\n </div>\n </form>\n </ion-content>\n </ion-slide-page>\n\n <!--<cs-extension-point name="last-slide"></cs-extension-point>-->\n\n <!-- STEP 6: last slide -->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n\n <div class="padding text-center" translate>PROFILE.JOIN.LAST_SLIDE_CONGRATULATION</div>\n\n <div class="list">\n\n <ion-item class="item item-text-wrap item-border">\n <div class="dark pull-right padding-right" ng-if="formData.computing">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n <span class="input-label" translate>COMMON.PUBKEY</span>\n <span class="gray text-no-wrap" ng-if="formData.computing" translate>\n ACCOUNT.NEW.COMPUTING_PUBKEY\n </span>\n <span class="gray text-no-wrap" ng-if="formData.pubkey">\n {{formData.pubkey}}\n </span>\n </ion-item>\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" ng-click="doNewAccount()" translate>\n COMMON.BTN_SEND\n <i class="icon ion-android-send"></i>\n </button>\n </div>\n </ion-content>\n </ion-slide-page>\n\n \n</ion-slides></ion-modal-view>\n');
$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="COMMON.APP_VERSION" translate-values="{version: config.version}"></span>\n |\n <!-- about -->\n <a href="#" ng-click="showAboutModal()" translate>HOME.BTN_ABOUT</a>\n </div>\n </div>\n\n\n </ion-content>\n\n</ion-view>\n');
$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 <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>\n\n <!--div class="padding hidden-xs text-right">\n <a class="assertive ink" ng-click="openNewAccount()" type="button" translate>COMMON.NO_ACOUNT_QUESTION\n </a>\n </div-->\n </form>\n </ion-content>\n</ion-modal-view>\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 <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.rememberMe" ng-disabled="!formData.useLocalStorage">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n </div>\n\n <!-- second column -->\n <div class="col col-50 list item-border-large padding-left padding-right no-padding-xs no-padding-sm no-margin-xs no-margin-sm">\n\n <!--span class="item item-divider">\n {{\'SETTINGS.WALLETS_SETTINGS\' | translate}}\n </span>\n\n <div class="item item-toggle item-text-wrap dark">\n <span class="input-label" ng-class="{\'gray\': !formData.useLocalStorage}" translate>SETTINGS.USE_WALLETS_ENCRYPTION</span>\n <h4 class="gray text-wrap" ng-bind-html="\'SETTINGS.USE_WALLETS_ENCRYPTION_HELP\' | translate">\n </h4>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.useLocalStorageEncryption" ng-disabled="!formData.useLocalStorage">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n <span class="item item-divider" translate>SETTINGS.HISTORY_SETTINGS</span>\n\n <div class="item item-toggle item-text-wrap dark">\n <div class="input-label" translate>SETTINGS.DISPLAY_UD_HISTORY</div>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.showUDHistory" >\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n <div class="item item-toggle dark hidden-xs hidden-sm">\n <div class="input-label" translate>SETTINGS.TX_HISTORY_AUTO_REFRESH</div>\n <h4 class="gray text-wrap" ng-bind-html="\'SETTINGS.TX_HISTORY_AUTO_REFRESH_HELP\' | translate"></h4>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.walletHistoryAutoRefresh" >\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="history"></cs-extension-point-->\n\n <span class="item item-divider" translate>SETTINGS.NETWORK_SETTINGS</span>\n\n <!-- Duniter node -->\n <div class="item ink item-text-wrap item-icon-right hidden-xs hidden-sm" ng-click="changeNode()">\n <div class="input-label" translate>SETTINGS.PEER</div>\n\n <!-- node temporary changed -->\n <ng-if ng-if="formData.node.temporary">\n <h4 class="gray text-wrap assertive">\n <i class="icon ion-alert-circled"></i>\n <span ng-bind-html="\'SETTINGS.PEER_CHANGED_TEMPORARY\' | translate "></span>\n </h4>\n <div class="item-note assertive text-italic">{{bma.server}}</div>\n </ng-if>\n\n <div class="badge badge-balanced" ng-if="!formData.node.temporary">{{bma.server}}</div>\n <i class="icon ion-ios-arrow-right"></i>\n </div>\n <ion-item class="ink item-icon-right visible-xs visible-sm" ng-click="changeNode()">\n <div class="input-label hidden-xs" translate>SETTINGS.PEER</div>\n <div class="input-label visible-xs" translate>SETTINGS.PEER_SHORT</div>\n\n <!-- node temporary changed -->\n <ng-if ng-if="formData.node.temporary">\n <h4 class="gray text-wrap assertive">\n <b class="ion-alert-circled"></b>\n <span ng-bind-html="\'SETTINGS.PEER_CHANGED_TEMPORARY\' | translate "></span>\n </h4>\n <div class="badge badge-assertive">{{bma.server}}</div>\n </ng-if>\n <div class="badge badge-balanced" ng-if="!formData.node.temporary">{{bma.server}}</div>\n <i class="icon ion-ios-arrow-right"></i>\n </ion-item>\n\n <!-- Expert mode ?-->\n <div class="item item-text-wrap item-toggle dark hidden-xs hidden-sm">\n <div class="input-label" ng-bind-html="\'SETTINGS.EXPERT_MODE\' | translate"></div>\n <h4 class="gray" ng-bind-html="\'SETTINGS.EXPERT_MODE_HELP\' | translate"></h4>\n <label class="toggle toggle-royal">\n <input type="checkbox" ng-model="formData.expertMode">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\n </div>\n\n <!-- Block validity window\n <label class="item item-input item-select item-text-wrap">\n <div class="input-label hidden-xs">\n <span translate>SETTINGS.BLOCK_VALIDITY_WINDOW</span>\n <h4 class="gray text-wrap hidden-xs" ng-bind-html="\'SETTINGS.BLOCK_VALIDITY_WINDOW_HELP\' | translate"></h4>\n </div>\n <div class="input-label visible-xs" translate>SETTINGS.BLOCK_VALIDITY_WINDOW_SHORT</div>\n <select ng-model="formData.blockValidityWindow"\n ng-options="i as (blockValidityWindowLabels[i].labelKey | translate:blockValidityWindowLabels[i].labelParams ) for i in blockValidityWindows track by i">\n </select>\n </label>-->\n\n <!-- Allow extension here -->\n <cs-extension-point name="network"></cs-extension-point>\n\n <span class="item item-divider" ng-if="$root.config.plugins" translate>SETTINGS.PLUGINS_SETTINGS</span>\n\n <!-- Allow extension here -->\n <cs-extension-point name="plugins"></cs-extension-point>\n\n </div>\n </div>\n </ion-content>\n</ion-view>\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:\'COMMON.PUBKEY\'|translate}}\n <h4 id="pubkey" class="dark">{{:rebind:formData.pubkey}}</h4>\n </span> -->\n\n <!-- Uid + Registration date\n <ion-item class="item-icon-left" ng-if=":rebind:formData.sigDate||formData.uid">\n <i class="icon ion-calendar"></i>\n <span translate>COMMON.UID</span>\n <h5 class="dark" ng-if=":rebind:formData.sigDate">\n <span translate>WOT.REGISTERED_SINCE</span>\n {{:rebind:formData.sigDate | formatDate}}\n </h5>\n <span class="badge badge-stable">{{:rebind:formData.uid}}</span>\n </ion-item>-->\n\n <!-- Certifications\n <a id="helptip-wallet-certifications"\n class="item item-icon-left item-icon-right item-text-wrap ink"\n ng-if="formData.isMember||formData.requirements.pendingMembership"\n ng-click="showCertifications()">\n <i class="icon ion-ribbon-b"></i>\n <b ng-if="formData.requirements.isSentry" class="ion-star icon-secondary" style="color: yellow; font-size: 16px; left: 25px; top: -7px;"></b>\n {{:locale:\'ACCOUNT.CERTIFICATION_COUNT\'|translate}}\n <cs-badge-certification requirements="formData.requirements"\n parameters="{sigQty: formData.parameters.sigQty}">\n </cs-badge-certification>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a> -->\n\n <!-- Signature stock\n <a id="helptip-wallet-given-certifications"\n class="item item-icon-left item-text-wrap item-icon-right ink visible-xs visible-sm"\n ng-if="formData.isMember"\n ng-click="showGivenCertifications()">\n <i class="icon ion-ribbon-a"></i>\n <span translate>WOT.GIVEN_CERTIFICATIONS.SENT</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a> -->\n\n <!-- Events\n <span class="item item-divider" ng-if="formData.events.length">\n {{:locale:\'ACCOUNT.EVENTS\' | translate}}\n </span>\n\n <div\n class="item item-text-wrap item-icon-left item-wallet-event"\n ng-repeat="event in formData.events">\n <i class="icon"\n ng-class="{\'ion-information-circled royal\': event.type==\'info\',\'ion-alert-circled\': event.type==\'warn\'||event.type==\'error\',\'assertive\': event.type==\'error\',\'ion-clock\': event.type==\'pending\'}"></i>\n <span trust-as-html="event.message | translate:event.messageParams"></span>\n </div> -->\n\n <!-- Account transaction\n <a class="item item-icon-left item-icon-right ink"\n ng-if="!loading"\n ui-sref="app.view_wallet_tx">\n <i class="icon ion-card"></i>\n <span translate>WOT.ACCOUNT_OPERATIONS</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>-->\n\n <cs-extension-point name="general"></cs-extension-point>\n\n <cs-extension-point name="after-general"></cs-extension-point>\n\n\n </div>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n </div>\n </ion-content>\n\n</ion-view>\n');
$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 <!--endRemoveIf(device)-->\n\n <!-- multi selection -->\n <div ng-if="::allowMultiple" class="list {{::motion.ionListClass}}">\n\n <ion-checkbox ng-repeat="identity in search.results" ng-model="identity.checked" class="item item-border-large item-avatar ink" ng-click="toggleCheck($index, $event)">\n <ng-include src="\'templates/wot/identity.html\'"></ng-include>\n </ion-checkbox>\n </div>\n\n <ion-infinite-scroll ng-if="search.hasMore" spinner="android" on-infinite="showMore()" distance="2%">\n </ion-infinite-scroll>\n\n </ng-if>\n</div>\n');
$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_ACCOUNT_HELP</h4>\n </div>-->\n\n <!-- Uid + Registration date\n <ion-item class="item-icon-left" ng-if=":rebind:formData.sigDate||formData.uid">\n <i class="icon ion-calendar"></i>\n <span translate>COMMON.UID</span>\n <h5 class="dark" ng-if=":rebind:formData.sigDate">\n <span translate>WOT.REGISTERED_SINCE</span>\n {{:rebind:formData.sigDate | formatDate}}\n </h5>\n <span class="badge badge-stable">{{:rebind:formData.uid}}</span>\n </ion-item>-->\n\n <!-- Received certifications count\n <a id="helptip-wot-view-certifications"\n class="item item-icon-left item-text-wrap item-icon-right ink"\n ng-if=":rebind:formData.hasSelf"\n ng-click="showCertifications()">\n <i class="icon ion-ribbon-b"></i>\n <b ng-if=":rebind:formData.requirements.isSentry" class="ion-star icon-secondary" style="color: yellow; font-size: 16px; left: 25px; top: -7px;"></b>\n <span translate>ACCOUNT.CERTIFICATION_COUNT</span>\n <cs-badge-certification cs-id="helptip-wot-view-certifications-count"\n requirements="formData.requirements"\n parameters="{sigQty: formData.sigQty}">\n </cs-badge-certification>\n\n <i class="gray icon ion-ios-arrow-right"></i>\n </a> -->\n\n <!-- Signature stock\n <a class="item item-icon-left item-text-wrap item-icon-right ink visible-xs visible-sm"\n ng-if=":rebind:formData.hasSelf && formData.isMember"\n ng-click="showGivenCertifications()">\n <i class="icon ion-ribbon-a"></i>\n <span translate>WOT.GIVEN_CERTIFICATIONS.SENT</span>\n <cs-badge-given-certification identity="formData"\n parameters="{sigStock: formData.sigStock}">\n </cs-badge-given-certification>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>-->\n\n <!-- Account transaction\n <a class="item item-icon-left item-icon-right ink"\n ng-if="!loading"\n ui-sref="app.wot_identity_tx_uid({uid:formData.uid,pubkey:formData.pubkey})">\n <i class="icon ion-card"></i>\n <span translate>WOT.ACCOUNT_OPERATIONS</span>\n <i class="gray icon ion-ios-arrow-right"></i>\n </a>-->\n\n <div class="item item-text-wrap item-icon-left item-wallet-event assertive" ng-repeat="event in :rebind:formData.events | filter: {type: \'error\'}">\n <i class="icon ion-alert-circled"></i>\n <span trust-as-html="event.message | translate:event.messageParams"></span>\n </div>\n\n <cs-extension-point name="general"></cs-extension-point>\n\n <cs-extension-point name="after-general"></cs-extension-point>\n\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n </div>\n\n </ion-content>\n\n <!-- fab button\n <div class="visible-xs visible-sm" ng-hide="loading">\n <button id="fab-transfer" class="button button-fab button-fab-bottom-right button-assertive drop"\n ng-click="showTransferModal({pubkey:formData.pubkey, uid: formData.uid})">\n <i class="icon ion-android-send"></i>\n </button>\n </div>-->\n</ion-view>\n');
$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" style="width: 100%" placeholder="{{\'COMMENTS.COMMENT_HELP\'|translate}}" on-return="save();" ng-model="formData.message">\n <button type="submit" class="button button-small button-small-padding button-icon button-dark button-icon gray">\n <i class="icon ion-android-send"></i>\n </button>\n </div>\n </div>\n </div>\n </div>\n</form>\n');
$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 COMMON.BTN_PUBLISH\n </button>\n <button class="button button-assertive button-raised ink" type="submit" ng-if="id" translate>\n COMMON.BTN_SAVE\n </button>\n </div>\n </form>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n </div>\n \n </ion-content>\n</ion-view>\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="_blank" href="https://www.google.fr/maps/?q={{formData.address}},%20{{formData.city}}">\n <span ng-bind-html="formData.address"></span>\n <span ng-if="formData.city"> - </span>\n <span ng-bind-html="formData.city"></span>\n </a>\n </h4>\n </ion-item>\n\n <!-- Socials networks -->\n <ng-if ng-if="formData.socials && formData.socials.length>0">\n <ion-item class="item-icon-left" type="no-padding item-text-wrap" ng-repeat="social in formData.socials 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\'}"></i>\n <p ng-if="social.type && social.type != \'web\'">{{social.type}}</p>\n <h2>\n <a href="{{social.url}}" ng-if="social.type != \'email\'" target="_blank">{{social.url}}</a>\n <a href="mailto:{{social.url}}" ng-if="social.type == \'email\'">{{social.url}}</a>\n </h2>\n </ion-item>\n </ng-if>\n\n <div class="lazy-load">\n\n <!-- pictures -->\n <ng-include src="\'plugins/es/templates/common/view_pictures.html\'"></ng-include>\n\n\n <span class="item item-divider" ng-if="formData.pubkey">\n <span translate>REGISTRY.TECHNICAL_DIVIDER</span>\n </span>\n\n <!-- pubkey -->\n <div class="item item-icon-left item-text-wrap ink" ng-if="formData.pubkey" copy-on-click="{{::formData.pubkey}}">\n <i class="icon ion-key"></i>\n <span translate>REGISTRY.EDIT.RECORD_PUBKEY</span>\n <h4 class="dark">{{::formData.pubkey}}</h4>\n </div>\n\n <!-- comments -->\n <ng-include src="\'plugins/es/templates/common/view_comments.html\'"></ng-include>\n </div>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n </div>\n </ion-content>\n\n <button class="button button-fab button-fab-bottom-right button-assertive icon ion-android-send visible-xs visible-sm" ng-if="formData.pubkey && !isUserPubkey(formData.pubkey)" ng-click="showTransferModal({pubkey: formData.pubkey, uid: formData.title})">\n </button>\n\n\n</ion-view>\n');
$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-item>\n </ion-list>\n </ion-content>\n\n <button id="fab-add-message-record" class="button button-fab button-fab-bottom-right button-assertive icon ion-compose visible-xs visible-sm spin" ng-click="showNewMessageModal()">\n </button>\n</ion-view>\n');
$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> {{\'NETWORK.INFO.ONLY_SSL_PEERS\'|translate}}</small>\n </ion-item>\n\n <div class="item center" ng-if="loading">\n <ion-spinner class="icon" icon="android"></ion-spinner>\n </div>\n\n <div class="list no-padding {{::motion.ionListClass}}" ng-if="isReachable">\n\n <div ng-repeat="peer in :rebind:peers track by peer.id" class="item item-peer item-icon-left ink" ng-class="::ionItemClass" ng-click="selectPeer(peer)" ng-include="\'plugins/es/templates/network/item_content_peer.html\'">\n </div>\n\n </div>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n </div>\n\n </ion-content>\n</ion-view>\n');
$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\'|translate}}" ng-model="formData.description" rows="8" cols="10">\n </textarea>\n </div>\n\n <!-- category -->\n <div class="item item-icon-right ink" ng-if="loading || formData.type===\'company\' || formData.type===\'shop\'" ng-class="{\'item-input-error\': form.$submitted && !formData.category.id, \'done in\': !loading}" ng-click="showCategoryModal()">\n <span translate>REGISTRY.CATEGORY</span>\n <span class="badge badge-royal">{{formData.category.name | formatCategory}}</span>&nbsp;\n <i class="gray icon ion-ios-arrow-right"></i>\n </div>\n <input type="hidden" name="category" ng-model="formData.category.id" required-if="formData.type==\'company\' || formData.type==\'shop\'">\n <div class="form-errors" ng-if="form.$submitted && form.category.$error" ng-messages="form.category.$error">\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- position -->\n <ng-include src="\'plugins/es/templates/common/edit_position.html\'"></ng-include>\n\n <!-- social networks -->\n <ng-include src="\'plugins/es/templates/common/edit_socials.html\'" ng-controller="ESSocialsEditCtrl"></ng-include>\n\n <div class="item item-divider" translate>REGISTRY.TECHNICAL_DIVIDER</div>\n\n <!-- pubkey -->\n <div class="item item-input item-floating-label">\n <span class="input-label" translate>REGISTRY.EDIT.RECORD_PUBKEY</span>\n <input type="text" placeholder="{{\'REGISTRY.EDIT.RECORD_PUBKEY_HELP\'|translate}}" ng-model="formData.pubkey">\n </div>\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 COMMON.BTN_PUBLISH\n </button>\n <button class="button button-assertive button-raised ink" type="submit" ng-if="id" translate>\n COMMON.BTN_SAVE\n </button>\n </div>\n </form>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n </div>\n \n </ion-content>\n</ion-view>\n');
$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 class="gray no-padding" ng-if="search.total">\n <span ng-if="search.geoPoint && search.total">{{\'REGISTRY.SEARCH.LAST_RECORD_COUNT_LOCATION\'|translate:{count: search.total, location: search.location} }}</span>\n <span ng-if="!search.geoPoint && search.total">{{\'REGISTRY.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>COMMON.RESULTS_LIST</h4>\n <small class="gray no-padding" ng-if="search.total">\n <span ng-if="search.geoPoint && search.total">{{\'REGISTRY.SEARCH.RESULT_COUNT_LOCATION\'|translate:{count: search.total, location: search.location} }}</span>\n <span ng-if="!search.geoPoint && search.total">{{\'REGISTRY.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" translate>\n COMMON.SEARCH_NO_RESULT\n</div>\n');
$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-if="canEdit" ng-click="edit()">\n {{\'COMMON.BTN_EDIT\' | translate}}\n </button>\n </div>\n\n <ion-item>\n <h2 trust-as-html="formData.description"></h2>\n </ion-item>\n\n <ion-item ng-if="formData.category || formData.address">\n <h4 ng-if="formData.category">\n <span class="gray" translate>REGISTRY.VIEW.CATEGORY</span>\n <a class="positive" ng-if="formData.category" ui-sref="app.registry_lookup({category:formData.category.id})">\n <span ng-bind-html="formData.category.name"></span>\n </a>\n </h4>\n <h4 ng-if="formData.address">\n <span class="gray" translate>REGISTRY.VIEW.LOCATION</span>\n <a class="positive" target="_system" href="https://www.openstreetmap.org/search?query={{formData.address}},%20{{formData.city}}">\n <span ng-bind-html="formData.address"></span>\n <span ng-if="formData.city"> - </span>\n <span ng-bind-html="formData.city"></span>\n </a>\n </h4>\n </ion-item>\n\n <!-- Socials networks -->\n <ng-if ng-if="formData.socials && formData.socials.length>0">\n <ion-item class="item-icon-left" type="no-padding item-text-wrap" ng-repeat="social in formData.socials 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\'}"></i>\n <p ng-if="social.type && social.type != \'web\'">{{social.type}}</p>\n <h2>\n <a ng-click="openLink($event, social.url, social.type)">{{social.url}}</a>\n </h2>\n </ion-item>\n </ng-if>\n\n <!-- pubkey -->\n <div class="item item-icon-left item-text-wrap ink" ng-if="formData.pubkey" copy-on-click="{{::formData.pubkey}}">\n <i class="icon ion-key"></i>\n <span translate>REGISTRY.EDIT.RECORD_PUBKEY</span>\n <h4 class="dark">{{::formData.pubkey}}</h4>\n </div>\n\n <div class="lazy-load">\n\n <!-- pictures -->\n <ng-include src="\'plugins/es/templates/common/view_pictures.html\'"></ng-include>\n\n <!-- comments -->\n <ng-include src="\'plugins/es/templates/common/view_comments.html\'"></ng-include>\n </div>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n </div>\n </ion-content>\n\n <button class="button button-fab button-fab-bottom-right button-assertive icon ion-android-send visible-xs visible-sm" ng-if="formData.pubkey && !isUserPubkey(formData.pubkey)" ng-click="showTransferModal({pubkey: formData.pubkey, uid: formData.title})">\n </button>\n\n\n</ion-view>\n');
$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 </form>\n </ion-content>\n</ion-modal-view>\n\n\n\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 </ion-item>\n\n <!-- position -->\n <ng-include src="\'plugins/es/templates/common/edit_position.html\'" ng-controller="ESPositionEditCtrl"></ng-include>\n\n <!-- social networks -->\n <ng-include src="\'plugins/es/templates/common/edit_socials.html\'" ng-controller="ESSocialsEditCtrl"></ng-include>\n\n <div class="item item-divider">\n {{\'PROFILE.TECHNICAL_DIVIDER\' | translate}}\n </div>\n\n <!-- pubkey -->\n <div class="item item-input item-floating-label" ng-class="{\'item-input-error\': form.$submitted && form.pubkey.$invalid}">\n <span class="input-label" translate>REGISTRY.EDIT.RECORD_PUBKEY</span>\n <input type="text" name="pubkey" placeholder="{{\'REGISTRY.EDIT.RECORD_PUBKEY_HELP\'|translate}}" ng-model="formData.pubkey" autocomplete="off" ng-pattern="pubkeyPattern" ng-model-options="{ debounce: 250 }">\n </div>\n <div class="form-errors" ng-show="form.pubkey.$error" ng-messages="form.pubkey.$error">\n <div class="form-error" ng-message="pattern">\n <span translate="ERROR.INVALID_PUBKEY"></span>\n </div>\n </div>\n\n <div class="item padding hidden-xs hidden-sm 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-calm ink" ng-class="{\'button-assertive\': dirty}" type="submit">\n {{\'COMMON.BTN_SAVE\' | translate}}\n </button>\n </div>\n </div>\n </form>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;\n </div>\n </div>\n\n </ion-content>\n\n</ion-view>\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-->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n <form name="passwordForm" novalidate="" ng-submit="doNext(\'passwordForm\')">\n\n <div class="item item-text-wrap text-center padding hidden-xs">\n <a class="pull-right icon-help" ng-click="showHelpModal(\'join-password\')"></a>\n <span translate>ACCOUNT.NEW.PASSWORD_WARNING</span>\n </div>\n\n <div class="list" ng-init="setForm(passwordForm, \'passwordForm\')">\n\n <!-- password -->\n <div class="item item-input" ng-class="{ \'item-input-error\': passwordForm.$submitted && passwordForm.password.$invalid}">\n <span class="input-label" translate>LOGIN.PASSWORD</span>\n <input ng-if="!showPassword" name="password" type="password" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}" ng-model="formData.password" ng-change="formDataChanged()" ng-minlength="8" required>\n <input ng-if="showPassword" name="text" type="text" placeholder="{{\'LOGIN.PASSWORD_HELP\' | translate}}" autocomplete="off" ng-model="formData.password" ng-change="formDataChanged()" ng-minlength="8" required>\n </div>\n <div class="form-errors" ng-show="passwordForm.$submitted && passwordForm.password.$error" ng-messages="passwordForm.password.$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 password -->\n <div class="item item-input" ng-class="{ \'item-input-error\': passwordForm.$submitted && passwordForm.confirmPassword.$invalid}">\n <span class="input-label" translate>ACCOUNT.NEW.PASSWORD_CONFIRM</span>\n <input ng-if="!showPassword" name="confirmPassword" type="password" placeholder="{{\'ACCOUNT.NEW.PASSWORD_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmPassword" compare-to="formData.password">\n <input ng-if="showPassword" name="confirmPassword" type="text" autocomplete="off" placeholder="{{\'ACCOUNT.NEW.PASSWORD_CONFIRM_HELP\' | translate}}" ng-model="formData.confirmPassword" compare-to="formData.password">\n </div>\n <div class="form-errors" ng-show="passwordForm.$submitted && passwordForm.confirmPassword.$error" ng-messages="passwordForm.confirmPassword.$error">\n <div class="form-error" ng-message="compareTo">\n <span translate="ERROR.PASSWORD_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 class="toggle toggle-royal">\n <input type="checkbox" ng-model="showPassword">\n <div class="track">\n <div class="handle"></div>\n </div>\n </label>\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 </button>\n </div>\n\n <div class="padding hidden-xs">\n </div>\n </form>\n </ion-content>\n </ion-slide-page>\n\n <!-- STEP 5: profile -->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n <form name="profileForm" novalidate="" ng-submit="doNext(\'profileForm\')">\n\n\n <div class="list" ng-init="setForm(profileForm, \'profileForm\')">\n\n <div class="item item-text-wrap text-center padding hidden-xs">\n <span translate>MARKET.JOIN.PROFILE.WARNING</span>\n </div>\n\n <!-- title -->\n <div class="item item-input" ng-class="{\'item-input-error\': profileForm.$submitted && profileForm.title.$invalid}">\n <span class="input-label" translate>MARKET.JOIN.PROFILE.TITLE</span>\n <input name="title" type="text" placeholder="{{\'MARKET.JOIN.PROFILE.TITLE_HELP\' | translate}}" autocomplete="off" ng-model="formData.title" ng-minlength="4" ng-maxlength="100" required>\n </div>\n <div class="form-errors" ng-show="profileForm.$submitted && profileForm.title.$error" ng-messages="profileForm.title.$error">\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 3}"></span>\n </div>\n <div class="form-error" ng-message="maxlength">\n <span translate="ERROR.FIELD_TOO_LONG_WITH_LENGTH" translate-values="{maxLength: 100}"></span>\n </div>\n <div class="form-error" ng-message="required">\n <span translate="ERROR.FIELD_REQUIRED"></span>\n </div>\n </div>\n\n <!-- description -->\n <ion-item class="item-input">\n <span class="input-label" translate>MARKET.JOIN.PROFILE.DESCRIPTION</span>\n <textarea placeholder="{{\'MARKET.JOIN.PROFILE.DESCRIPTION_HELP\' | translate}}" ng-model="formData.description" ng-model-options="{ debounce: 350 }" rows="4" cols="10">\n </textarea>\n </ion-item>\n\n <!-- email -->\n <div class="item item-input" ng-class="{\'item-input-error\': profileForm.$submitted && profileForm.email.$invalid}">\n <span class="input-label" translate>MARKET.JOIN.SUBSCRIPTION.EMAIL</span>\n <input name="email" type="text" placeholder="{{\'MARKET.JOIN.SUBSCRIPTION.EMAIL_HELP\' | translate}}" ng-model="formData.email" autocomplete="off" ng-pattern="emailPattern" ng-minlength="4" ng-maxlength="100">\n </div>\n <div class="form-errors" ng-show="profileForm.$submitted && profileForm.email.$error" ng-messages="profileForm.email.$error">\n <div class="form-error" ng-message="minlength">\n <span translate="ERROR.FIELD_TOO_SHORT_WITH_LENGTH" translate-values="{minLength: 3}"></span>\n </div>\n <div class="form-error" ng-message="maxlength">\n <span translate="ERROR.FIELD_TOO_LONG_WITH_LENGTH" translate-values="{maxLength: 100}"></span>\n </div>\n <div class="form-error" ng-message="pattern">\n <span translate="ERROR.FIELD_NOT_EMAIL"></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 </button>\n </div>\n </div>\n </form>\n </ion-content>\n </ion-slide-page>\n\n <!--<cs-extension-point name="last-slide"></cs-extension-point>-->\n\n <!-- STEP 6: last slide -->\n <ion-slide-page>\n <ion-content class="has-header" scroll="false">\n\n <div class="padding text-center" translate>MARKET.JOIN.LAST_SLIDE_CONGRATULATION</div>\n\n <div class="list">\n\n <ion-item class="item item-text-wrap item-border">\n <div class="dark pull-right padding-right" ng-if="formData.computing">\n <ion-spinner icon="android"></ion-spinner>\n </div>\n </ion-item>\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" ng-click="doNewAccount()" translate>\n COMMON.BTN_SEND\n <i class="icon ion-android-send"></i>\n </button>\n </div>\n </ion-content>\n </ion-slide-page>\n\n \n</ion-slides></ion-modal-view>\n');
$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="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>-->\n\n <!--div class="padding hidden-xs text-right">\n <a class="assertive ink" ng-click="openNewAccount()" type="button" translate>COMMON.NO_ACOUNT_QUESTION\n </a>\n </div-->\n </form>\n </ion-content>\n</ion-modal-view>\n');
$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>-->\n </form>\n </ion-content>\n</ion-modal-view>\n');
$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 <ion-item class="item-input item-floating-label item-button-right" ng-class="{\'item-input-error\': form.$submitted && form.price.$invalid}">\n\n <div class="input-label">\n <span translate>MARKET.EDIT.RECORD_PRICE</span>\n (<span ng-bind-html="formData.currency| currencySymbol:formData.useRelative"></span>)\n </div>\n\n <input type="number" autocomplete="off" name="price" placeholder="{{::\'MARKET.EDIT.RECORD_PRICE_HELP\' | translate}}" ng-model="formData.price" number-float>\n <a class="button button-clear button-stable dark ink" tabindex="-1" style="z-index:110; padding: 0 16px" ng-if="options.unit.canEdit" ng-click="showUnitPopover($event)">\n <span ng-bind-html="$root.currency.name | currencySymbol:formData.useRelative">\n </span>\n &nbsp;<b class="ion-arrow-down-b" style="font-size: 12pt"></b>\n </a>\n </ion-item>\n <div class="form-errors" ng-show="form.$submitted && form.price.$invalid" ng-messages="form.price.$error">\n <div class="form-error" ng-message="numberFloat">\n <span translate="ERROR.FIELD_NOT_NUMBER"></span>\n </div>\n <div class="form-error" ng-message="numberInt">\n <span translate="ERROR.FIELD_NOT_INT"></span>\n </div>\n </div>\n\n <!--dev class="item item-icon-right ink"\n ng-show="formData.price"\n ng-click="openCurrencyLookup()" >\n <span class="item-label gray" translate>MARKET.EDIT.RECORD_CURRENCY</span>\n <span class="badge badge-royal">{{formData.currency}}</span>&nbsp;\n <i class="gray icon ion-ios-arrow-right"></i>\n </dev-->\n\n <!-- fees -->\n <div class="item item-input item-floating-label item-button-right" ng-if="formData.type==\'offer\'" ng-class="{\'item-input-error\': form.$submitted && form.fees.$invalid}">\n <div class="input-label">\n <span translate>MARKET.EDIT.RECORD_FEES</span>\n (<span ng-bind-html="formData.currency| currencySymbol:formData.useRelative"></span>)\n </div>\n\n <input type="number" autocomplete="off" name="fees" placeholder="{{::\'MARKET.EDIT.RECORD_FEES_HELP\' | translate}}" ng-model="formData.fees" number-float>\n <div class="button button-clear button-stable dark ink" tabindex="-1" style="z-index:110; padding: 0 16px" ng-if="options.unit.canEdit">\n <span ng-bind-html="$root.currency.name | currencySymbol:formData.useRelative">\n </span>\n </div>\n </div>\n <div class="form-errors" ng-show="form.$submitted && form.fees.$invalid" ng-messages="form.fees.$error">\n <div class="form-error" ng-message="numberFloat">\n <span translate="ERROR.FIELD_NOT_NUMBER"></span>\n </div>\n <div class="form-error" ng-message="numberInt">\n <span translate="ERROR.FIELD_NOT_INT"></span>\n </div>\n </div>\n\n <!-- stock -->\n <div class="item item-input item-floating-label item-button-right" ng-if="formData.type==\'offer\'" ng-class="{\'item-input-error\': form.$submitted && form.stock.$invalid}">\n <div class="input-label">{{::\'MARKET.EDIT.RECORD_STOCK\' | translate}}</div>\n\n <input type="number" name="stock" placeholder="{{::\'MARKET.EDIT.RECORD_STOCK_HELP\' | translate}}" ng-model="formData.stock" number-int>\n </div>\n <div class="form-errors" ng-show="form.$submitted && form.stock.$invalid" ng-messages="form.stock.$error">\n <div class="form-error" ng-message="numberInt">\n <span translate="ERROR.FIELD_NOT_INT"></span>\n </div>\n </div>\n\n <!-- position -->\n <ng-include src="\'plugins/es/templates/common/edit_position.html\'" ng-controller="ESPositionEditCtrl"></ng-include>\n\n <!-- buttons -->\n <div class="item 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 COMMON.BTN_PUBLISH\n </button>\n <button class="button button-assertive button-raised ink" type="submit" ng-if="id" translate>\n COMMON.BTN_SAVE\n </button>\n </div>\n </div>\n </form>\n </div>\n\n <div class="col col-20 hidden-xs hidden-sm">&nbsp;</div>\n\n </div>\n \n </ion-content>\n</ion-view>\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 class="icon ion-location"></i>\n {{::options.location.prefix|translate}}<span ng-bind-html="::formData.city"></span>\n </a>\n <br>\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\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 class="positive" ng-if="issuer.name||issuer.uid">\n <i class="icon ion-person"></i>\n {{::issuer.name||issuer.uid}}\n </span>\n <span class="gray" ng-if="!issuer.name && !issuer.uid">\n <i class="icon ion-key"></i>\n {{::issuer.pubkey|formatPubkey}}\n </span>\n </a>\n <span>\n {{formData.time|formatFromNow}}\n <span class="gray hidden-xs">|\n {{formData.time | formatDate}}\n </span>\n </span>\n </h4>\n\n <!-- likes -->\n <h4>\n <ng-include src="\'plugins/es/templates/common/view_likes.html\'"></ng-include>\n </h4>\n\n <!-- fab button-->\n <div class="visible-xs visible-sm">\n\n <!-- like -->\n <button id="fab-like-market-record-{{id}}" class="button button-fab button-fab-top-right button-stable mini spin" ng-click="toggleLike($event)">\n <i class="icon ion-heart" ng-class="{\'gray\': !likeData.likes.wasHit, \'calm\': likeData.likes.wasHit}"></i>\n </button>\n\n </div>\n </div>\n\n\n <!-- mobile: price -->\n <div class="item visible-xs no-padding-top no-padding-bottom">\n\n <div class="badge badge-price" ng-class="::{\'badge-energized\': formData.type == \'need\', \'badge-calm\': formData.type == \'offer\'}" ng-if="formData.price">\n <i class="icon" ng-class="::{\'cion-market-need\': formData.type === \'need\', \'ion-pricetag\': formData.type === \'offer\'}"></i>\n <span ng-bind-html="::formData.price | formatAmount: {currency: formData.currency, useRelative: $root.settings.useRelative} "></span>\n </div>\n <div class="badge badge-secondary" ng-if="formData.fees">\n <span class="dark">\n <i class="ion-plus" ng-if="::formData.price"></i>\n <ng-bind-html ng-bind-html="::formData.fees | formatAmount: {currency: formData.feesCurrency, useRelative: $root.settings.useRelative}"></ng-bind-html>\n </span>\n <span class="gray">{{\'MARKET.VIEW.RECORD_FEES_PARENTHESIS\'|translate}} | </span>\n <span class="gray">\n <i class="ion-pie-graph"></i>\n {{\'MARKET.VIEW.RECORD_STOCK\'|translate}}\n <ng-if ng-if="formData.stock > 0"><span class="dark">{{::formData.stock}}</span> <i class="ion-checkmark balanced"></i></ng-if>\n <ng-if ng-if="formData.stock === 0"><i class="ion-close assertive"></i> <span class="assertive bold" translate>MARKET.COMMON.SOLD</span></ng-if>\n </span>\n </div>\n <div class="badge badge-secondary badge-assertive" ng-if="!formData.fees&&!formData.stock">\n <i class="ion-close"></i> <span class="bold" translate>MARKET.COMMON.SOLD</span>\n </div>\n </div>\n\n\n\n <!-- Buttons bar-->\n <a id="record-share-anchor-{{id}}"></a>\n <div class="item large-button-bar hidden-xs hidden-sm">\n\n <!-- Share button -->\n <button class="button button-stable button-small-padding icon ion-android-share-alt" ng-click="showSharePopover($event)">\n </button>\n\n <!-- Message button -->\n <button class="button button-stable button-small-padding icon ion-compose" ng-if="!canEdit" ng-click="showNewMessageModal()" title="{{\'MARKET.VIEW.BTN_WRITE_\'+formData.type |upper|translate}}">\n </button>\n\n <!-- Like button -->\n <button class="button button-stable button-small-padding ink-dark" ng-if="!canEdit" title="{{\'COMMON.BTN_LIKE\' | translate }}" ng-click="toggleLike($event)">\n <i class="icon ion-heart" ng-class="{\'gray\': !likeData.likes.wasHit, \'calm\': likeData.likes.wasHit}"></i>\n </button>\n\n <!--<button class="button button-stable icon-left ink-dark"\n ng-if="canEdit"\n 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-left ink-dark" ng-if="canSold" title="{{\'MARKET.VIEW.BTN_SOLD_AD\' | translate}}" ng-click="sold()">\n <i class="icon ion-minus-circled"></i>\n {{\'MARKET.VIEW.BTN_SOLD\' | translate}}\n </button>\n <button class="button button-stable icon-left ink-dark" ng-if="canReopen" ng-click="reopen()">\n <i class="icon ion-unlocked"></i>\n {{\'MARKET.VIEW.BTN_REOPEN\' | translate}}\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\n <button class="button button-stable button-small-padding icon ion-android-more-vertical" ng-if="!canEdit" ng-click="showActionsPopover($event)">\n </button>\n\n </div>\n\n <ion-item class="item-text-wrap" ng-if="options.description.show && formData.description">\n <p class="text-italic">\n <i class="icon ion-quote"></i>\n <span trust-as-html="formData.description"></span>\n </p>\n </ion-item>\n\n <span class="item item-icon-left item-button-right hidden-xs" ng-if="formData.price||formData.fees">\n <ng-if ng-if="formData.price">\n <i class="calm icon ion-pricetag"></i>\n <h1 class="calm" ng-bind-html="::formData.price | formatAmount: {currency: formData.currency, useRelative: $root.settings.useRelative}"></h1>\n </ng-if>\n <h3>\n <ng-if ng-if="formData.fees">\n <span class="dark">\n <i class="ion-plus" ng-if="::formData.price"></i>\n <ng-bind-html ng-bind-html="formData.fees | formatAmount: {currency: formData.feesCurrency, useRelative: $root.settings.useRelative}"></ng-bind-html>\n </span>\n <span class="gray">{{\'MARKET.VIEW.RECORD_FEES_PARENTHESIS\'|translate}} | </span>\n </ng-if>\n <span class="gray">\n <i class="ion-pie-graph"></i>\n {{\'MARKET.VIEW.RECORD_STOCK\'|translate}}\n <ng-if ng-if="formData.stock > 0"><span class="dark">{{::formData.stock}}</span> <i class="ion-checkmark balanced"></i></ng-if>\n <ng-if ng-if="formData.stock === 0"><i class="ion-close assertive"></i> <span class="assertive bold" translate>MARKET.COMMON.SOLD</span></ng-if>\n </span>\n </h3>\n </span>\n <span class="item hidden-xs" ng-if="!formData.price && !formData.fees && formData.stock === 0">\n <div class="badge badge-secondary badge-assertive"><i class="ion-close"></i> <span class="bold" translate>MARKET.COMMON.SOLD</span></div>\n </span>\n\n <div class="lazy-load">\n <!-- pictures -->\n <ng-include src="\'plugins/es/templates/common/view_pictures.html\'"></ng-include>\n\n <!-- comments -->\n <ng-include src="\'plugins/es/templates/common/view_comments.html\'"></ng-include>\n </div>\n </div>\n </div>\n\n <div class="col col-33 hidden-xs hidden-sm hidden-md padding padding-top list" style="display: inline-block; max-width: 350px">\n\n <!-- issuer card -->\n <a class="item item-avatar card-meta item item-border-large dark-100-bg dark animate-ripple animate-show-hide ng-hide" ng-show="issuer.stars && !loading" ui-sref="app.wot_identity({pubkey: issuer.pubkey, uid: issuer.name})">\n <div class="item-text-wrap light">\n\n <i ng-if="!issuer.avatar" class="item-image icon ion-person"></i>\n <i ng-if="issuer.avatar" class="item-image avatar" style="background-image: url({{issuer.avatar.src}})"></i>\n\n <h3>\n <span ng-if="issuer.name||issuer.uid" class="positive">\n {{::issuer.name||issuer.uid}}\n </span>\n <span ng-if="!issuer.name&&!issuer.uid" class="gray">\n <b class="ion-key"></b>\n {{::issuer.pubkey|formatPubkey}}\n </span>\n </h3>\n\n <h3 class="align-right" title="{{\'WOT.VIEW.STARS\' | translate }}">\n <span ng-repeat="value in [1,2,3,4,5]" ng-class="{\'energized\': issuer.stars.levelAvg > 3, \'assertive\': issuer.stars.levelAvg <= 2}">\n <b class="ion-android-star" ng-if="value <= issuer.stars.levelAvg"></b>\n <b class="ion-android-star-half" ng-if="value > issuer.stars.levelAvg && value - 0.5 <= issuer.stars.levelAvg"></b>\n <b class="ion-android-star-outline" ng-if="value > issuer.stars.levelAvg && value - 0.5 > issuer.stars.levelAvg"></b>\n </span><br>\n </h3>\n <h4 class="gray">{{issuer.stars.levelAvg}}/5 ({{\'WOT.VIEW.STAR_HIT_COUNT\' | translate: issuer.stars }})</h4>\n </div>\n </a>\n\n\n\n <!-- More similar ads -->\n <div class="list list-more-record animate-ripple no-padding" ng-if="!search.loading && search.results.length">\n\n <div class="item item-divider" ng-if="!search.loading && search.results.length">\n <span translate>MARKET.VIEW.MORE_LIKE_THIS</span>\n </div>\n\n <ng-include ng-repeat="rec in search.results" src="\'plugins/market/templates/search/item_record.html\'">\n </ng-include>\n </div>\n </div>\n\n </div>\n </ion-content>\n</ion-view>\n');
$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">\n <i class="icon ion-quote"></i>\n <span ng-bind-html="::rec.description | truncText:500"></span>\n </h4>\n <div ng-if="::!rec.stock" class="badge badge-assertive" translate>MARKET.COMMON.SOLD</div>\n </div>\n </div>\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/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 button-small button-text-positive pull-right ink" ui-sref="app.market_categories">\n <i class="icon ion-android-funnel"></i>\n {{\'MARKET.SEARCH.BTN_SHOW_CATEGORIES\' | translate}}\n </a>\n </div>\n\n <div class="padding-top padding-xs" style="display: block; height: 60px">\n <div class="hidden-xs hidden-sm pull-left">\n\n <a class="button button-text button-small ink" ng-class="{\'button-text-positive\': search.advanced, \'button-text-stable\': !search.advanced}" ng-click="search.advanced=!search.advanced">\n {{\'MARKET.SEARCH.BTN_OPTIONS\' | translate}}\n <i class="icon" ng-class="{\'ion-arrow-down-b\': !search.advanced, \'ion-arrow-up-b\': search.advanced}"></i>\n </a>\n &nbsp;\n\n </div>\n\n <div class="hidden-xs hidden-sm pull-right">\n\n <a class="button button-text button-small ink icon ion-clock" ng-if="!options.type.show" ng-class="{\'button-text-positive\': search.type==\'last\'}" ng-click="doGetLastRecord()">\n {{\'MARKET.SEARCH.BTN_LAST_RECORDS\' | translate}}\n </a>\n &nbsp;\n <a class="button button-text button-small ink icon cion-market-offer" ng-if="options.type.show" ng-class="{\'button-text-positive\': search.type==\'offer\'}" ng-click="toggleAdType(\'offer\')">\n {{\'MARKET.SEARCH.BTN_OFFERS\' | translate}}\n </a>\n\n <a class="button button-text button-small ink icon cion-market-need" ng-if="options.type.show" ng-class="{\'button-text-positive\': search.type==\'need\'}" ng-click="toggleAdType(\'need\')">\n {{\'MARKET.SEARCH.BTN_NEEDS\' | 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 </div>\n </div>\n\n <!-- list of records -->\n <ng-include src="\'plugins/market/templates/search/list_records_lg.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-compose hidden-md hidden-lg spin" ng-click="showNewRecordModal()">\n </button>\n</ion-view>\n');
$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;
});
};
}