Skip to content
169 changes: 169 additions & 0 deletions osu.Game.Tests/Visual/Online/TestSceneChatTicker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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 disable

using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
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;

namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public partial class TestSceneChatTicker : OsuManualInputManagerTestScene
{
private APIUser friend;
private APIUser importantPerson;
private Channel publicChannel;
private Channel privateMessageChannel;
private TestContainer testContainer;

private int messageIdCounter;

[Resolved]
private OsuConfigManager config { get; set; } = null!;

[SetUp]
public void Setup() => Schedule(() =>
{
if (API is DummyAPIAccess daa)
{
daa.HandleRequest = dummyAPIHandleRequest;
}

friend = new APIUser { Id = 0, Username = "SomeFriend" };
importantPerson = new APIUser { Username = @"i-am-important", Id = 42, Colour = "#250cc9" };
publicChannel = new Channel { Id = 1, Name = "#osu" };
privateMessageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM };

Schedule(() =>
{
Child = testContainer = new TestContainer(API, new[] { publicChannel, privateMessageChannel })
{
RelativeSizeAxes = Axes.Both,
};
});
});

private bool dummyAPIHandleRequest(APIRequest request)
{
switch (request)
{
case GetMessagesRequest messagesRequest:
messagesRequest.TriggerSuccess(new List<Message>(0));
return true;

case CreateChannelRequest createChannelRequest:
var apiChatChannel = new APIChatChannel
{
RecentMessages = new List<Message>(0),
ChannelID = (int)createChannelRequest.Channel.Id
};
createChannelRequest.TriggerSuccess(apiChatChannel);
return true;

case ListChannelsRequest listChannelsRequest:
listChannelsRequest.TriggerSuccess(new List<Channel>(1) { publicChannel });
return true;

case GetUpdatesRequest updatesRequest:
updatesRequest.TriggerSuccess(new GetUpdatesResponse
{
Messages = new List<Message>(0),
Presence = new List<Channel>(0)
});
return true;

case JoinChannelRequest joinChannelRequest:
joinChannelRequest.TriggerSuccess();
return true;

default:
return false;
}
}

[Test]
public void TestChatTicker()
{
AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);

AddToggleStep("toggle show ticker", b => config.SetValue(OsuSetting.ChatTicker, b));

AddStep("receive public message", () => receiveMessage(friend, publicChannel, "Hello everyone"));

AddStep("receive message containing mention", () => receiveMessage(friend, publicChannel, $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!"));

AddStep("receive message from VIP", () => receiveMessage(importantPerson, publicChannel, "Hello everyone!"));

AddStep("receive message from VIP containing mention", () => receiveMessage(importantPerson, publicChannel, $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!"));

AddStep("receive very long message", () => receiveMessage(importantPerson, publicChannel, string.Concat(Enumerable.Repeat("Hello everyone! ", 50))));
}

private void receiveMessage(APIUser sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content));

private Message createMessage(APIUser sender, Channel channel, string content) => new Message(messageIdCounter++)
{
Content = content,
Sender = sender,
ChannelId = channel.Id
};

private partial class TestContainer : Container
{
[Cached]
public ChannelManager ChannelManager { get; }

[Cached(typeof(INotificationOverlay))]
public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
};

[Cached]
public ChatOverlay ChatOverlay { get; } = new ChatOverlay();

[Cached]
public ChatTicker ChatTicker { get; } = new ChatTicker();

private readonly MessageNotifier messageNotifier = new MessageNotifier();

private readonly Channel[] channels;

public TestContainer(IAPIProvider api, Channel[] channels)
{
this.channels = channels;
ChannelManager = new ChannelManager(api);
}

[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
ChannelManager,
ChatOverlay,
ChatTicker,
messageNotifier,
};

((BindableList<Channel>)ChannelManager.AvailableChannels).AddRange(channels);

foreach (var channel in channels)
ChannelManager.JoinChannel(channel);
}
}
}
}
43 changes: 43 additions & 0 deletions osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
Expand All @@ -32,6 +33,9 @@ public partial class TestSceneMessageNotifier : OsuManualInputManagerTestScene

private int messageIdCounter;

[Resolved]
private OsuConfigManager config { get; set; } = null!;

[SetUp]
public void Setup() => Schedule(() =>
{
Expand Down Expand Up @@ -222,6 +226,41 @@ public void TestSendInUnresolvedChannel()
AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
}

[Test]
public void TestChatTicker()
{
AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);

AddStep("receive message on channel", () => receiveMessage(friend, publicChannel, "Hello everyone!"));
AddAssert("ticker is hidden", () => testContainer.ChatTicker.State.Value == Visibility.Hidden);

AddStep("toggle show ticker on", () => config.SetValue(OsuSetting.ChatTicker, true));

AddStep("receive message on channel", () => receiveMessage(friend, publicChannel, "Hello everyone!"));
AddAssert("ticker is hidden", () => testContainer.ChatTicker.State.Value == Visibility.Hidden);

AddStep("close overlay", () => testContainer.ChatOverlay.Hide());

AddStep("receive PM", () => receiveMessage(friend, privateMessageChannel, "hey hey"));
AddAssert("ticker is hidden", () => testContainer.ChatTicker.State.Value == Visibility.Hidden);

AddStep("receive message on channel", () => receiveMessage(friend, publicChannel, "Hello everyone!"));
AddAssert("ticker is present", () => testContainer.ChatTicker.State.Value == Visibility.Visible);

AddStep("receive last message only", () =>
{
List<Message> messages = new List<Message> { createMessage(friend, publicChannel, "This should be my first message") };
messages.AddRange(Enumerable.Range(0, 10).Select(i => createMessage(friend, publicChannel, $"Hey hey {i}")));
messages.Add(createMessage(friend, publicChannel, "This should be my last message."));

publicChannel.AddNewMessages(messages.ToArray());
});
AddAssert("ticker is present", () => testContainer.ChatTicker.State.Value == Visibility.Visible);

AddStep("toggle show ticker off", () => config.SetValue(OsuSetting.ChatTicker, false));
AddAssert("ticker is hidden", () => testContainer.ChatTicker.State.Value == Visibility.Hidden);
}

