diff --git a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs index f6c8c727e2f..7a0bf2f3c05 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanControllers/ReviewsController.cs @@ -27,13 +27,15 @@ public class ReviewsController : BaseApiController public readonly UserPreferenceCache _preferenceCache; private readonly ICosmosUserProfileRepository _userProfileRepository; private readonly IHubContext _signalRHubContext; + private readonly INotificationManager _notificationManager; private readonly IWebHostEnvironment _env; public ReviewsController(ILogger logger, IAPIRevisionsManager reviewRevisionsManager, IReviewManager reviewManager, ICommentsManager commentManager, IBlobCodeFileRepository codeFileRepository, IConfiguration configuration, UserPreferenceCache preferenceCache, - ICosmosUserProfileRepository userProfileRepository, IHubContext signalRHub, IWebHostEnvironment env) + ICosmosUserProfileRepository userProfileRepository, IHubContext signalRHub, + INotificationManager notificationManager, IWebHostEnvironment env) { _logger = logger; _apiRevisionsManager = reviewRevisionsManager; @@ -44,6 +46,7 @@ public ReviewsController(ILogger logger, _preferenceCache = preferenceCache; _userProfileRepository = userProfileRepository; _signalRHubContext = signalRHub; + _notificationManager = notificationManager; _env = env; } @@ -117,6 +120,20 @@ public async Task ToggleReviewApprovalAsync(string reviewId, strin return new LeanJsonResult(updatedReview, StatusCodes.Status200OK); } + /// + /// Endpoint used by Client SPA toggling Subscription to a review + /// + /// + /// true = subscribe, false = unsubscribe + /// + [HttpPost("{reviewId}/toggleSubscribe", Name = "ToggleSubscribe")] + public async Task> ToggleSubscribeAsync(string reviewId, [FromQuery] bool state) + { + string userName = User.GetGitHubLogin(); + await _notificationManager.ToggleSubscribedAsync(User, reviewId, state); + return Ok(); + } + /// ///Retrieve the Content (codeLines and Navigation) of a review /// diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/INotificationManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/INotificationManager.cs index 6fa124dbb36..5d402ec82e7 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/INotificationManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/INotificationManager.cs @@ -12,7 +12,7 @@ public interface INotificationManager public Task NotifyUserOnCommentTag(CommentItemModel comment); public Task NotifyApproversOfReview(ClaimsPrincipal user, string apiRevisionId, HashSet reviewers); public Task NotifySubscribersOnNewRevisionAsync(ReviewListItemModel review, APIRevisionListItemModel revision, ClaimsPrincipal user); - public Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId); + public Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId, bool? state = null); public Task SubscribeAsync(ReviewListItemModel review, ClaimsPrincipal user); public Task UnsubscribeAsync(ReviewListItemModel review, ClaimsPrincipal user); } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs index ed201668e0a..6d1a4b55118 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs @@ -87,16 +87,27 @@ public async Task NotifySubscribersOnNewRevisionAsync(ReviewListItemModel review /// /// /// + /// true = subscribe, false = unsubscribe /// - public async Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId) + public async Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId, bool? state = null) { var review = await _reviewRepository.GetReviewAsync(reviewId); if (PageModelHelpers.IsUserSubscribed(user, review.Subscribers)) { + if (state == true) + { + return; // already subscribed + } + await UnsubscribeAsync(review, user); } else { + if (state == false) + { + return; // already unsubscribed + } + await SubscribeAsync(review, user); } } diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.html index 3ae96ec6361..e2ca7906c2f 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.html @@ -154,6 +154,16 @@ + +
    +
  • + + +
  • +
+
+
  • diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.ts index 370a78c1501..9214d100b45 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page-options/review-page-options.component.ts @@ -38,6 +38,7 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{ @Output() showLeftNavigationEmitter : EventEmitter = new EventEmitter(); @Output() disableCodeLinesLazyLoadingEmitter : EventEmitter = new EventEmitter(); @Output() markAsViewedEmitter : EventEmitter = new EventEmitter(); + @Output() subscribeEmitter : EventEmitter = new EventEmitter(); @Output() showLineNumbersEmitter : EventEmitter = new EventEmitter(); @Output() apiRevisionApprovalEmitter : EventEmitter = new EventEmitter(); @Output() reviewApprovalEmitter : EventEmitter = new EventEmitter(); @@ -50,6 +51,7 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{ showHiddenAPISwitch : boolean = false; showLeftNavigationSwitch : boolean = true; markedAsViewSwitch : boolean = false; + subscribeSwitch : boolean = false; showLineNumbersSwitch : boolean = true; disableCodeLinesLazyLoading: boolean = false; @@ -104,26 +106,29 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{ } ngOnChanges(changes: SimpleChanges) { - if (changes['diffStyleInput']) { + if (changes['diffStyleInput'] && changes['diffStyleInput'].currentValue != undefined) { this.setSelectedDiffStyle(); } - if (changes['userProfile']) { + if (changes['userProfile'] && changes['userProfile'].currentValue != undefined) { + this.setSubscribeSwitch(); + this.setMarkedAsViewSwitch(); this.setPageOptionValues(); } if (changes['activeAPIRevision'] && changes['activeAPIRevision'].currentValue != undefined) { - this.markedAsViewSwitch = this.activeAPIRevision!.viewedBy.includes(this.userProfile?.userName!); + this.setMarkedAsViewSwitch(); this.selectedApprovers = this.activeAPIRevision!.assignedReviewers.map(reviewer => reviewer.assingedTo); this.setAPIRevisionApprovalStates(); this.setPullRequestsInfo(); } - if (changes['diffAPIRevision']) { + if (changes['diffAPIRevision'] && changes['diffAPIRevision'].currentValue != undefined) { this.setAPIRevisionApprovalStates(); } - if (changes['review']) { + if (changes['review'] && changes['review'].currentValue != undefined) { + this.setSubscribeSwitch(); this.setReviewApprovalStatus(); } } @@ -193,6 +198,14 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{ this.markAsViewedEmitter.emit(event.checked); } + /** + * Callback for markedAsViewSwitch Change + * @param event the Filter event + */ + onSubscribeSwitchChange(event: InputSwitchOnChangeEvent) { + this.subscribeEmitter.emit(event.checked); + } + /** * Callback for showLineNumbersSwitch Change * @param event the Filter event @@ -287,6 +300,14 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{ }); } } + + setSubscribeSwitch() { + this.subscribeSwitch = (this.userProfile && this.review) ? this.review!.subscribers.includes(this.userProfile?.email!) : this.subscribeSwitch; + } + + setMarkedAsViewSwitch() { + this.markedAsViewSwitch = (this.activeAPIRevision && this.userProfile)? this.activeAPIRevision!.viewedBy.includes(this.userProfile?.userName!): this.markedAsViewSwitch; + } handleAPIRevisionApprovalAction() { if (!this.activeAPIRevisionIsApprovedByCurrentUser && (this.hasActiveConversation || this.hasFatalDiagnostics)) { diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html index 866e07f4608..5cf8e2d3bcd 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.html @@ -64,6 +64,7 @@ (showLeftNavigationEmitter)="handleShowLeftNavigationEmitter($event)" (diffStyleEmitter)="handleDiffStyleEmitter($event)" (markAsViewedEmitter)="handleMarkAsViewedEmitter($event)" + (subscribeEmitter)="handleSubscribeEmitter($event)" (showLineNumbersEmitter)="handleShowLineNumbersEmitter($event)" (apiRevisionApprovalEmitter)="handleApiRevisionApprovalEmitter($event)" (reviewApprovalEmitter)="handleReviewApprovalEmitter($event)" diff --git a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts index d6f30ce8e2e..c99e373e078 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_components/review-page/review-page.component.ts @@ -387,6 +387,10 @@ export class ReviewPageComponent implements OnInit { }); } + handleSubscribeEmitter(state: boolean) { + this.reviewsService.toggleReviewSubscriptionByUser(this.reviewId!, state).pipe(take(1)).subscribe(); + } + handleApiRevisionApprovalEmitter(value: boolean) { if (value) { this.apiRevisionsService.toggleAPIRevisionApproval(this.reviewId!, this.activeApiRevisionId!).pipe(take(1)).subscribe({ diff --git a/src/dotnet/APIView/ClientSPA/src/app/_models/review.ts b/src/dotnet/APIView/ClientSPA/src/app/_models/review.ts index 28dee62bcad..e63817a5806 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_models/review.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_models/review.ts @@ -14,6 +14,7 @@ export class Review { isDeleted: boolean isApproved: boolean changeHistory: ChangeHistory[] + subscribers: string[] constructor() { this.id = '' @@ -23,6 +24,7 @@ export class Review { this.isDeleted = false this.isApproved = false this.changeHistory = [] + this.subscribers = [] } } diff --git a/src/dotnet/APIView/ClientSPA/src/app/_services/reviews/reviews.service.ts b/src/dotnet/APIView/ClientSPA/src/app/_services/reviews/reviews.service.ts index 51c84a35300..abf4731980a 100644 --- a/src/dotnet/APIView/ClientSPA/src/app/_services/reviews/reviews.service.ts +++ b/src/dotnet/APIView/ClientSPA/src/app/_services/reviews/reviews.service.ts @@ -106,6 +106,22 @@ export class ReviewsService { }); } + toggleReviewSubscriptionByUser(reviewId: string, state: boolean) { + let params = new HttpParams(); + params = params.append('state', state.toString()); + + const headers = new HttpHeaders({ + 'Content-Type': 'application/json', + }); + + return this.http.post(this.baseUrl + `/${reviewId}/toggleSubscribe`, {}, + { + headers: headers, + params: params, + withCredentials: true + }); + } + getReviewContent(reviewId: string, activeApiRevisionId: string | null = null, diffApiRevisionId: string | null = null) : Observable{ let params = new HttpParams(); if (activeApiRevisionId) {