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

<root>

	<div class="output">
		@foreach (var entry in Entries)
		{
			<div class="chat_entry">
				<div class="author">@entry.author</div>
				<div class="message">@entry.message</div>
			</div>
		}
	</div>

	<div class="input">
		<TextEntry @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>
	</div>
	
</root>

@code
{

	[Property] public string MyStringValue { get; set; } = "Hello World!";

	TextEntry InputBox;

	public record Entry( string author, string message, RealTimeSince timeSinceAdded );
	List<Entry> Entries = new();

	/// <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();
		}

		if ( Entries.RemoveAll( x => x.timeSinceAdded > 30.0f ) > 0 )
		{
			StateHasChanged();
		}

		SetClass( "open", InputBox.HasFocus );
	}

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

		if (string.IsNullOrWhiteSpace(text))
			return;

		AddText( Sandbox.Utility.Steam.PersonaName, text );
	}

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

		if (string.IsNullOrWhiteSpace(message))
			return;

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

		Entries.Add(new Entry( author, message, 0.0f ));
		StateHasChanged();
	}

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

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

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

		AddText(  "💨", $"{channel.DisplayName} has left the game" );
	}
}