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
8 changes: 7 additions & 1 deletion Explorer/Assets/DCL/Chat/ChatBubbleComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ namespace DCL.Chat
{
public struct ChatBubbleComponent
{
public readonly string ChatMessage;
public readonly string MessageId;
public string ChatMessage;
public readonly bool IsMention;
public readonly string SenderDisplayName;
public readonly string RecipientValidatedName;
Expand All @@ -16,8 +17,11 @@ public struct ChatBubbleComponent
public readonly string CommunityName;
public readonly bool IsOwnMessage;
public bool IsDirty;

public bool IsTranslationUpdate;
public readonly Color RecipientNameColor;
public ChatBubbleComponent(
string messageId,
string chatMessage,
string senderDisplayName,
string senderWalletId,
Expand All @@ -31,6 +35,7 @@ public ChatBubbleComponent(
bool isCommunityMessage,
string communityName)
{
MessageId = messageId;
ChatMessage = chatMessage;
SenderDisplayName = senderDisplayName;
SenderWalletId = senderWalletId;
Expand All @@ -42,6 +47,7 @@ public ChatBubbleComponent(
RecipientWalletId = recipientWalletId;
RecipientNameColor = recipientNameColor;
IsDirty = true;
IsTranslationUpdate = false;
IsCommunityMessage = isCommunityMessage;
CommunityName = communityName;
}
Expand Down
24 changes: 24 additions & 0 deletions Explorer/Assets/DCL/Chat/History/ChatMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,29 @@ public static class ChatUtils
/// </summary>
public static string GetId(string walletId, double timestampRaw) =>
$"{walletId}:{timestampRaw.ToString(CultureInfo.InvariantCulture)}";

/// <summary>
/// Recovers the sender wallet address from an id produced by <see cref="GetId" />.
/// Returns false for ids that don't follow the composite format (e.g. system-message GUIDs).
/// </summary>
public static bool TryGetSenderWalletAddress(string? messageId, out string walletAddress)
{
if (string.IsNullOrEmpty(messageId))
{
walletAddress = string.Empty;
return false;
}

int separator = messageId.IndexOf(':');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullReferenceException

messageId.IndexOf(':') throws if messageId is null. TranslationEvents.MessageTranslated.MessageId is a struct field — its default is null — so any caller that forgets to set it will crash here.

All current callers always set it, so this is low risk today, but since the method is public static a null guard is cheap insurance:

Suggested change
int separator = messageId.IndexOf(':');
public static bool TryGetSenderWalletAddress(string messageId, out string walletAddress)
{
if (string.IsNullOrEmpty(messageId))
{
walletAddress = string.Empty;
return false;
}
int separator = messageId.IndexOf(':');


if (separator <= 0)
{
walletAddress = string.Empty;
return false;
}

walletAddress = messageId.Substring(0, separator);
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
using DCL.Nametags;
using DCL.Profiles;
using DCL.Settings.Settings;
using DCL.Translation;
using DCL.Utilities;
using System;
using UnityEngine;
using UnityEngine.InputSystem;
using Utility;
using Utility.Arch;

namespace DCL.Chat.ChatServices
Expand All @@ -23,8 +25,12 @@ public class ChatWorldBubbleService : IDisposable
private readonly ChatSettingsAsset chatSettings;
private readonly IChatHistory chatHistory;
private readonly ICommunityDataService communityDataService;
private readonly IEventBus chatEventBus;
private readonly ITranslationSettings translationSettings;
private static readonly Color DEFAULT_COLOR = Color.white;

private readonly IDisposable translationSubscription;

public ChatWorldBubbleService(
World world,
Entity playerEntity,
Expand All @@ -33,7 +39,9 @@ public ChatWorldBubbleService(
NametagsData nametagsData,
ChatSettingsAsset chatSettings,
IChatHistory chatHistory,
ICommunityDataService communityDataService)
ICommunityDataService communityDataService,
IEventBus chatEventBus,
ITranslationSettings translationSettings)
{
this.world = world;
this.playerEntity = playerEntity;
Expand All @@ -43,8 +51,11 @@ public ChatWorldBubbleService(
this.chatSettings = chatSettings;
this.chatHistory = chatHistory;
this.communityDataService = communityDataService;
this.chatEventBus = chatEventBus;
this.translationSettings = translationSettings;

chatHistory.MessageAdded += OnChatMessageAdded;
translationSubscription = chatEventBus.Subscribe<TranslationEvents.MessageTranslated>(OnMessageTranslated);
DCLInput.Instance.Shortcuts.ToggleNametags.performed += OnToggleNametagsShortcutPerformed;
}

Expand Down Expand Up @@ -120,6 +131,7 @@ private void GenerateChatBubbleComponent(Entity e, ChatMessage chatMessage, Colo
if (!world.Has<NametagHolder>(e)) return;

world.AddOrSet(e, new ChatBubbleComponent(
chatMessage.MessageId,
chatMessage.Message,
chatMessage.SenderValidatedName,
chatMessage.SenderWalletAddress,
Expand All @@ -134,10 +146,43 @@ private void GenerateChatBubbleComponent(Entity e, ChatMessage chatMessage, Colo
communityName ?? string.Empty));
}

private void OnMessageTranslated(TranslationEvents.MessageTranslated evt)
{
MessageTranslation translation = evt.Translation;

if (translation.State != TranslationState.Success || string.IsNullOrEmpty(translation.TranslatedBody))
return;

if (!translationSettings.IsTranslationFeatureActive())
return;

// The message id encodes the sender wallet address, which the participant table maps to the live avatar entity.
if (!ChatUtils.TryGetSenderWalletAddress(evt.MessageId, out string senderWalletAddress)
|| !entityParticipantTable.TryGet(senderWalletAddress, out var entry)
|| !world.IsAlive(entry.Entity))
return;

ref ChatBubbleComponent bubble = ref world.TryGetRef<ChatBubbleComponent>(entry.Entity, out bool exists);

// Guard against the sender having since posted a newer message into the same bubble.
if (!exists || bubble.MessageId != evt.MessageId)
return;

// Mirror automatic translations only. A manual translate from the chat panel targets a conversation
// where auto-translate is off (or an older message whose bubble is already gone), so it is ignored here.
if (!translationSettings.GetAutoTranslateForConversation(bubble.ChannelId))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: global feature flag not checked

The per-conversation check is here but the global IsTranslationFeatureActive() guard is missing. If a translation is still in-flight when the user disables the global flag, OnMessageTranslated will still fire (the async work was already kicked off) and the bubble text will be swapped — contradicting the documented behaviour ("global translation feature flag … respected").

ChatMessageFeedPresenter correctly gates on both:

// from ChatMessageFeedPresenter, line ~119 / 354
if (!translationSettings.IsTranslationFeatureActive())if (!translationSettings.GetAutoTranslateForConversation())

Fix:

Suggested change
if (!translationSettings.GetAutoTranslateForConversation(bubble.ChannelId))
// Guard against in-flight translations arriving after the global feature is disabled.
if (!translationSettings.IsTranslationFeatureActive())
return;
// Mirror automatic translations only. A manual translate from the chat panel targets a conversation
// where auto-translate is off (or an older message whose bubble is already gone), so it is ignored here.
if (!translationSettings.GetAutoTranslateForConversation(bubble.ChannelId))
return;

return;

bubble.ChatMessage = translation.TranslatedBody;
bubble.IsTranslationUpdate = true;
bubble.IsDirty = true;
}

public void Dispose()
{
DCLInput.Instance.Shortcuts.ToggleNametags.performed -= OnToggleNametagsShortcutPerformed;
chatHistory.MessageAdded -= OnChatMessageAdded;
translationSubscription.Dispose();
}

private void OnToggleNametagsShortcutPerformed(InputAction.CallbackContext obj)
Expand Down
1 change: 1 addition & 0 deletions Explorer/Assets/DCL/NameTags/Assets/NametagStyle.uss
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@
font-size: 16px;
white-space: normal;
margin-top: 10px;
-unity-font-definition: url("project://database/Assets/DCL/UIToolkit/Fonts/Inter_18pt-SemiBold.ttf?fileID=12800000&guid=3d5604d303a734a85bf5000c0e2e5d92&type=3#Inter_18pt-SemiBold");
}

.safe-easing {
Expand Down
15 changes: 13 additions & 2 deletions Explorer/Assets/DCL/NameTags/Systems/NametagPlacementSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,20 @@ private void ProcessChatBubbleComponents(in NametagHolder nametagHolder, ref Cha
if (!chatBubbleComponent.IsDirty)
return;

nametagHolder.Nametag.DisplayMessage(chatBubbleComponent.ChatMessage, chatBubbleComponent.IsMention, chatBubbleComponent.IsPrivateMessage, chatBubbleComponent.IsOwnMessage, chatBubbleComponent.RecipientValidatedName, chatBubbleComponent.RecipientWalletId, chatBubbleComponent.RecipientNameColor, chatBubbleComponent.IsCommunityMessage, chatBubbleComponent.CommunityName);

chatBubbleComponent.IsDirty = false;

if (chatBubbleComponent.IsTranslationUpdate)
{
chatBubbleComponent.IsTranslationUpdate = false;

// Swap the text in place only while the bubble is still visible; never revive an already-hidden bubble.
if (nametagHolder.Nametag.ShowMessage)
nametagHolder.Nametag.MessageText = chatBubbleComponent.ChatMessage;

return;
}

nametagHolder.Nametag.DisplayMessage(chatBubbleComponent.ChatMessage, chatBubbleComponent.IsMention, chatBubbleComponent.IsPrivateMessage, chatBubbleComponent.IsOwnMessage, chatBubbleComponent.RecipientValidatedName, chatBubbleComponent.RecipientWalletId, chatBubbleComponent.RecipientNameColor, chatBubbleComponent.IsCommunityMessage, chatBubbleComponent.CommunityName);
}

[Query]
Expand Down
4 changes: 3 additions & 1 deletion Explorer/Assets/DCL/PluginSystem/Global/ChatPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,9 @@ public async UniTask InitializeAsync(ChatPluginSettings settings, CancellationTo
nametagsData,
settings.ChatSettingsAsset,
chatHistory,
communityDataService);
communityDataService,
chatEventBus,
translationSettings);

var currentChannelService = externalCurrentChannelService ?? new CurrentChannelService();

Expand Down
22 changes: 19 additions & 3 deletions Explorer/Assets/DCL/UIToolkit/DCLTextSettings.asset
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,38 @@ MonoBehaviour:
m_Version:
m_DefaultFontAsset: {fileID: 0}
m_DefaultFontAssetPath: Fonts & Materials/
m_FallbackFontAssets: []
m_FallbackFontAssets:
- {fileID: 11400000, guid: 4d0381cbcc4aad846ab6f3156ecee770, type: 2}
- {fileID: 11400000, guid: f38dc3f6cf805594b86ad9a2f931897e, type: 2}
m_FallbackOSFontAssets: []
m_MatchMaterialPreset: 0
m_MissingCharacterUnicode: 0
m_ClearDynamicDataOnBuild: 1
m_EnableEmojiSupport: 0
m_EmojiFallbackTextAssets: []
m_EmojiFallbackTextAssets:
- {fileID: 11400000, guid: 6e91b23f4d82c4cbfb4001ba83a92072, type: 2}
m_DefaultSpriteAsset: {fileID: 11400000, guid: 6e91b23f4d82c4cbfb4001ba83a92072, type: 2}
m_DefaultSpriteAssetPath: Sprite Assets/
m_FallbackSpriteAssets: []
m_MissingSpriteCharacterUnicode: 0
m_DefaultStyleSheet: {fileID: 0}
m_StyleSheetsResourcePath: Text Style Sheets/
m_DefaultColorGradientPresetsPath: Text Color Gradients/
m_UnicodeLineBreakingRules:
m_UnicodeLineBreakingRules: {fileID: 0}
m_LeadingCharacters: {fileID: 4900000, guid: d82c1b31c7e74239bff1220585707d2b, type: 3}
m_FollowingCharacters: {fileID: 4900000, guid: fade42e8bc714b018fac513c043d323b, type: 3}
m_UseModernHangulLineBreakingRules: 0
m_DisplayWarnings: 0
m_FontReferences:
- font: {fileID: 12800000, guid: 16e3d6ebbb2afa8478140aa1fffd9d09, type: 3}
fontAsset: {fileID: 0}
- font: {fileID: 12800000, guid: 05befaedcb2db4303a48d552554db836, type: 3}
fontAsset: {fileID: 0}
- font: {fileID: 12800000, guid: 6662efddf575d46528acad7d7309708b, type: 3}
fontAsset: {fileID: 0}
- font: {fileID: 12800000, guid: 3d5604d303a734a85bf5000c0e2e5d92, type: 3}
fontAsset: {fileID: 0}
- font: {fileID: 2230732570650464555, guid: 10e86a2123940e44b95c43e0bbfb27d1, type: 3}
fontAsset: {fileID: 0}
- font: {fileID: 2230732570650464555, guid: 4deb06e376f984b6790130c58914a876, type: 3}
fontAsset: {fileID: 0}
Loading
Loading