Merge pull request #1739 from crazyserver/MOBILE-2843

Mobile 2843
main
Juan Leyva 2019-01-28 19:07:49 +01:00 committed by GitHub
commit 65a38ebbce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 227 additions and 58 deletions

View File

@ -10,6 +10,10 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="notesLoaded" class="core-loading-center">
<ion-item text-wrap *ngIf="user">
<ion-avatar core-user-avatar [user]="user" [courseId]="courseId" item-start [linkProfile]="false"></ion-avatar>
<h2>{{user.fullname}}</h2>
</ion-item>
<div padding>
<ion-select [(ngModel)]="type" (ngModelChange)="typeChanged()" interface="popover" class="core-button-select">
@ -29,8 +33,8 @@
<ion-list *ngIf="notes && notes.length > 0">
<ion-card *ngFor="let note of notes">
<ion-item text-wrap>
<ion-avatar core-user-avatar [user]="note" item-start></ion-avatar>
<h2>{{note.userfullname}}</h2>
<ion-avatar core-user-avatar [user]="note" [courseId]="courseId" item-start *ngIf="!userId"></ion-avatar>
<h2 *ngIf="!userId">{{note.userfullname}}</h2>
<p *ngIf="!note.offline" item-end>{{note.lastmodified | coreDateDayOrTime}}</p>
<p *ngIf="note.offline" item-end><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
</ion-item>
@ -39,5 +43,11 @@
</ion-item>
</ion-card>
</ion-list>
</core-loading>
<ion-fab core-fab bottom end *ngIf="userId && notesLoaded">
<button ion-fab (click)="addNote($event)" [attr.aria-label]="'addon.notes.addnewnote' |translate">
<ion-icon name="add"></ion-icon>
</button>
</ion-fab>
</ion-content>

View File

