Merge pull request #1704 from dpalou/MOBILE-2795

Mobile 2795
main
Juan Leyva 2019-01-04 16:21:02 +01:00 committed by GitHub
commit 49d0b3a121
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 259 additions and 67 deletions

View File

@ -107,14 +107,17 @@ export class AddonMessagesModule {
}
messagesProvider.invalidateDiscussionsCache(notification.site).finally(() => {
// Check if group messaging is enabled, to determine which page should be loaded.
messagesProvider.isGroupMessagingEnabledInSite(notification.site).then((enabled) => {
let pageName = 'AddonMessagesIndexPage';
if (messagesProvider.isGroupMessagingEnabled()) {
if (enabled) {
pageName = 'AddonMessagesGroupConversationsPage';
}
linkHelper.goInSite(undefined, pageName, undefined, notification.site);
});
});
});
});
};
if (appProvider.isDesktop()) {
@ -125,7 +128,10 @@ export class AddonMessagesModule {
// Register push notification clicks.
pushNotificationsDelegate.on('click').subscribe((notification) => {
if (utils.isFalseOrZero(notification.notif)) {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
notificationClicked(notification);
});
return true;
}

View File

@ -1770,17 +1770,32 @@ export class AddonMessagesProvider {
/**
* Returns whether or not group messaging is supported.
*
* @return {boolean} If related WS is avalaible on current site.
* @return {boolean} If related WS is available on current site.
* @since 3.6
*/
isGroupMessagingEnabled(): boolean {
return this.sitesProvider.wsAvailableInCurrentSite('core_message_get_conversations');
}
/**
* Returns whether or not group messaging is supported in a certain site.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: whether related WS is available on a certain site.
* @since 3.6
*/
isGroupMessagingEnabledInSite(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.wsAvailable('core_message_get_conversations');
}).catch(() => {
return false;
});
}
/**
* Returns whether or not we can mark all messages as read.
*
* @return {boolean} If related WS is avalaible on current site.
* @return {boolean} If related WS is available on current site.
* @since 3.2
*/
isMarkAllMessagesReadEnabled(): boolean {

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
@ -27,7 +28,7 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo
format: string;
constructor(protected fb: FormBuilder, protected timeUtils: CoreTimeUtilsProvider) {
constructor(protected fb: FormBuilder, protected timeUtils: CoreTimeUtilsProvider, protected translate: TranslateService) {
super(fb);
}
@ -40,7 +41,10 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo
}
let val;
this.format = this.timeUtils.getLocalizedDateFormat('LL');
// Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
this.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatefullshort'))
.replace(/[\[\]]/g, '');
if (this.mode == 'search') {
this.addControl('f_' + this.field.id + '_z');

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { NgModule, NgZone } from '@angular/core';
import { AddonNotificationsProvider } from './providers/notifications';
import { AddonNotificationsMainMenuHandler } from './providers/mainmenu-handler';
import { AddonNotificationsSettingsHandler } from './providers/settings-handler';
@ -47,7 +47,7 @@ export const ADDON_NOTIFICATIONS_PROVIDERS: any[] = [
export class AddonNotificationsModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: AddonNotificationsMainMenuHandler,
settingsDelegate: CoreSettingsDelegate, settingsHandler: AddonNotificationsSettingsHandler,
cronDelegate: CoreCronDelegate, cronHandler: AddonNotificationsCronHandler,
cronDelegate: CoreCronDelegate, cronHandler: AddonNotificationsCronHandler, zone: NgZone,
appProvider: CoreAppProvider, utils: CoreUtilsProvider, sitesProvider: CoreSitesProvider,
notificationsProvider: AddonNotificationsProvider, localNotifications: CoreLocalNotificationsProvider,
linkHelper: CoreContentLinksHelperProvider, pushNotificationsDelegate: AddonPushNotificationsDelegate) {
@ -76,7 +76,10 @@ export class AddonNotificationsModule {
// Register push notification clicks.
pushNotificationsDelegate.on('click').subscribe((notification) => {
if (utils.isTrueOrOne(notification.notif)) {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
notificationClicked(notification);
});
return true;
}

View File

@ -139,13 +139,15 @@ export class AddonNotificationsListPage {
this.loadingMarkAllNotificationsAsRead = true;
this.notificationsProvider.markAllNotificationsAsRead().catch(() => {
// Omit failure.
}).finally(() => {
}).then(() => {
const siteId = this.sitesProvider.getCurrentSiteId();
this.eventsProvider.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, null, siteId);
this.notificationsProvider.getUnreadNotificationsCount().then((unread) => {
this.canMarkAllNotificationsAsRead = unread > 0;
this.loadingMarkAllNotificationsAsRead = false;
// All marked as read, refresh the list.
this.notificationsLoaded = false;
return this.refreshNotifications().finally(() => {
this.notificationsLoaded = true;
});
});
}
@ -182,6 +184,7 @@ export class AddonNotificationsListPage {
return this.notificationsProvider.getUnreadNotificationsCount().then((unread) => {
this.canMarkAllNotificationsAsRead = unread > 0;
}).finally(() => {
this.loadingMarkAllNotificationsAsRead = false;
});
}
@ -193,9 +196,10 @@ export class AddonNotificationsListPage {
* Refresh notifications.
*
* @param {any} [refresher] Refresher.
* @return Promise<any> Promise resolved when done.
*/
refreshNotifications(refresher?: any): void {
this.notificationsProvider.invalidateNotificationsList().finally(() => {
refreshNotifications(refresher?: any): Promise<any> {
return this.notificationsProvider.invalidateNotificationsList().finally(() => {
return this.fetchNotifications(true).finally(() => {
if (refresher) {
refresher.complete();

View File

@ -14,6 +14,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
@ -30,7 +31,8 @@ export class AddonUserProfileFieldDatetimeComponent implements OnInit {
@Input() disabled = false; // True if disabled. Defaults to false.
@Input() form?: FormGroup; // Form where to add the form control.
constructor(private fb: FormBuilder, private timeUtils: CoreTimeUtilsProvider, protected utils: CoreUtilsProvider) { }
constructor(private fb: FormBuilder, private timeUtils: CoreTimeUtilsProvider, protected utils: CoreUtilsProvider,
private translate: TranslateService) { }
/**
* Component being initialized.
@ -44,7 +46,10 @@ export class AddonUserProfileFieldDatetimeComponent implements OnInit {
// Check if it's only date or it has time too.
const hasTime = this.utils.isTrueOrOne(field.param3);
field.format = hasTime ? this.timeUtils.getLocalizedDateFormat('LLL') : this.timeUtils.getLocalizedDateFormat('LL');
// Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
field.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.' +
(hasTime ? 'strftimedatetimeshort' : 'strftimedatefullshort'))).replace(/[\[\]]/g, '');
// Check min value.
if (field.param1) {

View File

@ -152,6 +152,22 @@ export class CoreFileComponent implements OnInit, OnDestroy {
return;
}
if (!this.canDownload) {
// File cannot be downloaded, just open it.
if (this.file.toURL) {
// Local file.
this.utils.openFile(this.file.toURL());
} else if (this.fileUrl) {
if (this.fileUrl.indexOf('http') === 0) {
this.utils.openOnlineFile(this.fileUrl);
} else {
this.utils.openFile(this.fileUrl);
}
}
return;
}
if (!this.appProvider.isOnline() && (!openAfterDownload || (openAfterDownload && !this.isDownloaded))) {
this.domUtils.showErrorModal('core.networkerrormsg', true);

View File

@ -14,9 +14,11 @@
import { Component, Optional, ElementRef, Renderer, ViewEncapsulation, forwardRef, ViewChild, Input,
OnDestroy } from '@angular/core';
import { Tabs, NavController, ViewController, App, Config, Platform, DeepLinker, Keyboard, RootNode } from 'ionic-angular';
import {
Tabs, Tab, NavController, ViewController, App, Config, Platform, DeepLinker, Keyboard, RootNode, NavOptions
} from 'ionic-angular';
import { CoreIonTabComponent } from './ion-tab';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils';
import { CoreAppProvider } from '@providers/app';
/**
@ -66,6 +68,7 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy {
protected firstSelectedTab: string;
protected unregisterBackButtonAction: any;
protected selectTabPromiseDefer: PromiseDefer;
constructor(protected utils: CoreUtilsProvider, protected appProvider: CoreAppProvider, @Optional() parent: NavController,
@Optional() viewCtrl: ViewController, _app: App, config: Config, elementRef: ElementRef, _plt: Platform,
@ -148,20 +151,18 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy {
// Tabs initialized. Force select the tab if it's not enabled.
if (this.selectedDisabled && typeof this.selectedIndex != 'undefined') {
const tab = this.getByIndex(this.selectedIndex);
if (tab && (!tab.enabled)) {
this.select(tab);
}
} else {
// Select first tab on init.
const tab = this._tabs.find((tab) => {
return tab.enabled;
});
if (tab) {
if (tab && !tab.enabled) {
this.select(tab);
}
}
this.firstSelectedTab = this._selectHistory[0] || null;
}).finally(() => {
// If there was a select promise pending to be resolved, do it now.
if (this.selectTabPromiseDefer) {
this.selectTabPromiseDefer.resolve();
delete this.selectTabPromiseDefer;
}
});
} else {
// Tabs not loaded yet. Set the tab bar position so the tab bar is shown, it'll have a spinner.
@ -265,21 +266,63 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy {
}
}
/**
* Select a tab.
*
* @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select.
* @param {NavOptions} Nav options.
* @param {boolean} [fromUrl=true] Whether to load from a URL.
* @return {Promise<any>} Promise resolved when selected.
*/
select(tabOrIndex: number | Tab, opts: NavOptions = {}, fromUrl: boolean = false): Promise<any> {
if (this.initialized) {
// Tabs have been initialized, select the tab.
return super.select(tabOrIndex, opts, fromUrl);
} else {
// Tabs not initialized yet. Mark it as "selectedIndex" input so it's treated when the tabs are initialized.
if (typeof tabOrIndex == 'number') {
this.selectedIndex = tabOrIndex;
} else {
this.selectedIndex = this.getIndex(tabOrIndex);
}
// Don't resolve the Promise until the tab is really selected (tabs are initialized).
this.selectTabPromiseDefer = this.selectTabPromiseDefer || this.utils.promiseDefer();
return this.selectTabPromiseDefer.promise;
}
}
/**
* Select a tab by Index. First it will reset the status of the tab.
*
* @param {number} index Index of the tab.
* @return {Promise<any>} Promise resolved when selected.
*/
selectTabRootByIndex(index: number): void {
selectTabRootByIndex(index: number): Promise<any> {
if (this.initialized) {
const tab = this.getByIndex(index);
if (tab) {
tab.goToRoot({animate: false, updateUrl: true, isNavRoot: true}).then(() => {
return tab.goToRoot({animate: false, updateUrl: true, isNavRoot: true}).then(() => {
// Tab not previously selected. Select it after going to root.
if (!tab.isSelected) {
this.select(tab, {animate: false, updateUrl: true, isNavRoot: true});
return this.select(tab, {animate: false, updateUrl: true, isNavRoot: true});
}
});
}
// Not found.
return Promise.reject(null);
} else {
// Tabs not initialized yet. Mark it as "selectedIndex" input so it's treated when the tabs are initialized.
this.selectedIndex = index;
// Don't resolve the Promise until the tab is really selected (tabs are initialized).
this.selectTabPromiseDefer = this.selectTabPromiseDefer || this.utils.promiseDefer();
return this.selectTabPromiseDefer.promise;
}
}
/**

View File

@ -22,7 +22,7 @@
<div class="core-button-spinner" *ngIf="courseOptionMenuEnabled">
<!-- Download course spinner. -->
<ion-spinner *ngIf="prefetchCourseData.prefetchCourseIcon == 'spinner' || showSpinner"></ion-spinner>
<ion-spinner *ngIf="(downloadCourseEnabled && prefetchCourseData.prefetchCourseIcon == 'spinner') || showSpinner"></ion-spinner>
<!-- Options menu. -->
<button ion-button icon-only clear color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner">

View File

@ -7,7 +7,7 @@
<ion-icon name="search"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item *ngIf="downloadCourseEnabled" [priority]="1000" [content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000" [content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
</core-context-menu>
</ion-buttons>
</ion-navbar>

View File

@ -52,6 +52,7 @@ export class CoreCoursesDashboardPage implements OnDestroy {
downloadEnabled: boolean;
downloadEnabledIcon = 'square-outline'; // Disabled by default.
downloadCourseEnabled: boolean;
downloadCoursesEnabled: boolean;
protected isDestroyed;
protected updateSiteObserver;
@ -69,11 +70,13 @@ export class CoreCoursesDashboardPage implements OnDestroy {
ionViewDidLoad(): void {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
this.downloadCourseEnabled = !this.coursesProvider.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite();
// Refresh the enabled flags if site is updated.
this.updateSiteObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
this.downloadCourseEnabled = !this.coursesProvider.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite();
this.switchDownload(this.downloadEnabled);
@ -196,7 +199,7 @@ export class CoreCoursesDashboardPage implements OnDestroy {
* @param {boolean} enable If enable or disable.
*/
protected switchDownload(enable: boolean): void {
this.downloadEnabled = this.downloadCourseEnabled && enable;
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && enable;
this.downloadEnabledIcon = this.downloadEnabled ? 'checkbox-outline' : 'square-outline';
this.eventsProvider.trigger(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, {enabled: this.downloadEnabled});
}

View File

@ -203,7 +203,8 @@ export class CoreLoginHelperProvider {
return this.requestPasswordReset(siteUrl).then(() => {
return true;
}).catch((error) => {
return error.available == 1 || (error.errorcode != 'invalidrecord' && error.errorcode != '');
return error.available == 1 || (typeof error.errorcode != 'undefined' && error.errorcode != 'invalidrecord' &&
error.errorcode != '');
});
}
@ -586,18 +587,24 @@ export class CoreLoginHelperProvider {
* @param {string} siteId Site to load.
*/
protected loadSiteAndPage(page: string, params: any, siteId: string): void {
const navCtrl = this.appProvider.getRootNavController();
if (siteId == CoreConstants.NO_SITE_ID) {
// Page doesn't belong to a site, just load the page.
this.appProvider.getRootNavController().setRoot(page, params);
navCtrl.setRoot(page, params);
} else {
const modal = this.domUtils.showModalLoading();
this.sitesProvider.loadSite(siteId, page, params).then((loggedIn) => {
if (loggedIn) {
this.loadPageInMainMenu(page, params);
// Due to DeepLinker, we need to remove the path from the URL before going to main menu.
// IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
this.location.replaceState('');
navCtrl.setRoot('CoreMainMenuPage', { redirectPage: page, redirectParams: params });
}
}).catch(() => {
}).catch((error) => {
// Site doesn't exist.
this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage');
navCtrl.setRoot('CoreLoginSitesPage');
}).finally(() => {
modal.dismiss();
});

View File

@ -43,6 +43,15 @@ export class CoreMainMenuPage implements OnDestroy {
constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider, navParams: NavParams,
private navCtrl: NavController, private eventsProvider: CoreEventsProvider) {
// Check if the menu was loaded with a redirect.
const redirectPage = navParams.get('redirectPage');
if (redirectPage) {
this.pendingRedirect = {
redirectPage: redirectPage,
redirectParams: navParams.get('redirectParams')
};
}
}
/**

View File

@ -240,6 +240,43 @@ export class CoreLangProvider {
});
}
/**
* Get the default language.
*
* @return {string} Default language.
*/
getDefaultLanguage(): string {
return this.defaultLanguage;
}
/**
* Get the fallback language.
*
* @return {string} Fallback language.
*/
getFallbackLanguage(): string {
return this.fallbackLanguage;
}
/**
* Get the full list of translations for a certain language.
*
* @param {string} lang The language to check.
* @return {Promise<any>} Promise resolved when done.
*/
getTranslationTable(lang: string): Promise<any> {
// Create a promise to convert the observable into a promise.
return new Promise((resolve, reject): void => {
const observer = this.translate.getTranslation(lang).subscribe((table) => {
resolve(table);
observer.unsubscribe();
}, (err) => {
reject(err);
observer.unsubscribe();
});
});
}
/**
* Load certain custom strings.
*

View File

@ -278,6 +278,7 @@ export class CoreTimeUtilsProvider {
/**
* Return the localized ISO format (i.e DDMMYY) from the localized moment format. Useful for translations.
* DO NOT USE this function for ion-datetime format. Moment escapes characters with [], but ion-datetime doesn't support it.
*
* @param {any} localizedFormat Format to use.
* @return {string} Localized ISO format

View File

@ -561,29 +561,68 @@ export class CoreUtilsProvider {
* @return {Promise<any>} Promise resolved with the list of countries.
*/
getCountryList(): Promise<any> {
// Get the current language.
return this.langProvider.getCurrentLanguage().then((lang) => {
// Get the full list of translations. Create a promise to convert the observable into a promise.
return new Promise((resolve, reject): void => {
const observer = this.translate.getTranslation(lang).subscribe((table) => {
resolve(table);
observer.unsubscribe();
}, (err) => {
reject(err);
observer.unsubscribe();
});
});
}).then((table) => {
// Get the keys of the countries.
return this.getCountryKeysList().then((keys) => {
// Now get the code and the translated name.
const countries = {};
keys.forEach((key) => {
if (key.indexOf('assets.countries.') === 0) {
const code = key.replace('assets.countries.', '');
countries[code] = this.translate.instant(key);
}
});
return countries;
});
}
/**
* Get the list of language keys of the countries.
*
* @return {Promise<string[]>} Promise resolved with the countries list. Rejected if not translated.
*/
protected getCountryKeysList(): Promise<string[]> {
// It's possible that the current language isn't translated, so try with default language first.
const defaultLang = this.langProvider.getDefaultLanguage();
return this.getCountryKeysListForLanguage(defaultLang).catch(() => {
// Not translated, try to use the fallback language.
const fallbackLang = this.langProvider.getFallbackLanguage();
if (fallbackLang === defaultLang) {
// Same language, just reject.
return Promise.reject('Countries not found.');
}
return this.getCountryKeysListForLanguage(fallbackLang);
});
}
/**
* Get the list of language keys of the countries, based on the translation table for a certain language.
*
* @param {string} lang Language to check.
* @return {Promise<string[]>} Promise resolved with the countries list. Rejected if not translated.
*/
protected getCountryKeysListForLanguage(lang: string): Promise<string[]> {
// Get the translation table for the language.
return this.langProvider.getTranslationTable(lang).then((table): any => {
// Gather all the keys for countries,
const keys = [];
for (const name in table) {
if (name.indexOf('assets.countries.') === 0) {
const code = name.replace('assets.countries.', '');
countries[code] = table[name];
keys.push(name);
}
}
return countries;
if (keys.length === 0) {
// Not translated, reject.
return Promise.reject('Countries not found.');
}
return keys;
});
}

View File

@ -254,14 +254,14 @@ export class CoreWSProvider {
if (!data || typeof data != 'object') {
return rejectWithError(this.createFakeWSError('core.serverconnection', true));
} else if (data.error) {
return rejectWithError(data.error);
return rejectWithError(data);
}
// Get the first response since only one request was done.
data = data[0];
if (data.error) {
return rejectWithError(data.exception);
return rejectWithError(data);
}
return data.data;