Merge pull request #3204 from crazyserver/MOBILE-3833

Mobile 3833
main
Dani Palou 2022-03-28 13:29:50 +02:00 committed by GitHub
commit 617a177fb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
122 changed files with 1118 additions and 1008 deletions

View File

@ -9,11 +9,11 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [core-swipe-navigation]="badges">
<ion-content [core-swipe-navigation]="badges" class="limited-width">
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="badgeLoaded" class="list-item-limited-width">
<core-loading [hideUntil]="badgeLoaded">
<ion-item-group *ngIf="badge">
<ion-item class="ion-text-wrap ion-text-center">
<ion-label>

View File

@ -3,7 +3,7 @@
<h2>{{ 'addon.block_activitymodules.pluginname' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap" *ngFor="let entry of entries" detail="true" button (click)="gotoCoureListModType(entry)">
<core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.iconModName" [showAlt]="false">
</core-mod-icon>

View File

@ -20,7 +20,7 @@
</div>
</div>
</ion-item-divider>
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<core-loading [hideUntil]="loaded">
<ion-row class="ion-justify-content-between ion-align-items-center addon-block-myoverview-filter" *ngIf="hasCourses">
<ion-col size="auto" *ngIf="filters.enabled">

View File

@ -7,7 +7,7 @@
</core-horizontal-scroll-controls>
</div>
</ion-item-divider>
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<core-loading [hideUntil]="loaded">
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg"
[message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box>
<!-- List of courses. -->

View File

@ -7,7 +7,7 @@
</core-horizontal-scroll-controls>
</div>
</ion-item-divider>
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<core-loading [hideUntil]="loaded">
<div [id]="scrollElementId" [hidden]="!items || items.length === 0" class="core-horizontal-scroll"
(scroll)="scrollControls.updateScrollPosition()">
<div *ngIf="items" (onResize)="scrollControls.updateScrollPosition()" class="flex-row">

View File

@ -3,7 +3,7 @@
<h2>{{ 'addon.block_sitemainmenu.pluginname' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="mainMenuBlock" class="core-course-module-list-wrapper">
<ion-item class="ion-text-wrap" *ngIf="mainMenuBlock.summary">
<ion-label>

View File

@ -7,7 +7,7 @@
</core-horizontal-scroll-controls>
</div>
</ion-item-divider>
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<core-loading [hideUntil]="loaded">
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg"
[message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box>
<!-- List of courses. -->

View File

@ -3,7 +3,7 @@
<h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<core-loading [hideUntil]="loaded">
<ion-row class="ion-no-padding ion-justify-content-between ion-align-items-center">
<ion-col size="auto" class="ion-no-padding">
<core-combobox [selection]="filter" (onChange)="switchFilter($event)" icon="fas-filter">
@ -57,11 +57,11 @@
</ion-col>
</ion-row>
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" [fullscreen]="false">
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'">
<addon-block-timeline-events [events]="timeline.events" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMore()"
[from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
</core-loading>
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" [fullscreen]="false" class="safe-area-page">
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'">
<ng-container *ngFor="let course of timelineCourses.courses">
<addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMore(course)"
[course]="course" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>

View File

@ -11,11 +11,11 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<ion-item *ngIf="showMyEntriesToggle">
<ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label>
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)"></ion-toggle>

View File

@ -7,7 +7,7 @@
</core-context-menu>
</core-navbar-buttons>
<core-loading [hideUntil]="loaded" class="safe-area-padding core-loading-full-height">
<core-loading [hideUntil]="loaded">
<div class="core-swipe-slides-container">
<!-- Period name and arrows to navigate. -->
<ion-grid class="ion-no-padding addon-calendar-navigation">

View File

@ -1,8 +1,8 @@
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="fas-calendar" [message]="'addon.calendar.noevents' | translate">
</core-empty-box>
<ion-list *ngIf="filteredEvents && filteredEvents.length" class="ion-no-margin">
<ion-list *ngIf="filteredEvents && filteredEvents.length" class="list-item-limited-width">
<ng-container *ngFor="let event of filteredEvents">
<ion-card>
<ion-item class="ion-text-wrap addon-calendar-event" [attr.aria-label]="event.name" (click)="eventClicked(event)" button

View File

@ -27,7 +27,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="core-loading-full-height">
<core-loading [hideUntil]="loaded">
<div class="core-swipe-slides-container">
<!-- Period name and arrows to navigate. -->
<ion-grid class="ion-no-padding safe-area-padding">
@ -50,9 +50,9 @@
<core-swipe-slides [manager]="manager">
<ng-template let-day="item">
<core-loading [hideUntil]="day.loaded" class="safe-area-padding list-item-limited-width">
<core-loading [hideUntil]="day.loaded">
<!-- There is data to be synchronized -->
<ion-card class="core-warning-card" *ngIf="day.hasOffline">
<ion-card class="core-warning-card list-item-limited-width" *ngIf="day.hasOffline">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}</ion-label>
@ -63,7 +63,7 @@
[message]="'addon.calendar.noevents' | translate">
</core-empty-box>
<ion-list *ngIf="day.filteredEvents && day.filteredEvents.length" class="ion-no-margin">
<ion-list *ngIf="day.filteredEvents && day.filteredEvents.length" class="list-item-limited-width">
<ng-container *ngFor="let event of day.filteredEvents">
<ion-card>
<ion-item class="addon-calendar-event ion-text-wrap" [attr.aria-label]="event.name"

View File

@ -10,11 +10,11 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [core-swipe-navigation]="competencies">
<ion-content [core-swipe-navigation]="competencies" class="limited-width">
<ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competencyLoaded" class="list-item-limited-width">
<core-loading [hideUntil]="competencyLoaded">
<ion-card *ngIf="user">
<ion-item class="ion-text-wrap">
<core-user-avatar [user]="user" slot="start"></core-user-avatar>

View File

@ -10,11 +10,11 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competencyLoaded" class="list-item-limited-width">
<core-loading [hideUntil]="competencyLoaded">
<ion-card *ngIf="competency">
<ion-item class="ion-text-wrap" *ngIf="competency.competency.description">
<ion-label>

View File

@ -8,11 +8,11 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshCourseCompetencies($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competencies.loaded" class="list-item-limited-width">
<core-loading [hideUntil]="competencies.loaded">
<ion-card *ngIf="!user && courseCompetencies && courseCompetencies.statistics.competencycount > 0">
<ng-container *ngIf="courseCompetencies.cangradecompetencies">
<ion-item class="ion-text-wrap" *ngIf="courseCompetencies.settings.pushratingstouserplans">

View File

@ -8,11 +8,11 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [core-swipe-navigation]="plans">
<ion-content [core-swipe-navigation]="plans" class="limited-width">
<ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshLearningPlan($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competencies.loaded" class="list-item-limited-width">
<core-loading [hideUntil]="competencies.loaded">
<ion-card *ngIf="user">
<ion-item class="ion-text-wrap">
<ion-label>

View File

@ -57,8 +57,8 @@
</core-context-menu>
</core-navbar-buttons>
</ion-header>
<ion-content class="has-footer" (ionScroll)="scrollFunction()">
<core-loading [hideUntil]="loaded" class="safe-area-padding-horizontal">
<ion-content (ionScroll)="scrollFunction()">
<core-loading [hideUntil]="loaded">
<!-- Load previous messages. -->
<core-infinite-loading [enabled]="canLoadMore" (action)="loadPrevious($event)" position="top" [error]="loadMoreError">
</core-infinite-loading>

View File

@ -445,13 +445,9 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
return;
}
// Don't use domUtils.getScrollHeight because it gives an outdated value after receiving a new message.
const scrollHeight = this.scrollElement ? this.scrollElement.scrollHeight : 0;
// Check if we are at the bottom to scroll it after render.
// Use a 5px error margin because in iOS there is 1px difference for some reason.
this.scrollBottom = Math.abs(scrollHeight - (this.scrollElement?.scrollTop || 0) -
(this.scrollElement?.clientHeight || 0)) < 5;
this.scrollBottom = CoreDom.scrollIsBottom(this.scrollElement, 5);
if (this.messagesBeingSent > 0) {
// Ignore polling due to a race condition.
@ -510,39 +506,39 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
* The scroll was moved. Update new messages count.
*/
scrollFunction(): void {
if (this.newMessages > 0) {
const scrollBottom = (this.scrollElement?.scrollTop || 0) + (this.scrollElement?.clientHeight || 0);
const scrollHeight = (this.scrollElement?.scrollHeight || 0);
if (scrollBottom > scrollHeight - 40) {
// At the bottom, reset.
this.setNewMessagesBadge(0);
if (this.newMessages == 0) {
return;
}
return;
if (CoreDom.scrollIsBottom(this.scrollElement, 40)) {
// At the bottom, reset.
this.setNewMessagesBadge(0);
return;
}
const scrollElRect = this.scrollElement?.getBoundingClientRect();
const scrollBottomPos = (scrollElRect && scrollElRect.bottom) || 0;
if (scrollBottomPos == 0) {
return;
}
const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine'))
.slice(-this.newMessages)
.reverse();
const newMessagesUnread = messages.findIndex((message) => {
const elementRect = message.getBoundingClientRect();
if (!elementRect) {
return false;
}
const scrollElRect = this.scrollElement?.getBoundingClientRect();
const scrollBottomPos = (scrollElRect && scrollElRect.bottom) || 0;
return elementRect.bottom <= scrollBottomPos;
});
if (scrollBottomPos == 0) {
return;
}
const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine'))
.slice(-this.newMessages)
.reverse();
const newMessagesUnread = messages.findIndex((message) => {
const elementRect = message.getBoundingClientRect();
if (!elementRect) {
return false;
}
return elementRect.bottom <= scrollBottomPos;
});
if (newMessagesUnread > 0 && newMessagesUnread < this.newMessages) {
this.setNewMessagesBadge(newMessagesUnread);
}
if (newMessagesUnread > 0 && newMessagesUnread < this.newMessages) {
this.setNewMessagesBadge(newMessagesUnread);
}
}

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -1,316 +1,92 @@
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<div class="list-item-limited-width">
<!-- User and status of the submission. -->
<ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId"
[attr.aria-label]="user!.fullname">
<core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar>
<ion-label>
<h2>{{ user!.fullname }}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-label>
</ion-item>
<!-- User and status of the submission. -->
<ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId"
[attr.aria-label]="user!.fullname">
<core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar>
<ion-label>
<h2>{{ user!.fullname }}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-label>
</ion-item>
<!-- Status of the submission if user is blinded. -->
<ion-item class="ion-text-wrap" *ngIf="blindMarking && !user">
<ion-label>
<h2>{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-label>
</ion-item>
<!-- Status of the submission if user is blinded. -->
<ion-item class="ion-text-wrap" *ngIf="blindMarking && !user">
<ion-label>
<h2>{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-label>
</ion-item>
<!-- Status of the submission in the rest of cases. -->
<ion-item class="ion-text-wrap" *ngIf="(blindMarking && user) || (!blindMarking && !user)">
<ion-label>
<h2>{{ 'addon.mod_assign.submissionstatus' | translate }}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-label>
</ion-item>
<!-- Status of the submission in the rest of cases. -->
<ion-item class="ion-text-wrap" *ngIf="(blindMarking && user) || (!blindMarking && !user)">
<ion-label>
<h2>{{ 'addon.mod_assign.submissionstatus' | translate }}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-label>
</ion-item>
<!-- Tabs: see the submission or grade it. -->
<core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true" (ionChange)="tabSelected($event)">
<!-- View the submission tab. -->
<core-tab [title]="'addon.mod_assign.submission' | translate" id="submission">
<ng-template>
<addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign"
[submission]="userSubmission" [plugin]="plugin">
</addon-mod-assign-submission-plugin>
<!-- Tabs: see the submission or grade it. -->
<core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true" (ionChange)="tabSelected($event)">
<!-- View the submission tab. -->
<core-tab [title]="'addon.mod_assign.submission' | translate" id="submission">
<ng-template>
<addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign" [submission]="userSubmission"
[plugin]="plugin">
</addon-mod-assign-submission-plugin>
<!-- Render some data about the submission. -->
<ion-item class="ion-text-wrap"
*ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">
<ion-label>
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
<p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<!-- Render some data about the submission. -->
<ion-item class="ion-text-wrap"
*ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">
<ion-label>
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
<p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]">
<ion-label>
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
<p [innerHTML]="timeRemaining"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]">
<ion-label>
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
<p [innerHTML]="timeRemaining"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading">
<ion-label>
<p *ngIf="assign!.intro"
[innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}">
</p>
<p *ngIf="!assign!.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate:
<ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading">
<ion-label>
<p *ngIf="assign!.intro"
[innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}">
</p>
<p *ngIf="!assign!.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate:
{'$a': fromDate}">
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="showDates && assign!.duedate && !isSubmittedForGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.duedate' | translate }}</h2>
<p *ngIf="assign!.duedate">{{ assign!.duedate * 1000 | coreFormatDate }}</p>
<p *ngIf="!assign!.duedate">{{ 'addon.mod_assign.duedateno' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="assign!.duedate && assign!.cutoffdate && isSubmittedForGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2>
<p>{{ assign!.cutoffdate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="assign!.duedate && lastAttempt?.extensionduedate && !isSubmittedForGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2>
<p>{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
<p *ngIf="assign!.maxattempts == unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
</p>
<p *ngIf="assign!.maxattempts != unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
</p>
</ion-label>
</ion-item>
<!-- Add or edit submission. -->
<ion-item class="ion-text-wrap" *ngIf="canEdit">
<ion-label>
<div *ngIf="!unsupportedEditPlugins.length && !showErrorStatementEdit">
<!-- If has offline data, show edit. -->
<ion-button expand="block" class="ion-text-wrap" *ngIf="hasOffline" (click)="goToEdit()">
{{ 'addon.mod_assign.editsubmission' | translate }}
</ion-button>
<!-- If no submission or is new, show add submission. -->
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline &&
(!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)">
{{ 'addon.mod_assign.addsubmission' | translate }}
</ion-button>
<!-- If reopened, show addfromprevious and addnewattempt. -->
<ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened">
<ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" (click)="copyPrevious()">
{{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}
</ion-button>
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()">
{{ 'addon.mod_assign.addnewattempt' | translate }}
</ion-button>
</ng-container>
<!-- Else show editsubmission. -->
<ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline &&
userSubmission && userSubmission!.status &&
userSubmission!.status != statusNew &&
userSubmission!.status != statusReopened" (click)="goToEdit()">
{{ 'addon.mod_assign.editsubmission' | translate }}
</ion-button>
</div>
<div *ngIf="unsupportedEditPlugins && unsupportedEditPlugins.length && !showErrorStatementEdit">
<p class="core-danger-item">{{ 'addon.mod_assign.erroreditpluginsnotsupported' | translate }}</p>
<p class="core-danger-item" *ngFor="let name of unsupportedEditPlugins">{{ name }}</p>
</div>
<div *ngIf="showErrorStatementEdit">
<p class="core-danger-item">{{ 'addon.mod_assign.cannoteditduetostatementsubmission' | translate }}</p>
</div>
</ion-label>
</ion-item>
<!-- Submit for grading form. -->
<ng-container *ngIf="canSubmit">
<ion-item class="ion-text-wrap" *ngIf="submissionStatement">
<ion-label>
<core-format-text [text]="submissionStatement" [filter]="false"></core-format-text>
</ion-label>
<ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="acceptStatement">
</ion-checkbox>
</ion-item>
<!-- Submit button. -->
<ion-item class="ion-text-wrap" *ngIf="!showErrorStatementSubmit">
<ion-label>
<ion-button expand="block" class="ion-text-wrap" (click)="submitForGrading(acceptStatement)">
{{ 'addon.mod_assign.submitassignment' | translate }}
</ion-button>
<p>{{ 'addon.mod_assign.submitassignment_help' | translate }}</p>
</ion-label>
</ion-item>
<!-- Error because we lack submissions statement. -->
<ion-item class="ion-text-wrap" *ngIf="showErrorStatementSubmit">
<ion-label>
<p class="core-danger-item">
{{ 'addon.mod_assign.cannotsubmitduetostatementsubmission' | translate }}
</p>
</ion-label>
</ion-item>
</ng-container>
<!-- Team members that need to submit it too. -->
<ion-item-divider class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0">
<ion-label>
<h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}</h2>
</ion-label>
</ion-item-divider>
<ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && !blindMarking">
<ng-container *ngFor="let user of membersToSubmit">
<ion-item class="ion-text-wrap" core-user-link [userId]="user.id" [courseId]="courseId"
[attr.aria-label]="user.fullname">
<core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar>
<ion-label>
<h2>{{ user.fullname }}</h2>
</ion-label>
</ion-item>
</ng-container>
</ng-container>
<ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && blindMarking">
<ng-container *ngFor="let blindId of membersToSubmitBlind">
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }}</ion-label>
</ion-item>
</ng-container>
</ng-container>
<!-- Submission is locked. -->
<ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked">
<ion-label>
<h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2>
</ion-label>
</ion-item>
<!-- Editing status. -->
<ion-item class="ion-text-wrap" *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined"
[ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}">
<ion-label>
<h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2>
<p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p>
<p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p>
</ion-label>
</ion-item>
</ng-template>
</core-tab>
<!-- Grade the submission tab. -->
<core-tab [title]="'addon.mod_assign.grade' | translate" *ngIf="feedback || isGrading" id="grade">
<ng-template>
<!-- Current grade if method is advanced. -->
<ion-item class="ion-text-wrap core-grading-summary"
*ngIf="feedback?.gradefordisplay && (!isGrading || grade.method != 'simple')">
<ion-label>
<h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
<p>
<core-format-text [text]="feedback!.gradefordisplay" [filter]="false"></core-format-text>
</p>
</ion-label>
<ion-button slot="end" *ngIf="feedback!.advancedgrade" (click)="showAdvancedGrade()"
[attr.aria-label]="'core.showadvanced' |translate">
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
<ng-container *ngIf="isGrading">
<!-- Numeric grade.
Use a text input because otherwise we cannot readthe value if it has an invalid character. -->
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && !grade.scale">
<ion-label position="stacked">
<h2>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</h2>
</ion-label>
<ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade"
[lang]="grade.lang">
</ion-input>
<p *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p>
</ion-item>
<!-- Grade using a scale. -->
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && grade.scale">
<ion-item class="ion-text-wrap" *ngIf="showDates && assign!.duedate && !isSubmittedForGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.grade' | translate }}</h2>
</ion-label>
<ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled"
[interfaceOptions]="{header: 'addon.mod_assign.grade' | translate}">
<ion-select-option *ngFor="let grade of grade.scale" [value]="grade.value">
{{grade.label}}
</ion-select-option>
</ion-select>
</ion-item>
<!-- Outcomes. -->
<ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes">
<ion-label>
<h2>{{ outcome.name }}</h2>
</ion-label>
<ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" interface="action-sheet"
[disabled]="gradeInfo!.disabled" [interfaceOptions]="{header: outcome.name }">
<ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value">
{{grade.label}}
</ion-select-option>
</ion-select>
<p *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p>
</ion-item>
<!-- Gradebook grade for simple grading. -->
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple'">
<ion-label>
<h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
<p *ngIf="grade.gradebookGrade && !grade.scale">
{{ grade.gradebookGrade }}
</p>
<p *ngIf="grade.gradebookGrade && grade.scale">
{{ grade.scale[grade.gradebookGrade].label }}
</p>
<p *ngIf="!grade.gradebookGrade">-</p>
<h2>{{ 'addon.mod_assign.duedate' | translate }}</h2>
<p *ngIf="assign!.duedate">{{ assign!.duedate * 1000 | coreFormatDate }}</p>
<p *ngIf="!assign!.duedate">{{ 'addon.mod_assign.duedateno' | translate }}</p>
</ion-label>
</ion-item>
</ng-container>
<ng-container *ngIf="feedback">
<addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback.plugins" [assign]="assign"
[submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades">
</addon-mod-assign-feedback-plugin>
</ng-container>
<!-- Workflow status. -->
<ion-item class="ion-text-wrap" *ngIf="workflowStatusTranslationId">
<ion-label>
<h2>{{ 'addon.mod_assign.markingworkflowstate' | translate }}</h2>
<p>{{ workflowStatusTranslationId | translate }}</p>
</ion-label>
</ion-item>
<!--- Apply grade to all team members. -->
<ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades">
<ion-label>
<h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2>
<p>{{ 'addon.mod_assign.applytoteam' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle>
</ion-item>
<!-- Attempt status. -->
<ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone">
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap" *ngIf="assign!.duedate && assign!.cutoffdate && isSubmittedForGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2>
<h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2>
<p>{{ assign!.cutoffdate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="assign!.duedate && lastAttempt?.extensionduedate && !isSubmittedForGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2>
<p>{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
<p *ngIf="assign!.maxattempts == unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
@ -319,53 +95,279 @@
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
</p>
</ion-label>
</ion-item>
<!-- Add or edit submission. -->
<ion-item class="ion-text-wrap" *ngIf="canEdit">
<ion-label>
<div *ngIf="!unsupportedEditPlugins.length && !showErrorStatementEdit">
<!-- If has offline data, show edit. -->
<ion-button expand="block" class="ion-text-wrap" *ngIf="hasOffline" (click)="goToEdit()">
{{ 'addon.mod_assign.editsubmission' | translate }}
</ion-button>
<!-- If no submission or is new, show add submission. -->
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline &&
(!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)">
{{ 'addon.mod_assign.addsubmission' | translate }}
</ion-button>
<!-- If reopened, show addfromprevious and addnewattempt. -->
<ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened">
<ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap"
(click)="copyPrevious()">
{{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}
</ion-button>
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()">
{{ 'addon.mod_assign.addnewattempt' | translate }}
</ion-button>
</ng-container>
<!-- Else show editsubmission. -->
<ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline &&
userSubmission && userSubmission!.status &&
userSubmission!.status != statusNew &&
userSubmission!.status != statusReopened" (click)="goToEdit()">
{{ 'addon.mod_assign.editsubmission' | translate }}
</ion-button>
</div>
<div *ngIf="unsupportedEditPlugins && unsupportedEditPlugins.length && !showErrorStatementEdit">
<p class="core-danger-item">{{ 'addon.mod_assign.erroreditpluginsnotsupported' | translate }}</p>
<p class="core-danger-item" *ngFor="let name of unsupportedEditPlugins">{{ name }}</p>
</div>
<div *ngIf="showErrorStatementEdit">
<p class="core-danger-item">{{ 'addon.mod_assign.cannoteditduetostatementsubmission' | translate }}</p>
</div>
</ion-label>
</ion-item>
<!-- Submit for grading form. -->
<ng-container *ngIf="canSubmit">
<ion-item class="ion-text-wrap" *ngIf="submissionStatement">
<ion-label>
<core-format-text [text]="submissionStatement" [filter]="false"></core-format-text>
</ion-label>
<ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="acceptStatement">
</ion-checkbox>
</ion-item>
<!-- Submit button. -->
<ion-item class="ion-text-wrap" *ngIf="!showErrorStatementSubmit">
<ion-label>
<ion-button expand="block" class="ion-text-wrap" (click)="submitForGrading(acceptStatement)">
{{ 'addon.mod_assign.submitassignment' | translate }}
</ion-button>
<p>{{ 'addon.mod_assign.submitassignment_help' | translate }}</p>
</ion-label>
</ion-item>
<!-- Error because we lack submissions statement. -->
<ion-item class="ion-text-wrap" *ngIf="showErrorStatementSubmit">
<ion-label>
<p class="core-danger-item">
{{ 'addon.mod_assign.cannotsubmitduetostatementsubmission' | translate }}
</p>
</ion-label>
</ion-item>
</ng-container>
<!-- Team members that need to submit it too. -->
<ion-item-divider class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0">
<ion-label>
<h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}</h2>
</ion-label>
</ion-item-divider>
<ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && !blindMarking">
<ng-container *ngFor="let user of membersToSubmit">
<ion-item class="ion-text-wrap" core-user-link [userId]="user.id" [courseId]="courseId"
[attr.aria-label]="user.fullname">
<core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar>
<ion-label>
<h2>{{ user.fullname }}</h2>
</ion-label>
</ion-item>
</ng-container>
</ng-container>
<ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && blindMarking">
<ng-container *ngFor="let blindId of membersToSubmitBlind">
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }}</ion-label>
</ion-item>
</ng-container>
</ng-container>
<!-- Submission is locked. -->
<ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked">
<ion-label>
<h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2>
</ion-label>
</ion-item>
<!-- Editing status. -->
<ion-item class="ion-text-wrap" *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined"
[ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}">
<ion-label>
<h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2>
<p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p>
<p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p>
</ion-label>
</ion-item>
</ng-template>
</core-tab>
<!-- Grade the submission tab. -->
<core-tab [title]="'addon.mod_assign.grade' | translate" *ngIf="feedback || isGrading" id="grade">
<ng-template>
<!-- Current grade if method is advanced. -->
<ion-item class="ion-text-wrap core-grading-summary"
*ngIf="feedback?.gradefordisplay && (!isGrading || grade.method != 'simple')">
<ion-label>
<h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
<p>
{{ 'addon.mod_assign.attemptreopenmethod' | translate }}:
{{ 'addon.mod_assign.attemptreopenmethod_' + assign!.attemptreopenmethod | translate }}
<core-format-text [text]="feedback!.gradefordisplay" [filter]="false"></core-format-text>
</p>
</ion-label>
<ion-button slot="end" *ngIf="feedback!.advancedgrade" (click)="showAdvancedGrade()"
[attr.aria-label]="'core.showadvanced' |translate">
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
<ion-item *ngIf="canSaveGrades && allowAddAttempt">
<ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label>
<ion-toggle [(ngModel)]="grade.addAttempt"></ion-toggle>
</ion-item>
</ng-container>
<!-- Data about the grader (teacher who graded). -->
<ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader!.id" [courseId]="courseId"
[attr.aria-label]="grader!.fullname" detail="true">
<core-user-avatar [user]="grader" slot="start" [linkProfile]="false"></core-user-avatar>
<ion-label>
<h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2>
<h2>{{ grader!.fullname }}</h2>
<p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<ng-container *ngIf="isGrading">
<!-- Numeric grade.
Use a text input because otherwise we cannot readthe value if it has an invalid character. -->
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && !grade.scale">
<ion-label position="stacked">
<h2>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</h2>
</ion-label>
<ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade"
[lang]="grade.lang">
</ion-input>
<p *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p>
</ion-item>
<!-- Grader is hidden, display only the grade date. -->
<ion-item class="ion-text-wrap" *ngIf="!grader && feedback?.gradeddate">
<ion-label>
<h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2>
<p>{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<!-- Grade using a scale. -->
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && grade.scale">
<ion-label>
<h2>{{ 'addon.mod_assign.grade' | translate }}</h2>
</ion-label>
<ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled"
[interfaceOptions]="{header: 'addon.mod_assign.grade' | translate}">
<ion-select-option *ngFor="let grade of grade.scale" [value]="grade.value">
{{grade.label}}
</ion-select-option>
</ion-select>
</ion-item>
<!-- Warning message if cannot save grades. -->
<ion-card *ngIf="isGrading && !canSaveGrades" class="core-warning-card">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
<!-- Outcomes. -->
<ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes">
<ion-label>
<h2>{{ outcome.name }}</h2>
</ion-label>
<ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId"
interface="action-sheet" [disabled]="gradeInfo!.disabled" [interfaceOptions]="{header: outcome.name }">
<ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value">
{{grade.label}}
</ion-select-option>
</ion-select>
<p *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p>
</ion-item>
<!-- Gradebook grade for simple grading. -->
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple'">
<ion-label>
<h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
<p *ngIf="grade.gradebookGrade && !grade.scale">
{{ grade.gradebookGrade }}
</p>
<p *ngIf="grade.gradebookGrade && grade.scale">
{{ grade.scale[grade.gradebookGrade].label }}
</p>
<p *ngIf="!grade.gradebookGrade">-</p>
</ion-label>
</ion-item>
</ng-container>
<ng-container *ngIf="feedback">
<addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback.plugins" [assign]="assign"
[submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades">
</addon-mod-assign-feedback-plugin>
</ng-container>
<!-- Workflow status. -->
<ion-item class="ion-text-wrap" *ngIf="workflowStatusTranslationId">
<ion-label>
<p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p>
<ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link [showBrowserWarning]="false">
{{ 'core.openinbrowser' | translate }}
<ion-icon name="fas-external-link-alt" slot="end" aria-hidden="true"></ion-icon>
</ion-button>
<h2>{{ 'addon.mod_assign.markingworkflowstate' | translate }}</h2>
<p>{{ workflowStatusTranslationId | translate }}</p>
</ion-label>
</ion-item>
</ion-card>
</ng-template>
</core-tab>
</core-tabs>
<!--- Apply grade to all team members. -->
<ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades">
<ion-label>
<h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2>
<p>{{ 'addon.mod_assign.applytoteam' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle>
</ion-item>
<!-- Attempt status. -->
<ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2>
<p *ngIf="assign!.maxattempts == unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
</p>
<p *ngIf="assign!.maxattempts != unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
</p>
<p>
{{ 'addon.mod_assign.attemptreopenmethod' | translate }}:
{{ 'addon.mod_assign.attemptreopenmethod_' + assign!.attemptreopenmethod | translate }}
</p>
</ion-label>
</ion-item>
<ion-item *ngIf="canSaveGrades && allowAddAttempt">
<ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label>
<ion-toggle [(ngModel)]="grade.addAttempt"></ion-toggle>
</ion-item>
</ng-container>
<!-- Data about the grader (teacher who graded). -->
<ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader!.id" [courseId]="courseId"
[attr.aria-label]="grader!.fullname" detail="true">
<core-user-avatar [user]="grader" slot="start" [linkProfile]="false"></core-user-avatar>
<ion-label>
<h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2>
<h2>{{ grader!.fullname }}</h2>
<p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<!-- Grader is hidden, display only the grade date. -->
<ion-item class="ion-text-wrap" *ngIf="!grader && feedback?.gradeddate">
<ion-label>
<h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2>
<p>{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<!-- Warning message if cannot save grades. -->
<ion-card *ngIf="isGrading && !canSaveGrades" class="core-warning-card">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p>
<ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link [showBrowserWarning]="false">
{{ 'core.openinbrowser' | translate }}
<ion-icon name="fas-external-link-alt" slot="end" aria-hidden="true"></ion-icon>
</ion-button>
</ion-label>
</ion-item>
</ion-card>
</ng-template>
</core-tab>
</core-tabs>
</div>
</core-loading>
<!-- Template to render some data regarding the submission status. -->

View File

@ -15,7 +15,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-page list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -1,10 +1,4 @@
:host {
core-loading ::ng-deep .core-loading-content {
min-height: 100%;
display: flex;
flex-direction: column;
}
.core-swipe-slides-container {
ion-card {
flex: none;

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-padding list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -16,8 +16,8 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="has-footer">
<core-loading [hideUntil]="loaded" class="safe-area-padding">
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list class="addon-messages-discussion-container" aria-live="polite">
<ng-container *ngFor="let message of messages; index as index; last as last">

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -12,7 +12,7 @@
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshMessages($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="safe-area-padding">
<core-loading [hideUntil]="loaded">
<ion-list class="addon-messages-discussion-container">
<ng-container *ngFor="let message of messages; index as index;">

View File

@ -7,66 +7,63 @@
<!-- Content. -->
<core-loading [hideUntil]="!showLoading">
<div class="list-item-limited-width">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
[courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()">
</core-course-module-info>
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
[courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()">
</core-course-module-info>
<!-- Activity availability messages -->
<ion-card class="core-info-card" *ngIf="choiceNotOpenYet">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<!-- Activity availability messages -->
<ion-card class="core-info-card" *ngIf="choiceNotOpenYet">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p *ngIf="options.length">{{ 'addon.mod_choice.previewonly' | translate:{$a: openTimeReadable} }}</p>
<p *ngIf="!options.length">{{ 'addon.mod_choice.notopenyet' | translate:{$a: openTimeReadable} }}</p>
</ion-label>
</ion-item>
</ion-card>
<ion-card class="core-info-card" *ngIf="choiceClosed">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p *ngIf="options.length">
{{ 'addon.mod_choice.yourselection' | translate }}
<core-format-text [text]="options[0].text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</p>
<p>{{ 'addon.mod_choice.expired' | translate:{$a: closeTimeReadable} }}</p>
</ion-label>
</ion-item>
</ion-card>
<!-- Inform what will happen with the choices. -->
<ion-card class="core-info-card" *ngIf="canEdit && publishInfo && options.length">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ publishInfo | translate }}</ion-label>
</ion-item>
</ion-card>
<!-- Choice options -->
<ion-card *ngIf="options.length && choice">
<ng-container *ngIf="choice.allowmultiple">
<ion-item class="ion-text-wrap" *ngFor="let option of options">
<ion-label>
<p *ngIf="options.length">{{ 'addon.mod_choice.previewonly' | translate:{$a: openTimeReadable} }}</p>
<p *ngIf="!options.length">{{ 'addon.mod_choice.notopenyet' | translate:{$a: openTimeReadable} }}</p>
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
</ion-label>
<ion-checkbox slot="end" [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit"></ion-checkbox>
</ion-item>
</ion-card>
<ion-card class="core-info-card" *ngIf="choiceClosed">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
</ng-container>
<ion-radio-group *ngIf="!choice.allowmultiple" [(ngModel)]="selectedOption.id">
<ion-item class="ion-text-wrap" *ngFor="let option of options">
<ion-label>
<p *ngIf="options.length">
{{ 'addon.mod_choice.yourselection' | translate }}
<core-format-text [text]="options[0].text" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</p>
<p>{{ 'addon.mod_choice.expired' | translate:{$a: closeTimeReadable} }}</p>
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
</ion-label>
<ion-radio slot="end" [value]="option.id" [disabled]="option.disabled || !canEdit"></ion-radio>
</ion-item>
</ion-card>
<!-- Inform what will happen with the choices. -->
<ion-card class="core-info-card" *ngIf="canEdit && publishInfo && options.length">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ publishInfo | translate }}</ion-label>
</ion-item>
</ion-card>
<!-- Choice options -->
<ion-card *ngIf="options.length && choice">
<ng-container *ngIf="choice.allowmultiple">
<ion-item class="ion-text-wrap" *ngFor="let option of options">
<ion-label>
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
</ion-label>
<ion-checkbox slot="end" [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit"></ion-checkbox>
</ion-item>
</ng-container>
<ion-radio-group *ngIf="!choice.allowmultiple" [(ngModel)]="selectedOption.id">
<ion-item class="ion-text-wrap" *ngFor="let option of options">
<ion-label>
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
</ion-label>
<ion-radio slot="end" [value]="option.id" [disabled]="option.disabled || !canEdit"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-card>
</div>
</ion-radio-group>
</ion-card>
<!-- Choice results -->
<div *ngIf="canSeeResults && choice">
<ion-item-divider>

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -18,7 +18,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -11,13 +11,12 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!entryLoaded || !(isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)"
(ionRefresh)="refreshDatabase($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="entryLoaded && (isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)"
class="list-item-limited-width">
<core-loading [hideUntil]="entryLoaded && (isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)">
<!-- Database entries found to be synchronized -->
<ion-card class="core-warning-card" *ngIf="entry && entry.hasOffline">
<ion-item>

View File

@ -15,7 +15,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -11,8 +11,8 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="feedbackLoaded" class="has-spacer list-item-limited-width">
<ion-content class="limited-width">
<core-loading [hideUntil]="feedbackLoaded" class="has-spacer">
<ng-container *ngIf="items && items.length">
<ion-list class="ion-no-margin has-spacer">
<ion-item class="ion-text-wrap">

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info *ngIf="!subfolder" [module]="module" [description]="description" [component]="component"

View File

@ -15,7 +15,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="subfolder || activityComponent?.showLoading"
(ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>

View File

@ -1,11 +1,4 @@
:host {
core-loading:not(.core-loading-loaded) > .core-loading-container {
position: relative !important;
padding-top: 10px !important;
padding-bottom: 10px !important;
overflow: hidden;
}
core-loading > .core-loading-container .core-loading-message {
display: none;
}
core-loading {
--loading-inline-min-height: 48px;
--loading-display-message: none;
}

View File

@ -55,12 +55,12 @@
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content [core-swipe-navigation]="discussions">
<ion-content [core-swipe-navigation]="discussions" class="limited-width">
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="discussionLoaded" class="list-item-limited-width">
<core-loading [hideUntil]="discussionLoaded">
<!-- Discussion replies found to be synchronized -->
<ion-card class="core-warning-card" *ngIf="postHasOffline || hasOfflineRatings">
<ion-item>

View File

@ -11,12 +11,12 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [core-swipe-navigation]="entries">
<ion-content [core-swipe-navigation]="entries" class="limited-width">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<ng-container *ngIf="entry && loaded">
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>

View File

@ -16,7 +16,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-padding">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-padding list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -15,6 +15,6 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<addon-mod-imscp-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-imscp-index>
</ion-content>

View File

@ -19,7 +19,7 @@
</ion-header>
<ion-content>
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="safe-area-padding core-loading-full-height">
<core-loading [hideUntil]="loaded">
<ion-card class="core-warning-card" *ngIf="warning">
<ion-item>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<core-tabs [hideUntil]="!showLoading" [selectedIndex]="selectedTab">
<!-- Index/Preview tab. -->
<core-tab [title]="'addon.mod_lesson.preview' | translate" (ionSelect)="indexSelected()">

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -17,8 +17,8 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<ion-content class="limited-width">
<core-loading [hideUntil]="loaded">
<!-- Info messages. Only show the first one. -->
<ion-card class="core-info-card" *ngIf="lesson && messages?.length">
<ion-item class="ion-text-wrap">

View File

@ -8,12 +8,12 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<div *ngIf="student">
<!-- Student data. -->
<ion-item class="ion-text-wrap" core-user-link [userId]="student.id" [courseId]="courseId" [attr.aria-label]="student.fullname">

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-padding list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="displayDescription && description" [component]="component"

View File

@ -15,7 +15,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-padding list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="displayDescription && description" [component]="component"

View File

@ -1,8 +0,0 @@
/* Solves iframe height */
.core-loading-content > div[padding] {
height: 100%;
}
core-format-text > .no-overflow {
display: inline;
}

View File

@ -27,7 +27,6 @@ import { AddonModPageHelper } from '../../services/page-helper';
@Component({
selector: 'addon-mod-page-index',
templateUrl: 'addon-mod-page-index.html',
styleUrls: ['index.scss'],
})
export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -12,11 +12,11 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="attempt">
<ion-item class="ion-text-wrap">
<ion-label>

View File

@ -15,7 +15,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -30,8 +30,8 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded" class="has-spacer list-item-limited-width">
<ion-content class="limited-width">
<core-loading [hideUntil]="loaded" class="has-spacer">
<!-- Button to start attempting. -->
<ion-button *ngIf="!attempt" expand="block" class="ion-margin" (click)="start()">
{{ 'addon.mod_quiz.startattempt' | translate }}

View File

@ -15,11 +15,11 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<!-- Review summary -->
<ion-card *ngIf="attempt">

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-padding core-loading-full-height list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [courseId]="courseId" [description]="displayDescription && description"

View File

@ -15,7 +15,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading || activityComponent?.mode == 'iframe'"
(ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-padding list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -20,7 +20,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded" class="core-loading-full-height">
<core-loading [hideUntil]="loaded">
<core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"
[showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true">
</core-iframe>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="safe-area-padding list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="survey && !survey.surveydone && !hasOffline && description"

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="displayDescription && description" [component]="component"

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -27,7 +27,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"

View File

@ -15,7 +15,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -1,4 +1,4 @@
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap" [detail]="canViewAssessment && !canSelfAssess" (click)="gotoAssessment($event)"
[button]="canViewAssessment && !canSelfAssess">
<core-user-avatar [user]="profile" slot="start" [courseId]="courseId" [userId]="profile?.id"></core-user-avatar>

View File

@ -6,7 +6,7 @@
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="!showLoading" class="list-item-limited-width">
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()">

View File

@ -1,4 +1,4 @@
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<core-loading [hideUntil]="loaded">
<div *ngIf="!summary">
<ion-item class="ion-text-wrap addon-workshop-submission-title">
<core-user-avatar [user]="profile" [courseId]="courseId" [userId]="profile?.id" slot="start">

View File

@ -14,7 +14,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="activityComponent?.showLoading" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -20,7 +20,7 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)"
*ngIf="!((assessmentId && access.assessingallowed) || canAddFeedback)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>

View File

@ -25,12 +25,12 @@
[closeOnClick]="false"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!notesLoaded" (ionRefresh)="refreshNotes(false, $event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="notesLoaded" class="list-item-limited-width">
<core-loading [hideUntil]="notesLoaded">
<ion-item class="ion-text-wrap" *ngIf="user">
<core-user-avatar [user]="user" [courseId]="courseId" slot="start" [linkProfile]="false"></core-user-avatar>
<ion-label>

View File

@ -1,90 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreContentLinksDelegate, CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
/**
* Component that displays the actions for a notification.
*/
@Component({
selector: 'addon-notifications-actions',
templateUrl: 'addon-notifications-actions.html',
})
export class AddonNotificationsActionsComponent implements OnInit {
@Input() contextUrl?: string;
@Input() courseId?: number;
@Input() data?: Record<string, unknown>; // Extra data to handle the URL.
actions: CoreContentLinksAction[] = [];
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
if (!this.contextUrl && (!this.data || !this.data.appurl)) {
// No URL, nothing to do.
return;
}
let actions: CoreContentLinksAction[] = [];
// Treat appurl first if any.
if (this.data?.appurl) {
actions = await CoreContentLinksDelegate.getActionsFor(
<string> this.data.appurl,
this.courseId,
undefined,
this.data,
);
}
if (!actions.length && this.contextUrl) {
// No appurl or cannot handle it. Try with contextUrl.
actions = await CoreContentLinksDelegate.getActionsFor(this.contextUrl, this.courseId, undefined, this.data);
}
if (!actions.length) {
// URL is not supported. Add an action to open it in browser.
actions.push({
message: 'core.view',
icon: 'fas-eye',
action: this.openInBrowser.bind(this),
});
}
this.actions = actions;
}
/**
* Default action. Open in browser.
*
* @param siteId Site ID to use.
*/
protected async openInBrowser(siteId?: string): Promise<void> {
const url = <string> this.data?.appurl || this.contextUrl;
if (!url) {
return;
}
const site = await CoreSites.getSite(siteId);
site.openInBrowserWithAutoLogin(url);
}
}

View File

@ -1,8 +0,0 @@
<ion-row *ngIf="actions && actions.length > 0" class="justify-content-around">
<ion-col *ngFor="let action of actions">
<ion-button fill="clear" expand="block" (click)="action.action()">
<ion-icon slot="start" name="{{action.icon}}" aria-hidden="true"></ion-icon>
{{ action.message | translate }}
</ion-button>
</ion-col>
</ion-row>

View File

@ -1,31 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonNotificationsActionsComponent } from './actions/actions';
@NgModule({
declarations: [
AddonNotificationsActionsComponent,
],
imports: [
CoreSharedModule,
],
exports: [
AddonNotificationsActionsComponent,
],
})
export class AddonNotificationsComponentsModule {}

View File

@ -2,6 +2,7 @@
:host {
--extra-icon-size: 16px;
--icon-size: 24px;
::ng-deep core-user-avatar {
.core-avatar-extra-img,
@ -28,71 +29,18 @@
}
}
.core-notification-icon {
width: var(--core-avatar-size);
height: var(--core-avatar-size);
@include margin(6px, 8px, 6px, 0px);
div.core-notification-icon {
img {
width: var(--icon-size);
height: var(--icon-size);
}
padding: 0.7rem;
background: var(--background-color);
border-radius: var(--small-radius);
}
.item core-format-text ::ng-deep {
.forumpost {
border: 1px solid var(--gray-200);
width: 100%;
margin: 0 0 1em 0;
td {
padding: 10px;
}
.header {
background-color: var(--light);
.picture {
width: 48px;
text-align: center;
@include padding-horizontal(4px, 0px);
padding-top: 8px;
img {
width: 44px !important;
}
}
}
.subject {
font-weight: 700;
margin-bottom: 1rem;
}
}
a {
text-decoration: none;
}
.userpicture {
border-radius: 50%;
}
.mdl-right {
text-align: end;
a {
display: none;
}
font {
font-size: 0.9em;
}
}
.commands {
display: none;
}
hr {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
background-color: var(--gray-200);
}
.core-notification-icon {
--module-icon-size: var(--icon-size);
@include margin(6px, 8px, 6px, 0px);
}
}

View File

@ -11,69 +11,50 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!notificationsLoaded" (ionRefresh)="refreshNotifications($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="notificationsLoaded">
<div class="list-item-limited-width">
<div class="ion-padding" *ngIf="canMarkAllNotificationsAsRead">
<ion-button [disabled]="loadingMarkAllNotificationsAsRead" expand="block" (click)="markAllNotificationsAsRead()"
fill="outline">
<ion-icon slot="start" name="fas-check" aria-hidden="true" *ngIf="!loadingMarkAllNotificationsAsRead"></ion-icon>
<ion-spinner slot="start" [attr.aria-label]="'core.loading' | translate" *ngIf="loadingMarkAllNotificationsAsRead">
</ion-spinner>
{{ 'addon.notifications.markallread' | translate }}
</ion-button>
</div>
<ion-card *ngFor="let notification of notifications">
<ion-item class="ion-text-wrap" [attr.aria-label]="
<ion-item *ngFor="let notification of notifications" class="ion-text-wrap" [attr.aria-label]="
notification.timeread
? notification.subject
: 'addon.notifications.unreadnotification' | translate: {$a: notification.subject}">
<core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start"
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
[userId]="notification.useridfrom">
<div class="core-avatar-extra-img" *ngIf="notification.iconurl && !notification.modname">
<img [src]="notification.iconurl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
[showAlt]="false">
</core-mod-icon>
</core-user-avatar>
: 'addon.notifications.unreadnotification' | translate: {$a: notification.subject}"
(click)="openNotification(notification)" button [detail]="false" lines="full">
<core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start"
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
[userId]="notification.useridfrom">
<div class="core-avatar-extra-img" *ngIf="notification.iconurl && !notification.modname">
<img [src]="notification.iconurl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
[showAlt]="false">
</core-mod-icon>
</core-user-avatar>
<img *ngIf="notification.useridfrom <= 0 && notification.iconurl" [src]="notification.iconurl" alt=""
role="presentation" class="core-notification-icon" slot="start">
<ng-container *ngIf="notification.useridfrom <= 0 && notification.iconurl">
<div class="core-notification-icon" *ngIf="!notification.modname" slot="start">
<img [src]="notification.iconurl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
[showAlt]="false" class="core-notification-icon" slot="start">
</core-mod-icon>
</ng-container>
<ion-label>
<p class="item-heading">
<core-format-text [text]="notification.subject" contextLevel="system" [contextInstanceId]="0"
[wsNotFiltered]="true">
</core-format-text>
</p>
<p *ngIf="notification.useridfrom > 0">{{ notification.userfromfullname }}</p>
</ion-label>
<ion-note slot="end" class="ion-float-end ion-text-end">
{{ notification.timecreated | coreDateDayOrTime }}
<span *ngIf="!notification.timeread">
<ion-icon name="fas-circle" color="primary" aria-hidden="true"></ion-icon>
</span>
</ion-note>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [text]="notification.mobiletext | coreCreateLinks" contextLevel="system" [contextInstanceId]="0"
collapsible-item>
</core-format-text>
</ion-label>
</ion-item>
<addon-notifications-actions [contextUrl]="notification.contexturl" [courseId]="notification.courseid"
[data]="notification.customdata">
</addon-notifications-actions>
</ion-card>
</div>
<ion-label>
<p class="item-heading">
<core-format-text [text]="notification.subject" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
</core-format-text>
</p>
<p>{{ notification.timecreated | coreTimeAgo }}<ng-container *ngIf="notification.useridfrom > 0"> · {{
notification.userfromfullname }}</ng-container>
</p>
</ion-label>
<ion-note slot="end" *ngIf="!notification.timeread">
<ion-icon name="fas-circle" color="primary" aria-hidden="true"></ion-icon>
</ion-note>
</ion-item>
<core-empty-box *ngIf="!notifications || notifications.length <= 0" icon="far-bell"
[message]="'addon.notifications.therearentnotificationsyet' | translate">
@ -81,4 +62,15 @@
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreNotifications($event)" [error]="loadMoreError">
</core-infinite-loading>
</core-loading>
<div class="mark-all-as-read" slot="fixed" collapsible-footer appearOnBottom>
<ion-chip *ngIf="notificationsLoaded && canMarkAllNotificationsAsRead" [disabled]="loadingMarkAllNotificationsAsRead"
color="primary" (click)="markAllNotificationsAsRead()">
<ion-icon name="fas-eye" aria-hidden="true" *ngIf="!loadingMarkAllNotificationsAsRead"></ion-icon>
<ion-spinner [attr.aria-label]="'core.loading' | translate" *ngIf="loadingMarkAllNotificationsAsRead">
</ion-spinner>
{{ 'addon.notifications.markallread' | translate }}
</ion-chip>
</div>
</ion-content>

View File

@ -16,7 +16,6 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonNotificationsComponentsModule } from '../../components/components.module';
import { AddonNotificationsListPage } from './list';
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
@ -31,7 +30,6 @@ const routes: Routes = [
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonNotificationsComponentsModule,
CoreMainMenuComponentsModule,
],
declarations: [

View File

@ -0,0 +1,72 @@
@import "~theme/globals";
ion-item {
ion-label {
margin-top: 8px;
margin-bottom: 8px;
p.item-heading {
font-size: 14px;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
display: -webkit-box;
}
p {
font-size: 12px;
}
}
ion-note {
margin: 0;
@include padding-horizontal(8px, 0);
padding-top: 12px;
ion-icon {
font-size: 6px;
vertical-align: middle;
}
}
[slot=start] {
align-self: start;
margin-top: 16px;
}
--icon-size: 16px;
--extra-icon-size: 12px;
--core-avatar-size: 32px;
div.core-notification-icon,
core-mod-icon.core-notification-icon {
padding: 8px;
}
}
.mark-all-as-read {
padding-bottom: 16px;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: transparent;
box-shadow: none;
pointer-events: none;
ion-chip.ion-color {
pointer-events: all;
margin: 0 auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, .4);
ion-spinner {
width: 16px;
height: 16px;
@include margin-horizontal(0, 8px);
}
}
}
ion-content::part(scroll) {
padding-bottom: 44px;
}

View File

@ -30,6 +30,8 @@ import {
AddonNotificationsNotificationToRender,
} from '@addons/notifications/services/notifications-helper';
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
import { CoreNavigator } from '@services/navigator';
import { CoreTimeUtils } from '@services/utils/time';
/**
* Page that displays the list of notifications.
@ -37,7 +39,7 @@ import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-lin
@Component({
selector: 'page-addon-notifications-list',
templateUrl: 'list.html',
styleUrls: ['../../notifications.scss'],
styleUrls: ['list.scss', '../../notifications.scss'],
})
export class AddonNotificationsListPage implements OnInit, OnDestroy {
@ -50,6 +52,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
protected isCurrentView?: boolean;
protected cronObserver?: CoreEventObserver;
protected readObserver?: CoreEventObserver;
protected pushObserver?: Subscription;
protected pendingRefresh = false;
@ -84,6 +87,21 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
this.refreshNotifications();
});
this.readObserver = CoreEvents.on(AddonNotificationsProvider.READ_CHANGED_EVENT, (data) => {
if (!data.id) {
return;
}
const notification = this.notifications.find((notification) => notification.id === data.id);
if (!notification) {
return;
}
notification.read = true;
notification.timeread = data.time;
this.loadMarkAllAsReadButton();
});
const deepLinkManager = new CoreMainMenuDeepLinkManager();
deepLinkManager.treatLink();
}
@ -91,7 +109,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
/**
* Convenience function to get notifications. Gets unread notifications first.
*
* @param refreh Whether we're refreshing data.
* @param refresh Whether we're refreshing data.
* @return Resolved when done.
*/
protected async fetchNotifications(refresh?: boolean): Promise<void> {
@ -110,7 +128,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
}
this.canLoadMore = result.canLoadMore;
this.markNotificationsAsRead(notifications);
await this.loadMarkAllAsReadButton();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.notifications.errorgetnotifications', true);
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
@ -129,7 +147,9 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
await CoreUtils.ignoreErrors(AddonNotifications.markAllNotificationsAsRead());
CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, {}, CoreSites.getCurrentSiteId());
CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, {
time: CoreTimeUtils.timestamp(),
}, CoreSites.getCurrentSiteId());
// All marked as read, refresh the list.
this.notificationsLoaded = false;
@ -138,28 +158,11 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
}
/**
* Mark notifications as read.
* Load mark all notifications as read button.
*
* @param notifications Array of notification objects.
* @return Promise resolved when done.
*/
protected async markNotificationsAsRead(notifications: AddonNotificationsNotificationToRender[]): Promise<void> {
if (notifications.length > 0) {
const promises = notifications.map(async (notification) => {
if (notification.read) {
// Already read, don't mark it.
return;
}
await AddonNotifications.markNotificationRead(notification.id);
});
await CoreUtils.ignoreErrors(Promise.all(promises));
await CoreUtils.ignoreErrors(AddonNotifications.invalidateNotificationsList());
CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, {}, CoreSites.getCurrentSiteId());
}
protected async loadMarkAllAsReadButton(): Promise<void> {
// Check if mark all as read should be displayed (there are unread notifications).
try {
this.loadingMarkAllNotificationsAsRead = true;
@ -201,6 +204,15 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
}
}
/**
* Open Notification page.
*
* @param notification Notification to open.
*/
openNotification(notification: AddonNotificationsNotificationToRender): void {
CoreNavigator.navigate('../notification', { params: { notification } });
}
/**
* User entered the page.
*/
@ -229,6 +241,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy {
*/
ngOnDestroy(): void {
this.cronObserver?.off();
this.readObserver?.off();
this.pushObserver?.unsubscribe();
}

View File

@ -4,34 +4,58 @@
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<h2>{{ 'addon.notifications.notifications' | translate }}</h2>
<h1>{{ 'addon.notifications.notifications' | translate }}</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap" [attr.aria-label]="subject">
<core-user-avatar *ngIf="userIdFrom > 0" slot="start" [userId]="userIdFrom" [profileUrl]="profileImageUrlFrom"
[fullname]="userFromFullName">
<div class="core-avatar-extra-img" *ngIf="iconUrl && !modname">
<img [src]="iconUrl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false">
</core-mod-icon>
</core-user-avatar>
<div class="list-item-limited-width">
<img *ngIf="userIdFrom <= 0 && iconUrl" [src]="iconUrl" alt="" role="presentation" class="core-notification-icon" slot="start">
<ion-item class="ion-text-wrap core-notification-title" lines="full">
<core-user-avatar *ngIf="userIdFrom > 0" slot="start" [userId]="userIdFrom" [profileUrl]="profileImageUrlFrom"
[fullname]="userFromFullName">
<div class="core-avatar-extra-img" *ngIf="iconUrl && !modname">
<img [src]="iconUrl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false">
</core-mod-icon>
</core-user-avatar>
<ion-label>
<p class="item-heading">{{ subject }}</p>
<p *ngIf="userIdFrom > 0">{{ userFromFullName }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [text]="content | coreCreateLinks" contextLevel="system" [contextInstanceId]="0">
</core-format-text>
</ion-label>
</ion-item>
<ng-container *ngIf="userIdFrom <= 0 && iconUrl">
<div class="core-notification-icon" *ngIf="!modname" slot="start">
<img [src]="iconUrl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false" class="core-notification-icon"
slot="start">
</core-mod-icon>
</ng-container>
<ion-label>
<p class="item-heading">
<core-format-text [text]="subject" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
</core-format-text>
</p>
<p>{{ timecreated | coreTimeAgo }}<ng-container *ngIf="userIdFrom > 0"> · {{
userFromFullName }}</ng-container>
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap core-notification-body">
<ion-label>
<core-format-text [text]="content | coreCreateLinks" contextLevel="system" [contextInstanceId]="0">
</core-format-text>
</ion-label>
</ion-item>
</div>
<div collapsible-footer appearOnBottom *ngIf="loaded && actions && actions.length > 0" slot="fixed">
<div class="list-item-limited-width adaptable-buttons-row">
<ion-button expand="block" (click)="action.action()" *ngFor="let action of actions">
<ion-icon slot="start" name="{{action.icon}}" aria-hidden="true"></ion-icon>
{{ action.message | translate }}
</ion-button>
</div>
</div>
</core-loading>
</ion-content>

View File

@ -16,7 +16,6 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonNotificationsComponentsModule } from '../../components/components.module';
import { AddonNotificationsNotificationPage } from './notification';
const routes: Routes = [
@ -30,7 +29,6 @@ const routes: Routes = [
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonNotificationsComponentsModule,
],
declarations: [
AddonNotificationsNotificationPage,

View File

@ -0,0 +1,87 @@
@import "~theme/globals";
:host {
.core-notification-title {
[slot=start] {
align-self: start;
margin-top: 16px;
}
p.item-heading {
font-size: 16px;
}
p {
font-size: 12px;
}
}
.core-notification-body {
core-format-text {
font-size: 14px;
}
h2 {
font-weight: bold;
}
core-format-text ::ng-deep {
.forumpost {
border: 1px solid var(--gray-200);
width: 100%;
margin: 0 0 1em 0;
td {
padding: 10px;
}
.header {
background-color: var(--light);
.picture {
width: 48px;
text-align: center;
@include padding-horizontal(4px, 0px);
padding-top: 8px;
img {
width: 44px !important;
}
}
}
.subject {
font-weight: 700;
margin-bottom: 1rem;
}
}
a {
text-decoration: none;
}
.userpicture {
border-radius: 50%;
}
.mdl-right {
text-align: end;
a {
display: none;
}
font {
font-size: 0.9em;
}
}
.commands {
display: none;
}
hr {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
background-color: var(--gray-200);
}
}
}
}

View File

@ -19,7 +19,9 @@ import {
AddonNotificationsNotificationToRender,
} from '@addons/notifications/services/notifications-helper';
import { Component, OnInit } from '@angular/core';
import { CoreContentLinksAction, CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
@ -29,7 +31,7 @@ import { CoreUtils } from '@services/utils/utils';
@Component({
selector: 'page-addon-notifications-notification',
templateUrl: 'notification.html',
styleUrls: ['../../notifications.scss'],
styleUrls: ['../../notifications.scss', 'notification.scss'],
})
export class AddonNotificationsNotificationPage implements OnInit {
@ -41,12 +43,19 @@ export class AddonNotificationsNotificationPage implements OnInit {
iconUrl?: string; // Icon URL.
modname?: string; // Module name.
loaded = false;
timecreated = 0;
// Actions data.
actions: CoreContentLinksAction[] = [];
contextUrl?: string;
courseId?: number;
actionsData?: Record<string, unknown>; // Extra data to handle the URL.
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
let notification: AddonNotificationsNotificationToRender | AddonNotificationsNotificationData;
let notification: AddonNotificationsNotification;
try {
notification = CoreNavigator.getRequiredRouteParam('notification');
@ -85,6 +94,8 @@ export class AddonNotificationsNotificationPage implements OnInit {
this.modname = modname;
}
}
this.timecreated = notification.timecreated;
} else {
this.subject = notification.title || '';
this.content = notification.message || '';
@ -93,7 +104,73 @@ export class AddonNotificationsNotificationPage implements OnInit {
this.userFromFullName = notification.userfromfullname;
}
await this.loadActions(notification);
AddonNotificationsHelper.markNotificationAsRead(notification);
this.loaded = true;
}
/**
* Load notification actions
*
* @param notification Notification.
* @return Promise resolved when done.
*/
async loadActions(notification: AddonNotificationsNotification): Promise<void> {
if (!notification.contexturl && (!notification.customdata || !notification.customdata.appurl)) {
// No URL, nothing to do.
return;
}
let actions: CoreContentLinksAction[] = [];
this.actionsData = notification.customdata;
this.contextUrl = notification.contexturl;
this.courseId = 'courseid' in notification ? notification.courseid : undefined;
// Treat appurl first if any.
if (this.actionsData?.appurl) {
actions = await CoreContentLinksDelegate.getActionsFor(
<string> this.actionsData.appurl,
this.courseId,
undefined,
this.actionsData,
);
}
if (!actions.length && this.contextUrl) {
// No appurl or cannot handle it. Try with contextUrl.
actions = await CoreContentLinksDelegate.getActionsFor(this.contextUrl, this.courseId, undefined, this.actionsData);
}
if (!actions.length) {
// URL is not supported. Add an action to open it in browser.
actions.push({
message: 'core.view',
icon: 'fas-eye',
action: this.openInBrowser.bind(this),
});
}
this.actions = actions;
}
/**
* Default action. Open in browser.
*
* @param siteId Site ID to use.
*/
protected async openInBrowser(siteId?: string): Promise<void> {
const url = <string> this.actionsData?.appurl || this.contextUrl;
if (!url) {
return;
}
const site = await CoreSites.getSite(siteId);
site.openInBrowserWithAutoLogin(url);
}
}
type AddonNotificationsNotification = AddonNotificationsNotificationToRender | AddonNotificationsNotificationData;

View File

@ -18,12 +18,12 @@ import { CoreNavigator } from '@services/navigator';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate';
import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { AddonNotifications, AddonNotificationsProvider } from '../notifications';
import { AddonNotifications } from '../notifications';
import { AddonNotificationsMainMenuHandlerService } from './mainmenu';
import { AddonNotificationsHelper } from '../notifications-helper';
/**
* Handler for non-messaging push notifications clicks.
@ -64,15 +64,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi
* @return Promise resolved when done.
*/
protected async markAsRead(notification: AddonNotificationsNotificationData): Promise<void> {
const notifId = notification.savedmessageid || notification.id;
if (!notifId) {
return;
}
await CoreUtils.ignoreErrors(AddonNotifications.markNotificationRead(notifId, notification.site));
CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, {}, notification.site);
await CoreUtils.ignoreErrors(AddonNotificationsHelper.markNotificationAsRead(notification));
}
/**

View File

@ -18,13 +18,18 @@ import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonMessageOutputDelegate } from '@addons/messageoutput/services/messageoutput-delegate';
import {
AddonNotifications,
AddonNotificationsNotificationMessageFormatted,
AddonNotificationsPreferences,
AddonNotificationsPreferencesComponent,
AddonNotificationsPreferencesNotification,
AddonNotificationsPreferencesNotificationProcessor,
AddonNotificationsPreferencesProcessor,
AddonNotificationsProvider,
} from './notifications';
import { CoreEvents } from '@singletons/events';
import { AddonNotificationsNotificationData } from './handlers/push-click';
import { CoreTimeUtils } from '@services/utils/time';
/**
* Service that provides some helper functions for notifications.
@ -115,6 +120,46 @@ export class AddonNotificationsHelperProvider {
return result;
}
/**
* Mark notification as read, trigger event and invalidate data.
*
* @param notification Notification object.
* @return Promise resolved when done.
*/
async markNotificationAsRead(
notification: AddonNotificationsNotificationMessageFormatted | AddonNotificationsNotificationData,
siteId?: string,
): Promise<boolean> {
if ('read' in notification && (notification.read || notification.timeread > 0)) {
// Already read, don't mark it.
return false;
}
const notifId = 'savedmessageid' in notification ? notification.savedmessageid || notification.id : notification.id;
if (!notifId) {
return false;
}
siteId = 'site' in notification ? notification.site : siteId;
await CoreUtils.ignoreErrors(AddonNotifications.markNotificationRead(notifId, siteId));
const time = CoreTimeUtils.timestamp();
if ('read' in notification) {
notification.read = true;
notification.timeread = time;
}
await CoreUtils.ignoreErrors(AddonNotifications.invalidateNotificationsList());
CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, {
id: notifId,
time,
}, siteId);
return true;
}
}
export const AddonNotificationsHelper = makeSingleton(AddonNotificationsHelperProvider);

View File

@ -24,6 +24,19 @@ import { CoreLogger } from '@singletons/logger';
import { makeSingleton } from '@singletons';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
declare module '@singletons/events' {
/**
* Augment CoreEventsData interface with events specific to this service.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
[AddonNotificationsProvider.READ_CHANGED_EVENT]: AddonNotificationsReadChangedEvent;
}
}
const ROOT_CACHE_KEY = 'mmaNotifications:';
/**
@ -577,3 +590,11 @@ export enum AddonNotificationsGetReadType {
READ = 1,
BOTH = 2,
}
/**
* Event triggered when one or more notifications are read.
*/
export type AddonNotificationsReadChangedEvent = {
id?: number; // Set to the single id notification read. Undefined if multiple.
time: number; // Time of the change.
};

View File

@ -8,13 +8,13 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!filesLoaded || (!showPrivateFiles && !showSiteFiles)"
(ionRefresh)="refreshData($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="filesLoaded" *ngIf="showPrivateFiles || showSiteFiles" class="list-item-limited-width">
<core-loading [hideUntil]="filesLoaded" *ngIf="showPrivateFiles || showSiteFiles">
<!-- Allow selecting the files to see: private or site. -->
<core-combobox [selection]="root" (onChange)="rootChanged($event)" *ngIf="showPrivateFiles && showSiteFiles && !path">
<ion-select-option class="ion-text-wrap" value="my">

View File

@ -8,8 +8,8 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<ion-content class="limited-width">
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap">
<ion-label>
<p>{{ 'addon.storagemanager.courseinfo' | translate }}</p>

View File

@ -8,8 +8,8 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded" class="list-item-limited-width">
<ion-content class="limited-width">
<core-loading [hideUntil]="loaded">
<div class="ion-padding-horizontal ion-text-wrap" *ngIf="spaceUsage">
<h2>{{ 'addon.storagemanager.alldata' | translate }}</h2>
</div>

View File

@ -31,6 +31,7 @@ import { CoreUrlUtils } from '@services/utils/url';
import { CoreConstants } from '@/core/constants';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreDom } from '@singletons/dom';
const MOODLE_VERSION_PREFIX = 'version-';
const MOODLEAPP_VERSION_PREFIX = 'moodleapp-';
@ -90,9 +91,21 @@ export class AppComponent implements OnInit, AfterViewInit {
});
// Listen to scroll to add style when scroll is not 0.
win.addEventListener('ionScroll', ({ detail, target }: CustomEvent<ScrollDetail>) => {
const header = (target as HTMLElement).closest('.ion-page')?.querySelector('ion-header');
header?.classList.toggle('core-header-shadow', detail.scrollTop > 0);
win.addEventListener('ionScroll', async ({ detail, target }: CustomEvent<ScrollDetail>) => {
if ((target as HTMLElement).tagName != 'ION-CONTENT') {
return;
}
const content = (target as HTMLIonContentElement);
const page = content.closest('.ion-page');
if (!page) {
return;
}
page.querySelector<HTMLIonHeaderElement>('ion-header')?.classList.toggle('core-header-shadow', detail.scrollTop > 0);
const scrollElement = await content.getScrollElement();
content.classList.toggle('core-footer-shadow', !CoreDom.scrollIsBottom(scrollElement));
});
// Listen for session expired events.

View File

@ -1,5 +1,3 @@
:host {
core-loading {
--loading-inline-min-height: 60px;
}
core-loading {
--loading-inline-min-height: 60px;
}

View File

@ -1,5 +1,4 @@
<div [class.core-loading-container]="loading || !safeUrl" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}">
<core-loading [hideUntil]="!loading && safeUrl">
<core-navbar-buttons slot="end" prepend *ngIf="initialized && showFullscreenOnToolbar">
<ion-button fill="clear" (click)="toggleFullscreen()"
[attr.aria-label]="(fullscreen ? 'core.disablefullscreen' : 'core.fullscreen') | translate">
@ -17,8 +16,4 @@
class="core-button-as-link core-iframe-help">
{{ 'core.iframehelp' | translate }}
</ion-button>
<span class="core-loading-spinner">
<ion-spinner *ngIf="loading" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</span>
</div>
</core-loading>

Some files were not shown because too many files have changed in this diff Show More