-
Notifications
You must be signed in to change notification settings - Fork 305
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
: Allow user to forward messages
#9794
Conversation
…e-message-view' into feature/communication/consecutive-message-view
…-view # Conflicts: # src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts # src/main/webapp/app/shared/metis/answer-post/answer-post.component.scss
src/main/java/de/tum/cit/aet/artemis/communication/repository/ForwardedMessageRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/repository/ForwardedMessageRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add security checks (important!!!) and adapt the REST calls as specified in my inline comments. The requirement with the module prefix is new, but I'd like to address this here as well. However, the missing security checks are really problematic, please take this into account in general (we have documentation in the server coding guidelines)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🧹 Nitpick comments (7)
src/main/webapp/app/shared/metis/metis.service.ts (1)
768-825
: Refactor long method into smaller functions.The method is quite long and handles multiple responsibilities. Consider breaking it down into smaller, more focused functions for better maintainability.
Apply this diff to extract the creation of forwarded messages into a separate method:
+private createForwardedMessageObjects(originalPosts: Posting[], createdPost: Post, sourceType: PostingType, newContent: string): ForwardedMessage[] { + return originalPosts.map( + (post) => new ForwardedMessage(undefined, post.id, sourceType, { id: createdPost.id } as Post, undefined, newContent || ''), + ); +} +private updateCachedPosts(createdPost: Post, createdForwardedMessages: ForwardedMessage[], targetConversationId: number): void { + if (targetConversationId === this.currentConversation?.id) { + const existingPostIndex = this.cachedPosts.findIndex((post) => post.id === createdPost.id); + if (existingPostIndex === -1) { + this.cachedPosts = [createdPost, ...this.cachedPosts]; + } + + createdForwardedMessages.forEach((fm) => { + const postIndex = this.cachedPosts.findIndex((post) => post.id === fm.destinationPost?.id); + if (postIndex > -1) { + const post = this.cachedPosts[postIndex]; + const updatedPost = { ...post, hasForwardedMessages: true }; + this.cachedPosts[postIndex] = updatedPost; + } + }); + this.posts$.next(this.cachedPosts); + this.cachedTotalNumberOfPosts += 1; + this.totalNumberOfPosts$.next(this.cachedTotalNumberOfPosts); + } +} createForwardedMessages(originalPosts: Posting[], targetConversation: Conversation, isAnswer: boolean, newContent?: string): Observable<ForwardedMessage[]> { if (!this.courseId) { return throwError(() => new Error('Course ID is not set. Ensure that setCourse() is called before forwarding posts.')); } const newPost: Post = { content: newContent || '', conversation: targetConversation, hasForwardedMessages: true, }; const sourceType = isAnswer ? PostingType.ANSWER : PostingType.POST; return this.postService.create(this.courseId, newPost).pipe( switchMap((createdPost: HttpResponse<Post>) => { const createdPostBody = createdPost.body!; - const forwardedMessages: ForwardedMessage[] = originalPosts.map( - (post) => new ForwardedMessage(undefined, post.id, sourceType, { id: createdPostBody.id } as Post, undefined, newContent || ''), - ); + const forwardedMessages = this.createForwardedMessageObjects(originalPosts, createdPostBody, sourceType, newContent || ''); const createForwardedMessageObservables = forwardedMessages.map((fm) => this.forwardedMessageService.createForwardedMessage(fm, this.courseId).pipe(map((res: HttpResponse<ForwardedMessage>) => res.body!)), ); return forkJoin(createForwardedMessageObservables).pipe( - tap((createdForwardedMessages: ForwardedMessage[]) => { - if (targetConversation.id === this.currentConversation?.id) { - const existingPostIndex = this.cachedPosts.findIndex((post) => post.id === createdPostBody.id); - if (existingPostIndex === -1) { - this.cachedPosts = [createdPostBody, ...this.cachedPosts]; - } - - createdForwardedMessages.forEach((fm) => { - const postIndex = this.cachedPosts.findIndex((post) => post.id === fm.destinationPost?.id); - if (postIndex > -1) { - const post = this.cachedPosts[postIndex]; - const updatedPost = { ...post, hasForwardedMessages: true }; - this.cachedPosts[postIndex] = updatedPost; - } - }); - this.posts$.next(this.cachedPosts); - this.cachedTotalNumberOfPosts += 1; - this.totalNumberOfPosts$.next(this.cachedTotalNumberOfPosts); - } - }), + tap((createdForwardedMessages: ForwardedMessage[]) => + this.updateCachedPosts(createdPostBody, createdForwardedMessages, targetConversation.id) + ), catchError((error) => throwError(() => error)), ); }), catchError((error) => throwError(() => error)), ); }Also, consider extracting error messages to constants:
private static readonly COURSE_ID_NOT_SET_ERROR = 'Course ID is not set. Ensure that setCourse() is called before forwarding posts.';src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java (2)
217-217
: Inconsistent endpoint path naming.The endpoint path uses inconsistent naming:
- Uses "communication" prefix while other endpoints don't
- Uses "messages-source-posts" instead of following the pattern of other endpoints
- Uses hyphen instead of camelCase
Apply this diff to align with the existing endpoint naming pattern:
- @GetMapping("communication/courses/{courseId}/messages-source-posts") + @GetMapping("courses/{courseId}/messages/source")
209-216
: Enhance API documentation.The JavaDoc should include information about error responses and validation requirements.
Apply this diff to improve the documentation:
/** * GET /courses/{courseId}/messages/source-posts : Retrieve posts by their IDs * * @param courseId id of the course the posts belong to * @param postIds list of IDs of the posts to retrieve * @return ResponseEntity with status 200 (OK) containing the list of posts in the response body, - * or with status 400 (Bad Request) if the checks on user, course or post validity fail + * or with status 400 (Bad Request) if postIds is null or empty, + * or with status 404 (Not Found) if no posts exist for the given IDs, + * or with status 403 (Forbidden) if the user doesn't have access to the course */src/main/java/de/tum/cit/aet/artemis/communication/dto/ForwardedMessageDTO.java (1)
33-52
: Consider validating entity state before returning.The
toEntity
method should validate that eitherdestinationPost
ordestinationAnswerPost
is set, but not both.public ForwardedMessage toEntity() { ForwardedMessage message = new ForwardedMessage(); message.setId(this.id); message.setSourceId(this.sourceId); message.setSourceType(this.sourceType); + // Validate that exactly one destination is set + if ((this.destinationPostId != null && this.destinationAnswerPostId != null) || + (this.destinationPostId == null && this.destinationAnswerPostId == null)) { + throw new IllegalStateException("Exactly one destination must be set"); + } if (this.destinationPostId != null) { Post post = new Post(); post.setId(this.destinationPostId); message.setDestinationPost(post); } if (this.destinationAnswerPostId != null) { AnswerPost answerPost = new AnswerPost(); answerPost.setId(this.destinationAnswerPostId); message.setDestinationAnswerPost(answerPost); } return message; }src/main/webapp/app/entities/metis/forwarded-message.model.ts (1)
23-27
: Simplify validation logic.The validation can be simplified using nullish coalescing operator.
private validateDestinations(): boolean { - const isDestinationPostValid = this.destinationPost !== undefined && this.destinationPost !== null; - const isDestinationAnswerPostValid = this.destinationAnswerPost !== undefined && this.destinationAnswerPost !== null; - return (isDestinationPostValid && !isDestinationAnswerPostValid) || (!isDestinationPostValid && isDestinationAnswerPostValid); + return (this.destinationPost ?? null) !== null !== (this.destinationAnswerPost ?? null) !== null; }src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java (1)
95-123
: Add pagination support for large result sets.The endpoint should support pagination to handle large numbers of forwarded messages efficiently.
@GetMapping("forwarded-messages") @EnforceAtLeastStudentInCourse -public ResponseEntity<List<ForwardedMessagesGroupDTO>> getForwardedMessages(@RequestParam Long courseId, @RequestParam Set<Long> postingIds, @RequestParam PostingType type) { +public ResponseEntity<Page<ForwardedMessagesGroupDTO>> getForwardedMessages( + @RequestParam Long courseId, + @RequestParam Set<Long> postingIds, + @RequestParam PostingType type, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { // ... existing validation code ... Set<ForwardedMessage> forwardedMessages; if (type == PostingType.POST) { - forwardedMessages = forwardedMessageRepository.findAllByDestinationPostIds(postingIds); + forwardedMessages = forwardedMessageRepository.findAllByDestinationPostIds(postingIds, PageRequest.of(page, size)); } else { - forwardedMessages = forwardedMessageRepository.findAllByDestinationAnswerPostIds(postingIds); + forwardedMessages = forwardedMessageRepository.findAllByDestinationAnswerPostIds(postingIds, PageRequest.of(page, size)); } // ... rest of the code ... }src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts (1)
329-402
: Consider refactoring the forwarded messages handling logic.The current implementation has high cyclomatic complexity with multiple nested conditions and API calls. Consider breaking it down into smaller, more manageable methods.
Here's a suggested refactoring approach:
- if (postIdsWithForwardedMessages.length > 0) { - this.metisService.getForwardedMessagesByIds(postIdsWithForwardedMessages, PostingType.POST)?.subscribe((response) => { + private fetchForwardedMessages(postIds: number[]): void { + if (postIds.length === 0) { + this.groupPosts(); + return; + } + + this.metisService.getForwardedMessagesByIds(postIds, PostingType.POST)?.subscribe({ + next: (response) => this.handleForwardedMessagesResponse(response), + error: (error) => { + console.error('Error fetching forwarded messages:', error); + this.groupPosts(); + } + }); + } + + private handleForwardedMessagesResponse(response: any): void { + if (!response?.body) { + this.groupPosts(); + return; + } + + const forwardedMessagesGroups = response.body; + const map = new Map<number, ForwardedMessageDTO[]>( + forwardedMessagesGroups.map((group) => [group.id, group.messages]) + ); + + this.fetchSourceMessages(this.extractSourceIds(map)); + }
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
src/main/java/de/tum/cit/aet/artemis/communication/dto/ForwardedMessageDTO.java
(1 hunks)src/main/java/de/tum/cit/aet/artemis/communication/repository/ForwardedMessageRepository.java
(1 hunks)src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java
(3 hunks)src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
(2 hunks)src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
(1 hunks)src/main/webapp/app/entities/metis/forwarded-message.model.ts
(1 hunks)src/main/webapp/app/overview/course-conversations/course-conversations.component.ts
(2 hunks)src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts
(5 hunks)src/main/webapp/app/shared/metis/answer-post.service.ts
(2 hunks)src/main/webapp/app/shared/metis/forwarded-message.service.ts
(1 hunks)src/main/webapp/app/shared/metis/metis.service.ts
(4 hunks)src/main/webapp/app/shared/metis/post.service.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/webapp/app/shared/metis/post.service.ts
- src/main/java/de/tum/cit/aet/artemis/communication/repository/ForwardedMessageRepository.java
- src/main/webapp/app/overview/course-conversations/course-conversations.component.ts
🧰 Additional context used
📓 Path-based instructions (2)
`src/main/java/**/*.java`: naming:CamelCase; principles:{sin...
src/main/java/**/*.java
: naming:CamelCase; principles:{single_responsibility,small_methods,no_duplication}; db:{perf_queries,datetime_not_timestamp}; rest:{stateless,singleton,delegate_logic,http_only,minimal_dtos}; dtos:{java_records,no_entities,min_data,single_resp}; di:constructor_injection; kiss:simple_code; file_handling:os_indep_paths; practices:{least_access,avoid_transactions,code_reuse,static_member_ref,prefer_primitives}; sql:{param_annotation,uppercase,avoid_subqueries};java:avoid_star_imports
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
src/main/java/de/tum/cit/aet/artemis/communication/dto/ForwardedMessageDTO.java
src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java
`src/main/webapp/**/*.ts`: angular_style:https://angular.io/...
src/main/webapp/app/shared/metis/answer-post.service.ts
src/main/webapp/app/entities/metis/forwarded-message.model.ts
src/main/webapp/app/shared/metis/forwarded-message.service.ts
src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts
src/main/webapp/app/shared/metis/metis.service.ts
🧠 Learnings (1)
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java (1)
Learnt from: PaRangger
PR: ls1intum/Artemis#9794
File: src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java:79-79
Timestamp: 2024-12-10T12:02:00.447Z
Learning: In `ForwardedMessageResource.java`, when improving type safety for parameters representing posting types, prefer using the existing `PostingType` enum instead of defining new enums.
⏰ Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: server-tests
- GitHub Check: Call Build Workflow / Build and Push Docker Image
- GitHub Check: Analyse
🔇 Additional comments (12)
src/main/webapp/app/shared/metis/metis.service.ts (5)
3-3
: LGTM!The imports are well-organized and properly scoped for the forwarded message functionality.
Also applies to: 8-8, 13-13, 36-38
48-48
: LGTM!The ForwardedMessageService is properly injected using Angular's inject function.
727-733
: Return an empty Observable instead of undefined.For consistency with RxJS patterns and to simplify error handling for consumers, return an empty Observable instead of undefined.
Apply this diff:
-getForwardedMessagesByIds(postingIds: number[], type: PostingType): Observable<HttpResponse<{ id: number; messages: ForwardedMessageDTO[] }[]>> | undefined { +getForwardedMessagesByIds(postingIds: number[], type: PostingType): Observable<HttpResponse<{ id: number; messages: ForwardedMessageDTO[] }[]>> { if (postingIds && postingIds.length > 0) { return this.forwardedMessageService.getForwardedMessages(postingIds, type, this.courseId); } else { - return undefined; + return of(new HttpResponse<{ id: number; messages: ForwardedMessageDTO[] }[]>({ body: [] })); } }
741-744
: Add explicit return type and make the method private.The method lacks a return type annotation and should be private as suggested in past reviews.
Apply this diff:
-getSourcePostsByIds(postIds: number[]) { - if (postIds) return this.postService.getSourcePostsByIds(this.courseId, postIds); - else return; +private getSourcePostsByIds(postIds: number[]): Observable<Post[]> { + return postIds ? this.postService.getSourcePostsByIds(this.courseId, postIds) : of([]); }
752-755
: Add explicit return type and make the method private.Similar to getSourcePostsByIds, this method needs consistent return types and should be private.
Apply this diff:
-getSourceAnswerPostsByIds(answerPostIds: number[]) { - if (answerPostIds) return this.answerPostService.getSourceAnswerPostsByIds(this.courseId, answerPostIds); - else return; +private getSourceAnswerPostsByIds(answerPostIds: number[]): Observable<AnswerPost[]> { + return answerPostIds ? this.answerPostService.getSourceAnswerPostsByIds(this.courseId, answerPostIds) : of([]); }src/main/java/de/tum/cit/aet/artemis/communication/dto/ForwardedMessageDTO.java (2)
15-16
: LGTM! Well-structured DTO using Java record.The use of Java record with
@JsonInclude
annotation is a good choice for a DTO. It provides immutability and efficient JSON serialization.
23-26
: LGTM! Safe null handling in constructor.The constructor properly handles null values using Optional, preventing potential NPEs.
src/main/webapp/app/entities/metis/forwarded-message.model.ts (2)
6-13
: LGTM! Well-defined DTO interface.The interface properly defines all required fields with optional types.
41-50
: LGTM! Safe DTO conversion.The
toDTO
method safely handles optional fields using the optional chaining operator.src/main/webapp/app/shared/metis/forwarded-message.service.ts (1)
43-54
: LGTM! Proper error handling for invalid input.The method correctly handles empty IDs array and returns a typed error.
src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.ts (2)
45-48
: LGTM! Clean implementation of new imports and output event.The new imports and output event emitter are well-organized and follow Angular best practices.
Also applies to: 111-111
547-549
: LGTM! Clean implementation of navigation method.The navigation method is concise and follows Angular's event emission patterns.
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
Outdated
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
Outdated
Show resolved
Hide resolved
...verview/course-conversations/layout/conversation-messages/conversation-messages.component.ts
Show resolved
Hide resolved
src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (3)
src/main/webapp/app/overview/course-conversations/dialogs/forward-message-dialog/forward-message-dialog.component.ts (3)
95-99
: 🛠️ Refactor suggestionReplace setTimeout with queueMicrotask.
Using
setTimeout
for view initialization is not recommended. This is a duplicate of a past review comment.Apply this diff:
ngAfterViewInit(): void { - setTimeout(() => { + queueMicrotask(() => { this.checkIfContentOverflows(); - }, 0); + }); }
211-218
:⚠️ Potential issueAdd validation before sending.
The send method should validate selections and content before closing the modal. This is a duplicate of a past review comment.
Apply this diff:
send(): void { + if (!this.hasSelections()) { + return; + } + if (!this.newPost.content?.trim()) { + return; + } const selectedItems = { channels: this.selectedChannels, users: this.selectedUsers, messageContent: this.newPost.content, }; this.activeModal.close(selectedItems); }
122-152
: 🛠️ Refactor suggestionOptimize search with debounce.
The search functionality should include debounce to prevent excessive API calls. This is a duplicate of a past review comment.
Apply this diff:
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +private searchSubject = new Subject<string>(); +ngOnInit(): void { + this.setupSearch(); + // ... rest of the code +} +private setupSearch(): void { + this.searchSubject.pipe( + debounceTime(300), + distinctUntilChanged(), + ).subscribe(term => { + if (term.length >= 3) { + this.performSearch(term); + } + }); +} filterOptions(): void { if (this.searchTerm) { const lowerCaseSearchTerm = this.searchTerm.toLowerCase(); - if (lowerCaseSearchTerm.length >= 3) { - this.courseManagementService - .searchUsers(this.courseId()!, lowerCaseSearchTerm, ['students', 'tutors', 'instructors']) - .pipe( - map((response) => response.body || []), - map((users) => users.filter((user) => !this.selectedUsers.find((selectedUser) => selectedUser.id === user.id))), - catchError(() => { - return of([]); - }), - ) - .subscribe((users) => { - this.filteredUsers = users; - this.updateCombinedOptions(); - this.cdr.detectChanges(); - }); - } + this.searchSubject.next(lowerCaseSearchTerm); } }
🧹 Nitpick comments (7)
src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts (5)
27-27
: Simplify the import path for PostingType.The current import path is unnecessarily verbose and fragile. Consider using a more direct import path.
-import { PostingType } from '../../../../../../../../main/webapp/app/entities/metis/posting.model'; +import { PostingType } from 'app/entities/metis/posting.model';
276-291
: Improve type safety in mock data.Instead of using
unknown
type assertion, consider creating a proper mock object that matches theForwardedMessage
interface structure.-{ id: 101, sourceId: 10, sourceType: 'POST' } as unknown as ForwardedMessage, +{ + id: 101, + sourceId: 10, + sourceType: 'POST', + // Add other required properties from ForwardedMessage interface +} as ForwardedMessage,
319-328
: Enhance test specificity for better coverage.While the test verifies undefined properties, it could be more comprehensive by checking additional post properties to ensure the entire state is correct.
expect(component.posts[0].forwardedPosts).toBeUndefined(); expect(component.posts[0].forwardedAnswerPosts).toBeUndefined(); +expect(component.posts[0].hasForwardedMessages).toBeFalse(); +expect(component.posts[0].id).toBe(1);
330-347
: Document the error scenario being tested.The test should include a comment explaining the specific error scenario being tested to improve maintainability.
+// Test scenario: When source posts/answers are not found, the component should +// gracefully handle the missing data by setting empty arrays instead of undefined it('should handle forwarded messages with missing source gracefully', fakeAsync(() => {
368-408
: Reduce type assertion duplication using helper functions.Consider creating helper functions to generate mock data, reducing the need for repeated type assertions.
+const createMockForwardedMessage = (id: number, sourceId: number, sourceType: string): ForwardedMessage => ({ + id, + sourceId, + sourceType, + // Add other required properties +}); +const createMockPost = (id: number, content: string, conversation: Conversation): Post => ({ + id, + content, + conversation, + // Add other required properties +}); const mockForwardedMessages: ForwardedMessage[] = [ - { id: 101, sourceId: 10, sourceType: 'POST' } as unknown as ForwardedMessage, - { id: 102, sourceId: 11, sourceType: 'ANSWER' } as unknown as ForwardedMessage, + createMockForwardedMessage(101, 10, 'POST'), + createMockForwardedMessage(102, 11, 'ANSWER'), ];src/main/webapp/app/overview/course-conversations/dialogs/forward-message-dialog/forward-message-dialog.component.ts (2)
34-40
: Convert to standalone component.As suggested in the past review comments, this component should be converted to standalone for better modularity and tree-shaking.
Apply this diff:
@Component({ selector: 'jhi-forward-message-dialog', templateUrl: './forward-message-dialog.component.html', styleUrls: ['./forward-message-dialog.component.scss'], + standalone: true, imports: [ArtemisTranslatePipe, ProfilePictureComponent, NgClass, PostingContentComponent, MarkdownEditorMonacoComponent, FormsModule, TranslateDirective], providers: [MetisService, LinkPreviewService, LinkifyService], })
42-60
: Organize properties by type and consider using more signals.The properties should be organized by type with empty lines between groups. Also, consider converting more properties to signals for better reactivity.
Apply this diff to organize properties:
- channels = signal<ChannelDTO[] | []>([]); - users = signal<UserPublicInfoDTO[] | []>([]); - postToForward = signal<Post | undefined>(undefined); - courseId = signal<number | undefined>(undefined); - editorHeight = input<MarkdownEditorHeight>(MarkdownEditorHeight.INLINE); - filteredChannels: ChannelDTO[] = []; - filteredUsers: UserPublicInfoDTO[] = []; - selectedChannels: ChannelDTO[] = []; - selectedUsers: UserPublicInfoDTO[] = []; - combinedOptions: CombinedOption[] = []; - filteredOptions: CombinedOption[] = []; - defaultActions: TextEditorAction[]; - searchTerm: string = ''; - newPost = new Post(); - isInputFocused = false; - showDropdown = false; - showFullForwardedMessage = false; - isContentLong = false; + // Signals + channels = signal<ChannelDTO[] | []>([]); + users = signal<UserPublicInfoDTO[] | []>([]); + postToForward = signal<Post | undefined>(undefined); + courseId = signal<number | undefined>(undefined); + + // Inputs + editorHeight = input<MarkdownEditorHeight>(MarkdownEditorHeight.INLINE); + + // Arrays + filteredChannels = signal<ChannelDTO[]>([]); + filteredUsers = signal<UserPublicInfoDTO[]>([]); + selectedChannels = signal<ChannelDTO[]>([]); + selectedUsers = signal<UserPublicInfoDTO[]>([]); + combinedOptions = signal<CombinedOption[]>([]); + filteredOptions = signal<CombinedOption[]>([]); + defaultActions: TextEditorAction[] = []; + + // Other properties + searchTerm = signal<string>(''); + newPost = signal<Post>(new Post()); + isInputFocused = signal<boolean>(false); + showDropdown = signal<boolean>(false); + showFullForwardedMessage = signal<boolean>(false); + isContentLong = signal<boolean>(false);
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/webapp/app/overview/course-conversations/dialogs/forward-message-dialog/forward-message-dialog.component.ts
(1 hunks)src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts
(5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
`src/test/javascript/spec/**/*.ts`: jest: true; mock: NgMock...
src/test/javascript/spec/**/*.ts
: jest: true; mock: NgMocks; bad_practices: avoid_full_module_import; perf_improvements: mock_irrelevant_deps; service_testing: mock_http_for_logic; no_schema: avoid_NO_ERRORS_SCHEMA; expectation_specificity: true; solutions: {boolean: toBeTrue/False, reference: toBe, existence: toBeNull/NotNull, undefined: toBeUndefined, class_obj: toContainEntries/toEqual, spy_calls: {not_called: not.toHaveBeenCalled, once: toHaveBeenCalledOnce, with_value: toHaveBeenCalledWith|toHaveBeenCalledExactlyOnceWith}}
src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts
`src/main/webapp/**/*.ts`: angular_style:https://angular.io/...
src/main/webapp/app/overview/course-conversations/dialogs/forward-message-dialog/forward-message-dialog.component.ts
⏰ Context from checks skipped due to timeout of 90000ms (8)
- GitHub Check: Call Build Workflow / Build and Push Docker Image
- GitHub Check: Call Build Workflow / Build .war artifact
- GitHub Check: Codacy Static Code Analysis
- GitHub Check: server-style
- GitHub Check: client-tests-selected
- GitHub Check: server-tests
- GitHub Check: client-tests
- GitHub Check: Analyse
🔇 Additional comments (1)
src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts (1)
349-366
: LGTM! Well-structured test with proper spy verification.The test effectively verifies the optimization where source posts are not fetched when there are no forwarded messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (3)
src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java (1)
109-131
: Improve log message specificity.The log message could be more specific to match the entity type being handled.
Apply this diff to improve the log message:
- log.debug("GET getSourceAnswerPostsByIds invoked for course {} with {} posts", courseId, answerPostIds.size()); + log.debug("GET getSourceAnswerPostsByIds invoked for course {} with {} answer posts", courseId, answerPostIds.size());src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java (1)
208-215
: Enhance API documentation.Add documentation for the 404 (Not Found) response status.
Apply this diff to improve the documentation:
* @return ResponseEntity with status 200 (OK) containing the list of posts in the response body, + * or with status 404 (Not Found) if no posts are found, * or with status 400 (Bad Request) if the checks on user, course or post validity fail
src/main/webapp/app/shared/metis/forwarded-message.service.ts (1)
15-40
: Consider adding retry logic for network failures.While the error handling is good, consider adding retry logic for transient network failures using RxJS operators.
return this.http .post<ForwardedMessageDTO>(`${this.resourceUrl}`, dto, { params, observe: 'response', }) .pipe( + retry({ + count: 3, + delay: (error, retryCount) => timer(retryCount * 1000) + }), catchError((error) => { return throwError(() => new Error('Failed to create forwarded message')); }), );Don't forget to import:
import { timer, retry } from 'rxjs';
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java
(3 hunks)src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
(2 hunks)src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
(1 hunks)src/main/webapp/app/shared/metis/forwarded-message.service.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
`src/main/java/**/*.java`: naming:CamelCase; principles:{sin...
src/main/java/**/*.java
: naming:CamelCase; principles:{single_responsibility,small_methods,no_duplication}; db:{perf_queries,datetime_not_timestamp}; rest:{stateless,singleton,delegate_logic,http_only,minimal_dtos}; dtos:{java_records,no_entities,min_data,single_resp}; di:constructor_injection; kiss:simple_code; file_handling:os_indep_paths; practices:{least_access,avoid_transactions,code_reuse,static_member_ref,prefer_primitives}; sql:{param_annotation,uppercase,avoid_subqueries};java:avoid_star_imports
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
`src/main/webapp/**/*.ts`: angular_style:https://angular.io/...
src/main/webapp/app/shared/metis/forwarded-message.service.ts
🧠 Learnings (1)
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java (1)
Learnt from: PaRangger
PR: ls1intum/Artemis#9794
File: src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java:79-79
Timestamp: 2024-12-10T12:02:00.447Z
Learning: In `ForwardedMessageResource.java`, when improving type safety for parameters representing posting types, prefer using the existing `PostingType` enum instead of defining new enums.
⏰ Context from checks skipped due to timeout of 90000ms (7)
- GitHub Check: Call Build Workflow / Build .war artifact
- GitHub Check: Call Build Workflow / Build and Push Docker Image
- GitHub Check: Codacy Static Code Analysis
- GitHub Check: client-tests-selected
- GitHub Check: client-tests
- GitHub Check: server-tests
- GitHub Check: Analyse
🔇 Additional comments (3)
src/main/java/de/tum/cit/aet/artemis/communication/web/AnswerMessageResource.java (1)
42-50
: LGTM! Dependencies are properly injected.The constructor injection follows best practices with final fields and proper initialization.
src/main/webapp/app/shared/metis/forwarded-message.service.ts (1)
1-13
: LGTM! Service setup and dependency injection look good.The service is properly configured with root-level injection and uses the recommended dependency injection pattern with
inject()
.src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java (1)
1-55
: LGTM! Class structure and dependency injection look good.The class is properly structured with constructor-based dependency injection and appropriate logging setup.
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (3)
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java (3)
66-81
:⚠️ Potential issueAdd transaction boundary and security checks.
The endpoint needs transaction management and security validation for message source access.
@PostMapping("forwarded-messages") @EnforceAtLeastStudentInCourse +@Transactional public ResponseEntity<ForwardedMessageDTO> createForwardedMessage(@RequestParam Long courseId, @RequestBody ForwardedMessageDTO forwardedMessageDTO) throws URISyntaxException { log.debug("POST createForwardedMessage invoked with forwardedMessageDTO: {}", forwardedMessageDTO.toString()); long start = System.nanoTime(); if (forwardedMessageDTO.id() != null) { throw new BadRequestAlertException("A new forwarded message cannot already have an ID", ENTITY_NAME, "idExists"); } + var course = courseRepository.findByIdElseThrow(courseId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); + + // Validate source message ownership + if (!authCheckService.isAllowedToAccessMessage(forwardedMessageDTO.sourceId(), course)) { + throw new BadRequestAlertException("Not allowed to forward this message", ENTITY_NAME, "accessDenied"); + } ForwardedMessage forwardedMessage = forwardedMessageDTO.toEntity(); ForwardedMessage savedMessage = forwardedMessageRepository.save(forwardedMessage); log.info("createForwardedMessage took {}", TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.created(new URI("/api/communication/forwarded-messages/" + savedMessage.getId())).body(new ForwardedMessageDTO(savedMessage)); }
66-81
:⚠️ Potential issueAdd transaction boundary and security checks.
The method needs transaction boundary and security checks for message source access.
@PostMapping("forwarded-messages") @EnforceAtLeastStudentInCourse +@Transactional public ResponseEntity<ForwardedMessageDTO> createForwardedMessage(@RequestParam Long courseId, @RequestBody ForwardedMessageDTO forwardedMessageDTO) throws URISyntaxException { log.debug("POST createForwardedMessage invoked with forwardedMessageDTO: {}", forwardedMessageDTO.toString()); long start = System.nanoTime(); if (forwardedMessageDTO.id() != null) { throw new BadRequestAlertException("A new forwarded message cannot already have an ID", ENTITY_NAME, "idExists"); } + var course = courseRepository.findByIdElseThrow(courseId); + if (!authCheckService.isAllowedToAccessMessage(forwardedMessageDTO.sourceId(), course)) { + throw new BadRequestAlertException("Not allowed to forward this message", ENTITY_NAME, "accessDenied"); + } ForwardedMessage forwardedMessage = forwardedMessageDTO.toEntity(); ForwardedMessage savedMessage = forwardedMessageRepository.save(forwardedMessage); log.info("createForwardedMessage took {}", TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.created(new URI("/api/communication/forwarded-messages/" + savedMessage.getId())).body(new ForwardedMessageDTO(savedMessage)); }
93-118
:⚠️ Potential issueAdd security checks and transaction boundary.
The endpoint needs to validate destination message access and wrap the operation in a transaction.
Apply this diff to add security checks and transaction boundary:
@GetMapping("forwarded-messages") @EnforceAtLeastStudentInCourse + @Transactional(readOnly = true) public ResponseEntity<List<ForwardedMessagesGroupDTO>> getForwardedMessages(@RequestParam Long courseId, @RequestParam Set<Long> postingIds, @RequestParam PostingType type) { log.debug("GET getForwardedMessages invoked with postingIds {} and type {}", postingIds, type); long start = System.nanoTime(); if (type != PostingType.POST && type != PostingType.ANSWER) { throw new BadRequestAlertException("Invalid type provided. Must be 'post' or 'answer'.", ENTITY_NAME, "invalidType"); } + var course = courseRepository.findByIdElseThrow(courseId); + // Validate access to all requested messages + for (Long postingId : postingIds) { + if (!authCheckService.isAllowedToAccessMessage(postingId, course)) { + throw new BadRequestAlertException("Not allowed to access message " + postingId, ENTITY_NAME, "accessDenied"); + } + } Set<ForwardedMessage> forwardedMessages; if (type == PostingType.POST) { forwardedMessages = forwardedMessageRepository.findAllByDestinationPostIds(postingIds); } else { // type == PostingType.ANSWER forwardedMessages = forwardedMessageRepository.findAllByDestinationAnswerPostIds(postingIds); } List<ForwardedMessagesGroupDTO> result = forwardedMessages.stream() .collect(Collectors.groupingBy(fm -> type == PostingType.POST ? fm.getDestinationPost().getId() : fm.getDestinationAnswerPost().getId(), Collectors.mapping(ForwardedMessageDTO::new, Collectors.toSet()))) .entrySet().stream().map(entry -> new ForwardedMessagesGroupDTO(entry.getKey(), entry.getValue())).toList(); log.info("getForwardedMessages took {}", TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.ok(result); }
🧹 Nitpick comments (7)
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java (1)
218-238
: Consider optimizing the implementation.While the implementation is functionally correct with proper validation and error handling, there are opportunities for improvement:
- The course validation could be moved to the service layer to maintain consistency with other endpoints.
- Consider using Java records for the response to minimize data transfer.
Apply this diff to optimize the implementation:
@GetMapping("communication/courses/{courseId}/messages-source-posts") @EnforceAtLeastStudentInCourse public ResponseEntity<List<Post>> getSourcePostsByIds(@PathVariable Long courseId, @RequestParam List<Long> postIds) { log.debug("GET getSourcePostsByIds invoked for course {} with {} posts", courseId, postIds.size()); long start = System.nanoTime(); if (postIds == null || postIds.isEmpty()) { throw new BadRequestAlertException("Post IDs cannot be null or empty", conversationMessagingService.getEntityName(), "invalidPostIds"); } - List<Post> posts = conversationMessagingService.getMessageByIds(postIds); - - if (posts.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - if (posts.stream().anyMatch(post -> !post.getConversation().getCourse().getId().equals(courseId))) { - throw new BadRequestAlertException("Some posts do not belong to the specified course", conversationMessagingService.getEntityName(), "invalidCourse"); - } + List<Post> posts = conversationMessagingService.getMessageByIdsForCourse(postIds, courseId); log.debug("getSourcePostsByIds took {}", TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.ok().body(posts); }This change:
- Moves course validation to the service layer
- Simplifies the controller implementation
- Maintains separation of concerns
src/main/webapp/app/shared/metis/forwarded-message.service.ts (6)
36-39
: Enhance error handling with specific error messages.The current error handling returns a generic message. Consider capturing and handling specific error types (e.g., network errors, validation errors) to provide more informative feedback.
.pipe( catchError((error) => { - return throwError(() => new Error('Failed to create forwarded message')); + const errorMessage = error.status === 400 ? 'Invalid message data' : + error.status === 403 ? 'Not authorized to create message' : + error.status === 404 ? 'Course not found' : + 'Failed to create forwarded message'; + return throwError(() => new Error(errorMessage)); }), );
62-65
: Enhance error handling with specific error messages.Similar to the create method, consider providing more specific error messages based on the error type.
catchError((error) => { - return throwError(() => new Error('Failed to retrieve forwarded messages')); + const errorMessage = error.status === 400 ? 'Invalid request parameters' : + error.status === 403 ? 'Not authorized to access messages' : + error.status === 404 ? 'Course or messages not found' : + 'Failed to retrieve forwarded messages'; + return throwError(() => new Error(errorMessage)); }),
36-38
: Enhance error handling with specific error messages.The current error handling uses a generic message. Consider including the error details from the server response to provide more specific feedback.
catchError((error) => { - return throwError(() => new Error('Failed to create forwarded message')); + const errorMessage = error.error?.message || 'Failed to create forwarded message'; + return throwError(() => new Error(errorMessage)); }),
62-64
: Enhance error handling with specific error messages.Similar to the previous comment, consider including the error details from the server response.
catchError((error) => { - return throwError(() => new Error('Failed to retrieve forwarded messages')); + const errorMessage = error.error?.message || 'Failed to retrieve forwarded messages'; + return throwError(() => new Error(errorMessage)); }),
36-38
: Enhance error handling with specific error messages.The error handling could be more specific to help with debugging and user feedback.
Apply this diff to improve error handling:
catchError((error) => { - return throwError(() => new Error('Failed to create forwarded message')); + const errorMessage = error.status === 403 + ? 'You do not have permission to create forwarded messages' + : error.status === 400 + ? error.error?.message || 'Invalid forwarded message data' + : 'Failed to create forwarded message'; + return throwError(() => new Error(errorMessage)); }),
62-64
: Enhance error handling with specific error messages.The error handling could be more specific to help with debugging and user feedback.
Apply this diff to improve error handling:
catchError((error) => { - return throwError(() => new Error('Failed to retrieve forwarded messages')); + const errorMessage = error.status === 403 + ? 'You do not have permission to view these forwarded messages' + : error.status === 400 + ? error.error?.message || 'Invalid request parameters' + : 'Failed to retrieve forwarded messages'; + return throwError(() => new Error(errorMessage)); }),
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
(2 hunks)src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
(1 hunks)src/main/webapp/app/shared/metis/forwarded-message.service.ts
(1 hunks)
👮 Files not reviewed due to content moderation or server errors (2)
- src/main/webapp/app/shared/metis/forwarded-message.service.ts
- src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
🧰 Additional context used
📓 Path-based instructions (2)
`src/main/java/**/*.java`: naming:CamelCase; principles:{sin...
src/main/java/**/*.java
: naming:CamelCase; principles:{single_responsibility,small_methods,no_duplication}; db:{perf_queries,datetime_not_timestamp}; rest:{stateless,singleton,delegate_logic,http_only,minimal_dtos}; dtos:{java_records,no_entities,min_data,single_resp}; di:constructor_injection; kiss:simple_code; file_handling:os_indep_paths; practices:{least_access,avoid_transactions,code_reuse,static_member_ref,prefer_primitives}; sql:{param_annotation,uppercase,avoid_subqueries};java:avoid_star_imports
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java
`src/main/webapp/**/*.ts`: angular_style:https://angular.io/...
src/main/webapp/app/shared/metis/forwarded-message.service.ts
🧠 Learnings (1)
src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java (1)
Learnt from: PaRangger
PR: ls1intum/Artemis#9794
File: src/main/java/de/tum/cit/aet/artemis/communication/web/ForwardedMessageResource.java:79-79
Timestamp: 2024-12-10T12:02:00.447Z
Learning: In `ForwardedMessageResource.java`, when improving type safety for parameters representing posting types, prefer using the existing `PostingType` enum instead of defining new enums.
⏰ Context from checks skipped due to timeout of 90000ms (9)
- GitHub Check: client-style
- GitHub Check: server-tests
- GitHub Check: server-style
- GitHub Check: client-tests-selected
- GitHub Check: client-tests
- GitHub Check: Call Build Workflow / Build .war artifact
- GitHub Check: Call Build Workflow / Build and Push Docker Image
- GitHub Check: Codacy Static Code Analysis
- GitHub Check: Analyse
🔇 Additional comments (3)
src/main/java/de/tum/cit/aet/artemis/communication/web/ConversationMessageResource.java (3)
41-41
: LGTM!The import for
EnforceAtLeastStudentInCourse
is correctly placed and used in the new endpoint.
208-215
: LGTM!The API documentation is complete and follows the class's documentation style.
216-217
: LGTM!The endpoint mapping and authorization are correctly implemented:
- Path follows the REST API structure
@EnforceAtLeastStudentInCourse
ensures proper authorization
Checklist
General
Server
Client
Motivation and Context
Currently user cannot forward messages to other channels/direct-group chats.
(Closes #9066)
Description
The forward message feature has been added. Users can now navigate to the forward message dialog by clicking on the forward message option. They can select a random number of destinations and add content to the forwarded message (similar to Slack).
forwarded_message
table has been added to the database with the following fields:post
andanswer_post
table have been updated with a new column named has_forwarded_messages.Known Issue for a followup PR:
The server-side changes for forwarding multiple messages within a single message and forwarding a message as a reply to another message (inside an AnswerPost) are already in place, but the client-side implementation is not ready yet. To achieve this, a unique message link needs to be generated for each post/answer post, similar to the "Copy Link" feature in Slack. This will be completed in a follow-up PR.
Steps for Testing
Prerequisites:
Testserver States
Note
These badges show the state of the test servers.
Green = Currently available, Red = Currently locked
Click on the badges to get to the test servers.
Review Progress
Performance Review
Code Review
Manual Tests
Performance Tests
Test Coverage
Client
Server
Screenshots
forwarded message view

forward message dialog

Summary by CodeRabbit
Post
andAnswerPost
classes to manage forwarded messages effectively.ForwardedMessage
entity and related DTOs for improved message management.