Skip to content

VIH-11190 prevent ability to call screened participant whilst an invite is pending #2345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,9 @@ export class ConferenceTestData {
description: 'Spanish',
type: InterpreterType.Verbal
}),
linked_participants: []
linked_participants: [],
external_reference_id: 'B505FA9D-8072-4F96-8CA6-4F0489DD6E08',
protect_from: []
});

const participant2 = new ParticipantResponse({
Expand All @@ -498,7 +500,9 @@ export class ConferenceTestData {
tiled_display_name: 'CIVILIAN;James Green;9F681318-4955-49AF-A887-DED64554429J',
hearing_role: HearingRole.REPRESENTATIVE,
current_room: new RoomSummaryResponse(),
linked_participants: []
linked_participants: [],
external_reference_id: '072D80ED-6816-42AF-A0C0-2FAE0F65E17A',
protect_from: []
});

const participant3 = new ParticipantResponse({
Expand All @@ -511,7 +515,9 @@ export class ConferenceTestData {
tiled_display_name: 'JUDGE;Judge Fudge;9F681318-4955-49AF-A887-DED64554429T',
hearing_role: HearingRole.JUDGE,
current_room: new RoomSummaryResponse(),
linked_participants: []
linked_participants: [],
external_reference_id: '9B4737C9-5D8A-4B67-8569-EF8185FFE6E3',
protect_from: []
});

const participant4 = new ParticipantResponse({
Expand All @@ -524,7 +530,9 @@ export class ConferenceTestData {
tiled_display_name: 'Staff Member;Staff Member;9F681318-4965-49AF-A887-DED64554429T',
hearing_role: HearingRole.STAFF_MEMBER,
current_room: new RoomSummaryResponse({ label: 'ParticipantConsultationRoom1' }),
linked_participants: []
linked_participants: [],
external_reference_id: '9B4737C9-5D8A-4B67-8569-EF8185FFE6E3',
protect_from: []
});

participants.push(participant1);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { VideoWebService } from 'src/app/services/api/video-web.service';
import { translateServiceSpy } from 'src/app/testing/mocks/mock-translation.service';

import {
ConferenceResponse,
LoggedParticipantResponse,
ParticipantStatus,
Role,
RoomSummaryResponse
} from 'src/app/services/clients/api-client';
import { ConferenceResponse, ParticipantStatus, Role, RoomSummaryResponse } from 'src/app/services/clients/api-client';
import { Logger } from 'src/app/services/logging/logger-base';
import { ConferenceTestData } from 'src/app/testing/mocks/data/conference-test-data';
import { globalConference, globalEndpoint, globalParticipant } from '../../waiting-room-shared/tests/waiting-room-base-setup';
Expand All @@ -21,7 +15,6 @@ describe('JoinPrivateConsultationComponent', () => {
let logger: jasmine.SpyObj<Logger>;
let videoWebService: jasmine.SpyObj<VideoWebService>;

let logged: LoggedParticipantResponse;
const translateService = translateServiceSpy;

beforeAll(() => {
Expand All @@ -38,12 +31,6 @@ describe('JoinPrivateConsultationComponent', () => {
});
const judge = conference.participants.find(x => x.role === Role.Judge);

logged = new LoggedParticipantResponse({
participant_id: judge.id,
display_name: judge.display_name,
role: Role.Judge
});

component = new JoinPrivateConsultationComponent(logger, translateService);
});

Expand Down Expand Up @@ -91,7 +78,8 @@ describe('JoinPrivateConsultationComponent', () => {
it('should return participant hearing role text', () => {
const expectedText = 'hearing-role.litigant-in-person';
translateService.instant.calls.reset();
expect(component.getParticipantHearingRoleText(globalParticipant)).toEqual(expectedText);
const vhParticipant = mapParticipantToVHParticipant(globalParticipant);
expect(component.getParticipantHearingRoleText(vhParticipant)).toEqual(expectedText);
});

it('should return rooms available', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ParticipantResponse } from 'src/app/services/clients/api-client';
import { Logger } from 'src/app/services/logging/logger-base';
import { VHEndpoint, VHParticipant } from '../../store/models/vh-conference';

Expand Down Expand Up @@ -113,11 +112,9 @@ export class JoinPrivateConsultationComponent {
this.selectedRoomLabel = roomLabel;
}

getParticipantHearingRoleText(participant: ParticipantResponse) {
getParticipantHearingRoleText(participant: VHParticipant) {
const translatedtext = this.translateService.instant('join-private-consultation.for');
const hearingRoleText = this.translateService.instant(
'hearing-role.' + participant.hearing_role.toLowerCase().split(' ').join('-')
);
const hearingRoleText = this.translateService.instant('hearing-role.' + participant.hearingRole.toLowerCase().split(' ').join('-'));
return participant.representee ? `${hearingRoleText} ${translatedtext} ${participant.representee}` : hearingRoleText;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,40 @@
<div class="language-icon"
*ngIf="interpreter">
<fa-icon icon="language"></fa-icon>
<div class="language-icon" *ngIf="interpreter">
<fa-icon icon="language"></fa-icon>
</div>
<div [ngClass]="{'participant-endpoint-row': interpreter == null ,
'individual-endpoint-row': interpreter != null }">
<div class="header-left" [class]="getRowClasses(participant)">
<app-invite-participant
*ngIf="!isParticipantInCurrentRoom(participant) && isParticipantAvailable(participant) && isInterpreterAvailable() && canInvite"
[participantId]="participant.id"
[conferenceId]="conferenceId"
[roomLabel]="roomLabel">
</app-invite-participant>
<fa-icon *ngIf="isParticipantInCurrentRoom(participant)"
icon="check"
aria-hidden="true"></fa-icon>
</div>
<app-private-consultation-participant-display-name
[displayName]="participant?.displayName"
[isInCurrentRoom]="isParticipantInCurrentRoom(participant)"
[isAvailable]="isParticipantAvailable(participant)">
</app-private-consultation-participant-display-name>
<app-private-consultation-participant-status
[entity]="participant"
[status]="status"
[roomLabel]="roomLabel">
</app-private-consultation-participant-status>
<div [ngClass]="{ 'participant-endpoint-row': interpreter == null, 'individual-endpoint-row': interpreter != null }">
<div class="header-left" [class]="getRowClasses(participant)">
<app-invite-participant
*ngIf="
!isParticipantInCurrentRoom(participant) &&
isParticipantAvailable(participant) &&
isInterpreterAvailable() &&
!isProtected() &&
canInvite
"
[participantId]="participant.id"
[conferenceId]="conferenceId"
[roomLabel]="roomLabel"
>
</app-invite-participant>
<fa-icon *ngIf="isParticipantInCurrentRoom(participant)" icon="check" aria-hidden="true"></fa-icon>
</div>
<app-private-consultation-participant-display-name
[displayName]="participant?.displayName"
[isInCurrentRoom]="isParticipantInCurrentRoom(participant)"
[isAvailable]="isParticipantAvailable(participant)"
>
</app-private-consultation-participant-display-name>
<app-private-consultation-participant-status [entity]="participant" [status]="status" [roomLabel]="roomLabel">
</app-private-consultation-participant-status>
</div>
<div *ngIf="interpreter"
class="individual-endpoint-row">
<div class="header-left" [class]="getRowClasses(interpreter)"> </div>
<app-private-consultation-participant-display-name
[displayName]="interpreter?.displayName"
[isInCurrentRoom]="isParticipantInCurrentRoom(interpreter)"
[isAvailable]="isParticipantAvailable(interpreter)">
</app-private-consultation-participant-display-name>
<app-private-consultation-participant-status
[entity]="interpreter"
[status]="status"
[roomLabel]="roomLabel">
</app-private-consultation-participant-status>
<div *ngIf="interpreter" class="individual-endpoint-row">
<div class="header-left" [class]="getRowClasses(interpreter)"></div>
<app-private-consultation-participant-display-name
[displayName]="interpreter?.displayName"
[isInCurrentRoom]="isParticipantInCurrentRoom(interpreter)"
[isAvailable]="isParticipantAvailable(interpreter)"
>
</app-private-consultation-participant-display-name>
<app-private-consultation-participant-status [entity]="interpreter" [status]="status" [roomLabel]="roomLabel">
</app-private-consultation-participant-status>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,22 @@ describe('ParticipantItemComponent', () => {
expect(result).toBeFalse();
});
});

describe('isProtected', () => {
it('should return true when participant is protected', () => {
const participant = mapParticipantToVHParticipant(conference.participants[0]);
component.participant = participant;
component.participantCallStatuses[participant.id] = 'Protected';
const result = component.isProtected();
expect(result).toBeTrue();
});

it('should return false when participant is not protected', () => {
const participant = mapParticipantToVHParticipant(conference.participants[0]);
component.participant = participant;
component.participantCallStatuses[participant.id] = null;
const result = component.isProtected();
expect(result).toBeFalse();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ export class ParticipantItemComponent {

return this.isParticipantAvailable(this.interpreter);
}

isProtected(): boolean {
return this.participantCallStatuses[this.participant.id] === 'Protected';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,13 @@ describe('PrivateConsultationParticipantsComponent', () => {

it('should set answer on response message', () => {
component.roomLabel = 'Room1';
const participantId1 = conference.participants.find(x => x.role === Role.Individual).id;
consultationRequestResponseMessageSubjectMock.next(
new ConsultationRequestResponseMessage(conference.id, invitationId, 'Room1', 'Participant1', ConsultationAnswer.Rejected)
new ConsultationRequestResponseMessage(conference.id, invitationId, 'Room1', participantId1, ConsultationAnswer.Rejected)
);

// Assert
expect(component.participantCallStatuses['Participant1']).toBe('Rejected');
expect(component.participantCallStatuses[participantId1]).toBe('Rejected');
});

it('should not set answer if different room', () => {
Expand Down Expand Up @@ -226,31 +227,34 @@ describe('PrivateConsultationParticipantsComponent', () => {

it('should set answer on response message then reset after timeout', fakeAsync(() => {
component.roomLabel = 'Room1';
const participantid = conference.participants.find(x => x.role === Role.Individual).id;
consultationRequestResponseMessageSubjectMock.next(
new ConsultationRequestResponseMessage(conference.id, invitationId, 'Room1', 'Participant1', ConsultationAnswer.Rejected)
new ConsultationRequestResponseMessage(conference.id, invitationId, 'Room1', participantid, ConsultationAnswer.Rejected)
);
flushMicrotasks();

// Assert
expect(component.participantCallStatuses['Participant1']).toBe('Rejected');
expect(component.participantCallStatuses[participantid]).toBe('Rejected');
tick(10000);
expect(component.participantCallStatuses['Participant1']).toBeNull();
expect(component.participantCallStatuses[participantid]).toBeNull();
}));

it('should a 2nd call after answering should prevent timeout call', fakeAsync(() => {
component.roomLabel = 'Room1';
const participantid1 = conference.participants.find(x => x.role === Role.Individual).id;
const participantid2 = conference.participants.find(x => x.role === Role.Representative).id;
consultationRequestResponseMessageSubjectMock.next(
new ConsultationRequestResponseMessage(conference.id, invitationId, 'Room1', 'Participant1', ConsultationAnswer.Rejected)
new ConsultationRequestResponseMessage(conference.id, invitationId, 'Room1', participantid1, ConsultationAnswer.Rejected)
);
flushMicrotasks();
tick(2000);
requestedConsultationMessageSubjectMock.next(
new RequestedConsultationMessage(conference.id, invitationId, 'Room1', 'Participant2', 'Participant1')
new RequestedConsultationMessage(conference.id, invitationId, 'Room1', participantid2, participantid1)
);
tick(9000);

// Assert
expect(component.participantCallStatuses['Participant1']).toBe('Calling');
expect(component.participantCallStatuses[participantid1]).toBe('Calling');
}));

it('should not set calling if different room', () => {
Expand All @@ -275,15 +279,17 @@ describe('PrivateConsultationParticipantsComponent', () => {

it('should reset participant call status on status message', () => {
component.roomLabel = 'Room1';
const participantid1 = conference.participants.find(x => x.role === Role.Individual).id;
const participantid2 = conference.participants.find(x => x.role === Role.Representative).id;
requestedConsultationMessageSubjectMock.next(
new RequestedConsultationMessage(conference.id, invitationId, 'Room1', 'Participant2', 'Participant1')
new RequestedConsultationMessage(conference.id, invitationId, 'Room1', participantid2, participantid1)
);
participantStatusSubjectMock.next(
new ParticipantStatusMessage('Participant1', 'Username', conference.id, ParticipantStatus.Disconnected)
new ParticipantStatusMessage(participantid1, 'Username', conference.id, ParticipantStatus.Disconnected)
);

// Assert
expect(component.participantCallStatuses['Participant1']).toBeNull();
expect(component.participantCallStatuses[participantid1]).toBeNull();
});

it('should get participant status', () => {
Expand Down Expand Up @@ -811,4 +817,28 @@ describe('PrivateConsultationParticipantsComponent', () => {
expect(result).toBeFalse();
});
});

describe('setParticipantCallStatus', () => {
it('should set the participant call status and that of participants protected by the participant', () => {
const participant = conference.participants.find(x => x.role === Role.Individual);
const protectedParticipant = conference.participants.find(x => x.role === Role.Representative);
participant.protectedFrom = [protectedParticipant.externalReferenceId];

component.setParticipantCallStatus(participant.id, 'Calling', 'Restricted');

expect(component.participantCallStatuses[participant.id]).toBe('Calling');
expect(component.participantCallStatuses[protectedParticipant.id]).toBe('Restricted');
});

it('should set the participant call status and that of participants protected by the participant', () => {
const participant = conference.participants.find(x => x.role === Role.Individual);
const protectedParticipant = conference.participants.find(x => x.role === Role.Representative);
protectedParticipant.protectedFrom = [participant.externalReferenceId];

component.setParticipantCallStatus(participant.id, 'Calling', 'Protected');

expect(component.participantCallStatuses[participant.id]).toBe('Calling');
expect(component.participantCallStatuses[protectedParticipant.id]).toBe('Protected');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ export class PrivateConsultationParticipantsComponent extends WRParticipantStatu
this.eventHubSubscriptions$.add(
this.eventService.getConsultationRequestResponseMessage().subscribe(message => {
if (message.roomLabel === this.roomLabel && message.conferenceId === this.conference.id) {
this.participantCallStatuses[message.requestedFor] = message.answer;
this.setParticipantCallStatus(message.requestedFor, message.answer, null);
setTimeout(() => {
if (this.participantCallStatuses[message.requestedFor] === message.answer) {
this.participantCallStatuses[message.requestedFor] = null;
this.setParticipantCallStatus(message.requestedFor, null, null);
}
}, 10000);
}
Expand All @@ -74,7 +74,7 @@ export class PrivateConsultationParticipantsComponent extends WRParticipantStatu
// Set 'Calling...'
// No need to timeout here the text because when the notification times out it will send another event.
if (message.roomLabel === this.roomLabel && message.conferenceId === this.conference.id) {
this.participantCallStatuses[message.requestedFor] = 'Calling';
this.setParticipantCallStatus(message.requestedFor, 'Calling', 'Protected');
}
})
);
Expand All @@ -83,11 +83,28 @@ export class PrivateConsultationParticipantsComponent extends WRParticipantStatu
this.eventHubSubscriptions$.add(
this.eventService.getParticipantStatusMessage().subscribe(message => {
// If the participant state changes reset the state.
this.participantCallStatuses[message.participantId] = null;
this.setParticipantCallStatus(message.participantId, null, null);
})
);
}

setParticipantCallStatus(participantId: string, status, protectedFromStatus): void {
// Update the call status for the given participant
this.participantCallStatuses[participantId] = status;

// Find the participant with the given ID
const participant = this.nonJudgeParticipants.find(p => p.id === participantId);
// for each non-judge participant, if the participant is on their protected from list, disable the call button
this.nonJudgeParticipants.forEach(p => {
if (p.protectedFrom.includes(participant?.externalReferenceId)) {
this.participantCallStatuses[p.id] = protectedFromStatus;
}
if (participant.protectedFrom.includes(p?.externalReferenceId)) {
this.participantCallStatuses[p.id] = protectedFromStatus;
}
});
}

canCallEndpoint(endpoint: VHEndpoint): boolean {
return (
!this.isParticipantInCurrentRoom(endpoint) &&
Expand Down
Loading