1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 07:23:14 +08:00

Merge pull request #18033 from jai-x/new-chat-overlay

Implement basic layout and behaviour of new chat overlay
This commit is contained in:
Dean Herbert 2022-05-06 00:10:46 +09:00 committed by GitHub
commit 14d2159b8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 884 additions and 42 deletions

View File

@ -0,0 +1,422 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat;
using osu.Game.Overlays.Chat.Listing;
using osu.Game.Overlays.Chat.ChannelList;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
{
private ChatOverlayV2 chatOverlay;
private ChannelManager channelManager;
private APIUser testUser;
private Channel testPMChannel;
private Channel[] testChannels;
private Channel testChannel1 => testChannels[0];
private Channel testChannel2 => testChannels[1];
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
testUser = new APIUser { Username = "test user", Id = 5071479 };
testPMChannel = new Channel(testUser);
testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(ChannelManager), channelManager = new ChannelManager()),
},
Children = new Drawable[]
{
channelManager,
chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both },
},
};
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Setup request handler", () =>
{
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case GetUpdatesRequest getUpdates:
getUpdates.TriggerFailure(new WebException());
return true;
case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess();
return true;
case LeaveChannelRequest leaveChannel:
leaveChannel.TriggerSuccess();
return true;
case GetMessagesRequest getMessages:
getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel));
return true;
case GetUserRequest getUser:
if (getUser.Lookup == testUser.Username)
getUser.TriggerSuccess(testUser);
else
getUser.TriggerFailure(new WebException());
return true;
case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000))
{
Content = postMessage.Message.Content,
ChannelId = postMessage.Message.ChannelId,
Sender = postMessage.Message.Sender,
Timestamp = new DateTimeOffset(DateTime.Now),
});
return true;
default:
Logger.Log($"Unhandled Request Type: {req.GetType()}");
return false;
}
};
});
AddStep("Add test channels", () =>
{
(channelManager.AvailableChannels as BindableList<Channel>)?.AddRange(testChannels);
});
}
[Test]
public void TestShowHide()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
AddStep("Hide overlay", () => chatOverlay.Hide());
AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestChatHeight()
{
Bindable<float> configChatHeight = null;
float newHeight = 0;
AddStep("Bind config chat height", () => configChatHeight = config.GetBindable<float>(OsuSetting.ChatDisplayHeight).GetBoundCopy());
AddStep("Set config chat height", () => configChatHeight.Value = 0.4f);
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Overlay uses config height", () => chatOverlay.Height == 0.4f);
AddStep("Drag overlay to new height", () =>
{
InputManager.MoveMouseTo(chatOverlayTopBar);
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300));
InputManager.ReleaseButton(MouseButton.Left);
});
AddStep("Store new height", () => newHeight = chatOverlay.Height);
AddAssert("Config height changed", () => configChatHeight.Value != 0.4f && configChatHeight.Value == newHeight);
AddStep("Hide overlay", () => chatOverlay.Hide());
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight);
}
[Test]
public void TestChannelSelection()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("Listing is hidden", () => listingVisibility == Visibility.Hidden);
AddAssert("Loading is hidden", () => loadingVisibility == Visibility.Hidden);
AddAssert("Current channel is correct", () => channelManager.CurrentChannel.Value == testChannel1);
AddAssert("DrawableChannel is correct", () => currentDrawableChannel.Channel == testChannel1);
}
[Test]
public void TestSearchInListing()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible);
AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
AddUntilStep("Only channel 2 visibile", () =>
{
IEnumerable<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
.Where(item => item.IsPresent);
return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
});
}
[Test]
public void TestChannelCloseButton()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join PM and public channels", () =>
{
channelManager.JoinChannel(testChannel1);
channelManager.JoinChannel(testPMChannel);
});
AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddStep("Click close button", () =>
{
ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType<ChannelListItemCloseButton>().Single();
clickDrawable(closeButton);
});
AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel));
AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Click close button", () =>
{
ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType<ChannelListItemCloseButton>().Single();
clickDrawable(closeButton);
});
AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1));
}
[Test]
public void TestChatCommand()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
AddAssert("PM channel is selected", () =>
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist"));
AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage);
// Make sure no unnecessary requests are made when the PM channel is already open.
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null);
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
AddAssert("PM channel is selected", () =>
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
}
[Test]
public void TestMultiplayerChannelIsNotShown()
{
Channel multiplayerChannel = null;
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
{
Name = "#mp_1",
Type = ChannelType.Multiplayer,
}));
AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType<ChannelListingItem>()
.Where(item => item.IsPresent)
.Select(item => item.Channel)
.Contains(multiplayerChannel));
}
[Test]
public void TestHighlightOnCurrentChannel()
{
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 1", () =>
{
testChannel1.AddNewMessages(message = new Message
{
ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
}
[Test]
public void TestHighlightOnAnotherChannel()
{
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 2", () =>
{
testChannel2.AddNewMessages(message = new Message
{
ChannelId = testChannel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2);
AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2);
}
[Test]
public void TestHighlightOnLeftChannel()
{
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 2", () =>
{
testChannel2.AddNewMessages(message = new Message
{
ChannelId = testChannel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2));
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2);
AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2);
}
[Test]
public void TestHighlightWhileChatNeverOpen()
{
Message message = null;
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Send message in channel 1", () =>
{
testChannel1.AddNewMessages(message = new Message
{
ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
}
[Test]
public void TestHighlightWithNullChannel()
{
Message message = null;
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Send message in channel 1", () =>
{
testChannel1.AddNewMessages(message = new Message
{
ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
}
[Test]
public void TextBoxRetainsFocus()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click selector", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListSelector>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click drawable channel", () => clickDrawable(chatOverlay.ChildrenOfType<DrawableChannel>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Hide overlay", () => chatOverlay.Hide());
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
}
private Visibility listingVisibility =>
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value;
private Visibility loadingVisibility =>
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value;
private DrawableChannel currentDrawableChannel =>
chatOverlay.ChildrenOfType<Container<DrawableChannel>>().Single().Child;
private ChannelListItem getChannelListItem(Channel channel) =>
chatOverlay.ChildrenOfType<ChannelListItem>().Single(item => item.Channel == channel);
private ChatTextBox chatOverlayTextBox =>
chatOverlay.ChildrenOfType<ChatTextBox>().Single();
private ChatOverlayTopBar chatOverlayTopBar =>
chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single();
private void clickDrawable(Drawable d)
{
InputManager.MoveMouseTo(d);
InputManager.Click(MouseButton.Left);
}
private List<Message> createChannelMessages(Channel channel)
{
var message = new Message
{
ChannelId = channel.Id,
Content = $"Hello, this is a message in {channel.Name}",
Sender = testUser,
Timestamp = new DateTimeOffset(DateTime.Now),
};
return new List<Message> { message };
}
private Channel createPublicChannel(int id) => new Channel
{
Id = id,
Name = $"#channel-{id}",
Topic = $"We talk about the number {id} here",
Type = ChannelType.Public,
};
}
}

View File

@ -25,14 +25,14 @@ namespace osu.Game.Overlays.Chat.ChannelList
public event Action<Channel>? OnRequestSelect;
public event Action<Channel>? OnRequestLeave;
public readonly Channel Channel;
public readonly BindableInt Mentions = new BindableInt();
public readonly BindableBool Unread = new BindableBool();
public readonly BindableBool SelectorActive = new BindableBool();
private readonly Channel channel;
private Box hoverBox = null!;
private Box selectBox = null!;
private OsuSpriteText text = null!;
@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
public ChannelListItem(Channel channel)
{
this.channel = channel;
Channel = channel;
}
[BackgroundDependencyLoader]
@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = channel.Name,
Text = Channel.Name,
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 3 },
Action = () => OnRequestLeave?.Invoke(channel),
Action = () => OnRequestLeave?.Invoke(Channel),
}
}
},
@ -119,20 +119,16 @@ namespace osu.Game.Overlays.Chat.ChannelList
},
};
Action = () => OnRequestSelect?.Invoke(channel);
Action = () => OnRequestSelect?.Invoke(Channel);
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedChannel.BindValueChanged(_ => updateSelectState(), true);
SelectorActive.BindValueChanged(_ => updateSelectState(), true);
Unread.BindValueChanged(change =>
{
text.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint);
}, true);
selectedChannel.BindValueChanged(_ => updateState(), true);
SelectorActive.BindValueChanged(_ => updateState(), true);
Unread.BindValueChanged(_ => updateState(), true);
}
protected override bool OnHover(HoverEvent e)
@ -151,10 +147,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
private Drawable createIcon()
{
if (channel.Type != ChannelType.PM)
if (Channel.Type != ChannelType.PM)
return Drawable.Empty();
return new UpdateableAvatar(channel.Users.First(), isInteractive: false)
return new UpdateableAvatar(Channel.Users.First(), isInteractive: false)
{
Size = new Vector2(20),
Margin = new MarginPadding { Right = 5 },
@ -165,12 +161,19 @@ namespace osu.Game.Overlays.Chat.ChannelList
};
}
private void updateSelectState()
private void updateState()
{
if (selectedChannel.Value == channel && !SelectorActive.Value)
bool selected = selectedChannel.Value == Channel && !SelectorActive.Value;
if (selected)
selectBox.FadeIn(300, Easing.OutQuint);
else
selectBox.FadeOut(200, Easing.OutQuint);
if (Unread.Value || selected)
text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
else
text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint);
}
}
}

