Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile 3881 #2990

Merged
merged 2 commits into from Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -28,7 +28,7 @@
}

.userpicture {
vertical-align: text-bottom;
border-radius: 50%;
}
}

Expand Down
4 changes: 0 additions & 4 deletions src/addons/block/timeline/components/components.module.ts
Expand Up @@ -15,8 +15,6 @@
import { NgModule } from '@angular/core';

import { CoreSharedModule } from '@/core/shared.module';
import { CoreCoursesComponentsModule } from '@features/courses/components/components.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';

import { AddonBlockTimelineComponent } from './timeline/timeline';
import { AddonBlockTimelineEventsComponent } from './events/events';
Expand All @@ -28,8 +26,6 @@ import { AddonBlockTimelineEventsComponent } from './events/events';
],
imports: [
CoreSharedModule,
CoreCoursesComponentsModule,
CoreCourseComponentsModule,
],
exports: [
AddonBlockTimelineComponent,
Expand Down
@@ -1,50 +1,61 @@
<ion-item lines="none" *ngIf="course">
<ion-label class="ion-text-wrap">
<h3>
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text>
</h3>
</ion-label>
</ion-item>
<ion-item-group *ngFor="let dayEvents of filteredEvents">
<ion-item-divider [color]="dayEvents.color">
<ion-label><h3>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h3></ion-label>
</ion-item-divider>
<ion-item lines="none">
<ion-label>
<h4 [class.core-bold]="!course">{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h4>
</ion-label>
</ion-item>
<ng-container *ngFor="let event of dayEvents.events">
<ion-item class="ion-text-wrap core-course-module-handler item-media" detail="false" (click)="action($event, event.url)"
[attr.aria-label]="event.name" button>
<core-mod-icon *ngIf="event.iconUrl" slot="start" [modicon]="event.iconUrl" [componentId]="event.instance"
[modname]="event.modulename">
</core-mod-icon>
<ion-item class="addon-block-timeline-activity" detail="false" (click)="action($event, event.url)" [attr.aria-label]="event.name"
button lines="full">
<ion-label>
<p class="item-heading">
<core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id"
[courseId]="event.course && event.course.id">
</core-format-text>
</p>
<p *ngIf="showCourse && event.course">
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
[contextInstanceId]="event.course.id">
</core-format-text>
</p>

<ion-button fill="clear" class="ion-hide-md-up ion-text-wrap" (click)="action($event, event.action.url)"
[title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
{{event.action.name}}
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}}
</ion-badge>
</ion-button>
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding">
<ion-col class="addon-block-timeline-activity-main ion-no-padding">
<ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding">
<ion-col class="addon-block-timeline-activity-time ion-no-padding">
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
<core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance"
[modname]="event.modulename">
</core-mod-icon>
</ion-col>
<ion-col class="addon-block-timeline-activity-name ion-no-padding">
<p class="item-heading">
<core-format-text [text]="event.activityname || event.name" contextLevel="module"
[contextInstanceId]="event.id" [courseId]="event.course && event.course.id">
</core-format-text>
<ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }}
</ion-badge>
</p>
<p *ngIf="(showCourse && event.course) || event.activitystr">
<span *ngIf="showCourse && event.course">
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
[contextInstanceId]="event.course.id">
</core-format-text> ·
</span>
<core-format-text [text]="event.activitystr" contextLevel="module" [contextInstanceId]="event.id">
</core-format-text>
</p>
</ion-col>
</ion-row>
</ion-col>
<ion-col class="addon-block-timeline-activity-action ion-no-padding">
<ion-button fill="clear" (click)="action($event, event.action.url)" [title]="event.action.name"
[disabled]="!event.action.actionable" *ngIf="event.action">
{{event.action.name}}
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">
{{event.action.itemcount}}
</ion-badge>
</ion-button>
</ion-col>
</ion-row>
</ion-label>

<div slot="end" class="events-info">
<div>
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
</div>
<ion-button
class="ion-hide-md-down"
fill="clear"
(click)="action($event, event.action.url)"
[title]="event.action.name"
[disabled]="!event.action.actionable" *ngIf="event.action"
>
{{event.action.name}}
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">
{{event.action.itemcount}}
</ion-badge>
</ion-button>
</div>
</ion-item>
</ng-container>
</ion-item-group>
Expand All @@ -57,6 +68,10 @@
<ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</div>

<core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate"
inline="true">
</core-empty-box>
<ion-item lines="none" *ngIf="empty && course">
<ion-label class="ion-text-wrap">
<p>{{'addon.block_timeline.noevents' | translate}}</p>
</ion-label>
</ion-item>
<core-empty-box *ngIf="empty && !course" image="assets/img/icons/activities.svg" inline="true"
[message]="'addon.block_timeline.noevents' | translate"></core-empty-box>
45 changes: 40 additions & 5 deletions src/addons/block/timeline/components/events/events.scss
@@ -1,6 +1,41 @@
.events-info {
display: flex;
flex-direction: column;
text-align: end;
padding: 10px 0;
@import "~theme/globals";

h3 {
font-weight: bold;
font-size: 18px;
}

h4 {
font-size: 15px;
}

h4.core-bold {
font-weight: bold;
}

.addon-block-timeline-activity ion-badge {
@include margin-horizontal(0.25rem, 0.5rem);
}

.addon-block-timeline-activity core-mod-icon {
--margin-end: 0.5rem;
}

.addon-block-timeline-activity-time,
.addon-block-timeline-activity-action {
flex-grow: 0;
}

.addon-block-timeline-activity-main,
.addon-block-timeline-activity-name {
flex-grow: 1;
p {
overflow: hidden;
text-overflow: ellipsis;
}
}

.addon-block-timeline-activity-name {
flex-grow: 1;
overflow: hidden;
}
46 changes: 23 additions & 23 deletions src/addons/block/timeline/components/events/events.ts
Expand Up @@ -17,11 +17,11 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourse } from '@features/course/services/course';
import moment from 'moment';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';

