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 }; } }