using Collaboration.Domain.Enums;
namespace Collaboration.Domain.Entities;
///
/// Represents a user's participation in a session.
/// A user can rejoin after leaving — each join creates a new record.
///
public class CollabParticipant
{
public Guid ParticipantId { get; private set; }
public Guid SessionId { get; private set; }
public Guid UserId { get; private set; }
public string DisplayName { get; private set; } = default!;
public ParticipantRole Role { get; private set; }
public bool IsActive { get; private set; }
public bool HasControl { get; private set; } // Only one per session at a time
public bool IsFollowingHost { get; private set; } // Auto-navigate with host
public string? SignalRConnectionId { get; private set; }
/// Assigned color for cursor and presence indicator (hex).
public string CursorColor { get; private set; } = default!;
public DateTime JoinedAt { get; private set; }
public DateTime? LeftAt { get; private set; }
public DateTime? LastHeartbeatAt { get; private set; }
// Navigation
public CollabSession Session { get; private set; } = default!;
private CollabParticipant() { }
public static CollabParticipant Create(
Guid sessionId,
Guid userId,
string displayName,
ParticipantRole role,
string cursorColor)
{
return new CollabParticipant
{
ParticipantId = Guid.NewGuid(),
SessionId = sessionId,
UserId = userId,
DisplayName = displayName,
Role = role,
IsActive = true,
HasControl = role == ParticipantRole.Host, // Host starts with control
IsFollowingHost = role != ParticipantRole.Host,
CursorColor = cursorColor,
JoinedAt = DateTime.UtcNow,
LastHeartbeatAt = DateTime.UtcNow
};
}
public void UpdateConnectionId(string connectionId)
{
SignalRConnectionId = connectionId;
IsActive = true;
LastHeartbeatAt = DateTime.UtcNow;
}
public void GrantControl()
{
HasControl = true;
}
public void RevokeControl()
{
HasControl = false;
}
public void ChangeRole(ParticipantRole newRole)
{
Role = newRole;
}
public void ToggleFollowHost(bool follow)
{
IsFollowingHost = follow;
}
public void Leave()
{
IsActive = false;
HasControl = false;
LeftAt = DateTime.UtcNow;
SignalRConnectionId = null;
}
public void Heartbeat()
{
LastHeartbeatAt = DateTime.UtcNow;
}
}
///
/// Invite sent to a user to join a session. Supports in-app and link-based invites.
///
public class CollabSessionInvite
{
public Guid InviteId { get; private set; }
public Guid SessionId { get; private set; }
/// Null for link-based invites not targeting a specific user.
public Guid? InvitedUserId { get; private set; }
public string InviteToken { get; private set; } = default!;
public ParticipantRole OfferedRole { get; private set; }
public InviteStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime ExpiresAt { get; private set; }
public DateTime? RespondedAt { get; private set; }
public Guid CreatedBy { get; private set; }
// Navigation
public CollabSession Session { get; private set; } = default!;
private CollabSessionInvite() { }
public static CollabSessionInvite Create(
Guid sessionId,
Guid createdBy,
ParticipantRole offeredRole,
int expiryMinutes,
Guid? invitedUserId = null)
{
return new CollabSessionInvite
{
InviteId = Guid.NewGuid(),
SessionId = sessionId,
InvitedUserId = invitedUserId,
InviteToken = GenerateToken(),
OfferedRole = offeredRole,
Status = InviteStatus.Pending,
CreatedBy = createdBy,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddMinutes(expiryMinutes)
};
}
public bool IsValid() => Status == InviteStatus.Pending && ExpiresAt > DateTime.UtcNow;
public void Accept()
{
if (!IsValid()) throw new InvalidOperationException("Invite is no longer valid.");
Status = InviteStatus.Accepted;
RespondedAt = DateTime.UtcNow;
}
public void Decline()
{
if (!IsValid()) throw new InvalidOperationException("Invite is no longer valid.");
Status = InviteStatus.Declined;
RespondedAt = DateTime.UtcNow;
}
public void Revoke()
{
Status = InviteStatus.Revoked;
}
public void MarkExpired()
{
if (Status == InviteStatus.Pending)
Status = InviteStatus.Expired;
}
private static string GenerateToken() =>
Convert.ToBase64String(Guid.NewGuid().ToByteArray())
.Replace("+", "-").Replace("/", "_").TrimEnd('=');
}
///
/// Immutable audit event. Written for every significant session action.
/// Foundation for audit trail and (later) session replay.
///
public class CollabSessionEvent
{
public long EventId { get; private set; } // Long for high-volume append
public Guid SessionId { get; private set; }
public SessionEventType EventType { get; private set; }
public Guid TriggeredBy { get; private set; }
public string? Payload { get; private set; } // JSON, event-specific data
public DateTime Timestamp { get; private set; }
// Navigation
public CollabSession Session { get; private set; } = default!;
private CollabSessionEvent() { }
public static CollabSessionEvent Create(
Guid sessionId,
SessionEventType eventType,
Guid triggeredBy,
string? payload = null)
{
return new CollabSessionEvent
{
SessionId = sessionId,
EventType = eventType,
TriggeredBy = triggeredBy,
Payload = payload,
Timestamp = DateTime.UtcNow
};
}
}