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.blockuser": "message",
"addon.messages.blockuserconfirm": "message",
"addon.messages.cantblockuser": "message",
"addon.messages.contactableprivacy": "message",
"addon.messages.contactableprivacy_coursemember": "message",
"addon.messages.contactableprivacy_onlycontacts": "message",

View File

@ -7,6 +7,7 @@
"blocknoncontacts": "Prevent non-contacts from messaging me",
"blockuser": "Block user",
"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_coursemember": "My contacts and anyone in my courses",
"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.');
}
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 okText = Translate.instant('addon.messages.blockuser');

View File

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

View File

@ -235,12 +235,12 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
* @param inputData The input data.
* @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.
this.saveOffline = this.hasOffline;
try {
return AddonModAssignHelper.prepareSubmissionPluginData(
return await AddonModAssignHelper.prepareSubmissionPluginData(
this.assign!,
this.userSubmission,
inputData,
@ -251,7 +251,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
// Cannot submit in online, prepare for offline usage.
this.saveOffline = true;
return AddonModAssignHelper.prepareSubmissionPluginData(
return await AddonModAssignHelper.prepareSubmissionPluginData(
this.assign!,
this.userSubmission,
inputData,

View File

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

View File

@ -233,8 +233,8 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
*
* @return Promise resolved when done.
*/
protected async sync(): Promise<void> {
await AddonModSurveySync.syncSurvey(this.survey!.id, this.currentUserId);
protected sync(): Promise<AddonModSurveySyncResult> {
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) => {
// This callback should have the lowest priority in the app.
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.
// 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.
const initialPath = CoreNavigator.getCurrentPath();
// The path seems to change immediately (0 ms timeout), but use 50ms just in case.
await CoreUtils.wait(50);
@ -238,8 +244,7 @@ export class AppComponent implements OnInit, AfterViewInit {
}
// Quit the app.
const nav = <any> window.navigator; // eslint-disable-line @typescript-eslint/no-explicit-any
nav.app?.exitApp();
CoreApp.closeApp();
});
});
}

View File

@ -7,7 +7,7 @@
<ion-item *ngIf="model[modelValueName]">
<ion-label color="success">{{ 'core.answered' | translate }}</ion-label>
</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-item>
</div>

View File

@ -87,8 +87,7 @@ export class CoreRecaptchaComponent implements OnInit {
}
if (event.data.action == 'expired') {
this.expired = true;
this.model![this.modelValueName] = '';
this.expireRecaptchaAnswer();
} else if (event.data.action == 'callback') {
this.expired = false;
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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
@ -36,7 +38,7 @@ import { CoreForms } from '@singletons/form';
})
export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
@ViewChild('credentialsForm') formElement?: ElementRef;
@ViewChild('credentialsForm') formElement?: ElementRef<HTMLFormElement>;
credForm!: FormGroup;
siteUrl!: string;
@ -57,6 +59,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
protected viewLeft = false;
protected siteId?: string;
protected urlToOpen?: string;
protected valueChangeSubscription?: Subscription;
constructor(
protected fb: FormBuilder,
@ -96,6 +99,28 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
this.siteChecked = 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 {
this.viewLeft = true;
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';
import { CoreNavigator } from '@services/navigator';
import { CoreForms } from '@singletons/form';
import { CoreRecaptchaComponent } from '@components/recaptcha/recaptcha';
/**
* Page to signup using email.
@ -45,6 +46,7 @@ import { CoreForms } from '@singletons/form';
export class CoreLoginEmailSignupPage implements OnInit {
@ViewChild(IonContent) content?: IonContent;
@ViewChild(CoreRecaptchaComponent) recaptchaComponent?: CoreRecaptchaComponent;
@ViewChild('ageForm') ageFormElement?: ElementRef;
@ViewChild('signupFormEl') signupFormElement?: ElementRef;
@ -341,9 +343,12 @@ export class CoreLoginEmailSignupPage implements OnInit {
CoreDomUtils.showAlert(Translate.instant('core.success'), message);
CoreNavigator.back();
} else {
if (result.warnings && result.warnings.length) {
let error = result.warnings[0].message;
if (error == 'incorrect-captcha-sol') {
this.recaptchaComponent?.expireRecaptchaAnswer();
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');
}

View File

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

View File

@ -226,16 +226,10 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
// Use a priority lower than 0 (navigation).
event.detail.register(-10, async (processNextHandler: () => void) => {
// 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.
// 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.
await CoreUtils.wait(50);
if (CoreNavigator.getCurrentPath() != initialPath) {
// Ionic has navigated back, nothing else to do.
return;
// Check if user is already at the root of a tab.
const mainMenuRootRoute = CoreNavigator.getCurrentRoute({ routeData: { isMainMenuRoot: true } });
if (!mainMenuRootRoute) {
return; // Not at root level, let Ionic handle the navigation.
}
// 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],
useFactory: (injector: Injector) => buildTabMainRoutes(injector, {
component: CoreMainMenuMorePage,
data: {
isMainMenuRoot: true,
},
}),
},
],

View File

@ -562,6 +562,14 @@ export class CoreAppProvider {
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.
*/