public void SetAsTyping(UserId userId, RoomId roomId, Action onStartedTyping, Action onStoppedTyping) { // Update Activity Tracker for that user/room SetLastActiveInRoom(userId, roomId); // if the user is already typing in the room, just prolonge the typing duration var chatUser = GetUser(userId); if (chatUser.TypingTimer.Running && chatUser.RoomTypingIn == roomId) { chatUser.TypingTimer.ReTime(TypingTimeOut); return; } // if the user has switched to another room, complete is current existing activity if (chatUser.TypingTimer.Running && chatUser.RoomTypingIn != roomId) { UnsetAsTyping(chatUser.Id); } var onStoppedTypingEnhanced = new Action(() => { onStoppedTyping?.Invoke(); chatUser.RoomTypingIn = null; }); // Start the tracker chatUser.TypingTimer.Start(onStartedTyping, onStoppedTypingEnhanced, TypingTimeOut); chatUser.RoomTypingIn = roomId; }
public List <ITextChatMessage> GetHistory(RoomId roomId, List <Enumerables.MessageVisibility> withVisibilities, int messageCount) { using (var db = new HellolingoEntities()) { // [Replaced by a SPROC call to try to improve perf issues] var entityResult = db.TextChats.AsNoTracking() .OrderByDescending(a => a.ID) .Where(a => a.RoomId == roomId && withVisibilities.Any(b => (byte)b == a.Visibility)) // THIS NEXT LINE IS CRITICAL!!!! NOT USING MAKES THE QUERY WAY SLOWER, TO THE POINT THAT IT BRINGS // THE SERVER DOWN WHEN THE IIS APPLICATION POOL IS RECYCLED AND ALL USERS ARE RELOADING ALL THEIR ROOMS. // EF ACTUALLY LOADS THE FULL DEFINITION OF THE USER WHEN ToList IS CALLED... WHICH WILL CLEARLY TAKE A WHOLE LOT OF TIME. .Select(a => new { a.ID, a.When, a.UserId, a.FirstName, a.RoomId, a.Text, a.Visibility }) .Take(messageCount).ToList().OrderBy(a => a.ID); //var entityResult = db.TextChat_GetHistory(messageCount, roomId, string.Join(",", withVisibilities.Cast<int>())); return(entityResult.Select( msg => (ITextChatMessage) new TextChatMessage { When = msg.When, UserId = msg.UserId, FirstName = msg.FirstName, RoomId = msg.RoomId, Text = msg.Text, Visibility = (Enumerables.MessageVisibility)msg.Visibility }).OrderBy(msg => msg.When).ToList()); } }
public void RemoveUserFromRoom(UserId userId, RoomId roomId) { var user = GetUser(userId); _rooms[roomId].RemoveUser(user.Id); _allUsers[userId].JoinedRooms.Remove(roomId); }
public void AddUserToRoom(UserId userId, RoomId roomId) { var user = _allUsers[userId]; PrepareRoom(roomId); _rooms[roomId].AddUser(user.Id); user.JoinedRooms.Add(roomId); }
public virtual void LeaveRoom(UserId userId, RoomId roomId) { ChatModel.RemoveUserFromRoom(userId, roomId); if (roomId.IsPublic()) { OnCountOfUsersUpdated?.Invoke(roomId, ChatModel.UsersCountOf(roomId)); } OnUserLeftRoom?.Invoke(roomId, userId); }
public async Task SetTypingActivity(UserId userId, RoomId roomId) { // Mark user as active SetLastActivity(userId); ChatModel.SetLastActiveInRoom(userId, roomId); // Mark user as Typing var onStartedTyping = new Action(() => OnUserStartedTyping?.Invoke(roomId, userId)); var onStoppedTyping = new Action(() => OnUserStoppedTyping?.Invoke(roomId, userId)); ChatModel.SetAsTyping(userId, roomId, onStartedTyping, onStoppedTyping); }
public List <ITextChatMessage> LatestMessagesIn(RoomId roomId, int messageCount, List <MessageVisibility> withVisibilities = null) { if (withVisibilities == null) { withVisibilities = new List <MessageVisibility> { MessageVisibility.Everyone } } ; return(HasRoom(roomId) && _rooms[roomId].ValidHistory ? _rooms[roomId].Messages.Where(msg => withVisibilities.Contains(msg.Visibility)).Reverse().Take(messageCount).Reverse().ToList() : _storage.GetHistory(roomId, withVisibilities, messageCount)); }
private void PrepareRoom(RoomId roomId) { lock (_locker) { if (!HasRoom(roomId)) { _rooms.Add(roomId, new RoomModel()); } if (!_rooms[roomId].ValidHistory) { var withVisibilities = new List <MessageVisibility> { MessageVisibility.Everyone, MessageVisibility.Ephemeral, MessageVisibility.Sender, MessageVisibility.News }; _rooms[roomId].Messages = LatestMessagesIn(roomId, MinimumHistoryLength, withVisibilities); _rooms[roomId].ValidHistory = true; } } }
public static async Task <Result <bool> > JoinRoom(User user, RoomId roomId) { var result = Result <bool> .True; if (!roomId.IsPrivate()) { return(result); } int userIdEf = user.Id; string roomIdEf = roomId; // Casting named types into their base type because EF doesn't get it otherwise. int partnerId = ChatModel.PartnerIdFrom(roomId, user.Id); using (var db = new HellolingoEntities()) { var userTracker = db.TextChatTrackers.Find(userIdEf, roomIdEf); var partnerTracker = db.TextChatTrackers.Find(partnerId, roomIdEf); // Handle people directly joining by the url from outside the chat if (userTracker == null || partnerTracker == null) { await RequestPrivateChat(user, roomId, partnerId); result.Reports.AddReport(LogTag.ChatRequestAddedFromJoinRoom, new { userId = user.Id, roomId }); return(result); } // Set new statuses according to current status if ((userTracker.Status == TrackerStatus.Invited || userTracker.Status == TrackerStatus.IgnoredInvite) && partnerTracker.Status == TrackerStatus.Inviting) { userTracker.Status = TrackerStatus.AcceptedInvite; partnerTracker.Status = TrackerStatus.InviteAccepted; userTracker.StatusAt = partnerTracker.StatusAt = DateTime.Now; result.Reports = new LogReports(LogTag.ChatRequestAccepted); } else if (userTracker.Status == TrackerStatus.InviteAccepted) { userTracker.Status = TrackerStatus.SeenInviteResponse; userTracker.StatusAt = DateTime.Now; } await db.SaveChangesAsync(); } return(result); }
public static async Task <Result <bool> > IgnorePrivateChat(UserId userId, RoomId roomId, UserId partnerId) { int userIdEf = userId, partnerIdEf = partnerId; string roomIdEf = roomId; // Casting named types into their base type because EF doesn't get it otherwise. using (var db = new HellolingoEntities()) { var inviterRecord = db.TextChatTrackers.Find(partnerIdEf, roomIdEf); var inviteeRecord = db.TextChatTrackers.Find(userIdEf, roomIdEf); if (inviterRecord == null || inviteeRecord == null) { return(new Result <bool>(false, LogTag.IgnorePrivateChatMissingTrackerRecord, new { userId, roomId, partnerId }, LogLevel.Error)); } // Set record for invitee inviteeRecord.Status = TrackerStatus.IgnoredInvite; inviteeRecord.StatusAt = DateTime.Now; await db.SaveChangesAsync(); } return(Result <bool> .True); }
public static async Task <Result <bool> > MarkAllCaughtUp(UserId userId, RoomId roomId) { int userIdEf = userId; string roomIdEf = roomId; // Casting named types into their base type because EF doesn't get it otherwise. var result = Result <bool> .True; using (var db = new HellolingoEntities()) { var userRecord = db.TextChatTrackers.Find(userIdEf, roomIdEf); if (userRecord == null) { return(new Result <bool>(false, LogTag.UnexpectedTrackerStatus, new { method = "MarkAllCaughtUp", userId, roomId }, LogLevel.Error)); } if (userRecord.Status == TrackerStatus.AllCaughtUp || userRecord.Status == TrackerStatus.Inviting || userRecord.Status == TrackerStatus.Invited) { return(result); } userRecord.Status = TrackerStatus.AllCaughtUp; userRecord.StatusAt = DateTime.Now; await db.SaveChangesAsync(); } return(result); }
public static async Task <Result <bool> > PostTo(UserId userId, RoomId roomId) { var result = Result <bool> .True; string roomIdEf = roomId; // Casting named types into their base type because EF doesn't get it otherwise. int partnerId = ChatModel.PartnerIdFrom(roomId, userId); using (var db = new HellolingoEntities()) { var partnerRecord = db.TextChatTrackers.Find(partnerId, roomIdEf); if (partnerRecord == null) { return(new Result <bool>(false, LogTag.UnexpectedTrackerStatus, new { method = "PostTo", userId, roomId }, LogLevel.Error)); } if (partnerRecord.Status == TrackerStatus.Invited || partnerRecord.Status == TrackerStatus.AutoBlocked || partnerRecord.Status == TrackerStatus.DeclinedInvite || partnerRecord.Status == TrackerStatus.IgnoredInvite || partnerRecord.Status == TrackerStatus.UnreadMessages) { return(new Result <bool>(true)); } partnerRecord.Status = TrackerStatus.UnreadMessages; partnerRecord.StatusAt = DateTime.Now; await db.SaveChangesAsync(); } return(result); }
public void CloseRoom(RoomId roomId) => _rooms.Remove(roomId);
public int CountOfMessagesInRoom(RoomId roomId) => LatestMessagesIn(roomId, MinimumHistoryLength).Count;
public bool HasRoom(RoomId roomId) => _rooms.ContainsKey(roomId);
public static UserId PartnerIdFrom(RoomId roomId, UserId userId) { var userIds = roomId.ToString().Split('-').Select(int.Parse).ToArray(); return(userId == userIds[0] ? userIds[1] : userIds[0]); }
public static UserId PartnerInPrivateRoom(RoomId roomId, UserId thisUserId) => UserIdsInPrivateRoom(roomId).First(id => id != thisUserId);
public void AudioCallConnected(UserId userId, RoomId roomId) => OnAudioCallConnected?.Invoke(roomId, userId);
public void DeclineAudioCall(RoomId roomId, string reason, ConnectionId connId) => OnUserDeclinedAudioCall?.Invoke(roomId, reason, connId);
public void RequestAudioCall(RoomId roomId, UserId userId) { SetLastActivity(userId); OnUserRequestedAudioCall?.Invoke(roomId, userId); }
public bool IsEmpty(RoomId roomId) => !_rooms[roomId].Users.Any();
public bool IsInRoom(RoomId roomId, UserId userId) => HasRoom(roomId) && _rooms[roomId].HasUser(userId);
public void CancelAudioCall(UserId userId, RoomId roomId) => OnUserCancelledAudioCall?.Invoke(roomId, userId);
public int UsersCountOf(RoomId roomId) => _rooms[roomId].Users.Count;
public void HangoutAudioCall(UserId userId, RoomId roomId) => OnUserHangoutedAudioCall?.Invoke(roomId, userId);
public ITextChatMessage LastMessageIn(RoomId roomId) => _rooms.ContainsKey(roomId) ? _rooms[roomId].LastMessage : null;
// Simple Helpers public static List <UserId> UserIdsInPrivateRoom(RoomId roomId) => roomId.ToString().Split('-').Select(id => (UserId)int.Parse(id)).ToList();
public List <UserId> UsersInRoom(RoomId roomId, UserId except = null) => _rooms[roomId].Users.Keys.Where(r => r != except).ToList();
public virtual Tuple <List <UserId>, List <ITextChatMessage> > JoinRoom(UserId userId, RoomId roomId) { var user = ChatModel.GetUser(userId); // If the room is private and the user has nothing to do here, EXPLODE! if (roomId.IsPrivate() && !UserIdsInPrivateRoom(roomId).Contains(userId)) { throw new LogReadyException(LogTag.PrivateRoomIntrusionAttempt, new { userId, roomId }); } if (!ChatModel.IsInRoom(roomId, user.Id)) { ChatModel.AddUserToRoom(userId, roomId); OnUserJoinedRoom?.Invoke(roomId, user.Id); if (roomId.IsPublic()) { OnCountOfUsersUpdated?.Invoke(roomId, ChatModel.UsersCountOf(roomId)); } } var withVisibilities = new List <MessageVisibility> { MessageVisibility.Everyone, MessageVisibility.Sender, MessageVisibility.Ephemeral, MessageVisibility.News }; var customMessageHistory = ChatModel.LatestMessagesIn(roomId, HistoryLength * 2, withVisibilities) .Where(a => (a.Visibility != MessageVisibility.Sender && a.Visibility != MessageVisibility.Ephemeral) || a.UserId == userId) .Reverse().Take(HistoryLength).Reverse() .ToList(); // If in a private room and the partner doesn't want private chat, Signal it with a service message if (roomId.IsGroup()) { goto Skip; } var partnerId = PartnerInPrivateRoom(roomId, userId); if (!ChatModel.IsInChat(partnerId)) { goto Skip; } var partner = ChatModel.GetUser(partnerId); if (partner.IsNoPrivateChat) { customMessageHistory.Add(new TextChatMessage { RoomId = roomId, Text = JsonConvert.SerializeObject(new { noPrivateChat = ChatModel.GetPublicRoomsFor(partner.Id) }, Formatting.None, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), Visibility = MessageVisibility.System }); } Skip: return(new Tuple <List <UserId>, List <ITextChatMessage> >( ChatModel.UsersInRoom(roomId, user.Id), customMessageHistory )); }
public bool IsTypingInRoom(ITextChatUser user, RoomId roomId) => user.TypingTimer.Running && user.RoomTypingIn == roomId;