MOBILE-2327 messages: Fix errors from Peer review

main
Pau Ferrer Ocaña 2018-03-08 13:25:20 +01:00
parent 06b8bcc026
commit f938b37f73
39 changed files with 309 additions and 320 deletions

View File

@ -7,7 +7,7 @@
"email": "mobile@moodle.com" "email": "mobile@moodle.com"
}, },
"config": { "config": {
"ionic_webpack": "./webpack.config.js" "ionic_webpack": "./config/webpack.config.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -3,7 +3,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-search-box (onSubmit)="search($event)" (onClear])="clearSearch($event)" [placeholder]=" 'addon.messages.contactname' | translate" autocorrect="off" spellcheck="false" lengthCheck="2"></core-search-box> <core-search-box (onSubmit)="search($event)" (onClear])="clearSearch($event)" [placeholder]=" 'addon.messages.contactname' | translate" autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded"></core-search-box>
<core-loading [hideUntil]="loaded" [message]="loadingMessage"> <core-loading [hideUntil]="loaded" [message]="loadingMessage">
<core-empty-box *ngIf="!hasContacts && searchString == ''" icon="person" [message]="'addon.messages.contactlistempty' | translate"></core-empty-box> <core-empty-box *ngIf="!hasContacts && searchString == ''" icon="person" [message]="'addon.messages.contactlistempty' | translate"></core-empty-box>

View File

@ -134,8 +134,6 @@ export class AddonMessagesContactsComponent {
this.clearSearch(); this.clearSearch();
}).catch((error) => { }).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true);
return Promise.reject(null);
}); });
} }
@ -202,8 +200,6 @@ export class AddonMessagesContactsComponent {
this.contacts['search'] = this.sortUsers(result); this.contacts['search'] = this.sortUsers(result);
}).catch((error) => { }).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true);
return Promise.reject(null);
}); });
} }

View File

@ -3,10 +3,10 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-search-box *ngIf="search.enabled" (onSubmit)="searchMessage($event)" (onClear)="clearSearch($event)" [placeholder]=" 'addon.messages.message' | translate" autocorrect="off" spellcheck="false" lengthCheck="2"></core-search-box> <core-search-box *ngIf="search.enabled" (onSubmit)="searchMessage($event)" (onClear)="clearSearch($event)" [placeholder]=" 'addon.messages.message' | translate" autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded"></core-search-box>
<core-loading [hideUntil]="loaded" [message]="loadingMessage"> <core-loading [hideUntil]="loaded" [message]="loadingMessage">
<!-- Message telling there are no files. -->
<core-empty-box *ngIf="(!discussions || discussions.length <= 0) && !search.showResults" icon="chatbubbles" [message]="'addon.messages.nomessages' | translate"></core-empty-box> <core-empty-box *ngIf="(!discussions || discussions.length <= 0) && !search.showResults" icon="chatbubbles" [message]="'addon.messages.nomessages' | translate"></core-empty-box>
<core-empty-box *ngIf="(!search.results || search.results.length <= 0) && search.showResults" icon="search" [message]="'core.noresults' | translate"></core-empty-box> <core-empty-box *ngIf="(!search.results || search.results.length <= 0) && search.showResults" icon="search" [message]="'core.noresults' | translate"></core-empty-box>
@ -33,7 +33,7 @@
<h2> <h2>
<core-format-text [text]="discussion.fullname"></core-format-text> <core-format-text [text]="discussion.fullname"></core-format-text>
<ion-note *ngIf="discussion.message.timecreated > 0 || discussion.unread"> <ion-note *ngIf="discussion.message.timecreated > 0 || discussion.unread">
<div *ngIf="discussion.message.timecreated> 0">{{discussion.message.timecreated / 1000 | coreDateDayOrTime}}</div> <div *ngIf="discussion.message.timecreated > 0">{{discussion.message.timecreated / 1000 | coreDateDayOrTime}}</div>
<div text-right *ngIf="discussion.unread" class="core-primary-circle"></div> <div text-right *ngIf="discussion.unread" class="core-primary-circle"></div>
</ion-note> </ion-note>
</h2> </h2>

View File

@ -42,7 +42,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
loadingMessage: string; loadingMessage: string;
discussions: any; discussions: any;
discussionUserId: number; discussionUserId: number;
messageId: number; pushObserver: any;
search = { search = {
enabled: false, enabled: false,
showResults: false, showResults: false,
@ -54,7 +54,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService, constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService,
private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider, private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider,
private pushNotificationsDelegate: AddonPushNotificationsDelegate) { pushNotificationsDelegate: AddonPushNotificationsDelegate) {
this.search.loading = translate.instant('core.searching'); this.search.loading = translate.instant('core.searching');
this.loadingMessages = translate.instant('core.loading'); this.loadingMessages = translate.instant('core.loading');
@ -114,7 +114,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
this.discussionUserId = navParams.get('discussionUserId') || false; this.discussionUserId = navParams.get('discussionUserId') || false;
// If a message push notification is received, refresh the view. // If a message push notification is received, refresh the view.
pushNotificationsDelegate.registerReceiveHandler('AddonMessagesDiscussionsComponent', (notification) => { this.pushObserver = pushNotificationsDelegate.on('receive').subscribe((notification) => {
// New message received. If it's from current site, refresh the data. // New message received. If it's from current site, refresh the data.
if (utils.isFalseOrZero(notification.notif) && notification.site == this.siteId) { if (utils.isFalseOrZero(notification.notif) && notification.site == this.siteId) {
this.refreshData(); this.refreshData();
@ -211,7 +211,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
this.search.showResults = true; this.search.showResults = true;
this.search.results = searchResults; this.search.results = searchResults;
}).catch((error) => { }).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'mma.messages.errorwhileretrievingmessages', true); this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
}).finally(() => { }).finally(() => {
this.loaded = true; this.loaded = true;
}); });
@ -233,7 +233,6 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
}; };
if (messageId) { if (messageId) {
params['message'] = messageId; params['message'] = messageId;
this.messageId = messageId;
} }
this.eventsProvider.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_EVENT, params, this.siteId); this.eventsProvider.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_EVENT, params, this.siteId);
} }
@ -246,6 +245,6 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
this.readChangedObserver && this.readChangedObserver.off(); this.readChangedObserver && this.readChangedObserver.off();
this.cronObserver && this.cronObserver.off(); this.cronObserver && this.cronObserver.off();
this.appResumeSubscription && this.appResumeSubscription.unsubscribe(); this.appResumeSubscription && this.appResumeSubscription.unsubscribe();
this.pushNotificationsDelegate.unregisterReceiveHandler('AddonMessagesDiscussionsComponent'); this.pushObserver && this.pushObserver.unsubscribe();
} }
} }

View File

