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
43 changes: 15 additions & 28 deletions Libraries/Microsoft.Teams.Api/Activities/Activity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ public partial interface IActivity : IConvertible, ICloneable
public string GetPath();

/// <summary>
/// get the quote reply string form of this activity
/// Generates a quoted reply placeholder for the current activity.
/// See <see cref="MessageActivity.AddQuote(string, string?)"/> for the recommended approach.
/// </summary>
[Obsolete("Use MessageActivity.AddQuote() instead.")]
public string ToQuoteReply();
}

Expand Down Expand Up @@ -192,12 +194,6 @@ public virtual Activity WithId(string value)
return this;
}

public virtual Activity WithReplyToId(string value)
{
ReplyToId = value;
return this;
}

public virtual Activity WithChannelId(ChannelId value)
{
ChannelId = value;
Expand Down Expand Up @@ -225,19 +221,19 @@ public virtual Activity WithRelatesTo(ConversationReference value)
public virtual Activity WithRecipient(Account value)
{
Recipient = value;
#pragma warning disable ExperimentalTeamsTargeted
#pragma warning disable ExperimentalTeamsTargeted
Recipient.IsTargeted = null;
#pragma warning restore ExperimentalTeamsTargeted
#pragma warning restore ExperimentalTeamsTargeted
return this;
}

[Experimental("ExperimentalTeamsTargeted")]
public virtual Activity WithRecipient(Account value, bool isTargeted)
{
Recipient = value;
#pragma warning disable ExperimentalTeamsTargeted
#pragma warning disable ExperimentalTeamsTargeted
Recipient.IsTargeted = isTargeted ? true : null;
#pragma warning restore ExperimentalTeamsTargeted
#pragma warning restore ExperimentalTeamsTargeted
return this;
}

Expand Down Expand Up @@ -446,24 +442,15 @@ public Activity Merge(Activity from)
return this;
}

public string ToQuoteReply()
/// <summary>
/// Generates a quoted reply placeholder for the current activity.
/// See <see cref="MessageActivity.AddQuote(string, string?)"/> for the recommended approach.
/// </summary>
[Obsolete("Use MessageActivity.AddQuote() instead.")]
public virtual string ToQuoteReply()
{
var text = string.Empty;

if (this is MessageActivity message)
{
text = $"<p itemprop=\"preview\">{message.Text}</p>";
}

return $"""
<blockquote itemscope="" itemtype="http://schema.skype.com/Reply" itemid="{Id}">
<strong itemprop="mri" itemid="{From.Id}">
{From.Name}
</strong>
<span itemprop="time" itemid="{Id}"></span>
{text}
</blockquote>
""";
if (Id == null) return string.Empty;
return $"<quoted messageId=\"{Id}\"/>";
Comment thread
corinagum marked this conversation as resolved.
}

public override string ToString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ public bool IsRecipientMentioned
{
get => (Entities ?? []).Any(e => e is MentionEntity mention && mention.Mentioned.Id == Recipient.Id);
}
/// <summary>
/// Get all quoted reply entities from this message.
/// </summary>
[Experimental("ExperimentalTeamsQuotedReplies")]
#pragma warning disable ExperimentalTeamsQuotedReplies
public IReadOnlyList<QuotedReplyEntity> GetQuotedMessages()
{
return Entities?.OfType<QuotedReplyEntity>().ToList()
?? new List<QuotedReplyEntity>();
}
Comment thread
corinagum marked this conversation as resolved.
#pragma warning restore ExperimentalTeamsQuotedReplies

public MessageActivity() : base(ActivityType.Message)
{
Expand Down Expand Up @@ -153,12 +164,58 @@ public override MessageActivity WithRecipient(Account value)
}

[Experimental("ExperimentalTeamsTargeted")]
#pragma warning disable ExperimentalTeamsTargeted
#pragma warning disable ExperimentalTeamsTargeted
public override MessageActivity WithRecipient(Account value, bool isTargeted = false)
{
return (MessageActivity)base.WithRecipient(value, isTargeted);
}
#pragma warning restore ExperimentalTeamsTargeted
#pragma warning restore ExperimentalTeamsTargeted

