2019-10-29 05:32:38 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 08:43:03 +00:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 09:19:50 +00:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Linq ;
2019-06-20 14:01:39 +00:00
using osu.Framework.Allocation ;
2018-11-20 07:51:59 +00:00
using osuTK.Graphics ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Game.Graphics.Containers ;
using osu.Game.Graphics.Cursor ;
using osu.Game.Online.Chat ;
2019-10-21 21:44:58 +00:00
using osu.Framework.Graphics.Shapes ;
2019-10-21 22:30:37 +00:00
using osu.Game.Graphics ;
using osu.Framework.Extensions.Color4Extensions ;
using osu.Framework.Graphics.Sprites ;
2021-02-02 06:16:10 +00:00
using osu.Framework.Utils ;
2019-11-25 02:30:55 +00:00
using osu.Game.Graphics.Sprites ;
2018-04-13 09:19:50 +00:00
namespace osu.Game.Overlays.Chat
{
2018-07-09 16:30:41 +00:00
public class DrawableChannel : Container
2018-04-13 09:19:50 +00:00
{
2018-07-09 16:30:41 +00:00
public readonly Channel Channel ;
2019-10-22 00:11:19 +00:00
protected FillFlowContainer ChatLineFlow ;
2021-01-31 20:37:52 +00:00
private ChannelScrollContainer scroll ;
2018-04-13 09:19:50 +00:00
2020-03-23 03:03:33 +00:00
private bool scrollbarVisible = true ;
public bool ScrollbarVisible
{
set
{
if ( scrollbarVisible = = value ) return ;
scrollbarVisible = value ;
if ( scroll ! = null )
scroll . ScrollbarVisible = value ;
}
}
2019-10-21 22:30:37 +00:00
[Resolved]
private OsuColour colours { get ; set ; }
2018-07-09 16:30:41 +00:00
public DrawableChannel ( Channel channel )
2018-04-13 09:19:50 +00:00
{
2018-07-09 16:30:41 +00:00
Channel = channel ;
2018-04-13 09:19:50 +00:00
RelativeSizeAxes = Axes . Both ;
2019-06-20 14:01:39 +00:00
}
2018-04-13 09:19:50 +00:00
2019-06-20 14:01:39 +00:00
[BackgroundDependencyLoader]
private void load ( )
{
2019-08-14 01:53:47 +00:00
Child = new OsuContextMenuContainer
2018-04-13 09:19:50 +00:00
{
2019-08-14 01:53:47 +00:00
RelativeSizeAxes = Axes . Both ,
Masking = true ,
2021-01-31 20:37:52 +00:00
Child = scroll = new ChannelScrollContainer
2018-04-13 09:19:50 +00:00
{
2020-03-23 03:03:33 +00:00
ScrollbarVisible = scrollbarVisible ,
2018-04-13 09:19:50 +00:00
RelativeSizeAxes = Axes . Both ,
2019-08-14 01:53:47 +00:00
// Some chat lines have effects that slightly protrude to the bottom,
// which we do not want to mask away, hence the padding.
Padding = new MarginPadding { Bottom = 5 } ,
2019-10-22 00:11:19 +00:00
Child = ChatLineFlow = new FillFlowContainer
2018-04-13 09:19:50 +00:00
{
2019-08-14 01:53:47 +00:00
Padding = new MarginPadding { Left = 20 , Right = 20 } ,
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
Direction = FillDirection . Vertical ,
}
} ,
2018-04-13 09:19:50 +00:00
} ;
2018-07-24 02:56:34 +00:00
newMessagesArrived ( Channel . Messages ) ;
2018-12-03 09:13:10 +00:00
Channel . NewMessagesArrived + = newMessagesArrived ;
Channel . MessageRemoved + = messageRemoved ;
Channel . PendingMessageResolved + = pendingMessageResolved ;
2019-06-20 14:01:39 +00:00
}
2018-12-03 09:13:10 +00:00
2018-04-13 09:19:50 +00:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2018-07-09 16:30:41 +00:00
Channel . NewMessagesArrived - = newMessagesArrived ;
Channel . MessageRemoved - = messageRemoved ;
Channel . PendingMessageResolved - = pendingMessageResolved ;
2018-04-13 09:19:50 +00:00
}
2018-12-21 08:54:12 +00:00
protected virtual ChatLine CreateChatLine ( Message m ) = > new ChatLine ( m ) ;
2019-10-21 22:30:37 +00:00
protected virtual DaySeparator CreateDaySeparator ( DateTimeOffset time ) = > new DaySeparator ( time )
2019-10-21 21:44:58 +00:00
{
2019-10-21 22:45:04 +00:00
Margin = new MarginPadding { Vertical = 10 } ,
Colour = colours . ChatBlue . Lighten ( 0.7f ) ,
2019-10-21 21:44:58 +00:00
} ;
2020-12-21 07:39:46 +00:00
private void newMessagesArrived ( IEnumerable < Message > newMessages ) = > Schedule ( ( ) = >
2018-04-13 09:19:50 +00:00
{
2020-04-29 06:23:28 +00:00
if ( newMessages . Min ( m = > m . Id ) < chatLines . Max ( c = > c . Message . Id ) )
{
// there is a case (on initial population) that we may receive past messages and need to reorder.
// easiest way is to just combine messages and recreate drawables (less worrying about day separators etc.)
newMessages = newMessages . Concat ( chatLines . Select ( c = > c . Message ) ) . OrderBy ( m = > m . Id ) . ToList ( ) ;
ChatLineFlow . Clear ( ) ;
}
2018-07-09 16:30:41 +00:00
// Add up to last Channel.MAX_HISTORY messages
2019-10-29 05:33:05 +00:00
var displayMessages = newMessages . Skip ( Math . Max ( 0 , newMessages . Count ( ) - Channel . MAX_HISTORY ) ) ;
2018-04-13 09:19:50 +00:00
2019-10-22 15:14:22 +00:00
Message lastMessage = chatLines . LastOrDefault ( ) ? . Message ;
2018-04-13 09:19:50 +00:00
2019-10-22 15:16:17 +00:00
foreach ( var message in displayMessages )
2019-10-21 21:44:58 +00:00
{
2019-10-22 15:16:17 +00:00
if ( lastMessage = = null | | lastMessage . Timestamp . ToLocalTime ( ) . Date ! = message . Timestamp . ToLocalTime ( ) . Date )
ChatLineFlow . Add ( CreateDaySeparator ( message . Timestamp ) ) ;
2019-10-21 21:44:58 +00:00
2019-10-22 15:16:17 +00:00
ChatLineFlow . Add ( CreateChatLine ( message ) ) ;
lastMessage = message ;
}
2019-10-21 21:44:58 +00:00
2019-10-22 15:14:22 +00:00
var staleMessages = chatLines . Where ( c = > c . LifetimeEnd = = double . MaxValue ) . ToArray ( ) ;
2019-10-29 05:32:38 +00:00
int count = staleMessages . Length - Channel . MAX_HISTORY ;
2018-04-13 09:19:50 +00:00
2019-10-29 06:45:41 +00:00
if ( count > 0 )
2018-04-13 09:19:50 +00:00
{
2019-10-29 06:45:41 +00:00
void expireAndAdjustScroll ( Drawable d )
{
scroll . OffsetScrollPosition ( - d . DrawHeight ) ;
d . Expire ( ) ;
}
for ( int i = 0 ; i < count ; i + + )
expireAndAdjustScroll ( staleMessages [ i ] ) ;
// remove all adjacent day separators after stale message removal
for ( int i = 0 ; i < ChatLineFlow . Count - 1 ; i + + )
{
if ( ! ( ChatLineFlow [ i ] is DaySeparator ) ) break ;
if ( ! ( ChatLineFlow [ i + 1 ] is DaySeparator ) ) break ;
expireAndAdjustScroll ( ChatLineFlow [ i ] ) ;
}
2018-04-13 09:19:50 +00:00
}
2019-10-29 05:33:05 +00:00
2021-01-31 20:37:52 +00:00
// due to the scroll adjusts from old messages removal above, a scroll-to-end must be enforced,
// to avoid making the container think the user has scrolled back up and unwantedly disable auto-scrolling.
2021-02-02 06:16:10 +00:00
if ( newMessages . Any ( m = > m is LocalMessage ) )
scroll . ScrollToEnd ( ) ;
2020-12-21 07:39:46 +00:00
} ) ;
2018-04-13 09:19:50 +00:00
2020-12-21 07:39:46 +00:00
private void pendingMessageResolved ( Message existing , Message updated ) = > Schedule ( ( ) = >
2018-04-13 09:19:50 +00:00
{
2019-10-22 15:14:22 +00:00
var found = chatLines . LastOrDefault ( c = > c . Message = = existing ) ;
2019-04-01 03:16:05 +00:00
2018-04-13 09:19:50 +00:00
if ( found ! = null )
{
Trace . Assert ( updated . Id . HasValue , "An updated message was returned with no ID." ) ;
2018-12-21 08:54:12 +00:00
ChatLineFlow . Remove ( found ) ;
2018-04-13 09:19:50 +00:00
found . Message = updated ;
2018-12-21 08:54:12 +00:00
ChatLineFlow . Add ( found ) ;
2018-04-13 09:19:50 +00:00
}
2020-12-21 07:39:46 +00:00
} ) ;
2018-04-13 09:19:50 +00:00
2020-12-21 07:39:46 +00:00
private void messageRemoved ( Message removed ) = > Schedule ( ( ) = >
2018-04-13 09:19:50 +00:00
{
2019-10-22 15:14:22 +00:00
chatLines . FirstOrDefault ( c = > c . Message = = removed ) ? . FadeColour ( Color4 . Red , 400 ) . FadeOut ( 600 ) . Expire ( ) ;
2020-12-21 07:39:46 +00:00
} ) ;
2018-04-13 09:19:50 +00:00
2019-10-22 15:14:22 +00:00
private IEnumerable < ChatLine > chatLines = > ChatLineFlow . Children . OfType < ChatLine > ( ) ;
2019-10-21 21:44:58 +00:00
2019-10-29 06:27:08 +00:00
public class DaySeparator : Container
2019-10-21 22:30:37 +00:00
{
public float TextSize
{
get = > text . Font . Size ;
set = > text . Font = text . Font . With ( size : value ) ;
}
private float lineHeight = 2 ;
public float LineHeight
{
2019-10-21 23:16:52 +00:00
get = > lineHeight ;
2019-10-22 00:11:19 +00:00
set = > lineHeight = leftBox . Height = rightBox . Height = value ;
2019-10-21 22:30:37 +00:00
}
private readonly SpriteText text ;
private readonly Box leftBox ;
private readonly Box rightBox ;
public DaySeparator ( DateTimeOffset time )
{
RelativeSizeAxes = Axes . X ;
AutoSizeAxes = Axes . Y ;
2019-10-21 22:45:04 +00:00
Child = new GridContainer
2019-10-21 22:30:37 +00:00
{
2019-10-21 22:45:04 +00:00
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
ColumnDimensions = new [ ]
2019-10-21 22:30:37 +00:00
{
2019-10-21 22:45:04 +00:00
new Dimension ( ) ,
new Dimension ( GridSizeMode . AutoSize ) ,
new Dimension ( ) ,
} ,
2019-10-21 23:16:52 +00:00
RowDimensions = new [ ] { new Dimension ( GridSizeMode . AutoSize ) , } ,
Content = new [ ]
2019-10-21 22:45:04 +00:00
{
new Drawable [ ]
2019-10-21 22:30:37 +00:00
{
2019-10-21 22:45:04 +00:00
leftBox = new Box
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . X ,
Height = lineHeight ,
} ,
2019-11-25 02:30:55 +00:00
text = new OsuSpriteText
2019-10-21 22:45:04 +00:00
{
Margin = new MarginPadding { Horizontal = 10 } ,
Text = time . ToLocalTime ( ) . ToString ( "dd MMM yyyy" ) ,
} ,
rightBox = new Box
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . X ,
Height = lineHeight ,
} ,
}
2019-10-21 22:30:37 +00:00
}
} ;
}
}
2021-01-31 20:37:52 +00:00
/// <summary>
2021-02-01 08:02:08 +00:00
/// An <see cref="OsuScrollContainer"/> with functionality to automatically scroll whenever the maximum scrollable distance increases.
2021-01-31 20:37:52 +00:00
/// </summary>
2021-02-02 06:16:10 +00:00
private class ChannelScrollContainer : UserTrackingScrollContainer
2021-01-31 20:37:52 +00:00
{
2021-02-01 19:04:44 +00:00
/// <summary>
/// The chat will be automatically scrolled to end if and only if
/// the distance between the current scroll position and the end of the scroll
/// is less than this value.
/// </summary>
2021-01-31 20:37:52 +00:00
private const float auto_scroll_leniency = 10f ;
private float? lastExtent ;
2021-02-02 06:16:10 +00:00
protected override void OnUserScroll ( float value , bool animated = true , double? distanceDecay = default )
{
base . OnUserScroll ( value , animated , distanceDecay ) ;
lastExtent = null ;
}
2021-01-31 20:37:52 +00:00
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
2021-02-02 06:16:10 +00:00
// If the user has scrolled to the bottom of the container, we should resume tracking new content.
bool cancelUserScroll = UserScrolling & & IsScrolledToEnd ( auto_scroll_leniency ) ;
2021-02-01 19:08:55 +00:00
2021-02-02 06:16:10 +00:00
// If the user hasn't overridden our behaviour and there has been new content added to the container, we should update our scroll position to track it.
bool requiresScrollUpdate = ! UserScrolling & & ( lastExtent = = null | | Precision . AlmostBigger ( ScrollableExtent , lastExtent . Value ) ) ;
2021-01-31 20:37:52 +00:00
2021-02-02 06:16:10 +00:00
if ( cancelUserScroll | | requiresScrollUpdate )
{
ScheduleAfterChildren ( ( ) = >
{
ScrollToEnd ( ) ;
lastExtent = ScrollableExtent ;
} ) ;
}
2021-01-31 20:37:52 +00:00
}
}
2018-04-13 09:19:50 +00:00
}
}