View File

@ -21,6 +21,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
private Box hoverBox = null!;
private Box selectBox = null!;
private OsuSpriteText text = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
@ -46,11 +50,11 @@ namespace osu.Game.Overlays.Chat.ChannelList
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 18, Right = 10 },
Child = new OsuSpriteText
Child = text = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = "Add More Channels",
Text = "Add more channels",
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
@ -68,9 +72,15 @@ namespace osu.Game.Overlays.Chat.ChannelList
SelectorActive.BindValueChanged(selector =>
{
if (selector.NewValue)
{
text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
selectBox.FadeIn(300, Easing.OutQuint);
}
else
{
text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint);
selectBox.FadeOut(200, Easing.OutQuint);
}
}, true);
Action = () => SelectorActive.Value = true;

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
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.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Chat
{
public class ChatOverlayTopBar : Container
{
private Box background = null!;
private Color4 backgroundColour;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, TextureStore textures)
{
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour = colourProvider.Background3,
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 50),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = textures.Get("Icons/Hexacons/messaging"),
Size = new Vector2(18),
},
// Placeholder text
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = "osu!chat",
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Bottom = 2f },
},
},
},
},
};
}
protected override bool OnHover(HoverEvent e)
{
background.FadeColour(backgroundColour.Lighten(0.1f), 300, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
background.FadeColour(backgroundColour, 300, Easing.OutQuint);
base.OnHoverLost(e);
}
}
}