/// <summary>
/// Add a quoted message reference and append a placeholder to text.
/// Teams renders the quoted message as a preview bubble above the response text.
/// If text is provided, it is appended to the quoted message placeholder.
/// </summary>
/// <param name="messageId">the ID of the message to quote</param>
/// <param name="text">optional text, appended to the quoted message placeholder</param>
[Experimental("ExperimentalTeamsQuotedReplies")]
#pragma warning disable ExperimentalTeamsQuotedReplies
public MessageActivity AddQuote(string messageId, string? text = null)
{
Entities ??= new List<IEntity>();
Entities.Add(new QuotedReplyEntity
{
QuotedReply = new QuotedReplyData { MessageId = messageId }
});
AddText($"<quoted messageId=\"{messageId}\"/>");
if (text != null)
{
AddText($" {text}");
}
return this;
}
#pragma warning restore ExperimentalTeamsQuotedReplies

/// <summary>
/// Prepend a QuotedReply entity and placeholder before existing text.
/// Used by Reply()/Quote() for quote-above-response.
/// </summary>
[Experimental("ExperimentalTeamsQuotedReplies")]
#pragma warning disable ExperimentalTeamsQuotedReplies
public MessageActivity PrependQuote(string messageId)
{
Entities ??= new List<IEntity>();
Entities.Add(new QuotedReplyEntity
{
QuotedReply = new QuotedReplyData { MessageId = messageId }
});
var placeholder = $"<quoted messageId=\"{messageId}\"/>";
var hasText = !string.IsNullOrWhiteSpace(Text);
Text = hasText ? $"{placeholder} {Text}" : placeholder;
Comment thread
corinagum marked this conversation as resolved.
return this;
}
#pragma warning restore ExperimentalTeamsQuotedReplies


public MessageActivity AddAttachment(params Attachment[] value)
{
Expand Down
11 changes: 11 additions & 0 deletions Libraries/Microsoft.Teams.Api/Entities/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ public override bool CanConvert(Type typeToConvert)
"message" or "https://schema.org/Message" => (Entity?)element.Deserialize<IMessageEntity>(options),
"ProductInfo" => element.Deserialize<ProductInfoEntity>(options),
"streaminfo" => element.Deserialize<StreamInfoEntity>(options),
#pragma warning disable ExperimentalTeamsQuotedReplies
"quotedReply" => element.Deserialize<QuotedReplyEntity>(options),
#pragma warning restore ExperimentalTeamsQuotedReplies
_ => null
};

Expand Down Expand Up @@ -161,6 +164,14 @@ public override void Write(Utf8JsonWriter writer, Entity value, JsonSerializerOp
return;
}

#pragma warning disable ExperimentalTeamsQuotedReplies
if (value is QuotedReplyEntity quotedReply)
{
JsonSerializer.Serialize(writer, quotedReply, options);
return;
}

#pragma warning restore ExperimentalTeamsQuotedReplies
JsonSerializer.Serialize(writer, value.ToJsonObject(options), options);
}
}
Expand Down
46 changes: 46 additions & 0 deletions Libraries/Microsoft.Teams.Api/Entities/QuotedReplyEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace Microsoft.Teams.Api.Entities;

[Experimental("ExperimentalTeamsQuotedReplies")]
public class QuotedReplyEntity : Entity
{
[JsonPropertyName("quotedReply")]
[JsonPropertyOrder(3)]
public required QuotedReplyData QuotedReply { get; set; }

public QuotedReplyEntity() : base("quotedReply") { }
}

[Experimental("ExperimentalTeamsQuotedReplies")]
public class QuotedReplyData
{
[JsonPropertyName("messageId")]
public required string MessageId { get; set; }

Comment thread
corinagum marked this conversation as resolved.
[JsonPropertyName("senderId")]
public string? SenderId { get; set; }

[JsonPropertyName("senderName")]
public string? SenderName { get; set; }

[JsonPropertyName("preview")]
public string? Preview { get; set; }

/// <summary>
/// Timestamp of the quoted message (IC3 epoch value, e.g. "1772050244572").
/// Populated on inbound; ignored on outbound. Absent for deleted quotes.
/// </summary>
[JsonPropertyName("time")]
public string? Time { get; set; }

Comment thread
corinagum marked this conversation as resolved.
[JsonPropertyName("isReplyDeleted")]
public bool? IsReplyDeleted { get; set; }

[JsonPropertyName("validatedMessageReference")]
public bool? ValidatedMessageReference { get; set; }
}
61 changes: 49 additions & 12 deletions Libraries/Microsoft.Teams.Apps/Contexts/Context.Send.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;

using Microsoft.Teams.Api.Activities;

namespace Microsoft.Teams.Apps;
Expand Down Expand Up @@ -29,26 +31,46 @@ public partial interface IContext<TActivity>
public Task<MessageActivity> Send(Cards.AdaptiveCard card, CancellationToken cancellationToken = default);

