2019-10-29 13:32:38 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Linq ;
2019-06-20 22:01:39 +08:00
using osu.Framework.Allocation ;
2018-11-20 15:51:59 +08:00
using osuTK.Graphics ;
2018-04-13 17:19:50 +08: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-22 05:44:58 +08:00
using osu.Framework.Graphics.Shapes ;
2019-10-22 06:30:37 +08:00
using osu.Game.Graphics ;
using osu.Framework.Extensions.Color4Extensions ;
using osu.Framework.Graphics.Sprites ;
2021-02-02 14:16:10 +08:00
using osu.Framework.Utils ;
2019-11-25 10:30:55 +08:00
using osu.Game.Graphics.Sprites ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Overlays.Chat
{
2018-07-10 00:30:41 +08:00
public class DrawableChannel : Container
2018-04-13 17:19:50 +08:00
{
2018-07-10 00:30:41 +08:00
public readonly Channel Channel ;
2019-10-22 08:11:19 +08:00
protected FillFlowContainer ChatLineFlow ;
2021-02-01 04:37:52 +08:00
private ChannelScrollContainer scroll ;
2018-04-13 17:19:50 +08:00
2020-03-23 11:03:33 +08:00
private bool scrollbarVisible = true ;
public bool ScrollbarVisible
{
set
{
if ( scrollbarVisible = = value ) return ;
scrollbarVisible = value ;
if ( scroll ! = null )
scroll . ScrollbarVisible = value ;
}
}
2019-10-22 06:30:37 +08:00
[Resolved]
private OsuColour colours { get ; set ; }
2018-07-10 00:30:41 +08:00
public DrawableChannel ( Channel channel )
2018-04-13 17:19:50 +08:00
{
2018-07-10 00:30:41 +08:00
Channel = channel ;
2018-04-13 17:19:50 +08:00
RelativeSizeAxes = Axes . Both ;
2019-06-20 22:01:39 +08:00
}
2018-04-13 17:19:50 +08:00
2019-06-20 22:01:39 +08:00
[BackgroundDependencyLoader]
private void load ( )
{
2019-08-14 09:53:47 +08:00
Child = new OsuContextMenuContainer
2018-04-13 17:19:50 +08:00
{
2019-08-14 09:53:47 +08:00
RelativeSizeAxes = Axes . Both ,
Masking = true ,
2021-02-01 04:37:52 +08:00
Child = scroll = new ChannelScrollContainer
2018-04-13 17:19:50 +08:00
{
2020-03-23 11:03:33 +08:00
ScrollbarVisible = scrollbarVisible ,
2018-04-13 17:19:50 +08:00
RelativeSizeAxes = Axes . Both ,
2019-08-14 09:53:47 +08: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 08:11:19 +08:00
Child = ChatLineFlow = new FillFlowContainer
2018-04-13 17:19:50 +08:00
{
2019-08-14 09:53:47 +08:00
Padding = new MarginPadding { Left = 20 , Right = 20 } ,
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
Direction = FillDirection . Vertical ,
}
} ,
2018-04-13 17:19:50 +08:00
} ;
2018-07-24 10:56:34 +08:00
newMessagesArrived ( Channel . Messages ) ;
2018-12-03 17:13:10 +08:00
Channel . NewMessagesArrived + = newMessagesArrived ;
Channel . MessageRemoved + = messageRemoved ;
Channel . PendingMessageResolved + = pendingMessageResolved ;
2019-06-20 22:01:39 +08:00
}
2018-12-03 17:13:10 +08:00
2018-04-13 17:19:50 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2018-07-10 00:30:41 +08:00
Channel . NewMessagesArrived - = newMessagesArrived ;
Channel . MessageRemoved - = messageRemoved ;
Channel . PendingMessageResolved - = pendingMessageResolved ;
2018-04-13 17:19:50 +08:00
}
2018-12-21 16:54:12 +08:00
protected virtual ChatLine CreateChatLine ( Message m ) = > new ChatLine ( m ) ;
2019-10-22 06:30:37 +08:00
protected virtual DaySeparator CreateDaySeparator ( DateTimeOffset time ) = > new DaySeparator ( time )
2019-10-22 05:44:58 +08:00
{
2019-10-22 06:45:04 +08:00
Margin = new MarginPadding { Vertical = 10 } ,
Colour = colours . ChatBlue . Lighten ( 0.7f ) ,
2019-10-22 05:44:58 +08:00
} ;
2020-12-21 15:39:46 +08:00
private void newMessagesArrived ( IEnumerable < Message > newMessages ) = > Schedule ( ( ) = >
2018-04-13 17:19:50 +08:00
{
2020-04-29 14:23:28 +08: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-10 00:30:41 +08:00
// Add up to last Channel.MAX_HISTORY messages
2019-10-29 13:33:05 +08:00
var displayMessages = newMessages . Skip ( Math . Max ( 0 , newMessages . Count ( ) - Channel . MAX_HISTORY ) ) ;
2018-04-13 17:19:50 +08:00
2019-10-22 23:14:22 +08:00
Message lastMessage = chatLines . LastOrDefault ( ) ? . Message ;
2018-04-13 17:19:50 +08:00
2019-10-22 23:16:17 +08:00
foreach ( var message in displayMessages )
2019-10-22 05:44:58 +08:00
{
2019-10-22 23:16:17 +08:00
if ( lastMessage = = null | | lastMessage . Timestamp . ToLocalTime ( ) . Date ! = message . Timestamp . ToLocalTime ( ) . Date )
ChatLineFlow . Add ( CreateDaySeparator ( message . Timestamp ) ) ;
2019-10-22 05:44:58 +08:00
2019-10-22 23:16:17 +08:00
ChatLineFlow . Add ( CreateChatLine ( message ) ) ;
lastMessage = message ;
}
2019-10-22 05:44:58 +08:00
2019-10-22 23:14:22 +08:00
var staleMessages = chatLines . Where ( c = > c . LifetimeEnd = = double . MaxValue ) . ToArray ( ) ;
2019-10-29 13:32:38 +08:00
int count = staleMessages . Length - Channel . MAX_HISTORY ;
2018-04-13 17:19:50 +08:00
2019-10-29 14:45:41 +08:00
if ( count > 0 )
2018-04-13 17:19:50 +08:00
{
2019-10-29 14:45:41 +08: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 17:19:50 +08:00
}
2019-10-29 13:33:05 +08:00
2021-02-01 04:37:52 +08: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 14:16:10 +08:00
if ( newMessages . Any ( m = > m is LocalMessage ) )
scroll . ScrollToEnd ( ) ;
2020-12-21 15:39:46 +08:00
} ) ;
2018-04-13 17:19:50 +08:00
2020-12-21 15:39:46 +08:00
private void pendingMessageResolved ( Message existing , Message updated ) = > Schedule ( ( ) = >
2018-04-13 17:19:50 +08:00
{
2019-10-22 23:14:22 +08:00
var found = chatLines . LastOrDefault ( c = > c . Message = = existing ) ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
if ( found ! = null )
{
Trace . Assert ( updated . Id . HasValue , "An updated message was returned with no ID." ) ;
2018-12-21 16:54:12 +08:00
ChatLineFlow . Remove ( found ) ;
2018-04-13 17:19:50 +08:00
found . Message = updated ;
2018-12-21 16:54:12 +08:00
ChatLineFlow . Add ( found ) ;
2018-04-13 17:19:50 +08:00
}
2020-12-21 15:39:46 +08:00
} ) ;
2018-04-13 17:19:50 +08:00
2020-12-21 15:39:46 +08:00
private void messageRemoved ( Message removed ) = > Schedule ( ( ) = >
2018-04-13 17:19:50 +08:00
{
2019-10-22 23:14:22 +08:00
chatLines . FirstOrDefault ( c = > c . Message = = removed ) ? . FadeColour ( Color4 . Red , 400 ) . FadeOut ( 600 ) . Expire ( ) ;
2020-12-21 15:39:46 +08:00
} ) ;
2018-04-13 17:19:50 +08:00
2019-10-22 23:14:22 +08:00
private IEnumerable < ChatLine > chatLines = > ChatLineFlow . Children . OfType < ChatLine > ( ) ;
2019-10-22 05:44:58 +08:00
2019-10-29 14:27:08 +08:00
public class DaySeparator : Container
2019-10-22 06:30:37 +08: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-22 07:16:52 +08:00
get = > lineHeight ;
2019-10-22 08:11:19 +08:00
set = > lineHeight = leftBox . Height = rightBox . Height = value ;
2019-10-22 06:30:37 +08: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-22 06:45:04 +08:00
Child = new GridContainer
2019-10-22 06:30:37 +08:00
{
2019-10-22 06:45:04 +08:00
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
ColumnDimensions = new [ ]
2019-10-22 06:30:37 +08:00
{
2019-10-22 06:45:04 +08:00
new Dimension ( ) ,
new Dimension ( GridSizeMode . AutoSize ) ,
new Dimension ( ) ,
} ,
2019-10-22 07:16:52 +08:00
RowDimensions = new [ ] { new Dimension ( GridSizeMode . AutoSize ) , } ,
Content = new [ ]
2019-10-22 06:45:04 +08:00
{
new Drawable [ ]
2019-10-22 06:30:37 +08:00
{
2019-10-22 06:45:04 +08:00
leftBox = new Box
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . X ,
Height = lineHeight ,
} ,
2019-11-25 10:30:55 +08:00
text = new OsuSpriteText
2019-10-22 06:45:04 +08: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-22 06:30:37 +08:00
}
} ;
}
}
2021-02-01 04:37:52 +08:00
/// <summary>
2021-02-01 16:02:08 +08:00
/// An <see cref="OsuScrollContainer"/> with functionality to automatically scroll whenever the maximum scrollable distance increases.
2021-02-01 04:37:52 +08:00
/// </summary>
2021-02-02 14:16:10 +08:00
private class ChannelScrollContainer : UserTrackingScrollContainer
2021-02-01 04:37:52 +08:00
{
2021-02-02 03:04:44 +08: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-02-01 04:37:52 +08:00
private const float auto_scroll_leniency = 10f ;
private float? lastExtent ;
2021-02-02 14:16:10 +08:00
protected override void OnUserScroll ( float value , bool animated = true , double? distanceDecay = default )
{
base . OnUserScroll ( value , animated , distanceDecay ) ;
lastExtent = null ;
}
2021-02-01 04:37:52 +08:00
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
2021-02-02 14:16:10 +08:00
// If the user has scrolled to the bottom of the container, we should resume tracking new content.
2021-02-02 14:44:03 +08:00
if ( UserScrolling & & IsScrolledToEnd ( auto_scroll_leniency ) )
CancelUserScroll ( ) ;
2021-02-02 03:08:55 +08:00
2021-02-02 14:16:10 +08: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-02-01 04:37:52 +08:00
2021-02-02 14:44:03 +08:00
if ( requiresScrollUpdate )
2021-02-02 14:16:10 +08:00
{
ScheduleAfterChildren ( ( ) = >
{
2021-02-02 14:44:03 +08:00
if ( ! UserScrolling )
{
ScrollToEnd ( ) ;
lastExtent = ScrollableExtent ;
}
2021-02-02 14:16:10 +08:00
} ) ;
}
2021-02-01 04:37:52 +08:00
}
}
2018-04-13 17:19:50 +08:00
}
}