Commit 6c195d08 authored by Benoit Lavenier's avatar Benoit Lavenier

[enh] ES: allow to search in private messages - fix #647

parent 71fbd200
......@@ -134,7 +134,11 @@
"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"
......
......@@ -134,7 +134,11 @@
"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"
......
......@@ -134,7 +134,11 @@
"LIST": {
"INBOX": "Bandeja de entrada",
"OUTBOX": "Mensajes mandados",
"LAST_INBOX": "Nuevos mensajes",
"LAST_OUTBOX": "Mensajes enviados",
"BTN_LAST_MESSAGES": "Mensajes recientes",
"TITLE": "Mensajes",
"SEARCH_HELP": "Buscar en mensajes",
"POPOVER_ACTIONS": {
"TITLE": "Opciónes",
"DELETE_ALL": "Suprimir todos los mensajes"
......@@ -156,7 +160,8 @@
"TITLE": "Mensaje",
"SENDER": "Mandado por",
"RECIPIENT": "Mandado a",
"NO_CONTENT": "Mensaje vacío"
"NO_CONTENT": "Mensaje vacío",
"DELETE": "Eliminar el mensaje"
},
"CONFIRM": {
"REMOVE": "Está usted segura/o querer <b>suprimir este mensaje</b> ?<br/><br/>Esta operación es ireversible.",
......@@ -279,14 +284,16 @@
"BTN_ADVANCED_SEARCH": "búsqueda avanzada",
"BTN_OPTIONS": "Búsqueda avanzada",
"TYPE": "Tipo de página",
"LOCATION": "Localización",
"LOCATION_HELP": "Ciudad",
"RESULTS": "Resultados",
"RESULT_COUNT_LOCATION": "{{count}} Resultado{{count>0?'s':''}}, cerca de {{location}}",
"RESULT_COUNT": "{{count}} resultado{{count>0?'s':''}}",
"LAST_RECORDS": "Páginas recientes",
"LAST_RECORD_COUNT_LOCATION": "{{count}} página{{count>0?'s':''}} reciente{{count>0?'s':''}}, cerca de {{location}}",
"LAST_RECORD_COUNT": "{{count}} página{{count>0?'s':''}} reciente{{count>0?'s':''}}"
"LAST_RECORD_COUNT": "{{count}} página{{count>0?'s':''}} reciente{{count>0?'s':''}}",
"POPOVER_FILTERS": {
"BTN_ADVANCED_SEARCH": "Opciones avanzadas?"
}
},
"VIEW": {
"TITLE": "Anuario",
......@@ -346,6 +353,8 @@
"NO_PROFILE_DEFINED": "Ningún perfil Cesium+",
"BTN_ADD": "Ingresar mi perfil",
"BTN_EDIT": "Editar mi perfil",
"BTN_DELETE": "Eliminar mi perfil",
"BTN_REORDER": "Reordenar",
"UID": "Seudónimo",
"TITLE": "Nombre, Apellido",
"TITLE_HELP": "Nombre, Apellido",
......@@ -362,13 +371,18 @@
"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ás seguro de que quieres <b>eliminar tu perfil Cesium+?</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": {
......@@ -490,6 +504,9 @@
"UPDATE_REPLY_COMMENT": "<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> 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 inalcanzable 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 inalcanzable o no válida.<br/><br/>Cesium continuará funcionando, <b>sin la extensión Cesium+</b> (perfiles de usuario, mensajes privados), mapas y gráficos).<br/><br/>Verifique su conexión a Internet, o cambie el nodo de datos en <a class=\"positive\" ng-click=\"doQuickFix('settings')\"> configuración de extensión </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."
......
......@@ -134,7 +134,11 @@
"LIST": {
"INBOX": "Boite 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"
......
......@@ -117,12 +117,23 @@ angular.module('cesium.es.message.controllers', ['cesium.es.services'])
;
function ESMessageAbstractListController($scope, $state, $translate, $ionicHistory, $ionicPopover, $timeout,
function ESMessageAbstractListController($scope, $state, $translate, $ionicHistory, $ionicPopover, $timeout, $filter,
csWallet, esModals, UIUtils, esMessage) {
'ngInject';
$scope.loading = true;
$scope.messages = [];
var defaultSearchLimit = 40;
$scope.search = {
loading: true,
results: [],
hasMore : false,
loadingMore : false,
limit: defaultSearchLimit,
type: 'last',
text: null,
options: {
}
};
$scope.fabButtonNewMessageId = undefined;
......@@ -154,28 +165,39 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
return $scope.load(undefined, undefined, silent);
};
$scope.load = function(size, offset, silent) {
var options = {};
options.from = offset || 0;
options.size = size || 20;
$scope.load = function(from, size, silent) {
var options = angular.copy($scope.search.options);
options.from = options.from || from || 0;
options.size = options.size || size || defaultSearchLimit;
options.type = $scope.type;
options.summary = false;
options.filter = ($scope.search.type == 'text' && $scope.search.text && $scope.search.text.trim().length > 0) ?
$scope.search.text : undefined;
$scope.loading = !silent;
$scope.search.loading = !silent;
return esMessage.load(options)
.then(function(messages) {
$scope.messages = messages;
.then(function(res) {
UIUtils.loading.hide();
$scope.loading = false;
if (messages.length > 0) {
$scope.motion.show({selector: '.view-messages .list .item'});
if (!options.from) {
$scope.search.results = res || [];
}
else if (res){
$scope.search.results = $scope.search.results.concat(res);
}
UIUtils.loading.hide();
$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 (!options.from) {
$scope.search.results = [];
}
$scope.search.hasMore = false;
UIUtils.onError('MESSAGE.ERROR.LOAD_MESSAGES_FAILED')(err);
$scope.messages = [];
$scope.loading = false;
});
};
......@@ -184,16 +206,39 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
$scope.load();
};
$scope.updateView = function() {
if ($scope.motion && $scope.motion.ionListClass && $scope.search.results.length) {
$scope.motion.show({selector: '.view-messages .list .item'});
}
};
$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,
true /*silent*/)
.then(function() {
$scope.search.loadingMore = false;
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
$scope.markAllAsRead = function() {
$scope.hideActionsPopover();
if (!$scope.messages || !$scope.messages.length) return;
if (!$scope.search.results || !$scope.search.results.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){
_.forEach($scope.search.results, function(msg){
msg.read = true;
});
})
......@@ -203,7 +248,7 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
};
$scope.delete = function(index) {
var message = $scope.messages[index];
var message = $scope.search.results[index];
if (!message) return;
UIUtils.alert.confirm('MESSAGE.CONFIRM.REMOVE')
......@@ -211,7 +256,7 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
if (confirm) {
esMessage.remove(message.id, $scope.type)
.then(function () {
$scope.messages.splice(index,1); // remove from messages array
$scope.search.results.splice(index,1); // remove from messages array
UIUtils.toast.show('MESSAGE.INFO.MESSAGE_REMOVED');
})
.catch(UIUtils.onError('MESSAGE.ERROR.REMOVE_MESSAGE_FAILED'));
......@@ -221,14 +266,14 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
$scope.deleteAll = function() {
$scope.hideActionsPopover();
if (!$scope.messages || !$scope.messages.length) return;
if (!$scope.search.results || !$scope.search.results.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
$scope.search.results.splice(0,$scope.search.results.length); // reset array
UIUtils.toast.show('MESSAGE.INFO.All_MESSAGE_REMOVED');
})
.catch(UIUtils.onError('MESSAGE.ERROR.REMOVE_All_MESSAGES_FAILED'));
......@@ -236,6 +281,26 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
});
};
$scope.doSearchLast = function() {
$scope.search.type='last';
$scope.search.loadingMore=false;
$scope.search.limit = defaultSearchLimit;
return $scope.load();
};
$scope.doSearch = function() {
if (!$scope.search.text || $scope.search.text.length < 3) {
return;
}
$scope.search.type='text';
$scope.search.loadingMore=false;
$scope.search.results = [];
$scope.search.limit = defaultSearchLimit;
console.debug('[message] [{0}] Searching for: {1}'.format($scope.type, $scope.search.text));
return $scope.load();
};
/* -- Modals -- */
$scope.showNewMessageModal = function(parameters) {
......@@ -250,7 +315,7 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
};
$scope.showReplyModal = function(index) {
var message = $scope.messages[index];
var message = $scope.search.results[index];
if (!message) return;
$translate('MESSAGE.REPLY_TITLE_PREFIX')
......@@ -301,11 +366,11 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
// Message deletion
$scope.onMessageDelete = function(id) {
var index = _.findIndex($scope.messages, function(msg) {
var index = _.findIndex($scope.search.results, function(msg) {
return msg.id == id;
});
if (index) {
$scope.messages.splice(index,1); // remove from messages array
$scope.search.results.splice(index,1); // remove from messages array
}
};
esMessage.api.data.on.delete($scope, $scope.onMessageDelete);
......@@ -320,7 +385,7 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
return esMessage.get(id, {type: $scope.type, summary: true});
}, 500 /*waiting ES propagation*/)
.then(function(msg) {
$scope.messages.splice(0,0,msg);
$scope.search.results.splice(0,0,msg);
$scope.loading = false;
$scope.motion.show({selector: '.view-messages .list .item'});
})
......@@ -338,12 +403,12 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
// 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.search.results.splice(0,0,msg);
$scope.search.loading = false;
$scope.motion.show({selector: '.view-messages .list .item'});
})
.catch(function() {
$scope.loading = false;
$scope.search.loading = false;
});
};
esMessage.api.data.on.new($scope, $scope.onNewInboxMessage);
......@@ -351,8 +416,8 @@ function ESMessageAbstractListController($scope, $state, $translate, $ionicHisto
// Watch unauth
$scope.onUnauth = function() {
// Reset all data
$scope.messages = undefined;
$scope.loading = false;
$scope.search.results = undefined;
$scope.search.loading = false;
$scope.entered = false;
};
csWallet.api.data.on.unauth($scope, $scope.onUnauth);
......
......@@ -77,7 +77,7 @@ function NotificationsController($scope, $rootScope, $ionicPopover, $state, $tim
$scope.search.loading = !silent;
return esNotification.load(csWallet.data.pubkey, options)
.then(function(res) {
if (!from) {
if (!options.from) {
$scope.search.results = res || [];
}
else if (res){
......@@ -89,7 +89,7 @@ function NotificationsController($scope, $rootScope, $ionicPopover, $state, $tim
})
.catch(function(err) {
$scope.search.loading = false;
if (!from) {
if (!options.from) {
$scope.search.results = [];
}
$scope.search.hasMore = false;
......
......@@ -228,8 +228,10 @@ angular.module('cesium.es.message.services', ['ngResource', 'cesium.platform',
options.type = options.type || 'inbox';
options._source = fields.commons;
options.summary = angular.isDefined(options.summary) ? options.summary : true;
options.filter = angular.isDefined(options.filter) ? options.filter : undefined;
options.from = options.from || 0;
return csWallet.auth()
var promise = csWallet.auth()
.then(function(walletData) {
// Get encrypted message (with common fields)
......@@ -249,11 +251,35 @@ angular.module('cesium.es.message.services', ['ngResource', 'cesium.platform',
// Update message count
.then(function(messages){
csWallet.data.messages = csWallet.data.messages || {};
csWallet.data.messages.count = messages.length;
if (messages.length && options.filter){
var filteredMessages = filterMessages(messages, options.filter);
// Recursive loop, if need more
if (filteredMessages.length < messages.length) {
options = angular.copy(options);
options.from += options.size;
options.size = messages.length - filteredMessages.length;
return loadMessages(options)
.then(function(messages) {
return filteredMessages.concat(messages);
});
}
}
if (options.from === 0 && !options.filter) {
csWallet.data.messages = csWallet.data.messages || {};
csWallet.data.messages.count = messages.length;
}
return messages;
});
// If filter, apply sorting (only once)
if (options.from === 0 && options.filter) {
promise.then(sortFilteredMessages);
}
return promise;
}
function getAndDecrypt(id, options) {
......@@ -471,6 +497,75 @@ angular.module('cesium.es.message.services', ['ngResource', 'cesium.platform',
});
}
// Filter messages (after decryption) searching on [title, content]
function filterMessages(messages, filter) {
if (filter && !filter.trim().length) return messages;
// Init summary, removing reply content (lines starting with '>')
messages.forEach(function(msg) {
if (msg.content) {
msg.summary = msg.content.replace(/(^|[\n\r]+)\s*>[^\n\r]*/g, '').trim() || '';
}
});
// For each search words
var words = filter.trim().split(' ');
words.forEach(function(word) {
var regexp = new RegExp(word, 'gi');
messages.forEach(function(msg) {
// Search on title
var matches = regexp.exec(msg.title);
if (matches) {
msg.title = msg.title.replace(regexp, '<b>$&</b>');
msg.titleMatch = (msg.titleMatch || 0) + 1;
while(true) {
matches = regexp.exec(msg.title.substring(matches.index + word.length));
if (!matches || msg.titleMatch >= 10) break;
msg.titleMatch = msg.titleMatch + 1;
}
return;
}
// Search on summary
matches = regexp.exec(msg.summary);
if (matches) {
if (matches.index > 140) {
msg.summary = '...' + msg.summary.substring(matches.index - 20);
}
msg.summary = msg.summary.replace(regexp, '<b>$&</b>');
msg.contentMatch = (msg.contentMatch || 0) + 1;
while(true) {
matches = regexp.exec(msg.summary.substring(matches.index + word.length));
if (!matches || msg.contentMatch >= 10) break;
msg.contentMatch++;
}
if (msg.summary.length > 140) {
msg.summary = msg.summary.substr(0, 137) + '...';
}
}
});
});
// Keep only matches
messages = _.filter(messages, function(msg) {
return msg.titleMatch || msg.contentMatch;
});
return messages;
}
// Sort filtered messages by matches
function sortFilteredMessages(messages) {
// Sort by matches
return _.sortBy(messages, function(msg) {
return -1 * (
1000 * (msg.titleMatch || 0) +
100 * (msg.contentMatch || 0) +
(msg.time / 10000000000));
});
}
// Send message to developers - need for issue #524
function onSendError(message) {
var developers = csConfig.developers || [{pubkey: '38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE'/*kimamila*/}];
......
<div class="padding gray" ng-if="!search.loading && !search.results.length">
<span ng-if="search.type=='last'">{{ ('MESSAGE.NO_MESSAGE_' + type) | upper | translate }}</span>
<span ng-if="search.type=='text'" translate>COMMON.SEARCH_NO_RESULT</span>
</div>
<ion-list class="{{::motion.ionListClass}}"
can-swipe="$root.device.enable"
>
<div class="padding gray" ng-if="!messages.length">
<span ng-if="type=='inbox'" translate>MESSAGE.NO_MESSAGE_INBOX</span>
<span ng-if="type=='outbox'" translate>MESSAGE.NO_MESSAGE_OUTBOX</span>
</div>
can-swipe="$root.device.enable">
<ion-item
class="item item-border-large item-avatar item-icon-right ink"
ng-repeat="msg in messages"
ng-repeat="msg in search.results"
ui-sref="app.user_view_message({type:type, id:msg.id})">
<i ng-if="::!msg.avatar" class="item-image icon" ng-class="{'ion-person': msg.uid, 'ion-email': !msg.uid}"></i>
......@@ -35,8 +32,8 @@
{{::msg.issuer|formatPubkey}}
</a>
</h3>
<h2 ng-class="{'unread': !msg.read}">{{::msg.title}}</h2>
<p>{{::msg.summary||msg.content}}</p>
<h2 ng-class="{'unread': !msg.read}" ng-bind-html="::msg.title"></h2>
<p ng-bind-html="::msg.summary||msg.content"></p>
<i class="icon ion-ios-arrow-right "></i>
<ion-option-button class="button-stable"
ng-click="showReplyModal($index)"
......@@ -47,3 +44,10 @@
</ion-item>
</ion-list>
<ion-infinite-scroll
ng-if="!search.loading && search.hasMore"
icon="ion-loading-c"
on-infinite="showMore()"
distance="10%">
</ion-infinite-scroll>
......@@ -14,5 +14,4 @@
</ion-tabs>
</ion-view>
......@@ -26,14 +26,14 @@
{{'MESSAGE.BTN_COMPOSE' | translate}}
</button>
<button class="button button-stable icon-right ink"
ng-click="showActionsPopover($event)">
&nbsp; <i class="icon ion-android-more-vertical"></i>&nbsp;
{{'COMMON.BTN_OPTIONS' | translate}}
<button class="button button-stable button-small-padding icon ion-android-more-vertical"
ng-click="showActionsPopover($event)"
title="{{'COMMON.POPOVER_ACTIONS_TITLE' | translate}}">
</button>
</div>
</div>
<!-- button tabs -->
<div class="buttons-tabs border-bottom hidden-sm hidden-xs">
<div class="pull-right">
......@@ -56,16 +56,69 @@
{{'MESSAGE.LIST.OUTBOX' | translate}}
</a>
</div>
</div>
<div class="item no-padding">
<div class="item-input ">
<i class="icon ion-search placeholder-icon"></i>
<input type="text"
class="visible-xs visible-sm"
placeholder="{{'MESSAGE.LIST.SEARCH_HELP'|translate}}"
ng-model="search.text"
ng-model-options="{ debounce: 650 }"
ng-change="doSearch()"
on-return="doSearch()"
select-on-click>
<input type="text"
class="hidden-xs hidden-sm"
placeholder="{{'MESSAGE.LIST.SEARCH_HELP'|translate}}"
ng-model="search.text"
on-return="doSearch()">
<div class="helptip-anchor-center">
<a id="helptip-message-search-text"></a>
</div>
</div>
</div>
<div class="padding-top hidden-xs" style="display: block; height: 60px;">
<div class="pull-left" ng-if="!search.loading">
<ng-if ng-if="search.type=='last'">
<h4>{{ ('MESSAGE.LIST.LAST_' + type) | upper | translate}}</h4>
<!--<small class="gray no-padding" ng-if="search.total">{{'WOT.LOOKUP.NEWCOMERS_COUNT'|translate:{count: search.total} }}</small>-->
</ng-if>
<ng-if ng-if="search.type=='text'">
<h4 translate>COMMON.RESULTS_LIST</h4>
<!--<small class="gray no-padding" ng-if="search.total">{{'WOT.LOOKUP.PENDING_COUNT'|translate:{count: search.total} }}</small>-->
</ng-if>
</div>
<div class="pull-right hidden-xs hidden-sm">
<a class="button button-text button-small ink"
ng-class="{'button-text-positive': search.type=='last'}"
ng-click="doSearchLast()">
<i class="icon ion-clock"></i>
{{'MESSAGE.LIST.BTN_LAST_MESSAGES' | translate}}
</a>
<!-- Allow extension here -->
<cs-extension-point name="filter-buttons"></cs-extension-point>
&nbsp;
<button class="button button-small button-stable ink"
ng-click="doSearch()">
{{'COMMON.BTN_SEARCH' | translate}}
</button>
</div>
</div>
<div class="center padding" ng-if="loading">
<div class="center padding" ng-if="search.loading">
<ion-spinner icon="android"></ion-spinner>
</div>
<!-- list -->
<ng-include src="'plugins/es/templates/message/list.html'" ng-hide="loading"></ng-include>
<ng-include src="'plugins/es/templates/message/list.html'"></ng-include>
</ion-content>
......
......@@ -19,8 +19,8 @@
<!-- list -->
<ng-include src="'plugins/es/templates/message/list.html'"></ng-include>
</ion-content>
</ion-content>
<!-- fab button -->
<div class="visible-xs visible-sm" >
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment