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 recents section to sidebar #10033

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -33,7 +33,6 @@
(onBrowsePressed)="openChannelOverviewDialog()"
(onDirectChatPressed)="openCreateOneToOneChatDialog()"
(onGroupChatPressed)="openCreateGroupChatDialog()"
[showAddOption]="CHANNEL_TYPE_SHOW_ADD_OPTION"
[channelTypeIcon]="CHANNEL_TYPE_ICON"
[sidebarItemAlwaysShow]="DEFAULT_SHOW_ALWAYS"
[collapseState]="DEFAULT_COLLAPSE_STATE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PageType, SortDirection } from 'app/shared/metis/metis.util';
import {
faBan,
faBookmark,
faClock,
faComment,
faComments,
faFile,
Expand All @@ -27,7 +28,7 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { ButtonType } from 'app/shared/components/button.component';
import { CourseWideSearchComponent, CourseWideSearchConfig } from 'app/overview/course-conversations/course-wide-search/course-wide-search.component';
import { AccordionGroups, ChannelAccordionShowAdd, ChannelTypeIcons, CollapseState, SidebarCardElement, SidebarData, SidebarItemShowAlways } from 'app/types/sidebar';
import { AccordionGroups, ChannelTypeIcons, CollapseState, SidebarCardElement, SidebarData, SidebarItemShowAlways } from 'app/types/sidebar';
import { CourseOverviewService } from 'app/overview/course-overview.service';
import { GroupChatCreateDialogComponent } from 'app/overview/course-conversations/dialogs/group-chat-create-dialog/group-chat-create-dialog.component';
import { defaultFirstLayerDialogOptions, defaultSecondLayerDialogOptions } from 'app/overview/course-conversations/other/conversation.util';
Expand All @@ -44,6 +45,7 @@ import { canCreateChannel } from 'app/shared/metis/conversations/conversation-pe

const DEFAULT_CHANNEL_GROUPS: AccordionGroups = {
favoriteChannels: { entityData: [] },
recently: { entityData: [] },
generalChannels: { entityData: [] },
exerciseChannels: { entityData: [] },
lectureChannels: { entityData: [] },
Expand All @@ -52,18 +54,6 @@ const DEFAULT_CHANNEL_GROUPS: AccordionGroups = {
savedPosts: { entityData: [] },
};

const CHANNEL_TYPE_SHOW_ADD_OPTION: ChannelAccordionShowAdd = {
generalChannels: true,
exerciseChannels: true,
examChannels: true,
groupChats: true,
directMessages: true,
favoriteChannels: false,
lectureChannels: true,
hiddenChannels: false,
savedPosts: false,
};

const CHANNEL_TYPE_ICON: ChannelTypeIcons = {
generalChannels: faMessage,
exerciseChannels: faList,
Expand All @@ -74,6 +64,7 @@ const CHANNEL_TYPE_ICON: ChannelTypeIcons = {
lectureChannels: faFile,
hiddenChannels: faBan,
savedPosts: faBookmark,
recently: faClock,
};

const DEFAULT_COLLAPSE_STATE: CollapseState = {
Expand All @@ -86,6 +77,7 @@ const DEFAULT_COLLAPSE_STATE: CollapseState = {
lectureChannels: true,
hiddenChannels: true,
savedPosts: true,
recently: true,
};

const DEFAULT_SHOW_ALWAYS: SidebarItemShowAlways = {
Expand All @@ -98,6 +90,7 @@ const DEFAULT_SHOW_ALWAYS: SidebarItemShowAlways = {
lectureChannels: false,
hiddenChannels: false,
savedPosts: true,
recently: true,
};

@Component({
Expand Down Expand Up @@ -135,7 +128,6 @@ export class CourseConversationsComponent implements OnInit, OnDestroy {
openThreadOnFocus = false;
selectedSavedPostStatus: null | SavedPostStatus = null;

readonly CHANNEL_TYPE_SHOW_ADD_OPTION = CHANNEL_TYPE_SHOW_ADD_OPTION;
readonly CHANNEL_TYPE_ICON = CHANNEL_TYPE_ICON;
readonly DEFAULT_COLLAPSE_STATE = DEFAULT_COLLAPSE_STATE;
protected readonly DEFAULT_SHOW_ALWAYS = DEFAULT_SHOW_ALWAYS;
Expand Down Expand Up @@ -409,8 +401,10 @@ export class CourseConversationsComponent implements OnInit, OnDestroy {
prepareSidebarData() {
this.metisConversationService.forceRefresh().subscribe({
complete: () => {
this.sidebarConversations = this.courseOverviewService.mapConversationsToSidebarCardElements(this.conversationsOfUser);
this.accordionConversationGroups = this.courseOverviewService.groupConversationsByChannelType(this.conversationsOfUser, this.messagingEnabled);
this.sidebarConversations = this.courseOverviewService.mapConversationsToSidebarCardElements(this.course!, this.conversationsOfUser);
this.accordionConversationGroups = this.courseOverviewService.groupConversationsByChannelType(this.course!, this.conversationsOfUser, this.messagingEnabled);
const currentConversations = this.sidebarConversations?.filter((item) => item.isCurrent) || [];
this.accordionConversationGroups.recently.entityData = currentConversations;
this.updateSidebarData();
},
});
Expand Down
68 changes: 53 additions & 15 deletions src/main/webapp/app/overview/course-overview.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model
import { ConversationService } from 'app/shared/metis/conversations/conversation.service';
import { StudentExam } from 'app/entities/student-exam.model';
import { SavedPostStatusMap } from 'app/entities/metis/posting.model';
import { Course } from 'app/entities/course.model';

const DEFAULT_UNIT_GROUPS: AccordionGroups = {
future: { entityData: [] },
Expand Down Expand Up @@ -58,6 +59,7 @@ const GROUP_DECISION_MATRIX: Record<StartDateGroup, Record<EndDateGroup, TimeGro

const DEFAULT_CHANNEL_GROUPS: AccordionGroups = {
favoriteChannels: { entityData: [] },
recently: { entityData: [] },
generalChannels: { entityData: [] },
exerciseChannels: { entityData: [] },
lectureChannels: { entityData: [] },
Expand Down Expand Up @@ -169,20 +171,26 @@ export class CourseOverviewService {
return 'future';
}

getConversationGroup(conversation: ConversationDTO): ChannelGroupCategory {
getConversationGroup(conversation: ConversationDTO): ChannelGroupCategory[] {
const groups: ChannelGroupCategory[] = [];

if (conversation.isFavorite) {
return 'favoriteChannels';
groups.push('favoriteChannels');
}
if (conversation.isHidden) {
return 'hiddenChannels';
groups.push('hiddenChannels');
}

if (isGroupChatDTO(conversation)) {
return 'groupChats';
}
if (isOneToOneChatDTO(conversation)) {
return 'directMessages';
groups.push('groupChats');
} else if (isOneToOneChatDTO(conversation)) {
groups.push('directMessages');
} else {
const subTypeGroup = this.getCorrespondingChannelSubType(getAsChannelDTO(conversation)?.subType);
groups.push(subTypeGroup);
}
return this.getCorrespondingChannelSubType(getAsChannelDTO(conversation)?.subType);

return groups;
}

getCorrespondingChannelSubType(channelSubType: ChannelSubType | undefined): ChannelGroupCategory {
Expand Down Expand Up @@ -219,7 +227,7 @@ export class CourseOverviewService {
return groupedLectureGroups;
}

groupConversationsByChannelType(conversations: ConversationDTO[], messagingEnabled: boolean): AccordionGroups {
groupConversationsByChannelType(course: Course, conversations: ConversationDTO[], messagingEnabled: boolean): AccordionGroups {
const channelGroups = messagingEnabled ? { ...DEFAULT_CHANNEL_GROUPS, groupChats: { entityData: [] }, directMessages: { entityData: [] } } : DEFAULT_CHANNEL_GROUPS;
const groupedConversationGroups = cloneDeep(channelGroups) as AccordionGroups;

Expand Down Expand Up @@ -251,11 +259,21 @@ export class CourseOverviewService {
};

for (const conversation of conversations) {
const conversationGroup = this.getConversationGroup(conversation);
const conversationCardItem = this.mapConversationToSidebarCardElement(conversation);
groupedConversationGroups[conversationGroup].entityData.push(conversationCardItem);
const conversationGroups = this.getConversationGroup(conversation);
const conversationCardItem = this.mapConversationToSidebarCardElement(course, conversation);

for (const group of conversationGroups) {
groupedConversationGroups[group].entityData.push(conversationCardItem);
}
}

for (const group in groupedConversationGroups) {
groupedConversationGroups[group].entityData.sort((a, b) => {
const aIsFavorite = a.conversation?.isFavorite ? 1 : 0;
const bIsFavorite = b.conversation?.isFavorite ? 1 : 0;
return bIsFavorite - aIsFavorite;
});
}
return groupedConversationGroups;
}

Expand All @@ -273,8 +291,8 @@ export class CourseOverviewService {
return exams.map((exam, index) => this.mapExamToSidebarCardElement(exam, studentExams?.[index]));
}

mapConversationsToSidebarCardElements(conversations: ConversationDTO[]) {
return conversations.map((conversation) => this.mapConversationToSidebarCardElement(conversation));
mapConversationsToSidebarCardElements(course: Course, conversations: ConversationDTO[]) {
return conversations.map((conversation) => this.mapConversationToSidebarCardElement(course, conversation));
}

mapLectureToSidebarCardElement(lecture: Lecture): SidebarCardElement {
Expand Down Expand Up @@ -349,14 +367,34 @@ export class CourseOverviewService {
}
}

mapConversationToSidebarCardElement(conversation: ConversationDTO): SidebarCardElement {
mapConversationToSidebarCardElement(course: Course, conversation: ConversationDTO): SidebarCardElement {
let isCurrent = false;
const channelDTO = getAsChannelDTO(conversation);
const subTypeRefId = channelDTO?.subTypeReferenceId;
const now = dayjs();
const oneAndHalfWeekBefore = now.subtract(1.5, 'week');
const oneAndHalfWeekLater = now.add(1.5, 'week');
let dueDate = null;
if (subTypeRefId && course.exercises && channelDTO?.subType === 'exercise') {
const exercise = course.exercises.find((exercise) => exercise.id === subTypeRefId);
dueDate = exercise?.dueDate || null;
} else if (subTypeRefId && course.lectures && channelDTO?.subType === 'lecture') {
const lecture = course.lectures.find((lecture) => lecture.id === subTypeRefId);
dueDate = lecture?.startDate || null;
} else if (subTypeRefId && course.exams && channelDTO?.subType === 'exam') {
const exam = course.exams.find((exam) => exam.id === subTypeRefId);
dueDate = exam?.startDate || null;
}
isCurrent = dueDate ? dayjs(dueDate).isBetween(oneAndHalfWeekBefore, oneAndHalfWeekLater, 'day', '[]') : false;

asliayk marked this conversation as resolved.
Show resolved Hide resolved
const conversationCardItem: SidebarCardElement = {
title: this.conversationService.getConversationName(conversation) ?? '',
id: conversation.id ?? '',
type: conversation.type,
icon: this.getChannelIcon(conversation),
conversation: conversation,
size: 'S',
isCurrent: isCurrent,
};
return conversationCardItem;
}
Expand Down
3 changes: 1 addition & 2 deletions src/main/webapp/app/shared/sidebar/sidebar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { faFilter, faFilterCircleXmark, faHashtag, faPeopleGroup, faPlusCircle,
import { ActivatedRoute, Params } from '@angular/router';
import { Subscription, distinctUntilChanged } from 'rxjs';
import { ProfileService } from '../layouts/profiles/profile.service';
import { ChannelAccordionShowAdd, ChannelTypeIcons, CollapseState, SidebarCardSize, SidebarData, SidebarItemShowAlways, SidebarTypes } from 'app/types/sidebar';
import { ChannelTypeIcons, CollapseState, SidebarCardSize, SidebarData, SidebarItemShowAlways, SidebarTypes } from 'app/types/sidebar';
asliayk marked this conversation as resolved.
Show resolved Hide resolved
import { SidebarEventService } from './sidebar-event.service';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { cloneDeep } from 'lodash-es';
Expand Down Expand Up @@ -32,7 +32,6 @@ export class SidebarComponent implements OnDestroy, OnChanges, OnInit {
@Input() sidebarData: SidebarData;
@Input() courseId?: number;
@Input() itemSelected?: boolean;
@Input() showAddOption?: ChannelAccordionShowAdd;
@Input() channelTypeIcon?: ChannelTypeIcons;
@Input() collapseState: CollapseState;
sidebarItemAlwaysShow = input.required<SidebarItemShowAlways>();
Expand Down
4 changes: 3 additions & 1 deletion src/main/webapp/app/types/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type AccordionGroups = Record<
>;
export type ChannelGroupCategory =
| 'favoriteChannels'
| 'recently'
| 'generalChannels'
| 'exerciseChannels'
| 'lectureChannels'
Expand All @@ -27,7 +28,6 @@ export type ChannelGroupCategory =
export type CollapseState = {
[key: string]: boolean;
} & (Record<TimeGroupCategory, boolean> | Record<ChannelGroupCategory, boolean> | Record<ExamGroupCategory, boolean> | Record<TutorialGroupCategory, boolean>);
export type ChannelAccordionShowAdd = Record<ChannelGroupCategory, boolean>;
export type ChannelTypeIcons = Record<ChannelGroupCategory, IconProp>;
export type SidebarItemShowAlways = {
[key: string]: boolean;
Expand Down Expand Up @@ -135,4 +135,6 @@ export interface SidebarCardElement {
* Set for Conversation. Will be removed after refactoring
*/
conversation?: ConversationDTO;

isCurrent?: boolean;
}
1 change: 1 addition & 0 deletions src/main/webapp/i18n/de/student-dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"createDirectChat": "Direkt-Chat erstellen",
"groupChats": "Gruppenchats",
"directMessages": "Direktnachrichten",
"recently": "Kürzlich",
"filterConversationPlaceholder": "Konversationen filtern"
},
"menu": {
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/i18n/en/student-dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"createDirectChat": "Create direct chat",
"groupChats": "Group Chats",
"directMessages": "Direct Messages",
"recently": "Recently",
"filterConversationPlaceholder": "Filter conversations"
},
"menu": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ describe('CourseOverviewService', () => {

jest.spyOn(service, 'getCorrespondingChannelSubType');
jest.spyOn(service, 'mapConversationToSidebarCardElement');
const groupedConversations = service.groupConversationsByChannelType(conversations, true);
const groupedConversations = service.groupConversationsByChannelType(course, conversations, true);

expect(groupedConversations['generalChannels'].entityData).toHaveLength(1);
expect(groupedConversations['examChannels'].entityData).toHaveLength(1);
Expand All @@ -445,7 +445,7 @@ describe('CourseOverviewService', () => {

jest.spyOn(service, 'getCorrespondingChannelSubType');
jest.spyOn(service, 'mapConversationToSidebarCardElement');
const groupedConversations = service.groupConversationsByChannelType(conversations, true);
const groupedConversations = service.groupConversationsByChannelType(course, conversations, true);

expect(groupedConversations['generalChannels'].entityData).toHaveLength(2);
expect(service.mapConversationToSidebarCardElement).toHaveBeenCalledTimes(2);
Expand All @@ -460,21 +460,39 @@ describe('CourseOverviewService', () => {
jest.spyOn(service, 'mapConversationToSidebarCardElement');
jest.spyOn(service, 'getConversationGroup');
jest.spyOn(service, 'getCorrespondingChannelSubType');
const groupedConversations = service.groupConversationsByChannelType(conversations, true);
const groupedConversations = service.groupConversationsByChannelType(course, conversations, true);

expect(groupedConversations['generalChannels'].entityData).toHaveLength(2);
expect(groupedConversations['generalChannels'].entityData).toHaveLength(4);
expect(groupedConversations['examChannels'].entityData).toHaveLength(1);
expect(groupedConversations['exerciseChannels'].entityData).toHaveLength(1);
expect(groupedConversations['favoriteChannels'].entityData).toHaveLength(1);
expect(groupedConversations['hiddenChannels'].entityData).toHaveLength(1);
expect(service.mapConversationToSidebarCardElement).toHaveBeenCalledTimes(6);
expect(service.getConversationGroup).toHaveBeenCalledTimes(6);
expect(service.getCorrespondingChannelSubType).toHaveBeenCalledTimes(4);
expect(getAsChannelDTO(groupedConversations['generalChannels'].entityData[0].conversation)?.name).toBe('General');
expect(getAsChannelDTO(groupedConversations['generalChannels'].entityData[1].conversation)?.name).toBe('General 2');
expect(service.getCorrespondingChannelSubType).toHaveBeenCalledTimes(6);
expect(getAsChannelDTO(groupedConversations['generalChannels'].entityData[0].conversation)?.name).toBe('fav-channel');
expect(getAsChannelDTO(groupedConversations['generalChannels'].entityData[1].conversation)?.name).toBe('General');
expect(getAsChannelDTO(groupedConversations['generalChannels'].entityData[2].conversation)?.name).toBe('General 2');
expect(getAsChannelDTO(groupedConversations['examChannels'].entityData[0].conversation)?.name).toBe('exam-test');
expect(getAsChannelDTO(groupedConversations['exerciseChannels'].entityData[0].conversation)?.name).toBe('exercise-test');
expect(getAsChannelDTO(groupedConversations['favoriteChannels'].entityData[0].conversation)?.name).toBe('fav-channel');
expect(getAsChannelDTO(groupedConversations['hiddenChannels'].entityData[0].conversation)?.name).toBe('hidden-channel');
});

it('should not remove favorite conversations from their original section but keep them at the top of the related section', () => {
const conversations = [generalChannel, examChannel, exerciseChannel, favoriteChannel];

jest.spyOn(service, 'getCorrespondingChannelSubType');
jest.spyOn(service, 'mapConversationToSidebarCardElement');
jest.spyOn(service, 'getConversationGroup');
const groupedConversations = service.groupConversationsByChannelType(course, conversations, true);

expect(groupedConversations['favoriteChannels'].entityData).toContainEqual(expect.objectContaining({ id: favoriteChannel.id }));

expect(groupedConversations['generalChannels'].entityData[0].id).toBe(favoriteChannel.id);

expect(service.mapConversationToSidebarCardElement).toHaveBeenCalledTimes(4);
expect(service.getConversationGroup).toHaveBeenCalledTimes(4);
expect(service.getCorrespondingChannelSubType).toHaveBeenCalledTimes(4);
});
});
Loading
Loading