Typing indicator (typing chat bubble) (#8215)

This commit is contained in:
Alex Evgrashin
2022-05-17 12:55:19 +03:00
committed by GitHub
parent 2557e45662
commit af926c5279
55 changed files with 678 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
using Content.Shared.CCVar;
using Content.Shared.Chat.TypingIndicator;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Client.Chat.TypingIndicator;
// Client-side typing system tracks user input in chat box
public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
{
[Dependency] private readonly IGameTiming _time = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly TimeSpan _typingTimeout = TimeSpan.FromSeconds(2);
private TimeSpan _lastTextChange;
private bool _isClientTyping;
public override void Initialize()
{
base.Initialize();
_cfg.OnValueChanged(CCVars.ChatShowTypingIndicator, OnShowTypingChanged);
}
public void ClientChangedChatText()
{
// don't update it if player don't want to show typing indicator
if (!_cfg.GetCVar(CCVars.ChatShowTypingIndicator))
return;
// client typed something - show typing indicator
ClientUpdateTyping(true);
_lastTextChange = _time.CurTime;
}
public void ClientSubmittedChatText()
{
// don't update it if player don't want to show typing
if (!_cfg.GetCVar(CCVars.ChatShowTypingIndicator))
return;
// client submitted text - hide typing indicator
ClientUpdateTyping(false);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// check if client didn't changed chat text box for a long time
if (_isClientTyping)
{
var dif = _time.CurTime - _lastTextChange;
if (dif > _typingTimeout)
{
// client didn't typed anything for a long time - hide indicator
ClientUpdateTyping(false);
}
}
}
private void ClientUpdateTyping(bool isClientTyping)
{
if (_isClientTyping == isClientTyping)
return;
_isClientTyping = isClientTyping;
// check if player controls any pawn
var playerPawn = _playerManager.LocalPlayer?.ControlledEntity;
if (playerPawn == null)
return;
// send a networked event to server
RaiseNetworkEvent(new TypingChangedEvent(playerPawn.Value, isClientTyping));
}
private void OnShowTypingChanged(bool showTyping)
{
// hide typing indicator immediately if player don't want to show it anymore
if (!showTyping)
{
ClientUpdateTyping(false);
}
}
}

View File

@@ -0,0 +1,50 @@
using Content.Shared.Chat.TypingIndicator;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
namespace Content.Client.Chat.TypingIndicator;
public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingIndicatorComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TypingIndicatorComponent, ComponentInit>(OnInit);
}
private void OnInit(EntityUid uid, TypingIndicatorComponent component, ComponentInit args)
{
if (!TryComp(uid, out SpriteComponent? sprite))
return;
if (!_prototypeManager.TryIndex<TypingIndicatorPrototype>(component.Prototype, out var proto))
{
Logger.Error($"Unknown typing indicator id: {component.Prototype}");
return;
}
var layer = sprite.LayerMapReserveBlank(TypingIndicatorLayers.Base);
sprite.LayerSetRSI(layer, proto.SpritePath);
sprite.LayerSetState(layer, proto.TypingState);
sprite.LayerSetShader(layer, proto.Shader);
sprite.LayerSetOffset(layer, proto.Offset);
sprite.LayerSetVisible(layer, false);
}
protected override void OnAppearanceChange(EntityUid uid, TypingIndicatorComponent component, ref AppearanceChangeEvent args)
{
base.OnAppearanceChange(uid, component, ref args);
if (!TryComp(uid, out SpriteComponent? sprite))
return;
args.Component.TryGetData(TypingIndicatorVisuals.IsTyping, out bool isTyping);
if (sprite.LayerMapTryGet(TypingIndicatorLayers.Base, out var layer))
{
sprite.LayerSetVisible(layer, isTyping);
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Content.Client.Alerts.UI;
using Content.Client.Chat.Managers;
using Content.Client.Chat.TypingIndicator;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Chat;
@@ -475,6 +476,9 @@ namespace Content.Client.Chat.UI
{
// Update channel select button to correct channel if we have a prefix.
UpdateChannelSelectButton();
// Warn typing indicator about change
EntitySystem.Get<TypingIndicatorSystem>().ClientChangedChatText();
}
private static ChatSelectChannel GetChannelFromPrefix(char prefix)
@@ -518,6 +522,9 @@ namespace Content.Client.Chat.UI
private void Input_OnTextEntered(LineEdit.LineEditEventArgs args)
{
// Warn typing indicator about entered text
EntitySystem.Get<TypingIndicatorSystem>().ClientSubmittedChatText();
if (!string.IsNullOrWhiteSpace(args.Text))
{
var (prefixChannel, text) = SplitInputContents();

View File

@@ -0,0 +1,63 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Chat.TypingIndicator;
using Robust.Server.GameObjects;
namespace Content.Server.Chat.TypingIndicator;
// Server-side typing system
// It receives networked typing events from clients
// And sync typing indicator using appearance component
public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<TypingIndicatorComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeNetworkEvent<TypingChangedEvent>(OnClientTypingChanged);
}
private void OnPlayerAttached(PlayerAttachedEvent ev)
{
// when player poses entity we want to make sure that there is typing indicator
EnsureComp<TypingIndicatorComponent>(ev.Entity);
// we also need appearance component to sync visual state
EnsureComp<ServerAppearanceComponent>(ev.Entity);
}
private void OnPlayerDetached(EntityUid uid, TypingIndicatorComponent component, PlayerDetachedEvent args)
{
// player left entity body - hide typing indicator
SetTypingIndicatorEnabled(uid, false);
}
private void OnClientTypingChanged(TypingChangedEvent ev)
{
// make sure that this entity still exist
if (!Exists(ev.Uid))
{
Logger.Warning($"Client attached entity {ev.Uid} from TypingChangedEvent doesn't exist on server.");
return;
}
// check if this entity can speak or emote
if (!_actionBlocker.CanSpeak(ev.Uid) && !_actionBlocker.CanEmote(ev.Uid))
{
// nah, make sure that typing indicator is disabled
SetTypingIndicatorEnabled(ev.Uid, false);
return;
}
SetTypingIndicatorEnabled(ev.Uid, ev.IsTyping);
}
private void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref appearance, false))
return;
appearance.SetData(TypingIndicatorVisuals.IsTyping, isEnabled);
}
}

