MOBILE-3643 forum: Infer event payloads

main
Noel De Martin 2021-03-01 13:40:17 +01:00
parent 205fb9e7cb
commit a695f9cb66
7 changed files with 139 additions and 28 deletions

View File

@ -22,6 +22,8 @@ import {
AddonModForumProvider, AddonModForumProvider,
AddonModForumSortOrder, AddonModForumSortOrder,
AddonModForumDiscussion, AddonModForumDiscussion,
AddonModForumNewDiscussionData,
AddonModForumReplyDiscussionData,
} from '@addons/mod/forum/services/forum.service'; } from '@addons/mod/forum/services/forum.service';
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/offline.service'; import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/offline.service';
import { ModalController, PopoverController, Translate } from '@singletons'; import { ModalController, PopoverController, Translate } from '@singletons';
@ -127,7 +129,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
AddonModForumProvider.REPLY_DISCUSSION_EVENT, AddonModForumProvider.REPLY_DISCUSSION_EVENT,
this.eventReceived.bind(this, false), this.eventReceived.bind(this, false),
); );
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data: any) => { this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module!.id) { if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module!.id) {
AddonModForum.instance.invalidateDiscussionsList(this.forum!.id).finally(() => { AddonModForum.instance.invalidateDiscussionsList(this.forum!.id).finally(() => {
if (data.discussionId) { if (data.discussionId) {
@ -152,7 +154,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
} }
if (typeof data.deleted != 'undefined' && data.deleted) { if (typeof data.deleted != 'undefined' && data.deleted) {
if (data.post.parentid == 0 && CoreScreen.instance.isTablet && !this.discussions.empty) { if (data.post?.parentid == 0 && CoreScreen.instance.isTablet && !this.discussions.empty) {
// Discussion deleted, clear details page. // Discussion deleted, clear details page.
this.discussions.select(this.discussions[0]); this.discussions.select(this.discussions[0]);
} }
@ -275,7 +277,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
if (updated) { if (updated) {
// Sync successful, send event. // Sync successful, send event.
CoreEvents.trigger<AddonModForumManualSyncData>(AddonModForumSyncProvider.MANUAL_SYNCED, { CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, {
forumId: forum.id, forumId: forum.id,
userId: CoreSites.instance.getCurrentSiteUserId(), userId: CoreSites.instance.getCurrentSiteUserId(),
source: 'index', source: 'index',
@ -553,18 +555,22 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
* @param isNewDiscussion Whether it's a new discussion event. * @param isNewDiscussion Whether it's a new discussion event.
* @param data Event data. * @param data Event data.
*/ */
protected eventReceived(isNewDiscussion: boolean, data: any): void { protected eventReceived(
isNewDiscussion: boolean,
data: AddonModForumNewDiscussionData | AddonModForumReplyDiscussionData,
): void {
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module?.id) { if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module?.id) {
this.showLoadingAndRefresh(false).finally(() => { this.showLoadingAndRefresh(false).finally(() => {
// If it's a new discussion in tablet mode, try to open it. // If it's a new discussion in tablet mode, try to open it.
if (isNewDiscussion && CoreScreen.instance.isTablet) { if (isNewDiscussion && CoreScreen.instance.isTablet) {
const newDiscussionData = data as AddonModForumNewDiscussionData;
const discussion = this.discussions.items.find(disc => { const discussion = this.discussions.items.find(disc => {
if (this.discussions.isOfflineDiscussion(disc)) { if (this.discussions.isOfflineDiscussion(disc)) {
return disc.timecreated === data.discTimecreated; return disc.timecreated === newDiscussionData.discTimecreated;
} }
if (this.discussions.isOnlineDiscussion(disc)) { if (this.discussions.isOnlineDiscussion(disc)) {
return CoreArray.contains(data.discussionIds, disc.discussion); return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion);
} }
return false; return false;

View File

@ -37,7 +37,7 @@ import {
} from '../../services/forum.service'; } from '../../services/forum.service';
import { AddonModForumHelper } from '../../services/helper.service'; import { AddonModForumHelper } from '../../services/helper.service';
import { AddonModForumOffline } from '../../services/offline.service'; import { AddonModForumOffline } from '../../services/offline.service';
import { AddonModForumManualSyncData, AddonModForumSync, AddonModForumSyncProvider } from '../../services/sync.service'; import { AddonModForumSync, AddonModForumSyncProvider } from '../../services/sync.service';
type SortType = 'flat-newest' | 'flat-oldest' | 'nested'; type SortType = 'flat-newest' | 'flat-oldest' | 'nested';
@ -170,7 +170,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
} }
// Refresh data if this discussion is synchronized automatically. // Refresh data if this discussion is synchronized automatically.
this.syncObserver = CoreEvents.on(AddonModForumSyncProvider.AUTO_SYNCED, (data: any) => { this.syncObserver = CoreEvents.on(AddonModForumSyncProvider.AUTO_SYNCED, data => {
if (data.forumId == this.forumId && this.discussionId == data.discussionId if (data.forumId == this.forumId && this.discussionId == data.discussionId
&& data.userId == CoreSites.instance.getCurrentSiteUserId()) { && data.userId == CoreSites.instance.getCurrentSiteUserId()) {
// Refresh the data. // Refresh the data.
@ -180,7 +180,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
}, CoreSites.instance.getCurrentSiteId()); }, CoreSites.instance.getCurrentSiteId());
// Refresh data if this forum discussion is synchronized from discussions list. // Refresh data if this forum discussion is synchronized from discussions list.
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data: AddonModForumManualSyncData) => { this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, data => {
if (data.source != 'discussion' && data.forumId == this.forumId && if (data.source != 'discussion' && data.forumId == this.forumId &&
data.userId == CoreSites.instance.getCurrentSiteUserId()) { data.userId == CoreSites.instance.getCurrentSiteUserId()) {
// Refresh the data. // Refresh the data.
@ -196,7 +196,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
// @todo Listen for offline ratings saved and synced. // @todo Listen for offline ratings saved and synced.
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, (data: any) => { this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
if ((this.forumId && this.forumId === data.forumId) || data.cmId === this.cmId) { if ((this.forumId && this.forumId === data.forumId) || data.cmId === this.cmId) {
AddonModForum.instance.invalidateDiscussionsList(this.forumId).finally(() => { AddonModForum.instance.invalidateDiscussionsList(this.forumId).finally(() => {
if (typeof data.locked != 'undefined') { if (typeof data.locked != 'undefined') {
@ -210,7 +210,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
} }
if (typeof data.deleted != 'undefined' && data.deleted) { if (typeof data.deleted != 'undefined' && data.deleted) {
if (!data.post.parentid) { if (!data.post?.parentid) {
// @todo // @todo
// if (this.svComponent && this.svComponent.isOn()) { // if (this.svComponent && this.svComponent.isOn()) {
// this.svComponent.emptyDetails(); // this.svComponent.emptyDetails();
@ -545,7 +545,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
if (result && result.updated) { if (result && result.updated) {
// Sync successful, send event. // Sync successful, send event.
CoreEvents.trigger<AddonModForumManualSyncData>(AddonModForumSyncProvider.MANUAL_SYNCED, { CoreEvents.trigger(AddonModForumSyncProvider.MANUAL_SYNCED, {
forumId: this.forumId, forumId: this.forumId,
userId: CoreSites.instance.getCurrentSiteUserId(), userId: CoreSites.instance.getCurrentSiteUserId(),
source: 'discussion', source: 'discussion',

View File

@ -120,7 +120,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
} }
// Refresh data if this discussion is synchronized automatically. // Refresh data if this discussion is synchronized automatically.
this.syncObserver = CoreEvents.on(AddonModForumSyncProvider.AUTO_SYNCED, (data: any) => { this.syncObserver = CoreEvents.on(AddonModForumSyncProvider.AUTO_SYNCED, data => {
if (data.forumId == this.forumId && data.userId == CoreSites.instance.getCurrentSiteUserId()) { if (data.forumId == this.forumId && data.userId == CoreSites.instance.getCurrentSiteUserId()) {
CoreDomUtils.instance.showAlertTranslated('core.notice', 'core.contenteditingsynced'); CoreDomUtils.instance.showAlertTranslated('core.notice', 'core.contenteditingsynced');
this.returnToDiscussions(); this.returnToDiscussions();

View File

@ -30,6 +30,22 @@ import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumRepl
const ROOT_CACHE_KEY = 'mmaModForum:'; const ROOT_CACHE_KEY = 'mmaModForum:';
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 {
[AddonModForumProvider.NEW_DISCUSSION_EVENT]: AddonModForumNewDiscussionData;
[AddonModForumProvider.REPLY_DISCUSSION_EVENT]: AddonModForumReplyDiscussionData;
[AddonModForumProvider.CHANGE_DISCUSSION_EVENT]: AddonModForumChangeDiscussionData;
[AddonModForumProvider.MARK_READ_EVENT]: AddonModForumMarkReadData;
}
}
/** /**
* Service that provides some features for forums. * Service that provides some features for forums.
* *
@ -2151,3 +2167,44 @@ export type AddonModForumUpdateDiscussionPostWSParams = {
* Data returned by mod_forum_update_discussion_post WS. * Data returned by mod_forum_update_discussion_post WS.
*/ */
export type AddonModForumUpdateDiscussionPostWSResponse = CoreStatusWithWarningsWSResponse; export type AddonModForumUpdateDiscussionPostWSResponse = CoreStatusWithWarningsWSResponse;
/**
* Data passed to NEW_DISCUSSION_EVENT event.
*/
export type AddonModForumNewDiscussionData = {
forumId: number;
cmId: number;
discussionIds?: number[] | null;
discTimecreated?: number;
};
/**
* Data passed to REPLY_DISCUSSION_EVENT event.
*/
export type AddonModForumReplyDiscussionData = {
forumId: number;
discussionId: number;
cmId: number;
};
/**
* Data passed to CHANGE_DISCUSSION_EVENT event.
*/
export type AddonModForumChangeDiscussionData = {
forumId: number;
discussionId: number;
cmId: number;
deleted?: boolean;
post?: AddonModForumPost;
locked?: boolean;
pinned?: boolean;
starred?: boolean;
};
/**
* Data passed to MARK_READ_EVENT event.
*/
export type AddonModForumMarkReadData = {
courseId: number;
moduleId: number;
};

View File

@ -95,7 +95,7 @@ export class AddonModForumModuleHandlerService implements CoreCourseModuleHandle
const event = CoreEvents.on( const event = CoreEvents.on(
AddonModForumProvider.MARK_READ_EVENT, AddonModForumProvider.MARK_READ_EVENT,
(eventData: { courseId?: number; moduleId?: number; siteId?: string }) => { eventData => {
if (eventData.courseId !== courseId || eventData.moduleId !== module.id) { if (eventData.courseId !== courseId || eventData.moduleId !== module.id) {
return; return;
} }

View File

@ -35,6 +35,20 @@ import {
import { AddonModForumHelper } from './helper.service'; import { AddonModForumHelper } from './helper.service';
import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumOfflineReply } from './offline.service'; import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumOfflineReply } from './offline.service';
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 {
[AddonModForumSyncProvider.AUTO_SYNCED]: AddonModForumAutoSyncData;
[AddonModForumSyncProvider.MANUAL_SYNCED]: AddonModForumManualSyncData;
}
}
/** /**
* Service to sync forums. * Service to sync forums.
*/ */
@ -95,7 +109,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
if (result && result.updated) { if (result && result.updated) {
// Sync successful, send event. // Sync successful, send event.
CoreEvents.trigger<AddonModForumAutoSyncData>(AddonModForumSyncProvider.AUTO_SYNCED, { CoreEvents.trigger(AddonModForumSyncProvider.AUTO_SYNCED, {
forumId: discussion.forumid, forumId: discussion.forumid,
userId: discussion.userid, userId: discussion.userid,
warnings: result.warnings, warnings: result.warnings,
@ -127,7 +141,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
if (result && result.updated) { if (result && result.updated) {
// Sync successful, send event. // Sync successful, send event.
CoreEvents.trigger<AddonModForumAutoSyncData>(AddonModForumSyncProvider.AUTO_SYNCED, { CoreEvents.trigger(AddonModForumSyncProvider.AUTO_SYNCED, {
forumId: reply.forumid, forumId: reply.forumid,
discussionId: reply.discussionid, discussionId: reply.discussionid,
userId: reply.userid, userId: reply.userid,

View File

@ -28,6 +28,25 @@ export interface CoreEventObserver {
off: () => void; off: () => void;
} }
/**
* Event payloads.
*/
export interface CoreEventsData {
[CoreEvents.SITE_UPDATED]: CoreEventSiteUpdatedData;
[CoreEvents.SITE_ADDED]: CoreEventSiteAddedData;
[CoreEvents.SESSION_EXPIRED]: CoreEventSessionExpiredData;
[CoreEvents.CORE_LOADING_CHANGED]: CoreEventLoadingChangedData;
[CoreEvents.COURSE_STATUS_CHANGED]: CoreEventCourseStatusChanged;
[CoreEvents.PACKAGE_STATUS_CHANGED]: CoreEventPackageStatusChanged;
[CoreEvents.USER_DELETED]: CoreEventUserDeletedData;
[CoreEvents.FORM_ACTION]: CoreEventFormActionData;
[CoreEvents.NOTIFICATION_SOUND_CHANGED]: CoreEventNotificationSoundChangedData;
[CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData;
[CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData;
[CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData;
[CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData;
};
/* /*
* Service to send and listen to events. * Service to send and listen to events.
*/ */
@ -84,15 +103,15 @@ export class CoreEvents {
* @param siteId Site where to trigger the event. Undefined won't check the site. * @param siteId Site where to trigger the event. Undefined won't check the site.
* @return Observer to stop listening. * @return Observer to stop listening.
*/ */
static on<T = unknown>( static on<Fallback = unknown, Event extends string = string>(
eventName: string, eventName: Event,
callBack: (value: T & { siteId?: string }) => void, callBack: (value: CoreEventData<Event, Fallback> & { siteId?: string }) => void,
siteId?: string, siteId?: string,
): CoreEventObserver { ): CoreEventObserver {
// If it's a unique event and has been triggered already, call the callBack. // If it's a unique event and has been triggered already, call the callBack.
// We don't need to create an observer because the event won't be triggered again. // We don't need to create an observer because the event won't be triggered again.
if (this.uniqueEvents[eventName]) { if (this.uniqueEvents[eventName]) {
callBack(<T> this.uniqueEvents[eventName].data); callBack(this.uniqueEvents[eventName].data as CoreEventData<Event, Fallback> & { siteId?: string });
// Return a fake observer to prevent errors. // Return a fake observer to prevent errors.
return { return {
@ -106,14 +125,16 @@ export class CoreEvents {
if (typeof this.observables[eventName] == 'undefined') { if (typeof this.observables[eventName] == 'undefined') {
// No observable for this event, create a new one. // No observable for this event, create a new one.
this.observables[eventName] = new Subject<T>(); this.observables[eventName] = new Subject();
} }
const subscription = this.observables[eventName].subscribe((value: T & {siteId?: string}) => { const subscription = this.observables[eventName].subscribe(
(value: CoreEventData<Event, Fallback> & { siteId?: string }) => {
if (!siteId || value.siteId == siteId) { if (!siteId || value.siteId == siteId) {
callBack(value); callBack(value);
} }
}); },
);
// Create and return a CoreEventObserver. // Create and return a CoreEventObserver.
return { return {
@ -155,7 +176,11 @@ export class CoreEvents {
* @param data Data to pass to the observers. * @param data Data to pass to the observers.
* @param siteId Site where to trigger the event. Undefined means no Site. * @param siteId Site where to trigger the event. Undefined means no Site.
*/ */
static trigger<T = unknown>(eventName: string, data?: T, siteId?: string): void { static trigger<Fallback = unknown, Event extends string = string>(
eventName: Event,
data?: CoreEventData<Event, Fallback>,
siteId?: string,
): void {
this.logger.debug(`Event '${eventName}' triggered.`); this.logger.debug(`Event '${eventName}' triggered.`);
if (this.observables[eventName]) { if (this.observables[eventName]) {
if (siteId) { if (siteId) {
@ -172,7 +197,11 @@ export class CoreEvents {
* @param data Data to pass to the observers. * @param data Data to pass to the observers.
* @param siteId Site where to trigger the event. Undefined means no Site. * @param siteId Site where to trigger the event. Undefined means no Site.
*/ */
static triggerUnique<T = unknown>(eventName: string, data: T, siteId?: string): void { static triggerUnique<Fallback = unknown, Event extends string = string>(
eventName: Event,
data: CoreEventData<Event, Fallback>,
siteId?: string,
): void {
if (this.uniqueEvents[eventName]) { if (this.uniqueEvents[eventName]) {
this.logger.debug(`Unique event '${eventName}' ignored because it was already triggered.`); this.logger.debug(`Unique event '${eventName}' ignored because it was already triggered.`);
} else { } else {
@ -196,6 +225,11 @@ export class CoreEvents {
} }
/**
* Resolve payload type for a given event.
*/
export type CoreEventData<Event, Fallback> = Event extends keyof CoreEventsData ? CoreEventsData[Event] : Fallback;
/** /**
* Some events contains siteId added by the trigger function. This type is intended to be combined with others. * Some events contains siteId added by the trigger function. This type is intended to be combined with others.
*/ */