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 ;
2021-05-26 14:59:29 +08:00
using System.Collections.Specialized ;
2019-12-17 13:59:27 +08:00
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 ;
2020-02-07 23:50:22 +08:00
private Bindable < bool > notifyOnPM ;
2021-05-28 03:58:54 +08:00
private readonly IBindable < User > localUser = new Bindable < User > ( ) ;
2021-06-05 17:03:49 +08:00
private readonly IBindableList < Channel > joinedChannels = new BindableList < Channel > ( ) ;
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 ) ;
2020-02-07 23:50:22 +08:00
notifyOnPM = config . GetBindable < bool > ( OsuSetting . ChatMessageNotification ) ;
2021-06-05 17:03:49 +08:00
localUser . BindTo ( api . LocalUser ) ;
2020-01-25 23:43:51 +08:00
2019-12-26 10:32:40 +08:00
// Listen for new messages
2021-05-26 14:59:29 +08:00
joinedChannels . CollectionChanged + = channelsChanged ;
2021-06-05 17:03:49 +08:00
joinedChannels . BindTo ( channelManager . JoinedChannels ) ;
2021-05-26 14:59:29 +08:00
}
2019-12-26 10:32:40 +08:00
2021-05-26 14:59:29 +08:00
private void channelsChanged ( object sender , NotifyCollectionChangedEventArgs e )
{
switch ( e . Action )
2019-12-26 10:32:40 +08:00
{
2021-05-26 14:59:29 +08:00
case NotifyCollectionChangedAction . Add :
foreach ( var channel in e . NewItems . Cast < Channel > ( ) )
channel . NewMessagesArrived + = newMessagesArrived ;
break ;
case NotifyCollectionChangedAction . Remove :
foreach ( var channel in e . OldItems . Cast < Channel > ( ) )
channel . NewMessagesArrived - = newMessagesArrived ;
break ;
}
2019-12-26 10:32:40 +08:00
}
2020-02-06 02:20:16 +08:00
private void newMessagesArrived ( IEnumerable < Message > messages )
2019-12-26 10:32:40 +08:00
{
if ( messages = = null | | ! messages . Any ( ) )
return ;
HandleMessages ( messages . First ( ) . ChannelId , messages ) ;
}
2021-06-05 20:02:48 +08:00
/// <summary>
/// Searches for a channel with the matching <paramref name="channelId"/>, returns <see langword="null"/> when none found.
/// </summary>
private Channel fetchJoinedChannel ( long channelId )
{
return channelManager . JoinedChannels . SingleOrDefault ( c = > c . Id = = channelId ) ;
}
2019-12-26 10:32:40 +08:00
public void HandleMessages ( long channelId , IEnumerable < Message > messages )
{
2021-06-05 20:02:48 +08:00
// Fetch channel object
var channel = fetchJoinedChannel ( channelId ) ;
2019-12-26 10:32:40 +08:00
if ( channel = = null )
{
Logger . Log ( $"Couldn't resolve channel id {channelId}" , LoggingTarget . Information ) ;
return ;
}
2020-01-22 07:13:07 +08:00
// Only send notifications, if ChatOverlay and the target channel aren't visible.
2020-01-22 17:47:51 +08:00
if ( chatOverlay ? . IsPresent = = true & & channelManager . CurrentChannel . Value = = channel )
2019-12-17 13:59:27 +08:00
return ;
2020-01-22 07:27:46 +08:00
foreach ( var message in messages . OrderByDescending ( m = > m . Id ) )
2019-12-17 13:59:27 +08:00
{
2020-01-17 07:00:10 +08:00
// ignore messages that already have been read
2020-01-22 07:28:08 +08:00
if ( message . Id < = channel . LastReadId )
2020-01-17 07:00:10 +08:00
return ;
2020-01-22 06:42:15 +08:00
if ( message . Sender . Id = = localUser . Value . Id )
2019-12-17 13:59:27 +08:00
continue ;
2020-02-07 23:52:53 +08:00
// check for private messages first,
// to avoid both posting two notifications about the same message
2020-01-20 00:55:17 +08:00
if ( checkForPMs ( channel , message ) )
continue ;
2019-12-17 13:59:27 +08:00
2021-06-05 21:57:14 +08:00
_ = checkForMentions ( channel , message , localUser . Value . Username ) ;
2020-01-20 00:55:17 +08:00
}
}
2020-01-18 21:17:26 +08:00
2021-06-05 21:57:14 +08:00
/// <summary>
/// Checks whether the user enabled private message notifications and whether specified <paramref name="message"/> is a direct message.
/// </summary>
/// <param name="channel">The channel associated to the <paramref name="message"/></param>
/// <param name="message">The message to be checked</param>
2020-01-20 00:55:17 +08:00
private bool checkForPMs ( Channel channel , Message message )
{
2020-02-07 23:50:22 +08:00
if ( ! notifyOnPM . Value | | channel . Type ! = ChannelType . PM )
2020-01-20 00:55:17 +08:00
return false ;
2020-01-17 06:15:30 +08:00
2021-06-05 21:57:14 +08:00
if ( channel . Id ! = message . ChannelId )
throw new ArgumentException ( "The provided channel doesn't match with the channel id provided by the message parameter." , nameof ( channel ) ) ;
2020-01-22 07:28:59 +08:00
2021-06-05 21:57:14 +08:00
var notification = new PrivateMessageNotification ( message . Sender . Username , channel ) ;
2020-01-22 17:48:55 +08:00
notificationOverlay ? . Post ( notification ) ;
2020-01-20 00:55:17 +08:00
return true ;
}
2021-06-05 21:57:14 +08:00
/// <summary>
/// Checks whether the user enabled mention notifications and whether specified <paramref name="message"/> mentions the provided <paramref name="username"/>.
/// </summary>
/// <param name="channel">The channel associated to the <paramref name="message"/></param>
/// <param name="message">The message to be checked</param>
/// <param name="username">The username that will be checked for</param>
private bool checkForMentions ( Channel channel , Message message , string username )
2020-01-20 00:55:17 +08:00
{
2020-01-29 09:07:08 +08:00
if ( ! notifyOnMention . Value | | ! isMentioning ( message . Content , username ) )
2021-06-05 21:57:14 +08:00
return false ;
if ( channel . Id ! = message . ChannelId )
throw new ArgumentException ( "The provided channel doesn't match with the channel id provided by the message parameter." , nameof ( channel ) ) ;
2020-01-20 00:55:17 +08:00
var notification = new MentionNotification ( message . Sender . Username , channel ) ;
notificationOverlay ? . Post ( notification ) ;
2021-06-05 21:57:14 +08:00
return true ;
2019-12-17 13:59:27 +08:00
}
/// <summary>
2020-01-25 21:40:53 +08:00
/// Checks if <paramref name="message"/> contains <paramref name="username"/>, if not, retries making spaces into underscores.
2019-12-17 13:59:27 +08:00
/// </summary>
2020-01-25 21:40:53 +08:00
/// <returns>If the <paramref name="message"/> mentions the <paramref name="username"/></returns>
2021-05-27 17:48:30 +08:00
private static bool isMentioning ( string message , string username ) = > message . Contains ( username , StringComparison . OrdinalIgnoreCase ) | | message . Contains ( username . Replace ( ' ' , '_' ) , StringComparison . OrdinalIgnoreCase ) ;
2019-12-17 13:59:27 +08:00
2021-05-27 07:00:26 +08:00
public class OpenChannelNotification : SimpleNotification
2019-12-17 13:59:27 +08:00
{
2021-05-27 07:00:26 +08:00
public OpenChannelNotification ( Channel channel )
2019-12-17 13:59:27 +08:00
{
2020-02-04 06:03:27 +08:00
this . channel = channel ;
2019-12-17 13:59:27 +08:00
}
2020-02-04 06:03:27 +08:00
private readonly Channel channel ;
2020-01-22 07:28:59 +08:00
2019-12-17 13:59:27 +08:00
public override bool IsImportant = > false ;
[BackgroundDependencyLoader]
2020-01-22 07:28:59 +08:00
private void load ( OsuColour colours , ChatOverlay chatOverlay , NotificationOverlay notificationOverlay , ChannelManager channelManager )
2019-12-17 13:59:27 +08:00
{
IconBackgound . Colour = colours . PurpleDark ;
2020-01-22 07:28:59 +08:00
2019-12-17 13:59:27 +08:00
Activated = delegate
{
2020-01-20 00:55:17 +08:00
notificationOverlay . Hide ( ) ;
chatOverlay . Show ( ) ;
2020-02-04 06:03:27 +08:00
channelManager . CurrentChannel . Value = channel ;
2020-01-17 07:00:10 +08:00
2019-12-17 13:59:27 +08:00
return true ;
} ;
}
}
2021-05-27 07:00:26 +08:00
public class PrivateMessageNotification : OpenChannelNotification
2019-12-17 13:59:27 +08:00
{
2021-05-28 03:58:54 +08:00
public PrivateMessageNotification ( string username , Channel channel )
: base ( channel )
2019-12-17 13:59:27 +08:00
{
2021-05-27 07:00:26 +08:00
Icon = FontAwesome . Solid . Envelope ;
Text = $"You received a private message from '{username}'. Click to read it!" ;
2019-12-17 13:59:27 +08:00
}
2021-05-27 07:00:26 +08:00
}
2019-12-17 13:59:27 +08:00
2021-05-27 07:00:26 +08:00
public class MentionNotification : OpenChannelNotification
{
2021-05-28 03:58:54 +08:00
public MentionNotification ( string username , Channel channel )
: base ( channel )
2019-12-17 13:59:27 +08:00
{
2021-05-27 07:00:26 +08:00
Icon = FontAwesome . Solid . At ;
Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!" ;
2019-12-17 13:59:27 +08:00
}
}
}
}