MOBILE-2978 forum: Private reply

main
Albert Gasset 2019-04-09 15:35:24 +02:00
parent 265f4a40f4
commit 6eab6068f4
10 changed files with 112 additions and 10 deletions

View File

@ -496,7 +496,9 @@
"addon.mod_forum.modulenameplural": "forum", "addon.mod_forum.modulenameplural": "forum",
"addon.mod_forum.numdiscussions": "local_moodlemobileapp", "addon.mod_forum.numdiscussions": "local_moodlemobileapp",
"addon.mod_forum.numreplies": "local_moodlemobileapp", "addon.mod_forum.numreplies": "local_moodlemobileapp",
"addon.mod_forum.postisprivatereply": "forum",
"addon.mod_forum.posttoforum": "forum", "addon.mod_forum.posttoforum": "forum",
"addon.mod_forum.privatereply": "forum",
"addon.mod_forum.re": "forum", "addon.mod_forum.re": "forum",
"addon.mod_forum.refreshdiscussions": "local_moodlemobileapp", "addon.mod_forum.refreshdiscussions": "local_moodlemobileapp",
"addon.mod_forum.refreshposts": "local_moodlemobileapp", "addon.mod_forum.refreshposts": "local_moodlemobileapp",

View File

@ -13,6 +13,9 @@
</ion-item> </ion-item>
</ion-card-header> </ion-card-header>
<ion-card-content padding-top> <ion-card-content padding-top>
<div padding-bottom *ngIf="post.isprivatereply">
<ion-note>{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note>
</div>
<core-format-text [component]="component" [componentId]="componentId" [text]="post.message"></core-format-text> <core-format-text [component]="component" [componentId]="componentId" [text]="post.message"></core-format-text>
<div no-lines> <div no-lines>
<ng-container *ngFor="let attachment of post.attachments"> <ng-container *ngFor="let attachment of post.attachments">
@ -25,7 +28,7 @@
</ion-card-content> </ion-card-content>
<core-rating-rate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" [userId]="post.userid" (onUpdate)="ratingUpdated()"></core-rating-rate> <core-rating-rate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" [userId]="post.userid" (onUpdate)="ratingUpdated()"></core-rating-rate>
<core-rating-aggregate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"></core-rating-aggregate> <core-rating-aggregate *ngIf="forum && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"></core-rating-aggregate>
<ion-item no-padding text-end *ngIf="post.id && post.canreply" class="addon-forum-reply-button"> <ion-item no-padding text-end *ngIf="post.id && post.canreply && !post.isprivatereply" class="addon-forum-reply-button">
<button ion-button icon-left clear small (click)="showReply()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id"> <button ion-button icon-left clear small (click)="showReply()" [attr.aria-controls]="'addon-forum-reply-edit-form-' + uniqueId" [attr.aria-expanded]="replyData.replyingTo === post.id">
<ion-icon name="undo"></ion-icon> {{ 'addon.mod_forum.reply' | translate }} <ion-icon name="undo"></ion-icon> {{ 'addon.mod_forum.reply' | translate }}
</button> </button>
@ -45,6 +48,10 @@
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId"></core-rich-text-editor>
</ion-item> </ion-item>
<core-attachments *ngIf="forum.id && forum.maxattachments > 0" [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments> <core-attachments *ngIf="forum.id && forum.maxattachments > 0" [files]="replyData.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments>
<ion-item text-wrap *ngIf="accessInfo.canpostprivatereply">
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>
<ion-checkbox item-end [(ngModel)]="replyData.isprivatereply"></ion-checkbox>
</ion-item>
<ion-grid> <ion-grid>
<ion-row> <ion-row>
<ion-col> <ion-col>

View File

@ -44,6 +44,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
@Input() originalData: any; // Object with the original post data. Usually shared between posts. @Input() originalData: any; // Object with the original post data. Usually shared between posts.
@Input() trackPosts: boolean; // True if post is being tracked. @Input() trackPosts: boolean; // True if post is being tracked.
@Input() forum: any; // The forum the post belongs to. Required for attachments and offline posts. @Input() forum: any; // The forum the post belongs to. Required for attachments and offline posts.
@Input() accessInfo: any; // Forum access information.
@Input() defaultSubject: string; // Default subject to set to new posts. @Input() defaultSubject: string; // Default subject to set to new posts.
@Input() ratingInfo?: CoreRatingInfo; // Rating info item. @Input() ratingInfo?: CoreRatingInfo; // Rating info item.
@Output() onPostChange: EventEmitter<void>; // Event emitted when a reply is posted or modified. @Output() onPostChange: EventEmitter<void>; // Event emitted when a reply is posted or modified.
@ -95,9 +96,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
* @param {boolean} [isEditing] True it's an offline reply beeing edited, false otherwise. * @param {boolean} [isEditing] True it's an offline reply beeing edited, false otherwise.
* @param {string} [subject] Subject of the reply. * @param {string} [subject] Subject of the reply.
* @param {string} [message] Message of the reply. * @param {string} [message] Message of the reply.
* @param {boolean} [isPrivate] True if it's private reply.
* @param {any[]} [files] Reply attachments. * @param {any[]} [files] Reply attachments.
*/ */
protected setReplyData(replyingTo?: number, isEditing?: boolean, subject?: string, message?: string, files?: any[]): void { protected setReplyData(replyingTo?: number, isEditing?: boolean, subject?: string, message?: string, files?: any[],
isPrivate?: boolean): void {
// Delete the local files from the tmp folder if any. // Delete the local files from the tmp folder if any.
this.uploaderProvider.clearTmpFiles(this.replyData.files); this.uploaderProvider.clearTmpFiles(this.replyData.files);
@ -106,6 +109,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
this.replyData.subject = subject || this.defaultSubject || ''; this.replyData.subject = subject || this.defaultSubject || '';
this.replyData.message = message || null; this.replyData.message = message || null;
this.replyData.files = files || []; this.replyData.files = files || [];
this.replyData.isprivatereply = !!isPrivate;
// Update rich text editor. // Update rich text editor.
this.messageControl.setValue(this.replyData.message); this.messageControl.setValue(this.replyData.message);
@ -114,6 +118,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
this.originalData.subject = this.replyData.subject; this.originalData.subject = this.replyData.subject;
this.originalData.message = this.replyData.message; this.originalData.message = this.replyData.message;
this.originalData.files = this.replyData.files.slice(); this.originalData.files = this.replyData.files.slice();
this.originalData.isprivatereply = this.replyData.isprivatereply;
} }
/** /**
@ -163,7 +168,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
this.syncId = this.forumSync.getDiscussionSyncId(this.discussionId); this.syncId = this.forumSync.getDiscussionSyncId(this.discussionId);
this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId); this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId);
this.setReplyData(this.post.parent, true, this.post.subject, this.post.message, this.post.attachments); this.setReplyData(this.post.parent, true, this.post.subject, this.post.message, this.post.attachments,
this.post.isprivatereply);
}).catch(() => { }).catch(() => {
// Cancelled. // Cancelled.
}); });
@ -206,6 +212,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
// Add some HTML to the message if needed. // Add some HTML to the message if needed.
message = this.textUtils.formatHtmlLines(message); message = this.textUtils.formatHtmlLines(message);
// Set private option if checked.
if (this.replyData.isprivatereply) {
options.private = true;
}
// Upload attachments first if any. // Upload attachments first if any.
if (files.length) { if (files.length) {
promise = this.forumHelper.uploadOrStoreReplyFiles(this.forum.id, replyingTo, files, false).catch((error) => { promise = this.forumHelper.uploadOrStoreReplyFiles(this.forum.id, replyingTo, files, false).catch((error) => {

View File

@ -24,7 +24,9 @@
"modulenameplural": "Forums", "modulenameplural": "Forums",
"numdiscussions": "{{numdiscussions}} discussions", "numdiscussions": "{{numdiscussions}} discussions",
"numreplies": "{{numreplies}} replies", "numreplies": "{{numreplies}} replies",
"postisprivatereply": "This post was made privately and is not visible to all users.",
"posttoforum": "Post to forum", "posttoforum": "Post to forum",
"privatereply": "Reply privately",
"re": "Re:", "re": "Re:",
"refreshdiscussions": "Refresh discussions", "refreshdiscussions": "Refresh discussions",
"refreshposts": "Refresh posts", "refreshposts": "Refresh posts",

View File

@ -31,13 +31,13 @@
</ion-card> </ion-card>
<ion-card *ngIf="discussion" margin-bottom class="highlight"> <ion-card *ngIf="discussion" margin-bottom class="highlight">
<addon-mod-forum-post [post]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [defaultSubject]="defaultSubject" [forum]="forum" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post> <addon-mod-forum-post [post]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [defaultSubject]="defaultSubject" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post>
</ion-card> </ion-card>
<ion-card *ngIf="sort != 'nested'"> <ion-card *ngIf="sort != 'nested'">
<ng-container *ngFor="let post of posts; first as first"> <ng-container *ngFor="let post of posts; first as first">
<ion-item-divider *ngIf="!first"></ion-item-divider> <ion-item-divider *ngIf="!first"></ion-item-divider>
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [defaultSubject]="defaultSubject" [forum]="forum" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post> <addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [defaultSubject]="defaultSubject" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post>
</ng-container> </ng-container>
</ion-card> </ion-card>
@ -49,7 +49,7 @@
<ng-template #nestedPosts let-post="post"> <ng-template #nestedPosts let-post="post">
<ion-card> <ion-card>
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [defaultSubject]="defaultSubject" [forum]="forum" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post> <addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [defaultSubject]="defaultSubject" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post>
</ion-card> </ion-card>
<div padding-left *ngIf="post.children.length && post.children[0].subject"> <div padding-left *ngIf="post.children.length && post.children[0].subject">
<ng-container *ngFor="let child of post.children"> <ng-container *ngFor="let child of post.children">

View File

@ -47,6 +47,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
courseId: number; courseId: number;
discussionId: number; discussionId: number;
forum: any; forum: any;
accessInfo: any;
discussion: any; discussion: any;
posts: any[]; posts: any[];
discussionLoaded = false; discussionLoaded = false;
@ -63,11 +64,13 @@ export class AddonModForumDiscussionPage implements OnDestroy {
subject: '', subject: '',
message: null, // Null means empty or just white space. message: null, // Null means empty or just white space.
files: [], files: [],
isprivatereply: false,
}; };
originalData = { originalData = {
subject: null, // Null means original data is not set. subject: null, // Null means original data is not set.
message: null, // Null means empty or just white space. message: null, // Null means empty or just white space.
files: [], files: [],
isprivatereply: false,
}; };
refreshIcon = 'spinner'; refreshIcon = 'spinner';
syncIcon = 'spinner'; syncIcon = 'spinner';
@ -318,9 +321,14 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.forumId = forum.id; this.forumId = forum.id;
this.cmId = forum.cmid; this.cmId = forum.cmid;
this.forum = forum; this.forum = forum;
}).then(() => {
return this.forumProvider.getAccessInformation(this.forum.id).then((accessInfo) => {
this.accessInfo = accessInfo;
});
}).catch(() => { }).catch(() => {
// Ignore errors. // Ignore errors.
this.forum = {}; this.forum = {};
this.accessInfo = {};
}); });
}).then(() => { }).then(() => {
return this.ratingOffline.hasRatings('mod_forum', 'post', 'module', this.cmId, this.discussionId).then((hasRatings) => { return this.ratingOffline.hasRatings('mod_forum', 'post', 'module', this.cmId, this.discussionId).then((hasRatings) => {
@ -420,7 +428,12 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.refreshIcon = 'spinner'; this.refreshIcon = 'spinner';
this.syncIcon = 'spinner'; this.syncIcon = 'spinner';
return this.forumProvider.invalidateDiscussionPosts(this.discussionId).catch(() => { const promises = [
this.forumProvider.invalidateDiscussionPosts(this.discussionId),
this.forumProvider.invalidateAccessInformation(this.forumId)
];
return this.utils.allPromises(promises).catch(() => {
// Ignore errors. // Ignore errors.
}).then(() => { }).then(() => {
return this.fetchPosts(sync, showErrors); return this.fetchPosts(sync, showErrors);

View File

@ -79,6 +79,16 @@ export class AddonModForumProvider {
return this.ROOT_CACHE_KEY + 'forum:' + courseId; return this.ROOT_CACHE_KEY + 'forum:' + courseId;
} }
/**
* Get cache key for forum access information WS calls.
*
* @param {number} forumId Forum ID.
* @return {string} Cache key.
*/
protected getAccessInformationCacheKey(forumId: number): string {
return this.ROOT_CACHE_KEY + 'accessInformation:' + forumId;
}
/** /**
* Get cache key for forum discussion posts WS calls. * Get cache key for forum discussion posts WS calls.
* *
@ -365,6 +375,34 @@ export class AddonModForumProvider {
}); });
} }
/**
* Get access information for a given forum.
*
* @param {number} forumId Forum ID.
* @param {boolean} [forceCache] True to always get the value from cache. false otherwise.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Object with access information.
* @since 3.7
*/
getAccessInformation(forumId: number, forceCache?: boolean, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
if (!site.wsAvailable('mod_forum_get_forum_access_information')) {
// Access information not available for 3.6 or older sites.
return Promise.resolve({});
}
const params = {
forumid: forumId
};
const preSets = {
cacheKey: this.getAccessInformationCacheKey(forumId),
omitExpires: forceCache
};
return site.read('mod_forum_get_forum_access_information', params, preSets);
});
}
/** /**
* Get forum discussion posts. * Get forum discussion posts.
* *
@ -537,6 +575,7 @@ export class AddonModForumProvider {
promises.push(this.invalidateForumData(courseId)); promises.push(this.invalidateForumData(courseId));
promises.push(this.invalidateDiscussionsList(forum.id)); promises.push(this.invalidateDiscussionsList(forum.id));
promises.push(this.invalidateCanAddDiscussion(forum.id)); promises.push(this.invalidateCanAddDiscussion(forum.id));
promises.push(this.invalidateAccessInformation(forum.id));
response.discussions.forEach((discussion) => { response.discussions.forEach((discussion) => {
promises.push(this.invalidateDiscussionPosts(discussion.discussion)); promises.push(this.invalidateDiscussionPosts(discussion.discussion));
@ -547,6 +586,19 @@ export class AddonModForumProvider {
}); });
} }
/**
* Invalidates access information.
*
* @param {number} forumId Forum ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateAccessInformation(forumId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(forumId));
});
}
/** /**
* Invalidates forum discussion posts. * Invalidates forum discussion posts.
* *

View File

@ -54,7 +54,8 @@ export class AddonModForumHelperProvider {
postread: false, postread: false,
subject: offlineReply.subject, subject: offlineReply.subject,
totalscore: 0, totalscore: 0,
userid: offlineReply.userid userid: offlineReply.userid,
isprivatereply: offlineReply.options && offlineReply.options.private
}, },
promises = []; promises = [];
@ -164,6 +165,10 @@ export class AddonModForumHelperProvider {
return true; return true;
} }
if (post.isprivatereply != original.isprivatereply) {
return true;
}
return this.uploaderProvider.areFileListDifferent(post.files, original.files); return this.uploaderProvider.areFileListDifferent(post.files, original.files);
} }

View File

@ -183,8 +183,10 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
protected prefetchForum(module: any, courseId: number, single: boolean, siteId: string): Promise<any> { protected prefetchForum(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
// Get the forum data. // Get the forum data.
return this.forumProvider.getForum(courseId, module.id).then((forum) => { return this.forumProvider.getForum(courseId, module.id).then((forum) => {
const promises = [];
// Prefetch the posts. // Prefetch the posts.
return this.getPostsForPrefetch(forum).then((posts) => { promises.push(this.getPostsForPrefetch(forum).then((posts) => {
const promises = []; const promises = [];
// Prefetch user profiles. // Prefetch user profiles.
@ -201,7 +203,12 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
promises.push(this.prefetchGroupsInfo(forum, courseId, forum.cancreatediscussions)); promises.push(this.prefetchGroupsInfo(forum, courseId, forum.cancreatediscussions));
return Promise.all(promises); return Promise.all(promises);
}); }));
// Prefetch access information.
promises.push(this.forumProvider.getAccessInformation(forum.id));
return Promise.all(promises);
}); });
} }

View File

@ -496,7 +496,9 @@
"addon.mod_forum.modulenameplural": "Forums", "addon.mod_forum.modulenameplural": "Forums",
"addon.mod_forum.numdiscussions": "{{numdiscussions}} discussions", "addon.mod_forum.numdiscussions": "{{numdiscussions}} discussions",
"addon.mod_forum.numreplies": "{{numreplies}} replies", "addon.mod_forum.numreplies": "{{numreplies}} replies",
"addon.mod_forum.postisprivatereply": "This post was made privately and is not visible to all users.",
"addon.mod_forum.posttoforum": "Post to forum", "addon.mod_forum.posttoforum": "Post to forum",
"addon.mod_forum.privatereply": "Reply privately",
"addon.mod_forum.re": "Re:", "addon.mod_forum.re": "Re:",
"addon.mod_forum.refreshdiscussions": "Refresh discussions", "addon.mod_forum.refreshdiscussions": "Refresh discussions",
"addon.mod_forum.refreshposts": "Refresh posts", "addon.mod_forum.refreshposts": "Refresh posts",
@ -1342,6 +1344,7 @@
"core.favourites": "Starred", "core.favourites": "Starred",
"core.filename": "Filename", "core.filename": "Filename",
"core.filenameexist": "File name already exists: {{$a}}", "core.filenameexist": "File name already exists: {{$a}}",
"core.filenotfound": "File not found, sorry.",
"core.fileuploader.addfiletext": "Add file", "core.fileuploader.addfiletext": "Add file",
"core.fileuploader.audio": "Audio", "core.fileuploader.audio": "Audio",
"core.fileuploader.camera": "Camera", "core.fileuploader.camera": "Camera",