// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Lists; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.Listing; namespace osu.Game.Online.Chat { public class Channel { public const int MAX_HISTORY = 300; /// <summary> /// Contains every joined user except the current logged in user. Currently only returned for PM channels. /// </summary> public readonly ObservableCollection<APIUser> Users = new ObservableCollection<APIUser>(); [JsonProperty(@"users")] private int[] userIds { set { foreach (int id in value) Users.Add(new APIUser { Id = id }); } } /// <summary> /// Contains all the messages send in the channel. /// </summary> public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default); /// <summary> /// Contains all the messages that weren't read by the user. /// </summary> public IEnumerable<Message> UnreadMessages => Messages.Where(m => LastReadId < m.Id); /// <summary> /// Contains all the messages that are still pending for submission to the server. /// </summary> private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>(); /// <summary> /// An event that fires when new messages arrived. /// </summary> public event Action<IEnumerable<Message>> NewMessagesArrived; /// <summary> /// An event that fires when a pending message gets resolved. /// </summary> public event Action<LocalEchoMessage, Message> PendingMessageResolved; /// <summary> /// An event that fires when a pending message gets removed. /// </summary> public event Action<Message> MessageRemoved; public bool ReadOnly => false; // todo: not yet used. public override string ToString() => Name; [JsonProperty(@"name")] public string Name; [JsonProperty(@"description")] public string Topic; [JsonProperty(@"type")] public ChannelType Type; [JsonProperty(@"channel_id")] public long Id; [JsonProperty(@"last_message_id")] public long? LastMessageId; [JsonProperty(@"last_read_id")] public long? LastReadId; /// <remarks> /// Purposefully nullable for the sake of <see cref="ChannelListing.ChannelListingChannel"/>. /// </remarks> [JsonProperty(@"message_length_limit")] public int? MessageLengthLimit; /// <summary> /// Signals if the current user joined this channel or not. Defaults to false. /// Note that this does not guarantee a join has completed. Check Id > 0 for confirmation. /// </summary> public Bindable<bool> Joined = new Bindable<bool>(); /// <summary> /// Signals if there is a message to highlight. /// This is automatically cleared by the associated <see cref="DrawableChannel"/> after highlighting. /// </summary> public Bindable<Message> HighlightedMessage = new Bindable<Message>(); /// <summary> /// The current text box message while in this <see cref="Channel"/>. /// </summary> public Bindable<string> TextBoxMessage = new Bindable<string>(string.Empty); [JsonConstructor] public Channel() { } /// <summary> /// Create a private messaging channel with the specified user. /// </summary> /// <param name="user">The user to create the private conversation with.</param> public Channel(APIUser user) { Type = ChannelType.PM; Users.Add(user); Name = user.Username; } /// <summary> /// Adds the argument message as a local echo. When this local echo is resolved <see cref="PendingMessageResolved"/> will get called. /// </summary> /// <param name="message"></param> public void AddLocalEcho(LocalEchoMessage message) { pendingMessages.Add(message); Messages.Add(message); NewMessagesArrived?.Invoke(new[] { message }); } public bool MessagesLoaded; /// <summary> /// Adds new messages to the channel and purges old messages. Triggers the <see cref="NewMessagesArrived"/> event. /// </summary> /// <param name="messages"></param> public void AddNewMessages(params Message[] messages) { foreach (var m in messages) { LocalEchoMessage localEcho = pendingMessages.FirstOrDefault(local => local.Uuid == m.Uuid); if (localEcho != null) ReplaceMessage(localEcho, m); } messages = messages.Except(Messages).ToArray(); if (messages.Length == 0) return; Messages.AddRange(messages); long? maxMessageId = messages.Max(m => m.Id); if (LastMessageId == null || maxMessageId > LastMessageId) LastMessageId = maxMessageId; purgeOldMessages(); NewMessagesArrived?.Invoke(messages); } public void RemoveMessagesFromUser(int userId) { for (int i = 0; i < Messages.Count; i++) { var message = Messages[i]; if (message.SenderId == userId) { Messages.RemoveAt(i--); MessageRemoved?.Invoke(message); } } } /// <summary> /// Replace or remove a message from the channel. /// </summary> /// <param name="echo">The local echo message (client-side).</param> /// <param name="final">The response message, or null if the message became invalid.</param> public void ReplaceMessage(LocalEchoMessage echo, Message final) { if (!pendingMessages.Remove(echo)) throw new InvalidOperationException("Attempted to remove echo that wasn't present"); Messages.Remove(echo); if (final == null) { MessageRemoved?.Invoke(echo); return; } if (Messages.Contains(final)) throw new InvalidOperationException("Attempted to add the same message again"); Messages.Add(final); if (final.Id > LastMessageId) LastMessageId = final.Id; PendingMessageResolved?.Invoke(echo, final); } private void purgeOldMessages() { // never purge local echos int messageCount = Messages.Count - pendingMessages.Count; if (messageCount > MAX_HISTORY) Messages.RemoveRange(0, messageCount - MAX_HISTORY); } } }