- implements
// ng-container to render answerPostCreateEditModalComponent
@ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef: ViewContainerRef;
- @ViewChild(AnswerPostReactionsBarComponent) private reactionsBarComponent!: AnswerPostReactionsBarComponent;
+ @ViewChild(PostingReactionsBarComponent) protected reactionsBarComponent!: PostingReactionsBarComponent;
// Icons
faBookmark = faBookmark;
diff --git a/src/main/webapp/app/shared/metis/post/post.component.html b/src/main/webapp/app/shared/metis/post/post.component.html
index 0278db06a3ae..6c05c881803b 100644
--- a/src/main/webapp/app/shared/metis/post/post.component.html
+++ b/src/main/webapp/app/shared/metis/post/post.component.html
@@ -92,9 +92,9 @@
@if (!previewMode) {
-
@if (!previewMode) {
- implements OnInit, OnC
@ViewChild('createEditAnswerPostContainer', { read: ViewContainerRef }) containerRef: ViewContainerRef;
@ViewChild('postFooter') postFooterComponent: PostingFooterComponent;
@ViewChild('emojiPickerTrigger') emojiPickerTrigger!: CdkOverlayOrigin;
- @ViewChild(PostReactionsBarComponent) protected reactionsBarComponent!: PostReactionsBarComponent;
+ @ViewChild(PostingReactionsBarComponent) protected reactionsBarComponent!: PostingReactionsBarComponent;
static activeDropdownPost: PostComponent | undefined = undefined;
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html
deleted file mode 100644
index 87d39a5b6354..000000000000
--- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.html
+++ /dev/null
@@ -1,100 +0,0 @@
- implements OnInit, OnChanges {
- @Input() isReadOnlyMode = false;
- @Input() isEmojiCount = false;
- @Input() isLastAnswer = false;
-
- @Output() openPostingCreateEditModal = new EventEmitter();
- @Output() postingUpdated = new EventEmitter();
-
- // Icons
- readonly farSmile = faSmile;
- readonly faCheck = faCheck;
- readonly faPencilAlt = faPencilAlt;
-
- isAuthorOfOriginalPost: boolean;
- isAnswerOfAnnouncement: boolean;
- mayDelete: boolean;
- mayEdit: boolean;
-
- mayDeleteOutput = output();
- mayEditOutput = output();
-
- ngOnInit() {
- super.ngOnInit();
- this.setMayEdit();
- this.setMayDelete();
- }
-
- ngOnChanges() {
- super.ngOnChanges();
- this.setMayEdit();
- this.setMayDelete();
- }
-
- isAnyReactionCountAboveZero(): boolean {
- return Object.values(this.reactionMetaDataMap).some((reaction) => reaction.count >= 1);
- }
-
- /**
- * invokes the metis service to delete an answer post
- */
- deletePosting(): void {
- this.isDeleteEvent.emit(true);
- }
-
- /**
- * builds and returns a Reaction model out of an emojiId and thereby sets the answerPost property properly
- * @param emojiId emojiId to build the model for
- */
- buildReaction(emojiId: string): Reaction {
- const reaction = new Reaction();
- reaction.emojiId = emojiId;
- reaction.answerPost = this.posting;
- return reaction;
- }
-
- setMayDelete(): void {
- // determines if the current user is the author of the original post, that the answer belongs to
- this.isAuthorOfOriginalPost = this.metisService.metisUserIsAuthorOfPosting(this.posting.post!);
- this.isAnswerOfAnnouncement = getAsChannelDTO(this.posting.post?.conversation)?.isAnnouncementChannel ?? false;
- const isCourseWideChannel = getAsChannelDTO(this.posting.post?.conversation)?.isCourseWide ?? false;
- const canDeletePost = this.isAnswerOfAnnouncement ? this.metisService.metisUserIsAtLeastInstructorInCourse() : this.metisService.metisUserIsAtLeastTutorInCourse();
- const mayDeleteOtherUsersAnswer =
- (isCourseWideChannel && canDeletePost) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false);
- this.mayDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || (mayDeleteOtherUsersAnswer && canDeletePost));
- this.mayDeleteOutput.emit(this.mayDelete);
- }
-
- setMayEdit() {
- this.mayEdit = this.isAuthorOfPosting;
- this.mayEditOutput.emit(this.mayEdit);
- }
-
- editPosting() {
- this.openPostingCreateEditModal.emit();
- }
-
- /**
- * toggles the resolvesPost property of an answer post if the user is at least tutor in a course or the user is the author of the original post,
- * delegates the update to the metis service
- */
- toggleResolvesPost(): void {
- if (this.isAtLeastTutorInCourse || this.isAuthorOfOriginalPost) {
- this.posting.resolvesPost = !this.posting.resolvesPost;
- this.metisService.updateAnswerPost(this.posting).subscribe();
- }
- }
-}
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html
deleted file mode 100644
index 60a95dfbd683..000000000000
--- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.html
+++ /dev/null
@@ -1,149 +0,0 @@
- implements OnInit, OnChanges, OnDestroy {
- private accountService = inject(AccountService);
-
- pinTooltip: string;
- displayPriority: DisplayPriority;
- canPin = false;
- readonly DisplayPriority = DisplayPriority;
-
- // Icons
- faSmile = faSmile;
- faArrowRight = faArrowRight;
- faPencilAlt = faPencilAlt;
- faTrash = faTrashAlt;
-
- @Input() readOnlyMode = false;
- @Input() showAnswers: boolean;
- @Input() sortedAnswerPosts: AnswerPost[];
- @Input() isCommunicationPage: boolean;
- @Input() lastReadDate?: dayjs.Dayjs;
- @Input() previewMode: boolean;
- @Input() isEmojiCount = false;
- @Input() hoverBar = true;
-
- @Output() showAnswersChange = new EventEmitter();
- @Output() openPostingCreateEditModal = new EventEmitter();
- @Output() closePostingCreateEditModal = new EventEmitter();
- @Output() openThread = new EventEmitter();
- @Output() canPinOutput = new EventEmitter();
-
- @ViewChild(PostCreateEditModalComponent) postCreateEditModal?: PostCreateEditModalComponent;
- @ViewChild('createEditModal') createEditModal!: PostCreateEditModalComponent;
-
- isAtLeastInstructorInCourse: boolean;
- mayDeleteOutput = output();
- mayEditOutput = output();
- mayEdit: boolean;
- mayDelete: boolean;
-
- isAnyReactionCountAboveZero(): boolean {
- return Object.values(this.reactionMetaDataMap).some((reaction) => reaction.count >= 1);
- }
-
- openAnswerView() {
- this.showAnswersChange.emit(true);
- this.openPostingCreateEditModal.emit();
- }
-
- closeAnswerView() {
- this.showAnswersChange.emit(false);
- this.closePostingCreateEditModal.emit();
- }
-
- /**
- * on initialization: call resetTooltipsAndPriority
- */
- ngOnInit() {
- super.ngOnInit();
-
- const currentConversation = this.metisService.getCurrentConversation();
- this.setCanPin(currentConversation);
- this.setMayDelete();
- this.setMayEdit();
- this.resetTooltipsAndPriority();
- }
-
- ngOnDestroy() {
- this.postCreateEditModal?.modalRef?.close();
- }
-
- /**
- * Checks whether the user can pin the message in the conversation
- *
- * @param currentConversation the conversation the post belongs to
- */
- private setCanPin(currentConversation: ConversationDTO | undefined) {
- if (!currentConversation) {
- this.canPin = this.metisService.metisUserIsAtLeastTutorInCourse();
- return;
- }
-
- if (isChannelDTO(currentConversation)) {
- this.canPin = currentConversation.hasChannelModerationRights ?? false;
- } else if (isGroupChatDTO(currentConversation)) {
- this.canPin = currentConversation.creator?.id === this.accountService.userIdentity?.id;
- } else if (isOneToOneChatDTO(currentConversation)) {
- this.canPin = true;
- }
- this.canPinOutput.emit(this.canPin);
- }
-
- /**
- * on changes: call resetTooltipsAndPriority
- */
- ngOnChanges() {
- super.ngOnChanges();
- this.resetTooltipsAndPriority();
- this.setMayDelete();
- this.setMayEdit();
- }
-
- /**
- * builds and returns a Reaction model out of an emojiId and thereby sets the post property properly
- * @param emojiId emojiId to build the model for
- */
- buildReaction(emojiId: string): Reaction {
- const reaction = new Reaction();
- reaction.emojiId = emojiId;
- reaction.post = this.posting;
- return reaction;
- }
-
- /**
- * changes the state of the displayPriority property on a post to PINNED by invoking the metis service
- * in case the displayPriority is already set to PINNED, it will be changed to NONE
- */
- togglePin() {
- if (this.displayPriority === DisplayPriority.PINNED) {
- this.displayPriority = DisplayPriority.NONE;
- } else {
- this.displayPriority = DisplayPriority.PINNED;
- }
- this.posting.displayPriority = this.displayPriority;
- this.metisService.updatePostDisplayPriority(this.posting.id!, this.displayPriority).subscribe();
- }
-
- checkIfPinned(): DisplayPriority {
- return this.displayPriority;
- }
-
- /**
- * provides the tooltip for the pin icon dependent on the user authority and the pin state of a posting
- *
- */
- getPinTooltip(): string {
- if (this.canPin && this.displayPriority === DisplayPriority.PINNED) {
- return 'artemisApp.metis.removePinPostTooltip';
- }
- if (this.canPin && this.displayPriority !== DisplayPriority.PINNED) {
- return 'artemisApp.metis.pinPostTooltip';
- }
- return 'artemisApp.metis.pinnedPostTooltip';
- }
-
- getShowNewMessageIcon(): boolean {
- let showIcon = false;
- // iterate over all answer posts
- this.sortedAnswerPosts.forEach((answerPost) => {
- // check if the answer post is newer than the last read date
- const isAuthor = this.metisService.metisUserIsAuthorOfPosting(answerPost);
- if (!isAuthor && !!(!this.lastReadDate || (this.lastReadDate && answerPost.creationDate && answerPost.creationDate.isAfter(this.lastReadDate!)))) {
- showIcon = true;
- }
- });
- return showIcon;
- }
-
- private resetTooltipsAndPriority() {
- this.displayPriority = this.posting.displayPriority!;
- this.pinTooltip = this.getPinTooltip();
- }
-
- /**
- * invokes the metis service to delete a post
- */
- deletePosting(): void {
- this.isDeleteEvent.emit(true);
- }
-
- editPosting() {
- if (this.posting.title != '') {
- this.createEditModal.open();
- } else {
- this.isModalOpen.emit();
- }
- }
-
- setMayDelete(): void {
- this.isAtLeastInstructorInCourse = this.metisService.metisUserIsAtLeastInstructorInCourse();
- const isCourseWideChannel = getAsChannelDTO(this.posting.conversation)?.isCourseWide ?? false;
- const isAnswerOfAnnouncement = getAsChannelDTO(this.posting.conversation)?.isAnnouncementChannel ?? false;
- const isAtLeastTutorInCourse = this.metisService.metisUserIsAtLeastTutorInCourse();
- const canDeleteAnnouncement = isAnswerOfAnnouncement ? this.metisService.metisUserIsAtLeastInstructorInCourse() : true;
- const mayDeleteOtherUsersAnswer =
- (isCourseWideChannel && isAtLeastTutorInCourse) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false);
- this.mayDelete = !this.readOnlyMode && !this.previewMode && (this.isAuthorOfPosting || mayDeleteOtherUsersAnswer) && canDeleteAnnouncement;
- this.mayDeleteOutput.emit(this.mayDelete);
- }
-
- setMayEdit(): void {
- this.mayEdit = this.isAuthorOfPosting;
- this.mayEditOutput.emit(this.mayEdit);
- }
-}
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.html b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.html
new file mode 100644
index 000000000000..144f0e3859cb
--- /dev/null
+++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.html
@@ -0,0 +1,172 @@
+ implements OnInit, OnChanges {
+ protected metisService = inject(MetisService);
+ private accountService = inject(AccountService);
+
+ pinEmojiId: string = PIN_EMOJI_ID;
+ archiveEmojiId: string = ARCHIVE_EMOJI_ID;
+ closeCrossId: string = HEAVY_MULTIPLICATION_ID;
+
+ posting = input();
+ isThreadSidebar = input();
+ isEmojiCount = input(false);
+ isReadOnlyMode = input(false);
+
+ openPostingCreateEditModal = output();
+ closePostingCreateEditModal = output();
+ reactionsUpdated = output();
+ isModalOpen = output();
+
+ showReactionSelector = false;
+ isAtLeastTutorInCourse: boolean;
+ isAuthorOfPosting: boolean;
+ isAuthorOfOriginalPost: boolean;
+ isAnswerOfAnnouncement: boolean;
+ isAtLeastInstructorInCourse: boolean;
+ mayDeleteOutput = output();
+ mayEditOutput = output();
+ canPinOutput = output();
+ showAnswers = input();
+ sortedAnswerPosts = input();
+ isCommunicationPage = input();
+ lastReadDate = input();
+ previewMode = input();
+ hoverBar = input(true);
+
+ showAnswersChange = output();
+ isLastAnswer = input(false);
+ postingUpdated = output();
+ mayEdit: boolean;
+ mayDelete: boolean;
+ pinTooltip: string;
+ displayPriority: DisplayPriority;
+ canPin = false;
+ readonly DisplayPriority = DisplayPriority;
+
+ isDeleteEvent = output();
+ readonly onBookmarkClicked = output();
+ openThread = output();
+
+ // Icons
+ readonly faBookmark = faBookmark;
+ readonly faSmile = faSmile;
+ readonly faCheck = faCheck;
+ readonly faPencilAlt = faPencilAlt;
+ readonly faArrowRight = faArrowRight;
+ readonly faTrash = faTrashAlt;
+
+ createEditModal = viewChild.required('createEditModal');
+
+ /**
+ * on initialization: updates the current posting and its reactions,
+ * invokes metis service to check user authority
+ */
+ ngOnInit() {
+ this.updatePostingWithReactions();
+ this.isAuthorOfPosting = this.metisService.metisUserIsAuthorOfPosting(this.posting() as Posting);
+ this.isAtLeastTutorInCourse = this.metisService.metisUserIsAtLeastTutorInCourse();
+ this.isAtLeastInstructorInCourse = this.metisService.metisUserIsAtLeastInstructorInCourse();
+ this.isAnswerOfAnnouncement =
+ this.getPostingType() === 'answerPost' ? (getAsChannelDTO((this.posting() as AnswerPost).post?.conversation)?.isAnnouncementChannel ?? false) : false;
+ this.isAuthorOfOriginalPost = this.getPostingType() === 'answerPost' ? this.metisService.metisUserIsAuthorOfPosting((this.posting() as AnswerPost).post!) : false;
+
+ if (this.getPostingType() === 'post') {
+ const currentConversation = this.metisService.getCurrentConversation();
+ this.setCanPin(currentConversation);
+ this.resetTooltipsAndPriority();
+ }
+ this.setMayDelete();
+ this.setMayEdit();
+ }
+
+ /**
+ * on changes: updates the current posting and its reactions,
+ * invokes metis service to check user authority
+ */
+ ngOnChanges() {
+ this.updatePostingWithReactions();
+ this.isAtLeastTutorInCourse = this.metisService.metisUserIsAtLeastTutorInCourse();
+ if (this.getPostingType() === 'post') {
+ this.resetTooltipsAndPriority();
+ }
+ this.setMayDelete();
+ this.setMayEdit();
+ }
+
+ /*
+ * icons (as svg paths) to be used as category preview image in emoji mart selector
+ */
+ categoriesIcons: { [key: string]: string } = {
+ // category 'recent' (would show recently used emojis) is overwritten by a preselected set of emojis for that course,
+ // therefore category icon is an asterisk (indicating customization) instead of a clock (indicating the "recently used"-use case)
+ recent: `M10 1h3v21h-3zm10.186 4l1.5 2.598L3.5 18.098 2 15.5zM2 7.598L3.5 5l18.186 10.5-1.5 2.598z`,
+ };
+
+ /**
+ * Checks whether the user can pin the message in the conversation
+ *
+ * @param currentConversation the conversation the post belongs to
+ */
+ setCanPin(currentConversation: ConversationDTO | undefined) {
+ if (!currentConversation) {
+ this.canPin = this.metisService.metisUserIsAtLeastTutorInCourse();
+ return;
+ }
+
+ if (isChannelDTO(currentConversation)) {
+ this.canPin = currentConversation.hasChannelModerationRights ?? false;
+ } else if (isGroupChatDTO(currentConversation)) {
+ this.canPin = currentConversation.creator?.id === this.accountService.userIdentity?.id;
+ } else if (isOneToOneChatDTO(currentConversation)) {
+ this.canPin = true;
+ }
+ this.canPinOutput.emit(this.canPin);
+ }
+
+ private resetTooltipsAndPriority() {
+ this.displayPriority = (this.posting() as Post).displayPriority!;
+ this.pinTooltip = this.getPinTooltip();
+ }
+
+ getShowNewMessageIcon(): boolean {
+ let showIcon = false;
+ // iterate over all answer posts
+ (this.sortedAnswerPosts() as unknown as AnswerPost[]).forEach((answerPost: Posting) => {
+ // check if the answer post is newer than the last read date
+ const isAuthor = this.metisService.metisUserIsAuthorOfPosting(answerPost);
+ const lastReadDate = this.lastReadDate?.();
+ const creationDate = answerPost.creationDate;
+
+ if (lastReadDate && creationDate) {
+ const lastReadDateDayJs = dayjs(lastReadDate);
+ // @ts-ignore
+ if (!isAuthor && creationDate.isAfter(lastReadDateDayJs)) {
+ showIcon = true;
+ }
+ }
+ });
+ return showIcon;
+ }
+
+ /**
+ * provides the tooltip for the pin icon dependent on the user authority and the pin state of a posting
+ *
+ */
+ getPinTooltip(): string {
+ if (this.canPin && this.displayPriority === DisplayPriority.PINNED) {
+ return 'artemisApp.metis.removePinPostTooltip';
+ }
+ if (this.canPin && this.displayPriority !== DisplayPriority.PINNED) {
+ return 'artemisApp.metis.pinPostTooltip';
+ }
+ return 'artemisApp.metis.pinnedPostTooltip';
+ }
+
+ /**
+ * currently predefined fixed set of emojis that should be used within a course,
+ * they will be listed on first page of the emoji-mart selector
+ */
+ selectedCourseEmojis = ['smile', 'joy', 'sunglasses', 'tada', 'rocket', 'heavy_plus_sign', 'thumbsup', 'memo', 'coffee', 'recycle'];
+
+ /**
+ * emojis that have a predefined meaning, i.e. pin and archive emoji,
+ * should not appear in the emoji-mart selector
+ */
+ emojisToShowFilter: (emoji: string | EmojiData) => boolean = (emoji) => {
+ if (typeof emoji === 'string') {
+ return emoji !== PIN_EMOJI_UNICODE && emoji !== ARCHIVE_EMOJI_UNICODE && emoji !== SPEECH_BALLOON_UNICODE && emoji !== HEAVY_MULTIPLICATION_UNICODE;
+ } else {
+ return (
+ emoji.unified !== PIN_EMOJI_UNICODE &&
+ emoji.unified !== ARCHIVE_EMOJI_UNICODE &&
+ emoji.unified !== SPEECH_BALLOON_UNICODE &&
+ emoji.unified !== HEAVY_MULTIPLICATION_UNICODE
+ );
+ }
+ };
+
+ /**
+ * map that lists associated reaction (by emojiId) for the current posting together with its count
+ * and a flag that indicates if the current user has used this reaction
+ */
+ reactionMetaDataMap: ReactionMetaDataMap = {};
+
+ /**
+ * builds and returns a Reaction model out of an emojiId and thereby sets the answerPost property properly
+ * @param emojiId emojiId to build the model for
+ */
+ buildReaction(emojiId: string): Reaction {
+ const reaction = new Reaction();
+ reaction.emojiId = emojiId;
+ if (this.getPostingType() === 'answerPost') {
+ reaction.answerPost = this.posting() as AnswerPost;
+ } else {
+ reaction.post = this.posting() as Post;
+ }
+ return reaction;
+ }
+
+ /**
+ * updates the reaction based on the ReactionEvent emitted by the emoji-mart selector component
+ */
+ selectReaction(reactionEvent: ReactionEvent): void {
+ if (reactionEvent.emoji != undefined) {
+ this.addOrRemoveReaction(reactionEvent.emoji.id);
+ }
+ }
+
+ /**
+ * opens the emoji selector overlay if user clicks the '.reaction-button'
+ * closes the emoji selector overly if user clicks the '.reaction-button' again or somewhere outside the overlay
+ */
+ toggleEmojiSelect() {
+ this.showReactionSelector = !this.showReactionSelector;
+ }
+
+ /**
+ * updates the reaction based when a displayed emoji reaction is clicked,
+ * i.e. when agree on an existing reaction (+1) or removing own reactions (-1)
+ */
+ updateReaction(emojiId: string): void {
+ if (emojiId != undefined) {
+ this.addOrRemoveReaction(emojiId);
+ }
+ }
+
+ /**
+ * adds or removes a reaction by invoking the metis service,
+ * depending on if the current user already reacted with the given emojiId (remove) or not (add)
+ * @param emojiId emojiId representing the reaction to be added/removed
+ */
+ addOrRemoveReaction(emojiId: string): void {
+ const existingReactionIdx = (this.posting() as Posting).reactions
+ ? (this.posting() as Posting).reactions!.findIndex((reaction) => reaction.user?.id === this.metisService.getUser().id && reaction.emojiId === emojiId)
+ : -1;
+ if ((this.posting() as Posting).reactions && existingReactionIdx > -1) {
+ const reactionToDelete = (this.posting() as Posting).reactions![existingReactionIdx];
+ this.metisService.deleteReaction(reactionToDelete).subscribe(() => {
+ (this.posting() as Posting).reactions = (this.posting() as Posting).reactions?.filter((reaction) => reaction.id !== reactionToDelete.id);
+ this.updatePostingWithReactions();
+ this.showReactionSelector = false;
+ this.reactionsUpdated.emit((this.posting() as Posting).reactions!);
+ });
+ } else {
+ const reactionToCreate = this.buildReaction(emojiId);
+ this.metisService.createReaction(reactionToCreate).subscribe(() => {
+ this.updatePostingWithReactions();
+ this.showReactionSelector = false;
+ this.reactionsUpdated.emit((this.posting() as Posting).reactions || []);
+ });
+ }
+ }
+
+ /**
+ * updates the posting's reactions by calling the build function for the reactionMetaDataMap if there are any reaction on the posting
+ */
+ updatePostingWithReactions(): void {
+ if ((this.posting() as Posting).reactions && (this.posting() as Posting).reactions!.length > 0) {
+ // filter out emoji for pin and archive as they should not be listed in the reactionMetaDataMap
+ const filteredReactions = (this.posting() as Posting).reactions!.filter(
+ (reaction: Reaction) => reaction.emojiId !== this.pinEmojiId || reaction.emojiId !== this.archiveEmojiId,
+ );
+ this.reactionMetaDataMap = this.buildReactionMetaDataMap(filteredReactions);
+ } else {
+ this.reactionMetaDataMap = {};
+ }
+ }
+
+ /**
+ * builds the ReactionMetaDataMap data structure out of a given array of reactions
+ * @param reactions array of reactions associated to the current posting
+ */
+ buildReactionMetaDataMap(reactions: Reaction[]): ReactionMetaDataMap {
+ return reactions.reduce((metaDataMap: ReactionMetaDataMap, reaction: Reaction) => {
+ const hasReacted = reaction.user?.id === this.metisService.getUser().id;
+ // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
+ const reactingUser = hasReacted ? PLACEHOLDER_USER_REACTED : reaction.user?.name!;
+ const reactionMetaData: ReactionMetaData = {
+ count: metaDataMap[reaction.emojiId!] ? metaDataMap[reaction.emojiId!].count + 1 : 1,
+ hasReacted: metaDataMap[reaction.emojiId!] ? metaDataMap[reaction.emojiId!].hasReacted || hasReacted : hasReacted,
+ reactingUsers: metaDataMap[reaction.emojiId!] ? metaDataMap[reaction.emojiId!].reactingUsers.concat(reactingUser) : [reactingUser],
+ };
+ return { ...metaDataMap, [reaction.emojiId!]: reactionMetaData };
+ }, {});
+ }
+
+ protected bookmarkPosting() {
+ this.onBookmarkClicked.emit();
+ }
+
+ isAnyReactionCountAboveZero(): boolean {
+ return Object.values(this.reactionMetaDataMap).some((reaction) => reaction.count >= 1);
+ }
+
+ /**
+ * invokes the metis service to delete posting
+ */
+ deletePosting(): void {
+ this.isDeleteEvent.emit(true);
+ }
+
+ /**
+ * changes the state of the displayPriority property on a post to PINNED by invoking the metis service
+ * in case the displayPriority is already set to PINNED, it will be changed to NONE
+ */
+ togglePin() {
+ if (this.canPin) {
+ if (this.displayPriority === DisplayPriority.PINNED) {
+ this.displayPriority = DisplayPriority.NONE;
+ } else {
+ this.displayPriority = DisplayPriority.PINNED;
+ }
+ (this.posting() as Post).displayPriority = this.displayPriority;
+ this.metisService.updatePostDisplayPriority((this.posting() as Posting).id!, this.displayPriority).subscribe();
+ }
+ }
+
+ /**
+ * toggles the resolvesPost property of an answer post if the user is at least tutor in a course or the user is the author of the original post,
+ * delegates the update to the metis service
+ */
+ toggleResolvesPost(): void {
+ if (this.isAtLeastTutorInCourse || this.isAuthorOfOriginalPost) {
+ (this.posting() as AnswerPost).resolvesPost = !(this.posting() as AnswerPost).resolvesPost;
+ this.metisService.updateAnswerPost(this.posting() as AnswerPost).subscribe();
+ }
+ }
+
+ checkIfPinned(): DisplayPriority {
+ return this.displayPriority;
+ }
+
+ openAnswerView() {
+ this.showAnswersChange.emit(true);
+ this.openPostingCreateEditModal.emit();
+ }
+
+ closeAnswerView() {
+ this.showAnswersChange.emit(false);
+ this.closePostingCreateEditModal.emit();
+ }
+
+ setMayEdit(): void {
+ this.mayEdit = this.isAuthorOfPosting;
+ this.mayEditOutput.emit(this.mayEdit);
+ }
+
+ editPosting() {
+ if (this.getPostingType() === 'post') {
+ if ((this.posting() as Post)!.title != '') {
+ this.createEditModal().open();
+ } else {
+ this.isModalOpen.emit();
+ }
+ } else {
+ this.openPostingCreateEditModal.emit();
+ }
+ }
+
+ setMayDelete(): void {
+ const conversation = this.getConversation();
+ const channel = getAsChannelDTO(conversation);
+
+ const isAnswerOfAnnouncement = this.getPostingType() === 'answerPost' ? (channel?.isAnnouncementChannel ?? false) : false;
+ const isCourseWide = channel?.isCourseWide ?? false;
+
+ const canDeleteAnnouncement = isAnswerOfAnnouncement ? this.isAtLeastInstructorInCourse : true;
+ const mayDeleteOtherUsers =
+ (isCourseWide && this.isAtLeastTutorInCourse) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false);
+
+ this.mayDelete = !this.isReadOnlyMode() && !this.previewMode() && (this.isAuthorOfPosting || mayDeleteOtherUsers) && canDeleteAnnouncement;
+ this.mayDeleteOutput.emit(this.mayDelete);
+ }
+
+ private getConversation(): Conversation | undefined {
+ if (this.getPostingType() === 'answerPost') {
+ return (this.posting() as AnswerPost).post?.conversation;
+ } else {
+ return (this.posting() as Post).conversation;
+ }
+ }
+
+ getPostingType(): 'post' | 'answerPost' {
+ return this.posting() && 'post' in this.posting()! ? 'answerPost' : 'post';
+ }
+
+ getSaved(): boolean {
+ return (this.posting() as Posting)?.isSaved;
+ }
+
+ getResolvesPost(): boolean {
+ return (this.posting() as AnswerPost)?.resolvesPost;
+ }
+}
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive.ts
deleted file mode 100644
index d0cc31c1d752..000000000000
--- a/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-import { Directive, EventEmitter, Input, OnChanges, OnInit, Output, inject, output } from '@angular/core';
-import { Posting } from 'app/entities/metis/posting.model';
-import { MetisService } from 'app/shared/metis/metis.service';
-import { EmojiData } from '@ctrl/ngx-emoji-mart/ngx-emoji';
-import { Reaction } from 'app/entities/metis/reaction.model';
-import { PLACEHOLDER_USER_REACTED } from 'app/shared/pipes/reacting-users-on-posting.pipe';
-import { faBookmark } from '@fortawesome/free-solid-svg-icons';
-import { faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
-
-const PIN_EMOJI_ID = 'pushpin';
-const ARCHIVE_EMOJI_ID = 'open_file_folder';
-const HEAVY_MULTIPLICATION_ID = 'heavy_multiplication_x';
-
-const SPEECH_BALLOON_UNICODE = '1F4AC';
-const ARCHIVE_EMOJI_UNICODE = '1F4C2';
-const PIN_EMOJI_UNICODE = '1F4CC';
-const HEAVY_MULTIPLICATION_UNICODE = '2716';
-
-/**
- * event triggered by the emoji mart component, including EmojiData
- */
-interface ReactionEvent {
- $event: Event;
- emoji?: EmojiData;
-}
-
-/**
- * represents the amount of users that reacted
- * hasReacted indicates if the currently logged-in user is among those counted users
- */
-interface ReactionMetaData {
- count: number;
- hasReacted: boolean;
- reactingUsers: string[];
-}
-
-/**
- * data structure used for displaying emoji reactions with metadata on postings
- */
-interface ReactionMetaDataMap {
- [emojiId: string]: ReactionMetaData;
-}
-
-@Directive()
-export abstract class PostingsReactionsBarDirective implements OnInit, OnChanges {
- protected metisService = inject(MetisService);
-
- pinEmojiId: string = PIN_EMOJI_ID;
- archiveEmojiId: string = ARCHIVE_EMOJI_ID;
- closeCrossId: string = HEAVY_MULTIPLICATION_ID;
-
- @Input() posting: T;
- @Input() isThreadSidebar: boolean;
-
- @Output() openPostingCreateEditModal = new EventEmitter();
- @Output() reactionsUpdated = new EventEmitter();
- @Output() isModalOpen = new EventEmitter();
-
- showReactionSelector = false;
- currentUserIsAtLeastTutor: boolean;
- isAtLeastTutorInCourse: boolean;
- isAuthorOfPosting: boolean;
-
- isDeleteEvent = output();
- readonly onBookmarkClicked = output();
-
- // Icons
- readonly farBookmark = farBookmark;
- readonly faBookmark = faBookmark;
-
- /*
- * icons (as svg paths) to be used as category preview image in emoji mart selector
- */
- categoriesIcons: { [key: string]: string } = {
- // category 'recent' (would show recently used emojis) is overwritten by a preselected set of emojis for that course,
- // therefore category icon is an asterisk (indicating customization) instead of a clock (indicating the "recently used"-use case)
- recent: `M10 1h3v21h-3zm10.186 4l1.5 2.598L3.5 18.098 2 15.5zM2 7.598L3.5 5l18.186 10.5-1.5 2.598z`,
- };
-
- /**
- * currently predefined fixed set of emojis that should be used within a course,
- * they will be listed on first page of the emoji-mart selector
- */
- selectedCourseEmojis = ['smile', 'joy', 'sunglasses', 'tada', 'rocket', 'heavy_plus_sign', 'thumbsup', 'memo', 'coffee', 'recycle'];
-
- /**
- * emojis that have a predefined meaning, i.e. pin and archive emoji,
- * should not appear in the emoji-mart selector
- */
- emojisToShowFilter: (emoji: string | EmojiData) => boolean = (emoji) => {
- if (typeof emoji === 'string') {
- return emoji !== PIN_EMOJI_UNICODE && emoji !== ARCHIVE_EMOJI_UNICODE && emoji !== SPEECH_BALLOON_UNICODE && emoji !== HEAVY_MULTIPLICATION_UNICODE;
- } else {
- return (
- emoji.unified !== PIN_EMOJI_UNICODE &&
- emoji.unified !== ARCHIVE_EMOJI_UNICODE &&
- emoji.unified !== SPEECH_BALLOON_UNICODE &&
- emoji.unified !== HEAVY_MULTIPLICATION_UNICODE
- );
- }
- };
-
- /**
- * map that lists associated reaction (by emojiId) for the current posting together with its count
- * and a flag that indicates if the current user has used this reaction
- */
- reactionMetaDataMap: ReactionMetaDataMap = {};
-
- /**
- * on initialization: updates the current posting and its reactions,
- * invokes metis service to check user authority
- */
- ngOnInit(): void {
- this.updatePostingWithReactions();
- this.currentUserIsAtLeastTutor = this.metisService.metisUserIsAtLeastTutorInCourse();
- this.isAuthorOfPosting = this.metisService.metisUserIsAuthorOfPosting(this.posting);
- this.isAtLeastTutorInCourse = this.metisService.metisUserIsAtLeastTutorInCourse();
- }
-
- deletePosting(): void {
- this.metisService.deletePost(this.posting);
- }
-
- /**
- * on changes: updates the current posting and its reactions,
- * invokes metis service to check user authority
- */
- ngOnChanges() {
- this.updatePostingWithReactions();
- this.currentUserIsAtLeastTutor = this.metisService.metisUserIsAtLeastTutorInCourse();
- }
-
- abstract buildReaction(emojiId: string): Reaction;
-
- /**
- * updates the reaction based on the ReactionEvent emitted by the emoji-mart selector component
- */
- selectReaction(reactionEvent: ReactionEvent): void {
- if (reactionEvent.emoji != undefined) {
- this.addOrRemoveReaction(reactionEvent.emoji.id);
- }
- }
-
- /**
- * opens the emoji selector overlay if user clicks the '.reaction-button'
- * closes the emoji selector overly if user clicks the '.reaction-button' again or somewhere outside the overlay
- */
- toggleEmojiSelect() {
- this.showReactionSelector = !this.showReactionSelector;
- }
-
- /**
- * updates the reaction based when a displayed emoji reaction is clicked,
- * i.e. when agree on an existing reaction (+1) or removing own reactions (-1)
- */
- updateReaction(emojiId: string): void {
- if (emojiId != undefined) {
- this.addOrRemoveReaction(emojiId);
- }
- }
-
- /**
- * adds or removes a reaction by invoking the metis service,
- * depending on if the current user already reacted with the given emojiId (remove) or not (add)
- * @param emojiId emojiId representing the reaction to be added/removed
- */
- addOrRemoveReaction(emojiId: string): void {
- const existingReactionIdx = this.posting.reactions
- ? this.posting.reactions.findIndex((reaction) => reaction.user?.id === this.metisService.getUser().id && reaction.emojiId === emojiId)
- : -1;
- if (this.posting.reactions && existingReactionIdx > -1) {
- const reactionToDelete = this.posting.reactions[existingReactionIdx];
- this.metisService.deleteReaction(reactionToDelete).subscribe(() => {
- this.posting.reactions = this.posting.reactions?.filter((reaction) => reaction.id !== reactionToDelete.id);
- this.updatePostingWithReactions();
- this.showReactionSelector = false;
- this.reactionsUpdated.emit(this.posting.reactions);
- });
- } else {
- const reactionToCreate = this.buildReaction(emojiId);
- this.metisService.createReaction(reactionToCreate).subscribe(() => {
- this.updatePostingWithReactions();
- this.showReactionSelector = false;
- this.reactionsUpdated.emit(this.posting.reactions);
- });
- }
- }
-
- /**
- * updates the posting's reactions by calling the build function for the reactionMetaDataMap if there are any reaction on the posting
- */
- updatePostingWithReactions(): void {
- if (this.posting.reactions && this.posting.reactions.length > 0) {
- // filter out emoji for pin and archive as they should not be listed in the reactionMetaDataMap
- const filteredReactions = this.posting.reactions.filter((reaction: Reaction) => reaction.emojiId !== this.pinEmojiId || reaction.emojiId !== this.archiveEmojiId);
- this.reactionMetaDataMap = this.buildReactionMetaDataMap(filteredReactions);
- } else {
- this.reactionMetaDataMap = {};
- }
- }
-
- /**
- * builds the ReactionMetaDataMap data structure out of a given array of reactions
- * @param reactions array of reactions associated to the current posting
- */
- buildReactionMetaDataMap(reactions: Reaction[]): ReactionMetaDataMap {
- return reactions.reduce((metaDataMap: ReactionMetaDataMap, reaction: Reaction) => {
- const hasReacted = reaction.user?.id === this.metisService.getUser().id;
- // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
- const reactingUser = hasReacted ? PLACEHOLDER_USER_REACTED : reaction.user?.name!;
- const reactionMetaData: ReactionMetaData = {
- count: metaDataMap[reaction.emojiId!] ? metaDataMap[reaction.emojiId!].count + 1 : 1,
- hasReacted: metaDataMap[reaction.emojiId!] ? metaDataMap[reaction.emojiId!].hasReacted || hasReacted : hasReacted,
- reactingUsers: metaDataMap[reaction.emojiId!] ? metaDataMap[reaction.emojiId!].reactingUsers.concat(reactingUser) : [reactingUser],
- };
- return { ...metaDataMap, [reaction.emojiId!]: reactionMetaData };
- }, {});
- }
-
- protected bookmarkPosting() {
- this.onBookmarkClicked.emit();
- }
-}
diff --git a/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts b/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts
index 01e4f96bba36..efcc2ed8bf78 100644
--- a/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts
+++ b/src/test/javascript/spec/component/shared/metis/answer-post/answer-post.component.spec.ts
@@ -4,7 +4,6 @@ import { DebugElement, input, runInInjectionContext } from '@angular/core';
import { MockComponent, MockDirective, MockModule, MockPipe, ngMocks } from 'ng-mocks';
import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe';
import { By } from '@angular/platform-browser';
-import { AnswerPostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component';
import { PostingContentComponent } from 'app/shared/metis/posting-content/posting-content.components';
import { metisPostExerciseUser1, metisResolvingAnswerPostUser1 } from '../../../../helpers/sample/metis-sample-data';
import { OverlayModule } from '@angular/cdk/overlay';
@@ -27,9 +26,12 @@ import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { MockSyncStorage } from '../../../../helpers/mocks/service/mock-sync-storage.service';
-import { SessionStorageService } from 'ngx-webstorage';
+import { LocalStorageService, SessionStorageService } from 'ngx-webstorage';
import { MetisConversationService } from 'app/shared/metis/metis-conversation.service';
import { MockMetisConversationService } from '../../../../helpers/mocks/service/mock-metis-conversation.service';
+import { AccountService } from '../../../../../../../main/webapp/app/core/auth/account.service';
+import { MockAccountService } from '../../../../helpers/mocks/service/mock-account.service';
+import { MockLocalStorageService } from '../../../../helpers/mocks/service/mock-local-storage.service';
describe('AnswerPostComponent', () => {
let component: AnswerPostComponent;
@@ -50,7 +52,6 @@ describe('AnswerPostComponent', () => {
MockComponent(PostingContentComponent),
MockComponent(PostingHeaderComponent),
MockComponent(AnswerPostCreateEditModalComponent),
- MockComponent(AnswerPostReactionsBarComponent),
ArtemisDatePipe,
ArtemisTranslatePipe,
MockDirective(TranslateDirective),
@@ -63,6 +64,8 @@ describe('AnswerPostComponent', () => {
{ provide: TranslateService, useClass: MockTranslateService },
{ provide: SessionStorageService, useClass: MockSyncStorage },
{ provide: MetisConversationService, useClass: MockMetisConversationService },
+ { provide: AccountService, useClass: MockAccountService },
+ { provide: LocalStorageService, useClass: MockLocalStorageService },
],
})
.compileComponents()
@@ -118,7 +121,7 @@ describe('AnswerPostComponent', () => {
component.posting = metisResolvingAnswerPostUser1;
fixture.detectChanges();
- const reactionsBar = debugElement.query(By.css('jhi-answer-post-reactions-bar'));
+ const reactionsBar = debugElement.query(By.css('jhi-posting-reactions-bar'));
expect(reactionsBar).not.toBeNull();
});
diff --git a/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts b/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts
index 5866fbdeb437..40af94515518 100644
--- a/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts
+++ b/src/test/javascript/spec/component/shared/metis/post/post.component.spec.ts
@@ -4,8 +4,8 @@ import { DebugElement } from '@angular/core';
import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe';
import { PostComponent } from 'app/shared/metis/post/post.component';
import { getElement } from '../../../../helpers/utils/general.utils';
-import { PostingFooterComponent } from '../../../../../../../main/webapp/app/shared/metis/posting-footer/posting-footer.component';
-import { PostingHeaderComponent } from '../../../../../../../main/webapp/app/shared/metis/posting-header/posting-header.component';
+import { PostingFooterComponent } from 'app/shared/metis/posting-footer/posting-footer.component';
+import { PostingHeaderComponent } from 'app/shared/metis/posting-header/posting-header.component';
import { PostingContentComponent } from 'app/shared/metis/posting-content/posting-content.components';
import { MockMetisService } from '../../../../helpers/mocks/service/mock-metis-service.service';
import { MetisService } from 'app/shared/metis/metis.service';
@@ -33,17 +33,20 @@ import { OneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat
import { HttpResponse } from '@angular/common/http';
import { MockRouter } from '../../../../helpers/mocks/mock-router';
import { AnswerPostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component';
-import { PostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DOCUMENT } from '@angular/common';
import { Posting, PostingType } from 'app/entities/metis/posting.model';
import { Post } from 'app/entities/metis/post.model';
-import { ArtemisTranslatePipe } from '../../../../../../../main/webapp/app/shared/pipes/artemis-translate.pipe';
-import { ArtemisDatePipe } from '../../../../../../../main/webapp/app/shared/pipes/artemis-date.pipe';
-import { TranslateDirective } from '../../../../../../../main/webapp/app/shared/language/translate.directive';
+import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
+import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe';
+import { TranslateDirective } from 'app/shared/language/translate.directive';
import { TranslateService } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import dayjs from 'dayjs/esm';
+import { AccountService } from 'app/core/auth/account.service';
+import { MockAccountService } from '../../../../helpers/mocks/service/mock-account.service';
+import { MockLocalStorageService } from '../../../../helpers/mocks/service/mock-local-storage.service';
+import { LocalStorageService } from 'ngx-webstorage';
describe('PostComponent', () => {
let component: PostComponent;
@@ -71,6 +74,8 @@ describe('PostComponent', () => {
MockProvider(MetisConversationService),
MockProvider(OneToOneChatService),
{ provide: TranslateService, useClass: MockTranslateService },
+ { provide: AccountService, useClass: MockAccountService },
+ { provide: LocalStorageService, useClass: MockLocalStorageService },
],
declarations: [
PostComponent,
@@ -80,7 +85,6 @@ describe('PostComponent', () => {
MockComponent(PostingContentComponent),
MockComponent(PostingFooterComponent),
MockComponent(AnswerPostCreateEditModalComponent),
- MockComponent(PostReactionsBarComponent),
MockRouterLinkDirective,
MockQueryParamsDirective,
TranslatePipeMock,
diff --git a/src/test/javascript/spec/component/shared/metis/postings-footer/posting-footer.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-footer/posting-footer.component.spec.ts
index bfdc7d6e567b..3efd5fc951c0 100644
--- a/src/test/javascript/spec/component/shared/metis/postings-footer/posting-footer.component.spec.ts
+++ b/src/test/javascript/spec/component/shared/metis/postings-footer/posting-footer.component.spec.ts
@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MockComponent, MockModule } from 'ng-mocks';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
-import { PostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component';
import { ArtemisCoursesRoutingModule } from 'app/overview/courses-routing.module';
import { MetisService } from 'app/shared/metis/metis.service';
import { PostService } from 'app/shared/metis/post.service';
@@ -46,7 +45,6 @@ describe('PostingFooterComponent', () => {
PostingFooterComponent,
TranslatePipeMock,
MockComponent(FaIconComponent),
- MockComponent(PostReactionsBarComponent),
MockComponent(PostComponent),
MockComponent(AnswerPostComponent),
MockComponent(AnswerPostCreateEditModalComponent),
diff --git a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts
deleted file mode 100644
index 96517283b95c..000000000000
--- a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.spec.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { EmojiComponent } from 'app/shared/metis/emoji/emoji.component';
-import { MetisService } from 'app/shared/metis/metis.service';
-import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
-import { OverlayModule } from '@angular/cdk/overlay';
-import { Reaction } from 'app/entities/metis/reaction.model';
-import { provideHttpClientTesting } from '@angular/common/http/testing';
-import { ReactionService } from 'app/shared/metis/reaction.service';
-import { MockReactionService } from '../../../../../helpers/mocks/service/mock-reaction.service';
-import { AccountService } from 'app/core/auth/account.service';
-import { MockAccountService } from '../../../../../helpers/mocks/service/mock-account.service';
-import { AnswerPostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component';
-import { AnswerPost } from 'app/entities/metis/answer-post.model';
-import { EmojiModule } from '@ctrl/ngx-emoji-mart/ngx-emoji';
-import { PickerModule } from '@ctrl/ngx-emoji-mart';
-import { FaIconComponent } from '@fortawesome/angular-fontawesome';
-import { TranslateService } from '@ngx-translate/core';
-import { MockTranslateService, TranslatePipeMock } from '../../../../../helpers/mocks/service/mock-translate.service';
-import { Router } from '@angular/router';
-import { MockRouter } from '../../../../../helpers/mocks/mock-router';
-import { MockLocalStorageService } from '../../../../../helpers/mocks/service/mock-local-storage.service';
-import { LocalStorageService, SessionStorageService } from 'ngx-webstorage';
-import { ReactingUsersOnPostingPipe } from 'app/shared/pipes/reacting-users-on-posting.pipe';
-import { By } from '@angular/platform-browser';
-import { metisCourse, metisPostInChannel, metisResolvingAnswerPostUser1, metisUser1, post } from '../../../../../helpers/sample/metis-sample-data';
-import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
-import { NotificationService } from 'app/shared/notification/notification.service';
-import { MockNotificationService } from '../../../../../helpers/mocks/service/mock-notification.service';
-import { provideHttpClient } from '@angular/common/http';
-import { ConfirmIconComponent } from 'app/shared/confirm-icon/confirm-icon.component';
-import { getElement } from '../../../../../helpers/utils/general.utils';
-import { DebugElement } from '@angular/core';
-import { UserRole } from 'app/shared/metis/metis.util';
-
-describe('AnswerPostReactionsBarComponent', () => {
- let component: AnswerPostReactionsBarComponent;
- let fixture: ComponentFixture;
- let debugElement: DebugElement;
- let metisService: MetisService;
- let answerPost: AnswerPost;
- let reactionToCreate: Reaction;
- let reactionToDelete: Reaction;
- let metisServiceUserIsAtLeastTutorMock: jest.SpyInstance;
- let metisServiceUserIsAtLeastInstructorMock: jest.SpyInstance;
- let metisServiceUserPostingAuthorMock: jest.SpyInstance;
- let metisServiceUpdateAnswerPostMock: jest.SpyInstance;
-
- beforeEach(() => {
- return TestBed.configureTestingModule({
- imports: [MockModule(OverlayModule), MockModule(EmojiModule), MockModule(PickerModule), MockModule(NgbTooltipModule)],
- declarations: [
- AnswerPostReactionsBarComponent,
- TranslatePipeMock,
- MockPipe(ReactingUsersOnPostingPipe),
- MockComponent(FaIconComponent),
- MockComponent(EmojiComponent),
- MockComponent(ConfirmIconComponent),
- ],
- providers: [
- provideHttpClient(),
- provideHttpClientTesting(),
- MockProvider(SessionStorageService),
- { provide: NotificationService, useClass: MockNotificationService },
- { provide: MetisService, useClass: MetisService },
- { provide: ReactionService, useClass: MockReactionService },
- { provide: AccountService, useClass: MockAccountService },
- { provide: TranslateService, useClass: MockTranslateService },
- { provide: Router, useClass: MockRouter },
- { provide: LocalStorageService, useClass: MockLocalStorageService },
- ],
- })
- .compileComponents()
- .then(() => {
- fixture = TestBed.createComponent(AnswerPostReactionsBarComponent);
- metisService = TestBed.inject(MetisService);
- debugElement = fixture.debugElement;
- metisServiceUserIsAtLeastTutorMock = jest.spyOn(metisService, 'metisUserIsAtLeastTutorInCourse');
- metisServiceUserIsAtLeastInstructorMock = jest.spyOn(metisService, 'metisUserIsAtLeastInstructorInCourse');
- metisServiceUserPostingAuthorMock = jest.spyOn(metisService, 'metisUserIsAuthorOfPosting');
- metisServiceUpdateAnswerPostMock = jest.spyOn(metisService, 'updateAnswerPost');
- component = fixture.componentInstance;
- answerPost = new AnswerPost();
- answerPost.id = 1;
- answerPost.author = metisUser1;
- reactionToDelete = new Reaction();
- reactionToDelete.id = 1;
- reactionToDelete.emojiId = 'smile';
- reactionToDelete.user = metisUser1;
- reactionToDelete.answerPost = answerPost;
- answerPost.reactions = [reactionToDelete];
- component.posting = answerPost;
- metisService.setCourse(metisCourse);
- });
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- function getEditButton(): DebugElement | null {
- return debugElement.query(By.css('button.reaction-button.clickable.px-2.fs-small.edit'));
- }
-
- function getDeleteButton(): DebugElement | null {
- return debugElement.query(By.css('.delete'));
- }
-
- function getResolveButton(): DebugElement | null {
- return debugElement.query(By.css('#toggleElement'));
- }
-
- it('should invoke metis service method with correctly built reaction to create it', () => {
- component.ngOnInit();
- fixture.detectChanges();
- const metisServiceCreateReactionSpy = jest.spyOn(metisService, 'createReaction');
- reactionToCreate = new Reaction();
- reactionToCreate.emojiId = '+1';
- reactionToCreate.answerPost = component.posting;
- component.addOrRemoveReaction(reactionToCreate.emojiId);
- expect(metisServiceCreateReactionSpy).toHaveBeenCalledWith(reactionToCreate);
- expect(component.showReactionSelector).toBeFalse();
- });
-
- it('should display edit and delete options to post author', () => {
- metisServiceUserPostingAuthorMock.mockReturnValue(true);
- fixture.detectChanges();
- expect(getEditButton()).not.toBeNull();
- expect(getDeleteButton()).not.toBeNull();
- });
-
- it('should display the delete option to instructor if posting is in course-wide channel from a student', () => {
- metisServiceUserIsAtLeastInstructorMock.mockReturnValue(true);
- metisServiceUserPostingAuthorMock.mockReturnValue(false);
- metisServiceUserIsAtLeastTutorMock.mockReturnValue(true);
- component.posting = { ...metisResolvingAnswerPostUser1, post: { ...metisPostInChannel } };
- component.posting.authorRole = UserRole.USER;
- component.ngOnInit();
- fixture.detectChanges();
- expect(getDeleteButton()).not.toBeNull();
- });
-
- it('should not display the edit option to user (even instructor) if s/he is not the author of posting', () => {
- metisServiceUserIsAtLeastInstructorMock.mockReturnValue(true);
- metisServiceUserPostingAuthorMock.mockReturnValue(false);
-
- component.ngOnInit();
- fixture.detectChanges();
-
- expect(getEditButton()).toBeNull();
- });
-
- it('should display the edit option to user if s/he is the author of posting', () => {
- metisServiceUserIsAtLeastInstructorMock.mockReturnValue(false);
- metisServiceUserPostingAuthorMock.mockReturnValue(true);
-
- component.ngOnInit();
- fixture.detectChanges();
-
- expect(getEditButton()).not.toBeNull();
- });
-
- it('should display edit and delete options to tutor if posting is in course-wide channel from a student', () => {
- metisServiceUserIsAtLeastInstructorMock.mockReturnValue(false);
- metisServiceUserIsAtLeastTutorMock.mockReturnValue(true);
- metisServiceUserPostingAuthorMock.mockReturnValue(false);
- component.posting = { ...metisResolvingAnswerPostUser1, post: { ...metisPostInChannel } };
- component.posting.authorRole = UserRole.USER;
- component.ngOnInit();
- fixture.detectChanges();
- expect(getEditButton()).toBeNull();
- expect(getDeleteButton()).not.toBeNull();
- });
-
- it('should not display edit and delete options to users that are neither author or tutor', () => {
- metisServiceUserIsAtLeastTutorMock.mockReturnValue(false);
- metisServiceUserPostingAuthorMock.mockReturnValue(false);
- metisServiceUserIsAtLeastInstructorMock.mockReturnValue(false);
- fixture.detectChanges();
- expect(getEditButton()).toBeNull();
- expect(getDeleteButton()).toBeNull();
- });
-
- it('should emit event to create embedded view when edit icon is clicked', () => {
- component.posting = metisResolvingAnswerPostUser1;
- const openPostingCreateEditModalEmitSpy = jest.spyOn(component.openPostingCreateEditModal, 'emit');
- metisServiceUserPostingAuthorMock.mockReturnValue(true);
- fixture.detectChanges();
- getElement(debugElement, '.edit').click();
- expect(openPostingCreateEditModalEmitSpy).toHaveBeenCalledOnce();
- });
-
- it('should invoke metis service method with own reaction to delete it', () => {
- component.posting!.author!.id = 99;
- component.ngOnInit();
- fixture.detectChanges();
- const metisServiceCreateReactionSpy = jest.spyOn(metisService, 'deleteReaction');
- component.addOrRemoveReaction(reactionToDelete.emojiId!);
- expect(metisServiceCreateReactionSpy).toHaveBeenCalledWith(reactionToDelete);
- });
-
- it('should invoke metis service method with own reaction to remove it', () => {
- component.ngOnInit();
- const addOrRemoveSpy = jest.spyOn(component, 'addOrRemoveReaction');
- component.updateReaction(reactionToDelete.emojiId!);
- expect(addOrRemoveSpy).toHaveBeenCalledWith(reactionToDelete.emojiId!);
- });
-
- it('answer now button should be invisible if answer is not the last one', () => {
- component.posting = post;
- component.isLastAnswer = false;
- fixture.detectChanges();
- const answerNowButton = fixture.debugElement.query(By.css('.reply-btn'));
- expect(answerNowButton).toBeNull();
- });
-
- it('should invoke metis service when toggle resolve is clicked', () => {
- metisServiceUserPostingAuthorMock.mockReturnValue(true);
- fixture.detectChanges();
- expect(getResolveButton()).not.toBeNull();
- const previousState = component.posting.resolvesPost;
- component.toggleResolvesPost();
- expect(component.posting.resolvesPost).toEqual(!previousState);
- expect(metisServiceUpdateAnswerPostMock).toHaveBeenCalledOnce();
- });
-});
diff --git a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/posting-reactions-bar.component.spec.ts
similarity index 54%
rename from src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts
rename to src/test/javascript/spec/component/shared/metis/postings-reactions-bar/posting-reactions-bar.component.spec.ts
index bedc64c1611d..830fa68b6fda 100644
--- a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts
+++ b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/posting-reactions-bar.component.spec.ts
@@ -1,42 +1,53 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MetisService } from 'app/shared/metis/metis.service';
-import { DebugElement } from '@angular/core';
+import { DebugElement, input, runInInjectionContext } from '@angular/core';
import { Post } from 'app/entities/metis/post.model';
import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks';
-import { getElement } from '../../../../../helpers/utils/general.utils';
-import { PostReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component';
+import { getElement } from '../../../../helpers/utils/general.utils';
import { Reaction } from 'app/entities/metis/reaction.model';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { ReactionService } from 'app/shared/metis/reaction.service';
-import { MockReactionService } from '../../../../../helpers/mocks/service/mock-reaction.service';
+import { MockReactionService } from '../../../../helpers/mocks/service/mock-reaction.service';
import { AccountService } from 'app/core/auth/account.service';
-import { MockAccountService } from '../../../../../helpers/mocks/service/mock-account.service';
+import { MockAccountService } from '../../../../helpers/mocks/service/mock-account.service';
import { EmojiData } from '@ctrl/ngx-emoji-mart/ngx-emoji';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { DisplayPriority, UserRole } from 'app/shared/metis/metis.util';
-import { MockTranslateService, TranslatePipeMock } from '../../../../../helpers/mocks/service/mock-translate.service';
+import { MockTranslateService, TranslatePipeMock } from '../../../../helpers/mocks/service/mock-translate.service';
import { TranslateService } from '@ngx-translate/core';
import { Router } from '@angular/router';
-import { MockRouter } from '../../../../../helpers/mocks/mock-router';
-import { MockLocalStorageService } from '../../../../../helpers/mocks/service/mock-local-storage.service';
+import { MockRouter } from '../../../../helpers/mocks/mock-router';
+import { MockLocalStorageService } from '../../../../helpers/mocks/service/mock-local-storage.service';
import { LocalStorageService, SessionStorageService } from 'ngx-webstorage';
import { By } from '@angular/platform-browser';
import { PLACEHOLDER_USER_REACTED, ReactingUsersOnPostingPipe } from 'app/shared/pipes/reacting-users-on-posting.pipe';
-import { metisAnnouncement, metisCourse, metisPostExerciseUser1, metisPostInChannel, metisUser1, sortedAnswerArray } from '../../../../../helpers/sample/metis-sample-data';
+import {
+ metisAnnouncement,
+ metisCourse,
+ metisPostExerciseUser1,
+ metisPostInChannel,
+ metisResolvingAnswerPostUser1,
+ metisUser1,
+ sortedAnswerArray,
+ unApprovedAnswerPost1,
+} from '../../../../helpers/sample/metis-sample-data';
import { EmojiComponent } from 'app/shared/metis/emoji/emoji.component';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { NotificationService } from 'app/shared/notification/notification.service';
-import { MockNotificationService } from '../../../../../helpers/mocks/service/mock-notification.service';
+import { MockNotificationService } from '../../../../helpers/mocks/service/mock-notification.service';
import { ConversationDTO, ConversationType } from 'app/entities/metis/conversation/conversation.model';
import { ChannelDTO } from 'app/entities/metis/conversation/channel.model';
import { User } from 'app/core/user/user.model';
import { provideHttpClient } from '@angular/common/http';
import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component';
import { ConfirmIconComponent } from 'app/shared/confirm-icon/confirm-icon.component';
+import { PostingReactionsBarComponent } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.component';
+import { Posting } from 'app/entities/metis/posting.model';
+import { AnswerPost } from 'app/entities/metis/answer-post.model';
-describe('PostReactionsBarComponent', () => {
- let component: PostReactionsBarComponent;
- let fixture: ComponentFixture;
+describe('PostingReactionsBarComponent', () => {
+ let component: PostingReactionsBarComponent;
+ let fixture: ComponentFixture>;
let debugElement: DebugElement;
let metisService: MetisService;
let accountService: AccountService;
@@ -44,6 +55,7 @@ describe('PostReactionsBarComponent', () => {
let metisServiceUserIsAtLeastTutorStub: jest.SpyInstance;
let metisServiceUserIsAtLeastInstructorStub: jest.SpyInstance;
let metisServiceUserIsAuthorOfPostingStub: jest.SpyInstance;
+ let metisServiceUpdateAnswerPostMock: jest.SpyInstance;
let post: Post;
let reactionToCreate: Reaction;
let reactionToDelete: Reaction;
@@ -57,7 +69,6 @@ describe('PostReactionsBarComponent', () => {
return TestBed.configureTestingModule({
imports: [MockDirective(NgbTooltip)],
declarations: [
- PostReactionsBarComponent,
TranslatePipeMock,
MockPipe(ReactingUsersOnPostingPipe),
MockComponent(FaIconComponent),
@@ -80,7 +91,7 @@ describe('PostReactionsBarComponent', () => {
})
.compileComponents()
.then(() => {
- fixture = TestBed.createComponent(PostReactionsBarComponent);
+ fixture = TestBed.createComponent(PostingReactionsBarComponent);
metisService = TestBed.inject(MetisService);
accountService = TestBed.inject(AccountService);
debugElement = fixture.debugElement;
@@ -89,18 +100,21 @@ describe('PostReactionsBarComponent', () => {
metisServiceUserIsAtLeastTutorStub = jest.spyOn(metisService, 'metisUserIsAtLeastTutorInCourse');
metisServiceUserIsAtLeastInstructorStub = jest.spyOn(metisService, 'metisUserIsAtLeastInstructorInCourse');
metisServiceUserIsAuthorOfPostingStub = jest.spyOn(metisService, 'metisUserIsAuthorOfPosting');
+ metisServiceUpdateAnswerPostMock = jest.spyOn(metisService, 'updateAnswerPost');
post = new Post();
post.id = 1;
post.author = metisUser1;
post.displayPriority = DisplayPriority.NONE;
- component.sortedAnswerPosts = sortedAnswerArray;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.sortedAnswerPosts = input(sortedAnswerArray);
+ component.posting = input(post);
+ });
reactionToDelete = new Reaction();
reactionToDelete.id = 1;
reactionToDelete.emojiId = 'smile';
reactionToDelete.user = metisUser1;
reactionToDelete.post = post;
post.reactions = [reactionToDelete];
- component.posting = post;
metisService.setCourse(metisCourse);
});
});
@@ -117,11 +131,15 @@ describe('PostReactionsBarComponent', () => {
return debugElement.query(By.css('jhi-confirm-icon'));
}
+ function getResolveButton(): DebugElement | null {
+ return debugElement.query(By.css('#toggleElement'));
+ }
+
it('should initialize user authority and reactions correctly', () => {
metisCourse.isAtLeastTutor = false;
metisService.setCourse(metisCourse);
component.ngOnInit();
- expect(component.currentUserIsAtLeastTutor).toBeFalse();
+ expect(component.isAtLeastTutorInCourse).toBeFalse();
fixture.detectChanges();
const reaction = getElement(debugElement, 'ngx-emoji');
expect(reaction).toBeDefined();
@@ -135,11 +153,12 @@ describe('PostReactionsBarComponent', () => {
});
it('should display edit and delete options to the author when not in read-only or preview mode', () => {
- component.readOnlyMode = false;
- component.previewMode = false;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.isReadOnlyMode = input(false);
+ component.previewMode = input(false);
+ component.posting = input({ id: 1, title: 'Test Post' } as Post);
+ });
jest.spyOn(metisService, 'metisUserIsAuthorOfPosting').mockReturnValue(true);
- component.posting = { id: 1, title: 'Test Post' } as Post;
-
component.ngOnInit();
fixture.detectChanges();
@@ -148,9 +167,12 @@ describe('PostReactionsBarComponent', () => {
});
it('should display the delete option to user with channel moderation rights when not the author', () => {
- component.readOnlyMode = false;
- component.previewMode = false;
- component.isEmojiCount = false;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.isReadOnlyMode = input(false);
+ component.previewMode = input(false);
+ component.isEmojiCount = input(false);
+ component.posting = input({ id: 1, title: 'Test Post' } as Post);
+ });
const channelConversation = {
type: ConversationType.CHANNEL,
@@ -168,9 +190,11 @@ describe('PostReactionsBarComponent', () => {
});
it('should not display the edit option to user (even instructor) if s/he is not the author of posting', () => {
- component.readOnlyMode = false;
- component.previewMode = false;
- component.isEmojiCount = false;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.isReadOnlyMode = input(false);
+ component.previewMode = input(false);
+ component.isEmojiCount = input(false);
+ });
const channelConversation = {
type: ConversationType.CHANNEL,
@@ -188,17 +212,19 @@ describe('PostReactionsBarComponent', () => {
});
it('should display the edit option to user if s/he is the author of posting', () => {
- component.readOnlyMode = false;
- component.previewMode = false;
- component.isEmojiCount = false;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.isReadOnlyMode = input(false);
+ component.previewMode = input(false);
+ component.isEmojiCount = input(false);
+ });
const channelConversation = {
type: ConversationType.CHANNEL,
hasChannelModerationRights: true,
} as ChannelDTO;
- jest.spyOn(metisService, 'metisUserIsAuthorOfPosting').mockReturnValue(true);
- jest.spyOn(metisService, 'metisUserIsAtLeastInstructorInCourse').mockReturnValue(false);
+ metisServiceUserIsAuthorOfPostingStub.mockReturnValue(true);
+ metisServiceUserIsAtLeastInstructorStub.mockReturnValue(false);
jest.spyOn(metisService, 'getCurrentConversation').mockReturnValue(channelConversation);
component.ngOnInit();
@@ -207,12 +233,49 @@ describe('PostReactionsBarComponent', () => {
expect(getEditButton()).not.toBeNull();
});
+ it('should display the delete option to tutor if posting is in course-wide channel from a student', () => {
+ metisServiceUserIsAtLeastTutorStub.mockReturnValue(true);
+ metisServiceUserIsAuthorOfPostingStub.mockReturnValue(false);
+ const channelConversation = {
+ type: ConversationType.CHANNEL,
+ isCourseWide: true,
+ hasChannelModerationRights: true,
+ } as ChannelDTO;
+ jest.spyOn(metisService, 'getCurrentConversation').mockReturnValue(channelConversation);
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input({ ...metisResolvingAnswerPostUser1, post: { ...metisPostInChannel }, authorRole: UserRole.USER } as AnswerPost);
+ component.isEmojiCount = input(false);
+ });
+ component.ngOnInit();
+ fixture.detectChanges();
+ expect(getDeleteButton()).not.toBeNull();
+ });
+
+ it('should display edit and delete options to post author', () => {
+ metisServiceUserIsAuthorOfPostingStub.mockReturnValue(true);
+ fixture.detectChanges();
+ expect(getEditButton()).not.toBeNull();
+ expect(getDeleteButton()).not.toBeNull();
+ });
+
+ it('should not display the edit option to user (even instructor) if s/he is not the author of posting', () => {
+ metisServiceUserIsAtLeastInstructorStub.mockReturnValue(true);
+ metisServiceUserIsAuthorOfPostingStub.mockReturnValue(false);
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ expect(getEditButton()).toBeNull();
+ });
+
it('should not display edit and delete options when user is not the author and lacks permissions', () => {
- component.readOnlyMode = false;
- component.previewMode = false;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.isReadOnlyMode = input(false);
+ component.previewMode = input(false);
+ component.posting = input({ conversation: { isCourseWide: false } } as Post);
+ });
jest.spyOn(metisService, 'metisUserIsAuthorOfPosting').mockReturnValue(false);
jest.spyOn(metisService, 'metisUserIsAtLeastInstructorInCourse').mockReturnValue(false);
- component.posting = { conversation: { isCourseWide: false } } as Post;
component.ngOnInit();
fixture.detectChanges();
@@ -225,8 +288,15 @@ describe('PostReactionsBarComponent', () => {
metisServiceUserIsAtLeastInstructorStub.mockReturnValue(false);
metisServiceUserIsAtLeastTutorStub.mockReturnValue(true);
metisServiceUserIsAuthorOfPostingStub.mockReturnValue(false);
- component.posting = { ...metisPostInChannel };
- component.posting.authorRole = UserRole.USER;
+ const channelConversation = {
+ type: ConversationType.CHANNEL,
+ isCourseWide: true,
+ hasChannelModerationRights: true,
+ } as ChannelDTO;
+ jest.spyOn(metisService, 'getCurrentConversation').mockReturnValue(channelConversation);
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input({ ...metisPostInChannel, authorRole: UserRole.USER });
+ });
component.ngOnInit();
fixture.detectChanges();
expect(getEditButton()).toBeNull();
@@ -236,7 +306,9 @@ describe('PostReactionsBarComponent', () => {
it('should not display edit and delete options to tutor if posting is announcement', () => {
metisServiceUserIsAtLeastInstructorStub.mockReturnValue(false);
metisServiceUserIsAuthorOfPostingStub.mockReturnValue(false);
- component.posting = metisAnnouncement;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(metisAnnouncement);
+ });
component.ngOnInit();
fixture.detectChanges();
expect(getEditButton()).toBeNull();
@@ -246,7 +318,9 @@ describe('PostReactionsBarComponent', () => {
it('should display edit and delete options to instructor if his posting is announcement', () => {
metisServiceUserIsAtLeastInstructorStub.mockReturnValue(true);
metisServiceUserIsAuthorOfPostingStub.mockReturnValue(true);
- component.posting = metisAnnouncement;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(metisAnnouncement);
+ });
component.ngOnInit();
fixture.detectChanges();
expect(getEditButton()).not.toBeNull();
@@ -257,8 +331,15 @@ describe('PostReactionsBarComponent', () => {
metisServiceUserIsAtLeastInstructorStub.mockReturnValue(true);
metisServiceUserIsAtLeastTutorStub.mockReturnValue(true);
metisServiceUserIsAuthorOfPostingStub.mockReturnValue(false);
- component.posting = { ...metisPostInChannel };
- component.posting.authorRole = UserRole.USER;
+ const channelConversation = {
+ type: ConversationType.CHANNEL,
+ isCourseWide: true,
+ hasChannelModerationRights: true,
+ } as ChannelDTO;
+ jest.spyOn(metisService, 'getCurrentConversation').mockReturnValue(channelConversation);
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input({ ...metisPostInChannel, authorRole: UserRole.USER });
+ });
component.ngOnInit();
fixture.detectChanges();
@@ -271,16 +352,17 @@ describe('PostReactionsBarComponent', () => {
{ type: ConversationType.GROUP_CHAT, creator: { id: 99 } },
{ type: ConversationType.ONE_TO_ONE },
])('should initialize user authority and reactions correctly with same user', (dto: ConversationDTO) => {
- component.posting!.author!.id = 99;
jest.spyOn(metisService, 'getCurrentConversation').mockReturnValue(dto);
jest.spyOn(accountService, 'userIdentity', 'get').mockReturnValue({ id: 99 } as User);
reactionToDelete.user = { id: 99 } as User;
post.reactions = [reactionToDelete];
- component.posting = post;
-
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ post.author!!.id = 99;
+ component.posting = input(post);
+ component.isEmojiCount = input(true);
+ });
component.ngOnInit();
- component.isEmojiCount = true;
fixture.detectChanges();
expect(component.reactionMetaDataMap).toEqual({
smile: {
@@ -308,14 +390,17 @@ describe('PostReactionsBarComponent', () => {
const metisServiceCreateReactionMock = jest.spyOn(metisService, 'createReaction');
reactionToCreate = new Reaction();
reactionToCreate.emojiId = '+1';
- reactionToCreate.post = component.posting;
+ reactionToCreate.post = component.posting();
component.addOrRemoveReaction(reactionToCreate.emojiId);
expect(metisServiceCreateReactionMock).toHaveBeenCalledWith(reactionToCreate);
expect(component.showReactionSelector).toBeFalsy();
});
it('should invoke metis service method with own reaction to delete it', () => {
- component.posting!.author!.id = 99;
+ post.author!.id = 99;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(post);
+ });
component.ngOnInit();
fixture.detectChanges();
const metisServiceDeleteReactionMock = jest.spyOn(metisService, 'deleteReaction');
@@ -324,14 +409,21 @@ describe('PostReactionsBarComponent', () => {
expect(component.showReactionSelector).toBeFalsy();
});
+ it('should invoke metis service method with own reaction to remove it', () => {
+ component.ngOnInit();
+ const addOrRemoveSpy = jest.spyOn(component, 'addOrRemoveReaction');
+ component.updateReaction(reactionToDelete.emojiId!);
+ expect(addOrRemoveSpy).toHaveBeenCalledWith(reactionToDelete.emojiId!);
+ });
+
it('should invoke metis service method when pin icon is toggled', () => {
jest.spyOn(metisService, 'getCurrentConversation').mockReturnValue({ type: ConversationType.CHANNEL, hasChannelModerationRights: true } as ChannelDTO);
component.ngOnInit();
fixture.detectChanges();
const pinEmoji = getElement(debugElement, '.pin');
pinEmoji.click();
- component.posting.displayPriority = DisplayPriority.PINNED;
- expect(metisServiceUpdateDisplayPriorityMock).toHaveBeenCalledWith(component.posting.id!, DisplayPriority.PINNED);
+ (component.posting() as Post)!.displayPriority = DisplayPriority.PINNED;
+ expect(metisServiceUpdateDisplayPriorityMock).toHaveBeenCalledWith(component.posting()!.id!, DisplayPriority.PINNED);
component.ngOnChanges();
// set correct tooltips for tutor and post that is pinned and not archived
expect(component.pinTooltip).toBe('artemisApp.metis.removePinPostTooltip');
@@ -340,7 +432,10 @@ describe('PostReactionsBarComponent', () => {
it('should show non-clickable pin emoji with correct tooltip for student when post is pinned', () => {
metisCourse.isAtLeastTutor = false;
metisService.setCourse(metisCourse);
- component.posting.displayPriority = DisplayPriority.PINNED;
+ post.displayPriority = DisplayPriority.PINNED;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(post);
+ });
component.ngOnInit();
fixture.detectChanges();
const pinEmoji = getElement(debugElement, '.pin.reaction-button--not-hoverable');
@@ -352,25 +447,31 @@ describe('PostReactionsBarComponent', () => {
});
it('should display button to show single answer', () => {
- component.posting = post;
- component.sortedAnswerPosts = [metisPostExerciseUser1];
- component.showAnswers = false;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(post);
+ component.sortedAnswerPosts = input([metisPostExerciseUser1]);
+ component.showAnswers = input(false);
+ });
fixture.detectChanges();
const answerNowButton = fixture.debugElement.query(By.css('.expand-answers-btn')).nativeElement;
expect(answerNowButton.innerHTML).toContain('showSingleAnswer');
});
it('should display button to show multiple answers', () => {
- component.posting = post;
- component.showAnswers = false;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(post);
+ component.showAnswers = input(false);
+ });
fixture.detectChanges();
const answerNowButton = fixture.debugElement.query(By.css('.expand-answers-btn')).nativeElement;
expect(answerNowButton.innerHTML).toContain('showMultipleAnswers');
});
it('should display button to collapse answers', () => {
- component.posting = post;
- component.showAnswers = true;
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(post);
+ component.showAnswers = input(true);
+ });
fixture.detectChanges();
const answerNowButton = fixture.debugElement.query(By.css('.collapse-answers-btn')).nativeElement;
expect(answerNowButton.innerHTML).toContain('collapseAnswers');
@@ -395,4 +496,113 @@ describe('PostReactionsBarComponent', () => {
expect(showAnswersChangeSpy).toHaveBeenCalledWith(false);
expect(closePostingCreateEditModalSpy).toHaveBeenCalled();
});
+
+ it('should not display edit and delete options to users that are neither author or tutor', () => {
+ metisServiceUserIsAtLeastTutorStub.mockReturnValue(false);
+ metisServiceUserIsAuthorOfPostingStub.mockReturnValue(false);
+ metisServiceUserIsAtLeastInstructorStub.mockReturnValue(false);
+ fixture.detectChanges();
+ expect(getEditButton()).toBeNull();
+ expect(getDeleteButton()).toBeNull();
+ });
+
+ it('should emit event to create embedded view when edit icon is clicked', () => {
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(metisResolvingAnswerPostUser1);
+ });
+ const openPostingCreateEditModalEmitSpy = jest.spyOn(component.openPostingCreateEditModal, 'emit');
+ metisServiceUserIsAuthorOfPostingStub.mockReturnValue(true);
+ fixture.detectChanges();
+ getElement(debugElement, '.edit').click();
+ expect(openPostingCreateEditModalEmitSpy).toHaveBeenCalledOnce();
+ });
+
+ it('answer now button should be invisible if answer is not the last one', () => {
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(post);
+ component.isLastAnswer = input(false);
+ });
+ fixture.detectChanges();
+ const answerNowButton = fixture.debugElement.query(By.css('.reply-btn'));
+ expect(answerNowButton).toBeNull();
+ });
+
+ it('should invoke metis service when toggle resolve is clicked', () => {
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ unApprovedAnswerPost1.post = post;
+ component.posting = input(unApprovedAnswerPost1);
+ component.isEmojiCount = input(false);
+
+ metisServiceUserIsAtLeastTutorStub.mockReturnValue(true);
+ fixture.detectChanges();
+ expect(getResolveButton()).not.toBeNull();
+ const previousState = (component.posting() as AnswerPost).resolvesPost;
+ component.toggleResolvesPost();
+ expect(component.getResolvesPost()).toEqual(!previousState);
+ expect(metisServiceUpdateAnswerPostMock).toHaveBeenCalledOnce();
+ });
+ });
+
+ it('should create a Reaction with answerPost when posting type is answerPost', () => {
+ const answerPost = new AnswerPost();
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(answerPost);
+ });
+
+ const reaction = component.buildReaction('thumbsup');
+ expect(reaction.answerPost).toBe(answerPost);
+ expect(reaction.post).toBeUndefined();
+ });
+
+ it('should create a Reaction with post when posting type is post', () => {
+ const post = new Post();
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(post);
+ });
+
+ const reaction = component.buildReaction('thumbsup');
+ expect(reaction.post).toBe(post);
+ expect(reaction.answerPost).toBeUndefined();
+ });
+
+ it('should not toggle pin when user has no permission', () => {
+ const channelConversation = {
+ type: ConversationType.CHANNEL,
+ hasChannelModerationRights: false,
+ } as ChannelDTO;
+ component.setCanPin(channelConversation);
+ fixture.detectChanges();
+ component.togglePin();
+ expect(metisServiceUpdateDisplayPriorityMock).not.toHaveBeenCalled();
+ });
+
+ it('should emit isDeleteEvent when deletePosting is called', () => {
+ const spy = jest.spyOn(component.isDeleteEvent, 'emit');
+ component.deletePosting();
+ expect(spy).toHaveBeenCalledWith(true);
+ });
+
+ it('should toggle pin and update displayPriority when user has permission', () => {
+ jest.spyOn(metisService, 'metisUserIsAtLeastTutorInCourse').mockReturnValue(true);
+
+ const moderatorChannel = {
+ type: ConversationType.CHANNEL,
+ hasChannelModerationRights: true,
+ } as ChannelDTO;
+ jest.spyOn(metisService, 'getCurrentConversation').mockReturnValue(moderatorChannel);
+
+ runInInjectionContext(fixture.debugElement.injector, () => {
+ component.posting = input(post);
+ });
+ component.ngOnInit();
+ expect(component.displayPriority).toBe(DisplayPriority.NONE);
+
+ component.togglePin();
+ expect(metisServiceUpdateDisplayPriorityMock).toHaveBeenCalledWith(post.id!, DisplayPriority.PINNED);
+ expect(component.displayPriority).toBe(DisplayPriority.PINNED);
+
+ component.togglePin();
+ expect(metisServiceUpdateDisplayPriorityMock).toHaveBeenCalledWith(post.id!, DisplayPriority.NONE);
+ expect(component.displayPriority).toBe(DisplayPriority.NONE);
+ });
});
- @for (reactionMetaData of reactionMetaDataMap | keyvalue; track reactionMetaData) {
- @if (isEmojiCount) {
-
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts
deleted file mode 100644
index e68da18c1d0b..000000000000
--- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { Component, EventEmitter, Input, OnChanges, OnInit, Output, output } from '@angular/core';
-import { Reaction } from 'app/entities/metis/reaction.model';
-import { ConfirmIconComponent } from 'app/shared/confirm-icon/confirm-icon.component';
-import { EmojiPickerComponent } from 'app/shared/metis/emoji/emoji-picker.component';
-import { EmojiComponent } from 'app/shared/metis/emoji/emoji.component';
-import { PostingsReactionsBarDirective } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive';
-import { AnswerPost } from 'app/entities/metis/answer-post.model';
-import { faCheck, faPencilAlt, faSmile } from '@fortawesome/free-solid-svg-icons';
-import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model';
-import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
-import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
-import { FaIconComponent } from '@fortawesome/angular-fontawesome';
-import { AsyncPipe, KeyValuePipe, NgClass } from '@angular/common';
-import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
-import { ReactingUsersOnPostingPipe } from 'app/shared/pipes/reacting-users-on-posting.pipe';
-
-@Component({
- selector: 'jhi-answer-post-reactions-bar',
- templateUrl: './answer-post-reactions-bar.component.html',
- styleUrls: ['../posting-reactions-bar.component.scss'],
- imports: [
- NgbTooltip,
- EmojiComponent,
- CdkOverlayOrigin,
- FaIconComponent,
- CdkConnectedOverlay,
- EmojiPickerComponent,
- NgClass,
- ConfirmIconComponent,
- AsyncPipe,
- KeyValuePipe,
- ArtemisTranslatePipe,
- ReactingUsersOnPostingPipe,
- ],
-})
-export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirective
-
-
- }
- }
-
- @if ((isAnyReactionCountAboveZero() && isEmojiCount) || !isEmojiCount) {
-
-
- @if (!isReadOnlyMode) {
-
-
-
- }
-
- }
- @if (!isEmojiCount) {
- @if (!isAnswerOfAnnouncement && (isAtLeastTutorInCourse || isAuthorOfOriginalPost)) {
-
- }
- @if (mayEdit) {
-
- }
- @if (mayDelete) {
-
- }
-
- }
-
-
- @if (hoverBar && sortedAnswerPosts.length === 0) {
-
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts
deleted file mode 100644
index 963c4f99ac14..000000000000
--- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, ViewChild, inject, output } from '@angular/core';
-import { Reaction } from 'app/entities/metis/reaction.model';
-import { Post } from 'app/entities/metis/post.model';
-import { ConfirmIconComponent } from 'app/shared/confirm-icon/confirm-icon.component';
-import { TranslateDirective } from 'app/shared/language/translate.directive';
-import { EmojiPickerComponent } from 'app/shared/metis/emoji/emoji-picker.component';
-import { EmojiComponent } from 'app/shared/metis/emoji/emoji.component';
-import { PostingsReactionsBarDirective } from 'app/shared/metis/posting-reactions-bar/posting-reactions-bar.directive';
-import { DisplayPriority } from 'app/shared/metis/metis.util';
-import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import { faArrowRight, faPencilAlt, faSmile } from '@fortawesome/free-solid-svg-icons';
-import { AnswerPost } from 'app/entities/metis/answer-post.model';
-import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
-import dayjs from 'dayjs/esm';
-import { getAsChannelDTO, isChannelDTO } from 'app/entities/metis/conversation/channel.model';
-import { isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model';
-import { AccountService } from 'app/core/auth/account.service';
-import { isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model';
-import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model';
-import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component';
-import { FaIconComponent } from '@fortawesome/angular-fontawesome';
-import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
-import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
-import { AsyncPipe, KeyValuePipe } from '@angular/common';
-import { ReactingUsersOnPostingPipe } from 'app/shared/pipes/reacting-users-on-posting.pipe';
-
-@Component({
- selector: 'jhi-post-reactions-bar',
- templateUrl: './post-reactions-bar.component.html',
- styleUrls: ['../posting-reactions-bar.component.scss'],
- imports: [
- FaIconComponent,
- TranslateDirective,
- EmojiComponent,
- NgbTooltip,
- CdkOverlayOrigin,
- CdkConnectedOverlay,
- EmojiPickerComponent,
- PostCreateEditModalComponent,
- ConfirmIconComponent,
- AsyncPipe,
- KeyValuePipe,
- ArtemisTranslatePipe,
- ReactingUsersOnPostingPipe,
- ],
-})
-export class PostReactionsBarComponent extends PostingsReactionsBarDirective
-
-
- }
- @if (!isCommunicationPage) {
- @if (sortedAnswerPosts.length) {
-
- @if (showAnswers) {
-
-
-
- } @else {
-
-
-
-
- }
- }
- } @else {
- @if (!isThreadSidebar) {
-
- @if (!showAnswers && sortedAnswerPosts.length) {
-
-
-
- }
- }
- }
- @for (reactionMetaData of reactionMetaDataMap | keyvalue; track reactionMetaData) {
- @if (isEmojiCount) {
-
-
-
- }
- }
-
- @if ((isAnyReactionCountAboveZero() && isEmojiCount) || !isEmojiCount) {
-
-
-
-
- @if (!readOnlyMode) {
-
- }
-
-
- }
-
- @if (!isEmojiCount && mayEdit) {
-
- }
-
- @if (!isEmojiCount && mayDelete) {
-
- }
- @if (!isEmojiCount && (displayPriority === DisplayPriority.PINNED || canPin)) {
-
- }
- @if (!isEmojiCount) {
-
- }
-
-
- @if (isEmojiCount && getShowNewMessageIcon()) {
-
- }
-
-
+ @if (getPostingType() === 'post') {
+ @if (hoverBar() && sortedAnswerPosts()?.length === 0) {
+
diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.ts
new file mode 100644
index 000000000000..1dc6ddce5c0e
--- /dev/null
+++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/posting-reactions-bar.component.ts
@@ -0,0 +1,484 @@
+import { Component, OnChanges, OnInit, inject, input, output, viewChild } from '@angular/core';
+import { Posting } from 'app/entities/metis/posting.model';
+import { MetisService } from 'app/shared/metis/metis.service';
+import { EmojiData } from '@ctrl/ngx-emoji-mart/ngx-emoji';
+import { Reaction } from 'app/entities/metis/reaction.model';
+import { PLACEHOLDER_USER_REACTED, ReactingUsersOnPostingPipe } from 'app/shared/pipes/reacting-users-on-posting.pipe';
+import { faArrowRight, faBookmark, faCheck, faPencilAlt, faSmile, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import { EmojiComponent } from 'app/shared/metis/emoji/emoji.component';
+import { EmojiPickerComponent } from 'app/shared/metis/emoji/emoji-picker.component';
+import { ConfirmIconComponent } from 'app/shared/confirm-icon/confirm-icon.component';
+import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
+import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
+import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { AsyncPipe, KeyValuePipe, NgClass } from '@angular/common';
+import { AccountService } from 'app/core/auth/account.service';
+import { DisplayPriority } from '../metis.util';
+import { Post } from 'app/entities/metis/post.model';
+import { Conversation, ConversationDTO } from 'app/entities/metis/conversation/conversation.model';
+import { getAsChannelDTO, isChannelDTO } from 'app/entities/metis/conversation/channel.model';
+import { isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model';
+import { isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model';
+import { AnswerPost } from 'app/entities/metis/answer-post.model';
+import dayjs from 'dayjs';
+import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component';
+import { TranslateDirective } from 'app/shared/language/translate.directive';
+
+const PIN_EMOJI_ID = 'pushpin';
+const ARCHIVE_EMOJI_ID = 'open_file_folder';
+const HEAVY_MULTIPLICATION_ID = 'heavy_multiplication_x';
+
+const SPEECH_BALLOON_UNICODE = '1F4AC';
+const ARCHIVE_EMOJI_UNICODE = '1F4C2';
+const PIN_EMOJI_UNICODE = '1F4CC';
+const HEAVY_MULTIPLICATION_UNICODE = '2716';
+
+/**
+ * event triggered by the emoji mart component, including EmojiData
+ */
+interface ReactionEvent {
+ $event: Event;
+ emoji?: EmojiData;
+}
+
+/**
+ * represents the amount of users that reacted
+ * hasReacted indicates if the currently logged-in user is among those counted users
+ */
+interface ReactionMetaData {
+ count: number;
+ hasReacted: boolean;
+ reactingUsers: string[];
+}
+
+/**
+ * data structure used for displaying emoji reactions with metadata on postings
+ */
+interface ReactionMetaDataMap {
+ [emojiId: string]: ReactionMetaData;
+}
+
+@Component({
+ selector: 'jhi-posting-reactions-bar',
+ templateUrl: './posting-reactions-bar.component.html',
+ styleUrls: ['./posting-reactions-bar.component.scss'],
+ imports: [
+ NgbTooltip,
+ EmojiComponent,
+ CdkOverlayOrigin,
+ FaIconComponent,
+ TranslateDirective,
+ CdkConnectedOverlay,
+ EmojiPickerComponent,
+ ConfirmIconComponent,
+ AsyncPipe,
+ KeyValuePipe,
+ ArtemisTranslatePipe,
+ ReactingUsersOnPostingPipe,
+ NgClass,
+ PostCreateEditModalComponent,
+ ],
+})
+export class PostingReactionsBarComponent
+
+
+ }
+ @if (!isCommunicationPage() && sortedAnswerPosts()?.length) {
+ @if (showAnswers()) {
+
+
+
+ } @else {
+
+
+
+ }
+ } @else if (!isThreadSidebar() && !showAnswers() && sortedAnswerPosts()?.length) {
+
+
+
+ }
+ }
+
+ @for (reactionMetaData of reactionMetaDataMap | keyvalue; track reactionMetaData) {
+ @if (isEmojiCount()) {
+
+
+
+ }
+ }
+
+
+
+ @if ((isAnyReactionCountAboveZero() && isEmojiCount()) || !isEmojiCount()) {
+
+
+
+
+
+
+
+ }
+
+
+ @if (!isEmojiCount() && mayEdit) {
+
+ }
+
+
+ @if (!isEmojiCount() && mayDelete) {
+
+ }
+
+
+ @if (getPostingType() === 'answerPost' && !isEmojiCount()) {
+ @if (!isAnswerOfAnnouncement && (isAtLeastTutorInCourse || isAuthorOfOriginalPost)) {
+
+ }
+ }
+
+
+ @if (getPostingType() === 'post' && !isEmojiCount() && (displayPriority === DisplayPriority.PINNED || canPin)) {
+
+ }
+
+
+ @if (!isEmojiCount()) {
+
+ }
+
+
+
+ @if (getPostingType() === 'post' && isEmojiCount() && getShowNewMessageIcon()) {
+
+ }
+