private void receiveMessage(APIUser sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content));

private Message createMessage(APIUser sender, Channel channel, string content) => new Message(messageIdCounter++)
Expand Down Expand Up @@ -254,6 +293,9 @@ private partial class TestContainer : Container
[Cached]
public ChatOverlay ChatOverlay { get; } = new ChatOverlay();

[Cached]
public ChatTicker ChatTicker { get; } = new ChatTicker();

private readonly MessageNotifier messageNotifier = new MessageNotifier();

private readonly Channel[] channels;
Expand All @@ -271,6 +313,7 @@ private void load()
{
ChannelManager,
ChatOverlay,
ChatTicker,
NotificationOverlay,
messageNotifier,
};
Expand Down
2 changes: 2 additions & 0 deletions osu.Game/Configuration/OsuConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ protected override void InitialiseDefaults()

SetDefault(OsuSetting.ShowOnlineExplicitContent, false);

SetDefault(OsuSetting.ChatTicker, false);
SetDefault(OsuSetting.NotifyOnUsernameMentioned, true);
SetDefault(OsuSetting.NotifyOnPrivateMessage, true);
SetDefault(OsuSetting.NotifyOnFriendPresenceChange, true);
Expand Down Expand Up @@ -449,6 +450,7 @@ public enum OsuSetting
ScalingBackgroundDim,
UIScale,
IntroSequence,
ChatTicker,
NotifyOnUsernameMentioned,
NotifyOnPrivateMessage,
NotifyOnFriendPresenceChange,
Expand Down
5 changes: 4 additions & 1 deletion osu.Game/Graphics/Containers/OsuClickableContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
Expand All @@ -16,6 +17,8 @@ public partial class OsuClickableContainer : ClickableContainer, IHasTooltip
{
private readonly HoverSampleSet sampleSet;

public readonly Bindable<bool> MuteSounds = new Bindable<bool>();

private readonly Container content = new Container { RelativeSizeAxes = Axes.Both };

private HoverSounds samples = null!;
Expand All @@ -28,7 +31,7 @@ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>

protected override Container<Drawable> Content => content;

protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled }, MuteSounds = { BindTarget = MuteSounds } };

public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Default)
{
Expand Down
7 changes: 6 additions & 1 deletion osu.Game/Graphics/UserInterface/HoverClickSounds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ protected override bool OnClick(ClickEvent e)
return base.OnClick(e);
}

public void PlayClickSample() =>
public void PlayClickSample()
{
if (MuteSounds.Value)
return;

SamplePlaybackHelper.PlayWithRandomPitch(Enabled.Value ? sampleClick : sampleClickDisabled, pitchVariation: 0.01);
}
}
}
4 changes: 3 additions & 1 deletion osu.Game/Graphics/UserInterface/HoverSounds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public partial class HoverSounds : HoverSampleDebounceComponent
{
public readonly Bindable<bool> Enabled = new Bindable<bool>(true);

public readonly Bindable<bool> MuteSounds = new Bindable<bool>();

private Sample sampleHover;

protected readonly HoverSampleSet SampleSet;
Expand All @@ -40,7 +42,7 @@ private void load(AudioManager audio)

public override void PlayHoverSample()
{
if (!Enabled.Value)
if (!Enabled.Value || MuteSounds.Value)
return;

SamplePlaybackHelper.PlayWithRandomPitch(sampleHover, pitchVariation: 0.02);
Expand Down
5 changes: 5 additions & 0 deletions osu.Game/Localisation/OnlineSettingsStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public static class OnlineSettingsStrings
/// </summary>
public static LocalisableString AlertsAndPrivacyHeader => new TranslatableString(getKey(@"alerts_and_privacy_header"), @"Alerts and Privacy");

/// <summary>
/// "Chat ticker"
/// </summary>
public static LocalisableString ChatTicker => new TranslatableString(getKey(@"chat_ticker"), @"Chat ticker");

/// <summary>
/// "Show a notification when someone mentions your name"
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion osu.Game/Online/Chat/MessageNotifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public partial class MessageNotifier : Component
[Resolved]
private ChatOverlay chatOverlay { get; set; }

[Resolved]
private ChatTicker chatTicker { get; set; }

[Resolved]
private ChannelManager channelManager { get; set; }

Expand Down Expand Up @@ -99,11 +102,16 @@ private void checkNewMessages(IEnumerable<Message> messages)
if (channel == null)
return;

var sortedMessages = messages.OrderByDescending(m => m.Id);

if (channelManager.CurrentChannel.Value == channel)
chatTicker.PostMessage(sortedMessages.First());

// Only send notifications if ChatOverlay or the target channel aren't visible, or if the window is unfocused
if (chatOverlay.IsPresent && channelManager.CurrentChannel.Value == channel && host.IsActive.Value)
return;

foreach (var message in messages.OrderByDescending(m => m.Id))
foreach (var message in sortedMessages)
{
// ignore messages that already have been read
if (message.Id <= channel.LastReadId)
Expand Down
Loading
Loading