2019-12-17 14:04:55 +08:00
|
|
|
|
// 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;
|
2019-12-17 13:59:27 +08:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using osu.Framework.Allocation;
|
|
|
|
|
using osu.Framework.Bindables;
|
|
|
|
|
using osu.Framework.Graphics;
|
|
|
|
|
using osu.Framework.Graphics.Sprites;
|
2019-12-26 10:32:40 +08:00
|
|
|
|
using osu.Framework.Logging;
|
2019-12-17 13:59:27 +08:00
|
|
|
|
using osu.Game.Configuration;
|
|
|
|
|
using osu.Game.Graphics;
|
|
|
|
|
using osu.Game.Online.API;
|
|
|
|
|
using osu.Game.Overlays;
|
|
|
|
|
using osu.Game.Overlays.Notifications;
|
|
|
|
|
using osu.Game.Users;
|
|
|
|
|
|
|
|
|
|
namespace osu.Game.Online.Chat
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Component that handles creating and posting notifications for incoming messages.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class MessageNotifier : Component
|
|
|
|
|
{
|
|
|
|
|
[Resolved(CanBeNull = true)]
|
|
|
|
|
private NotificationOverlay notificationOverlay { get; set; }
|
|
|
|
|
|
|
|
|
|
[Resolved(CanBeNull = true)]
|
|
|
|
|
private ChatOverlay chatOverlay { get; set; }
|
|
|
|
|
|
|
|
|
|
[Resolved(CanBeNull = true)]
|
|
|
|
|
private ChannelManager channelManager { get; set; }
|
|
|
|
|
|
|
|
|
|
private Bindable<bool> notifyOnMention;
|
|
|
|
|
private Bindable<bool> notifyOnChat;
|
|
|
|
|
private Bindable<string> highlightWords;
|
|
|
|
|
private Bindable<User> localUser;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Determines if the user is able to see incoming messages.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool IsActive => chatOverlay?.IsPresent == true;
|
|
|
|
|
|
2020-01-17 07:00:10 +08:00
|
|
|
|
private List<PrivateMessageNotification> privateMessageNotifications = new List<PrivateMessageNotification>();
|
|
|
|
|
|
2019-12-17 13:59:27 +08:00
|
|
|
|
[BackgroundDependencyLoader]
|
2019-12-26 10:32:40 +08:00
|
|
|
|
private void load(OsuConfigManager config, IAPIProvider api)
|
2019-12-17 13:59:27 +08:00
|
|
|
|
{
|
|
|
|
|
notifyOnMention = config.GetBindable<bool>(OsuSetting.ChatHighlightName);
|
|
|
|
|
notifyOnChat = config.GetBindable<bool>(OsuSetting.ChatMessageNotification);
|
|
|
|
|
highlightWords = config.GetBindable<string>(OsuSetting.HighlightWords);
|
|
|
|
|
localUser = api.LocalUser;
|
2019-12-26 10:32:40 +08:00
|
|
|
|
|
|
|
|
|
// Listen for new messages
|
|
|
|
|
channelManager.JoinedChannels.ItemsAdded += (joinedChannels) =>
|
|
|
|
|
{
|
|
|
|
|
foreach (var channel in joinedChannels)
|
|
|
|
|
channel.NewMessagesArrived += channel_NewMessagesArrived;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
channelManager.JoinedChannels.ItemsRemoved += (leftChannels) =>
|
|
|
|
|
{
|
|
|
|
|
foreach (var channel in leftChannels)
|
|
|
|
|
channel.NewMessagesArrived -= channel_NewMessagesArrived;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-17 06:15:30 +08:00
|
|
|
|
private void channel_NewMessagesArrived(IEnumerable<Message> messages)
|
2019-12-26 10:32:40 +08:00
|
|
|
|
{
|
|
|
|
|
if (messages == null || !messages.Any())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
HandleMessages(messages.First().ChannelId, messages);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// Resolves the channel id
|
|
|
|
|
/// </remarks>
|
|
|
|
|
public void HandleMessages(long channelId, IEnumerable<Message> messages)
|
|
|
|
|
{
|
|
|
|
|
var channel = channelManager.JoinedChannels.FirstOrDefault(c => c.Id == channelId);
|
|
|
|
|
|
|
|
|
|
if (channel == null)
|
|
|
|
|
{
|
|
|
|
|
Logger.Log($"Couldn't resolve channel id {channelId}", LoggingTarget.Information);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HandleMessages(channel, messages);
|
2019-12-17 13:59:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void HandleMessages(Channel channel, IEnumerable<Message> messages)
|
|
|
|
|
{
|
2020-01-17 06:15:30 +08:00
|
|
|
|
// don't show if the ChatOverlay and the channel is visible.
|
2019-12-17 13:59:27 +08:00
|
|
|
|
if (IsActive && channelManager.CurrentChannel.Value == channel)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
foreach (var message in messages)
|
|
|
|
|
{
|
2020-01-17 07:00:10 +08:00
|
|
|
|
// ignore messages that already have been read
|
|
|
|
|
if (message.Id < channel.LastReadId)
|
|
|
|
|
return;
|
|
|
|
|
|
2019-12-17 13:59:27 +08:00
|
|
|
|
var localUsername = localUser.Value.Username;
|
|
|
|
|
|
|
|
|
|
if (message.Sender.Username == localUsername)
|
|
|
|
|
continue;
|
|
|
|
|
|
2020-01-17 07:00:10 +08:00
|
|
|
|
var words = getWords(message.Content);
|
|
|
|
|
|
2019-12-17 13:59:27 +08:00
|
|
|
|
void onClick()
|
|
|
|
|
{
|
2020-01-17 06:15:30 +08:00
|
|
|
|
notificationOverlay.Hide();
|
2019-12-26 10:32:40 +08:00
|
|
|
|
chatOverlay.Show();
|
2020-01-17 06:15:30 +08:00
|
|
|
|
channelManager.CurrentChannel.Value = channel;
|
2019-12-17 13:59:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (notifyOnChat.Value && channel.Type == ChannelType.PM)
|
|
|
|
|
{
|
2020-01-17 06:15:30 +08:00
|
|
|
|
// Scheduling because of possible "race-condition" (NotificationOverlay didn't add the notification yet).
|
|
|
|
|
Schedule(() =>
|
2019-12-17 13:59:27 +08:00
|
|
|
|
{
|
2020-01-17 07:00:10 +08:00
|
|
|
|
var existingNotification = privateMessageNotifications.OfType<PrivateMessageNotification>()
|
|
|
|
|
.FirstOrDefault(n => n.Username == message.Sender.Username);
|
2020-01-17 06:15:30 +08:00
|
|
|
|
|
|
|
|
|
if (existingNotification == null)
|
|
|
|
|
{
|
|
|
|
|
var notification = new PrivateMessageNotification(message.Sender.Username, onClick);
|
|
|
|
|
notificationOverlay?.Post(notification);
|
2020-01-17 07:00:10 +08:00
|
|
|
|
privateMessageNotifications.Add(notification);
|
2020-01-17 06:15:30 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
existingNotification.MessageCount++;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2019-12-17 13:59:27 +08:00
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-01-17 06:15:30 +08:00
|
|
|
|
|
2019-12-17 13:59:27 +08:00
|
|
|
|
if (notifyOnMention.Value && anyCaseInsensitive(words, localUsername))
|
|
|
|
|
{
|
|
|
|
|
var notification = new MentionNotification(message.Sender.Username, onClick);
|
|
|
|
|
notificationOverlay?.Post(notification);
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-01-17 06:15:30 +08:00
|
|
|
|
|
2019-12-17 13:59:27 +08:00
|
|
|
|
if (!string.IsNullOrWhiteSpace(highlightWords.Value))
|
|
|
|
|
{
|
|
|
|
|
var matchedWord = hasCaseInsensitive(words, getWords(highlightWords.Value));
|
|
|
|
|
|
|
|
|
|
if (matchedWord != null)
|
|
|
|
|
{
|
|
|
|
|
var notification = new HighlightNotification(message.Sender.Username, matchedWord, onClick);
|
|
|
|
|
notificationOverlay?.Post(notification);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-17 06:15:30 +08:00
|
|
|
|
|
|
|
|
|
//making sure if the notification drawer bugs out, we merge it afterwards again.
|
|
|
|
|
Schedule(() => mergeNotifications());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Checks current notifications if they aren't merged, and merges them together again.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void mergeNotifications()
|
|
|
|
|
{
|
|
|
|
|
if (notificationOverlay == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pmn = notificationOverlay.Notifications.OfType<PrivateMessageNotification>();
|
|
|
|
|
|
|
|
|
|
foreach (var notification in pmn)
|
|
|
|
|
{
|
|
|
|
|
var duplicates = pmn.Where(n => n.Username == notification.Username);
|
|
|
|
|
|
|
|
|
|
if (duplicates.Count() < 2)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var first = duplicates.First();
|
|
|
|
|
foreach (var notification2 in duplicates)
|
|
|
|
|
{
|
|
|
|
|
if (notification2 == first)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
first.MessageCount += notification2.MessageCount;
|
|
|
|
|
notification2.Close();
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-17 13:59:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string[] getWords(string input) => input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Finds the first matching string/word in both <paramref name="x"/> and <paramref name="y"/> (case-insensitive)
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static string hasCaseInsensitive(IEnumerable<string> x, IEnumerable<string> y) => x.FirstOrDefault(x2 => anyCaseInsensitive(y, x2));
|
|
|
|
|
|
2020-01-17 07:00:10 +08:00
|
|
|
|
private static bool anyCaseInsensitive(IEnumerable<string> x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.OrdinalIgnoreCase));
|
2019-12-17 13:59:27 +08:00
|
|
|
|
|
2020-01-17 07:00:10 +08:00
|
|
|
|
public class HighlightNotification : SimpleNotification
|
2019-12-17 13:59:27 +08:00
|
|
|
|
{
|
|
|
|
|
public HighlightNotification(string highlighter, string word, Action onClick)
|
|
|
|
|
{
|
|
|
|
|
Icon = FontAwesome.Solid.Highlighter;
|
|
|
|
|
Text = $"'{word}' was mentioned in chat by '{highlighter}'. Click to find out why!";
|
|
|
|
|
this.onClick = onClick;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private readonly Action onClick;
|
|
|
|
|
|
|
|
|
|
public override bool IsImportant => false;
|
|
|
|
|
|
|
|
|
|
[BackgroundDependencyLoader]
|
2020-01-17 06:15:30 +08:00
|
|
|
|
private void load(OsuColour colours)
|
2019-12-17 13:59:27 +08:00
|
|
|
|
{
|
|
|
|
|
IconBackgound.Colour = colours.PurpleDark;
|
|
|
|
|
Activated = delegate
|
|
|
|
|
{
|
|
|
|
|
onClick?.Invoke();
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-17 07:00:10 +08:00
|
|
|
|
public class PrivateMessageNotification : SimpleNotification
|
2019-12-17 13:59:27 +08:00
|
|
|
|
{
|
|
|
|
|
public PrivateMessageNotification(string username, Action onClick)
|
|
|
|
|
{
|
|
|
|
|
Icon = FontAwesome.Solid.Envelope;
|
|
|
|
|
Username = username;
|
|
|
|
|
MessageCount = 1;
|
|
|
|
|
this.onClick = onClick;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int messageCount = 0;
|
|
|
|
|
|
|
|
|
|
public int MessageCount
|
|
|
|
|
{
|
|
|
|
|
get => messageCount;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
messageCount = value;
|
2020-01-17 06:15:30 +08:00
|
|
|
|
|
2019-12-17 13:59:27 +08:00
|
|
|
|
if (messageCount > 1)
|
|
|
|
|
{
|
|
|
|
|
Text = $"You received {messageCount} private messages from '{Username}'. Click to read it!";
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Text = $"You received a private message from '{Username}'. Click to read it!";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Username { get; set; }
|
|
|
|
|
|
|
|
|
|
private readonly Action onClick;
|
|
|
|
|
|
|
|
|
|
public override bool IsImportant => false;
|
|
|
|
|
|
|
|
|
|
[BackgroundDependencyLoader]
|
2020-01-17 07:00:10 +08:00
|
|
|
|
private void load(OsuColour colours, MessageNotifier notifier)
|
2019-12-17 13:59:27 +08:00
|
|
|
|
{
|
|
|
|
|
IconBackgound.Colour = colours.PurpleDark;
|
|
|
|
|
Activated = delegate
|
|
|
|
|
{
|
|
|
|
|
onClick?.Invoke();
|
2020-01-17 07:00:10 +08:00
|
|
|
|
|
|
|
|
|
if (notifier.privateMessageNotifications.Contains(this))
|
|
|
|
|
notifier.privateMessageNotifications.Remove(this);
|
|
|
|
|
|
2019-12-17 13:59:27 +08:00
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-17 07:00:10 +08:00
|
|
|
|
public class MentionNotification : SimpleNotification
|
2019-12-17 13:59:27 +08:00
|
|
|
|
{
|
|
|
|
|
public MentionNotification(string username, Action onClick)
|
|
|
|
|
{
|
|
|
|
|
Icon = FontAwesome.Solid.At;
|
|
|
|
|
Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!";
|
|
|
|
|
this.onClick = onClick;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private readonly Action onClick;
|
|
|
|
|
|
|
|
|
|
public override bool IsImportant => false;
|
|
|
|
|
|
|
|
|
|
[BackgroundDependencyLoader]
|
2020-01-17 06:15:30 +08:00
|
|
|
|
private void load(OsuColour colours)
|
2019-12-17 13:59:27 +08:00
|
|
|
|
{
|
|
|
|
|
IconBackgound.Colour = colours.PurpleDark;
|
|
|
|
|
Activated = delegate
|
|
|
|
|
{
|
|
|
|
|
onClick?.Invoke();
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|