View File

@ -11,7 +11,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osuTK;
@ -25,14 +26,19 @@ namespace osu.Game.Overlays.Chat
public event Action<string>? OnSearchTermsChanged;
public void TextBoxTakeFocus() => chatTextBox.TakeFocus();
public void TextBoxKillFocus() => chatTextBox.KillFocus();
[Resolved]
private Bindable<Channel> currentChannel { get; set; } = null!;
private OsuTextFlowContainer chattingTextContainer = null!;
private Container chattingTextContainer = null!;
private OsuSpriteText chattingText = null!;
private Container searchIconContainer = null!;
private ChatTextBox chatTextBox = null!;
private const float chatting_text_width = 180;
private const float chatting_text_width = 240;
private const float search_icon_width = 40;
[BackgroundDependencyLoader]
@ -61,16 +67,20 @@ namespace osu.Game.Overlays.Chat
{
new Drawable[]
{
chattingTextContainer = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 20))
chattingTextContainer = new Container
{
Masking = true,
Width = chatting_text_width,
Padding = new MarginPadding { Left = 10 },
RelativeSizeAxes = Axes.Y,
TextAnchor = Anchor.CentreRight,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = colourProvider.Background1,
Width = chatting_text_width,
Masking = true,
Padding = new MarginPadding { Right = 5 },
Child = chattingText = new OsuSpriteText
{
Font = OsuFont.Torus.With(size: 20),
Colour = colourProvider.Background1,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Truncate = true,
},
},
searchIconContainer = new Container
{
@ -131,15 +141,15 @@ namespace osu.Game.Overlays.Chat
switch (newChannel?.Type)
{
case ChannelType.Public:
chattingTextContainer.Text = $"chatting in {newChannel.Name}";
chattingText.Text = $"chatting in {newChannel.Name}";
break;
case ChannelType.PM:
chattingTextContainer.Text = $"chatting with {newChannel.Name}";
chattingText.Text = $"chatting with {newChannel.Name}";
break;
default:
chattingTextContainer.Text = string.Empty;
chattingText.Text = string.Empty;
break;
}
}, true);

