From 64ae2cf2d924f466d4a3397f2d029aaff27194fc Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Fri, 12 Jan 2024 14:58:29 -0800 Subject: [PATCH] feat(PeerChat): IsTyping indicator (#1575) Co-authored-by: Jonathan Lim-Breitbart --- .../peer-chat-chat-box.component.html | 8 ++ .../peer-chat-chat-box.component.scss | 8 +- .../peer-chat-chat-box.component.spec.ts | 9 +- .../peer-chat-chat-box.component.ts | 4 + ...hat-member-typing-indicator.component.html | 1 + ...-member-typing-indicator.component.spec.ts | 36 ++++++ ...-chat-member-typing-indicator.component.ts | 109 ++++++++++++++++++ .../peer-chat-message-input.component.ts | 34 +++++- .../peer-chat-student.component.html | 2 + .../components/peerChat/peer-chat.module.ts | 6 +- src/messages.xlf | 48 +++++--- 11 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.html create mode 100644 src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.spec.ts create mode 100644 src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.ts diff --git a/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.html b/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.html index f8801f50bbd..05bacc66cfc 100644 --- a/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.html +++ b/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.html @@ -11,8 +11,16 @@ > + diff --git a/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.scss b/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.scss index 6e303cb1137..261d7fbe254 100644 --- a/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.scss +++ b/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.scss @@ -1,8 +1,14 @@ .mat-mdc-card { - padding: 12px; + padding: 16px 16px 20px; } peer-chat-messages, peer-chat-members { margin-bottom: 12px; display: block; } + +peer-chat-member-typing-indicator { + padding: 0 8px; + position: absolute; + bottom: 2px; +} diff --git a/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.spec.ts b/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.spec.ts index 48112a6f380..58a0d310905 100644 --- a/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.spec.ts +++ b/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.spec.ts @@ -6,9 +6,12 @@ import { MatInputModule } from '@angular/material/input'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { PeerChatMembersComponent } from '../peer-chat-members/peer-chat-members.component'; import { PeerChatMessageInputComponent } from '../peer-chat-message-input/peer-chat-message-input.component'; - import { PeerChatChatBoxComponent } from './peer-chat-chat-box.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ConfigService } from '../../../services/configService'; +import { StompService } from '../../../services/stompService'; +class MockConfigService {} describe('PeerChatChatBoxComponent', () => { let component: PeerChatChatBoxComponent; let fixture: ComponentFixture; @@ -26,7 +29,9 @@ describe('PeerChatChatBoxComponent', () => { PeerChatChatBoxComponent, PeerChatMembersComponent, PeerChatMessageInputComponent - ] + ], + providers: [{ provide: ConfigService, useClass: MockConfigService }, StompService], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); }); diff --git a/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.ts b/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.ts index bf998a1fd35..0d88a5844f5 100644 --- a/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.ts +++ b/src/assets/wise5/components/peerChat/peer-chat-chat-box/peer-chat-chat-box.component.ts @@ -1,5 +1,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { PeerChatMessage } from '../PeerChatMessage'; +import { PeerChatComponent } from '../PeerChatComponent'; +import { PeerGroup } from '../PeerGroup'; @Component({ selector: 'peer-chat-chat-box', @@ -7,10 +9,12 @@ import { PeerChatMessage } from '../PeerChatMessage'; styleUrls: ['./peer-chat-chat-box.component.scss'] }) export class PeerChatChatBoxComponent implements OnInit { + @Input() component: PeerChatComponent; @Input() isEnabled: boolean = true; @Input() isGrading: boolean = false; @Input() messages: PeerChatMessage[] = []; @Input() myWorkgroupId: number; + @Input() peerGroup: PeerGroup; @Input() response: string = ''; @Input() workgroupInfos: any = {}; workgroupInfosWithoutTeachers: any[]; diff --git a/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.html b/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.html new file mode 100644 index 00000000000..414c17f26ba --- /dev/null +++ b/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.html @@ -0,0 +1 @@ +{{ message }} diff --git a/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.spec.ts b/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.spec.ts new file mode 100644 index 00000000000..69fd9706c67 --- /dev/null +++ b/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PeerChatMemberTypingIndicatorComponent } from './peer-chat-member-typing-indicator.component'; +import { ConfigService } from '../../../services/configService'; +import { StompService } from '../../../services/stompService'; +import { PeerGroup } from '../PeerGroup'; +import { StudentDataService } from '../../../services/studentDataService'; +import { Subject } from 'rxjs'; + +class MockConfigService {} +class MockStudentDataService { + private studentWorkReceivedSource: Subject = new Subject(); + public studentWorkReceived$ = this.studentWorkReceivedSource.asObservable(); +} +describe('PeerChatMemberTypingIndicatorComponent', () => { + let component: PeerChatMemberTypingIndicatorComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [PeerChatMemberTypingIndicatorComponent], + providers: [ + { provide: ConfigService, useClass: MockConfigService }, + StompService, + { provide: StudentDataService, useClass: MockStudentDataService } + ] + }); + fixture = TestBed.createComponent(PeerChatMemberTypingIndicatorComponent); + component = fixture.componentInstance; + component.peerGroup = new PeerGroup(1, [], null); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.ts b/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.ts new file mode 100644 index 00000000000..052edbac7d3 --- /dev/null +++ b/src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.ts @@ -0,0 +1,109 @@ +import { Component, Input } from '@angular/core'; +import { StompService } from '../../../services/stompService'; +import { PeerChatComponent } from '../PeerChatComponent'; +import { PeerGroup } from '../PeerGroup'; +import { ConfigService } from '../../../services/configService'; +import { Subscription } from 'rxjs'; +import { StudentDataService } from '../../../services/studentDataService'; + +@Component({ + selector: 'peer-chat-member-typing-indicator', + templateUrl: './peer-chat-member-typing-indicator.component.html' +}) +export class PeerChatMemberTypingIndicatorComponent { + @Input() component: PeerChatComponent; + private intervalId: NodeJS.Timeout; + protected message: string; + @Input() myWorkgroupId: number; + @Input() peerGroup: PeerGroup; + private subscriptions: Subscription = new Subscription(); + private typingDurationBuffer: number = 5000; + private workgroupToLastTypingTimestamp: Map = new Map(); + + constructor( + private configService: ConfigService, + private dataService: StudentDataService, + private stompService: StompService + ) {} + + ngOnInit(): void { + this.intervalId = setInterval(() => { + this.updateMessage(); + }, 1000); + this.subscribeToIsTypingMessages(); + this.subscribeToStudentWork(); + } + + ngOnDestroy(): void { + clearInterval(this.intervalId); + this.subscriptions.unsubscribe(); + } + + private updateMessage(): void { + const workgroupsTyping = []; + this.workgroupToLastTypingTimestamp.forEach((lastTypingTimestamp, workgroupId) => { + if (new Date().getTime() - lastTypingTimestamp < this.typingDurationBuffer) { + workgroupsTyping.push(workgroupId); + } + }); + if (workgroupsTyping.length === 0) { + this.message = ''; + } else { + const classmateNames = workgroupsTyping + .map((workgroupId) => + this.configService.getStudentFirstNamesByWorkgroupId(workgroupId).join(', ') + ) + .join(', '); + this.message = classmateNames.includes(',') + ? $localize`${classmateNames} are typing...` + : $localize`${classmateNames} is typing...`; + } + } + + private subscribeToIsTypingMessages(): void { + this.subscriptions.add( + this.stompService.rxStomp + .watch(`/topic/peer-group/${this.peerGroup.id}/is-typing`) + .subscribe(({ body }) => { + const { nodeId, componentId, workgroupId } = JSON.parse(JSON.parse(body).content); + if ( + nodeId === this.component.nodeId && + componentId === this.component.id && + workgroupId !== this.myWorkgroupId + ) { + this.workgroupToLastTypingTimestamp.set(workgroupId, new Date().getTime()); + } + }) + ); + } + + private subscribeToStudentWork(): void { + this.subscriptions.add( + this.dataService.studentWorkReceived$.subscribe((componentState) => { + if (this.isMessageFromPeer(componentState)) { + this.workgroupToLastTypingTimestamp.delete(componentState.workgroupId); + this.updateMessage(); + } + }) + ); + } + + private isMessageFromPeer(componentState: any): boolean { + return ( + this.isForThisComponent(componentState) && + this.isFromClassmate(componentState) && + componentState.peerGroupId === this.peerGroup.id + ); + } + + private isForThisComponent(componentState: any): boolean { + return ( + componentState.nodeId === this.component.nodeId && + componentState.componentId === this.component.id + ); + } + + private isFromClassmate(componentState: any): boolean { + return componentState.workgroupId !== this.myWorkgroupId; + } +} diff --git a/src/assets/wise5/components/peerChat/peer-chat-message-input/peer-chat-message-input.component.ts b/src/assets/wise5/components/peerChat/peer-chat-message-input/peer-chat-message-input.component.ts index d5598ac865c..3e55083cd55 100644 --- a/src/assets/wise5/components/peerChat/peer-chat-message-input/peer-chat-message-input.component.ts +++ b/src/assets/wise5/components/peerChat/peer-chat-message-input/peer-chat-message-input.component.ts @@ -1,32 +1,63 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { StompService } from '../../../services/stompService'; +import { ConfigService } from '../../../services/configService'; +import { PeerChatComponent } from '../PeerChatComponent'; +import { PeerGroup } from '../PeerGroup'; @Component({ selector: 'peer-chat-message-input', templateUrl: './peer-chat-message-input.component.html' }) export class PeerChatMessageInputComponent implements OnInit { + @Input() component: PeerChatComponent; + private intervalId: NodeJS.Timeout; protected isSubmitEnabled: boolean = false; + private lastTypingTimestamp: number = 0; @Input() messageText: string = ''; + @Input() peerGroup: PeerGroup; @Output() responseChangedEvent: EventEmitter = new EventEmitter(); @Output('onSubmit') submit: EventEmitter = new EventEmitter(); + private typingDurationBuffer: number = 5000; - ngOnInit(): void {} + constructor(private configService: ConfigService, private stompService: StompService) {} + + ngOnInit(): void { + this.intervalId = setInterval(() => { + this.broadcastTypingStatus(); + }, 2500); + } ngOnChanges(): void { this.responseChanged(); } + ngOnDestroy(): void { + clearInterval(this.intervalId); + } + protected responseChanged(): void { this.isSubmitEnabled = this.messageText?.length > 0; this.responseChangedEvent.emit(this.messageText); } + private broadcastTypingStatus(): void { + if (new Date().getTime() - this.lastTypingTimestamp < this.typingDurationBuffer) { + this.stompService.rxStomp.publish({ + destination: `/app/api/peer-chat/${this.component.nodeId}/${this.component.id}/${ + this.peerGroup.id + }/${this.configService.getWorkgroupId()}/is-typing` + }); + } + } + protected keyPressed(event: any): void { if (event.keyCode === 13) { event.preventDefault(); if (this.isSubmitEnabled) { this.submitResponse(); } + } else { + this.lastTypingTimestamp = new Date().getTime(); } } @@ -34,6 +65,7 @@ export class PeerChatMessageInputComponent implements OnInit { this.submit.emit(this.messageText); this.messageText = ''; this.isSubmitEnabled = false; + this.lastTypingTimestamp = 0; } protected onFocus(event: any): void { diff --git a/src/assets/wise5/components/peerChat/peer-chat-student/peer-chat-student.component.html b/src/assets/wise5/components/peerChat/peer-chat-student/peer-chat-student.component.html index db585e5f0e4..3eb1fdbdc24 100644 --- a/src/assets/wise5/components/peerChat/peer-chat-student/peer-chat-student.component.html +++ b/src/assets/wise5/components/peerChat/peer-chat-student/peer-chat-student.component.html @@ -28,8 +28,10 @@
src/assets/wise5/components/conceptMap/concept-map-student/concept-map-student.component.ts - 396 + 395 @@ -14488,49 +14488,49 @@ Are you sure you want to proceed? Correctness column key: 0 = Incorrect, 1 = Correct, 2 = Correct bucket but wrong position src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.ts - 139 + 138 One Workgroup Per Row src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.ts - 162 + 161 Latest Student Work src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.ts - 164 + 163 All Student Work src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.ts - 166 + 165 Events src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.ts - 168 + 167 Raw Data src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.ts - 170 + 169 Downloading Export src/assets/wise5/classroomMonitor/dataExport/data-export/data-export.component.ts - 2027 + 1761 @@ -15822,7 +15822,7 @@ Are you ready to receive feedback on this answer? src/assets/wise5/components/conceptMap/concept-map-student/concept-map-student.component.ts - 330 + 329 @@ -16688,7 +16688,7 @@ Label: You have no more chances to receive feedback on your answer. src/assets/wise5/components/conceptMap/concept-map-student/concept-map-student.component.ts - 324 + 323 @@ -16697,7 +16697,7 @@ Label: Are you ready to receive feedback on this answer? src/assets/wise5/components/conceptMap/concept-map-student/concept-map-student.component.ts - 327 + 326 src/assets/wise5/components/openResponse/open-response-student/open-response-student.component.ts @@ -16708,11 +16708,11 @@ Are you ready to receive feedback on this answer? Feedback src/assets/wise5/components/conceptMap/concept-map-student/concept-map-student.component.ts - 405 + 404 src/assets/wise5/components/conceptMap/concept-map-student/concept-map-student.component.ts - 414 + 413 src/assets/wise5/directives/componentAnnotations/component-annotations.component.ts @@ -16723,7 +16723,7 @@ Are you ready to receive feedback on this answer? Are you sure you want to reset your work? src/assets/wise5/components/conceptMap/concept-map-student/concept-map-student.component.ts - 1413 + 1412 @@ -17277,11 +17277,11 @@ Category Name: Add Comment src/assets/wise5/components/discussion/class-response/class-response.component.html - 167 + 171 src/assets/wise5/components/discussion/class-response/class-response.component.html - 169 + 173 @@ -17351,7 +17351,7 @@ Category Name: replied to a discussion you were in! src/assets/wise5/components/discussion/discussion-student/discussion-student.component.ts - 196 + 203 @@ -20049,6 +20049,20 @@ If this problem continues, let your teacher know and move on to the next activit 5,8 + + are typing... + + src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.ts + 58 + + + + is typing... + + src/assets/wise5/components/peerChat/peer-chat-member-typing-indicator/peer-chat-member-typing-indicator.component.ts + 59 + + Chat members: