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"
},
"config": {
"ionic_webpack": "./webpack.config.js"
"ionic_webpack": "./config/webpack.config.js"
},
"repository": {
"type": "git",

View File

@ -3,7 +3,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</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-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();
}).catch((error) => {
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);
}).catch((error) => {
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>
<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">
<!-- 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="(!search.results || search.results.length <= 0) && search.showResults" icon="search" [message]="'core.noresults' | translate"></core-empty-box>
@ -33,7 +33,7 @@
<h2>
<core-format-text [text]="discussion.fullname"></core-format-text>
<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>
</ion-note>
</h2>

View File

@ -42,7 +42,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
loadingMessage: string;
discussions: any;
discussionUserId: number;
messageId: number;
pushObserver: any;
search = {
enabled: false,
showResults: false,
@ -54,7 +54,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService,
private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider,
private pushNotificationsDelegate: AddonPushNotificationsDelegate) {
pushNotificationsDelegate: AddonPushNotificationsDelegate) {
this.search.loading = translate.instant('core.searching');
this.loadingMessages = translate.instant('core.loading');
@ -114,7 +114,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
this.discussionUserId = navParams.get('discussionUserId') || false;
// 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.
if (utils.isFalseOrZero(notification.notif) && notification.site == this.siteId) {
this.refreshData();
@ -211,7 +211,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
this.search.showResults = true;
this.search.results = searchResults;
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'mma.messages.errorwhileretrievingmessages', true);
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
}).finally(() => {
this.loaded = true;
});
@ -233,7 +233,6 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
};
if (messageId) {
params['message'] = messageId;
this.messageId = messageId;
}
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.cronObserver && this.cronObserver.off();
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.
pushNotificationsDelegate.registerHandler('mmaMessages', (notification) => {
pushNotificationsDelegate.on('click').subscribe((notification) => {
if (utils.isFalseOrZero(notification.notif)) {
notificationClicked(notification);

View File

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

View File

@ -48,6 +48,7 @@ export class AddonMessagesSettingsPage implements OnDestroy {
/**
* Fetches preference data.
*
* @return {Promise<any>} Resolved when done.
*/
protected fetchPreferences(): Promise<any> {
@ -85,6 +86,7 @@ export class AddonMessagesSettingsPage implements OnDestroy {
/**
* Block non contacts.
*
* @param {boolean} block If it should be blocked or not.
*/
blockNonContacts(block: boolean): void {
@ -103,6 +105,7 @@ export class AddonMessagesSettingsPage implements OnDestroy {
/**
* Change the value of a certain preference.
*
* @param {any} notification Notification object.
* @param {string} state State name, ['loggedin', 'loggedoff'].
* @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.
pushNotificationsDelegate.registerReceiveHandler('AddonMessagesMainMenuHandler', (notification) => {
pushNotificationsDelegate.on('receive').subscribe((notification) => {
// New message received. If it's from current site, refresh the data.
if (utils.isFalseOrZero(notification.notif) && this.sitesProvider.isCurrentSite(notification.site)) {
this.updateBadge(notification.site);
@ -64,7 +64,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
});
// Register Badge counter.
pushNotificationsDelegate.registerCounterHandler('mmaMessages');
pushNotificationsDelegate.registerCounterHandler('AddonMessages');
}
/**
@ -91,7 +91,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
title: 'addon.messages.messages',
page: 'AddonMessagesIndexPage',
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,
loading: this.loading
};
@ -112,7 +112,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr
// Leave badge enter if there is a 0+ or a 0.
this.badge = parseInt(unread, 10) > 0 ? unread : '';
// Update badge.
this.pushNotificationsProvider.updateAddonCounter('mmaMessages', unread, siteId);
this.pushNotificationsProvider.updateAddonCounter('AddonMessages', unread, siteId);
}).catch(() => {
this.badge = '';
}).finally(() => {

View File

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

View File

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

View File

@ -21,7 +21,9 @@ import { AddonMessagesOfflineProvider } from './messages-offline';
import { AddonMessagesProvider } from './messages';
import { CoreUserProvider } from '@core/user/providers/user';
import { CoreEventsProvider } from '@providers/events';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { TranslateService } from '@ngx-translate/core';
import { CoreSyncProvider } from '@providers/sync';
/**
* Service to sync messages.
@ -34,8 +36,9 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
protected appProvider: CoreAppProvider, private messagesOffline: AddonMessagesOfflineProvider,
private eventsProvider: CoreEventsProvider, private messagesProvider: AddonMessagesProvider,
private userProvider: CoreUserProvider, private translate: TranslateService) {
super('AddonMessagesSync', sitesProvider, loggerProvider, appProvider);
private userProvider: CoreUserProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
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> {
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);
}
let syncPromise;
const warnings = [];
this.logger.debug(`Try to sync discussion with user '${userId}'`);
// 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) {
// Nothing to sync.
return [];
@ -123,33 +125,35 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
return Promise.reject(null);
}
let promise: Promise<any>;
promise = Promise.resolve();
let promise: Promise<any> = Promise.resolve();
const errors = [];
// Order message by timecreated.
messages = this.messagesProvider.sortMessages(messages);
// Send the messages. We don't use $mmaMessages#sendMessagesOnline because there's a problem with display order.
// @todo Use $mmaMessages#sendMessagesOnline once the display order is fixed.
// Send the messages.
// 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) => {
// Chain message sending. If 1 message fails to be sent we'll stop sending.
promise = promise.then(() => {
return this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId).catch((data) => {
if (data.wserror) {
return this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// Error returned by WS. Store the error to show a warning but keep sending messages.
if (errors.indexOf(data.error) == -1) {
errors.push(data.error);
}
} else {
// Error sending, stop execution.
if (this.appProvider.isOnline()) {
// App is online, unmark deviceoffline if marked.
this.messagesOffline.setMessagesDeviceOffline(messages, false);
if (errors.indexOf(error) == -1) {
errors.push(error);
}
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(() => {
// Message was sent, delete it from local DB.
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';
name = 'mmaMessages:blockContact';
priority = 600;
name = 'AddonMessages:addContact';
priority = 800;
type = CoreUserDelegate.TYPE_ACTION;
protected disabled = false;
@ -52,9 +52,9 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
/**
* 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();
}
@ -101,14 +101,14 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
return this.domUtils.showConfirm(template, title, title).then(() => {
return this.messagesProvider.removeContact(user.id);
}).catch(() => {
}, () => {
// Ignore on cancel.
});
} else {
return this.messagesProvider.addContact(user.id);
}
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.domUtils.showErrorModalDefault(error, 'core.error', true);
}).finally(() => {
this.eventsProvider.trigger(AddonMessagesAddContactUserHandler.UPDATED_EVENT, {userId: user.id});
this.checkButton(user.id).finally(() => {
@ -132,7 +132,7 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
if (isContact) {
this.updateButton({
title: 'addon.messages.removecontact',
class: 'mma-messages-removecontact-handler',
class: 'addon-messages-removecontact-handler',
icon: 'remove',
hidden: false,
spinner: false
@ -140,7 +140,7 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
} else {
this.updateButton({
title: 'addon.messages.addcontact',
class: 'mma-messages-addcontact-handler',
class: 'addon-messages-addcontact-handler',
icon: 'add',
hidden: false,
spinner: false

View File

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

View File

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

View File

@ -14,20 +14,22 @@
import { Injectable } from '@angular/core';
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()
export class AddonPushNotificationsDelegate {
protected logger;
protected clickHandlers: { [s: string]: Function } = {};
protected receiveHandlers: { [s: string]: Function } = {};
protected observables: { [s: string]: Subject<any> } = {};
protected counterHandlers: { [s: string]: string } = {};
constructor(loggerProvider: CoreLoggerProvider) {
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.
*/
clicked(notification: any): void {
for (const name in this.clickHandlers) {
const callback = this.clickHandlers[name];
if (typeof callback == 'function') {
const treated = callback(notification);
if (treated) {
return; // Stop execution when notification is treated.
}
}
}
this.observables['click'].next(notification);
}
/**
@ -54,60 +48,42 @@ export class AddonPushNotificationsDelegate {
* @param {any} notification Notification received.
*/
received(notification: any): void {
for (const name in this.receiveHandlers) {
const callback = this.receiveHandlers[name];
if (typeof callback == 'function') {
callback(notification);
}
this.observables['receive'].next(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>();
}
}
/**
* 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];
return this.observables[eventName];
}
/**
* 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 {
this.logger.debug(`Registered handler '${name}' as badge counter handler.`);
this.counterHandlers[name] = name;
if (typeof this.counterHandlers[name] == 'undefined') {
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 pushID: string;
protected appDB: any;
static COMPONENT = 'mmaPushNotifications';
static COMPONENT = 'AddonPushNotificationsProvider';
// Variables for database.
protected BADGE_TABLE = 'mma_pushnotifications_badge';
protected BADGE_TABLE = 'addon_pushnotifications_badge';
protected tablesSchema = [
{
name: this.BADGE_TABLE,
columns: [
{
name: 'siteid',
type: 'INTEGER'
type: 'TEXT'
},
{
name: 'addon',
@ -84,25 +84,26 @@ export class AddonPushNotificationsProvider {
}
/**
* Returns options for push notifications based on
* @return {Promise<PushOptions>} [description]
* Returns options for push notifications based on device.
*
* @return {Promise<PushOptions>} Promise with the push options resolved when done.
*/
protected getOptions(): Promise<PushOptions> {
// @todo: CoreSettingsProvider.NOTIFICATION_SOUND
return this.configProvider.get('CoreSettingsProvider.NOTIFICATION_SOUND', true).then((soundEnabled) => {
return {
android: {
senderID: CoreConfigConstants.gcmpn,
sound: !!soundEnabled
},
ios: {
alert: 'true',
badge: true,
sound: !!soundEnabled
},
windows: {
sound: !!soundEnabled
}
android: {
senderID: CoreConfigConstants.gcmpn,
sound: !!soundEnabled
},
ios: {
alert: 'true',
badge: true,
sound: !!soundEnabled
},
windows: {
sound: !!soundEnabled
}
};
});
}
@ -259,9 +260,9 @@ export class AddonPushNotificationsProvider {
return Promise.all(promises).then((counters) => {
const total = counters.reduce((previous, counter) => {
// The app badge counter does not support strings, so parse to int before.
return previous + parseInt(counter, 10);
}, 0);
// The app badge counter does not support strings, so parse to int before.
return previous + parseInt(counter, 10);
}, 0);
// Set the app badge.
return this.badge.set(total).then(() => {

View File

@ -44,7 +44,7 @@ export class CoreSyncBaseProvider {
protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise<any> } } = {};
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.component = component;
}
@ -52,12 +52,12 @@ export class CoreSyncBaseProvider {
/**
* 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 {string} [siteId] Site ID. If not defined, current site.
* @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();
const uniqueId = this.getUniqueSyncId(id);
@ -76,11 +76,11 @@ export class CoreSyncBaseProvider {
/**
* 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.
* @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();
if (this.isSyncing(id, siteId)) {
@ -94,59 +94,55 @@ export class CoreSyncBaseProvider {
/**
* 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.
* @return {Promise<number>} Promise resolved with the time.
*/
getSyncTime(id: number, siteId?: string): Promise<number> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.getRecord(CoreSyncProvider.SYNC_TABLE, { component: this.component, id: id }).then((entry) => {
return entry.time;
}).catch(() => {
return 0;
});
getSyncTime(id: string | number, siteId?: string): Promise<number> {
return this.syncProvider.getSyncRecord(this.component, id, siteId).then((entry) => {
return entry.time;
}).catch(() => {
return 0;
});
}
/**
* 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.
* @return {Promise<string[]>} Promise resolved with the warnings.
*/
getSyncWarnings(id: number, siteId?: string): Promise<string[]> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.getRecord(CoreSyncProvider.SYNC_TABLE, { component: this.component, id: id }).then((entry) => {
try {
return JSON.parse(entry.warnings);
} catch (ex) {
return [];
}
}).catch(() => {
getSyncWarnings(id: string | number, siteId?: string): Promise<string[]> {
return this.syncProvider.getSyncRecord(this.component, id, siteId).then((entry) => {
try {
return JSON.parse(entry.warnings);
} catch (ex) {
return [];
});
}
}).catch(() => {
return [];
});
}
/**
* 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.
*/
protected getUniqueSyncId(id: number): string {
protected getUniqueSyncId(id: string | number): string {
return this.component + '#' + 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.
* @return {boolean} Whether it's synchronizing.
*/
isSyncing(id: number, siteId?: string): boolean {
isSyncing(id: string | number, siteId?: string): boolean {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
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.
*
* @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.
* @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 Date.now() - this.syncInterval >= time;
});
@ -170,59 +166,47 @@ export class CoreSyncBaseProvider {
/**
* 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 {number} [time] Time to set. If not defined, current time.
* @return {Promise<any>} Promise resolved when the time is set.
*/
setSyncTime(id: number, siteId?: string, time?: number): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
time = typeof time != 'undefined' ? time : Date.now();
setSyncTime(id: string | number, siteId?: string, time?: number): Promise<any> {
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.
*
* @param {number} id Unique sync identifier per component.
* @param {string} id Unique sync identifier per component.
* @param {string[]} warnings Warnings to set.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
setSyncWarnings(id: number, warnings: string[], siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
warnings = warnings || [];
setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise<any> {
const warningsText = JSON.stringify(warnings || []);
return db.insertOrUpdateRecord(CoreSyncProvider.SYNC_TABLE, { warnings: JSON.stringify(warnings) },
{ component: this.component, id: id });
});
return this.syncProvider.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId);
}
/**
* Execute a sync function on selected sites.
*
* @param {string} syncFunctionLog Log message to explain the sync function purpose.
* @param {string} syncFunction Sync function to execute.
* @param {any} [params] Object that defines the params that admit the funcion.
* @param {Function} syncFunction Sync function to execute.
* @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.
* @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()) {
this.logger.debug(`Cannot sync '${syncFunctionLog}' because device is offline.`);
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;
if (!siteId) {
// No site ID defined, sync all sites.
@ -233,12 +217,13 @@ export class CoreSyncBaseProvider {
promise = Promise.resolve([siteId]);
}
params = params || [];
return promise.then((siteIds) => {
const sitePromises = [];
siteIds.forEach((siteId) => {
params['siteId'] = siteId;
// 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);
@ -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 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.
* @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);
if (promise) {
return promise.catch(() => {
@ -262,5 +247,5 @@ export class CoreSyncBaseProvider {
}
return Promise.resolve();
}
}
}

View File

@ -531,7 +531,7 @@ export class CoreSite {
} else {
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);
} catch (e) {
// 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(() => {

View File

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

View File

@ -38,6 +38,7 @@ export class CoreSearchBoxComponent implements OnInit {
@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() 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() onClear?: EventEmitter<void>; // Send event when clearing the search form.

View File

@ -83,4 +83,8 @@ core-tabs {
top: 0;
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
// limitations under the License.
/* tslint:disable:no-console */
import { SQLiteDB } from '@classes/sqlitedb';
/**

View File

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

View File

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

View File

@ -6,32 +6,32 @@
<core-split-view>
<ion-content>
<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>
<p>{{ 'core.settings.general' | translate }}</p>
</a>
<a ion-item (click)="openHandler('CoreSettingsSpaceUsagePage')" [title]="'core.settings.spaceusage' | translate" [class.core-split-item-selected]="'CoreSettingsSpaceUsagePage' == selectedPage">
</ion-item>
<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>
<p>{{ 'core.settings.spaceusage' | translate }}</p>
</a>
<a ion-item (click)="openHandler('CoreSettingSynchronizationPage')" [title]="'core.settings.synchronization' | translate" [class.core-split-item-selected]="'CoreSettingSynchronizationPage' == selectedPage">
</ion-item>
<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>
<p>{{ 'core.settings.synchronization' | translate }}</p>
</a>
<a ion-item *ngIf="isIOS" (click)="openHandler('CoreSharedFilesListPage', {manage: true})" [title]="'core.sharedfiles.sharedfiles' | translate" [class.core-split-item-selected]="'CoreSharedFilesListPage' == selectedPage">
</ion-item>
<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>
<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-icon [name]="handler.icon" item-start *ngIf="handler.icon"></ion-icon>
<p>{{ handler.title | translate}}</p>
</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>
<p>{{ 'core.settings.about' | translate }}</p>
</a>
</ion-item>
</ion-list>
</ion-content>
</core-split-view>

View File

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

View File

@ -52,9 +52,9 @@
</a>
<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>
<ion-icon *ngIf="!actHandler.spinner && actHandler.icon" [name]="actHandler.icon" start></ion-icon>
<span *ngIf="!actHandler.spinner">{{ actHandler.title | translate }}</span>
<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.icon" [name]="actHandler.icon" start></ion-icon>
<span>{{ actHandler.title | translate }}</span>
<ion-spinner *ngIf="actHandler.spinner"></ion-spinner>
</button>
</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 { CoreDomUtilsProvider } from '@providers/utils/dom';
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 { CoreSitesProvider } from '@providers/sites';
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 { 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 { CoreEventsProvider } from '@providers/events';
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 { CoreUserParticipantsCourseOptionHandler } from './providers/course-option-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';
@NgModule({

View File

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

View File

@ -42,10 +42,13 @@ export interface CoreILocalNotification extends ILocalNotification {
}
/*
Generated class for the LocalNotificationsProvider provider.
See https://angular.io/guide/dependency-injection for more info on providers
and Angular DI.
* Generated class for the LocalNotificationsProvider provider.
*
* See https://angular.io/guide/dependency-injection for more info on providers
* 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()
export class CoreLocalNotificationsProvider {

View File

@ -23,9 +23,9 @@ import { CoreSitesProvider } from './sites';
export class CoreSyncProvider {
// Variables for the database.
static SYNC_TABLE = 'sync';
protected SYNC_TABLE = 'sync';
protected tableSchema = {
name: CoreSyncProvider.SYNC_TABLE,
name: this.SYNC_TABLE,
columns: [
{
name: 'component',
@ -34,7 +34,7 @@ export class CoreSyncProvider {
},
{
name: 'id',
type: 'INTEGER',
type: 'TEXT',
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.
*

View File

@ -23,6 +23,7 @@ import { CoreEventsProvider } from '../events';
import { CoreLoggerProvider } from '../logger';
import { TranslateService } from '@ngx-translate/core';
import { CoreLangProvider } from '../lang';
import { CoreWSError } from '../ws';
/**
* 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);
}
/**
* 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.
*
* @param {string} error Error to check.
* @param {any} error Error to check.
* @return {boolean} Whether the error was returned by the WebService.
*/
isWebServiceError(error: string): boolean {
const localErrors = [
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;
isWebServiceError(error: any): boolean {
return typeof error.errorcode == 'undefined';
}
/**

View File

@ -174,9 +174,9 @@ export class CoreWSProvider {
let siteUrl;
if (!preSets) {
return Promise.reject(this.createFakeWSError('core.unexpectederror', true));
return Promise.reject(this.utils.createFakeWSError('core.unexpectederror', true));
} 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';
@ -320,23 +320,6 @@ export class CoreWSProvider {
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.
*
@ -532,11 +515,11 @@ export class CoreWSProvider {
}
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) {
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') {
@ -544,7 +527,7 @@ export class CoreWSProvider {
}
if (typeof data.debuginfo != 'undefined') {
return Promise.reject(this.createFakeWSError('Error. ' + data.message));
return Promise.reject(this.utils.createFakeWSError('Error. ' + data.message));
}
return data;
@ -572,7 +555,7 @@ export class CoreWSProvider {
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);