mirror of https://github.com/Squidex/squidex.git
Browse Source
* More the API more idempotent * Mention users. * Temp * Rebuild comments grain to use string key. * Improvements for event store. * Temporary. * Notifications. * Modal fixes. * Build fix. * Another build fix. * Frontend errors fixed.pull/464/head
committed by
GitHub
76 changed files with 1353 additions and 351 deletions
@ -0,0 +1,113 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using System.Threading.Tasks; |
||||
|
using Orleans; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
using Squidex.Shared.Users; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments |
||||
|
{ |
||||
|
public sealed class CommentsCommandMiddleware : ICommandMiddleware |
||||
|
{ |
||||
|
private static readonly Regex MentionRegex = new Regex(@"@(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); |
||||
|
private readonly IGrainFactory grainFactory; |
||||
|
private readonly IUserResolver userResolver; |
||||
|
|
||||
|
public CommentsCommandMiddleware(IGrainFactory grainFactory, IUserResolver userResolver) |
||||
|
{ |
||||
|
Guard.NotNull(grainFactory); |
||||
|
Guard.NotNull(userResolver); |
||||
|
|
||||
|
this.grainFactory = grainFactory; |
||||
|
|
||||
|
this.userResolver = userResolver; |
||||
|
} |
||||
|
|
||||
|
public async Task HandleAsync(CommandContext context, Func<Task> next) |
||||
|
{ |
||||
|
if (context.Command is CommentsCommand commentsCommand) |
||||
|
{ |
||||
|
if (commentsCommand is CreateComment createComment && !IsMention(createComment)) |
||||
|
{ |
||||
|
await MentionUsersAsync(createComment); |
||||
|
|
||||
|
if (createComment.Mentions != null) |
||||
|
{ |
||||
|
foreach (var userId in createComment.Mentions) |
||||
|
{ |
||||
|
var notificationCommand = SimpleMapper.Map(createComment, new CreateComment()); |
||||
|
|
||||
|
notificationCommand.AppId = null!; |
||||
|
notificationCommand.Mentions = null; |
||||
|
notificationCommand.CommentsId = userId; |
||||
|
notificationCommand.ExpectedVersion = EtagVersion.Any; |
||||
|
notificationCommand.IsMention = true; |
||||
|
|
||||
|
context.CommandBus.PublishAsync(notificationCommand).Forget(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await ExecuteCommandAsync(context, commentsCommand); |
||||
|
} |
||||
|
|
||||
|
await next(); |
||||
|
} |
||||
|
|
||||
|
private async Task ExecuteCommandAsync(CommandContext context, CommentsCommand commentsCommand) |
||||
|
{ |
||||
|
var grain = grainFactory.GetGrain<ICommentsGrain>(commentsCommand.CommentsId); |
||||
|
|
||||
|
var result = await grain.ExecuteAsync(commentsCommand.AsJ()); |
||||
|
|
||||
|
context.Complete(result.Value); |
||||
|
} |
||||
|
|
||||
|
private static bool IsMention(CreateComment createComment) |
||||
|
{ |
||||
|
return createComment.IsMention; |
||||
|
} |
||||
|
|
||||
|
private async Task MentionUsersAsync(CreateComment createComment) |
||||
|
{ |
||||
|
if (!string.IsNullOrWhiteSpace(createComment.Text)) |
||||
|
{ |
||||
|
var emails = MentionRegex.Matches(createComment.Text).Select(x => x.Value.Substring(1)).ToArray(); |
||||
|
|
||||
|
if (emails.Length > 0) |
||||
|
{ |
||||
|
var mentions = new List<string>(); |
||||
|
|
||||
|
foreach (var email in emails) |
||||
|
{ |
||||
|
var user = await userResolver.FindByIdOrEmailAsync(email); |
||||
|
|
||||
|
if (user != null) |
||||
|
{ |
||||
|
mentions.Add(user.Id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (mentions.Count > 0) |
||||
|
{ |
||||
|
createComment.Mentions = mentions.ToArray(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,19 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Squidex.Infrastructure.EventSourcing; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Comments.State |
|
||||
{ |
|
||||
public sealed class CommentsState : DomainObjectState<CommentsState> |
|
||||
{ |
|
||||
public override CommentsState Apply(Envelope<IEvent> @event) |
|
||||
{ |
|
||||
return this; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,102 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.Net.Http.Headers; |
||||
|
using Squidex.Areas.Api.Controllers.Comments.Models; |
||||
|
using Squidex.Domain.Apps.Entities.Comments; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
using Squidex.Web; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Comments.Notifications |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Manages user notifications.
|
||||
|
/// </summary>
|
||||
|
[ApiExplorerSettings(GroupName = nameof(Notifications))] |
||||
|
public sealed class UserNotificationsController : ApiController |
||||
|
{ |
||||
|
private static readonly NamedId<Guid> NoApp = NamedId.Of(Guid.Empty, "none"); |
||||
|
private readonly ICommentsLoader commentsLoader; |
||||
|
|
||||
|
public UserNotificationsController(ICommandBus commandBus, ICommentsLoader commentsLoader) |
||||
|
: base(commandBus) |
||||
|
{ |
||||
|
this.commentsLoader = commentsLoader; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get all notifications.
|
||||
|
/// </summary>
|
||||
|
/// <param name="userId">The user id.</param>
|
||||
|
/// <param name="version">The current version.</param>
|
||||
|
/// <remarks>
|
||||
|
/// When passing in a version you can retrieve all updates since then.
|
||||
|
/// </remarks>
|
||||
|
/// <returns>
|
||||
|
/// 200 => All comments returned.
|
||||
|
/// </returns>
|
||||
|
[HttpGet] |
||||
|
[Route("users/{userId}/notifications")] |
||||
|
[ProducesResponseType(typeof(CommentsDto), 200)] |
||||
|
[ApiPermission] |
||||
|
public async Task<IActionResult> GetNotifications(string userId, [FromQuery] long version = EtagVersion.Any) |
||||
|
{ |
||||
|
CheckPermissions(userId); |
||||
|
|
||||
|
var result = await commentsLoader.GetCommentsAsync(userId, version); |
||||
|
|
||||
|
var response = Deferred.Response(() => |
||||
|
{ |
||||
|
return CommentsDto.FromResult(result); |
||||
|
}); |
||||
|
|
||||
|
Response.Headers[HeaderNames.ETag] = result.Version.ToString(); |
||||
|
|
||||
|
return Ok(response); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Deletes the notification.
|
||||
|
/// </summary>
|
||||
|
/// <param name="userId">The user id.</param>
|
||||
|
/// <param name="commentId">The id of the comment.</param>
|
||||
|
/// <returns>
|
||||
|
/// 204 => Comment deleted.
|
||||
|
/// 404 => Comment not found.
|
||||
|
/// </returns>
|
||||
|
[HttpDelete] |
||||
|
[Route("users/{userId}/notifications/{commentId}")] |
||||
|
[ApiPermission] |
||||
|
public async Task<IActionResult> DeleteComment(string userId, Guid commentId) |
||||
|
{ |
||||
|
CheckPermissions(userId); |
||||
|
|
||||
|
await CommandBus.PublishAsync(new DeleteComment |
||||
|
{ |
||||
|
AppId = NoApp, |
||||
|
CommentsId = userId, |
||||
|
CommentId = commentId |
||||
|
}); |
||||
|
|
||||
|
return NoContent(); |
||||
|
} |
||||
|
|
||||
|
private void CheckPermissions(string userId) |
||||
|
{ |
||||
|
if (!string.Equals(userId, User.OpenIdSubject())) |
||||
|
{ |
||||
|
throw new DomainForbiddenException("You can only access your notifications."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,180 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Orleans; |
||||
|
using Squidex.Domain.Apps.Entities.Comments.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
using Squidex.Shared.Users; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Comments |
||||
|
{ |
||||
|
public class CommentsCommandMiddlewareTests |
||||
|
{ |
||||
|
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); |
||||
|
private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); |
||||
|
private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); |
||||
|
private readonly RefToken actor = new RefToken(RefTokenType.Subject, "me"); |
||||
|
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); |
||||
|
private readonly Guid commentsId = Guid.NewGuid(); |
||||
|
private readonly Guid commentId = Guid.NewGuid(); |
||||
|
private readonly CommentsCommandMiddleware sut; |
||||
|
|
||||
|
public CommentsCommandMiddlewareTests() |
||||
|
{ |
||||
|
A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>.Ignored)) |
||||
|
.Returns(Task.FromResult<IUser?>(null)); |
||||
|
|
||||
|
sut = new CommentsCommandMiddleware(grainFactory, userResolver); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_grain_for_comments_command() |
||||
|
{ |
||||
|
var command = CreateCommentsCommand(new CreateComment()); |
||||
|
var context = CreateContextForCommand(command); |
||||
|
|
||||
|
var grain = A.Fake<ICommentsGrain>(); |
||||
|
|
||||
|
var result = "Completed"; |
||||
|
|
||||
|
A.CallTo(() => grainFactory.GetGrain<ICommentsGrain>(commentsId.ToString(), null)) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
A.CallTo(() => grain.ExecuteAsync(A<J<CommentsCommand>>.That.Matches(x => x.Value == command))) |
||||
|
.Returns(new J<object>(result)); |
||||
|
|
||||
|
var isNextCalled = false; |
||||
|
|
||||
|
await sut.HandleAsync(context, () => |
||||
|
{ |
||||
|
isNextCalled = true; |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}); |
||||
|
|
||||
|
Assert.True(isNextCalled); |
||||
|
|
||||
|
A.CallTo(() => grain.ExecuteAsync(A<J<CommentsCommand>>.That.Matches(x => x.Value == command))) |
||||
|
.Returns(new J<object>(12)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_enrich_with_mentioned_user_ids_if_found() |
||||
|
{ |
||||
|
SetupUser("id1", "mail1@squidex.io"); |
||||
|
SetupUser("id2", "mail2@squidex.io"); |
||||
|
|
||||
|
var command = CreateCommentsCommand(new CreateComment |
||||
|
{ |
||||
|
Text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io" |
||||
|
}); |
||||
|
|
||||
|
var context = CreateContextForCommand(command); |
||||
|
|
||||
|
await sut.HandleAsync(context); |
||||
|
|
||||
|
Assert.Equal(command.Mentions, new[] { "id1", "id2" }); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_invoke_commands_for_mentioned_users() |
||||
|
{ |
||||
|
SetupUser("id1", "mail1@squidex.io"); |
||||
|
SetupUser("id2", "mail2@squidex.io"); |
||||
|
|
||||
|
var command = CreateCommentsCommand(new CreateComment |
||||
|
{ |
||||
|
Text = "Hi @mail1@squidex.io and @mail2@squidex.io" |
||||
|
}); |
||||
|
|
||||
|
var context = CreateContextForCommand(command); |
||||
|
|
||||
|
await sut.HandleAsync(context); |
||||
|
|
||||
|
A.CallTo(() => commandBus.PublishAsync(A<ICommand>.That.Matches(x => IsForUser(x, "id1")))) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => commandBus.PublishAsync(A<ICommand>.That.Matches(x => IsForUser(x, "id2")))) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_enrich_with_mentioned_user_ids_if_invalid_mentioned_tags_used() |
||||
|
{ |
||||
|
var command = CreateCommentsCommand(new CreateComment |
||||
|
{ |
||||
|
Text = "Hi invalid@squidex.io" |
||||
|
}); |
||||
|
|
||||
|
var context = CreateContextForCommand(command); |
||||
|
|
||||
|
await sut.HandleAsync(context); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>.Ignored)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_enrich_with_mentioned_user_ids_for_notification() |
||||
|
{ |
||||
|
var command = new CreateComment |
||||
|
{ |
||||
|
Text = "Hi @invalid@squidex.io", IsMention = true |
||||
|
}; |
||||
|
|
||||
|
var context = CreateContextForCommand(command); |
||||
|
|
||||
|
await sut.HandleAsync(context); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>.Ignored)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
protected CommandContext CreateContextForCommand<TCommand>(TCommand command) where TCommand : CommentsCommand |
||||
|
{ |
||||
|
return new CommandContext(command, commandBus); |
||||
|
} |
||||
|
|
||||
|
private static bool IsForUser(ICommand command, string id) |
||||
|
{ |
||||
|
return command is CreateComment createComment && |
||||
|
createComment.CommentsId == id && |
||||
|
createComment.Mentions == null && |
||||
|
createComment.AppId == null && |
||||
|
createComment.ExpectedVersion == EtagVersion.Any && |
||||
|
createComment.IsMention; |
||||
|
} |
||||
|
|
||||
|
private void SetupUser(string id, string email) |
||||
|
{ |
||||
|
var user = A.Fake<IUser>(); |
||||
|
|
||||
|
A.CallTo(() => user.Id).Returns(id); |
||||
|
A.CallTo(() => user.Email).Returns(email); |
||||
|
|
||||
|
A.CallTo(() => userResolver.FindByIdOrEmailAsync(email)) |
||||
|
.Returns(user); |
||||
|
} |
||||
|
|
||||
|
protected T CreateCommentsCommand<T>(T command) where T : CommentsCommand |
||||
|
{ |
||||
|
command.Actor = actor; |
||||
|
command.AppId = appId; |
||||
|
command.CommentsId = commentsId.ToString(); |
||||
|
command.CommentId = commentId; |
||||
|
|
||||
|
return command; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
<ul class="nav navbar-nav"> |
||||
|
<li class="nav-item nav-icon dropdown"> |
||||
|
<span class="nav-link dropdown-toggle" (click)="modalMenu.show()"> |
||||
|
<i class="icon-comments"></i> |
||||
|
|
||||
|
<span class="badge badge-pill" *ngIf="unread">{{unread}}</span> |
||||
|
</span> |
||||
|
|
||||
|
<ng-container *sqxModal="modalMenu;onRoot:false"> |
||||
|
<div class="dropdown-menu" #scrollMe [scrollTop]="scrollMe.scrollHeight" @fade> |
||||
|
<ng-container *ngIf="commentsState.comments | async; let comments"> |
||||
|
<small class="text-muted" *ngIf="comments.length === 0"> |
||||
|
No notifications yet. |
||||
|
</small> |
||||
|
|
||||
|
<sqx-comment *ngFor="let comment of comments; trackBy: trackByComment" |
||||
|
[comment]="comment" |
||||
|
[confirmDelete]="false" |
||||
|
[canDelete]="true" |
||||
|
[canFollow]="true" |
||||
|
(delete)="delete(comment)" |
||||
|
[userToken]="userToken"> |
||||
|
</sqx-comment> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</li> |
||||
|
</ul> |
||||
@ -0,0 +1,13 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.dropdown-menu { |
||||
|
left: auto; |
||||
|
max-height: 500px; |
||||
|
min-height: 5rem; |
||||
|
overflow-y: scroll; |
||||
|
padding: 1.5rem; |
||||
|
padding-bottom: 1rem; |
||||
|
right: 0; |
||||
|
width: 300px; |
||||
|
} |
||||
@ -0,0 +1,110 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; |
||||
|
import { timer } from 'rxjs'; |
||||
|
import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators'; |
||||
|
|
||||
|
import { |
||||
|
AuthService, |
||||
|
CommentDto, |
||||
|
CommentsService, |
||||
|
CommentsState, |
||||
|
DialogService, |
||||
|
fadeAnimation, |
||||
|
LocalStoreService, |
||||
|
ModalModel, |
||||
|
ResourceOwner |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-notifications-menu', |
||||
|
styleUrls: ['./notifications-menu.component.scss'], |
||||
|
templateUrl: './notifications-menu.component.html', |
||||
|
animations: [ |
||||
|
fadeAnimation |
||||
|
], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class NotificationsMenuComponent extends ResourceOwner implements OnInit { |
||||
|
private isOpen: boolean; |
||||
|
private configKey: string; |
||||
|
|
||||
|
public modalMenu = new ModalModel(); |
||||
|
|
||||
|
public commentsUrl: string; |
||||
|
public commentsState: CommentsState; |
||||
|
|
||||
|
public userId: string; |
||||
|
public userToken: string; |
||||
|
|
||||
|
public versionRead = -1; |
||||
|
public versionReceived = -1; |
||||
|
|
||||
|
public get unread() { |
||||
|
return Math.max(0, this.versionReceived - this.versionRead); |
||||
|
} |
||||
|
|
||||
|
constructor(authService: AuthService, |
||||
|
private readonly changeDetector: ChangeDetectorRef, |
||||
|
private readonly commentsService: CommentsService, |
||||
|
private readonly dialogs: DialogService, |
||||
|
private readonly localStore: LocalStoreService |
||||
|
) { |
||||
|
super(); |
||||
|
|
||||
|
this.userToken = authService.user!.token; |
||||
|
this.userId = authService.user!.id; |
||||
|
|
||||
|
this.configKey = `users.${this.userId}.notifications`; |
||||
|
|
||||
|
this.versionRead = localStore.getInt(this.configKey, -1); |
||||
|
this.versionReceived = this.versionRead; |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.commentsUrl = `users/${this.userId}/notifications`; |
||||
|
this.commentsState = new CommentsState(this.commentsUrl, this.commentsService, this.dialogs); |
||||
|
|
||||
|
this.own( |
||||
|
this.modalMenu.isOpen.pipe( |
||||
|
tap(isOpen => { |
||||
|
this.isOpen = isOpen; |
||||
|
|
||||
|
this.updateVersion(); |
||||
|
}) |
||||
|
)); |
||||
|
|
||||
|
this.own( |
||||
|
this.commentsState.versionNumber.pipe( |
||||
|
tap(version => { |
||||
|
this.versionReceived = version; |
||||
|
|
||||
|
this.updateVersion(); |
||||
|
|
||||
|
this.changeDetector.detectChanges(); |
||||
|
}))); |
||||
|
|
||||
|
this.own(timer(0, 4000).pipe(switchMap(() => this.commentsState.load(true).pipe(onErrorResumeNext())))); |
||||
|
} |
||||
|
|
||||
|
public delete(comment: CommentDto) { |
||||
|
this.commentsState.delete(comment); |
||||
|
} |
||||
|
|
||||
|
public trackByComment(comment: CommentDto) { |
||||
|
return comment.id; |
||||
|
} |
||||
|
|
||||
|
private updateVersion() { |
||||
|
if (this.isOpen) { |
||||
|
this.versionRead = this.versionReceived; |
||||
|
|
||||
|
this.localStore.setInt(this.configKey, this.versionRead); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue