Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ namespace DCL.Chat.MessageBus
{
public class LiveKitChatMessagesBus : IChatMessagesBus
{
// Hard ceiling on the dedup stamps retained per period. A flood of distinct timestamps
// restarts the window instead of growing it, bounding the memory a sender can make this
// cache hold.
private const int MAX_DEDUP_ENTRIES = 2048;

private readonly IMessagePipesHub messagePipesHub;
private readonly IMessageDeduplication<double> messageDeduplication;
private readonly CancellationTokenSource cancellationTokenSource = new ();
Expand All @@ -52,7 +57,7 @@ public LiveKitChatMessagesBus(IMessagePipesHub messagePipesHub,
IRoomHub roomHub)
{
this.messagePipesHub = messagePipesHub;
messageDeduplication = new MessageDeduplication<double>();
messageDeduplication = new MessageDeduplication<double>(MAX_DEDUP_ENTRIES);
this.userBlockingCache = userBlockingCache;
this.identityCache = identityCache;
this.messageFactory = messageFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public class EmojiContainer
[field: SerializeField] internal EmojiPanelView emojiPanel { get; private set; }
[field: SerializeField] internal AudioClipConfig addEmojiAudio { get; private set; }
[field: SerializeField] internal AudioClipConfig openEmojiPanelAudio { get; private set; }

[field: SerializeField]
[field: Tooltip("Space kept between the input field's top edge and the emoji panel's bottom edge.")]
internal float emojiPanelGap { get; private set; } = 5f;
}

[field: SerializeField] public CustomInputField inputField { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using DCL.UI.CustomInputField;
using MVC;
using System;
using UnityEngine;

namespace DCL.Chat.ChatInput
{
Expand All @@ -13,6 +14,7 @@ public class EmojiPanelChatInputState : IndependentMVCState, IDisposable
private readonly EmojiPanelView emojiPanelView;
private readonly ChatInputView.EmojiContainer emojiContainer;
private readonly CustomInputField inputField;
private readonly RectTransform inputFieldRect;
private readonly ChatClickDetectionHandler clickDetectionHandler;

public EmojiPanelChatInputState(ChatInputView view, EmojiPanelPresenter emojiPanelPresenter, EmojiPanelView emojiPanelView)
Expand All @@ -23,6 +25,10 @@ public EmojiPanelChatInputState(ChatInputView view, EmojiPanelPresenter emojiPan

inputField = view.inputField;

// The field, not its container: the container keeps a fixed height while the field grows upwards as
// the text wraps.
inputFieldRect = (RectTransform)view.inputField.transform;

clickDetectionHandler = new ChatClickDetectionHandler(
emojiPanelView.transform,
emojiContainer.emojiPanelButton.transform);
Expand All @@ -32,7 +38,10 @@ public EmojiPanelChatInputState(ChatInputView view, EmojiPanelPresenter emojiPan

protected override void Activate()
{
emojiPanelView.ResetToDefaultPosition();
// Measured from the input field on every open: the chat panel is re-laid-out whenever the voice-chat
// panel changes height, and the field itself grows with wrapped text — neither of which a stored
// position would survive.
emojiPanelView.PositionAbove(inputFieldRect, emojiContainer.emojiPanelGap);
emojiPanelPresenter.SetPanelVisibility(true);
emojiContainer.emojiPanelButton.SetState(true);
emojiPanelPresenter.EmojiSelected += OnEmojiSelected;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using DCL.Chat.ChatReactions.Simulation.World;
using DCL.Chat.History;
using DCL.Diagnostics;
using DCL.FeatureFlags;
using DCL.Friends.UserBlocking;
using DCL.Multiplayer.Connections.DecentralandUrls;
using DCL.Multiplayer.Connections.Messaging.Hubs;
Expand Down Expand Up @@ -155,6 +156,15 @@ private static IReactionMessageBus CreateReactionBus(
ChatReactionsConfig reactionsConfig,
int maxValidEmojiIndex)
{
// Gated here rather than only in the UI: subscribing the pipes would let any
// co-located peer drive reaction decoding and retention on a client that cannot
// show reactions at all.
if (!FeatureFlagsConfiguration.Instance.IsEnabled(FeatureFlagsStrings.CHAT_REACTIONS_ENABLED))
{
ReportHub.Log(ReportCategory.CHAT_MESSAGES, "[ChatPlugin] Chat reactions disabled — using NullReactionMessageBus (no pipes subscribed)");
return new NullReactionMessageBus();
}

string serverEnv = environment switch
{
DecentralandEnvironment.Org => "prd",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using DCL.Multiplayer.Connections.Messaging.Pipe;
using DCL.Multiplayer.Deduplication;
using DCL.Utilities;
using DCL.Web3;
using DCL.Web3.Identities;
using Decentraland.Kernel.Comms.Rfc4;
using LiveKit.Proto;
Expand All @@ -27,15 +28,25 @@ public sealed class MultiplayerReactionMessageBus : IReactionMessageBus
// The effective bound is tightened to the actual atlas tile count via the constructor.
private const int MAX_VALID_EMOJI_INDEX = 4096;

// Message IDs are produced locally in one of two shapes: ChatUtils.GetId
// ("{42-char address}:{invariant double}", ~67 characters) or a 36-character GUID for
// system messages. 96 clears both with headroom while keeping an ID that arrives from
// the network bounded before it is used to build anything.
private const int MAX_MESSAGE_ID_LENGTH = 96;

// Hard ceiling on the dedup keys either cache retains. A flood of distinct keys restarts
// the window instead of growing it, bounding the memory a peer can make this bus hold.
private const int MAX_DEDUP_ENTRIES = 2048;

private readonly IMessagePipesHub messagePipesHub;
private readonly IUserBlockingCache userBlockingCache;
private readonly IWeb3IdentityCache identityCache;
private readonly string routingUser;
private readonly int maxValidEmojiIndex;
private readonly int situationalReceiveCountCap;
private readonly CancellationTokenSource cts = new ();
private readonly IMessageDeduplication<float> situationalDedup = new MessageDeduplication<float>();
private readonly IMessageDeduplication<string> chatReactionDedup = new MessageDeduplication<string>();
private readonly IMessageDeduplication<float> situationalDedup = new MessageDeduplication<float>(MAX_DEDUP_ENTRIES);
private readonly IMessageDeduplication<string> chatReactionDedup = new MessageDeduplication<string>(MAX_DEDUP_ENTRIES);
private readonly PerSenderRateLimiter situationalRateLimiter;
private readonly PerSenderRateLimiter chatReactionRateLimiter;

Expand Down Expand Up @@ -187,25 +198,25 @@ private void OnChatReactionReceived(ReceivedMessage<ChatReaction> receivedMessag
{
using (receivedMessage)
{
// For community messages relayed through the message-router, FromWalletId
// is the relay's identity, not the original sender. Fall back to Payload.Address
// only in that case. For direct messages (nearby/DM), always use the trusted
// transport-level FromWalletId to prevent identity spoofing.
// NOTE: Add server-stamped ForwardedFrom to the ChatReaction protocol (like Chat
// has) so the relay writes the verified sender identity, not the client.
string walletId = receivedMessage.FromWalletId == routingUser
&& !string.IsNullOrEmpty(receivedMessage.Payload.Address)
? receivedMessage.Payload.Address
: receivedMessage.FromWalletId;

ReportHub.Log(ReportCategory.CHAT_MESSAGES, $"[MultiplayerReactionBus] OnChatReactionReceived raw: emoji={receivedMessage.Payload.EmojiIndex} messageId={receivedMessage.Payload.MessageId} address={receivedMessage.Payload.Address} fromWallet={receivedMessage.FromWalletId}");

if (cts.IsCancellationRequested)
{
ReportHub.LogWarning(ReportCategory.CHAT_MESSAGES, "[MultiplayerReactionBus] OnChatReactionReceived skipped — CTS cancelled");
return;
}

string messageId = receivedMessage.Payload.MessageId;

// Checked before the dedup keys exist, so an ID no local message could carry
// never reaches the dedup cache. Dropped silently: ReportHub.LogWarning is not
// compiled out, so naming the ID here would allocate it once per packet in
// retail builds.
if (string.IsNullOrEmpty(messageId) || messageId.Length > MAX_MESSAGE_ID_LENGTH)
return;

string walletId = ResolveSenderWalletId(receivedMessage);

ReportHub.Log(ReportCategory.CHAT_MESSAGES, $"[MultiplayerReactionBus] OnChatReactionReceived raw: emoji={receivedMessage.Payload.EmojiIndex} messageId={messageId} fromWallet={receivedMessage.FromWalletId} resolved={walletId}");

if (IsUserBlocked(walletId))
{
ReportHub.Log(ReportCategory.CHAT_MESSAGES, $"[MultiplayerReactionBus] OnChatReactionReceived skipped — user blocked: {walletId}");
Expand All @@ -223,8 +234,8 @@ private void OnChatReactionReceived(ReceivedMessage<ChatReaction> receivedMessag

// Use raw value in dedup key so add/remove have distinct keys.
// Evict the opposite key so toggling (add→remove→add) isn't blocked.
string dedupKey = $"{receivedMessage.Payload.MessageId}:{rawEmojiIndex}";
string oppositeKey = $"{receivedMessage.Payload.MessageId}:{ReactionWireEncoding.Encode(emojiIndex, !isRemoval)}";
string dedupKey = $"{messageId}:{rawEmojiIndex}";
string oppositeKey = $"{messageId}:{ReactionWireEncoding.Encode(emojiIndex, !isRemoval)}";

if (!chatReactionDedup.TryPass(walletId, dedupKey))
{
Expand All @@ -239,18 +250,37 @@ private void OnChatReactionReceived(ReceivedMessage<ChatReaction> receivedMessag
if (!chatReactionRateLimiter.TryPass(walletId, UnityEngine.Time.unscaledTime))
return;

ReportHub.Log(ReportCategory.CHAT_MESSAGES, $"[MultiplayerReactionBus] Received chat reaction: emoji={emojiIndex} isRemoval={isRemoval} messageId={receivedMessage.Payload.MessageId} from={walletId}");
ReportHub.Log(ReportCategory.CHAT_MESSAGES, $"[MultiplayerReactionBus] Received chat reaction: emoji={emojiIndex} isRemoval={isRemoval} messageId={messageId} from={walletId}");

ReactionReceived?.Invoke(new ReactionReceivedArgs(
walletId,
emojiIndex,
1,
ReactionType.Message,
receivedMessage.Payload.MessageId,
messageId,
isRemoval));
}
}

private string ResolveSenderWalletId(ReceivedMessage<ChatReaction> receivedMessage)
{
// Community reactions relayed through the message-router arrive carrying the relay's
// identity in FromWalletId, so the original sender can only come from the payload.
// Reading it solely from the router, and solely when it is a canonical wallet
// address, keeps a direct peer from asserting an arbitrary identity and keeps the
// block, dedup and rate-limit keys bounded.
// NOTE: Add a server-stamped ForwardedFrom to the ChatReaction protocol (like Chat
// has) so the relay writes the verified sender identity, not the client.
if (receivedMessage.FromWalletId != routingUser)
return receivedMessage.FromWalletId;

string relayedAddress = receivedMessage.Payload.Address;

return Web3Address.IsValidWalletAddress(relayedAddress)
? relayedAddress
: receivedMessage.FromWalletId;
}

private bool IsUserBlocked(string userAddress) =>
userBlockingCache.UserIsBlocked(userAddress);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;

namespace DCL.Chat.ChatReactions.Networking
{
/// <summary>
/// Inert bus used while the chat-reactions feature flag is off. It subscribes to no
/// message pipe and drops every send, so no reaction traffic is decoded, retained or
/// emitted while the feature is unavailable.
/// </summary>
public sealed class NullReactionMessageBus : IReactionMessageBus
{
/// <summary>
/// Never raised — nothing is subscribed, so no reaction can arrive.
/// </summary>
public event Action<ReactionReceivedArgs> ReactionReceived
{
add { }
remove { }
}

public void Dispose() { }

public void SendSituationalReaction(int emojiIndex, int count = 1, float overrideTimestamp = 0f) { }

public void SendMessageReaction(int emojiIndex, string messageId, ReactionChannelRouting routing) { }
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading