Merge pull request #2812 from dpalou/MOBILE-3320

Mobile 3320
main
Pau Ferrer Ocaña 2021-06-04 17:06:04 +02:00 committed by GitHub
commit 75cedafa33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 88 additions and 36 deletions

View File

@ -237,6 +237,7 @@
"addon.messages.blocknoncontacts": "message", "addon.messages.blocknoncontacts": "message",
"addon.messages.blockuser": "message", "addon.messages.blockuser": "message",
"addon.messages.blockuserconfirm": "message", "addon.messages.blockuserconfirm": "message",
"addon.messages.cantblockuser": "message",
"addon.messages.contactableprivacy": "message", "addon.messages.contactableprivacy": "message",
"addon.messages.contactableprivacy_coursemember": "message", "addon.messages.contactableprivacy_coursemember": "message",
"addon.messages.contactableprivacy_onlycontacts": "message", "addon.messages.contactableprivacy_onlycontacts": "message",

View File

@ -7,6 +7,7 @@
"blocknoncontacts": "Prevent non-contacts from messaging me", "blocknoncontacts": "Prevent non-contacts from messaging me",
"blockuser": "Block user", "blockuser": "Block user",
"blockuserconfirm": "Are you sure you want to block {{$a}}?", "blockuserconfirm": "Are you sure you want to block {{$a}}?",
"cantblockuser": "You can't block {{$a}} because they have a role with permission to message all users.",
"contactableprivacy": "Accept messages from:", "contactableprivacy": "Accept messages from:",
"contactableprivacy_coursemember": "My contacts and anyone in my courses", "contactableprivacy_coursemember": "My contacts and anyone in my courses",
"contactableprivacy_onlycontacts": "My contacts only", "contactableprivacy_onlycontacts": "My contacts only",

View File

@ -1446,6 +1446,12 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
throw new CoreError('No member selected to be blocked.'); throw new CoreError('No member selected to be blocked.');
} }
if (this.otherMember.canmessageevenifblocked) {
CoreDomUtils.showErrorModal(Translate.instant('addon.messages.cantblockuser', { $a: this.otherMember.fullname }));
return;
}
const template = Translate.instant('addon.messages.blockuserconfirm', { $a: this.otherMember.fullname }); const template = Translate.instant('addon.messages.blockuserconfirm', { $a: this.otherMember.fullname });
const okText = Translate.instant('addon.messages.blockuser'); const okText = Translate.instant('addon.messages.blockuser');

View File

@ -407,8 +407,8 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async sync(): Promise<void> { protected sync(): Promise<AddonModAssignSyncResult> {
await AddonModAssignSync.syncAssign(this.assign!.id); return AddonModAssignSync.syncAssign(this.assign!.id);
} }
/** /**

View File

@ -235,12 +235,12 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
* @param inputData The input data. * @param inputData The input data.
* @return Promise resolved with the data to submit. * @return Promise resolved with the data to submit.
*/ */
protected prepareSubmissionData(inputData: CoreFormFields): Promise<AddonModAssignSavePluginData> { protected async prepareSubmissionData(inputData: CoreFormFields): Promise<AddonModAssignSavePluginData> {
// If there's offline data, always save it in offline. // If there's offline data, always save it in offline.
this.saveOffline = this.hasOffline; this.saveOffline = this.hasOffline;
try { try {
return AddonModAssignHelper.prepareSubmissionPluginData( return await AddonModAssignHelper.prepareSubmissionPluginData(
this.assign!, this.assign!,
this.userSubmission, this.userSubmission,
inputData, inputData,
@ -251,7 +251,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
// Cannot submit in online, prepare for offline usage. // Cannot submit in online, prepare for offline usage.
this.saveOffline = true; this.saveOffline = true;
return AddonModAssignHelper.prepareSubmissionPluginData( return await AddonModAssignHelper.prepareSubmissionPluginData(
this.assign!, this.assign!,
this.userSubmission, this.userSubmission,
inputData, inputData,

View File

@ -514,8 +514,8 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async sync(): Promise<void> { protected sync(): Promise<AddonModDataSyncResult> {
await AddonModDataPrefetchHandler.sync(this.module, this.courseId); return AddonModDataPrefetchHandler.sync(this.module, this.courseId);
} }
/** /**

View File

@ -233,8 +233,8 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async sync(): Promise<void> { protected sync(): Promise<AddonModSurveySyncResult> {
await AddonModSurveySync.syncSurvey(this.survey!.id, this.currentUserId); return AddonModSurveySync.syncSurvey(this.survey!.id, this.currentUserId);
} }
/** /**

View File

@ -224,11 +224,17 @@ export class AppComponent implements OnInit, AfterViewInit {
document.addEventListener('ionBackButton', (event: BackButtonEvent) => { document.addEventListener('ionBackButton', (event: BackButtonEvent) => {
// This callback should have the lowest priority in the app. // This callback should have the lowest priority in the app.
event.detail.register(-100, async () => { event.detail.register(-100, async () => {
const initialPath = CoreNavigator.getCurrentPath();
if (initialPath.startsWith('/main/')) {
// Main menu has its own callback to handle back. If this callback is called it means we should exit app.
CoreApp.closeApp();
return;
}
// This callback can be called at the same time as Ionic's back navigation callback. // This callback can be called at the same time as Ionic's back navigation callback.
// Check if the path changes due to the back navigation handler, to know if we're at root level. // Check if the path changes due to the back navigation handler, to know if we're at root level.
// Ionic doc recommends IonRouterOutlet.canGoBack, but there's no easy way to get the current outlet from here. // Ionic doc recommends IonRouterOutlet.canGoBack, but there's no easy way to get the current outlet from here.
const initialPath = CoreNavigator.getCurrentPath();
// The path seems to change immediately (0 ms timeout), but use 50ms just in case. // The path seems to change immediately (0 ms timeout), but use 50ms just in case.
await CoreUtils.wait(50); await CoreUtils.wait(50);
@ -238,8 +244,7 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
// Quit the app. // Quit the app.
const nav = <any> window.navigator; // eslint-disable-line @typescript-eslint/no-explicit-any CoreApp.closeApp();
nav.app?.exitApp();
}); });
}); });
} }

View File

@ -7,7 +7,7 @@
<ion-item *ngIf="model[modelValueName]"> <ion-item *ngIf="model[modelValueName]">
<ion-label color="success">{{ 'core.answered' | translate }}</ion-label> <ion-label color="success">{{ 'core.answered' | translate }}</ion-label>
</ion-item> </ion-item>
<ion-item *ngIf="expired"> <ion-item *ngIf="expired" class="ion-text-wrap">
<ion-label color="danger">{{ 'core.login.recaptchaexpired' | translate }}</ion-label> <ion-label color="danger">{{ 'core.login.recaptchaexpired' | translate }}</ion-label>
</ion-item> </ion-item>
</div> </div>

View File

@ -87,8 +87,7 @@ export class CoreRecaptchaComponent implements OnInit {
} }
if (event.data.action == 'expired') { if (event.data.action == 'expired') {
this.expired = true; this.expireRecaptchaAnswer();
this.model![this.modelValueName] = '';
} else if (event.data.action == 'callback') { } else if (event.data.action == 'callback') {
this.expired = false; this.expired = false;
this.model![this.modelValueName] = event.data.value; this.model![this.modelValueName] = event.data.value;
@ -101,4 +100,12 @@ export class CoreRecaptchaComponent implements OnInit {
}); });
} }
/**
* Expire the recaptcha answer.
*/
expireRecaptchaAnswer(): void {
this.expired = true;
this.model![this.modelValueName] = '';
}
} }

View File

@ -14,6 +14,8 @@
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
@ -36,7 +38,7 @@ import { CoreForms } from '@singletons/form';
}) })
export class CoreLoginCredentialsPage implements OnInit, OnDestroy { export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
@ViewChild('credentialsForm') formElement?: ElementRef; @ViewChild('credentialsForm') formElement?: ElementRef<HTMLFormElement>;
credForm!: FormGroup; credForm!: FormGroup;
siteUrl!: string; siteUrl!: string;
@ -57,6 +59,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
protected viewLeft = false; protected viewLeft = false;
protected siteId?: string; protected siteId?: string;
protected urlToOpen?: string; protected urlToOpen?: string;
protected valueChangeSubscription?: Subscription;
constructor( constructor(
protected fb: FormBuilder, protected fb: FormBuilder,
@ -96,6 +99,28 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
this.siteChecked = true; this.siteChecked = true;
this.pageLoaded = true; this.pageLoaded = true;
} }
if (CoreApp.isIOS()) {
// Make iOS auto-fill work. The field that isn't focused doesn't get updated, do it manually.
// Debounce it to prevent triggering this function too often when the user is typing.
this.valueChangeSubscription = this.credForm.valueChanges.pipe(debounceTime(1000)).subscribe((changes) => {
if (!this.formElement || !this.formElement.nativeElement) {
return;
}
const usernameInput = this.formElement.nativeElement.querySelector<HTMLInputElement>('input[name="username"]');
const passwordInput = this.formElement.nativeElement.querySelector<HTMLInputElement>('input[name="password"]');
const usernameValue = usernameInput?.value;
const passwordValue = passwordInput?.value;
if (usernameValue !== undefined && usernameValue !== changes.username) {
this.credForm.get('username')?.setValue(usernameValue);
}
if (passwordValue !== undefined && passwordValue !== changes.password) {
this.credForm.get('password')?.setValue(passwordValue);
}
});
}
} }
/** /**
@ -307,6 +332,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.viewLeft = true; this.viewLeft = true;
CoreEvents.trigger(CoreEvents.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId); CoreEvents.trigger(CoreEvents.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId);
this.valueChangeSubscription?.unsubscribe();
} }
} }

View File

@ -33,6 +33,7 @@ import {
} from '@features/login/services/login-helper'; } from '@features/login/services/login-helper';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { CoreRecaptchaComponent } from '@components/recaptcha/recaptcha';
/** /**
* Page to signup using email. * Page to signup using email.
@ -45,6 +46,7 @@ import { CoreForms } from '@singletons/form';
export class CoreLoginEmailSignupPage implements OnInit { export class CoreLoginEmailSignupPage implements OnInit {
@ViewChild(IonContent) content?: IonContent; @ViewChild(IonContent) content?: IonContent;
@ViewChild(CoreRecaptchaComponent) recaptchaComponent?: CoreRecaptchaComponent;
@ViewChild('ageForm') ageFormElement?: ElementRef; @ViewChild('ageForm') ageFormElement?: ElementRef;
@ViewChild('signupFormEl') signupFormElement?: ElementRef; @ViewChild('signupFormEl') signupFormElement?: ElementRef;
@ -341,9 +343,12 @@ export class CoreLoginEmailSignupPage implements OnInit {
CoreDomUtils.showAlert(Translate.instant('core.success'), message); CoreDomUtils.showAlert(Translate.instant('core.success'), message);
CoreNavigator.back(); CoreNavigator.back();
} else { } else {
if (result.warnings && result.warnings.length) { this.recaptchaComponent?.expireRecaptchaAnswer();
let error = result.warnings[0].message;
if (error == 'incorrect-captcha-sol') { const warning = result.warnings?.[0];
if (warning) {
let error = warning.message;
if (error == 'incorrect-captcha-sol' || (!error && warning.item == 'recaptcharesponse')) {
error = Translate.instant('core.login.recaptchaincorrect'); error = Translate.instant('core.login.recaptchaincorrect');
} }

View File

@ -661,9 +661,7 @@ export class CoreLoginHelperProvider {
// Always open it in browser because the user might have the session stored in there. // Always open it in browser because the user might have the session stored in there.
CoreUtils.openInBrowser(loginUrl); CoreUtils.openInBrowser(loginUrl);
CoreApp.closeApp();
const nav = <any> window.navigator; // eslint-disable-line @typescript-eslint/no-explicit-any
nav.app?.exitApp();
return true; return true;
} }
@ -695,9 +693,7 @@ export class CoreLoginHelperProvider {
}); });
} else { } else {
CoreUtils.openInBrowser(loginUrl); CoreUtils.openInBrowser(loginUrl);
CoreApp.closeApp();
const nav = <any> window.navigator; // eslint-disable-line @typescript-eslint/no-explicit-any
nav.app?.exitApp();
} }
} }

View File

@ -226,16 +226,10 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
// Use a priority lower than 0 (navigation). // Use a priority lower than 0 (navigation).
event.detail.register(-10, async (processNextHandler: () => void) => { event.detail.register(-10, async (processNextHandler: () => void) => {
// This callback can be called at the same time as Ionic's back navigation callback. // This callback can be called at the same time as Ionic's back navigation callback.
// Check if the path changes due to the back navigation handler, to know if we're at root level of the tab. // Check if user is already at the root of a tab.
// Ionic doc recommends IonRouterOutlet.canGoBack, but there's no easy way to get the current outlet from here. const mainMenuRootRoute = CoreNavigator.getCurrentRoute({ routeData: { isMainMenuRoot: true } });
const initialPath = CoreNavigator.getCurrentPath(); if (!mainMenuRootRoute) {
return; // Not at root level, let Ionic handle the navigation.
// The path seems to change immediately (0 ms timeout), but use 50ms just in case.
await CoreUtils.wait(50);
if (CoreNavigator.getCurrentPath() != initialPath) {
// Ionic has navigated back, nothing else to do.
return;
} }
// No back navigation, already at root level. Check if we should change tab. // No back navigation, already at root level. Check if we should change tab.

View File

@ -30,6 +30,9 @@ import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.modu
deps: [Injector], deps: [Injector],
useFactory: (injector: Injector) => buildTabMainRoutes(injector, { useFactory: (injector: Injector) => buildTabMainRoutes(injector, {
component: CoreMainMenuMorePage, component: CoreMainMenuMorePage,
data: {
isMainMenuRoot: true,
},
}), }),
}, },
], ],

View File

@ -562,6 +562,14 @@ export class CoreAppProvider {
return redirect; return redirect;
} }
/**
* Close the app.
*/
closeApp(): void {
const nav = <any> window.navigator; // eslint-disable-line @typescript-eslint/no-explicit-any
nav.app?.exitApp();
}
/** /**
* Forget redirect data. * Forget redirect data.
*/ */