diff --git a/src/addons/mod/chat/chat-lazy.module.ts b/src/addons/mod/chat/chat-lazy.module.ts index 6338bace3..3a667037e 100644 --- a/src/addons/mod/chat/chat-lazy.module.ts +++ b/src/addons/mod/chat/chat-lazy.module.ts @@ -17,18 +17,54 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { AddonModChatComponentsModule } from './components/components.module'; import { AddonModChatIndexPage } from './pages/index/index'; +import { AddonModChatChatPage } from './pages/chat/chat'; +import { AddonModChatSessionMessagesPage } from './pages/session-messages/session-messages'; +import { CoreScreen } from '@services/screen'; +import { conditionalRoutes } from '@/app/app-routing.module'; +import { AddonModChatSessionsPage } from './pages/sessions/sessions'; -const routes: Routes = [ +const commonRoutes: Routes = [ { path: ':courseId/:cmId', component: AddonModChatIndexPage, }, { path: ':courseId/:cmId/chat', - loadChildren: () => import('./pages/chat/chat.module').then(m => m.AddonModChatChatPageModule), + component: AddonModChatChatPage, }, ]; +const mobileRoutes: Routes = [ + ...commonRoutes, + { + path: ':courseId/:cmId/sessions', + component: AddonModChatSessionsPage, + }, + { + path: ':courseId/:cmId/sessions/:sessionStart/:sessionEnd', + component: AddonModChatSessionMessagesPage, + }, +]; + +const tabletRoutes: Routes = [ + ...commonRoutes, + { + path: ':courseId/:cmId/sessions', + component: AddonModChatSessionsPage, + children: [ + { + path: ':sessionStart/:sessionEnd', + component: AddonModChatSessionMessagesPage, + }, + ], + }, +]; + +const routes: Routes = [ + ...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile), + ...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet), +]; + @NgModule({ imports: [ RouterModule.forChild(routes), @@ -37,6 +73,9 @@ const routes: Routes = [ ], declarations: [ AddonModChatIndexPage, + AddonModChatChatPage, + AddonModChatSessionsPage, + AddonModChatSessionMessagesPage, ], }) export class AddonModChatLazyModule {} diff --git a/src/addons/mod/chat/pages/session-messages/session-messages.html b/src/addons/mod/chat/pages/session-messages/session-messages.html new file mode 100644 index 000000000..cdb8e84fa --- /dev/null +++ b/src/addons/mod/chat/pages/session-messages/session-messages.html @@ -0,0 +1,100 @@ + + + + + + {{ 'addon.mod_chat.messages' | translate }} + + + + + + + + + + + + {{ message.timestamp * 1000 | coreFormatDate:"strftimedayshort" }} + + + + + + + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} + {{ 'addon.mod_chat.messageenter' | translate:{$a: message.userfullname} }} + + + + + + + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} + {{ 'addon.mod_chat.messageexit' | translate:{$a: message.userfullname} }} + + + + + + + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} + {{ 'addon.mod_chat.messagebeepseveryone' | translate:{$a: message.userfullname} }} + + + + + + + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} + {{ 'addon.mod_chat.messagebeepsyou' | translate:{$a: message.userfullname} }} + + + + + + + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} + {{ 'addon.mod_chat.messageyoubeep' | translate:{$a: message.beepWho} }} + + + + + + + {{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }} + + {{ message.userfullname }} + + + + + + + + + + + + {{ message.userfullname }} + {{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }} + + + + + + + + + + + + + diff --git a/src/addons/mod/chat/pages/session-messages/session-messages.scss b/src/addons/mod/chat/pages/session-messages/session-messages.scss new file mode 100644 index 000000000..2ee6b3c8c --- /dev/null +++ b/src/addons/mod/chat/pages/session-messages/session-messages.scss @@ -0,0 +1,6 @@ +:host { + .addon-mod_chat-notice { + margin-top: 8px; + margin-bottom: 8px; + } +} diff --git a/src/addons/mod/chat/pages/session-messages/session-messages.ts b/src/addons/mod/chat/pages/session-messages/session-messages.ts new file mode 100644 index 000000000..badb7a60d --- /dev/null +++ b/src/addons/mod/chat/pages/session-messages/session-messages.ts @@ -0,0 +1,142 @@ +// (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, OnInit } from '@angular/core'; +import { CoreUser } from '@features/user/services/user'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonModChat } from '../../services/chat'; +import { AddonModChatFormattedSessionMessage, AddonModChatHelper } from '../../services/chat-helper'; + +/** + * Page that displays list of chat session messages. + */ +@Component({ + selector: 'page-addon-mod-chat-session-messages', + templateUrl: 'session-messages.html', + styleUrls: ['session-messages.scss', '../../../../messages/pages/discussion/discussion.scss'], +}) +export class AddonModChatSessionMessagesPage implements OnInit { + + currentUserId!: number; + cmId!: number; + messages: AddonModChatFormattedSessionMessage[] = []; + loaded = false; + courseId!: number; + + protected chatId!: number; + protected sessionStart!: number; + protected sessionEnd!: number; + protected groupId!: number; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + this.sessionStart = CoreNavigator.getRouteNumberParam('sessionStart')!; + this.sessionEnd = CoreNavigator.getRouteNumberParam('sessionEnd')!; + this.chatId = CoreNavigator.getRouteNumberParam('chatId')!; + this.groupId = CoreNavigator.getRouteNumberParam('groupId') || 0; + + this.currentUserId = CoreSites.getCurrentSiteUserId(); + + this.fetchMessages(); + } + + /** + * Fetch session messages. + * + * @return Promise resolved when done. + */ + protected async fetchMessages(): Promise { + try { + const messages = await AddonModChat.getSessionMessages( + this.chatId, + this.sessionStart, + this.sessionEnd, + this.groupId, + { cmId: this.cmId }, + ); + + this.messages = await AddonModChat.getMessagesUserData(messages, this.courseId); + + // Calculate which messages need to display the date or user data. + for (let index = 0 ; index < this.messages.length; index++) { + const prevMessage = index > 0 ? this.messages[index - 1] : undefined; + + this.messages[index] = AddonModChatHelper.formatMessage(this.currentUserId, this.messages[index], prevMessage); + + const message = this.messages[index]; + + if (message.beep && message.beep != String(this.currentUserId)) { + this.loadMessageBeepWho(message); + } + } + + this.messages[this.messages.length - 1].showTail = true; + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); + } finally { + this.loaded = true; + } + } + + protected async loadMessageBeepWho(message: AddonModChatFormattedSessionMessage): Promise { + message.beepWho = await this.getUserFullname(message.beep!); + } + + /** + * Get the user fullname for a beep. + * + * @param id User Id before parsing. + * @return User fullname. + */ + protected async getUserFullname(id: string): Promise { + const idNumber = parseInt(id, 10); + + if (isNaN(idNumber)) { + return id; + } + + try { + const user = await CoreUser.getProfile(idNumber, this.courseId, true); + + return user.fullname; + } catch { + // Error getting profile. + return id; + } + } + + /** + * Refresh session messages. + * + * @param refresher Refresher. + */ + async refreshMessages(refresher: IonRefresher): Promise { + try { + await CoreUtils.ignoreErrors(AddonModChat.invalidateSessionMessages(this.chatId, this.sessionStart, this.groupId)); + + await this.fetchMessages(); + } finally { + refresher.complete(); + } + } + +} diff --git a/src/addons/mod/chat/pages/sessions/sessions.html b/src/addons/mod/chat/pages/sessions/sessions.html new file mode 100644 index 000000000..7b65afbda --- /dev/null +++ b/src/addons/mod/chat/pages/sessions/sessions.html @@ -0,0 +1,61 @@ + + + + + + {{ 'addon.mod_chat.chatreport' | translate }} + + + + + + + + + + + {{'core.groupsseparate' | translate }} + {{'core.groupsvisible' | translate }} + + + + {{groupOpt.name}} + + + + + + {{ 'addon.mod_chat.showincompletesessions' | translate }} + + + + + + + + {{ session.sessionstart * 1000 | coreFormatDate }} + {{ session.duration | coreDuration }} + + + + + + {{ user.userfullname }} ({{ user.messagecount }}) + + + + + + {{ 'core.showmore' | translate }} + + + + + + + + + diff --git a/src/addons/mod/chat/pages/sessions/sessions.scss b/src/addons/mod/chat/pages/sessions/sessions.scss new file mode 100644 index 000000000..066605cdc --- /dev/null +++ b/src/addons/mod/chat/pages/sessions/sessions.scss @@ -0,0 +1,8 @@ +ion-app.app-root page-addon-mod-chat-sessions { + .addon-mod-chat-session-show-more .card-content{ + padding-bottom: 0; + } + .addon-mod-chat-session-selected { + border-top: 5px solid $core-splitview-selected; + } +} diff --git a/src/addons/mod/chat/pages/sessions/sessions.ts b/src/addons/mod/chat/pages/sessions/sessions.ts new file mode 100644 index 000000000..c579fcb35 --- /dev/null +++ b/src/addons/mod/chat/pages/sessions/sessions.ts @@ -0,0 +1,234 @@ +// (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 { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { Params } from '@angular/router'; +import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreUser } from '@features/user/services/user'; +import { IonRefresher } from '@ionic/angular'; +import { CoreGroupInfo, CoreGroups } from '@services/groups'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { AddonModChat, AddonModChatSession, AddonModChatSessionUser } from '../../services/chat'; + +/** + * Page that displays list of chat sessions. + */ +@Component({ + selector: 'page-addon-mod-chat-sessions', + templateUrl: 'sessions.html', +}) +export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy { + + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + + sessions!: AddonChatSessionsManager; + showAll = false; + groupId = 0; + groupInfo?: CoreGroupInfo; + + protected courseId!: number; + protected cmId!: number; + protected chatId!: number; + + constructor() { + this.sessions = new AddonChatSessionsManager(AddonModChatSessionsPage); + } + + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + this.chatId = CoreNavigator.getRouteNumberParam('chatId')!; + this.sessions.setChatId(this.chatId); + + await this.fetchSessions(); + + this.sessions.start(this.splitView); + } + + /** + * Fetch chat sessions. + * + * @param showLoading Display a loading modal. + * @return Promise resolved when done. + */ + async fetchSessions(showLoading?: boolean): Promise { + const modal = showLoading ? await CoreDomUtils.showModalLoading() : null; + + try { + this.groupInfo = await CoreGroups.getActivityGroupInfo(this.cmId, false); + + this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo); + this.sessions.setGroupId(this.groupId); + + const sessions = await AddonModChat.getSessions(this.chatId, this.groupId, this.showAll, { cmId: this.cmId }); + + // Fetch user profiles. + const promises: Promise[] = []; + + const formattedSessions = sessions.map((session: AddonModChatSessionFormatted) => { + session.duration = session.sessionend - session.sessionstart; + session.sessionusers.forEach((sessionUser) => { + // The WS does not return the user name, fetch user profile. + promises.push(this.loadUserFullname(sessionUser)); + }); + + // If session has more than 4 users we display a "Show more" link. + session.allsessionusers = session.sessionusers; + if (session.sessionusers.length > 4) { + session.sessionusers = session.allsessionusers.slice(0, 3); + } + + return session; + }); + + await Promise.all(promises); + + this.sessions.setItems(formattedSessions); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); + } finally { + modal?.dismiss(); + } + } + + /** + * Load the fullname of a user. + * + * @param id User ID. + * @return Promise resolved when done. + */ + protected async loadUserFullname(sessionUser: AddonModChatUserSessionFormatted): Promise { + if (sessionUser.userfullname) { + return; + } + + try { + const user = await CoreUser.getProfile(sessionUser.userid, this.courseId, true); + + sessionUser.userfullname = user.fullname; + } catch { + // Error getting profile, most probably the user is deleted. + sessionUser.userfullname = Translate.instant('core.deleteduser') + ' ' + sessionUser.userid; + } + } + + /** + * Refresh chat sessions. + * + * @param refresher Refresher. + */ + async refreshSessions(refresher: IonRefresher): Promise { + try { + await CoreUtils.ignoreErrors(CoreUtils.allPromises([ + CoreGroups.invalidateActivityGroupInfo(this.cmId), + AddonModChat.invalidateSessions(this.chatId, this.groupId, this.showAll), + ])); + + await this.fetchSessions(); + } finally { + refresher.complete(); + } + } + + /** + * Show more session users. + * + * @param session Chat session. + * @param event The event. + */ + showMoreUsers(session: AddonModChatSessionFormatted, event: Event): void { + session.sessionusers = session.allsessionusers!; + event.stopPropagation(); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.sessions.destroy(); + } + +} + +/** + * Helper class to manage sessions. + */ +class AddonChatSessionsManager extends CorePageItemsListManager { + + chatId = -1; + groupId = 0; + + constructor(pageComponent: unknown) { + super(pageComponent); + } + + /** + * Set chat ID. + * + * @param chatId Chat ID. + */ + setChatId(chatId: number): void { + this.chatId = chatId; + } + + /** + * Set group ID. + * + * @param groupId Group ID. + */ + setGroupId(groupId: number): void { + this.groupId = groupId; + } + + /** + * @inheritdoc + */ + protected getItemPath(session: AddonModChatSessionFormatted): string { + return `${session.sessionstart}/${session.sessionend}`; + } + + /** + * @inheritdoc + */ + protected getItemQueryParams(): Params { + return { + chatId: this.chatId, + groupId: this.groupId, + }; + } + +} + +/** + * Fields added to chat session in this view. + */ +type AddonModChatSessionFormatted = Omit & { + duration?: number; // Session duration. + sessionusers: AddonModChatUserSessionFormatted[]; + allsessionusers?: AddonModChatUserSessionFormatted[]; // All session users. +}; + +/** + * Fields added to user session in this view. + */ +type AddonModChatUserSessionFormatted = AddonModChatSessionUser & { + userfullname?: string; // User full name. +};
+ + +
{{ session.duration | coreDuration }}