UI/Screen/Chat/Chat.razor
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@implements Component.INetworkListener

<root>

	<div class="output" @ref="OutputPanel">
		@foreach (var entry in Entries)
		{
			<div class="chat_entry" style="opacity: @GetEntryOpacity(entry)">
				@if (entry.steamId.ValueUnsigned != 0)
				{
					<img class="avatar" src=@($"avatar:{entry.steamId}") />
				}
				else
				{
					<div class="avatar avatar--empty"></div>
				}
				<div class="author">@entry.author</div>
				<div class="message">@entry.message</div>
			</div>
		}
	</div>

	<div class="input">
		<TextEntry Placeholder="Enter to chat" @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>
	</div>

</root>

@code
{

	[Property] public string MyStringValue { get; set; } = "Hello World!";
	[Property] public SoundEvent OpenSound { get; set; } = ResourceLibrary.Get<SoundEvent>( "combo_finish" );
	[Property] public SoundEvent CloseSound { get; set; } = ResourceLibrary.Get<SoundEvent>( "combo_finish" );
	[Property] public SoundEvent SendSound { get; set; } = ResourceLibrary.Get<SoundEvent>( "combo_finish" );

	TextEntry InputBox;
	Panel OutputPanel;

	public record Entry(SteamId steamId, string author, string message, RealTimeSince timeSinceAdded);
	List<Entry> Entries = new();
	private bool _wasOpen;
	private RealTimeSince _timeSinceFadeUpdate;

	/// <summary>
	/// the hash determines if the system should be rebuilt. If it changes, it will be rebuilt
	/// </summary>
	protected override int BuildHash() => System.HashCode.Combine(MyStringValue);

	protected override void OnUpdate()
	{
		if (!InputBox.IsValid())
			return;

		Panel.AcceptsFocus = false;

		if (Input.Pressed("chat"))
		{
			InputBox.Focus();
		}

		var isOpen = InputBox.HasFocus;
		if (isOpen != _wasOpen)
		{
			_wasOpen = isOpen;
			_timeSinceFadeUpdate = 0f;
			PlayUiSound( isOpen ? OpenSound : CloseSound );
			OutputPanel?.TryScrollToBottom();
			StateHasChanged();
		}
		else if (!isOpen && Entries.Count > 0 && _timeSinceFadeUpdate > 0.1f)
		{
			_timeSinceFadeUpdate = 0f;
			StateHasChanged();
		}

		if (OutputPanel is not null)
			OutputPanel.PreferScrollToBottom = true;

		SetClass("open", isOpen);
	}

	void ChatFinished()
	{
		var text = InputBox.Text;
		InputBox.Text = "";

		if (string.IsNullOrWhiteSpace(text))
			return;

		PlayUiSound( SendSound );
		AddText(Connection.Local.SteamId, Sandbox.Utility.Steam.PersonaName, text);
	}

	[Rpc.Broadcast]
	public void AddText(SteamId steamId, string author, string message)
	{
		message = message.Truncate(300);

		if (string.IsNullOrWhiteSpace(message))
			return;

		Log.Info($"{author}: {message}");

		Entries.Add(new Entry(steamId, author, message, 0.0f));
		OutputPanel?.TryScrollToBottom();
		StateHasChanged();
	}

	void Component.INetworkListener.OnConnected(Connection channel)
	{
		if (IsProxy) return;

		AddText(channel.SteamId, ">", $"{channel.DisplayName} has joined the game");
	}

	void Component.INetworkListener.OnDisconnected(Connection channel)
	{
		if (IsProxy) return;

		AddText(channel.SteamId, "<", $"{channel.DisplayName} has left the game");
	}
	private string GetEntryOpacity(Entry entry)
	{
		if (InputBox?.HasFocus ?? false)
			return "1";

		const float fadeStart = 3f;
		const float fadeEnd = 10f;

		var t = (float)entry.timeSinceAdded;
		if (t <= fadeStart)
			return "1";
		if (t >= fadeEnd)
			return "0";

		var alpha = 1f - (t - fadeStart) / (fadeEnd - fadeStart);
		return alpha.ToString("0.###");
	}

	private static void PlayUiSound( SoundEvent sound )
	{
		if ( sound is null || !sound.IsValid() )
			return;

		Sound.Play( sound );
	}
}