/// <summary>
/// send an activity to the conversation as a reply
/// send an activity to the conversation as a reply, automatically quoting the inbound message
/// </summary>
/// <param name="activity">activity activity to send</param>
/// <param name="activity">activity to send</param>
/// <param name="cancellationToken">optional cancellation token</param>
public Task<T> Reply<T>(T activity, CancellationToken cancellationToken = default) where T : IActivity;
Comment thread
corinagum marked this conversation as resolved.

/// <summary>
/// send a message activity to the conversation as a reply
/// send a message activity to the conversation as a reply, automatically quoting the inbound message
/// </summary>
Comment thread
corinagum marked this conversation as resolved.
/// <param name="text">the text to send</param>
/// <param name="cancellationToken">optional cancellation token</param>
public Task<MessageActivity> Reply(string text, CancellationToken cancellationToken = default);

/// <summary>
/// send a message activity with a card attachment as a reply
/// send a message activity with a card attachment as a reply, automatically quoting the inbound message
/// </summary>
/// <param name="card">the card to send as an attachment</param>
/// <param name="cancellationToken">optional cancellation token</param>
public Task<MessageActivity> Reply(Cards.AdaptiveCard card, CancellationToken cancellationToken = default);

/// <summary>
/// Send a message to the conversation with a quoted message reference prepended to the text.
/// Teams renders the quoted message as a preview bubble above the response text.
/// </summary>
/// <param name="messageId">the ID of the message to quote</param>
/// <param name="activity">the activity to send — a quote placeholder for messageId will be prepended to its text</param>
/// <param name="cancellationToken">optional cancellation token</param>
[Experimental("ExperimentalTeamsQuotedReplies")]
public Task<T> Quote<T>(string messageId, T activity, CancellationToken cancellationToken = default) where T : IActivity;

Comment thread
corinagum marked this conversation as resolved.
/// <summary>
/// Send a message to the conversation with a quoted message reference prepended to the text.
/// Teams renders the quoted message as a preview bubble above the response text.
/// </summary>
/// <param name="messageId">the ID of the message to quote</param>
/// <param name="text">the response text, appended to the quoted message placeholder</param>
/// <param name="cancellationToken">optional cancellation token</param>
[Experimental("ExperimentalTeamsQuotedReplies")]
public Task<MessageActivity> Quote(string messageId, string text, CancellationToken cancellationToken = default);
Comment thread
corinagum marked this conversation as resolved.

/// <summary>
/// send a typing activity
/// </summary>
Expand Down Expand Up @@ -76,21 +98,17 @@ public Task<MessageActivity> Send(Cards.AdaptiveCard card, CancellationToken can
return Send(new MessageActivity().AddAttachment(card), cancellationToken);
}

#pragma warning disable ExperimentalTeamsQuotedReplies
public Task<T> Reply<T>(T activity, CancellationToken cancellationToken = default) where T : IActivity
{
activity.Conversation = Ref.Conversation.Copy();
activity.Conversation.Id = Ref.Conversation.ThreadId;

if (activity is MessageActivity message)
if (Activity.Id != null)
Comment thread
corinagum marked this conversation as resolved.
{
message.Text = string.Join("\n", [
Activity.ToQuoteReply(),
message.Text != string.Empty ? $"<p>{message.Text}</p>" : string.Empty
]);
return Quote(Activity.Id, activity, cancellationToken);
}

return Send(activity, cancellationToken);
}
#pragma warning restore ExperimentalTeamsQuotedReplies

public Task<MessageActivity> Reply(string text, CancellationToken cancellationToken = default)
{
Expand All @@ -102,6 +120,25 @@ public Task<MessageActivity> Reply(Cards.AdaptiveCard card, CancellationToken ca
return Reply(new MessageActivity().AddAttachment(card), cancellationToken);
}

[Experimental("ExperimentalTeamsQuotedReplies")]
#pragma warning disable ExperimentalTeamsQuotedReplies
public Task<T> Quote<T>(string messageId, T activity, CancellationToken cancellationToken = default) where T : IActivity
{
if (activity is MessageActivity message)
{
message.PrependQuote(messageId);
}
Comment thread
corinagum marked this conversation as resolved.

return Send(activity, cancellationToken);
}
#pragma warning restore ExperimentalTeamsQuotedReplies

[Experimental("ExperimentalTeamsQuotedReplies")]
public Task<MessageActivity> Quote(string messageId, string text, CancellationToken cancellationToken = default)
{
return Quote(messageId, new MessageActivity(text), cancellationToken);
}

public Task<TypingActivity> Typing(string? text = null, CancellationToken cancellationToken = default)
{
var activity = new TypingActivity();
Expand Down
Loading
Loading