diff --git a/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs index 1c1675a67c..1d39cba81d 100644 --- a/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs +++ b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs @@ -87,15 +87,18 @@ namespace osu.Game.Tests.Visual private void addRandomUser() { - channelTabControl.AddChannel(new PrivateChannel + channelTabControl.AddChannel(new Channel { - User = users?.Count > 0 + Users = + { + users?.Count > 0 ? users[RNG.Next(0, users.Count - 1)] : new User { Id = RNG.Next(), Username = "testuser" + RNG.Next(1000) } + } }); } diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index e6ad72a2bc..c49490ea19 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -17,9 +17,19 @@ namespace osu.Game.Online.Chat public readonly int MaxHistory = 300; /// - /// Contains every joined user except the current logged in user. + /// Contains every joined user except the current logged in user. Currently only returned for PM channels. /// - public readonly ObservableCollection JoinedUsers = new ObservableCollection(); + public readonly ObservableCollection Users = new ObservableCollection(); + + [JsonProperty(@"users")] + private long[] userIds + { + set + { + foreach (var id in value) + Users.Add(new User { Id = id }); + } + } /// /// Contains all the messages send in the channel. @@ -47,11 +57,6 @@ namespace osu.Game.Online.Chat /// public event Action MessageRemoved; - /// - /// Signalles whether the channels target is a private channel or public channel. - /// - public TargetType Target { get; protected set; } - public bool ReadOnly => false; //todo not yet used. public override string ToString() => Name; diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 377e9ee7bb..8099f97999 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -39,12 +39,12 @@ namespace osu.Game.Online.Chat /// /// The Channels the player has joined /// - public ObservableCollection JoinedChannels { get; } = new ObservableCollection(); + public ObservableCollection JoinedChannels { get; } = new ObservableCollection(); //todo: should be publicly readonly /// /// The channels available for the player to join /// - public ObservableCollection AvailableChannels { get; } = new ObservableCollection(); + public ObservableCollection AvailableChannels { get; } = new ObservableCollection(); //todo: should be publicly readonly /*private readonly IncomingMessagesHandler privateMessagesHandler;*/ @@ -54,12 +54,6 @@ namespace osu.Game.Online.Chat public ChannelManager() { CurrentChannel.ValueChanged += currentChannelChanged; - - /*channelMessagesHandler = new IncomingMessagesHandler( - lastId => new GetMessagesRequest(JoinedChannels.Where(c => c.Target == TargetType.Channel)), handleChannelMessages); - - privateMessagesHandler = new IncomingMessagesHandler( - lastId => new GetPrivateMessagesRequest(lastId),handleUserMessages);*/ } /// @@ -85,14 +79,13 @@ namespace osu.Game.Online.Chat if (user == null) throw new ArgumentNullException(nameof(user)); - CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Target == TargetType.User && c.Id == user.Id) - ?? new PrivateChannel { User = user }; + CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Users.Any(u => u.Id == user.Id)) + ?? new Channel { Name = user.Username, Users = { user } }; } private void currentChannelChanged(Channel channel) { - if (!JoinedChannels.Contains(channel)) - JoinedChannels.Add(channel); + JoinChannel(channel); } /// @@ -169,71 +162,6 @@ namespace osu.Game.Online.Chat } } - private void fetchNewMessages() - { - /*if (channelMessagesHandler.CanRequestNewMessages) - channelMessagesHandler.RequestNewMessages(api); - - if (privateMessagesHandler.CanRequestNewMessages) - privateMessagesHandler.RequestNewMessages(api);*/ - } - - private void handleUserMessages(IEnumerable messages) - { - var joinedPrivateChannels = JoinedChannels.Where(c => c.Target == TargetType.User).ToList(); - - Channel getChannelForUser(User user) - { - var channel = joinedPrivateChannels.FirstOrDefault(c => c.Id == user.Id); - - if (channel == null) - { - channel = new PrivateChannel { User = user }; - JoinedChannels.Add(channel); - joinedPrivateChannels.Add(channel); - } - - return channel; - } - - long localUserId = api.LocalUser.Value.Id; - - var outgoingGroups = messages.Where(m => m.Sender.Id == localUserId).GroupBy(m => m.ChannelId); - var incomingGroups = messages.Where(m => m.Sender.Id != localUserId).GroupBy(m => m.UserId); - - foreach (var group in incomingGroups) - { - var targetUser = group.First().Sender; - - var channel = getChannelForUser(targetUser); - - channel.AddNewMessages(group.ToArray()); - - var outgoingTargetMessages = outgoingGroups.FirstOrDefault(g => g.Key == targetUser.Id); - if (outgoingTargetMessages != null) - channel.AddNewMessages(outgoingTargetMessages.ToArray()); - } - - // Because of the way the API provides data right now, outgoing messages do not contain required - // user (or in the future, target channel) metadata. As such we need to do a second request - // to find out the specifics of the user. - var withoutReplyGroups = outgoingGroups.Where(g => joinedPrivateChannels.All(m => m.Id != g.Key)); - - foreach (var withoutReplyGroup in withoutReplyGroups) - { - var userReq = new GetUserRequest(withoutReplyGroup.First().ChannelId); - - userReq.Failure += exception => Logger.Error(exception, "Failed to get user informations."); - userReq.Success += user => - { - var channel = getChannelForUser(user); - channel.AddNewMessages(withoutReplyGroup.ToArray()); - }; - - api.Queue(userReq); - } - } - private void handleChannelMessages(IEnumerable messages) { var channels = JoinedChannels.ToList(); @@ -246,32 +174,24 @@ namespace osu.Game.Online.Chat { var req = new ListChannelsRequest(); + //var joinDefaults = JoinedChannels.Count == 0; + req.Success += channels => { foreach (var channel in channels) { - if (JoinedChannels.Any(c => c.Id == channel.Id)) - continue; - // add as available if not already if (AvailableChannels.All(c => c.Id != channel.Id)) AvailableChannels.Add(channel); // join any channels classified as "defaults" - if (defaultChannels.Any(c => c.Equals(channel.Name, StringComparison.OrdinalIgnoreCase))) - { - JoinedChannels.Add(channel); - - FetchInitalMessages(channel); - } + /*if (joinDefaults && defaultChannels.Any(c => c.Equals(channel.Name, StringComparison.OrdinalIgnoreCase))) + JoinChannel(channel);*/ } - - fetchNewMessages(); }; req.Failure += error => { Logger.Error(error, "Fetching channel list failed"); - initializeDefaultChannels(); }; @@ -285,7 +205,7 @@ namespace osu.Game.Online.Chat /// right now it caps out at 50 messages and therefore only returns one channel's worth of content. /// /// The channel - public void FetchInitalMessages(Channel channel) + private void fetchInitalMessages(Channel channel) { var fetchInitialMsgReq = new GetMessagesRequest(channel); fetchInitialMsgReq.Success += handleChannelMessages; @@ -293,6 +213,62 @@ namespace osu.Game.Online.Chat api.Queue(fetchInitialMsgReq); } + public void JoinChannel(Channel channel) + { + if (channel == null) return; + + // ReSharper disable once AccessToModifiedClosure + var existing = JoinedChannels.FirstOrDefault(c => c.Id == channel.Id); + + if (existing != null) + { + // if we already have this channel loaded, we don't want to make a second one. + channel = existing; + } + else + { + var foundSelf = channel.Users.FirstOrDefault(u => u.Id == api.LocalUser.Value.Id); + if (foundSelf != null) + channel.Users.Remove(foundSelf); + + JoinedChannels.Add(channel); + + if (channel.Type == ChannelType.Public && !channel.Joined) + { + var req = new JoinChannelRequest(channel, api.LocalUser); + req.Success += () => JoinChannel(channel); + req.Failure += ex => LeaveChannel(channel); + api.Queue(req); + return; + } + } + + if (CurrentChannel.Value == null) + CurrentChannel.Value = channel; + + if (!channel.Joined.Value) + { + // let's fetch a small number of messages to bring us up-to-date with the backlog. + fetchInitalMessages(channel); + channel.Joined.Value = true; + } + } + + public void LeaveChannel(Channel channel) + { + if (channel == null) return; + + if (channel == CurrentChannel.Value) CurrentChannel.Value = null; + + JoinedChannels.Remove(channel); + + if (channel.Joined.Value) + { + api.Queue(new LeaveChannelRequest(channel, api.LocalUser)); + channel.Joined.Value = false; + } + } + public void APIStateChanged(APIAccess api, APIState state) { switch (state) @@ -301,18 +277,53 @@ namespace osu.Game.Online.Chat if (JoinedChannels.Count == 0) initializeDefaultChannels(); - fetchMessagesScheduleder = Scheduler.AddDelayed(fetchNewMessages, 1000, true); + fetchUpdates(); break; default: - /*channelMessagesHandler.CancelOngoingRequests(); - privateMessagesHandler.CancelOngoingRequests();*/ - fetchMessagesScheduleder?.Cancel(); fetchMessagesScheduleder = null; break; } } + private long lastMessageId; + private const int update_poll_interval = 1000; + + private void fetchUpdates() + { + fetchMessagesScheduleder?.Cancel(); + fetchMessagesScheduleder = Scheduler.AddDelayed(() => + { + var fetchReq = new GetUpdatesRequest(lastMessageId); + + fetchReq.Success += updates => + { + if (updates?.Presence != null) + { + foreach (var channel in updates.Presence) + { + JoinChannel(AvailableChannels.FirstOrDefault(c => c.Id == channel.Id) ?? channel); + } + + //todo: handle left channels + + handleChannelMessages(updates.Messages); + + foreach (var group in updates.Messages.GroupBy(m => m.ChannelId)) + JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); + + lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId; + } + + fetchUpdates(); + }; + + fetchReq.Failure += delegate { fetchUpdates(); }; + + api.Queue(fetchReq); + }, update_poll_interval); + } + [BackgroundDependencyLoader] private void load(IAPIProvider api) { diff --git a/osu.Game/Online/Chat/IncomingMessagesHandler.cs b/osu.Game/Online/Chat/IncomingMessagesHandler.cs deleted file mode 100644 index 46f2b805b3..0000000000 --- a/osu.Game/Online/Chat/IncomingMessagesHandler.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using osu.Framework.Logging; -using osu.Game.Online.API; - -namespace osu.Game.Online.Chat -{ - /// - /// Handles tracking and updating of a specific message type, allowing polling and requesting of only new messages on an ongoing basis. - /// - public class IncomingMessagesHandler - { - public delegate APIMessagesRequest CreateRequestDelegate(long? lastMessageId); - - public long? LastMessageId { get; private set; } - - private APIMessagesRequest getMessagesRequest; - - private readonly CreateRequestDelegate createRequest; - private readonly Action> onNewMessages; - - public bool CanRequestNewMessages => getMessagesRequest == null; - - public IncomingMessagesHandler([NotNull] CreateRequestDelegate createRequest, [NotNull] Action> onNewMessages) - { - this.createRequest = createRequest ?? throw new ArgumentNullException(nameof(createRequest)); - this.onNewMessages = onNewMessages ?? throw new ArgumentNullException(nameof(onNewMessages)); - } - - public void RequestNewMessages(IAPIProvider api) - { - if (!CanRequestNewMessages) - throw new InvalidOperationException("Requesting new messages is not possible yet, because the old request is still ongoing."); - - getMessagesRequest = createRequest.Invoke(LastMessageId); - getMessagesRequest.Success += handleNewMessages; - getMessagesRequest.Failure += exception => - { - Logger.Error(exception, "Fetching messages failed."); - - // allowing new messages to be requested even after the fail. - getMessagesRequest = null; - }; - - api.Queue(getMessagesRequest); - } - - private void handleNewMessages(List messages) - { - // allowing new messages to be requested. - getMessagesRequest = null; - - // in case of no new messages we simply do nothing. - if (messages == null || messages.Count == 0) - return; - - onNewMessages.Invoke(messages); - - LastMessageId = messages.Max(m => m.Id) ?? LastMessageId; - } - - public void CancelOngoingRequests() - { - getMessagesRequest?.Cancel(); - } - } -} diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index b4a7b6faa7..3f0f352ac7 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Newtonsoft.Json; using osu.Game.Users; @@ -69,12 +68,4 @@ namespace osu.Game.Online.Chat // ReSharper disable once ImpureMethodCallOnReadonlyValueField public override int GetHashCode() => Id.GetHashCode(); } - - public enum TargetType - { - [Description(@"channel")] - Channel, - [Description(@"user")] - User - } } diff --git a/osu.Game/Online/Chat/PrivateChannel.cs b/osu.Game/Online/Chat/PrivateChannel.cs deleted file mode 100644 index aac88ecb27..0000000000 --- a/osu.Game/Online/Chat/PrivateChannel.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Users; - -namespace osu.Game.Online.Chat -{ - public class PrivateChannel : Channel - { - public User User - { - set - { - Name = value.Username; - Id = value.Id; - JoinedUsers.Add(value); - } - } - - /// - /// Contructs a private channel - /// - /// The user - public PrivateChannel() - { - Target = TargetType.User; - } - } -} diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index 6470963b4f..08d4e40a64 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -54,11 +54,11 @@ namespace osu.Game.Overlays.Chat.Tabs protected override TabItem CreateTabItem(Channel value) { - switch (value.Target) + switch (value.Type) { - case TargetType.Channel: + case ChannelType.Public: return new ChannelTabItem(value) { OnRequestClose = tabCloseRequested }; - case TargetType.User: + case ChannelType.PM: return new PrivateChannelTabItem(value) { OnRequestClose = tabCloseRequested }; default: throw new InvalidOperationException("Only TargetType User and Channel are supported."); diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 7492de0123..c7ca1cb073 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Chat.Tabs public PrivateChannelTabItem(Channel value) : base(value) { - if (value.Target != TargetType.User) + if (value.Type != ChannelType.PM) throw new ArgumentException("Argument value needs to have the targettype user!"); AddRange(new Drawable[] @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Chat.Tabs Anchor = Anchor.Centre, Origin = Anchor.Centre, Masking = true, - Child = new DelayedLoadWrapper(new Avatar(value.JoinedUsers.First()) + Child = new DelayedLoadWrapper(new Avatar(value.Users.First()) { RelativeSizeAxes = Axes.Both, OnLoadComplete = d => d.FadeInFromZero(300, Easing.OutQuint), @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Chat.Tabs [BackgroundDependencyLoader] private void load(OsuColour colours) { - var user = Value.JoinedUsers.First(); + var user = Value.Users.First(); BackgroundActive = user.Colour != null ? OsuColour.FromHex(user.Colour) : colours.BlueDark; BackgroundInactive = BackgroundActive.Darken(0.5f); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 9fc4c15849..e45373c36f 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -153,7 +153,7 @@ namespace osu.Game.Overlays Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - OnRequestLeave = channel => channelManager.JoinedChannels.Remove(channel) + OnRequestLeave = channel => channelManager.LeaveChannel(channel) }, } }, @@ -176,15 +176,9 @@ namespace osu.Game.Overlays else textbox.HoldFocus = true; }; - channelSelection.OnRequestJoin = channel => - { - if (!channelManager.JoinedChannels.Contains(channel)) - { - channelManager.JoinedChannels.Add(channel); - channelManager.FetchInitalMessages(channel); - } - }; - channelSelection.OnRequestLeave = channel => channelManager.JoinedChannels.Remove(channel); + + channelSelection.OnRequestJoin = channel => channelManager.JoinChannel(channel); + channelSelection.OnRequestLeave = channel => channelManager.LeaveChannel(channel); } private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) @@ -195,18 +189,16 @@ namespace osu.Game.Overlays foreach (Channel newChannel in args.NewItems) { channelTabControl.AddChannel(newChannel); - - newChannel.Joined.Value = true; } + break; case NotifyCollectionChangedAction.Remove: foreach (Channel removedChannel in args.OldItems) { channelTabControl.RemoveChannel(removedChannel); - - loadedChannels.Remove(loadedChannels.Find(c => c.Channel == removedChannel )); - removedChannel.Joined.Value = false; + loadedChannels.Remove(loadedChannels.Find(c => c.Channel == removedChannel)); } + break; } }