mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 14:53:21 +08:00
Merge branch 'master' into only-rounded-shader
This commit is contained in:
commit
efe42f701a
12
UseLocalResources.ps1
Normal file
12
UseLocalResources.ps1
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
$CSPROJ="osu.Game/osu.Game.csproj"
|
||||||
|
$SLN="osu.sln"
|
||||||
|
|
||||||
|
dotnet remove $CSPROJ package ppy.osu.Game.Resources;
|
||||||
|
dotnet sln $SLN add ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
|
||||||
|
dotnet add $CSPROJ reference ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
|
||||||
|
|
||||||
|
$SLNF=Get-Content "osu.Desktop.slnf" | ConvertFrom-Json
|
||||||
|
$TMP=New-TemporaryFile
|
||||||
|
$SLNF.solution.projects += ("../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj")
|
||||||
|
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
|
||||||
|
Move-Item -Path $TMP -Destination "osu.Desktop.slnf" -Force
|
11
UseLocalResources.sh
Executable file
11
UseLocalResources.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
CSPROJ="osu.Game/osu.Game.csproj"
|
||||||
|
SLN="osu.sln"
|
||||||
|
|
||||||
|
dotnet remove $CSPROJ package ppy.osu.Game.Resources;
|
||||||
|
dotnet sln $SLN add ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
|
||||||
|
dotnet add $CSPROJ reference ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
|
||||||
|
|
||||||
|
SLNF="osu.Desktop.slnf"
|
||||||
|
TMP=$(mktemp)
|
||||||
|
jq '.solution.projects += ["../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj"]' $SLNF > $TMP
|
||||||
|
mv -f $TMP $SLNF
|
@ -23,6 +23,7 @@ namespace osu.Game.Tests.Chat
|
|||||||
private ChannelManager channelManager;
|
private ChannelManager channelManager;
|
||||||
private int currentMessageId;
|
private int currentMessageId;
|
||||||
private List<Message> sentMessages;
|
private List<Message> sentMessages;
|
||||||
|
private List<int> silencedUserIds;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public void Setup() => Schedule(() =>
|
||||||
@ -39,6 +40,7 @@ namespace osu.Game.Tests.Chat
|
|||||||
{
|
{
|
||||||
currentMessageId = 0;
|
currentMessageId = 0;
|
||||||
sentMessages = new List<Message>();
|
sentMessages = new List<Message>();
|
||||||
|
silencedUserIds = new List<int>();
|
||||||
|
|
||||||
((DummyAPIAccess)API).HandleRequest = req =>
|
((DummyAPIAccess)API).HandleRequest = req =>
|
||||||
{
|
{
|
||||||
@ -55,6 +57,19 @@ namespace osu.Game.Tests.Chat
|
|||||||
case MarkChannelAsReadRequest markRead:
|
case MarkChannelAsReadRequest markRead:
|
||||||
handleMarkChannelAsReadRequest(markRead);
|
handleMarkChannelAsReadRequest(markRead);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case ChatAckRequest ack:
|
||||||
|
ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToList() });
|
||||||
|
silencedUserIds.Clear();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case GetUpdatesRequest updatesRequest:
|
||||||
|
updatesRequest.TriggerSuccess(new GetUpdatesResponse
|
||||||
|
{
|
||||||
|
Messages = sentMessages.ToList(),
|
||||||
|
Presence = new List<Channel>()
|
||||||
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -95,6 +110,7 @@ namespace osu.Game.Tests.Chat
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddStep("post message", () => channelManager.PostMessage("Something interesting"));
|
AddStep("post message", () => channelManager.PostMessage("Something interesting"));
|
||||||
|
AddUntilStep("message postesd", () => !channel.Messages.Any(m => m is LocalMessage));
|
||||||
|
|
||||||
AddStep("post /help command", () => channelManager.PostCommand("help", channel));
|
AddStep("post /help command", () => channelManager.PostCommand("help", channel));
|
||||||
AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
|
AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
|
||||||
@ -106,6 +122,28 @@ namespace osu.Game.Tests.Chat
|
|||||||
AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id);
|
AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSilencedUsersAreRemoved()
|
||||||
|
{
|
||||||
|
Channel channel = null;
|
||||||
|
|
||||||
|
AddStep("join channel and select it", () =>
|
||||||
|
{
|
||||||
|
channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public));
|
||||||
|
channelManager.CurrentChannel.Value = channel;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("post message", () => channelManager.PostMessage("Definitely something bad"));
|
||||||
|
|
||||||
|
AddStep("mark user as silenced and send ack request", () =>
|
||||||
|
{
|
||||||
|
silencedUserIds.Add(API.LocalUser.Value.OnlineID);
|
||||||
|
channelManager.SendAck();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("channel has no more messages", () => channel.Messages, () => Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
private void handlePostMessageRequest(PostMessageRequest request)
|
private void handlePostMessageRequest(PostMessageRequest request)
|
||||||
{
|
{
|
||||||
var message = new Message(++currentMessageId)
|
var message = new Message(++currentMessageId)
|
||||||
@ -115,7 +153,8 @@ namespace osu.Game.Tests.Chat
|
|||||||
Content = request.Message.Content,
|
Content = request.Message.Content,
|
||||||
Links = request.Message.Links,
|
Links = request.Message.Links,
|
||||||
Timestamp = request.Message.Timestamp,
|
Timestamp = request.Message.Timestamp,
|
||||||
Sender = request.Message.Sender
|
Sender = request.Message.Sender,
|
||||||
|
Uuid = request.Message.Uuid
|
||||||
};
|
};
|
||||||
|
|
||||||
sentMessages.Add(message);
|
sentMessages.Add(message);
|
||||||
|
@ -46,6 +46,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
availableChannels.Add(new Channel { Name = "#english" });
|
availableChannels.Add(new Channel { Name = "#english" });
|
||||||
availableChannels.Add(new Channel { Name = "#japanese" });
|
availableChannels.Add(new Channel { Name = "#japanese" });
|
||||||
Dependencies.Cache(chatManager);
|
Dependencies.Cache(chatManager);
|
||||||
|
|
||||||
|
Add(chatManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
|
@ -40,8 +40,10 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
private ChannelManager channelManager;
|
private ChannelManager channelManager;
|
||||||
|
|
||||||
private readonly APIUser testUser = new APIUser { Username = "test user", Id = 5071479 };
|
private readonly APIUser testUser = new APIUser { Username = "test user", Id = 5071479 };
|
||||||
|
private readonly APIUser testUser1 = new APIUser { Username = "test user", Id = 5071480 };
|
||||||
|
|
||||||
private Channel[] testChannels;
|
private Channel[] testChannels;
|
||||||
|
private Message[] initialMessages;
|
||||||
|
|
||||||
private Channel testChannel1 => testChannels[0];
|
private Channel testChannel1 => testChannels[0];
|
||||||
private Channel testChannel2 => testChannels[1];
|
private Channel testChannel2 => testChannels[1];
|
||||||
@ -49,10 +51,14 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; } = null!;
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
|
private int currentMessageId;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
|
currentMessageId = 0;
|
||||||
testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
|
testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
|
||||||
|
initialMessages = testChannels.SelectMany(createChannelMessages).ToArray();
|
||||||
|
|
||||||
Child = new DependencyProvidingContainer
|
Child = new DependencyProvidingContainer
|
||||||
{
|
{
|
||||||
@ -99,7 +105,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GetMessagesRequest getMessages:
|
case GetMessagesRequest getMessages:
|
||||||
getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel));
|
getMessages.TriggerSuccess(initialMessages.ToList());
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GetUserRequest getUser:
|
case GetUserRequest getUser:
|
||||||
@ -495,6 +501,35 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
waitForChannel1Visible();
|
waitForChannel1Visible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRemoveMessages()
|
||||||
|
{
|
||||||
|
AddStep("Show overlay with channel", () =>
|
||||||
|
{
|
||||||
|
chatOverlay.Show();
|
||||||
|
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
||||||
|
waitForChannel1Visible();
|
||||||
|
|
||||||
|
AddStep("Send message from another user", () =>
|
||||||
|
{
|
||||||
|
testChannel1.AddNewMessages(new Message
|
||||||
|
{
|
||||||
|
ChannelId = testChannel1.Id,
|
||||||
|
Content = "Message from another user",
|
||||||
|
Timestamp = DateTimeOffset.Now,
|
||||||
|
Sender = testUser1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Remove messages from other user", () =>
|
||||||
|
{
|
||||||
|
testChannel1.RemoveMessagesFromUser(testUser.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void joinTestChannel(int i)
|
private void joinTestChannel(int i)
|
||||||
{
|
{
|
||||||
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
||||||
@ -546,7 +581,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
private List<Message> createChannelMessages(Channel channel)
|
private List<Message> createChannelMessages(Channel channel)
|
||||||
{
|
{
|
||||||
var message = new Message
|
var message = new Message(currentMessageId++)
|
||||||
{
|
{
|
||||||
ChannelId = channel.Id,
|
ChannelId = channel.Id,
|
||||||
Content = $"Hello, this is a message in {channel.Name}",
|
Content = $"Hello, this is a message in {channel.Name}",
|
||||||
|
@ -56,7 +56,9 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
{
|
{
|
||||||
Add(channelManager = new ChannelManager(parent.Get<IAPIProvider>()));
|
var api = parent.Get<IAPIProvider>();
|
||||||
|
|
||||||
|
Add(channelManager = new ChannelManager(api));
|
||||||
|
|
||||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Tournament.Components
|
|||||||
|
|
||||||
if (manager == null)
|
if (manager == null)
|
||||||
{
|
{
|
||||||
AddInternal(manager = new ChannelManager(api) { HighPollRate = { Value = true } });
|
AddInternal(manager = new ChannelManager(api));
|
||||||
Channel.BindTo(manager.CurrentChannel);
|
Channel.BindTo(manager.CurrentChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ using osu.Framework.Logging;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Notifications;
|
||||||
|
using osu.Game.Online.Notifications.WebSocket;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.API
|
||||||
@ -299,6 +301,9 @@ namespace osu.Game.Online.API
|
|||||||
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
|
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
|
||||||
new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
|
new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
|
||||||
|
|
||||||
|
public NotificationsClientConnector GetNotificationsConnector() =>
|
||||||
|
new WebSocketNotificationsClientConnector(this);
|
||||||
|
|
||||||
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
||||||
{
|
{
|
||||||
Debug.Assert(State.Value == APIState.Offline);
|
Debug.Assert(State.Value == APIState.Offline);
|
||||||
|
@ -9,6 +9,8 @@ using System.Threading.Tasks;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Notifications;
|
||||||
|
using osu.Game.Tests;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.API
|
||||||
@ -115,6 +117,8 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
|
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
|
||||||
|
|
||||||
|
public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this);
|
||||||
|
|
||||||
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
||||||
{
|
{
|
||||||
Thread.Sleep(200);
|
Thread.Sleep(200);
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Notifications;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.API
|
||||||
@ -112,6 +113,11 @@ namespace osu.Game.Online.API
|
|||||||
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
|
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
|
||||||
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
|
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new <see cref="NotificationsClientConnector"/>.
|
||||||
|
/// </summary>
|
||||||
|
NotificationsClientConnector GetNotificationsConnector();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new user account. This is a blocking operation.
|
/// Create a new user account. This is a blocking operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
39
osu.Game/Online/API/Requests/ChatAckRequest.cs
Normal file
39
osu.Game/Online/API/Requests/ChatAckRequest.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 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.Net.Http;
|
||||||
|
using osu.Framework.IO.Network;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A request which should be sent occasionally while interested in chat and online state.
|
||||||
|
///
|
||||||
|
/// This will:
|
||||||
|
/// - Mark the user as "online" (for 10 minutes since the last invocation).
|
||||||
|
/// - Return any silences since the last invocation (if either <see cref="SinceMessageId"/> or <see cref="SinceSilenceId"/> is not null).
|
||||||
|
///
|
||||||
|
/// For silence handling, a <see cref="SinceMessageId"/> should be provided as soon as a message is received by the client.
|
||||||
|
/// From that point forward, <see cref="SinceSilenceId"/> should be preferred after the first <see cref="ChatSilence"/>
|
||||||
|
/// arrives in a response from the ack request. Specifying both parameters will prioritise the latter.
|
||||||
|
/// </summary>
|
||||||
|
public class ChatAckRequest : APIRequest<ChatAckResponse>
|
||||||
|
{
|
||||||
|
public long? SinceMessageId;
|
||||||
|
public uint? SinceSilenceId;
|
||||||
|
|
||||||
|
protected override WebRequest CreateWebRequest()
|
||||||
|
{
|
||||||
|
var req = base.CreateWebRequest();
|
||||||
|
req.Method = HttpMethod.Post;
|
||||||
|
if (SinceMessageId != null)
|
||||||
|
req.AddParameter(@"since", SinceMessageId.ToString());
|
||||||
|
if (SinceSilenceId != null)
|
||||||
|
req.AddParameter(@"history_since", SinceSilenceId.Value.ToString());
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string Target => "chat/ack";
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ namespace osu.Game.Online.API.Requests
|
|||||||
req.AddParameter(@"target_id", user.Id.ToString());
|
req.AddParameter(@"target_id", user.Id.ToString());
|
||||||
req.AddParameter(@"message", message.Content);
|
req.AddParameter(@"message", message.Content);
|
||||||
req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant());
|
req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant());
|
||||||
|
req.AddParameter(@"uuid", message.Uuid);
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
osu.Game/Online/API/Requests/GetChannelRequest.cs
Normal file
19
osu.Game/Online/API/Requests/GetChannelRequest.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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 osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests
|
||||||
|
{
|
||||||
|
public class GetChannelRequest : APIRequest<GetChannelResponse>
|
||||||
|
{
|
||||||
|
private readonly long channelId;
|
||||||
|
|
||||||
|
public GetChannelRequest(long channelId)
|
||||||
|
{
|
||||||
|
this.channelId = channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string Target => $"chat/channels/{channelId}";
|
||||||
|
}
|
||||||
|
}
|
12
osu.Game/Online/API/Requests/GetNotificationsRequest.cs
Normal file
12
osu.Game/Online/API/Requests/GetNotificationsRequest.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 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 osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests
|
||||||
|
{
|
||||||
|
public class GetNotificationsRequest : APIRequest<APINotificationsBundle>
|
||||||
|
{
|
||||||
|
protected override string Target => @"notifications";
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ namespace osu.Game.Online.API.Requests
|
|||||||
req.Method = HttpMethod.Post;
|
req.Method = HttpMethod.Post;
|
||||||
req.AddParameter(@"is_action", Message.IsAction.ToString().ToLowerInvariant());
|
req.AddParameter(@"is_action", Message.IsAction.ToString().ToLowerInvariant());
|
||||||
req.AddParameter(@"message", Message.Content);
|
req.AddParameter(@"message", Message.Content);
|
||||||
|
req.AddParameter(@"uuid", Message.Uuid);
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
37
osu.Game/Online/API/Requests/Responses/APINotification.cs
Normal file
37
osu.Game/Online/API/Requests/Responses/APINotification.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// 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;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
|
{
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class APINotification
|
||||||
|
{
|
||||||
|
[JsonProperty(@"id")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"name")]
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty(@"created_at")]
|
||||||
|
public DateTimeOffset? CreatedAt { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"object_type")]
|
||||||
|
public string ObjectType { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty(@"object_id")]
|
||||||
|
public string ObjectId { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty(@"source_user_id")]
|
||||||
|
public long? SourceUserId { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"is_read")]
|
||||||
|
public bool IsRead { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"details")]
|
||||||
|
public JObject? Details { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
|
{
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class APINotificationsBundle
|
||||||
|
{
|
||||||
|
[JsonProperty(@"has_more")]
|
||||||
|
public bool HasMore { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"notifications")]
|
||||||
|
public APINotification[] Notifications { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty(@"notification_endpoint")]
|
||||||
|
public string Endpoint { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
15
osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs
Normal file
15
osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
|
{
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class ChatAckResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("silences")]
|
||||||
|
public List<ChatSilence> Silences { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
17
osu.Game/Online/API/Requests/Responses/ChatSilence.cs
Normal file
17
osu.Game/Online/API/Requests/Responses/ChatSilence.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
|
{
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class ChatSilence
|
||||||
|
{
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public uint Id { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("user_id")]
|
||||||
|
public int UserId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
19
osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs
Normal file
19
osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
|
{
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class GetChannelResponse
|
||||||
|
{
|
||||||
|
[JsonProperty(@"channel")]
|
||||||
|
public Channel Channel { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty(@"users")]
|
||||||
|
public List<APIUser> Users { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
@ -134,6 +134,14 @@ namespace osu.Game.Online.Chat
|
|||||||
/// <param name="messages"></param>
|
/// <param name="messages"></param>
|
||||||
public void AddNewMessages(params Message[] messages)
|
public void AddNewMessages(params Message[] messages)
|
||||||
{
|
{
|
||||||
|
foreach (var m in messages)
|
||||||
|
{
|
||||||
|
LocalEchoMessage localEcho = pendingMessages.FirstOrDefault(local => local.Uuid == m.Uuid);
|
||||||
|
|
||||||
|
if (localEcho != null)
|
||||||
|
ReplaceMessage(localEcho, m);
|
||||||
|
}
|
||||||
|
|
||||||
messages = messages.Except(Messages).ToArray();
|
messages = messages.Except(Messages).ToArray();
|
||||||
|
|
||||||
if (messages.Length == 0) return;
|
if (messages.Length == 0) return;
|
||||||
@ -149,6 +157,20 @@ namespace osu.Game.Online.Chat
|
|||||||
NewMessagesArrived?.Invoke(messages);
|
NewMessagesArrived?.Invoke(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RemoveMessagesFromUser(int userId)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Messages.Count; i++)
|
||||||
|
{
|
||||||
|
var message = Messages[i];
|
||||||
|
|
||||||
|
if (message.SenderId == userId)
|
||||||
|
{
|
||||||
|
Messages.RemoveAt(i--);
|
||||||
|
MessageRemoved?.Invoke(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replace or remove a message from the channel.
|
/// Replace or remove a message from the channel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -171,6 +193,10 @@ namespace osu.Game.Online.Chat
|
|||||||
throw new InvalidOperationException("Attempted to add the same message again");
|
throw new InvalidOperationException("Attempted to add the same message again");
|
||||||
|
|
||||||
Messages.Add(final);
|
Messages.Add(final);
|
||||||
|
|
||||||
|
if (final.Id > LastMessageId)
|
||||||
|
LastMessageId = final.Id;
|
||||||
|
|
||||||
PendingMessageResolved?.Invoke(echo, final);
|
PendingMessageResolved?.Invoke(echo, final);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,16 +6,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Input;
|
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Notifications;
|
||||||
using osu.Game.Overlays.Chat.Listing;
|
using osu.Game.Overlays.Chat.Listing;
|
||||||
|
|
||||||
namespace osu.Game.Online.Chat
|
namespace osu.Game.Online.Chat
|
||||||
@ -23,7 +24,7 @@ namespace osu.Game.Online.Chat
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages everything channel related
|
/// Manages everything channel related
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChannelManager : PollingComponent, IChannelPostTarget
|
public class ChannelManager : CompositeComponent, IChannelPostTarget
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The channels the player joins on startup
|
/// The channels the player joins on startup
|
||||||
@ -64,44 +65,50 @@ namespace osu.Game.Online.Chat
|
|||||||
public IBindableList<Channel> AvailableChannels => availableChannels;
|
public IBindableList<Channel> AvailableChannels => availableChannels;
|
||||||
|
|
||||||
private readonly IAPIProvider api;
|
private readonly IAPIProvider api;
|
||||||
|
private readonly NotificationsClientConnector connector;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private UserLookupCache users { get; set; }
|
private UserLookupCache users { get; set; }
|
||||||
|
|
||||||
public readonly BindableBool HighPollRate = new BindableBool();
|
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||||
|
private bool channelsInitialised;
|
||||||
|
private ScheduledDelegate scheduledAck;
|
||||||
|
|
||||||
private readonly IBindable<bool> isIdle = new BindableBool();
|
private long? lastSilenceMessageId;
|
||||||
|
private uint? lastSilenceId;
|
||||||
|
|
||||||
public ChannelManager(IAPIProvider api)
|
public ChannelManager(IAPIProvider api)
|
||||||
{
|
{
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
|
||||||
|
connector = api.GetNotificationsConnector();
|
||||||
|
|
||||||
CurrentChannel.ValueChanged += currentChannelChanged;
|
CurrentChannel.ValueChanged += currentChannelChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(permitNulls: true)]
|
[BackgroundDependencyLoader]
|
||||||
private void load(IdleTracker idleTracker)
|
private void load()
|
||||||
{
|
{
|
||||||
HighPollRate.BindValueChanged(updatePollRate);
|
connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
|
||||||
isIdle.BindValueChanged(updatePollRate, true);
|
|
||||||
|
|
||||||
if (idleTracker != null)
|
connector.ChannelParted += ch => Schedule(() => LeaveChannel(getChannel(ch)));
|
||||||
isIdle.BindTo(idleTracker.IsIdle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePollRate(ValueChangedEvent<bool> valueChangedEvent)
|
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
|
||||||
{
|
|
||||||
// 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.
|
|
||||||
int millisecondsBetweenPolls = HighPollRate.Value ? 1000 : 60000;
|
|
||||||
|
|
||||||
if (isIdle.Value)
|
connector.PresenceReceived += () => Schedule(() =>
|
||||||
millisecondsBetweenPolls *= 10;
|
|
||||||
|
|
||||||
if (TimeBetweenPolls.Value != millisecondsBetweenPolls)
|
|
||||||
{
|
{
|
||||||
TimeBetweenPolls.Value = millisecondsBetweenPolls;
|
if (!channelsInitialised)
|
||||||
Logger.Log($"Chat is now polling every {TimeBetweenPolls.Value} ms");
|
{
|
||||||
}
|
channelsInitialised = true;
|
||||||
|
// we want this to run after the first presence so we can see if the user is in any channels already.
|
||||||
|
initializeChannels();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connector.Start();
|
||||||
|
|
||||||
|
apiState.BindTo(api.State);
|
||||||
|
apiState.BindValueChanged(_ => SendAck(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -181,7 +188,8 @@ namespace osu.Game.Online.Chat
|
|||||||
Timestamp = DateTimeOffset.Now,
|
Timestamp = DateTimeOffset.Now,
|
||||||
ChannelId = target.Id,
|
ChannelId = target.Id,
|
||||||
IsAction = isAction,
|
IsAction = isAction,
|
||||||
Content = text
|
Content = text,
|
||||||
|
Uuid = Guid.NewGuid().ToString()
|
||||||
};
|
};
|
||||||
|
|
||||||
target.AddLocalEcho(message);
|
target.AddLocalEcho(message);
|
||||||
@ -191,13 +199,7 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(target.Users.First(), message);
|
var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(target.Users.First(), message);
|
||||||
|
|
||||||
createNewPrivateMessageRequest.Success += createRes =>
|
createNewPrivateMessageRequest.Success += _ => dequeueAndRun();
|
||||||
{
|
|
||||||
target.Id = createRes.ChannelID;
|
|
||||||
target.ReplaceMessage(message, createRes.Message);
|
|
||||||
dequeueAndRun();
|
|
||||||
};
|
|
||||||
|
|
||||||
createNewPrivateMessageRequest.Failure += exception =>
|
createNewPrivateMessageRequest.Failure += exception =>
|
||||||
{
|
{
|
||||||
handlePostException(exception);
|
handlePostException(exception);
|
||||||
@ -211,12 +213,7 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
var req = new PostMessageRequest(message);
|
var req = new PostMessageRequest(message);
|
||||||
|
|
||||||
req.Success += m =>
|
req.Success += m => dequeueAndRun();
|
||||||
{
|
|
||||||
target.ReplaceMessage(message, m);
|
|
||||||
dequeueAndRun();
|
|
||||||
};
|
|
||||||
|
|
||||||
req.Failure += exception =>
|
req.Failure += exception =>
|
||||||
{
|
{
|
||||||
handlePostException(exception);
|
handlePostException(exception);
|
||||||
@ -328,12 +325,14 @@ namespace osu.Game.Online.Chat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleChannelMessages(IEnumerable<Message> messages)
|
private void addMessages(List<Message> messages)
|
||||||
{
|
{
|
||||||
var channels = JoinedChannels.ToList();
|
var channels = JoinedChannels.ToList();
|
||||||
|
|
||||||
foreach (var group in messages.GroupBy(m => m.ChannelId))
|
foreach (var group in messages.GroupBy(m => m.ChannelId))
|
||||||
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
|
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
|
||||||
|
|
||||||
|
lastSilenceMessageId ??= messages.LastOrDefault()?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeChannels()
|
private void initializeChannels()
|
||||||
@ -376,13 +375,51 @@ namespace osu.Game.Online.Chat
|
|||||||
var fetchInitialMsgReq = new GetMessagesRequest(channel);
|
var fetchInitialMsgReq = new GetMessagesRequest(channel);
|
||||||
fetchInitialMsgReq.Success += messages =>
|
fetchInitialMsgReq.Success += messages =>
|
||||||
{
|
{
|
||||||
handleChannelMessages(messages);
|
addMessages(messages);
|
||||||
channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none.
|
channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none.
|
||||||
};
|
};
|
||||||
|
|
||||||
api.Queue(fetchInitialMsgReq);
|
api.Queue(fetchInitialMsgReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends an acknowledgement request to the API.
|
||||||
|
/// This marks the user as online to receive messages from public channels, while also returning a list of silenced users.
|
||||||
|
/// It needs to be called at least once every 10 minutes to remain visibly marked as online.
|
||||||
|
/// </summary>
|
||||||
|
public void SendAck()
|
||||||
|
{
|
||||||
|
if (apiState.Value != APIState.Online)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var req = new ChatAckRequest
|
||||||
|
{
|
||||||
|
SinceMessageId = lastSilenceMessageId,
|
||||||
|
SinceSilenceId = lastSilenceId
|
||||||
|
};
|
||||||
|
|
||||||
|
req.Failure += _ => scheduleNextRequest();
|
||||||
|
req.Success += ack =>
|
||||||
|
{
|
||||||
|
foreach (var silence in ack.Silences)
|
||||||
|
{
|
||||||
|
foreach (var channel in JoinedChannels)
|
||||||
|
channel.RemoveMessagesFromUser(silence.UserId);
|
||||||
|
lastSilenceId = Math.Max(lastSilenceId ?? 0, silence.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNextRequest();
|
||||||
|
};
|
||||||
|
|
||||||
|
api.Queue(req);
|
||||||
|
|
||||||
|
void scheduleNextRequest()
|
||||||
|
{
|
||||||
|
scheduledAck?.Cancel();
|
||||||
|
scheduledAck = Scheduler.AddDelayed(SendAck, 60000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Find an existing channel instance for the provided channel. Lookup is performed basd on ID.
|
/// 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.
|
/// The provided channel may be used if an existing instance is not found.
|
||||||
@ -395,7 +432,13 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
Channel found = null;
|
Channel found = null;
|
||||||
|
|
||||||
bool lookupCondition(Channel ch) => lookup.Id > 0 ? ch.Id == lookup.Id : lookup.Name == ch.Name;
|
bool lookupCondition(Channel ch)
|
||||||
|
{
|
||||||
|
if (ch.Id > 0 && lookup.Id > 0)
|
||||||
|
return ch.Id == lookup.Id;
|
||||||
|
|
||||||
|
return ch.Name == lookup.Name;
|
||||||
|
}
|
||||||
|
|
||||||
var available = AvailableChannels.FirstOrDefault(lookupCondition);
|
var available = AvailableChannels.FirstOrDefault(lookupCondition);
|
||||||
if (available != null)
|
if (available != null)
|
||||||
@ -415,6 +458,12 @@ namespace osu.Game.Online.Chat
|
|||||||
if (foundSelf != null)
|
if (foundSelf != null)
|
||||||
found.Users.Remove(foundSelf);
|
found.Users.Remove(foundSelf);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
found.Id = lookup.Id;
|
||||||
|
found.Name = lookup.Name;
|
||||||
|
found.LastMessageId = Math.Max(found.LastMessageId ?? 0, lookup.LastMessageId ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
if (joined == null && addToJoined) joinedChannels.Add(found);
|
if (joined == null && addToJoined) joinedChannels.Add(found);
|
||||||
if (available == null && addToAvailable) availableChannels.Add(found);
|
if (available == null && addToAvailable) availableChannels.Add(found);
|
||||||
@ -464,7 +513,7 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
channel.Id = resChannel.ChannelID.Value;
|
channel.Id = resChannel.ChannelID.Value;
|
||||||
|
|
||||||
handleChannelMessages(resChannel.RecentMessages);
|
addMessages(resChannel.RecentMessages);
|
||||||
channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none.
|
channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none.
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -574,57 +623,6 @@ namespace osu.Game.Online.Chat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long lastMessageId;
|
|
||||||
|
|
||||||
private bool channelsInitialised;
|
|
||||||
|
|
||||||
protected override Task Poll()
|
|
||||||
{
|
|
||||||
if (!api.IsLoggedIn)
|
|
||||||
return base.Poll();
|
|
||||||
|
|
||||||
var fetchReq = new GetUpdatesRequest(lastMessageId);
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
fetchReq.Success += updates =>
|
|
||||||
{
|
|
||||||
if (updates?.Presence != null)
|
|
||||||
{
|
|
||||||
foreach (var channel in updates.Presence)
|
|
||||||
{
|
|
||||||
// we received this from the server so should mark the channel already joined.
|
|
||||||
channel.Joined.Value = true;
|
|
||||||
joinChannel(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
//todo: handle left channels
|
|
||||||
|
|
||||||
handleChannelMessages(updates.Messages);
|
|
||||||
|
|
||||||
foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
|
|
||||||
JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
|
|
||||||
|
|
||||||
lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
tcs.SetResult(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchReq.Failure += _ => tcs.SetResult(false);
|
|
||||||
|
|
||||||
api.Queue(fetchReq);
|
|
||||||
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks the <paramref name="channel"/> as read
|
/// Marks the <paramref name="channel"/> as read
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -646,6 +644,12 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
api.Queue(req);
|
api.Queue(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
connector?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -30,6 +30,19 @@ namespace osu.Game.Online.Chat
|
|||||||
[JsonProperty(@"sender")]
|
[JsonProperty(@"sender")]
|
||||||
public APIUser Sender;
|
public APIUser Sender;
|
||||||
|
|
||||||
|
[JsonProperty(@"sender_id")]
|
||||||
|
public int SenderId
|
||||||
|
{
|
||||||
|
get => Sender?.Id ?? 0;
|
||||||
|
set => Sender = new APIUser { Id = value };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unique identifier for this message. Sent to and from osu!web to use for deduplication.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(@"uuid")]
|
||||||
|
public string Uuid { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Message()
|
public Message()
|
||||||
{
|
{
|
||||||
|
@ -49,6 +49,9 @@ namespace osu.Game.Online
|
|||||||
this.api = api;
|
this.api = api;
|
||||||
this.versionHash = versionHash;
|
this.versionHash = versionHash;
|
||||||
this.preferMessagePack = preferMessagePack;
|
this.preferMessagePack = preferMessagePack;
|
||||||
|
|
||||||
|
// Automatically start these connections.
|
||||||
|
Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken)
|
protected override Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken)
|
||||||
|
76
osu.Game/Online/Notifications/NotificationsClient.cs
Normal file
76
osu.Game/Online/Notifications/NotificationsClient.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Notifications
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An abstract client which receives notification-related events (chat/notifications).
|
||||||
|
/// </summary>
|
||||||
|
public abstract class NotificationsClient : PersistentEndpointClient
|
||||||
|
{
|
||||||
|
public Action<Channel>? ChannelJoined;
|
||||||
|
public Action<Channel>? ChannelParted;
|
||||||
|
public Action<List<Message>>? NewMessages;
|
||||||
|
public Action? PresenceReceived;
|
||||||
|
|
||||||
|
protected readonly IAPIProvider API;
|
||||||
|
|
||||||
|
private long lastMessageId;
|
||||||
|
|
||||||
|
protected NotificationsClient(IAPIProvider api)
|
||||||
|
{
|
||||||
|
API = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ConnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
API.Queue(CreateFetchMessagesRequest(0));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected APIRequest CreateFetchMessagesRequest(long? lastMessageId = null)
|
||||||
|
{
|
||||||
|
var fetchReq = new GetUpdatesRequest(lastMessageId ?? this.lastMessageId);
|
||||||
|
|
||||||
|
fetchReq.Success += updates =>
|
||||||
|
{
|
||||||
|
if (updates?.Presence != null)
|
||||||
|
{
|
||||||
|
foreach (var channel in updates.Presence)
|
||||||
|
HandleChannelJoined(channel);
|
||||||
|
|
||||||
|
//todo: handle left channels
|
||||||
|
|
||||||
|
HandleMessages(updates.Messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
PresenceReceived?.Invoke();
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetchReq;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void HandleChannelJoined(Channel channel)
|
||||||
|
{
|
||||||
|
channel.Joined.Value = true;
|
||||||
|
ChannelJoined?.Invoke(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void HandleChannelParted(Channel channel) => ChannelParted?.Invoke(channel);
|
||||||
|
|
||||||
|
protected void HandleMessages(List<Message> messages)
|
||||||
|
{
|
||||||
|
NewMessages?.Invoke(messages);
|
||||||
|
lastMessageId = Math.Max(lastMessageId, messages.LastOrDefault()?.Id ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Notifications
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An abstract connector or <see cref="NotificationsClient"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class NotificationsClientConnector : PersistentEndpointClientConnector
|
||||||
|
{
|
||||||
|
public event Action<Channel>? ChannelJoined;
|
||||||
|
public event Action<Channel>? ChannelParted;
|
||||||
|
public event Action<List<Message>>? NewMessages;
|
||||||
|
public event Action? PresenceReceived;
|
||||||
|
|
||||||
|
protected NotificationsClientConnector(IAPIProvider api)
|
||||||
|
: base(api)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed override async Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var client = await BuildNotificationClientAsync(cancellationToken);
|
||||||
|
|
||||||
|
client.ChannelJoined = c => ChannelJoined?.Invoke(c);
|
||||||
|
client.ChannelParted = c => ChannelParted?.Invoke(c);
|
||||||
|
client.NewMessages = m => NewMessages?.Invoke(m);
|
||||||
|
client.PresenceReceived = () => PresenceReceived?.Invoke();
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<NotificationsClient> BuildNotificationClientAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
19
osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs
Normal file
19
osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Notifications.WebSocket
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A websocket message notifying the server that the client no longer wants to receive chat messages.
|
||||||
|
/// </summary>
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class EndChatRequest : SocketMessage
|
||||||
|
{
|
||||||
|
public EndChatRequest()
|
||||||
|
{
|
||||||
|
Event = @"chat.end";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Notifications.WebSocket
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A websocket message sent from the server when new messages arrive.
|
||||||
|
/// </summary>
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class NewChatMessageData
|
||||||
|
{
|
||||||
|
[JsonProperty(@"messages")]
|
||||||
|
public List<Message> Messages { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty(@"users")]
|
||||||
|
private List<APIUser> users { get; set; } = null!;
|
||||||
|
|
||||||
|
[OnDeserialized]
|
||||||
|
private void onDeserialised(StreamingContext context)
|
||||||
|
{
|
||||||
|
foreach (var m in Messages)
|
||||||
|
m.Sender = users.Single(u => u.OnlineID == m.SenderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
osu.Game/Online/Notifications/WebSocket/SocketMessage.cs
Normal file
24
osu.Game/Online/Notifications/WebSocket/SocketMessage.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Notifications.WebSocket
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A websocket message, sent either from the client or server.
|
||||||
|
/// </summary>
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class SocketMessage
|
||||||
|
{
|
||||||
|
[JsonProperty(@"event")]
|
||||||
|
public string Event { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty(@"data")]
|
||||||
|
public JObject? Data { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"error")]
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
}
|
19
osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs
Normal file
19
osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Notifications.WebSocket
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A websocket message notifying the server that the client wants to receive chat messages.
|
||||||
|
/// </summary>
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class StartChatRequest : SocketMessage
|
||||||
|
{
|
||||||
|
public StartChatRequest()
|
||||||
|
{
|
||||||
|
Event = @"chat.start";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,179 @@
|
|||||||
|
// 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;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Notifications.WebSocket
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A notifications client which receives events via a websocket.
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketNotificationsClient : NotificationsClient
|
||||||
|
{
|
||||||
|
private readonly ClientWebSocket socket;
|
||||||
|
private readonly string endpoint;
|
||||||
|
private readonly ConcurrentDictionary<long, Channel> channelsMap = new ConcurrentDictionary<long, Channel>();
|
||||||
|
|
||||||
|
public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api)
|
||||||
|
: base(api)
|
||||||
|
{
|
||||||
|
this.socket = socket;
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ConnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
|
||||||
|
await sendMessage(new StartChatRequest(), CancellationToken.None);
|
||||||
|
|
||||||
|
runReadLoop(cancellationToken);
|
||||||
|
|
||||||
|
await base.ConnectAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
StringBuilder messageResult = new StringBuilder();
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken);
|
||||||
|
|
||||||
|
switch (result.MessageType)
|
||||||
|
{
|
||||||
|
case WebSocketMessageType.Text:
|
||||||
|
messageResult.Append(Encoding.UTF8.GetString(buffer[..result.Count]));
|
||||||
|
|
||||||
|
if (result.EndOfMessage)
|
||||||
|
{
|
||||||
|
SocketMessage? message = JsonConvert.DeserializeObject<SocketMessage>(messageResult.ToString());
|
||||||
|
messageResult.Clear();
|
||||||
|
|
||||||
|
Debug.Assert(message != null);
|
||||||
|
|
||||||
|
if (message.Error != null)
|
||||||
|
{
|
||||||
|
Logger.Log($"{GetType().ReadableName()} error: {message.Error}", LoggingTarget.Network);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await onMessageReceivedAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WebSocketMessageType.Binary:
|
||||||
|
throw new NotImplementedException("Binary message type not supported.");
|
||||||
|
|
||||||
|
case WebSocketMessageType.Close:
|
||||||
|
throw new Exception("Connection closed by remote host.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await InvokeClosed(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
private async Task closeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, @"Disconnecting", CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Closure can fail if the connection is aborted. Don't really care since it's disposed anyway.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (socket.State != WebSocketState.Open)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task onMessageReceivedAsync(SocketMessage message)
|
||||||
|
{
|
||||||
|
switch (message.Event)
|
||||||
|
{
|
||||||
|
case @"chat.channel.join":
|
||||||
|
Debug.Assert(message.Data != null);
|
||||||
|
|
||||||
|
Channel? joinedChannel = JsonConvert.DeserializeObject<Channel>(message.Data.ToString());
|
||||||
|
Debug.Assert(joinedChannel != null);
|
||||||
|
|
||||||
|
HandleChannelJoined(joinedChannel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case @"chat.channel.part":
|
||||||
|
Debug.Assert(message.Data != null);
|
||||||
|
|
||||||
|
Channel? partedChannel = JsonConvert.DeserializeObject<Channel>(message.Data.ToString());
|
||||||
|
Debug.Assert(partedChannel != null);
|
||||||
|
|
||||||
|
HandleChannelParted(partedChannel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case @"chat.message.new":
|
||||||
|
Debug.Assert(message.Data != null);
|
||||||
|
|
||||||
|
NewChatMessageData? messageData = JsonConvert.DeserializeObject<NewChatMessageData>(message.Data.ToString());
|
||||||
|
Debug.Assert(messageData != null);
|
||||||
|
|
||||||
|
foreach (var msg in messageData.Messages)
|
||||||
|
HandleChannelJoined(await getChannel(msg.ChannelId));
|
||||||
|
|
||||||
|
HandleMessages(messageData.Messages);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Channel> getChannel(long channelId)
|
||||||
|
{
|
||||||
|
if (channelsMap.TryGetValue(channelId, out Channel channel))
|
||||||
|
return channel;
|
||||||
|
|
||||||
|
var tsc = new TaskCompletionSource<Channel>();
|
||||||
|
var req = new GetChannelRequest(channelId);
|
||||||
|
|
||||||
|
req.Success += response =>
|
||||||
|
{
|
||||||
|
channelsMap[channelId] = response.Channel;
|
||||||
|
tsc.SetResult(response.Channel);
|
||||||
|
};
|
||||||
|
|
||||||
|
req.Failure += ex => tsc.SetException(ex);
|
||||||
|
|
||||||
|
API.Queue(req);
|
||||||
|
|
||||||
|
return await tsc.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await base.DisposeAsync();
|
||||||
|
await closeAsync();
|
||||||
|
socket.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
// 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.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Notifications.WebSocket
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A connector for <see cref="WebSocketNotificationsClient"/>s that receive events via a websocket.
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketNotificationsClientConnector : NotificationsClientConnector
|
||||||
|
{
|
||||||
|
private readonly IAPIProvider api;
|
||||||
|
|
||||||
|
public WebSocketNotificationsClientConnector(IAPIProvider api)
|
||||||
|
: base(api)
|
||||||
|
{
|
||||||
|
this.api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<NotificationsClient> BuildNotificationClientAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<string>();
|
||||||
|
|
||||||
|
var req = new GetNotificationsRequest();
|
||||||
|
req.Success += bundle => tcs.SetResult(bundle.Endpoint);
|
||||||
|
req.Failure += ex => tcs.SetException(ex);
|
||||||
|
api.Queue(req);
|
||||||
|
|
||||||
|
string endpoint = await tcs.Task;
|
||||||
|
|
||||||
|
ClientWebSocket socket = new ClientWebSocket();
|
||||||
|
socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}");
|
||||||
|
socket.Options.Proxy = WebRequest.DefaultWebProxy;
|
||||||
|
if (socket.Options.Proxy != null)
|
||||||
|
socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials;
|
||||||
|
|
||||||
|
return new WebSocketNotificationsClient(socket, endpoint, api);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,11 +23,13 @@ namespace osu.Game.Online
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public PersistentEndpointClient? CurrentConnection { get; private set; }
|
public PersistentEndpointClient? CurrentConnection { get; private set; }
|
||||||
|
|
||||||
|
protected readonly IAPIProvider API;
|
||||||
|
|
||||||
|
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||||
private readonly Bindable<bool> isConnected = new Bindable<bool>();
|
private readonly Bindable<bool> isConnected = new Bindable<bool>();
|
||||||
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
|
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
|
||||||
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
|
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
|
||||||
|
private bool started;
|
||||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new <see cref="PersistentEndpointClientConnector"/>.
|
/// Constructs a new <see cref="PersistentEndpointClientConnector"/>.
|
||||||
@ -35,8 +37,20 @@ namespace osu.Game.Online
|
|||||||
/// <param name="api"> An API provider used to react to connection state changes.</param>
|
/// <param name="api"> An API provider used to react to connection state changes.</param>
|
||||||
protected PersistentEndpointClientConnector(IAPIProvider api)
|
protected PersistentEndpointClientConnector(IAPIProvider api)
|
||||||
{
|
{
|
||||||
|
API = api;
|
||||||
apiState.BindTo(api.State);
|
apiState.BindTo(api.State);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to connect and begins processing messages from the remote endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (started)
|
||||||
|
return;
|
||||||
|
|
||||||
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
|
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
|
||||||
|
started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Reconnect()
|
public Task Reconnect()
|
||||||
|
@ -910,19 +910,6 @@ namespace osu.Game
|
|||||||
|
|
||||||
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
|
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
|
||||||
|
|
||||||
chatOverlay.State.BindValueChanged(_ => updateChatPollRate());
|
|
||||||
// Multiplayer modes need to increase poll rate temporarily.
|
|
||||||
API.Activity.BindValueChanged(_ => updateChatPollRate(), true);
|
|
||||||
|
|
||||||
void updateChatPollRate()
|
|
||||||
{
|
|
||||||
channelManager.HighPollRate.Value =
|
|
||||||
chatOverlay.State.Value == Visibility.Visible
|
|
||||||
|| API.Activity.Value is UserActivity.InLobby
|
|
||||||
|| API.Activity.Value is UserActivity.InMultiplayerGame
|
|
||||||
|| API.Activity.Value is UserActivity.SpectatingMultiplayerGame;
|
|
||||||
}
|
|
||||||
|
|
||||||
Add(difficultyRecommender);
|
Add(difficultyRecommender);
|
||||||
Add(externalLinkOpener = new ExternalLinkOpener());
|
Add(externalLinkOpener = new ExternalLinkOpener());
|
||||||
Add(new MusicKeyBindingHandler());
|
Add(new MusicKeyBindingHandler());
|
||||||
|
@ -82,21 +82,14 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
{
|
{
|
||||||
|
// Temporary until default skin has a valid hit lighting.
|
||||||
|
if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty();
|
||||||
|
|
||||||
if (base.GetDrawableComponent(lookup) is Drawable c)
|
if (base.GetDrawableComponent(lookup) is Drawable c)
|
||||||
return c;
|
return c;
|
||||||
|
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case SkinnableSprite.SpriteComponentLookup spriteLookup:
|
|
||||||
switch (spriteLookup.LookupName)
|
|
||||||
{
|
|
||||||
// Temporary until default skin has a valid hit lighting.
|
|
||||||
case @"lighting":
|
|
||||||
return Drawable.Empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case GlobalSkinComponentLookup globalLookup:
|
case GlobalSkinComponentLookup globalLookup:
|
||||||
switch (globalLookup.Lookup)
|
switch (globalLookup.Lookup)
|
||||||
{
|
{
|
||||||
|
@ -396,9 +396,6 @@ namespace osu.Game.Skinning
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case SkinnableSprite.SpriteComponentLookup sprite:
|
|
||||||
return this.GetAnimation(sprite.LookupName, false, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -158,6 +158,10 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
|
// This fallback is important for user skins which use SkinnableSprites.
|
||||||
|
case SkinnableSprite.SpriteComponentLookup sprite:
|
||||||
|
return this.GetAnimation(sprite.LookupName, false, false);
|
||||||
|
|
||||||
case GlobalSkinComponentLookup target:
|
case GlobalSkinComponentLookup target:
|
||||||
if (!DrawableComponentInfo.TryGetValue(target.Lookup, out var skinnableInfo))
|
if (!DrawableComponentInfo.TryGetValue(target.Lookup, out var skinnableInfo))
|
||||||
return null;
|
return null;
|
||||||
|
@ -60,21 +60,14 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
{
|
{
|
||||||
|
// Temporary until default skin has a valid hit lighting.
|
||||||
|
if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty();
|
||||||
|
|
||||||
if (base.GetDrawableComponent(lookup) is Drawable c)
|
if (base.GetDrawableComponent(lookup) is Drawable c)
|
||||||
return c;
|
return c;
|
||||||
|
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case SkinnableSprite.SpriteComponentLookup spriteLookup:
|
|
||||||
switch (spriteLookup.LookupName)
|
|
||||||
{
|
|
||||||
// Temporary until default skin has a valid hit lighting.
|
|
||||||
case @"lighting":
|
|
||||||
return Drawable.Empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case GlobalSkinComponentLookup target:
|
case GlobalSkinComponentLookup target:
|
||||||
switch (target.Lookup)
|
switch (target.Lookup)
|
||||||
{
|
{
|
||||||
|
35
osu.Game/Tests/PollingNotificationsClient.cs
Normal file
35
osu.Game/Tests/PollingNotificationsClient.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Notifications;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A notifications client which polls for new messages every second.
|
||||||
|
/// </summary>
|
||||||
|
public class PollingNotificationsClient : NotificationsClient
|
||||||
|
{
|
||||||
|
public PollingNotificationsClient(IAPIProvider api)
|
||||||
|
: base(api)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ConnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await API.PerformAsync(CreateFetchMessagesRequest());
|
||||||
|
await Task.Delay(1000, cancellationToken);
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
osu.Game/Tests/PollingNotificationsClientConnector.cs
Normal file
24
osu.Game/Tests/PollingNotificationsClientConnector.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.Notifications;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A connector for <see cref="PollingNotificationsClient"/>s that poll for new messages.
|
||||||
|
/// </summary>
|
||||||
|
public class PollingNotificationsClientConnector : NotificationsClientConnector
|
||||||
|
{
|
||||||
|
public PollingNotificationsClientConnector(IAPIProvider api)
|
||||||
|
: base(api)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<NotificationsClient> BuildNotificationClientAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.FromResult((NotificationsClient)new PollingNotificationsClient(API));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user