@ -105,7 +105,7 @@ export class AddonMessagesModule {
} }
// Register push notification clicks. // Register push notification clicks.
pushNotificationsDelegate.registerHandler('mmaMessages', (notification) => { pushNotificationsDelegate.on('click').subscribe((notification) => {
if (utils.isFalseOrZero(notification.notif)) { if (utils.isFalseOrZero(notification.notif)) {
notificationClicked(notification); notificationClicked(notification);

View File

@ -172,7 +172,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
}).catch((error) => { }).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
}).finally(() => { }).finally(() => {
this.triggerDiscussionLoadedEvent(); this.checkCanDelete();
this.resizeContent(); this.resizeContent();
this.loaded = true; this.loaded = true;
}); });
@ -201,9 +201,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
protected fetchData(): Promise<any> { protected fetchData(): Promise<any> {
this.logger.debug(`Polling new messages for discussion with user '${this.userId}'`); this.logger.debug(`Polling new messages for discussion with user '${this.userId}'`);
if (this.messagesBeingSent > 0) { if (this.messagesBeingSent > 0) {
// We do not poll while a message is being sent or we could confuse the user // We do not poll while a message is being sent or we could confuse the user.
// as his message would disappear from the list, and he'd have to wait for the // Otherwise, his message would disappear from the list, and he'd have to wait for the interval to check for messages.
// interval to check for new messages.
return Promise.reject(null); return Promise.reject(null);
} else if (this.fetching) { } else if (this.fetching) {
// Already fetching. // Already fetching.
@ -384,7 +383,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
// Update navBar links and buttons. // Update navBar links and buttons.
const newCanDelete = (last && last.id && this.messages.length == 1) || this.messages.length > 1; const newCanDelete = (last && last.id && this.messages.length == 1) || this.messages.length > 1;
if (this.canDelete != newCanDelete) { if (this.canDelete != newCanDelete) {
this.triggerDiscussionLoadedEvent(); this.checkCanDelete();
} }
} }
} }
@ -425,7 +424,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
/** /**
* Check if there's any message in the list that can be deleted. * Check if there's any message in the list that can be deleted.
*/ */
protected triggerDiscussionLoadedEvent(): void { protected checkCanDelete(): void {
// All messages being sent should be at the end of the list. // All messages being sent should be at the end of the list.
const first = this.messages[0]; const first = this.messages[0];
this.canDelete = first && !first.sending; this.canDelete = first && !first.sending;
@ -503,7 +502,6 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.utils.copyToClipboard(text); this.utils.copyToClipboard(text);
} }
/** /**
* Function to delete a message. * Function to delete a message.
* *
@ -627,8 +625,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
} }
promise.catch(() => { promise.catch(() => {
// Fetch failed or is offline message, mark the message as sent. If fetch is successful there's no need // Fetch failed or is offline message, mark the message as sent.
// to mark it because the fetch will already show the message received from the server. // If fetch is successful there's no need to mark it because the fetch will already show the message received.
message.sending = false; message.sending = false;
if (data.sent) { if (data.sent) {
// Message sent to server, not pending anymore. // Message sent to server, not pending anymore.
@ -642,8 +640,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
}).catch((error) => { }).catch((error) => {
this.messagesBeingSent--; this.messagesBeingSent--;
// Only close the keyboard if an error happens, we want the user to be able to send multiple // Only close the keyboard if an error happens.
// messages without the keyboard being closed. // We want the user to be able to send multiple messages without the keyboard being closed.
this.appProvider.closeKeyboard(); this.appProvider.closeKeyboard();
this.domUtils.showErrorModalDefault(error, 'addon.messages.messagenotsent', true); this.domUtils.showErrorModalDefault(error, 'addon.messages.messagenotsent', true);

View File

@ -17,8 +17,6 @@ import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { AddonMessagesIndexPage } from './index'; import { AddonMessagesIndexPage } from './index';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives';
import { CorePipesModule } from '@pipes';
import { AddonMessagesComponentsModule } from '../../components/components.module'; import { AddonMessagesComponentsModule } from '../../components/components.module';
@NgModule({ @NgModule({
@ -27,8 +25,6 @@ import { AddonMessagesComponentsModule } from '../../components/components.modul
], ],
imports: [ imports: [
CoreComponentsModule, CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
AddonMessagesComponentsModule, AddonMessagesComponentsModule,
IonicPageModule.forChild(AddonMessagesIndexPage), IonicPageModule.forChild(AddonMessagesIndexPage),
TranslateModule.forChild() TranslateModule.forChild()

View File

@ -48,6 +48,7 @@ export class AddonMessagesSettingsPage implements OnDestroy {
/** /**
* Fetches preference data. * Fetches preference data.
*
* @return {Promise<any>} Resolved when done. * @return {Promise<any>} Resolved when done.
*/ */
protected fetchPreferences(): Promise<any> { protected fetchPreferences(): Promise<any> {
@ -85,6 +86,7 @@ export class AddonMessagesSettingsPage implements OnDestroy {
/** /**
* Block non contacts. * Block non contacts.
*
* @param {boolean} block If it should be blocked or not. * @param {boolean} block If it should be blocked or not.
*/ */
blockNonContacts(block: boolean): void { blockNonContacts(block: boolean): void {
@ -103,6 +105,7 @@ export class AddonMessagesSettingsPage implements OnDestroy {
/** /**
* Change the value of a certain preference. * Change the value of a certain preference.
*
* @param {any} notification Notification object. * @param {any} notification Notification object.
* @param {string} state State name, ['loggedin', 'loggedoff']. * @param {string} state State name, ['loggedin', 'loggedoff'].
* @param {any} processor Notification processor. * @param {any} processor Notification processor.

View File

@ -56,7 +56,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
}); });
// If a message push notification is received, refresh the count. // If a message push notification is received, refresh the count.
pushNotificationsDelegate.registerReceiveHandler('AddonMessagesMainMenuHandler', (notification) => { pushNotificationsDelegate.on('receive').subscribe((notification) => {
// New message received. If it's from current site, refresh the data. // New message received. If it's from current site, refresh the data.
if (utils.isFalseOrZero(notification.notif) && this.sitesProvider.isCurrentSite(notification.site)) { if (utils.isFalseOrZero(notification.notif) && this.sitesProvider.isCurrentSite(notification.site)) {
this.updateBadge(notification.site); this.updateBadge(notification.site);
@ -64,7 +64,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
}); });
// Register Badge counter. // Register Badge counter.
pushNotificationsDelegate.registerCounterHandler('mmaMessages'); pushNotificationsDelegate.registerCounterHandler('AddonMessages');
} }
/** /**
@ -91,7 +91,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
title: 'addon.messages.messages', title: 'addon.messages.messages',
page: 'AddonMessagesIndexPage', page: 'AddonMessagesIndexPage',
class: 'addon-messages-handler', class: 'addon-messages-handler',
showBadge: true, // Do not check isMessageCountEnabled because we'll use fallback it not enabled., showBadge: true, // Do not check isMessageCountEnabled because we'll use fallback it not enabled.
badge: this.badge, badge: this.badge,
loading: this.loading loading: this.loading
}; };
@ -112,7 +112,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
// Leave badge enter if there is a 0+ or a 0. // Leave badge enter if there is a 0+ or a 0.
this.badge = parseInt(unread, 10) > 0 ? unread : ''; this.badge = parseInt(unread, 10) > 0 ? unread : '';
// Update badge. // Update badge.
this.pushNotificationsProvider.updateAddonCounter('mmaMessages', unread, siteId); this.pushNotificationsProvider.updateAddonCounter('AddonMessages', unread, siteId);
}).catch(() => { }).catch(() => {
this.badge = ''; this.badge = '';
}).finally(() => { }).finally(() => {

View File

@ -26,7 +26,7 @@ export class AddonMessagesOfflineProvider {
protected logger; protected logger;
// Variables for database. // Variables for database.
protected MESSAGES_TABLE = 'mma_messages_offline_messages'; protected MESSAGES_TABLE = 'addon_messages_offline_messages';
protected tablesSchema = [ protected tablesSchema = [
{ {
name: this.MESSAGES_TABLE, name: this.MESSAGES_TABLE,
@ -140,7 +140,7 @@ export class AddonMessagesOfflineProvider {
*/ */
saveMessage(toUserId: number, message: string, siteId?: string): Promise<any> { saveMessage(toUserId: number, message: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
const entry = { const entry = {
touserid: toUserId, touserid: toUserId,
useridfrom: site.getUserId(), useridfrom: site.getUserId(),
smallmessage: message, smallmessage: message,

View File

@ -29,10 +29,10 @@ export class AddonMessagesProvider {
protected ROOT_CACHE_KEY = 'mmaMessages:'; protected ROOT_CACHE_KEY = 'mmaMessages:';
protected LIMIT_MESSAGES = 50; protected LIMIT_MESSAGES = 50;
protected LIMIT_SEARCH_MESSAGES = 50; protected LIMIT_SEARCH_MESSAGES = 50;
static NEW_MESSAGE_EVENT = 'new_message_event'; static NEW_MESSAGE_EVENT = 'addon_messages_new_message_event';
static READ_CHANGED_EVENT = 'read_changed_event'; static READ_CHANGED_EVENT = 'addon_messages_read_changed_event';
static READ_CRON_EVENT = 'read_cron_event'; static READ_CRON_EVENT = 'addon_messages_read_cron_event';
static SPLIT_VIEW_LOAD_EVENT = 'split_view_load_event'; static SPLIT_VIEW_LOAD_EVENT = 'addon_messages_split_view_load_event';
static POLL_INTERVAL = 10000; static POLL_INTERVAL = 10000;
static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation'; static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation';
@ -148,6 +148,16 @@ export class AddonMessagesProvider {
return this.ROOT_CACHE_KEY + 'discussion:' + userId; return this.ROOT_CACHE_KEY + 'discussion:' + userId;
} }
/**
* Get the cache key for the message count.
*
* @param {number} userId User ID.
* @return {string} Cache key.
*/
protected getCacheKeyForMessageCount(userId: number): string {
return this.ROOT_CACHE_KEY + 'count:' + userId;
}
/** /**
* Get the cache key for the list of discussions. * Get the cache key for the list of discussions.
* *
@ -267,8 +277,8 @@ export class AddonMessagesProvider {
hasSent; hasSent;
if (lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0) { if (lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0) {
// Do not use cache when retrieving older messages. This is to prevent storing too much data // Do not use cache when retrieving older messages.
// and to prevent inconsistencies between "pages" loaded. // This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded.
preSets['getFromCache'] = false; preSets['getFromCache'] = false;
preSets['saveToCache'] = false; preSets['saveToCache'] = false;
preSets['emergencyCache'] = false; preSets['emergencyCache'] = false;
@ -566,9 +576,10 @@ export class AddonMessagesProvider {
useridto: userId useridto: userId
}, },
preSets = { preSets = {
cacheKey: this.getCacheKeyForMessageCount(userId),
getFromCache: false, getFromCache: false,
emergencyCache: false, emergencyCache: true,
saveToCache: false, saveToCache: true,
typeExpected: 'number' typeExpected: 'number'
}; };
@ -590,11 +601,11 @@ export class AddonMessagesProvider {
return this.getMessages(params, undefined, false, siteId).then((response) => { return this.getMessages(params, undefined, false, siteId).then((response) => {
// Count the discussions by filtering same senders. // Count the discussions by filtering same senders.
const discussions = {}; const discussions = {};
let count;
response.messages.forEach((message) => { response.messages.forEach((message) => {
discussions[message.useridto] = 1; discussions[message.useridto] = 1;
}); });
count = Object.keys(discussions).length; const count = Object.keys(discussions).length;
// Add + sign if there are more than the limit reachable. // Add + sign if there are more than the limit reachable.
return (count > this.LIMIT_MESSAGES) ? count + '+' : count; return (count > this.LIMIT_MESSAGES) ? count + '+' : count;
@ -700,9 +711,11 @@ export class AddonMessagesProvider {
*/ */
invalidateDiscussionsCache(siteId?: string): Promise<any> { invalidateDiscussionsCache(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getCacheKeyForDiscussions()).then(() => { const promises = [];
return this.invalidateContactsCache(site.getId()); promises.push(site.invalidateWsCacheForKey(this.getCacheKeyForDiscussions()));
}); promises.push(this.invalidateContactsCache(site.getId()));
return Promise.all(promises);
}); });
} }
@ -797,12 +810,10 @@ export class AddonMessagesProvider {
* @return {Promise<any>} Resolved when enabled, otherwise rejected. * @return {Promise<any>} Resolved when enabled, otherwise rejected.
*/ */
isMessagingEnabledForSite(siteId?: string): Promise<any> { isMessagingEnabledForSite(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.isPluginEnabled(siteId).then((enabled) => {
if (!site.canUseAdvancedFeature('messaging')) { if (!enabled) {
return Promise.reject(null); return Promise.reject(null);
} }
return Promise.resolve(true);
}); });
} }
@ -986,14 +997,14 @@ export class AddonMessagesProvider {
// Online and no messages stored. Send it to server. // Online and no messages stored. Send it to server.
return this.sendMessageOnline(toUserId, message).then(() => { return this.sendMessageOnline(toUserId, message).then(() => {
return { sent: true }; return { sent: true };
}).catch((data) => { }).catch((error) => {
if (data.wserror) { if (this.utils.isWebServiceError(error)) {
// It's a WebService error, the user cannot send the message so don't store it. // It's a WebService error, the user cannot send the message so don't store it.
return Promise.reject(data.error); return Promise.reject(error);
} else {
// Error sending message, store it to retry later.
return storeOffline();
} }
// Error sending message, store it to retry later.
return storeOffline();
}); });
}); });
} }
@ -1004,9 +1015,7 @@ export class AddonMessagesProvider {
* @param {number} toUserId User ID to send the message to. * @param {number} toUserId User ID to send the message to.
* @param {string} message The message to send * @param {string} message The message to send
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if success, rejected if failure. Reject param is an object with: * @return {Promise<any>} Promise resolved if success, rejected if failure.
* - error: The error message.
* - wserror: True if it's an error returned by the WebService, false otherwise.
*/ */
sendMessageOnline(toUserId: number, message: string, siteId?: string): Promise<any> { sendMessageOnline(toUserId: number, message: string, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
@ -1019,18 +1028,10 @@ export class AddonMessagesProvider {
} }
]; ];
return this.sendMessagesOnline(messages, siteId).catch((error) => { return this.sendMessagesOnline(messages, siteId).then((response) => {
return Promise.reject({
error: error,
wserror: this.utils.isWebServiceError(error)
});
}).then((response) => {
if (response && response[0] && response[0].msgid === -1) { if (response && response[0] && response[0].msgid === -1) {
// There was an error, and it should be translated already. // There was an error, and it should be translated already.
return Promise.reject({ return this.utils.createFakeWSError(response[0].errormessage);
error: response[0].errormessage,
wserror: true
});
} }
return this.invalidateDiscussionCache(toUserId, siteId).catch(() => { return this.invalidateDiscussionCache(toUserId, siteId).catch(() => {

View File

@ -31,7 +31,7 @@ export class AddonMessagesSettingsHandler implements CoreSettingsHandler {
/** /**
* Check if the handler is enabled on a site level. * Check if the handler is enabled on a site level.
* *
* @return {boolean} Whether or not the handler is enabled on a site level. * @return {boolean | Promise<boolean>} Whether or not the handler is enabled on a site level.
*/ */
isEnabled(): boolean | Promise<boolean> { isEnabled(): boolean | Promise<boolean> {
return this.messagesProvider.isMessagePreferencesEnabled(); return this.messagesProvider.isMessagePreferencesEnabled();

View File

@ -21,7 +21,9 @@ import { AddonMessagesOfflineProvider } from './messages-offline';
import { AddonMessagesProvider } from './messages'; import { AddonMessagesProvider } from './messages';
import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUserProvider } from '@core/user/providers/user';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreSyncProvider } from '@providers/sync';
/** /**
* Service to sync messages. * Service to sync messages.
@ -34,8 +36,9 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
protected appProvider: CoreAppProvider, private messagesOffline: AddonMessagesOfflineProvider, protected appProvider: CoreAppProvider, private messagesOffline: AddonMessagesOfflineProvider,
private eventsProvider: CoreEventsProvider, private messagesProvider: AddonMessagesProvider, private eventsProvider: CoreEventsProvider, private messagesProvider: AddonMessagesProvider,
private userProvider: CoreUserProvider, private translate: TranslateService) { private userProvider: CoreUserProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
super('AddonMessagesSync', sitesProvider, loggerProvider, appProvider); syncProvider: CoreSyncProvider) {
super('AddonMessagesSync', sitesProvider, loggerProvider, appProvider, syncProvider);
} }
/** /**
@ -49,7 +52,7 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
syncAllDiscussions(siteId?: string, onlyDeviceOffline: boolean = false): Promise<any> { syncAllDiscussions(siteId?: string, onlyDeviceOffline: boolean = false): Promise<any> {
const syncFunctionLog = 'all discussions' + (onlyDeviceOffline ? ' (Only offline)' : ''); const syncFunctionLog = 'all discussions' + (onlyDeviceOffline ? ' (Only offline)' : '');
return this.syncOnSites(syncFunctionLog, 'syncAllDiscussionsFunc', {onlyDeviceOffline: onlyDeviceOffline}, siteId); return this.syncOnSites(syncFunctionLog, this.syncAllDiscussionsFunc.bind(this), [onlyDeviceOffline], siteId);
} }
/** /**
@ -106,13 +109,12 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
return this.getOngoingSync(userId, siteId); return this.getOngoingSync(userId, siteId);
} }
let syncPromise;
const warnings = []; const warnings = [];
this.logger.debug(`Try to sync discussion with user '${userId}'`); this.logger.debug(`Try to sync discussion with user '${userId}'`);
// Get offline messages to be sent. // Get offline messages to be sent.
syncPromise = this.messagesOffline.getMessages(userId, siteId).then((messages) => { const syncPromise = this.messagesOffline.getMessages(userId, siteId).then((messages) => {
if (!messages.length) { if (!messages.length) {
// Nothing to sync. // Nothing to sync.
return []; return [];
@ -123,33 +125,35 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
return Promise.reject(null); return Promise.reject(null);
} }
let promise: Promise<any>; let promise: Promise<any> = Promise.resolve();
promise = Promise.resolve();
const errors = []; const errors = [];
// Order message by timecreated. // Order message by timecreated.
messages = this.messagesProvider.sortMessages(messages); messages = this.messagesProvider.sortMessages(messages);
// Send the messages. We don't use $mmaMessages#sendMessagesOnline because there's a problem with display order. // Send the messages.
// @todo Use $mmaMessages#sendMessagesOnline once the display order is fixed. // We don't use AddonMessagesProvider#sendMessagesOnline because there's a problem with display order.
// @todo Use AddonMessagesProvider#sendMessagesOnline once the display order is fixed.
messages.forEach((message, index) => { messages.forEach((message, index) => {
// Chain message sending. If 1 message fails to be sent we'll stop sending. // Chain message sending. If 1 message fails to be sent we'll stop sending.
promise = promise.then(() => { promise = promise.then(() => {
return this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId).catch((data) => { return this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId).catch((error) => {
if (data.wserror) { if (this.utils.isWebServiceError(error)) {
// Error returned by WS. Store the error to show a warning but keep sending messages. // Error returned by WS. Store the error to show a warning but keep sending messages.
if (errors.indexOf(data.error) == -1) { if (errors.indexOf(error) == -1) {
errors.push(data.error); errors.push(error);
}
} else {
// Error sending, stop execution.
if (this.appProvider.isOnline()) {
// App is online, unmark deviceoffline if marked.
this.messagesOffline.setMessagesDeviceOffline(messages, false);
} }
return Promise.reject(data.error); return;
} }
// Error sending, stop execution.
if (this.appProvider.isOnline()) {
// App is online, unmark deviceoffline if marked.
this.messagesOffline.setMessagesDeviceOffline(messages, false);
}
return Promise.reject(error);
}).then(() => { }).then(() => {
// Message was sent, delete it from local DB. // Message was sent, delete it from local DB.
return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId);

View File

@ -33,8 +33,8 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
*/ */
static UPDATED_EVENT = 'AddonMessagesAddContactUserHandler_updated_event'; static UPDATED_EVENT = 'AddonMessagesAddContactUserHandler_updated_event';
name = 'mmaMessages:blockContact'; name = 'AddonMessages:addContact';
priority = 600; priority = 800;
type = CoreUserDelegate.TYPE_ACTION; type = CoreUserDelegate.TYPE_ACTION;
protected disabled = false; protected disabled = false;
@ -52,9 +52,9 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
/** /**
* Check if handler is enabled. * Check if handler is enabled.
* *
* @return {Promise<any>} Promise resolved with true if enabled, rejected or resolved with false otherwise. * @return {Promise<boolean>} Promise resolved with true if enabled, rejected or resolved with false otherwise.
*/ */
isEnabled(): Promise<any> { isEnabled(): Promise<boolean> {
return this.messagesProvider.isPluginEnabled(); return this.messagesProvider.isPluginEnabled();
} }
@ -101,14 +101,14 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
return this.domUtils.showConfirm(template, title, title).then(() => { return this.domUtils.showConfirm(template, title, title).then(() => {
return this.messagesProvider.removeContact(user.id); return this.messagesProvider.removeContact(user.id);
}).catch(() => { }, () => {
// Ignore on cancel. // Ignore on cancel.
}); });
} else { } else {
return this.messagesProvider.addContact(user.id); return this.messagesProvider.addContact(user.id);
} }
}).catch((error) => { }).catch((error) => {
this.domUtils.showErrorModal(error); this.domUtils.showErrorModalDefault(error, 'core.error', true);
}).finally(() => { }).finally(() => {
this.eventsProvider.trigger(AddonMessagesAddContactUserHandler.UPDATED_EVENT, {userId: user.id}); this.eventsProvider.trigger(AddonMessagesAddContactUserHandler.UPDATED_EVENT, {userId: user.id});
this.checkButton(user.id).finally(() => { this.checkButton(user.id).finally(() => {
@ -132,7 +132,7 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
if (isContact) { if (isContact) {
this.updateButton({ this.updateButton({
title: 'addon.messages.removecontact', title: 'addon.messages.removecontact',
class: 'mma-messages-removecontact-handler', class: 'addon-messages-removecontact-handler',
icon: 'remove', icon: 'remove',
hidden: false, hidden: false,
spinner: false spinner: false
@ -140,7 +140,7 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
} else { } else {
this.updateButton({ this.updateButton({
title: 'addon.messages.addcontact', title: 'addon.messages.addcontact',
class: 'mma-messages-addcontact-handler', class: 'addon-messages-addcontact-handler',
icon: 'add', icon: 'add',
hidden: false, hidden: false,
spinner: false spinner: false

View File

@ -33,8 +33,8 @@ export class AddonMessagesBlockContactUserHandler implements CoreUserProfileHand
*/ */
static UPDATED_EVENT = 'AddonMessagesBlockContactUserHandler_updated_event'; static UPDATED_EVENT = 'AddonMessagesBlockContactUserHandler_updated_event';
name = 'mmaMessages:addContact'; name = 'AddonMessages:blockContact';
priority = 800; priority = 600;
type = CoreUserDelegate.TYPE_ACTION; type = CoreUserDelegate.TYPE_ACTION;
protected disabled = false; protected disabled = false;
@ -52,9 +52,9 @@ export class AddonMessagesBlockContactUserHandler implements CoreUserProfileHand
/** /**
* Check if handler is enabled. * Check if handler is enabled.
* *
* @return {Promise<any>} Promise resolved with true if enabled, rejected or resolved with false otherwise. * @return {Promise<boolean>} Promise resolved with true if enabled, rejected or resolved with false otherwise.
*/ */
isEnabled(): Promise<any> { isEnabled(): Promise<boolean> {
return this.messagesProvider.isPluginEnabled(); return this.messagesProvider.isPluginEnabled();
} }
@ -104,12 +104,12 @@ export class AddonMessagesBlockContactUserHandler implements CoreUserProfileHand
return this.domUtils.showConfirm(template, title, title).then(() => { return this.domUtils.showConfirm(template, title, title).then(() => {
return this.messagesProvider.blockContact(user.id); return this.messagesProvider.blockContact(user.id);
}).catch(() => { }, () => {
// Ignore on cancel. // Ignore on cancel.
}); });
} }
}).catch((error) => { }).catch((error) => {
this.domUtils.showErrorModal(error); this.domUtils.showErrorModalDefault(error, 'core.error', true);
}).finally(() => { }).finally(() => {
this.eventsProvider.trigger(AddonMessagesBlockContactUserHandler.UPDATED_EVENT, {userId: user.id}); this.eventsProvider.trigger(AddonMessagesBlockContactUserHandler.UPDATED_EVENT, {userId: user.id});
this.checkButton(user.id).finally(() => { this.checkButton(user.id).finally(() => {
@ -133,7 +133,7 @@ export class AddonMessagesBlockContactUserHandler implements CoreUserProfileHand
if (isBlocked) { if (isBlocked) {
this.updateButton({ this.updateButton({
title: 'addon.messages.unblockcontact', title: 'addon.messages.unblockcontact',
class: 'mma-messages-unblockcontact-handler', class: 'addon-messages-unblockcontact-handler',
icon: 'checkmark-circle', icon: 'checkmark-circle',
hidden: false, hidden: false,
spinner: false spinner: false
@ -141,7 +141,7 @@ export class AddonMessagesBlockContactUserHandler implements CoreUserProfileHand
} else { } else {
this.updateButton({ this.updateButton({
title: 'addon.messages.blockcontact', title: 'addon.messages.blockcontact',
class: 'mma-messages-blockcontact-handler', class: 'addon-messages-blockcontact-handler',
icon: 'close-circle', icon: 'close-circle',
hidden: false, hidden: false,
spinner: false spinner: false

View File

@ -23,7 +23,7 @@ import { AddonMessagesProvider } from './messages';
*/ */
@Injectable() @Injectable()
export class AddonMessagesSendMessageUserHandler implements CoreUserProfileHandler { export class AddonMessagesSendMessageUserHandler implements CoreUserProfileHandler {
name = 'mmaMessages:sendMessage'; name = 'AddonMessages:sendMessage';
priority = 1000; priority = 1000;
type = CoreUserDelegate.TYPE_COMMUNICATION; type = CoreUserDelegate.TYPE_COMMUNICATION;

View File

@ -14,20 +14,22 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { Subject } from 'rxjs';
/** /**
* Service to handle push notifications clicks. * Service to handle push notifications actions to perform when clicked and received.
*/ */
@Injectable() @Injectable()
export class AddonPushNotificationsDelegate { export class AddonPushNotificationsDelegate {
protected logger; protected logger;
protected clickHandlers: { [s: string]: Function } = {}; protected observables: { [s: string]: Subject<any> } = {};
protected receiveHandlers: { [s: string]: Function } = {};
protected counterHandlers: { [s: string]: string } = {}; protected counterHandlers: { [s: string]: string } = {};
constructor(loggerProvider: CoreLoggerProvider) { constructor(loggerProvider: CoreLoggerProvider) {
this.logger = loggerProvider.getInstance('AddonPushNotificationsDelegate'); this.logger = loggerProvider.getInstance('AddonPushNotificationsDelegate');
this.observables['click'] = new Subject<any>();
this.observables['receive'] = new Subject<any>();
} }
/** /**
@ -36,15 +38,7 @@ export class AddonPushNotificationsDelegate {
* @param {any} notification Notification clicked. * @param {any} notification Notification clicked.
*/ */
clicked(notification: any): void { clicked(notification: any): void {
for (const name in this.clickHandlers) { this.observables['click'].next(notification);
const callback = this.clickHandlers[name];
if (typeof callback == 'function') {
const treated = callback(notification);
if (treated) {
return; // Stop execution when notification is treated.
}
}
}
} }
/** /**
@ -54,60 +48,42 @@ export class AddonPushNotificationsDelegate {
* @param {any} notification Notification received. * @param {any} notification Notification received.
*/ */
received(notification: any): void { received(notification: any): void {
for (const name in this.receiveHandlers) { this.observables['receive'].next(notification);
const callback = this.receiveHandlers[name]; }
if (typeof callback == 'function') {
callback(notification); /**
} * Register a push notifications observable for click and receive notification event.
* When a notification is clicked or received, the observable will receive a notification to treat.
* let observer = pushNotificationsDelegate.on('click').subscribe((notification) => {
* ...
* observer.unsuscribe();
*
* @param {string} eventName Only click and receive are permitted.
* @return {Subject<any>} Observer to subscribe.
*/
on(eventName: string): Subject<any> {
if (typeof this.observables[eventName] == 'undefined') {
const eventNames = Object.keys(this.observables).join(', ');
this.logger.warn(`'${eventName}' event name is not allowed. Use one of the following: '${eventNames}'.`);
return new Subject<any>();
} }
}
/** return this.observables[eventName];
* Register a push notifications handler for CLICKS.
* When a notification is clicked, the handler will receive a notification to treat.
*
* @param {string} name Handler's name.
* @param {Function} callback The callback function. Will get as parameter the clicked notification.
* @description
* The handler should return true if the notification is the one expected, false otherwise.
* @see {@link AddonPushNotificationsDelegate#clicked}
*/
registerHandler(name: string, callback: Function): void {
this.logger.debug(`Registered handler '${name}' as CLICK push notification handler.`);
this.clickHandlers[name] = callback;
}
/**
* Register a push notifications handler for RECEIVE notifications in foreground (cannot tell when it's received in background).
* When a notification is received, the handler will receive a notification to treat.
*
* @param {string} name Handler's name.
* @param {Function} callback The callback function. Will get as parameter the clicked notification.
* @see {@link AddonPushNotificationsDelegate#received}
*/
registerReceiveHandler(name: string, callback: Function): void {
this.logger.debug(`Registered handler '${name}' as RECEIVE push notification handler.`);
this.receiveHandlers[name] = callback;
}
/**
* Unregister a push notifications handler for RECEIVE notifications.
*
* @param {string} name Handler's name.
*/
unregisterReceiveHandler(name: string): void {
this.logger.debug(`Unregister handler '${name}' from RECEIVE push notification handlers.`);
delete this.receiveHandlers[name];
} }
/** /**
* Register a push notifications handler for update badge counter. * Register a push notifications handler for update badge counter.
* *
* @param {string} name Handler's name. * @param {string} name Handler's name.
*/ */
registerCounterHandler(name: string): void { registerCounterHandler(name: string): void {
this.logger.debug(`Registered handler '${name}' as badge counter handler.`); if (typeof this.counterHandlers[name] == 'undefined') {
this.counterHandlers[name] = name; this.logger.debug(`Registered handler '${name}' as badge counter handler.`);
this.counterHandlers[name] = name;
} else {
this.logger.log(`Handler '${name}' as badge counter handler already registered.`);
}
} }
/** /**

View File

@ -36,17 +36,17 @@ export class AddonPushNotificationsProvider {
protected logger; protected logger;
protected pushID: string; protected pushID: string;
protected appDB: any; protected appDB: any;
static COMPONENT = 'mmaPushNotifications'; static COMPONENT = 'AddonPushNotificationsProvider';
// Variables for database. // Variables for database.
protected BADGE_TABLE = 'mma_pushnotifications_badge'; protected BADGE_TABLE = 'addon_pushnotifications_badge';
protected tablesSchema = [ protected tablesSchema = [
{ {
name: this.BADGE_TABLE, name: this.BADGE_TABLE,
columns: [ columns: [
{ {
name: 'siteid', name: 'siteid',
type: 'INTEGER' type: 'TEXT'
}, },
{ {
name: 'addon', name: 'addon',
@ -84,25 +84,26 @@ export class AddonPushNotificationsProvider {
} }
/** /**
* Returns options for push notifications based on * Returns options for push notifications based on device.
* @return {Promise<PushOptions>} [description] *
* @return {Promise<PushOptions>} Promise with the push options resolved when done.
*/ */
protected getOptions(): Promise<PushOptions> { protected getOptions(): Promise<PushOptions> {
// @todo: CoreSettingsProvider.NOTIFICATION_SOUND // @todo: CoreSettingsProvider.NOTIFICATION_SOUND
return this.configProvider.get('CoreSettingsProvider.NOTIFICATION_SOUND', true).then((soundEnabled) => { return this.configProvider.get('CoreSettingsProvider.NOTIFICATION_SOUND', true).then((soundEnabled) => {
return { return {
android: { android: {
senderID: CoreConfigConstants.gcmpn, senderID: CoreConfigConstants.gcmpn,
sound: !!soundEnabled sound: !!soundEnabled
}, },
ios: { ios: {
alert: 'true', alert: 'true',
badge: true, badge: true,
sound: !!soundEnabled sound: !!soundEnabled
}, },
windows: { windows: {
sound: !!soundEnabled sound: !!soundEnabled
} }
}; };
}); });
} }
@ -259,9 +260,9 @@ export class AddonPushNotificationsProvider {
return Promise.all(promises).then((counters) => { return Promise.all(promises).then((counters) => {
const total = counters.reduce((previous, counter) => { const total = counters.reduce((previous, counter) => {
// The app badge counter does not support strings, so parse to int before. // The app badge counter does not support strings, so parse to int before.
return previous + parseInt(counter, 10); return previous + parseInt(counter, 10);
}, 0); }, 0);
// Set the app badge. // Set the app badge.
return this.badge.set(total).then(() => { return this.badge.set(total).then(() => {

View File

@ -44,7 +44,7 @@ export class CoreSyncBaseProvider {
protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise<any> } } = {}; protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise<any> } } = {};
constructor(component: string, protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, constructor(component: string, protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
protected appProvider: CoreAppProvider) { protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider) {
this.logger = this.loggerProvider.getInstance(component); this.logger = this.loggerProvider.getInstance(component);
this.component = component; this.component = component;
} }
@ -52,12 +52,12 @@ export class CoreSyncBaseProvider {
/** /**
* Add an ongoing sync to the syncPromises list. On finish the promise will be removed. * Add an ongoing sync to the syncPromises list. On finish the promise will be removed.
* *
* @param {number} id Unique sync identifier per component. * @param {string | number} id Unique sync identifier per component.
* @param {Promise<any>} promise The promise of the sync to add. * @param {Promise<any>} promise The promise of the sync to add.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} The sync promise. * @return {Promise<any>} The sync promise.
*/ */
addOngoingSync(id: number, promise: Promise<any>, siteId?: string): Promise<any> { addOngoingSync(id: string | number, promise: Promise<any>, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
const uniqueId = this.getUniqueSyncId(id); const uniqueId = this.getUniqueSyncId(id);
@ -76,11 +76,11 @@ export class CoreSyncBaseProvider {
/** /**
* If there's an ongoing sync for a certain identifier return it. * If there's an ongoing sync for a certain identifier return it.
* *
* @param {number} id Unique sync identifier per component. * @param {string | number} id Unique sync identifier per component.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise of the current sync or undefined if there isn't any. * @return {Promise<any>} Promise of the current sync or undefined if there isn't any.
*/ */
getOngoingSync(id: number, siteId?: string): Promise<any> { getOngoingSync(id: string | number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (this.isSyncing(id, siteId)) { if (this.isSyncing(id, siteId)) {
@ -94,59 +94,55 @@ export class CoreSyncBaseProvider {
/** /**
* Get the synchronization time. Returns 0 if no time stored. * Get the synchronization time. Returns 0 if no time stored.
* *
* @param {number} id Unique sync identifier per component. * @param {string | number} id Unique sync identifier per component.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<number>} Promise resolved with the time. * @return {Promise<number>} Promise resolved with the time.
*/ */
getSyncTime(id: number, siteId?: string): Promise<number> { getSyncTime(id: string | number, siteId?: string): Promise<number> {
return this.sitesProvider.getSiteDb(siteId).then((db) => { return this.syncProvider.getSyncRecord(this.component, id, siteId).then((entry) => {
return db.getRecord(CoreSyncProvider.SYNC_TABLE, { component: this.component, id: id }).then((entry) => { return entry.time;
return entry.time; }).catch(() => {
}).catch(() => { return 0;
return 0;
});
}); });
} }
/** /**
* Get the synchronization warnings of an instance. * Get the synchronization warnings of an instance.
* *
* @param {number} id Unique sync identifier per component. * @param {string | number} id Unique sync identifier per component.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<string[]>} Promise resolved with the warnings. * @return {Promise<string[]>} Promise resolved with the warnings.
*/ */
getSyncWarnings(id: number, siteId?: string): Promise<string[]> { getSyncWarnings(id: string | number, siteId?: string): Promise<string[]> {
return this.sitesProvider.getSiteDb(siteId).then((db) => { return this.syncProvider.getSyncRecord(this.component, id, siteId).then((entry) => {
return db.getRecord(CoreSyncProvider.SYNC_TABLE, { component: this.component, id: id }).then((entry) => { try {
try { return JSON.parse(entry.warnings);
return JSON.parse(entry.warnings); } catch (ex) {
} catch (ex) {
return [];
}
}).catch(() => {
return []; return [];
}); }
}).catch(() => {
return [];
}); });
} }
/** /**
* Create a unique identifier from component and id. * Create a unique identifier from component and id.
* *
* @param {number} id Unique sync identifier per component. * @param {string | number} id Unique sync identifier per component.
* @return {string} Unique identifier from component and id. * @return {string} Unique identifier from component and id.
*/ */
protected getUniqueSyncId(id: number): string { protected getUniqueSyncId(id: string | number): string {
return this.component + '#' + id; return this.component + '#' + id;
} }
/** /**
* Check if a there's an ongoing syncronization for the given id. * Check if a there's an ongoing syncronization for the given id.
* *
* @param {number} id Unique sync identifier per component. * @param {string | number} id Unique sync identifier per component.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {boolean} Whether it's synchronizing. * @return {boolean} Whether it's synchronizing.
*/ */
isSyncing(id: number, siteId?: string): boolean { isSyncing(id: string | number, siteId?: string): boolean {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
const uniqueId = this.getUniqueSyncId(id); const uniqueId = this.getUniqueSyncId(id);
@ -157,11 +153,11 @@ export class CoreSyncBaseProvider {
/** /**
* Check if a sync is needed: if a certain time has passed since the last time. * Check if a sync is needed: if a certain time has passed since the last time.
* *
* @param {number} id Unique sync identifier per component. * @param {string} id Unique sync identifier per component.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: whether sync is needed. * @return {Promise<boolean>} Promise resolved with boolean: whether sync is needed.
*/ */
isSyncNeeded(id: number, siteId?: string): Promise<boolean> { isSyncNeeded(id: string, siteId?: string): Promise<boolean> {
return this.getSyncTime(id, siteId).then((time) => { return this.getSyncTime(id, siteId).then((time) => {
return Date.now() - this.syncInterval >= time; return Date.now() - this.syncInterval >= time;
}); });
@ -170,59 +166,47 @@ export class CoreSyncBaseProvider {
/** /**
* Set the synchronization time. * Set the synchronization time.
* *
* @param {number} id Unique sync identifier per component. * @param {string | number} id Unique sync identifier per component.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [time] Time to set. If not defined, current time. * @param {number} [time] Time to set. If not defined, current time.
* @return {Promise<any>} Promise resolved when the time is set. * @return {Promise<any>} Promise resolved when the time is set.
*/ */
setSyncTime(id: number, siteId?: string, time?: number): Promise<any> { setSyncTime(id: string | number, siteId?: string, time?: number): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => { time = typeof time != 'undefined' ? time : Date.now();
time = typeof time != 'undefined' ? time : Date.now();
return db.insertOrUpdateRecord(CoreSyncProvider.SYNC_TABLE, { time: time }, { component: this.component, id: id }); return this.syncProvider.insertOrUpdateSyncRecord(this.component, id, { time: time }, siteId);
});
} }
/** /**
* Set the synchronization warnings. * Set the synchronization warnings.
* *
* @param {number} id Unique sync identifier per component. * @param {string} id Unique sync identifier per component.
* @param {string[]} warnings Warnings to set. * @param {string[]} warnings Warnings to set.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
setSyncWarnings(id: number, warnings: string[], siteId?: string): Promise<any> { setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => { const warningsText = JSON.stringify(warnings || []);
warnings = warnings || [];
return db.insertOrUpdateRecord(CoreSyncProvider.SYNC_TABLE, { warnings: JSON.stringify(warnings) }, return this.syncProvider.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId);
{ component: this.component, id: id });
});
} }
/** /**
* Execute a sync function on selected sites. * Execute a sync function on selected sites.
* *
* @param {string} syncFunctionLog Log message to explain the sync function purpose. * @param {string} syncFunctionLog Log message to explain the sync function purpose.
* @param {string} syncFunction Sync function to execute. * @param {Function} syncFunction Sync function to execute.
* @param {any} [params] Object that defines the params that admit the funcion. * @param {any[]} [params] Array that defines the params that admit the funcion.
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites. * @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
* @return {Promise<any>} Resolved with siteIds selected. Rejected if offline. * @return {Promise<any>} Resolved with siteIds selected. Rejected if offline.
*/ */
syncOnSites(syncFunctionLog: string, syncFunction: string, params?: any, siteId?: string): Promise<any> { syncOnSites(syncFunctionLog: string, syncFunction: Function, params?: any[], siteId?: string): Promise<any> {
if (!this.appProvider.isOnline()) { if (!this.appProvider.isOnline()) {
this.logger.debug(`Cannot sync '${syncFunctionLog}' because device is offline.`); this.logger.debug(`Cannot sync '${syncFunctionLog}' because device is offline.`);
return Promise.reject(null); return Promise.reject(null);
} }
if (!this[syncFunction]) {
this.logger.debug(`Cannot sync '${syncFunctionLog}' function '${syncFunction}' does not exist.`);
return Promise.reject(null);
}
params = params || {};
let promise; let promise;
if (!siteId) { if (!siteId) {
// No site ID defined, sync all sites. // No site ID defined, sync all sites.
@ -233,12 +217,13 @@ export class CoreSyncBaseProvider {
promise = Promise.resolve([siteId]); promise = Promise.resolve([siteId]);
} }
params = params || [];
return promise.then((siteIds) => { return promise.then((siteIds) => {
const sitePromises = []; const sitePromises = [];
siteIds.forEach((siteId) => { siteIds.forEach((siteId) => {
params['siteId'] = siteId;
// Execute function for every site selected. // Execute function for every site selected.
sitePromises.push(this[syncFunction].apply(this, params)); sitePromises.push(syncFunction.apply(syncFunction, [siteId].concat(params)));
}); });
return Promise.all(sitePromises); return Promise.all(sitePromises);
@ -249,11 +234,11 @@ export class CoreSyncBaseProvider {
* If there's an ongoing sync for a certain identifier, wait for it to end. * If there's an ongoing sync for a certain identifier, wait for it to end.
* If there's no sync ongoing the promise will be resolved right away. * If there's no sync ongoing the promise will be resolved right away.
* *
* @param {number} id Unique sync identifier per component. * @param {string | number} id Unique sync identifier per component.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when there's no sync going on for the identifier. * @return {Promise<any>} Promise resolved when there's no sync going on for the identifier.
*/ */
waitForSync(id: number, siteId?: string): Promise<any> { waitForSync(id: string | number, siteId?: string): Promise<any> {
const promise = this.getOngoingSync(id, siteId); const promise = this.getOngoingSync(id, siteId);
if (promise) { if (promise) {
return promise.catch(() => { return promise.catch(() => {
@ -262,5 +247,5 @@ export class CoreSyncBaseProvider {
} }
return Promise.resolve(); return Promise.resolve();
} }
} }

View File

@ -531,7 +531,7 @@ export class CoreSite {
} else { } else {
this.logger.error(`WS function '${method}' is not available, even in compatibility mode.`); this.logger.error(`WS function '${method}' is not available, even in compatibility mode.`);
return Promise.reject(this.wsProvider.createFakeWSError('core.wsfunctionnotavailable', true)); return Promise.reject(this.utils.createFakeWSError('core.wsfunctionnotavailable', true));
} }
} }
@ -560,7 +560,7 @@ export class CoreSite {
data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode); data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode);
} catch (e) { } catch (e) {
// Empty cleaned text found. // Empty cleaned text found.
return Promise.reject(this.wsProvider.createFakeWSError('core.unicodenotsupportedcleanerror', true)); return Promise.reject(this.utils.createFakeWSError('core.unicodenotsupportedcleanerror', true));
} }
return this.getFromCache(method, data, preSets).catch(() => { return this.getFromCache(method, data, preSets).catch(() => {

View File

@ -1,11 +1,11 @@
<ion-card> <ion-card>
<form #f="ngForm" (ngSubmit)="submitForm()"> <form #f="ngForm" (ngSubmit)="submitForm()">
<ion-item> <ion-item>
<ion-input type="text" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus"></ion-input> <ion-input type="text" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled"></ion-input>
<button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="!searchText || (searchText.length < lengthCheck)"> <button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="!searchText || (searchText.length < lengthCheck)" [disabled]="disabled">
<ion-icon name="search"></ion-icon> <ion-icon name="search"></ion-icon>
</button> </button>
<button *ngIf="showClear" item-end ion-button clear icon-only class="button-small" [attr.aria-label]="'core.clearsearch' | translate" [disabled]="!searched" (click)="clearForm()"> <button *ngIf="showClear" item-end ion-button clear icon-only class="button-small" [attr.aria-label]="'core.clearsearch' | translate" [disabled]="!searched" (click)="clearForm()" [disabled]="disabled">
<ion-icon name="close"></ion-icon> <ion-icon name="close"></ion-icon>
</button> </button>
</ion-item> </ion-item>

View File

@ -38,6 +38,7 @@ export class CoreSearchBoxComponent implements OnInit {
@Input() autoFocus?: string | boolean; // Enables/disable Autofocus when entering view. @Input() autoFocus?: string | boolean; // Enables/disable Autofocus when entering view.
@Input() lengthCheck? = 3; // Check value length before submit. If 0, any string will be submitted. @Input() lengthCheck? = 3; // Check value length before submit. If 0, any string will be submitted.
@Input() showClear? = true; // Show/hide clear button. @Input() showClear? = true; // Show/hide clear button.
@Input() disabled? = false; // Disables the input text.
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form. @Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form.
@Output() onClear?: EventEmitter<void>; // Send event when clearing the search form. @Output() onClear?: EventEmitter<void>; // Send event when clearing the search form.

View File

@ -83,4 +83,8 @@ core-tabs {
top: 0; top: 0;
height: 100%; height: 100%;
} }
ion-content core-tabs core-tab .core-avoid-header ion-content {
top: 0;
height: 100%;
}
} }

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
/* tslint:disable:no-console */
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
/** /**

View File

@ -91,7 +91,6 @@ export class CoreMainMenuPage implements OnDestroy {
}); });
if (tab) { if (tab) {
tab.badge = data.badge; tab.badge = data.badge;
tab.loading = false;
} }
}, site.getId()); }, site.getId());

View File

@ -68,7 +68,6 @@ export class CoreMainMenuMorePage implements OnDestroy {
}); });
if (handler) { if (handler) {
handler.badge = data.badge; handler.badge = data.badge;
handler.loading = false;
} }
}, this.sitesProvider.getCurrentSiteId()); }, this.sitesProvider.getCurrentSiteId());
} }

View File

@ -6,32 +6,32 @@
<core-split-view> <core-split-view>
<ion-content> <ion-content>
<ion-list> <ion-list>
<a ion-item (click)="openHandler('CoreSettingsGeneralPage')" [title]="'core.settings.general' | translate" [class.core-split-item-selected]="'CoreSettingsGeneralPage' == selectedPage"> <ion-item (click)="openHandler('CoreSettingsGeneralPage')" [title]="'core.settings.general' | translate" [class.core-split-item-selected]="'CoreSettingsGeneralPage' == selectedPage" detail-push>
<ion-icon name="construct" item-start></ion-icon> <ion-icon name="construct" item-start></ion-icon>
<p>{{ 'core.settings.general' | translate }}</p> <p>{{ 'core.settings.general' | translate }}</p>
</a> </ion-item>
<a ion-item (click)="openHandler('CoreSettingsSpaceUsagePage')" [title]="'core.settings.spaceusage' | translate" [class.core-split-item-selected]="'CoreSettingsSpaceUsagePage' == selectedPage"> <ion-item (click)="openHandler('CoreSettingsSpaceUsagePage')" [title]="'core.settings.spaceusage' | translate" [class.core-split-item-selected]="'CoreSettingsSpaceUsagePage' == selectedPage" detail-push>
<ion-icon name="stats" item-start></ion-icon> <ion-icon name="stats" item-start></ion-icon>
<p>{{ 'core.settings.spaceusage' | translate }}</p> <p>{{ 'core.settings.spaceusage' | translate }}</p>
</a> </ion-item>
<a ion-item (click)="openHandler('CoreSettingSynchronizationPage')" [title]="'core.settings.synchronization' | translate" [class.core-split-item-selected]="'CoreSettingSynchronizationPage' == selectedPage"> <ion-item (click)="openHandler('CoreSettingSynchronizationPage')" [title]="'core.settings.synchronization' | translate" [class.core-split-item-selected]="'CoreSettingSynchronizationPage' == selectedPage" detail-push>
<ion-icon name="sync" item-start></ion-icon> <ion-icon name="sync" item-start></ion-icon>
<p>{{ 'core.settings.synchronization' | translate }}</p> <p>{{ 'core.settings.synchronization' | translate }}</p>
</a> </ion-item>
<a ion-item *ngIf="isIOS" (click)="openHandler('CoreSharedFilesListPage', {manage: true})" [title]="'core.sharedfiles.sharedfiles' | translate" [class.core-split-item-selected]="'CoreSharedFilesListPage' == selectedPage"> <ion-item *ngIf="isIOS" (click)="openHandler('CoreSharedFilesListPage', {manage: true})" [title]="'core.sharedfiles.sharedfiles' | translate" [class.core-split-item-selected]="'CoreSharedFilesListPage' == selectedPage" detail-push>
<ion-icon name="folder" item-start></ion-icon> <ion-icon name="folder" item-start></ion-icon>
<p>{{ 'core.sharedfiles.sharedfiles' | translate }}</p> <p>{{ 'core.sharedfiles.sharedfiles' | translate }}</p>
</a> </ion-item>
<ion-item *ngFor="let handler of handlers" [ngClass]="['core-settings-handler', handler.class]" (click)="openHandler(handler.page, handler.params)" [title]="handler.title | translate" detail-push [class.core-split-item-selected]="handler.page == selectedPage"> <ion-item *ngFor="let handler of handlers" [ngClass]="['core-settings-handler', handler.class]" (click)="openHandler(handler.page, handler.params)" [title]="handler.title | translate" detail-push [class.core-split-item-selected]="handler.page == selectedPage">
<ion-icon [name]="handler.icon" item-start *ngIf="handler.icon"></ion-icon> <ion-icon [name]="handler.icon" item-start *ngIf="handler.icon"></ion-icon>
<p>{{ handler.title | translate}}</p> <p>{{ handler.title | translate}}</p>
</ion-item> </ion-item>
<a ion-item (click)="openHandler('CoreSettingsAboutPage')" [title]="'core.settings.about' | translate" [class.core-split-item-selected]="'CoreSettingsAboutPage' == selectedPage"> <ion-item (click)="openHandler('CoreSettingsAboutPage')" [title]="'core.settings.about' | translate" [class.core-split-item-selected]="'CoreSettingsAboutPage' == selectedPage" detail-push>
<ion-icon name="contacts" item-start></ion-icon> <ion-icon name="contacts" item-start></ion-icon>
<p>{{ 'core.settings.about' | translate }}</p> <p>{{ 'core.settings.about' | translate }}</p>
</a> </ion-item>
</ion-list> </ion-list>
</ion-content> </ion-content>
</core-split-view> </core-split-view>

View File

@ -29,7 +29,6 @@ export class CoreSettingsListPage {
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
handlers: CoreSettingsHandlerData[]; handlers: CoreSettingsHandlerData[];
areHandlersLoaded: Function;
isIOS: boolean; isIOS: boolean;
selectedPage: string; selectedPage: string;
@ -61,7 +60,7 @@ export class CoreSettingsListPage {
*/ */
openHandler(page: string, params?: any): void { openHandler(page: string, params?: any): void {
this.selectedPage = page; this.selectedPage = page;
this.navCtrl.push(page, params); this.splitviewCtrl.push(page, params);
} }
} }

View File

@ -52,9 +52,9 @@
</a> </a>
<ion-item *ngIf="actionHandlers && actionHandlers.length"> <ion-item *ngIf="actionHandlers && actionHandlers.length">
<button *ngFor="let actHandler of actionHandlers" ion-button block outline [ngClass]="['core-user-profile-handler', actHandler.class]" (click)="handlerClicked($event, actHandler)" [hidden]="actHandler.hidden" title="{{ actHandler.title | translate }}" icon-start> <button *ngFor="let actHandler of actionHandlers" ion-button block outline [ngClass]="['core-user-profile-handler', actHandler.class]" (click)="handlerClicked($event, actHandler)" [hidden]="actHandler.hidden" title="{{ actHandler.title | translate }}" icon-start [disabled]="actHandler.spinner">
<ion-icon *ngIf="!actHandler.spinner && actHandler.icon" [name]="actHandler.icon" start></ion-icon> <ion-icon *ngIf="actHandler.icon" [name]="actHandler.icon" start></ion-icon>
<span *ngIf="!actHandler.spinner">{{ actHandler.title | translate }}</span> <span>{{ actHandler.title | translate }}</span>
<ion-spinner *ngIf="actHandler.spinner"></ion-spinner> <ion-spinner *ngIf="actHandler.spinner"></ion-spinner>
</button> </button>
</ion-item> </ion-item>

View File

@ -31,4 +31,10 @@ page-core-user-profile {
} }
} }
} }
.core-user-profile-handler {
ion-spinner {
margin-left: 0.3em;
}
}
} }

View File

@ -18,11 +18,11 @@ import { CoreUserProvider } from '../../providers/user';
import { CoreUserHelperProvider } from '../../providers/helper'; import { CoreUserHelperProvider } from '../../providers/helper';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreCoursesProvider } from '../../../courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreFileUploaderHelperProvider } from '../../../fileuploader/providers/helper'; import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper';
import { CoreUserDelegate, CoreUserProfileHandlerData } from '../../providers/user-delegate'; import { CoreUserDelegate, CoreUserProfileHandlerData } from '../../providers/user-delegate';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';

View File

@ -20,11 +20,11 @@ import { CoreUserHelperProvider } from './providers/helper';
import { CoreUserProfileMailHandler } from './providers/user-handler'; import { CoreUserProfileMailHandler } from './providers/user-handler';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreUserProfileLinkHandler } from './providers/user-link-handler'; import { CoreUserProfileLinkHandler } from './providers/user-link-handler';
import { CoreUserParticipantsCourseOptionHandler } from './providers/course-option-handler'; import { CoreUserParticipantsCourseOptionHandler } from './providers/course-option-handler';
import { CoreUserParticipantsLinkHandler } from './providers/participants-link-handler'; import { CoreUserParticipantsLinkHandler } from './providers/participants-link-handler';
import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { CoreUserComponentsModule } from './components/components.module'; import { CoreUserComponentsModule } from './components/components.module';
@NgModule({ @NgModule({

View File

@ -59,7 +59,7 @@ export class CoreDateDayOrTimePipe implements PipeTransform {
} }
return moment(timestamp * 1000).calendar(null, { return moment(timestamp * 1000).calendar(null, {
sameDay: 'LT', //this.translate.instant('core.dftimedate'), sameDay: 'LT',
lastDay: this.translate.instant('core.dflastweekdate'), lastDay: this.translate.instant('core.dflastweekdate'),
lastWeek: this.translate.instant('core.dflastweekdate'), lastWeek: this.translate.instant('core.dflastweekdate'),
sameElse: 'L' sameElse: 'L'

View File

@ -42,10 +42,13 @@ export interface CoreILocalNotification extends ILocalNotification {
} }
/* /*
Generated class for the LocalNotificationsProvider provider. * Generated class for the LocalNotificationsProvider provider.
*
See https://angular.io/guide/dependency-injection for more info on providers * See https://angular.io/guide/dependency-injection for more info on providers
and Angular DI. * and Angular DI.
*
* @todo We might have to translate the old component name to the new one.
* Otherwise the unique ID of local notifications could change.
*/ */
@Injectable() @Injectable()
export class CoreLocalNotificationsProvider { export class CoreLocalNotificationsProvider {

View File

@ -23,9 +23,9 @@ import { CoreSitesProvider } from './sites';
export class CoreSyncProvider { export class CoreSyncProvider {
// Variables for the database. // Variables for the database.
static SYNC_TABLE = 'sync'; protected SYNC_TABLE = 'sync';
protected tableSchema = { protected tableSchema = {
name: CoreSyncProvider.SYNC_TABLE, name: this.SYNC_TABLE,
columns: [ columns: [
{ {
name: 'component', name: 'component',
@ -34,7 +34,7 @@ export class CoreSyncProvider {
}, },
{ {
name: 'id', name: 'id',
type: 'INTEGER', type: 'TEXT',
notNull: true notNull: true
}, },
{ {
@ -116,6 +116,36 @@ export class CoreSyncProvider {
} }
} }
/**
* Returns a sync record.
* @param {string} component Component name.
* @param {string | number} id Unique ID per component.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Record if found or reject.
*/
getSyncRecord(component: string, id: string | number, siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.getRecord(this.SYNC_TABLE, { component: component, id: id });
});
}
/**
* Inserts or Updates info of a sync record.
* @param {string} component Component name.
* @param {string | number} id Unique ID per component.
* @param {any} data Data that updates the record.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with done.
*/
insertOrUpdateSyncRecord(component: string, id: string | number, data: any, siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
data.component = component;
data.id = id;
return db.insertOrUpdateRecord(this.SYNC_TABLE, data, { component: component, id: id });
});
}
/** /**
* Convenience function to create unique identifiers for a component and id. * Convenience function to create unique identifiers for a component and id.
* *

View File

@ -23,6 +23,7 @@ import { CoreEventsProvider } from '../events';
import { CoreLoggerProvider } from '../logger'; import { CoreLoggerProvider } from '../logger';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreLangProvider } from '../lang'; import { CoreLangProvider } from '../lang';
import { CoreWSError } from '../ws';
/** /**
* Deferred promise. It's similar to the result of $q.defer() in AngularJS. * Deferred promise. It's similar to the result of $q.defer() in AngularJS.
@ -601,28 +602,31 @@ export class CoreUtilsProvider {
return typeof value != 'undefined' && (value === true || value === 'true' || parseInt(value, 10) === 1); return typeof value != 'undefined' && (value === true || value === 'true' || parseInt(value, 10) === 1);
} }
/**
* Create a "fake" WS error for local errors.
*
* @param {string} message The message to include in the error.
* @param {boolean} [needsTranslate] If the message needs to be translated.
* @return {CoreWSError} Fake WS error.
*/
createFakeWSError(message: string, needsTranslate?: boolean): CoreWSError {
if (needsTranslate) {
message = this.translate.instant(message);
}
return {
message: message
};
}
/** /**
* Given an error returned by a WS call, check if the error is generated by the app or it has been returned by the WebSwervice. * Given an error returned by a WS call, check if the error is generated by the app or it has been returned by the WebSwervice.
* *
* @param {string} error Error to check. * @param {any} error Error to check.
* @return {boolean} Whether the error was returned by the WebService. * @return {boolean} Whether the error was returned by the WebService.
*/ */
isWebServiceError(error: string): boolean { isWebServiceError(error: any): boolean {
const localErrors = [ return typeof error.errorcode == 'undefined';
this.translate.instant('core.wsfunctionnotavailable'),
this.translate.instant('core.lostconnection'),
this.translate.instant('core.userdeleted'),
this.translate.instant('core.unexpectederror'),
this.translate.instant('core.networkerrormsg'),
this.translate.instant('core.serverconnection'),
this.translate.instant('core.errorinvalidresponse'),
this.translate.instant('core.sitemaintenance'),
this.translate.instant('core.upgraderunning'),
this.translate.instant('core.nopasswordchangeforced'),
this.translate.instant('core.unicodenotsupported')
];
return error && localErrors.indexOf(error) == -1;
} }
/** /**

View File

@ -174,9 +174,9 @@ export class CoreWSProvider {
let siteUrl; let siteUrl;
if (!preSets) { if (!preSets) {
return Promise.reject(this.createFakeWSError('core.unexpectederror', true)); return Promise.reject(this.utils.createFakeWSError('core.unexpectederror', true));
} else if (!this.appProvider.isOnline()) { } else if (!this.appProvider.isOnline()) {
return Promise.reject(this.createFakeWSError('core.networkerrormsg', true)); return Promise.reject(this.utils.createFakeWSError('core.networkerrormsg', true));
} }
preSets.typeExpected = preSets.typeExpected || 'object'; preSets.typeExpected = preSets.typeExpected || 'object';
@ -320,23 +320,6 @@ export class CoreWSProvider {
return result; return result;
} }
/**
* Create a "fake" WS error for local errors.
*
* @param {string} message The message to include in the error.
* @param {boolean} [needsTranslate] If the message needs to be translated.
* @return {CoreWSError} Fake WS error.
*/
createFakeWSError(message: string, needsTranslate?: boolean): CoreWSError {
if (needsTranslate) {
message = this.translate.instant(message);
}
return {
message: message
};
}
/** /**
* Downloads a file from Moodle using Cordova File API. * Downloads a file from Moodle using Cordova File API.
* *
@ -532,11 +515,11 @@ export class CoreWSProvider {
} }
if (!data) { if (!data) {
return Promise.reject(this.createFakeWSError('core.serverconnection', true)); return Promise.reject(this.utils.createFakeWSError('core.serverconnection', true));
} else if (typeof data != preSets.typeExpected) { } else if (typeof data != preSets.typeExpected) {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(this.utils.createFakeWSError('core.errorinvalidresponse', true));
} }
if (typeof data.exception !== 'undefined') { if (typeof data.exception !== 'undefined') {
@ -544,7 +527,7 @@ export class CoreWSProvider {
} }
if (typeof data.debuginfo != 'undefined') { if (typeof data.debuginfo != 'undefined') {
return Promise.reject(this.createFakeWSError('Error. ' + data.message)); return Promise.reject(this.utils.createFakeWSError('Error. ' + data.message));
} }
return data; return data;
@ -572,7 +555,7 @@ export class CoreWSProvider {
return retryPromise; return retryPromise;
} }
return Promise.reject(this.createFakeWSError('core.serverconnection', true)); return Promise.reject(this.utils.createFakeWSError('core.serverconnection', true));
}); });
promise = this.setPromiseHttp(promise, 'post', preSets.siteUrl, ajaxData); promise = this.setPromiseHttp(promise, 'post', preSets.siteUrl, ajaxData);