View File

@@ -820,6 +820,9 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> ChatSanitizerEnabled =
CVarDef.Create("chat.chat_sanitizer_enabled", true, CVar.SERVERONLY);
public static readonly CVarDef<bool> ChatShowTypingIndicator =
CVarDef.Create("chat.show_typing_indicator", true, CVar.CLIENTONLY);
/*
* AFK
*/

View File

@@ -0,0 +1,9 @@
namespace Content.Shared.Chat.TypingIndicator;
/// <summary>
/// Sync typing indicator icon between client and server.
/// </summary>
public abstract class SharedTypingIndicatorSystem : EntitySystem
{
}

View File

@@ -0,0 +1,20 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Chat.TypingIndicator;
/// <summary>
/// Networked event from client.
/// Send to server when client started/stopped typing in chat input field.
/// </summary>
[Serializable, NetSerializable]
public sealed class TypingChangedEvent : EntityEventArgs
{
public readonly EntityUid Uid;
public readonly bool IsTyping;
public TypingChangedEvent(EntityUid uid, bool isTyping)
{
Uid = uid;
IsTyping = isTyping;
}
}

View File

@@ -0,0 +1,20 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Chat.TypingIndicator;
/// <summary>
/// Show typing indicator icon when player typing text in chat box.
/// Added automatically when player poses entity.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Friend(typeof(SharedTypingIndicatorSystem))]
public sealed class TypingIndicatorComponent : Component
{
/// <summary>
/// Prototype id that store all visual info about typing indicator.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField("proto", customTypeSerializer: typeof(PrototypeIdSerializer<TypingIndicatorPrototype>))]
public string Prototype = "default";
}

View File