/**
* Directive to render a list of events in course overview.
Expand All @@ -34,50 +34,51 @@ import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
export class AddonBlockTimelineEventsComponent implements OnChanges {

@Input() events: AddonBlockTimelineEvent[] = []; // The events to render.
@Input() showCourse?: boolean | string; // Whether to show the course name.
@Input() course?: CoreEnrolledCourseDataWithOptions; // Whether to show the course name.
@Input() from = 0; // Number of days from today to offset the events.
@Input() to?: number; // Number of days from today to limit the events to. If not defined, no limit.
@Input() canLoadMore?: boolean; // Whether more events can be loaded.
@Output() loadMore: EventEmitter<void>; // Notify that more events should be loaded.
@Input() canLoadMore = false; // Whether more events can be loaded.
@Output() loadMore = new EventEmitter(); // Notify that more events should be loaded.

showCourse = false; // Whether to show the course name.
empty = true;
loadingMore = false;
filteredEvents: AddonBlockTimelineEventFilteredEvent[] = [];

constructor() {
this.loadMore = new EventEmitter();
}

/**
* Detect changes on input properties.
* @inheritdoc
*/
async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise<void> {
this.showCourse = CoreUtils.isTrueOrOne(this.showCourse);
this.showCourse = !this.course;

if (changes.events || changes.from || changes.to) {
if (this.events && this.events.length > 0) {
const filteredEvents = await this.filterEventsByTime(this.from, this.to);
this.empty = !filteredEvents || filteredEvents.length <= 0;

const now = CoreTimeUtils.timestamp();

const eventsByDay: Record<number, AddonCalendarEvent[]> = {};
filteredEvents.forEach((event) => {
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);

// Already calculated on 4.0 onwards but this will be live.
event.overdue = event.timesort < now;

if (eventsByDay[dayTimestamp]) {
eventsByDay[dayTimestamp].push(event);
} else {
eventsByDay[dayTimestamp] = [event];
}
});

const todaysMidnight = CoreTimeUtils.getMidnightForTimestamp();
this.filteredEvents = [];
Object.keys(eventsByDay).forEach((key) => {
this.filteredEvents = Object.keys(eventsByDay).map((key) => {
const dayTimestamp = parseInt(key);
this.filteredEvents.push({
color: dayTimestamp < todaysMidnight ? 'danger' : 'light',

return {
dayTimestamp,
events: eventsByDay[dayTimestamp],
});
};
});
} else {
this.empty = true;
Expand All @@ -94,7 +95,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
*/
protected async filterEventsByTime(start: number, end?: number): Promise<AddonBlockTimelineEvent[]> {
start = moment().add(start, 'days').startOf('day').unix();
end = typeof end != 'undefined' ? moment().add(end, 'days').startOf('day').unix() : end;
end = end !== undefined ? moment().add(end, 'days').startOf('day').unix() : end;

return await Promise.all(this.events.filter((event) => {
if (end) {
Expand Down Expand Up @@ -122,12 +123,12 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
/**
* Action clicked.
*
* @param e Click event.
* @param event Click event.
* @param url Url of the action.
*/
async action(e: Event, url: string): Promise<void> {
e.preventDefault();
e.stopPropagation();
async action(event: Event, url: string): Promise<void> {
event.preventDefault();
event.stopPropagation();

// Fix URL format.
url = CoreTextUtils.decodeHTMLEntities(url);
Expand All @@ -137,7 +138,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
try {
const treated = await CoreContentLinksHelper.handleLink(url);
if (!treated) {
return CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url);
return CoreSites.getRequiredCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
}
} finally {
modal.dismiss();
Expand All @@ -154,5 +155,4 @@ type AddonBlockTimelineEvent = AddonCalendarEvent & {
type AddonBlockTimelineEventFilteredEvent = {
events: AddonBlockTimelineEvent[];
dayTimestamp: number;
color: string;
};
@@ -1,5 +1,7 @@
<ion-item-divider sticky="true">
<ion-label><h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2></ion-label>
<ion-label>
<h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2>
</ion-label>
<core-context-menu slot="end">
<core-context-menu-item *ngIf="loaded" [priority]="900" [content]="'addon.block_timeline.sortbydates' | translate"
(action)="switchSort('sortbydates')" [iconAction]="sort == 'sortbydates' ? 'far-dot-circle' : 'far-circle'">
Expand All @@ -18,7 +20,7 @@
<ion-select-option class="ion-text-wrap" value="overdue">
{{ 'addon.block_timeline.overdue' | translate }}
</ion-select-option>
<ion-select-option class="ion-text-wrap" disabled value="disabled">
<ion-select-option class="ion-text-wrap core-select-option-title" disabled value="disabled">
{{ 'addon.block_timeline.duedate' | translate }}
</ion-select-option>
<ion-select-option class="ion-text-wrap" value="next7days">
Expand All @@ -36,21 +38,14 @@
</core-combobox>
</div>
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" [fullscreen]="false">
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore"
(loadMore)="loadMoreTimeline()" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
<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-padding">
<ion-grid class="ion-no-padding">
<ion-row class="ion-no-padding">
<ion-col *ngFor="let course of timelineCourses.courses" class="ion-no-padding" size="12" size-md="6">
<core-courses-course-progress [course]="course">
<addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore"
(loadMore)="loadMoreCourse(course)" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
</core-courses-course-progress>
</ion-col>
</ion-row>
</ion-grid>
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" [fullscreen]="false" class="safe-area-page">
<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>
</ng-container>
<core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
[message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box>
</core-loading>
Expand Down