2019-01-24 16:43:03 +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.
2018-04-14 19:32:48 +08:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2018-12-10 20:08:14 +08:00
using System.Threading.Tasks ;
2018-04-14 19:32:48 +08:00
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2022-01-03 16:31:12 +08:00
using osu.Framework.Extensions ;
2018-04-14 19:32:48 +08:00
using osu.Framework.Logging ;
2020-12-21 03:18:00 +08:00
using osu.Game.Database ;
2021-12-26 15:26:47 +08:00
using osu.Game.Input ;
2018-04-14 19:32:48 +08:00
using osu.Game.Online.API ;
using osu.Game.Online.API.Requests ;
2021-11-04 17:02:44 +08:00
using osu.Game.Online.API.Requests.Responses ;
2022-05-19 18:26:14 +08:00
using osu.Game.Overlays.Chat.Listing ;
2018-04-14 19:32:48 +08:00
namespace osu.Game.Online.Chat
{
/// <summary>
/// Manages everything channel related
/// </summary>
2020-04-19 14:12:36 +08:00
public class ChannelManager : PollingComponent , IChannelPostTarget
2018-04-14 19:32:48 +08:00
{
/// <summary>
/// The channels the player joins on startup
/// </summary>
private readonly string [ ] defaultChannels =
{
@"#lazer" ,
@"#osu" ,
@"#lobby"
} ;
2019-01-07 17:50:27 +08:00
private readonly BindableList < Channel > availableChannels = new BindableList < Channel > ( ) ;
private readonly BindableList < Channel > joinedChannels = new BindableList < Channel > ( ) ;
2018-11-22 02:15:55 +08:00
2020-12-18 06:56:34 +08:00
/// <summary>
2020-12-21 03:18:00 +08:00
/// Keeps a stack of recently closed channels
2020-12-18 06:56:34 +08:00
/// </summary>
2021-06-03 19:59:25 +08:00
private readonly List < ClosedChannel > closedChannels = new List < ClosedChannel > ( ) ;
2020-12-14 02:21:50 +08:00
2020-12-14 09:46:02 +08:00
// For efficiency purposes, this constant bounds the number of closed channels we store.
// This number is somewhat arbitrary; future developers are free to modify it.
// Must be a positive number.
2020-12-14 12:27:48 +08:00
private const int closed_channels_max_size = 50 ;
2020-12-14 09:46:02 +08:00
2018-04-14 19:32:48 +08:00
/// <summary>
/// The currently opened channel
/// </summary>
public Bindable < Channel > CurrentChannel { get ; } = new Bindable < Channel > ( ) ;
/// <summary>
/// The Channels the player has joined
/// </summary>
2019-01-07 17:50:27 +08:00
public IBindableList < Channel > JoinedChannels = > joinedChannels ;
2018-04-14 19:32:48 +08:00
/// <summary>
/// The channels available for the player to join
/// </summary>
2019-01-07 17:50:27 +08:00
public IBindableList < Channel > AvailableChannels = > availableChannels ;
2018-04-14 19:32:48 +08:00
2020-02-14 21:14:00 +08:00
[Resolved]
private IAPIProvider api { get ; set ; }
2018-12-10 20:08:14 +08:00
2020-12-21 03:18:00 +08:00
[Resolved]
private UserLookupCache users { get ; set ; }
2018-12-10 20:08:14 +08:00
public readonly BindableBool HighPollRate = new BindableBool ( ) ;
2018-04-14 19:32:48 +08:00
2021-12-31 22:18:03 +08:00
private readonly IBindable < bool > isIdle = new BindableBool ( ) ;
2021-12-26 15:26:47 +08:00
2018-09-14 11:06:04 +08:00
public ChannelManager ( )
{
CurrentChannel . ValueChanged + = currentChannelChanged ;
2021-12-26 15:26:47 +08:00
}
2021-12-31 22:18:03 +08:00
[BackgroundDependencyLoader(permitNulls: true)]
2021-12-26 15:26:47 +08:00
private void load ( IdleTracker idleTracker )
{
HighPollRate . BindValueChanged ( updatePollRate ) ;
isIdle . BindValueChanged ( updatePollRate , true ) ;
2021-12-31 22:18:03 +08:00
if ( idleTracker ! = null )
isIdle . BindTo ( idleTracker . IsIdle ) ;
2021-12-26 15:26:47 +08:00
}
private void updatePollRate ( ValueChangedEvent < bool > valueChangedEvent )
{
// Polling will eventually be replaced with websocket, but let's avoid doing these background operations as much as possible for now.
// The only loss will be delayed PM/message highlight notifications.
2022-01-13 16:33:55 +08:00
int millisecondsBetweenPolls = HighPollRate . Value ? 1000 : 60000 ;
if ( isIdle . Value )
millisecondsBetweenPolls * = 10 ;
if ( TimeBetweenPolls . Value ! = millisecondsBetweenPolls )
{
TimeBetweenPolls . Value = millisecondsBetweenPolls ;
Logger . Log ( $"Chat is now polling every {TimeBetweenPolls.Value} ms" ) ;
}
2018-09-14 11:06:04 +08:00
}
2018-07-24 10:54:11 +08:00
/// <summary>
/// Opens a channel or switches to the channel if already opened.
/// </summary>
/// <exception cref="ChannelNotFoundException">If the name of the specifed channel was not found this exception will be thrown.</exception>
/// <param name="name"></param>
2018-04-14 19:32:48 +08:00
public void OpenChannel ( string name )
{
if ( name = = null )
throw new ArgumentNullException ( nameof ( name ) ) ;
2018-11-14 12:19:20 +08:00
CurrentChannel . Value = AvailableChannels . FirstOrDefault ( c = > c . Name = = name ) ? ? throw new ChannelNotFoundException ( name ) ;
2018-04-14 19:32:48 +08:00
}
2018-07-24 10:54:11 +08:00
/// <summary>
/// Opens a new private channel.
/// </summary>
2018-07-24 11:14:33 +08:00
/// <param name="user">The user the private channel is opened with.</param>
2021-11-04 17:02:44 +08:00
public void OpenPrivateChannel ( APIUser user )
2018-04-14 19:32:48 +08:00
{
if ( user = = null )
throw new ArgumentNullException ( nameof ( user ) ) ;
2019-06-03 17:25:19 +08:00
if ( user . Id = = api . LocalUser . Value . Id )
return ;
2018-11-13 14:20:40 +08:00
CurrentChannel . Value = JoinedChannels . FirstOrDefault ( c = > c . Type = = ChannelType . PM & & c . Users . Count = = 1 & & c . Users . Any ( u = > u . Id = = user . Id ) )
2020-06-08 16:49:45 +08:00
? ? JoinChannel ( new Channel ( user ) ) ;
2018-04-14 19:32:48 +08:00
}
2019-05-12 07:13:48 +08:00
private void currentChannelChanged ( ValueChangedEvent < Channel > e )
{
2022-05-30 03:07:42 +08:00
bool isSelectorChannel = e . NewValue is ChannelListing . ChannelListingChannel ;
2022-05-16 02:38:37 +08:00
if ( ! isSelectorChannel )
2019-05-12 07:13:48 +08:00
JoinChannel ( e . NewValue ) ;
}
2018-11-13 16:24:11 +08:00
/// <summary>
/// Ensure we run post actions in sequence, once at a time.
/// </summary>
private readonly Queue < Action > postQueue = new Queue < Action > ( ) ;
2018-04-14 19:32:48 +08:00
/// <summary>
/// Posts a message to the currently opened channel.
/// </summary>
/// <param name="text">The message text that is going to be posted</param>
/// <param name="isAction">Is true if the message is an action, e.g.: user is currently eating </param>
2018-12-20 16:01:08 +08:00
/// <param name="target">An optional target channel. If null, <see cref="CurrentChannel"/> will be used.</param>
public void PostMessage ( string text , bool isAction = false , Channel target = null )
2018-04-14 19:32:48 +08:00
{
2020-06-03 15:48:44 +08:00
target ? ? = CurrentChannel . Value ;
2018-04-14 19:32:48 +08:00
2018-12-20 16:01:08 +08:00
if ( target = = null )
return ;
2018-07-10 03:00:39 +08:00
2018-11-13 16:24:11 +08:00
void dequeueAndRun ( )
2018-04-14 19:32:48 +08:00
{
2018-11-13 16:24:11 +08:00
if ( postQueue . Count > 0 )
postQueue . Dequeue ( ) . Invoke ( ) ;
2018-04-14 19:32:48 +08:00
}
2018-11-13 16:24:11 +08:00
postQueue . Enqueue ( ( ) = >
2018-04-14 19:32:48 +08:00
{
2018-11-13 16:24:11 +08:00
if ( ! api . IsLoggedIn )
{
2018-12-20 16:01:08 +08:00
target . AddNewMessages ( new ErrorMessage ( "Please sign in to participate in chat!" ) ) ;
2018-11-13 16:24:11 +08:00
return ;
}
2018-04-14 19:32:48 +08:00
2018-11-13 16:24:11 +08:00
var message = new LocalEchoMessage
{
Sender = api . LocalUser . Value ,
Timestamp = DateTimeOffset . Now ,
2018-12-20 16:01:08 +08:00
ChannelId = target . Id ,
2018-11-13 16:24:11 +08:00
IsAction = isAction ,
Content = text
} ;
2018-04-14 19:32:48 +08:00
2018-12-20 16:01:08 +08:00
target . AddLocalEcho ( message ) ;
2018-11-13 16:24:11 +08:00
// if this is a PM and the first message, we need to do a special request to create the PM channel
2020-06-08 16:49:45 +08:00
if ( target . Type = = ChannelType . PM & & target . Id = = 0 )
2018-11-13 16:24:11 +08:00
{
2018-12-20 16:01:08 +08:00
var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest ( target . Users . First ( ) , message ) ;
2018-11-14 12:59:02 +08:00
2018-11-13 16:24:11 +08:00
createNewPrivateMessageRequest . Success + = createRes = >
{
2018-12-20 16:01:08 +08:00
target . Id = createRes . ChannelID ;
target . ReplaceMessage ( message , createRes . Message ) ;
2018-11-13 16:24:11 +08:00
dequeueAndRun ( ) ;
} ;
2018-11-14 12:59:02 +08:00
2018-11-13 16:24:11 +08:00
createNewPrivateMessageRequest . Failure + = exception = >
{
2022-05-18 08:47:23 +08:00
handlePostException ( exception ) ;
2018-12-20 16:01:08 +08:00
target . ReplaceMessage ( message , null ) ;
2018-11-13 16:24:11 +08:00
dequeueAndRun ( ) ;
} ;
api . Queue ( createNewPrivateMessageRequest ) ;
return ;
}
var req = new PostMessageRequest ( message ) ;
2018-11-14 12:59:02 +08:00
2018-11-13 16:24:11 +08:00
req . Success + = m = >
{
2018-12-20 16:01:08 +08:00
target . ReplaceMessage ( message , m ) ;
2018-11-13 16:24:11 +08:00
dequeueAndRun ( ) ;
} ;
2018-11-14 12:59:02 +08:00
2018-11-13 16:24:11 +08:00
req . Failure + = exception = >
{
2021-02-26 12:37:58 +08:00
handlePostException ( exception ) ;
2018-12-20 16:01:08 +08:00
target . ReplaceMessage ( message , null ) ;
2018-11-13 16:24:11 +08:00
dequeueAndRun ( ) ;
} ;
2018-11-14 12:59:02 +08:00
2018-11-13 16:24:11 +08:00
api . Queue ( req ) ;
} ) ;
// always run if the queue is empty
if ( postQueue . Count = = 1 )
dequeueAndRun ( ) ;
2018-04-14 19:32:48 +08:00
}
2021-02-26 12:37:58 +08:00
private static void handlePostException ( Exception exception )
{
if ( exception is APIException apiException )
Logger . Log ( apiException . Message , level : LogLevel . Important ) ;
else
Logger . Error ( exception , "Posting message failed." ) ;
}
2018-07-24 11:14:33 +08:00
/// <summary>
/// Posts a command locally. Commands like /help will result in a help message written in the current channel.
/// </summary>
/// <param name="text">the text containing the command identifier and command parameters.</param>
2018-12-20 16:01:08 +08:00
/// <param name="target">An optional target channel. If null, <see cref="CurrentChannel"/> will be used.</param>
public void PostCommand ( string text , Channel target = null )
2018-04-14 19:32:48 +08:00
{
2020-06-03 15:48:44 +08:00
target ? ? = CurrentChannel . Value ;
2018-12-20 16:01:08 +08:00
if ( target = = null )
2018-04-14 19:32:48 +08:00
return ;
2021-10-27 12:04:41 +08:00
string [ ] parameters = text . Split ( ' ' , 2 ) ;
2018-04-14 19:32:48 +08:00
string command = parameters [ 0 ] ;
string content = parameters . Length = = 2 ? parameters [ 1 ] : string . Empty ;
switch ( command )
{
2020-04-19 14:12:36 +08:00
case "np" :
2021-06-29 14:58:07 +08:00
AddInternal ( new NowPlayingCommand ( target ) ) ;
2020-04-19 14:12:36 +08:00
break ;
2018-04-14 19:32:48 +08:00
case "me" :
if ( string . IsNullOrWhiteSpace ( content ) )
{
2018-12-20 16:01:08 +08:00
target . AddNewMessages ( new ErrorMessage ( "Usage: /me [action]" ) ) ;
2018-04-14 19:32:48 +08:00
break ;
}
2021-06-29 14:58:07 +08:00
PostMessage ( content , true , target ) ;
2018-04-14 19:32:48 +08:00
break ;
2019-08-05 07:02:42 +08:00
case "join" :
if ( string . IsNullOrWhiteSpace ( content ) )
{
target . AddNewMessages ( new ErrorMessage ( "Usage: /join [channel]" ) ) ;
break ;
}
2019-11-11 20:05:36 +08:00
var channel = availableChannels . FirstOrDefault ( c = > c . Name = = content | | c . Name = = $"#{content}" ) ;
2019-08-08 15:02:09 +08:00
2019-08-05 07:02:42 +08:00
if ( channel = = null )
{
target . AddNewMessages ( new ErrorMessage ( $"Channel '{content}' not found." ) ) ;
break ;
}
JoinChannel ( channel ) ;
break ;
2021-09-06 01:16:57 +08:00
case "chat" :
2021-09-08 00:25:47 +08:00
case "msg" :
2021-09-06 04:10:08 +08:00
case "query" :
2021-09-06 01:16:57 +08:00
if ( string . IsNullOrWhiteSpace ( content ) )
{
2021-09-06 04:20:19 +08:00
target . AddNewMessages ( new ErrorMessage ( $"Usage: /{command} [user]" ) ) ;
2021-09-06 01:16:57 +08:00
break ;
}
2021-09-12 15:50:53 +08:00
// Check if the user has joined the requested channel already.
// This uses the channel name for comparison as the PM user's username is unavailable after a restart.
2021-09-10 15:15:43 +08:00
var privateChannel = JoinedChannels . FirstOrDefault (
2021-09-08 00:22:59 +08:00
c = > c . Type = = ChannelType . PM & & c . Users . Count = = 1 & & c . Name . Equals ( content , StringComparison . OrdinalIgnoreCase ) ) ;
2021-09-10 15:01:38 +08:00
2021-09-10 15:15:43 +08:00
if ( privateChannel ! = null )
2021-09-10 15:01:38 +08:00
{
2021-09-10 15:15:43 +08:00
CurrentChannel . Value = privateChannel ;
2021-09-10 15:01:38 +08:00
break ;
}
2021-09-08 00:06:12 +08:00
2021-09-06 01:16:57 +08:00
var request = new GetUserRequest ( content ) ;
request . Success + = OpenPrivateChannel ;
2021-09-11 21:18:09 +08:00
request . Failure + = e = > target . AddNewMessages (
2021-09-12 18:56:36 +08:00
new ErrorMessage ( e . InnerException ? . Message = = @"NotFound" ? $"User '{content}' was not found." : $"Could not fetch user '{content}'." ) ) ;
2021-09-11 21:18:09 +08:00
2021-09-06 01:16:57 +08:00
api . Queue ( request ) ;
break ;
2018-04-14 19:32:48 +08:00
case "help" :
2021-09-06 01:16:57 +08:00
target . AddNewMessages ( new InfoMessage ( "Supported commands: /help, /me [action], /join [channel], /chat [user], /np" ) ) ;
2018-04-14 19:32:48 +08:00
break ;
default :
2018-12-20 16:01:08 +08:00
target . AddNewMessages ( new ErrorMessage ( $@"""/{command}"" is not supported! For a list of supported commands see /help" ) ) ;
2018-04-14 19:32:48 +08:00
break ;
}
}
private void handleChannelMessages ( IEnumerable < Message > messages )
{
var channels = JoinedChannels . ToList ( ) ;
2018-11-12 19:41:10 +08:00
foreach ( var group in messages . GroupBy ( m = > m . ChannelId ) )
2018-04-14 19:32:48 +08:00
channels . Find ( c = > c . Id = = group . Key ) ? . AddNewMessages ( group . ToArray ( ) ) ;
}
2018-11-13 16:24:11 +08:00
private void initializeChannels ( )
2018-04-14 19:32:48 +08:00
{
var req = new ListChannelsRequest ( ) ;
2021-10-27 12:04:41 +08:00
bool joinDefaults = JoinedChannels . Count = = 0 ;
2018-11-13 14:20:40 +08:00
2018-04-14 19:32:48 +08:00
req . Success + = channels = >
{
2018-07-24 23:51:20 +08:00
foreach ( var channel in channels )
{
2018-11-21 16:15:10 +08:00
var ch = getChannel ( channel , addToAvailable : true ) ;
2018-07-24 23:51:20 +08:00
// join any channels classified as "defaults"
2018-11-13 16:24:11 +08:00
if ( joinDefaults & & defaultChannels . Any ( c = > c . Equals ( channel . Name , StringComparison . OrdinalIgnoreCase ) ) )
2020-06-03 16:31:55 +08:00
joinChannel ( ch ) ;
2018-07-24 23:51:20 +08:00
}
2018-04-14 19:32:48 +08:00
} ;
2018-07-24 10:54:11 +08:00
req . Failure + = error = >
{
Logger . Error ( error , "Fetching channel list failed" ) ;
2018-11-13 16:24:11 +08:00
initializeChannels ( ) ;
2018-07-24 10:54:11 +08:00
} ;
2018-04-14 19:32:48 +08:00
api . Queue ( req ) ;
}
2018-07-30 03:40:43 +08:00
/// <summary>
/// Fetches inital messages of a channel
///
/// TODO: remove this when the API supports returning initial fetch messages for more than one channel by specifying the last message id per channel instead of one last message id globally.
/// right now it caps out at 50 messages and therefore only returns one channel's worth of content.
/// </summary>
/// <param name="channel">The channel </param>
2021-12-10 13:04:31 +08:00
private void fetchInitialMessages ( Channel channel )
2018-07-30 03:40:43 +08:00
{
2020-06-03 16:31:55 +08:00
if ( channel . Id < = 0 | | channel . MessagesLoaded ) return ;
2018-11-13 16:24:11 +08:00
2018-11-12 19:41:10 +08:00
var fetchInitialMsgReq = new GetMessagesRequest ( channel ) ;
2018-11-13 16:24:11 +08:00
fetchInitialMsgReq . Success + = messages = >
{
handleChannelMessages ( messages ) ;
2018-11-14 12:19:20 +08:00
channel . MessagesLoaded = true ; // this will mark the channel as having received messages even if there were none.
2018-11-13 16:24:11 +08:00
} ;
2018-07-30 03:40:43 +08:00
api . Queue ( fetchInitialMsgReq ) ;
}
2018-11-21 16:15:10 +08:00
/// <summary>
/// Find an existing channel instance for the provided channel. Lookup is performed basd on ID.
/// The provided channel may be used if an existing instance is not found.
/// </summary>
/// <param name="lookup">A candidate channel to be used for lookup or permanently on lookup failure.</param>
/// <param name="addToAvailable">Whether the channel should be added to <see cref="AvailableChannels"/> if not already.</param>
/// <param name="addToJoined">Whether the channel should be added to <see cref="JoinedChannels"/> if not already.</param>
/// <returns>The found channel.</returns>
private Channel getChannel ( Channel lookup , bool addToAvailable = false , bool addToJoined = false )
2018-11-13 14:20:40 +08:00
{
2018-11-21 16:15:10 +08:00
Channel found = null ;
2018-11-13 14:20:40 +08:00
2018-11-22 17:27:22 +08:00
bool lookupCondition ( Channel ch ) = > lookup . Id > 0 ? ch . Id = = lookup . Id : lookup . Name = = ch . Name ;
var available = AvailableChannels . FirstOrDefault ( lookupCondition ) ;
2018-11-21 16:15:10 +08:00
if ( available ! = null )
found = available ;
2018-11-13 14:20:40 +08:00
2018-11-22 17:27:22 +08:00
var joined = JoinedChannels . FirstOrDefault ( lookupCondition ) ;
2018-11-21 16:15:10 +08:00
if ( found = = null & & joined ! = null )
found = joined ;
if ( found = = null )
2018-11-13 14:20:40 +08:00
{
2018-11-21 16:15:10 +08:00
found = lookup ;
// if we're using a channel object from the server, we want to remove ourselves from the users list.
// this is because we check the first user in the channel to display a name/icon on tabs for now.
var foundSelf = found . Users . FirstOrDefault ( u = > u . Id = = api . LocalUser . Value . Id ) ;
2018-11-13 14:20:40 +08:00
if ( foundSelf ! = null )
2018-11-21 16:15:10 +08:00
found . Users . Remove ( foundSelf ) ;
}
2018-11-22 06:21:27 +08:00
if ( joined = = null & & addToJoined ) joinedChannels . Add ( found ) ;
if ( available = = null & & addToAvailable ) availableChannels . Add ( found ) ;
2018-11-21 16:15:10 +08:00
return found ;
}
2018-11-13 14:20:40 +08:00
2018-11-21 16:15:10 +08:00
/// <summary>
2021-01-21 14:42:23 +08:00
/// Joins a channel if it has not already been joined. Must be called from the update thread.
2018-11-21 16:15:10 +08:00
/// </summary>
/// <param name="channel">The channel to join.</param>
/// <returns>The joined channel. Note that this may not match the parameter channel as it is a backed object.</returns>
2022-05-16 02:38:37 +08:00
public Channel JoinChannel ( Channel channel ) = > joinChannel ( channel , true ) ;
2020-06-03 16:31:55 +08:00
2022-05-16 02:38:37 +08:00
private Channel joinChannel ( Channel channel , bool fetchInitialMessages = false )
2018-11-21 16:15:10 +08:00
{
if ( channel = = null ) return null ;
2018-11-13 14:20:40 +08:00
2018-11-21 16:15:10 +08:00
channel = getChannel ( channel , addToJoined : true ) ;
// ensure we are joined to the channel
if ( ! channel . Joined . Value )
{
2020-06-08 16:49:45 +08:00
channel . Joined . Value = true ;
2020-06-03 16:31:55 +08:00
switch ( channel . Type )
2018-11-13 14:20:40 +08:00
{
2020-06-03 20:28:29 +08:00
case ChannelType . Multiplayer :
// join is implicit. happens when you join a multiplayer game.
// this will probably change in the future.
2022-05-16 02:38:37 +08:00
joinChannel ( channel , fetchInitialMessages ) ;
2020-06-03 20:28:29 +08:00
return channel ;
2020-06-08 16:49:45 +08:00
case ChannelType . PM :
var createRequest = new CreateChannelRequest ( channel ) ;
createRequest . Success + = resChannel = >
{
if ( resChannel . ChannelID . HasValue )
{
channel . Id = resChannel . ChannelID . Value ;
handleChannelMessages ( resChannel . RecentMessages ) ;
channel . MessagesLoaded = true ; // this will mark the channel as having received messages even if there were none.
}
} ;
api . Queue ( createRequest ) ;
2020-06-03 16:31:55 +08:00
break ;
default :
2020-07-14 12:07:17 +08:00
var req = new JoinChannelRequest ( channel ) ;
2022-05-16 02:38:37 +08:00
req . Success + = ( ) = > joinChannel ( channel , fetchInitialMessages ) ;
2020-06-03 16:31:55 +08:00
req . Failure + = ex = > LeaveChannel ( channel ) ;
api . Queue ( req ) ;
return channel ;
2018-11-13 14:20:40 +08:00
}
}
2020-06-03 16:31:55 +08:00
else
{
if ( fetchInitialMessages )
2021-12-10 13:04:31 +08:00
this . fetchInitialMessages ( channel ) ;
2020-06-03 16:31:55 +08:00
}
2018-11-13 14:20:40 +08:00
2022-05-16 02:38:37 +08:00
CurrentChannel . Value ? ? = channel ;
2018-11-13 14:20:40 +08:00
2018-11-21 16:15:10 +08:00
return channel ;
2018-11-13 14:20:40 +08:00
}
2021-01-21 14:42:23 +08:00
/// <summary>
/// Leave the specified channel. Can be called from any thread.
/// </summary>
/// <param name="channel">The channel to leave.</param>
public void LeaveChannel ( Channel channel ) = > Schedule ( ( ) = >
2018-11-13 14:20:40 +08:00
{
if ( channel = = null ) return ;
2018-11-22 02:15:55 +08:00
if ( channel = = CurrentChannel . Value )
CurrentChannel . Value = null ;
2018-11-13 14:20:40 +08:00
2018-11-22 02:15:55 +08:00
joinedChannels . Remove ( channel ) ;
2020-12-14 09:46:02 +08:00
// Prevent the closedChannel list from exceeding the max size
// by removing the oldest element
2020-12-21 03:18:00 +08:00
if ( closedChannels . Count > = closed_channels_max_size )
2020-12-14 09:46:02 +08:00
{
2020-12-21 03:18:00 +08:00
closedChannels . RemoveAt ( 0 ) ;
2020-12-14 09:46:02 +08:00
}
2021-06-03 20:20:52 +08:00
// For PM channels, we store the user ID; else, we store the channel ID
2020-12-21 03:18:00 +08:00
closedChannels . Add ( channel . Type = = ChannelType . PM
? new ClosedChannel ( ChannelType . PM , channel . Users . Single ( ) . Id )
: new ClosedChannel ( channel . Type , channel . Id ) ) ;
2018-11-13 14:20:40 +08:00
if ( channel . Joined . Value )
{
2020-07-14 12:07:17 +08:00
api . Queue ( new LeaveChannelRequest ( channel ) ) ;
2018-11-13 14:20:40 +08:00
channel . Joined . Value = false ;
}
2021-01-21 14:42:23 +08:00
} ) ;
2018-11-13 14:20:40 +08:00
2020-12-14 09:46:02 +08:00
/// <summary>
2021-06-03 20:20:52 +08:00
/// Opens the most recently closed channel that has not already been reopened,
2020-12-14 10:02:35 +08:00
/// Works similarly to reopening the last closed tab on a web browser.
2020-12-14 09:46:02 +08:00
/// </summary>
2020-12-14 02:21:50 +08:00
public void JoinLastClosedChannel ( )
{
2021-06-03 20:20:52 +08:00
// This loop could be eliminated if the join channel operation ensured that every channel joined
// is removed from the closedChannels list, but it'd require a linear scan of closed channels on every join.
// To keep the overhead of joining channels low, just lazily scan the list of closed channels locally.
2020-12-21 03:18:00 +08:00
while ( closedChannels . Count > 0 )
2020-12-16 23:13:50 +08:00
{
2020-12-21 03:18:00 +08:00
ClosedChannel lastClosedChannel = closedChannels . Last ( ) ;
closedChannels . RemoveAt ( closedChannels . Count - 1 ) ;
2020-12-16 23:13:50 +08:00
2021-06-03 19:58:19 +08:00
// If the user has already joined the channel, try the next one
2021-06-03 20:13:01 +08:00
if ( joinedChannels . FirstOrDefault ( lastClosedChannel . Matches ) ! = null )
2021-06-03 19:58:19 +08:00
continue ;
2020-12-14 12:27:48 +08:00
2021-06-03 20:13:01 +08:00
Channel lastChannel = AvailableChannels . FirstOrDefault ( lastClosedChannel . Matches ) ;
2020-12-21 03:18:00 +08:00
2021-06-03 19:58:19 +08:00
if ( lastChannel ! = null )
{
2021-06-03 20:20:52 +08:00
// Channel exists as an available channel, directly join it
2021-06-03 19:58:19 +08:00
CurrentChannel . Value = JoinChannel ( lastChannel ) ;
}
else if ( lastClosedChannel . Type = = ChannelType . PM )
{
2021-06-03 20:20:52 +08:00
// Try to get user in order to open PM chat
2022-01-03 16:31:12 +08:00
users . GetUserAsync ( ( int ) lastClosedChannel . Id ) . ContinueWith ( task = >
2021-06-03 19:58:19 +08:00
{
2022-01-06 21:54:43 +08:00
var user = task . GetResultSafely ( ) ;
2022-01-05 14:54:10 +08:00
if ( user ! = null )
Schedule ( ( ) = > CurrentChannel . Value = JoinChannel ( new Channel ( user ) ) ) ;
2021-06-03 19:58:19 +08:00
} ) ;
2020-12-17 01:04:07 +08:00
}
2021-06-03 19:58:19 +08:00
return ;
2020-12-14 09:46:02 +08:00
}
2020-12-14 02:21:50 +08:00
}
2018-11-13 14:20:40 +08:00
private long lastMessageId ;
2018-11-13 16:24:11 +08:00
private bool channelsInitialised ;
2018-12-10 20:08:14 +08:00
protected override Task Poll ( )
2018-11-13 14:20:40 +08:00
{
2018-12-10 20:08:14 +08:00
if ( ! api . IsLoggedIn )
return base . Poll ( ) ;
var fetchReq = new GetUpdatesRequest ( lastMessageId ) ;
2018-11-13 14:20:40 +08:00
2018-12-10 20:08:14 +08:00
var tcs = new TaskCompletionSource < bool > ( ) ;
fetchReq . Success + = updates = >
{
if ( updates ? . Presence ! = null )
2018-11-13 14:20:40 +08:00
{
2018-12-10 20:08:14 +08:00
foreach ( var channel in updates . Presence )
2018-11-13 14:20:40 +08:00
{
2018-12-10 20:08:14 +08:00
// we received this from the server so should mark the channel already joined.
2020-06-03 16:31:55 +08:00
channel . Joined . Value = true ;
joinChannel ( channel ) ;
2018-12-10 20:08:14 +08:00
}
2018-11-13 16:24:11 +08:00
2018-12-10 20:08:14 +08:00
//todo: handle left channels
2018-11-13 14:20:40 +08:00
2018-12-10 20:08:14 +08:00
handleChannelMessages ( updates . Messages ) ;
2018-11-13 14:20:40 +08:00
2018-12-10 20:08:14 +08:00
foreach ( var group in updates . Messages . GroupBy ( m = > m . ChannelId ) )
JoinedChannels . FirstOrDefault ( c = > c . Id = = group . Key ) ? . AddNewMessages ( group . ToArray ( ) ) ;
2018-11-13 14:20:40 +08:00
2018-12-10 20:08:14 +08:00
lastMessageId = updates . Messages . LastOrDefault ( ) ? . Id ? ? lastMessageId ;
}
2018-11-13 14:20:40 +08:00
2018-12-10 20:08:14 +08:00
if ( ! channelsInitialised )
{
channelsInitialised = true ;
// we want this to run after the first presence so we can see if the user is in any channels already.
initializeChannels ( ) ;
}
2018-11-21 16:14:48 +08:00
2018-12-10 20:08:14 +08:00
tcs . SetResult ( true ) ;
} ;
fetchReq . Failure + = _ = > tcs . SetResult ( false ) ;
2018-11-13 14:20:40 +08:00
2018-12-10 20:08:14 +08:00
api . Queue ( fetchReq ) ;
2018-11-13 14:20:40 +08:00
2018-12-10 20:08:14 +08:00
return tcs . Task ;
2018-11-13 14:20:40 +08:00
}
2020-01-12 00:42:02 +08:00
/// <summary>
/// Marks the <paramref name="channel"/> as read
/// </summary>
/// <param name="channel">The channel that will be marked as read</param>
2020-01-12 02:47:35 +08:00
public void MarkChannelAsRead ( Channel channel )
2020-01-03 00:07:28 +08:00
{
2020-01-12 23:24:14 +08:00
if ( channel . LastMessageId = = channel . LastReadId )
return ;
2021-08-15 08:57:11 +08:00
var message = channel . Messages . FindLast ( msg = > ! ( msg is LocalMessage ) ) ;
2020-01-13 11:22:44 +08:00
if ( message = = null )
return ;
2020-01-03 00:07:28 +08:00
var req = new MarkChannelAsReadRequest ( channel , message ) ;
2020-01-12 00:42:02 +08:00
2020-01-03 00:07:28 +08:00
req . Success + = ( ) = > channel . LastReadId = message . Id ;
2022-02-24 15:02:16 +08:00
req . Failure + = e = > Logger . Log ( $"Failed to mark channel {channel} up to '{message}' as read ({e.Message})" , LoggingTarget . Network ) ;
2020-01-12 00:42:02 +08:00
2020-01-03 00:07:28 +08:00
api . Queue ( req ) ;
}
2018-04-14 19:32:48 +08:00
}
2018-07-10 00:45:11 +08:00
2018-07-24 11:14:33 +08:00
/// <summary>
/// An exception thrown when a channel could not been found.
/// </summary>
2018-07-10 00:45:11 +08:00
public class ChannelNotFoundException : Exception
{
public ChannelNotFoundException ( string channelName )
: base ( $"A channel with the name {channelName} could not be found." )
{
}
}
2020-12-21 03:18:00 +08:00
/// <summary>
2021-06-03 20:20:52 +08:00
/// Stores information about a closed channel
2020-12-21 03:18:00 +08:00
/// </summary>
public class ClosedChannel
{
2021-06-03 13:56:21 +08:00
public readonly ChannelType Type ;
public readonly long Id ;
2020-12-21 03:18:00 +08:00
public ClosedChannel ( ChannelType type , long id )
{
Type = type ;
Id = id ;
}
2021-06-03 20:13:01 +08:00
public bool Matches ( Channel channel )
2020-12-21 03:18:00 +08:00
{
2020-12-21 03:51:39 +08:00
if ( channel . Type ! = Type ) return false ;
2020-12-21 03:18:00 +08:00
2020-12-21 03:51:39 +08:00
return Type = = ChannelType . PM
? channel . Users . Single ( ) . Id = = Id
: channel . Id = = Id ;
2020-12-21 03:18:00 +08:00
}
}
2018-04-14 19:32:48 +08:00
}