@ -13,11 +13,12 @@
// limitations under the License.
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Content } from 'ionic-angular';
import { Content, ModalController } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonNotesProvider } from '../../providers/notes';
import { AddonNotesSyncProvider } from '../../providers/notes-sync';
@ -30,6 +31,7 @@ import { AddonNotesSyncProvider } from '../../providers/notes-sync';
})
export class AddonNotesListComponent implements OnInit, OnDestroy {
@Input() courseId: number;
@Input() userId?: number;
@ViewChild(Content) content: Content;
@ -41,10 +43,12 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
notes: any[];
hasOffline = false;
notesLoaded = false;
user: any;
constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider,
sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider) {
sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, private modalCtrl: ModalController,
private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider,
private userProvider: CoreUserProvider) {
// Refresh data if notes are synchronized automatically.
this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => {
if (data.courseId == this.courseId) {
@ -67,7 +71,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
*/
ngOnInit(): void {
this.fetchNotes(true).then(() => {
this.notesProvider.logView(this.courseId).catch(() => {
this.notesProvider.logView(this.courseId, this.userId).catch(() => {
// Ignore errors.
});
});
@ -86,14 +90,23 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
return promise.catch(() => {
// Ignore errors.
}).then(() => {
return this.notesProvider.getNotes(this.courseId).then((notes) => {
return this.notesProvider.getNotes(this.courseId, this.userId).then((notes) => {
notes = notes[this.type + 'notes'] || [];
this.hasOffline = notes.some((note) => note.offline);
if (this.userId) {
this.notes = notes;
// Get the user profile to retrieve the user image.
return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => {
this.user = user;
});
} else {
return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => {
this.notes = notes;
});
}
});
}).catch((message) => {
this.domUtils.showErrorModal(message);
@ -113,7 +126,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
refreshNotes(showErrors: boolean, refresher?: any): void {
this.refreshIcon = 'spinner';
this.syncIcon = 'spinner';
this.notesProvider.invalidateNotes(this.courseId).finally(() => {
this.notesProvider.invalidateNotes(this.courseId, this.userId).finally(() => {
this.fetchNotes(true, showErrors).finally(() => {
if (refresher) {
refresher.complete();
@ -130,12 +143,36 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
this.refreshIcon = 'spinner';
this.syncIcon = 'spinner';
this.fetchNotes(true).then(() => {
this.notesProvider.logView(this.courseId).catch(() => {
this.notesProvider.logView(this.courseId, this.userId).catch(() => {
// Ignore errors.
});
});
}
/**
* Add a new Note to user and course.
* @param {Event} e Event.
*/
addNote(e: Event): void {
e.preventDefault();
e.stopPropagation();
const modal = this.modalCtrl.create('AddonNotesAddPage', { userId: this.userId, courseId: this.courseId, type: this.type });
modal.onDidDismiss((data) => {
if (data && data.sent && data.type) {
if (data.type != this.type) {
this.type = data.type;
this.notesLoaded = false;
}
this.refreshNotes(true);
} else if (data && data.type && data.type != this.type) {
this.type = data.type;
this.typeChanged();
}
});
modal.present();
}
/**
* Tries to synchronize course notes.
*

View File

@ -44,8 +44,7 @@ export const ADDON_NOTES_PROVIDERS: any[] = [
AddonNotesSyncProvider,
AddonNotesCourseOptionHandler,
AddonNotesSyncCronHandler,
AddonNotesUserHandler
]
AddonNotesUserHandler ]
})
export class AddonNotesModule {
constructor(courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonNotesCourseOptionHandler,

View File

@ -8,11 +8,11 @@
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-content>
<form name="itemEdit" (ngSubmit)="addNote($event)">
<ion-item>
<ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label>
<ion-select [(ngModel)]="publishState" name="publishState" interface="popover">
<ion-select [(ngModel)]="type" name="publishState" interface="popover">
<ion-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-option>
<ion-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-option>
<ion-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-option>
@ -21,8 +21,10 @@
<ion-item>
<ion-textarea placeholder="{{ 'addon.notes.note' | translate }}" rows="5" [(ngModel)]="text" name="text" required="required"></ion-textarea>
</ion-item>
<button ion-button block margin-vertical type="submit" [disabled]="processing || text.length < 2">
<div padding>
<button ion-button block type="submit" [disabled]="processing || text.length < 2">
{{ 'addon.notes.addnewnote' | translate }}
</button>
</div>
</form>
</ion-content>

View File

@ -29,7 +29,7 @@ import { AddonNotesProvider } from '../../providers/notes';
export class AddonNotesAddPage {
userId: number;
courseId: number;
publishState = 'personal';
type = 'personal';
text = '';
processing = false;
@ -37,6 +37,7 @@ export class AddonNotesAddPage {
private domUtils: CoreDomUtilsProvider, private notesProvider: AddonNotesProvider) {
this.userId = params.get('userId');
this.courseId = params.get('courseId');
this.type = params.get('type') || 'personal';
}
/**
@ -52,10 +53,9 @@ export class AddonNotesAddPage {
const loadingModal = this.domUtils.showModalLoading('core.sending', true);
// Freeze the add note button.
this.processing = true;
this.notesProvider.addNote(this.userId, this.courseId, this.publishState, this.text).then((sent) => {
this.viewCtrl.dismiss().finally(() => {
const message = sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline';
this.domUtils.showAlertTranslated('core.success', message);
this.notesProvider.addNote(this.userId, this.courseId, this.type, this.text).then((sent) => {
this.viewCtrl.dismiss({type: this.type, sent: true}).finally(() => {
this.domUtils.showToast(sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline', true, 3000);
});
}).catch((error) => {
this.domUtils.showErrorModal(error);
@ -69,6 +69,6 @@ export class AddonNotesAddPage {
* Close modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
this.viewCtrl.dismiss({type: this.type});
}
}

View File

@ -0,0 +1,7 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.notes.notes' | translate }}</ion-title>
<ion-buttons end></ion-buttons>
</ion-navbar>
</ion-header>
<addon-notes-list class="core-avoid-header" [courseId]="courseId" [userId]="userId"></addon-notes-list>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonNotesListPage } from './list';
import { AddonNotesComponentsModule } from '../../components/components.module';
@NgModule({
declarations: [
AddonNotesListPage
],
imports: [
CoreDirectivesModule,
AddonNotesComponentsModule,
IonicPageModule.forChild(AddonNotesListPage),
TranslateModule.forChild()
]
})
export class AddonNotesListPageModule {}

View File

@ -0,0 +1,34 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
/**
* Page that displays a list of notes.
*/
@IonicPage({ segment: 'addon-notes-list-page' })
@Component({
selector: 'page-addon-notes-list-page',
templateUrl: 'list.html',
})
export class AddonNotesListPage {
userId: number;
courseId: number;
constructor(params: NavParams) {
this.userId = params.get('userId');
this.courseId = params.get('courseId');
}
}

View File

@ -79,6 +79,6 @@ export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler {
* @return {Promise<any>} Promise resolved when done.
*/
prefetch(course: any): Promise<any> {
return this.notesProvider.getNotes(course.id, true);
return this.notesProvider.getNotes(course.id, undefined, true);
}
}

View File

@ -118,6 +118,24 @@ export class AddonNotesOfflineProvider {
});
}
/**
* Get offline notes for a certain course and user.
*
* @param {number} courseId Course ID.
* @param {number} [userId] User ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with notes.
*/
getNotesForCourseAndUser(courseId: number, userId?: number, siteId?: string): Promise<any[]> {
if (!userId) {
return this.getNotesForCourse(courseId, siteId);
}
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_TABLE, {userid: userId, courseid: courseId});
});
}
/**
* Get offline notes for a certain course.
*

View File

@ -154,8 +154,8 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
});
// Fetch the notes from server to be sure they're up to date.
return this.notesProvider.invalidateNotes(courseId, siteId).then(() => {
return this.notesProvider.getNotes(courseId, false, true, siteId);
return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => {
return this.notesProvider.getNotes(courseId, undefined, false, true, siteId);
}).catch(() => {
// Ignore errors.
});

View File

@ -104,7 +104,7 @@ export class AddonNotesProvider {
}
// A note was added, invalidate the course notes.
return this.invalidateNotes(courseId, siteId).catch(() => {
return this.invalidateNotes(courseId, undefined, siteId).catch(() => {
// Ignore errors.
});
});
@ -184,37 +184,54 @@ export class AddonNotesProvider {
* @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false or rejected otherwise.
*/
isPluginViewNotesEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> {
return this.utils.promiseWorks(this.getNotes(courseId, false, true, siteId));
return this.utils.promiseWorks(this.getNotes(courseId, undefined, false, true, siteId));
}
/**
* Get prefix cache key for course notes.
*
* @param {number} courseId ID of the course to get the notes from.
* @return {string} Cache key.
*/
getNotesPrefixCacheKey(courseId: number): string {
return this.ROOT_CACHE_KEY + 'notes:' + courseId + ':';
}
/**
* Get the cache key for the get notes call.
*
* @param {number} courseId ID of the course to get the notes from.
* @param {number} [userId] ID of the user to get the notes from if requested.
* @return {string} Cache key.
*/
getNotesCacheKey(courseId: number): string {
return this.ROOT_CACHE_KEY + 'notes:' + courseId;
getNotesCacheKey(courseId: number, userId?: number): string {
return this.getNotesPrefixCacheKey(courseId) + (userId ? userId : '');
}
/**
* Get users notes for a certain site, course and personal notes.
*
* @param {number} courseId ID of the course to get the notes from.
* @param {number} [userId] ID of the user to get the notes from if requested.
* @param {boolean} [ignoreCache] True when we should not get the value from the cache.
* @param {boolean} [onlyOnline] True to return only online notes, false to return both online and offline.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise to be resolved when the notes are retrieved.
*/
getNotes(courseId: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string): Promise<any> {
getNotes(courseId: number, userId?: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string): Promise<any> {
this.logger.debug('Get notes for course ' + courseId);
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
courseid: courseId
};
if (userId) {
data['userid'] = userId;
}
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getNotesCacheKey(courseId)
cacheKey: this.getNotesCacheKey(courseId, userId)
};
if (ignoreCache) {
@ -228,7 +245,7 @@ export class AddonNotesProvider {
}
// Get offline notes and add them to the list.
return this.notesOffline.getNotesForCourse(courseId, siteId).then((offlineNotes) => {
return this.notesOffline.getNotesForCourseAndUser(courseId, userId, siteId).then((offlineNotes) => {
offlineNotes.forEach((note) => {
const fieldName = note.publishstate + 'notes';
if (!notes[fieldName]) {
@ -272,12 +289,17 @@ export class AddonNotesProvider {
* Invalidate get notes WS call.
*
* @param {number} courseId Course ID.
* @param {number} [userId] User ID if needed.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when data is invalidated.
*/
invalidateNotes(courseId: number, siteId?: string): Promise<any> {
invalidateNotes(courseId: number, userId?: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId));
if (userId) {
return site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId, userId));
}
return site.invalidateWsCacheForKeyStartingWith(this.getNotesPrefixCacheKey(courseId));
});
}
@ -285,14 +307,15 @@ export class AddonNotesProvider {
* Report notes as being viewed.
*
* @param {number} courseId ID of the course.
* @param {number} [userId] User ID if needed.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logView(courseId: number, siteId?: string): Promise<any> {
logView(courseId: number, userId?: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
courseid: courseId,
userid: 0
userid: userId || 0
};
return site.write('core_notes_view_notes', params);

View File

@ -13,10 +13,10 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { ModalController } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreUserProvider } from '@core/user/providers/user';
import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreSitesProvider } from '@providers/sites';
import { AddonNotesProvider } from './notes';
@ -25,30 +25,30 @@ import { AddonNotesProvider } from './notes';
*/
@Injectable()
export class AddonNotesUserHandler implements CoreUserProfileHandler {
name = 'AddonNotes:addNote';
priority = 200;
type = CoreUserDelegate.TYPE_COMMUNICATION;
addNoteEnabledCache = {};
name = 'AddonNotes:notes';
priority = 100;
type = CoreUserDelegate.TYPE_NEW_PAGE;
noteEnabledCache = {};
constructor(private modalCtrl: ModalController, private sitesProvider: CoreSitesProvider,
constructor(private linkHelper: CoreContentLinksHelperProvider, private sitesProvider: CoreSitesProvider,
private notesProvider: AddonNotesProvider, eventsProvider: CoreEventsProvider) {
eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearAddNoteCache.bind(this));
eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearNoteCache.bind(this));
eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
this.clearAddNoteCache(data.courseId);
this.clearNoteCache(data.courseId);
});
}
/**
* Clear add note cache.
* Clear note cache.
* If a courseId is specified, it will only delete the entry for that course.
*
* @param {number} [courseId] Course ID.
*/
private clearAddNoteCache(courseId?: number): void {
private clearNoteCache(courseId?: number): void {
if (courseId) {
delete this.addNoteEnabledCache[courseId];
delete this.noteEnabledCache[courseId];
} else {
this.addNoteEnabledCache = {};
this.noteEnabledCache = {};
}
}
@ -75,12 +75,12 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler {
return Promise.resolve(false);
}
if (typeof this.addNoteEnabledCache[courseId] != 'undefined') {
return this.addNoteEnabledCache[courseId];
if (typeof this.noteEnabledCache[courseId] != 'undefined') {
return this.noteEnabledCache[courseId];
}
return this.notesProvider.isPluginAddNoteEnabledForCourse(courseId).then((enabled) => {
this.addNoteEnabledCache[courseId] = enabled;
this.noteEnabledCache[courseId] = enabled;
return enabled;
});
@ -94,13 +94,13 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler {
getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData {
return {
icon: 'list',
title: 'addon.notes.addnewnote',
title: 'addon.notes.notes',
class: 'addon-notes-handler',
action: (event, navCtrl, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
const modal = this.modalCtrl.create('AddonNotesAddPage', { userId: user.id, courseId });
modal.present();
// Always use redirect to make it the new history root (to avoid "loops" in history).
this.linkHelper.goInSite(navCtrl, 'AddonNotesListPage', { userId: user.id, courseId: courseId });
}
};
}

View File

@ -73,6 +73,10 @@ ion-app.app-root {
}
}
.has-refresher > .scroll-content {
border-top: 0 !important;
}
// Define an alternative way to set a heading in an item without using a heading tag.
// This is done for accessibility reasons when a heading is semantically incorrect.
.item .item-heading {

View File

@ -29,6 +29,7 @@ ion-app.app-root core-empty-box {
.icon {
font-size: 120px;
width: auto;
}
img {
height: 125px;

View File

@ -758,10 +758,11 @@ export class CoreCourseHelperProvider {
*
* @param {any[]} courses Courses array to get info from.
* @param {any} prefetch Prefetch information.
* @param {number} [minCourses=2] Min course to show icon.
* @return {Promise<any>} Resolved with the prefetch information updated when done.
*/
initPrefetchCoursesIcons(courses: any[], prefetch: any): Promise<any> {
if (!courses || courses.length < 2) {
initPrefetchCoursesIcons(courses: any[], prefetch: any, minCourses: number = 2): Promise<any> {
if (!courses || courses.length < minCourses) {
// Not enough courses.
prefetch.icon = '';