View File

@ -25,11 +25,11 @@ namespace osu.Game.Overlays.Chat.Listing
public event Action<Channel>? OnRequestJoin;
public event Action<Channel>? OnRequestLeave;
public bool FilteringActive { get; set; }
public IEnumerable<string> FilterTerms => new[] { channel.Name, channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
public readonly Channel Channel;
private readonly Channel channel;
public bool FilteringActive { get; set; }
public IEnumerable<string> FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
private Box hoverBox = null!;
private SpriteIcon checkbox = null!;
@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Chat.Listing
public ChannelListingItem(Channel channel)
{
this.channel = channel;
Channel = channel;
}
[BackgroundDependencyLoader]
@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Chat.Listing
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = channel.Name,
Text = Channel.Name,
Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Bottom = 2 },
},
@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Chat.Listing
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = channel.Topic,
Text = Channel.Topic,
Font = OsuFont.Torus.With(size: text_size),
Margin = new MarginPadding { Bottom = 2 },
},
@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Chat.Listing
{
base.LoadComplete();
channelJoined = channel.Joined.GetBoundCopy();
channelJoined = Channel.Joined.GetBoundCopy();
channelJoined.BindValueChanged(change =>
{
const double duration = 500;
@ -155,7 +155,7 @@ namespace osu.Game.Overlays.Chat.Listing
}
}, true);
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel);
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel);
}
protected override bool OnHover(HoverEvent e)

View File

