mirror of https://github.com/Squidex/squidex.git
Browse Source
* Provide user role in context context. * Also provide user for backwards compatibility. * Show current users on resource. * Fix tests.pull/778/head
committed by
GitHub
28 changed files with 562 additions and 72 deletions
@ -0,0 +1,36 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Orleans; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Comments |
|||
{ |
|||
public sealed class GrainWatchingService : IWatchingService |
|||
{ |
|||
private readonly IGrainFactory grainFactory; |
|||
|
|||
public GrainWatchingService(IGrainFactory grainFactory) |
|||
{ |
|||
this.grainFactory = grainFactory; |
|||
} |
|||
|
|||
public Task<string[]> GetWatchingUsersAsync(DomainId appId, string resource, string userId) |
|||
{ |
|||
Guard.NotNullOrEmpty(resource, nameof(resource)); |
|||
Guard.NotNullOrEmpty(userId, nameof(userId)); |
|||
|
|||
return GetGrain(appId).GetWatchingUsersAsync(resource, userId); |
|||
} |
|||
|
|||
private IWatchingGrain GetGrain(DomainId appId) |
|||
{ |
|||
return grainFactory.GetGrain<IWatchingGrain>(appId.ToString()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Orleans; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Comments |
|||
{ |
|||
public interface IWatchingGrain : IGrainWithStringKey |
|||
{ |
|||
Task<string[]> GetWatchingUsersAsync(string resource, string userId); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Comments |
|||
{ |
|||
public interface IWatchingService |
|||
{ |
|||
Task<string[]> GetWatchingUsersAsync(DomainId appId, string resource, string userId); |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
// ==========================================================================
|
|||
// 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.Threading.Tasks; |
|||
using NodaTime; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Orleans; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Comments |
|||
{ |
|||
public sealed class WatchingGrain : GrainBase, IWatchingGrain |
|||
{ |
|||
private static readonly Duration Timeout = Duration.FromMinutes(1); |
|||
private readonly Dictionary<string, Dictionary<string, Instant>> users = new Dictionary<string, Dictionary<string, Instant>>(); |
|||
private readonly IClock clock; |
|||
|
|||
public WatchingGrain(IClock clock) |
|||
{ |
|||
this.clock = clock; |
|||
} |
|||
|
|||
public override Task OnActivateAsync() |
|||
{ |
|||
var time = TimeSpan.FromSeconds(30); |
|||
|
|||
RegisterTimer(x => |
|||
{ |
|||
Cleanup(); |
|||
|
|||
return Task.CompletedTask; |
|||
}, null, time, time); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task<string[]> GetWatchingUsersAsync(string resource, string userId) |
|||
{ |
|||
Guard.NotNullOrEmpty(resource, nameof(resource)); |
|||
Guard.NotNullOrEmpty(userId, nameof(userId)); |
|||
|
|||
var usersByResource = users.GetOrAddNew(resource); |
|||
|
|||
usersByResource[userId] = clock.GetCurrentInstant(); |
|||
|
|||
return Task.FromResult(usersByResource.Keys.ToArray()); |
|||
} |
|||
|
|||
public void Cleanup() |
|||
{ |
|||
if (users.Count == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var now = clock.GetCurrentInstant(); |
|||
|
|||
foreach (var (resource, usersByResource) in users.ToList()) |
|||
{ |
|||
foreach (var (userId, lastSeen) in usersByResource.ToList()) |
|||
{ |
|||
var timeSinceLastSeen = now - lastSeen; |
|||
|
|||
if (timeSinceLastSeen > Timeout) |
|||
{ |
|||
usersByResource.Remove(userId); |
|||
} |
|||
} |
|||
|
|||
if (usersByResource.Count == 0) |
|||
{ |
|||
users.Remove(resource); |
|||
} |
|||
} |
|||
|
|||
if (users.Count == 0) |
|||
{ |
|||
TryDeactivateOnIdle(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Orleans; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Comments |
|||
{ |
|||
public class GrainWatchingServiceTests |
|||
{ |
|||
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); |
|||
private readonly GrainWatchingService sut; |
|||
|
|||
public GrainWatchingServiceTests() |
|||
{ |
|||
sut = new GrainWatchingService(grainFactory); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_call_grain_if_retrieving_watching_users() |
|||
{ |
|||
var appId = DomainId.NewGuid(); |
|||
var userResource = "resource1"; |
|||
var userIdentity = "user1"; |
|||
|
|||
var grain = A.Fake<IWatchingGrain>(); |
|||
|
|||
A.CallTo(() => grainFactory.GetGrain<IWatchingGrain>(appId.ToString(), null)) |
|||
.Returns(grain); |
|||
|
|||
A.CallTo(() => grain.GetWatchingUsersAsync(userResource, userIdentity)) |
|||
.Returns(Task.FromResult(new[] { "user1", "user2" })); |
|||
|
|||
var result = await sut.GetWatchingUsersAsync(appId, userResource, userIdentity); |
|||
|
|||
Assert.Equal(new[] { "user1", "user2" }, result); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using NodaTime; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Comments |
|||
{ |
|||
public class WatchingGrainTests |
|||
{ |
|||
private readonly IClock clock = A.Fake<IClock>(); |
|||
private readonly WatchingGrain sut; |
|||
private Instant now = SystemClock.Instance.GetCurrentInstant(); |
|||
|
|||
public WatchingGrainTests() |
|||
{ |
|||
A.CallTo(() => clock.GetCurrentInstant()) |
|||
.ReturnsLazily(() => now); |
|||
|
|||
sut = new WatchingGrain(clock); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_only_return_self_if_no_one_watching() |
|||
{ |
|||
var watching = await sut.GetWatchingUsersAsync("resource1", "user1"); |
|||
|
|||
Assert.Equal(new[] { "user1" }, watching); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_users_watching_on_same_resource() |
|||
{ |
|||
await sut.GetWatchingUsersAsync("resource1", "user1"); |
|||
await sut.GetWatchingUsersAsync("resource2", "user2"); |
|||
|
|||
var watching1 = await sut.GetWatchingUsersAsync("resource1", "user3"); |
|||
var watching2 = await sut.GetWatchingUsersAsync("resource2", "user4"); |
|||
|
|||
Assert.Equal(new[] { "user1", "user3" }, watching1); |
|||
Assert.Equal(new[] { "user2", "user4" }, watching2); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_cleanup_old_users() |
|||
{ |
|||
await sut.GetWatchingUsersAsync("resource1", "user1"); |
|||
await sut.GetWatchingUsersAsync("resource2", "user2"); |
|||
|
|||
now = now.Plus(Duration.FromMinutes(2)); |
|||
|
|||
sut.Cleanup(); |
|||
|
|||
await sut.GetWatchingUsersAsync("resource1", "user3"); |
|||
await sut.GetWatchingUsersAsync("resource2", "user4"); |
|||
|
|||
sut.Cleanup(); |
|||
|
|||
var watching1 = await sut.GetWatchingUsersAsync("resource1", "user5"); |
|||
var watching2 = await sut.GetWatchingUsersAsync("resource2", "user6"); |
|||
|
|||
Assert.Equal(new[] { "user3", "user5" }, watching1); |
|||
Assert.Equal(new[] { "user4", "user6" }, watching2); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<ng-container *ngIf="users | async; let currentUsers"> |
|||
<ng-container *ngIf="currentUsers.length > 1"> |
|||
<div class="user-item" *ngFor="let userId of currentUsers | slice:0:5; let i = index" [style]="{ zIndex: i }"> |
|||
<img class="user-picture" title="{{userId | sqxUserName}}" [src]="userId | sqxUserPicture"> |
|||
</div> |
|||
|
|||
<div class="user-item" *ngIf="currentUsers.length > 5" [style]="{ zIndex: 6 }"> |
|||
<div class="user-more">+{{currentUsers.length - 5}}</div> |
|||
</div> |
|||
</ng-container> |
|||
</ng-container> |
|||
@ -0,0 +1,31 @@ |
|||
:host { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-right: .5rem; |
|||
} |
|||
|
|||
.user-item { |
|||
display: inline-block; |
|||
margin-right: 0; |
|||
margin-left: -1.1rem; |
|||
position: relative; |
|||
|
|||
&:hover { |
|||
z-index: 1000 !important; |
|||
} |
|||
|
|||
.user-picture, |
|||
.user-more { |
|||
@include circle(2.3rem); |
|||
border: 2px solid $color-white; |
|||
} |
|||
|
|||
.user-more { |
|||
background: $color-border; |
|||
font-size: 90%; |
|||
font-weight: bolder; |
|||
line-height: 2rem; |
|||
text-align: center; |
|||
z-index: 6; |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; |
|||
import { AppsState, CommentsService, switchSafe } from '@app/shared/internal'; |
|||
import { timer } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-watching-users[resource]', |
|||
styleUrls: ['./watching-users.component.scss'], |
|||
templateUrl: './watching-users.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class WatchingUsersComponent { |
|||
private appName: string; |
|||
|
|||
@Input() |
|||
public resource: string; |
|||
|
|||
public users = |
|||
timer(0, 5000).pipe( |
|||
switchSafe((() => this.commentsService.getWatchingUsers(this.appName, this.resource)))); |
|||
|
|||
constructor(appsState: AppsState, |
|||
private readonly commentsService: CommentsService, |
|||
) { |
|||
this.appName = appsState.appName; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue