Skip to content
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

Communication: Add shortcut to direct chats via usernames in conversation detail dialog #10277

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ <h4 class="modal-title">
<span>
@if (getAsChannel(activeConversation); as channel) {
<jhi-channel-icon [isPublic]="channel.isPublic!" [isAnnouncementChannel]="channel.isAnnouncementChannel!" />
}
@if (getAsGroupChat(activeConversation)) {
} @else if (getAsGroupChat(activeConversation)) {
<fa-icon [icon]="faPeopleGroup" size="xs" />
} @else {
<jhi-profile-picture
imageSizeInRem="2"
fontSizeInRem="0.8"
imageId="user-profile-picture"
defaultPictureId="user-default-profile-picture"
[authorId]="otherUser?.id"
[authorName]="otherUser?.name"
[imageUrl]="otherUser?.imageUrl"
/>
}
{{ conversationService.getConversationName(activeConversation, true) }}
</span>
Expand Down Expand Up @@ -37,16 +46,16 @@ <h4 class="modal-title">
jhiTranslate="artemisApp.dialogs.conversationDetail.tabs.info"
></a>
</li>
<li class="nav-item members-tab">
<a
class="nav-link"
[class.active]="selectedTab === Tabs.MEMBERS"
role="button"
(click)="selectedTab = Tabs.MEMBERS"
jhiTranslate="artemisApp.dialogs.conversationDetail.tabs.members"
></a>
</li>
@if (!isOneToOneChat(activeConversation)) {
@if (!isOneToOneChat) {
<li class="nav-item members-tab">
<a
class="nav-link"
[class.active]="selectedTab === Tabs.MEMBERS"
role="button"
(click)="selectedTab = Tabs.MEMBERS"
jhiTranslate="artemisApp.dialogs.conversationDetail.tabs.members"
></a>
</li>
<li class="nav-item settings-tab">
<a
class="nav-link"
Expand All @@ -62,7 +71,12 @@ <h4 class="modal-title">
<div class="modal-body">
@switch (selectedTab) {
@case (Tabs.MEMBERS) {
<jhi-conversation-members [course]="course" [activeConversationInput]="activeConversation" (changesPerformed)="changesWerePerformed = true" />
<jhi-conversation-members
[course]="course"
[activeConversationInput]="activeConversation"
(changesPerformed)="changesWerePerformed = true"
(userNameClicked)="onUserNameClicked($event)"
/>
}
@case (Tabs.INFO) {
<jhi-conversation-info [activeConversation]="activeConversation" [course]="course" (changesPerformed)="changesWerePerformed = true" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Component, Input, inject } from '@angular/core';
import { Component, Input, inject, output } from '@angular/core';
import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model';
import { Course } from 'app/entities/course.model';
import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model';
import { ConversationService } from 'app/shared/metis/conversations/conversation.service';
import { isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model';
import { getAsOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model';
import { getAsGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model';
import { AbstractDialogComponent } from 'app/overview/course-conversations/dialogs/abstract-dialog.component';
import { faPeopleGroup } from '@fortawesome/free-solid-svg-icons';
Expand All @@ -14,6 +14,8 @@ import { RouterLink } from '@angular/router';
import { ConversationMembersComponent } from './tabs/conversation-members/conversation-members.component';
import { ConversationInfoComponent } from './tabs/conversation-info/conversation-info.component';
import { ConversationSettingsComponent } from './tabs/conversation-settings/conversation-settings.component';
import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component';
import { ConversationUserDTO } from 'app/entities/metis/conversation/conversation-user-dto.model';

export enum ConversationDetailTabs {
MEMBERS = 'members',
Expand All @@ -24,7 +26,16 @@ export enum ConversationDetailTabs {
@Component({
selector: 'jhi-conversation-detail-dialog',
templateUrl: './conversation-detail-dialog.component.html',
imports: [ChannelIconComponent, FaIconComponent, TranslateDirective, RouterLink, ConversationMembersComponent, ConversationInfoComponent, ConversationSettingsComponent],
imports: [
ChannelIconComponent,
FaIconComponent,
TranslateDirective,
RouterLink,
ConversationMembersComponent,
ConversationInfoComponent,
ConversationSettingsComponent,
ProfilePictureComponent,
],
})
export class ConversationDetailDialogComponent extends AbstractDialogComponent {
conversationService = inject(ConversationService);
Expand All @@ -34,13 +45,22 @@ export class ConversationDetailDialogComponent extends AbstractDialogComponent {
@Input() selectedTab: ConversationDetailTabs = ConversationDetailTabs.MEMBERS;

isInitialized = false;
isOneToOneChat = false;
otherUser?: ConversationUserDTO;
readonly faPeopleGroup = faPeopleGroup;
readonly userNameClicked = output<number>();

initialize() {
super.initialize(['course', 'activeConversation', 'selectedTab']);
if (this.activeConversation) {
const conversation = getAsOneToOneChatDTO(this.activeConversation);
if (conversation) {
this.isOneToOneChat = true;
this.otherUser = conversation.members?.find((user) => !user.isRequestingUser);
}
}
}
asliayk marked this conversation as resolved.
Show resolved Hide resolved

isOneToOneChat = isOneToOneChatDTO;
getAsChannel = getAsChannelDTO;
getAsGroupChat = getAsGroupChatDTO;

Expand Down Expand Up @@ -76,4 +96,8 @@ export class ConversationDetailDialogComponent extends AbstractDialogComponent {
this.changesWerePerformed = true;
this.clear();
}

onUserNameClicked(userId: number) {
this.userNameClicked.emit(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@if (isChannel(activeConversation()!) && conversationMember()?.isChannelModerator) {
<fa-icon [icon]="faUserGear" [ngbTooltip]="'artemisApp.dialogs.conversationDetail.memberTab.memberRow.channelModeratorTooltip' | artemisTranslate" />
}
{{ userLabel }}
<a (click)="userNameClicked()" class="bs-body-color" [ngClass]="{ disabled: isCurrentUser }"> {{ userLabel }} </a>
@if (!conversationMember()?.isStudent) {
<fa-icon class="ms-1 text-secondary" [icon]="userIcon" [ngbTooltip]="userTooltip" />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
content: none;
}
}

.bs-body-color {
color: var(--bs-body-color);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-pict
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateDirective } from 'app/shared/language/translate.directive';
import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
import { NgClass } from '@angular/common';

@Component({
selector: '[jhi-conversation-member-row]',
Expand All @@ -40,6 +41,7 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
NgbDropdownItem,
TranslateDirective,
ArtemisTranslatePipe,
NgClass,
],
})
export class ConversationMemberRowComponent implements OnInit, OnDestroy {
Expand All @@ -49,6 +51,7 @@ export class ConversationMemberRowComponent implements OnInit, OnDestroy {
course = input<Course>();
changePerformed = output<void>();
conversationMember = input<ConversationUserDTO>();
readonly onUserNameClicked = output<number>();

idOfLoggedInUser: number;

Expand Down Expand Up @@ -297,4 +300,11 @@ export class ConversationMemberRowComponent implements OnInit, OnDestroy {
this.userTooltip = this.translateService.instant(toolTipTranslationPath + 'student');
}
}

userNameClicked() {
const memberId = this.conversationMember()?.id;
if (memberId) {
this.onUserNameClicked.emit(memberId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
[activeConversation]="activeConversation()!"
[course]="course()"
(changePerformed)="onChangePerformed()"
(onUserNameClicked)="userNameClicked.emit($event)"
></li>
}
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class ConversationMembersComponent implements OnInit, OnDestroy {
activeConversationInput = input.required<ConversationDTO>();
activeConversation = signal<ConversationDTO | undefined>(undefined);
changesPerformed = output<void>();
readonly userNameClicked = output<number>();

canAddUsersToConversation = canAddUsersToConversation;
getAsChannel = getAsChannelDTO;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,28 @@ export class ConversationHeaderComponent implements OnInit, OnChanges, OnDestroy
modalRef.componentInstance.course = this.course;
modalRef.componentInstance.activeConversation = this.activeConversation;
modalRef.componentInstance.selectedTab = tab;
if (this.getAsOneToOneChat(this.activeConversation)) {
modalRef.componentInstance.selectedTab = ConversationDetailTabs.INFO;
}
modalRef.componentInstance.initialize();

const userNameClicked = modalRef.componentInstance.userNameClicked;
if (userNameClicked) {
const subscription = userNameClicked.subscribe((username: number) => {
modalRef.dismiss();
this.metisConversationService
.createOneToOneChatWithId(username)
.pipe(
catchError((error) => {
return EMPTY;
}),
)
.subscribe();
});

modalRef.closed.subscribe(() => subscription.unsubscribe());
}

from(modalRef.result)
.pipe(
catchError(() => EMPTY),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { ConversationService } from 'app/shared/metis/conversations/conversation
import { ChannelDTO } from 'app/entities/metis/conversation/channel.model';
import { generateExampleChannelDTO, generateExampleGroupChatDTO, generateOneToOneChatDTO } from '../../helpers/conversationExampleModels';
import { initializeDialog } from '../dialog-test-helpers';
import { isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model';
import { By } from '@angular/platform-browser';
import { TranslateDirective } from 'app/shared/language/translate.directive';
import { provideHttpClient } from '@angular/common/http';
Expand Down Expand Up @@ -64,7 +63,7 @@ examples.forEach((activeConversation) => {
});

it('should not show the settings tab for one-to-one chats', () => {
if (isOneToOneChatDTO(activeConversation)) {
if (component.isOneToOneChat) {
expect(fixture.nativeElement.querySelector('.settings-tab')).toBeFalsy();
} else {
expect(fixture.nativeElement.querySelector('.settings-tab')).toBeTruthy();
Expand Down Expand Up @@ -102,7 +101,7 @@ examples.forEach((activeConversation) => {
});

it('should react correctly to events from settings tab', () => {
if (!isOneToOneChatDTO(activeConversation)) {
if (!component.isOneToOneChat) {
component.selectedTab = ConversationDetailTabs.SETTINGS;
fixture.detectChanges();

Expand Down Expand Up @@ -163,5 +162,12 @@ examples.forEach((activeConversation) => {
expect(closeSpy).toHaveBeenCalledTimes(1);
expect(dismissSpy).not.toHaveBeenCalled();
});

it('should emit userNameClicked event when onUserNameClicked is called', () => {
const testUserId = 42;
const spy = jest.spyOn(component.userNameClicked, 'emit');
component.onUserNameClicked(testUserId);
expect(spy).toHaveBeenCalledWith(testUserId);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { By } from '@angular/platform-browser';
import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component';
import { input, runInInjectionContext } from '@angular/core';
import { TranslateDirective } from 'app/shared/language/translate.directive';
import { faUser, faUserCheck, faUserGraduate } from '@fortawesome/free-solid-svg-icons';

const memberTemplate = {
id: 1,
Expand Down Expand Up @@ -57,6 +58,7 @@ examples.forEach((activeConversation) => {
const canGrantChannelModeratorRole = jest.fn();
const canRevokeChannelModeratorRole = jest.fn();
const canRemoveUsersFromConversation = jest.fn();
let translateService: TranslateService;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
Expand Down Expand Up @@ -101,6 +103,8 @@ examples.forEach((activeConversation) => {
component.canRevokeChannelModeratorRole = canRevokeChannelModeratorRole;
component.canGrantChannelModeratorRole = canGrantChannelModeratorRole;
component.canRemoveUsersFromConversation = canRemoveUsersFromConversation;
translateService = TestBed.inject(TranslateService) as TranslateService;
jest.spyOn(translateService, 'instant').mockImplementation((key: string) => key);
});

afterEach(() => {
Expand Down Expand Up @@ -275,6 +279,71 @@ examples.forEach((activeConversation) => {
expect(changesPerformedSpy).toHaveBeenCalledOnce();
}
}));

it('should emit userId when another user clicks the name', fakeAsync(() => {
fixture.detectChanges();
tick();
fixture.detectChanges();

component.idOfLoggedInUser = loggedInUser.id!;
const userNameClickedSpy = jest.spyOn(component.onUserNameClicked, 'emit');
component.userNameClicked();

expect(userNameClickedSpy).toHaveBeenCalledWith(conversationMember.id);
}));

it('should set isCurrentUser to true if conversation member is the logged-in user', fakeAsync(() => {
loggedInUser.id = conversationMember.id!;

fixture.detectChanges();
tick();
fixture.detectChanges();

expect(component.isCurrentUser).toBeTrue();
}));

it('should set isCurrentUser to false if conversation member is NOT the logged-in user', fakeAsync(() => {
loggedInUser.id = 999;

fixture.detectChanges();
tick();
fixture.detectChanges();

expect(component.isCurrentUser).toBeFalse();
}));

it('should prevent removal action if the user is the current user', fakeAsync(() => {
loggedInUser.id = conversationMember.id!;

fixture.detectChanges();
tick();
fixture.detectChanges();

expect(component.canBeRemovedFromConversation).toBeFalse();
}));

it.each`
role | isInstructor | isEditor | isTeachingAssistant | expectedIcon | expectedTooltip
${'instructor'} | ${true} | ${false} | ${false} | ${faUserGraduate} | ${'artemisApp.metis.userAuthorityTooltips.instructor'}
${'editor (tutor)'} | ${false} | ${true} | ${false} | ${faUserCheck} | ${'artemisApp.metis.userAuthorityTooltips.tutor'}
${'teachingAssistant (tutor)'} | ${false} | ${false} | ${true} | ${faUserCheck} | ${'artemisApp.metis.userAuthorityTooltips.tutor'}
${'regular student (default)'} | ${false} | ${false} | ${false} | ${faUser} | ${'artemisApp.metis.userAuthorityTooltips.student'}
`('should set correct icon and tooltip when role = $role', ({ isInstructor, isEditor, isTeachingAssistant, expectedIcon, expectedTooltip }) => {
const updatedMember: ConversationUserDTO = {
id: 123,
isInstructor,
isEditor,
isTeachingAssistant,
} as ConversationUserDTO;

runInInjectionContext(fixture.debugElement.injector, () => {
component.conversationMember = input<ConversationUserDTO>(updatedMember);
component.setUserAuthorityIconAndTooltip();
expect(component.userIcon).toBe(expectedIcon);
expect(component.userTooltip).toBe(expectedTooltip);
});
});

function genericConfirmationDialogTest(method: (event: MouseEvent) => void) {
const modalService = TestBed.inject(NgbModal);
const mockModalRef = {
Expand Down
Loading
Loading