@@ -0,0 +1,27 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Chat.TypingIndicator;
/// <summary>
/// Prototype to store chat typing indicator visuals.
/// </summary>
[Prototype("typingIndicator")]
public sealed class TypingIndicatorPrototype : IPrototype
{
[IdDataFieldAttribute]
public string ID { get; } = default!;
[DataField("spritePath")]
public ResourcePath SpritePath = new("/Textures/Effects/speech.rsi");
[DataField("typingState", required: true)]
public string TypingState = default!;
[DataField("offset")]
public Vector2 Offset = new(0.5f, 0.5f);
[DataField("shader")]
public string Shader = "unshaded";
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Chat.TypingIndicator;
[Serializable, NetSerializable]
public enum TypingIndicatorVisuals : byte
{
IsTyping
}
[Serializable]
public enum TypingIndicatorLayers : byte
{
Base
}

View File

@@ -68,6 +68,8 @@
Otherwise, wreak havoc on the station!
- type: ReplacementAccent
accent: genericAggressive
- type: TypingIndicator
proto: alien
- type: NoSlip
- type: entity
@@ -84,6 +86,8 @@
sprite: Mobs/Aliens/Carps/magic.rsi
- type: GhostTakeoverAvailable
name: magicarp
- type: TypingIndicator
proto: guardian
- type: entity
name: holocarp
@@ -110,6 +114,8 @@
- Opaque
- type: GhostTakeoverAvailable
name: holocarp
- type: TypingIndicator
proto: robot
- type: entity
id: MobCarpSalvage

View File

@@ -79,6 +79,8 @@
rules: You are an antagonist, smack, slash, and wack!
- type: ReplacementAccent
accent: xeno
- type: TypingIndicator
proto: alien
- type: Temperature
heatDamageThreshold: 360
coldDamageThreshold: -150

View File

@@ -56,6 +56,8 @@
- type: Internals
- type: Examiner
- type: Speech
- type: TypingIndicator
proto: guardian
- type: Pullable
- type: UnarmedCombat
range: 2
@@ -89,6 +91,8 @@
description: Listen to your owner. Don't tank damage. Punch people hard.
- type: NameIdentifier
group: Holoparasite
- type: TypingIndicator
proto: holo
- type: Sprite
layers:
- state: tech_base

View File

@@ -36,6 +36,8 @@
- type: GhostRadio
- type: DoAfter
- type: Actions
- type: TypingIndicator
proto: robot
- type: Speech
speechSounds: Pai
# This has to be installed because otherwise they're not "alive",

View File

@@ -0,0 +1,19 @@
- type: typingIndicator
id: default
typingState: default0
- type: typingIndicator
id: robot
typingState: robot0
- type: typingIndicator
id: alien
typingState: alien0
- type: typingIndicator
id: guardian
typingState: guardian0
- type: typingIndicator
id: holo
typingState: holo0

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

View File

@@ -0,0 +1,345 @@
{
"version": 1,
"size": {
"x": 64,
"y": 64
},
"license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c6e3401f2e7e1e55c57060cdf956a98ef1fefc24",
"states": [
{
"name": "alien0",
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.4
]
]
},
{
"name": "alien1"
},
{
"name": "alien2"
},
{
"name": "alienroyal0",
"delays": [
[
0.2,
0.3,
0.3,
0.3,
0.3,
0.5
]
]
},
{
"name": "alienroyal1",
},
{
"name": "alienroyal2"
},
{
"name": "blob0",
"delays": [
[
0.2,
0.4,
0.4,
0.4,
0.4,
0.4
]
]
},
{
"name": "blob1"
},
{
"name": "blob2"
},
{
"name": "clock0",
"delays": [
[
0.2,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.3
]
]
},
{
"name": "clock1",
"delays": [
[
0.6,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "clock2",
"delays": [
[
0.6,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "default0",
"delays": [
[
0.2,
0.3,
0.3,
0.5
]
]
},
{
"name": "default1"
},
{
"name": "default2"
},
{
"name": "guardian0",
"delays": [
[
0.4,
0.4,
0.4,
0.6
]
]
},
{
"name": "guardian1"
},
{
"name": "guardian2"
},
{
"name": "holo0",
"delays": [
[
0.2,
0.1,
0.1,
0.1,
0.1,
0.2
]
]
},
{
"name": "holo1"
},
{
"name": "holo2"
},
{
"name": "lawyer0",
"delays": [
[
0.3,
0.3,
0.3,
0.4
]
]
},
{
"name": "lawyer1",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "lawyer2",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "machine0",
"delays": [
[
0.4,
0.4,
0.4,
0.4
]
]
},
{
"name": "machine1"
},
{
"name": "machine2"
},
{
"name": "robot0",
"delays": [
[
0.4,
0.4,
0.4,
0.4
]
]
},
{
"name": "robot1"
},
{
"name": "robot2"
},
{
"name": "slime0",
"delays": [
[
0.1,
0.1,
0.2,
0.1,
0.1,
0.1,
0.1,
0.5
]
]
},
{
"name": "slime1",
"delays": [
[
0.1,
0.3,
0.1,
0.1,
0.1,
0.6
]
]
},
{
"name": "slime2",
"delays": [
[
0.1,
0.3,
0.1,
0.1,
0.1,
0.6
]
]
},
{
"name": "swarmer0",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "swarmer1",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "swarmer2",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "syndibot0",
"delays": [
[
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "syndibot1"
},
{
"name": "syndibot2"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B