From a48ccb56038b0daf80fa76abd634b6b1915b10e9 Mon Sep 17 00:00:00 2001 From: miterosan Date: Sun, 8 Apr 2018 22:12:57 +0200 Subject: [PATCH] Implement Private chat --- .../API/Requests/GetUserMessagesRequest.cs | 30 +++ osu.Game/Online/Chat/ChatManager.cs | 79 ++++++- osu.Game/Online/Chat/UserChat.cs | 23 ++ osu.Game/OsuGame.cs | 1 - ...ChatTabControl.cs => ChannelTabControl.cs} | 11 +- .../Overlays/Chat/ChatTabItemCloseButton.cs | 55 +++++ osu.Game/Overlays/Chat/UserChatTabControl.cs | 51 +++++ osu.Game/Overlays/Chat/UserChatTabItem.cs | 201 ++++++++++++++++++ osu.Game/Overlays/ChatOverlay.cs | 37 +++- 9 files changed, 480 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Online/API/Requests/GetUserMessagesRequest.cs create mode 100644 osu.Game/Online/Chat/UserChat.cs rename osu.Game/Overlays/Chat/{ChatTabControl.cs => ChannelTabControl.cs} (95%) create mode 100644 osu.Game/Overlays/Chat/ChatTabItemCloseButton.cs create mode 100644 osu.Game/Overlays/Chat/UserChatTabControl.cs create mode 100644 osu.Game/Overlays/Chat/UserChatTabItem.cs diff --git a/osu.Game/Online/API/Requests/GetUserMessagesRequest.cs b/osu.Game/Online/API/Requests/GetUserMessagesRequest.cs new file mode 100644 index 0000000000..ef9871c5d2 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUserMessagesRequest.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.IO.Network; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.API.Requests +{ + public class GetUserMessagesRequest : APIRequest> + { + private long? since; + + public GetUserMessagesRequest(long? sinceId = null) + { + since = sinceId; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + if (since.HasValue) + request.AddParameter(@"since", since.Value.ToString()); + + return request; + } + + protected override string Target => @"chat/messages/private"; + } +} diff --git a/osu.Game/Online/Chat/ChatManager.cs b/osu.Game/Online/Chat/ChatManager.cs index 69620c8f53..f8c1e53ad8 100644 --- a/osu.Game/Online/Chat/ChatManager.cs +++ b/osu.Game/Online/Chat/ChatManager.cs @@ -11,6 +11,7 @@ using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Users; namespace osu.Game.Online.Chat { @@ -39,12 +40,18 @@ namespace osu.Game.Online.Chat /// The channels available for the player to join /// public ObservableCollection AvailableChannels { get; } = new ObservableCollection(); + /// + /// The user chats opened. + /// + public ObservableCollection OpenedUserChats { get; } = new ObservableCollection(); private APIAccess api; private readonly Scheduler scheduler; private ScheduledDelegate fetchMessagesScheduleder; private GetChannelMessagesRequest fetchChannelMsgReq; + private GetUserMessagesRequest fetchUserMsgReq; private long? lastChannelMsgId; + private long? lastUserMsgId; public ChatManager(Scheduler scheduler) { @@ -55,8 +62,7 @@ namespace osu.Game.Online.Chat private void currentChatChanged(ChatBase chatBase) { if (chatBase is ChannelChat channel && !JoinedChannels.Contains(channel)) - JoinedChannels.Add(channel); - + JoinedChannels.Add(channel); } /// @@ -127,6 +133,63 @@ namespace osu.Game.Online.Chat { if (fetchChannelMsgReq == null) fetchNewChannelMessages(); + + if (fetchUserMsgReq == null) + fetchNewUserMessages(); + } + + private void fetchNewUserMessages() + { + fetchUserMsgReq = new GetUserMessagesRequest(lastUserMsgId); + + fetchUserMsgReq.Success += messages => + { + handleUserMessages(messages); + lastUserMsgId = messages.LastOrDefault()?.Id ?? lastUserMsgId; + fetchUserMsgReq = null; + }; + fetchUserMsgReq.Failure += exception => Logger.Error(exception, "Fetching user messages failed."); + + api.Queue(fetchUserMsgReq); + } + + private void handleUserMessages(IEnumerable messages) + { + var outgoingMessages = messages.Where(m => m.Sender.Id == api.LocalUser.Value.Id); + var outgoingMessagesGroups = outgoingMessages.GroupBy(m => m.TargetId); + var incomingMessagesGroups = messages.Except(outgoingMessages).GroupBy(m => m.UserId); + + foreach (var messageGroup in incomingMessagesGroups) + { + var targetUser = messageGroup.First().Sender; + var chat = OpenedUserChats.FirstOrDefault(c => c.User.Id == targetUser.Id); + + if (chat == null) + { + chat = new UserChat(targetUser); + OpenedUserChats.Add(chat); + } + + chat.AddNewMessages(messageGroup.ToArray()); + var outgoingTargetMessages = outgoingMessagesGroups.FirstOrDefault(g => g.Key == targetUser.Id); + chat.AddNewMessages(outgoingTargetMessages.ToArray()); + } + + var withoutReplyGroups = outgoingMessagesGroups.Where(g => OpenedUserChats.All(m => m.ChatID != g.Key)); + + foreach (var withoutReplyGroup in withoutReplyGroups) + { + var getUserRequest = new GetUserRequest(withoutReplyGroup.First().TargetId); + getUserRequest.Success += user => + { + var chat = new UserChat(user); + + chat.AddNewMessages(withoutReplyGroup.ToArray()); + OpenedUserChats.Add(chat); + }; + + api.Queue(getUserRequest); + } } private void fetchNewChannelMessages() @@ -135,6 +198,8 @@ namespace osu.Game.Online.Chat fetchChannelMsgReq.Success += messages => { + if (messages == null) + return; handleChannelMessages(messages); lastChannelMsgId = messages.LastOrDefault()?.Id ?? lastChannelMsgId; fetchChannelMsgReq = null; @@ -163,7 +228,13 @@ namespace osu.Game.Online.Chat channels.Where(channel => defaultChannels.Contains(channel.Name)) .Where(channel => JoinedChannels.All(c => c.ChatID != channel.ChatID)) - .ForEach(channel => JoinedChannels.Add(channel)); + .ForEach(channel => + { + JoinedChannels.Add(channel); + var fetchInitialMsgReq = new GetChannelMessagesRequest(new[] {channel}, null); + fetchInitialMsgReq.Success += handleChannelMessages; + api.Queue(fetchInitialMsgReq); + }); fetchNewMessages(); }; @@ -185,7 +256,9 @@ namespace osu.Game.Online.Chat break; default: fetchChannelMsgReq?.Cancel(); + fetchChannelMsgReq = null; fetchMessagesScheduleder?.Cancel(); + break; } } diff --git a/osu.Game/Online/Chat/UserChat.cs b/osu.Game/Online/Chat/UserChat.cs new file mode 100644 index 0000000000..2cbb38dad8 --- /dev/null +++ b/osu.Game/Online/Chat/UserChat.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Game.Users; + +namespace osu.Game.Online.Chat +{ + public class UserChat : ChatBase + { + public User User { get; } + + public UserChat(User user, Message[] messages = null) + { + User = user ?? throw new ArgumentNullException(nameof(user)); + + if (messages != null) AddNewMessages(messages); + } + + public override TargetType Target => TargetType.User; + public override long ChatID => User.Id; + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1b55418c7b..cc942a1e32 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -29,7 +29,6 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Input.Bindings; -using osu.Game.Online.Chat; using osu.Game.Rulesets.Mods; using osu.Game.Skinning; using OpenTK.Graphics; diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChannelTabControl.cs similarity index 95% rename from osu.Game/Overlays/Chat/ChatTabControl.cs rename to osu.Game/Overlays/Chat/ChannelTabControl.cs index e495faf944..bf15aa51e9 100644 --- a/osu.Game/Overlays/Chat/ChatTabControl.cs +++ b/osu.Game/Overlays/Chat/ChannelTabControl.cs @@ -21,7 +21,7 @@ using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Chat { - public class ChatTabControl : OsuTabControl + public class ChannelTabControl : OsuTabControl { private const float shear_width = 10; @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Chat private readonly ChannelTabItem.ChannelSelectorTabItem selectorTab; - public ChatTabControl() + public ChannelTabControl() { TabContainer.Margin = new MarginPadding { Left = 50 }; TabContainer.Spacing = new Vector2(-shear_width, 0); @@ -51,6 +51,13 @@ namespace osu.Game.Overlays.Chat ChannelSelectorActive.BindTo(selectorTab.Active); } + public void DeselectAll() + { + if (SelectedTab != null) + SelectedTab.Active.Value = false; + SelectedTab = null; + } + protected override void AddTabItem(TabItem item, bool addToDropdown = true) { if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue) diff --git a/osu.Game/Overlays/Chat/ChatTabItemCloseButton.cs b/osu.Game/Overlays/Chat/ChatTabItemCloseButton.cs new file mode 100644 index 0000000000..e87396356a --- /dev/null +++ b/osu.Game/Overlays/Chat/ChatTabItemCloseButton.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Chat +{ + public class ChatTabItemCloseButton : OsuClickableContainer + { + private readonly SpriteIcon icon; + + public ChatTabItemCloseButton() + { + Size = new Vector2(20); + + Child = icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.75f), + Icon = FontAwesome.fa_close, + RelativeSizeAxes = Axes.Both, + }; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + icon.ScaleTo(0.5f, 1000, Easing.OutQuint); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + icon.ScaleTo(0.75f, 1000, Easing.OutElastic); + return base.OnMouseUp(state, args); + } + + protected override bool OnHover(InputState state) + { + icon.FadeColour(Color4.Red, 200, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + icon.FadeColour(Color4.White, 200, Easing.OutQuint); + base.OnHoverLost(state); + } + } +} diff --git a/osu.Game/Overlays/Chat/UserChatTabControl.cs b/osu.Game/Overlays/Chat/UserChatTabControl.cs new file mode 100644 index 0000000000..73dee8f714 --- /dev/null +++ b/osu.Game/Overlays/Chat/UserChatTabControl.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Chat; +using OpenTK; + +namespace osu.Game.Overlays.Chat +{ + public class UserChatTabControl : OsuTabControl + { + protected override TabItem CreateTabItem(UserChat value) => new UserChatTabItem(value) { OnRequestClose = tabCloseRequested }; + + public Action OnRequestLeave; + + public UserChatTabControl() + { + TabContainer.Spacing = new Vector2(-10, 0); + TabContainer.Masking = false; + } + + protected override void AddTabItem(TabItem item, bool addToDropdown = true) + { + base.AddTabItem(item, addToDropdown); + + if (SelectedTab == null) + SelectTab(item); + } + + private void tabCloseRequested(TabItem priv) + { + int totalTabs = TabContainer.Count -1; // account for selectorTab + int currentIndex = MathHelper.Clamp(TabContainer.IndexOf(priv), 1, totalTabs); + + if (priv == SelectedTab && totalTabs > 1) + // Select the tab after tab-to-be-removed's index, or the tab before if current == last + SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]); + + OnRequestLeave?.Invoke(priv.Value); + } + + public void DeselectAll() + { + if (SelectedTab != null) + SelectedTab.Active.Value = false; + SelectedTab = null; + } + } +} diff --git a/osu.Game/Overlays/Chat/UserChatTabItem.cs b/osu.Game/Overlays/Chat/UserChatTabItem.cs new file mode 100644 index 0000000000..1426a1ac32 --- /dev/null +++ b/osu.Game/Overlays/Chat/UserChatTabItem.cs @@ -0,0 +1,201 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osu.Game.Screens.Menu; +using osu.Game.Users; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Chat +{ + public class UserChatTabItem : TabItem + { + private static readonly Vector2 shear = new Vector2(1f / 5f, 0); + + public override bool IsRemovable => true; + + private readonly Box highlightBox; + private readonly Container backgroundContainer; + private readonly Box backgroundBox; + private readonly OsuSpriteText username; + private readonly ChatTabItemCloseButton closeButton; + + public UserChatTabItem(UserChat value) + : base(value) + { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + Origin = Anchor.BottomRight; + Anchor = Anchor.BottomRight; + EdgeEffect = deactivateEdgeEffect; + Masking = false; + Shear = shear; + + Children = new Drawable[] + { + new Container() + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBox = new Box + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + EdgeSmoothness = new Vector2(1, 0), + }, + } + }, + highlightBox = new Box + { + Width = 5, + BypassAutoSizeAxes = Axes.X, + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + EdgeSmoothness = new Vector2(1, 0), + RelativeSizeAxes = Axes.Y, + Colour = new OsuColour().Yellow + }, + new Container + { + Masking = true, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Child = new FlowContainerWithOrigin + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + X = -5, + Direction = FillDirection.Horizontal, + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Shear = -shear, + + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Margin = new MarginPadding + { + Horizontal = 5 + }, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Children = new Drawable[] + { + + new SpriteIcon + { + Icon = FontAwesome.fa_eercast, + Origin = Anchor.Centre, + Scale = new Vector2(1.2f), + X = -5, + Y = 5, + Anchor = Anchor.Centre, + Colour = new OsuColour().BlueDarker, + RelativeSizeAxes = Axes.Both, + }, + new CircularContainer + { + RelativeSizeAxes = Axes.Y, + Scale = new Vector2(0.95f), + AutoSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + Child = new Avatar(value.User) + { + Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT), + } + }, + } + }, + username = new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Text = value.User.Username, + Margin = new MarginPadding(1), + TextSize = 18, + }, + closeButton = new ChatTabItemCloseButton + { + Height = 1, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Y, + + Action = delegate + { + if (IsRemovable) OnRequestClose?.Invoke(this); + }, + }, + } + } + } + }; + } + + public Action OnRequestClose; + + private readonly EdgeEffectParameters activateEdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 30, + Colour = Color4.Black.Opacity(0.3f), + }; + + protected override void OnActivated() + { + const int activate_length = 1000; + + backgroundBox.ResizeHeightTo(1.1f, activate_length, Easing.OutQuint); + highlightBox.ResizeHeightTo(1.1f, activate_length, Easing.OutQuint); + highlightBox.FadeIn(activate_length, Easing.OutQuint); + username.FadeIn(activate_length, Easing.OutQuint); + username.ScaleTo(new Vector2(1, 1), activate_length, Easing.OutQuint); + closeButton.ScaleTo(new Vector2(1, 1), activate_length, Easing.OutQuint); + closeButton.FadeIn(activate_length, Easing.OutQuint); + TweenEdgeEffectTo(activateEdgeEffect, activate_length); + } + + private readonly EdgeEffectParameters deactivateEdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.0f), + }; + + protected override void OnDeactivated() + { + const int deactivate_length = 500; + + backgroundBox.ResizeHeightTo(1, deactivate_length, Easing.OutQuint); + highlightBox.ResizeHeightTo(1, deactivate_length, Easing.OutQuint); + highlightBox.FadeOut(deactivate_length, Easing.OutQuint); + username.FadeOut(deactivate_length, Easing.OutQuint); + username.ScaleTo(new Vector2(0, 1), deactivate_length, Easing.OutQuint); + closeButton.FadeOut(deactivate_length, Easing.OutQuint); + closeButton.ScaleTo(new Vector2(0, 1), deactivate_length, Easing.OutQuint); + TweenEdgeEffectTo(deactivateEdgeEffect, deactivate_length); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + backgroundBox.Colour = Value.User.Colour != null ? OsuColour.FromHex(Value.User.Colour) : colours.BlueDark; + } + } +} diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 855a631f6b..251e4a2be0 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -45,7 +45,8 @@ namespace osu.Game.Overlays public const float TAB_AREA_HEIGHT = 50; - private readonly ChatTabControl channelTabs; + private readonly ChannelTabControl channelTabs; + private readonly UserChatTabControl userTabs; private readonly Container chatContainer; private readonly Container tabsArea; @@ -154,17 +155,23 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, - channelTabs = new ChatTabControl + channelTabs = new ChannelTabControl { RelativeSizeAxes = Axes.Both, OnRequestLeave = channel => chatManager.JoinedChannels.Remove(channel), }, + userTabs = new UserChatTabControl + { + RelativeSizeAxes = Axes.Both, + OnRequestLeave = privateChat => chatManager.OpenedUserChats.Remove(privateChat), + } } }, }, }, }; + userTabs.Current.ValueChanged += user => chatManager.CurrentChat.Value = user; channelTabs.Current.ValueChanged += newChannel => chatManager.CurrentChat.Value = newChannel; channelTabs.ChannelSelectorActive.ValueChanged += value => channelSelection.State = value ? Visibility.Visible : Visibility.Hidden; channelSelection.StateChanged += state => @@ -241,7 +248,16 @@ namespace osu.Game.Overlays textbox.Current.Disabled = chat.ReadOnly; if (chat is ChannelChat channelChat) + { channelTabs.Current.Value = channelChat; + userTabs.DeselectAll(); + } + + if (chat is UserChat userChat) + { + userTabs.Current.Value = userChat; + channelTabs.DeselectAll(); + } var loaded = loadedChannels.Find(d => d.Chat == chat); if (loaded == null) @@ -355,6 +371,23 @@ namespace osu.Game.Overlays chatManager.CurrentChat.ValueChanged += currentChatChanged; chatManager.JoinedChannels.CollectionChanged += joinedChannelsChanged; chatManager.AvailableChannels.CollectionChanged += availableChannelsChanged; + chatManager.OpenedUserChats.CollectionChanged += openedUserChatsChanged; + } + + private void openedUserChatsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + userTabs.AddItem(args.NewItems[0] as UserChat); + break; + case NotifyCollectionChangedAction.Remove: + userTabs.RemoveItem(args.OldItems[0] as UserChat); + break; + case NotifyCollectionChangedAction.Reset: + userTabs.Clear(); + break; + } } private void postMessage(TextBox textbox, bool newText)