@ -0,0 +1,314 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat;
using osu.Game.Overlays.Chat.ChannelList;
using osu.Game.Overlays.Chat.Listing;
namespace osu.Game.Overlays
{
public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent
{
public string IconTexture => "Icons/Hexacons/messaging";
public LocalisableString Title => ChatStrings.HeaderTitle;
public LocalisableString Description => ChatStrings.HeaderDescription;
private ChatOverlayTopBar topBar = null!;
private ChannelList channelList = null!;
private LoadingLayer loading = null!;
private ChannelListing channelListing = null!;
private ChatTextBar textBar = null!;
private Container<DrawableChannel> currentChannelContainer = null!;
private readonly Bindable<float> chatHeight = new Bindable<float>();
private bool isDraggingTopBar;
private float dragStartChatHeight;
private const int transition_length = 500;
private const float default_chat_height = 0.4f;
private const float top_bar_height = 40;
private const float side_bar_width = 190;
private const float chat_bar_height = 60;
private readonly BindableBool selectorActive = new BindableBool();
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Resolved]
private ChannelManager channelManager { get; set; } = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[Cached]
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
public ChatOverlayV2()
{
Height = default_chat_height;
Masking = true;
const float corner_radius = 7f;
CornerRadius = corner_radius;
// Hack to hide the bottom edge corner radius off-screen.
Margin = new MarginPadding { Bottom = -corner_radius };
Padding = new MarginPadding { Bottom = corner_radius };
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
}
[BackgroundDependencyLoader]
private void load()
{
// Required for the pop in/out animation
RelativePositionAxes = Axes.Both;
Children = new Drawable[]
{
topBar = new ChatOverlayTopBar
{
RelativeSizeAxes = Axes.X,
Height = top_bar_height,
},
channelList = new ChannelList
{
RelativeSizeAxes = Axes.Y,
Width = side_bar_width,
Padding = new MarginPadding { Top = top_bar_height },
SelectorActive = { BindTarget = selectorActive },
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Padding = new MarginPadding
{
Top = top_bar_height,
Left = side_bar_width,
Bottom = chat_bar_height,
},
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
currentChannelContainer = new Container<DrawableChannel>
{
RelativeSizeAxes = Axes.Both,
},
loading = new LoadingLayer(true),
channelListing = new ChannelListing
{
RelativeSizeAxes = Axes.Both,
},
},
},
textBar = new ChatTextBar
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Padding = new MarginPadding { Left = side_bar_width },
ShowSearch = { BindTarget = selectorActive },
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
loading.Show();
config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
currentChannel.BindTo(channelManager.CurrentChannel);
channelManager.CurrentChannel.BindValueChanged(currentChannelChanged, true);
channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
channelManager.AvailableChannels.BindCollectionChanged(availableChannelsChanged, true);
channelList.OnRequestSelect += channel =>
{
// Manually selecting a channel should dismiss the selector
selectorActive.Value = false;
channelManager.CurrentChannel.Value = channel;
};
channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel);
channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms;
textBar.OnChatMessageCommitted += handleChatMessage;
selectorActive.BindValueChanged(v => channelListing.State.Value = v.NewValue ? Visibility.Visible : Visibility.Hidden, true);
}
/// <summary>
/// Highlights a certain message in the specified channel.
/// </summary>
/// <param name="message">The message to highlight.</param>
/// <param name="channel">The channel containing the message.</param>
public void HighlightMessage(Message message, Channel channel)
{
Debug.Assert(channel.Id == message.ChannelId);
if (currentChannel.Value?.Id != channel.Id)
{
if (!channel.Joined.Value)
channel = channelManager.JoinChannel(channel);
channelManager.CurrentChannel.Value = channel;
}
selectorActive.Value = false;
channel.HighlightedMessage.Value = message;
Show();
}
protected override bool OnDragStart(DragStartEvent e)
{
isDraggingTopBar = topBar.IsHovered;
if (!isDraggingTopBar)
return base.OnDragStart(e);
dragStartChatHeight = chatHeight.Value;
return true;
}
protected override void OnDrag(DragEvent e)
{
if (!isDraggingTopBar)
return;
float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
chatHeight.Value = targetChatHeight;
}
protected override void OnDragEnd(DragEndEvent e)
{
isDraggingTopBar = false;
base.OnDragEnd(e);
}
protected override void PopIn()
{
base.PopIn();
this.MoveToY(0, transition_length, Easing.OutQuint);
this.FadeIn(transition_length, Easing.OutQuint);
}
protected override void PopOut()
{
base.PopOut();
this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine);
textBar.TextBoxKillFocus();
}
protected override void OnFocus(FocusEvent e)
{
textBar.TextBoxTakeFocus();
base.OnFocus(e);
}
private void currentChannelChanged(ValueChangedEvent<Channel> channel)
{
Channel? newChannel = channel.NewValue;
loading.Show();
// Channel is null when leaving the currently selected channel
if (newChannel == null)
{
// Find another channel to switch to
newChannel = channelManager.JoinedChannels.FirstOrDefault(c => c != channel.OldValue);
if (newChannel == null)
selectorActive.Value = true;
else
currentChannel.Value = newChannel;
return;
}
LoadComponentAsync(new DrawableChannel(newChannel), loaded =>
{
currentChannelContainer.Clear();
currentChannelContainer.Add(loaded);
loading.Hide();
});
}
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
IEnumerable<Channel> joinedChannels = filterChannels(args.NewItems);
foreach (var channel in joinedChannels)
channelList.AddChannel(channel);
break;
case NotifyCollectionChangedAction.Remove:
IEnumerable<Channel> leftChannels = filterChannels(args.OldItems);
foreach (var channel in leftChannels)
channelList.RemoveChannel(channel);
break;
}
}
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
=> channelListing.UpdateAvailableChannels(channelManager.AvailableChannels);
private IEnumerable<Channel> filterChannels(IList channels)
=> channels.Cast<Channel>().Where(c => c.Type == ChannelType.Public || c.Type == ChannelType.PM);
private void handleChatMessage(string message)
{
if (string.IsNullOrWhiteSpace(message))
return;
if (message[0] == '/')
channelManager.PostCommand(message.Substring(1));
else
channelManager.PostMessage(message);
}
}
}