Skip to content
Merged
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: 8 additions & 0 deletions src/Console/ClientModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,25 @@ enum MessageType
Content = 1,
Interactive = 2,
Reaction = 4,
Typing = 8
}

[JsonPolymorphic]
[JsonDerivedType(typeof(ContentMessage), "text")]
[JsonDerivedType(typeof(InteractiveMessage), "interactive")]
[JsonDerivedType(typeof(ReactionMessage), "reaction")]
[JsonDerivedType(typeof(TypingMessage), "typing")]
abstract record ClientMessage
{
public abstract MessageType Type { get; }
}

record TypingMessage : ClientMessage
{
public override MessageType Type => MessageType.Typing;
public override string ToString() => "...";
}

record ContentMessage(Context Context, Text Text) : ClientMessage
{
public override MessageType Type => MessageType.Content;
Expand Down
35 changes: 34 additions & 1 deletion src/Console/Interactive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ async Task ResponseListener()
}
}

CancellationTokenSource typingCancellation = new();
Task? typingStatus;

async Task RenderAsync(string json)
{
if (needsNewline)
Expand All @@ -182,15 +185,33 @@ async Task RenderAsync(string json)
try
{
// Move discriminator to top.
json = await JQ.ExecuteAsync(json, "{ \"$type\": .type } + .");
json = await JQ.ExecuteAsync(json,
"""
{ "$type": (."type" // "typing") } + .
""");

if (JsonSerializer.Deserialize(json, ClientContext.Default.ClientMessage) is { } message &&
message.ToString() is { } text)
{
if (message.Type == Client.MessageType.Typing)
{
await ResetTypingAsync();
typingStatus = AnsiConsole.Status().StartAsync("...", async x =>
{
while (!cts.IsCancellationRequested && !typingCancellation.IsCancellationRequested)
{
await Task.Delay(100);
}
});
return;
}

// Don't render empty reaction since it's the clearing of the emoji actually in WhatsApp
if (message.Type == Client.MessageType.Reaction && text.Length == 0)
return;

await ResetTypingAsync();

IRenderable body = message.Type == Client.MessageType.Reaction || (text.StartsWith("[") && text.EndsWith("]"))
? TryMarkup(text)
: new Spectre.Console.Text(text).Overflow(Overflow.Fold);
Expand Down Expand Up @@ -224,6 +245,18 @@ async Task RenderAsync(string json)
});
}

async Task ResetTypingAsync()
{
if (typingStatus != null && !typingStatus.IsCompleted)
{
typingCancellation.Cancel();
await typingStatus;
typingStatus = null;
if (!typingCancellation.TryReset())
typingCancellation = new CancellationTokenSource();
}
}

static IRenderable TryMarkup(string text)
{
try
Expand Down
4 changes: 4 additions & 0 deletions src/WhatsApp/AzureFunctionsConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace Devlooped.WhatsApp;
/// and exercise the WhatsApp API without requiring a full WhatsApp for Business account.
/// </summary>
class AzureFunctionsConsole(
IWhatsAppClient client,
IWhatsAppHandler handler,
ILogger<AzureFunctionsWebhook> logger,
IHostEnvironment environment)
Expand Down Expand Up @@ -51,6 +52,9 @@ public async Task<IActionResult> MessageConsole([HttpTrigger(AuthorizationLevel.
// Try to deserialize the message sent by the console
if (JsonSerializer.Deserialize(json, JsonContext.Default.Message) is Message message)
{
if (message is UserMessage user)
await user.SendProgress(client, true, true).Ignore();

message.FromConsole = true;
// Await all responses
// No action needed, just make sure all items are processed
Expand Down