From 6eda85e651b544d5a64be8eeba8c42a791ff04b1 Mon Sep 17 00:00:00 2001 From: Alex Acebo Date: Tue, 2 Dec 2025 13:19:53 -0800 Subject: [PATCH 01/11] Update Reaction.cs --- .../Microsoft.Teams.Api/Messages/Reaction.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs index 58fe0b35..7a7091ec 100644 --- a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs +++ b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs @@ -15,24 +15,69 @@ namespace Microsoft.Teams.Api.Messages; [JsonConverter(typeof(JsonConverter))] public class ReactionType(string value) : StringEnum(value) { + /// + /// 👍 + /// public static readonly ReactionType Like = new("like"); public bool IsLike => Like.Equals(Value); + /// + /// ❤️ + /// public static readonly ReactionType Heart = new("heart"); public bool IsHeart => Heart.Equals(Value); + /// + /// ✅ + /// + public static readonly ReactionType Checkmark = new("checkmark"); + public bool IsCheckmark => Checkmark.Equals(Value); + + /// + /// ⏳ + /// + public static readonly ReactionType Hourglass = new("hourglass"); + public bool IsHourglass => Hourglass.Equals(Value); + + /// + /// 📌 + /// + public static readonly ReactionType Pushpin = new("pushpin"); + public bool IsPushpin => Pushpin.Equals(Value); + + /// + /// ❗ + /// + public static readonly ReactionType Exclamation = new("exclamation"); + public bool IsExclamation => Exclamation.Equals(Value); + + /// + /// 😆 + /// public static readonly ReactionType Laugh = new("laugh"); public bool IsLaugh => Laugh.Equals(Value); + /// + /// 😮 + /// public static readonly ReactionType Surprise = new("surprise"); public bool IsSurprise => Surprise.Equals(Value); + /// + /// 🙁 + /// public static readonly ReactionType Sad = new("sad"); public bool IsSad => Sad.Equals(Value); + /// + /// 😠 + /// public static readonly ReactionType Angry = new("angry"); public bool IsAngry => Angry.Equals(Value); + /// + /// plus-one + /// public static readonly ReactionType PlusOne = new("plusOne"); public bool IsPlusOne => PlusOne.Equals(Value); } From aa3934354c138db7ca268fe7350d45f68b713c28 Mon Sep 17 00:00:00 2001 From: Alex Acebo Date: Tue, 2 Dec 2025 13:24:27 -0800 Subject: [PATCH 02/11] Update Reaction.cs --- Libraries/Microsoft.Teams.Api/Messages/Reaction.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs index 7a7091ec..ec8613c5 100644 --- a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs +++ b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs @@ -9,8 +9,6 @@ namespace Microsoft.Teams.Api.Messages; /// /// The type of reaction given to the -/// message. Possible values include: 'like', 'heart', 'laugh', 'surprised', -/// 'sad', 'angry', 'plusOne' /// [JsonConverter(typeof(JsonConverter))] public class ReactionType(string value) : StringEnum(value) From 2b3288c4499c77fa6be0c3b6556b9f75b4f1e719 Mon Sep 17 00:00:00 2001 From: Alex Acebo Date: Tue, 2 Dec 2025 14:02:21 -0800 Subject: [PATCH 03/11] add client --- .../Clients/ConversationClient.cs | 5 + .../Clients/ReactionClient.cs | 93 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs diff --git a/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs index 8523faae..3ec316aa 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs @@ -13,12 +13,14 @@ public class ConversationClient : Client public readonly string ServiceUrl; public readonly ActivityClient Activities; public readonly MemberClient Members; + public readonly ReactionClient Reactions; public ConversationClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { ServiceUrl = serviceUrl; Activities = new ActivityClient(serviceUrl, _http, cancellationToken); Members = new MemberClient(serviceUrl, _http, cancellationToken); + Reactions = new ReactionClient(serviceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) : base(client, cancellationToken) @@ -26,6 +28,7 @@ public ConversationClient(string serviceUrl, IHttpClient client, CancellationTok ServiceUrl = serviceUrl; Activities = new ActivityClient(serviceUrl, _http, cancellationToken); Members = new MemberClient(serviceUrl, _http, cancellationToken); + Reactions = new ReactionClient(serviceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) @@ -33,6 +36,7 @@ public ConversationClient(string serviceUrl, IHttpClientOptions options, Cancell ServiceUrl = serviceUrl; Activities = new ActivityClient(serviceUrl, _http, cancellationToken); Members = new MemberClient(serviceUrl, _http, cancellationToken); + Reactions = new ReactionClient(serviceUrl, _http, cancellationToken); } public ConversationClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) : base(factory, cancellationToken) @@ -40,6 +44,7 @@ public ConversationClient(string serviceUrl, IHttpClientFactory factory, Cancell ServiceUrl = serviceUrl; Activities = new ActivityClient(serviceUrl, _http, cancellationToken); Members = new MemberClient(serviceUrl, _http, cancellationToken); + Reactions = new ReactionClient(serviceUrl, _http, cancellationToken); } public async Task CreateAsync(CreateRequest request) diff --git a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs new file mode 100644 index 00000000..5759d686 --- /dev/null +++ b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Api.Messages; +using Microsoft.Teams.Common.Http; + +namespace Microsoft.Teams.Api.Clients; + +/// +/// Client for working with app message reactions for a given conversation/activity. +/// +public class ReactionClient : Client +{ + public readonly string ServiceUrl; + + public ReactionClient(string serviceUrl, CancellationToken cancellationToken = default) + : base(cancellationToken) + { + ServiceUrl = serviceUrl; + } + + public ReactionClient(string serviceUrl, IHttpClient client, CancellationToken cancellationToken = default) + : base(client, cancellationToken) + { + ServiceUrl = serviceUrl; + } + + public ReactionClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) + : base(options, cancellationToken) + { + ServiceUrl = serviceUrl; + } + + public ReactionClient(string serviceUrl, IHttpClientFactory factory, CancellationToken cancellationToken = default) + : base(factory, cancellationToken) + { + ServiceUrl = serviceUrl; + } + + /// + /// Creates or updates a reaction on an activity in a conversation. + /// + /// The conversation id. + /// The id of the activity to react to. + /// + /// The reaction type (for example: "like", "heart", "laugh", etc.). + /// + /// + /// Optional id of the user on whose behalf the reaction is added/updated (if supported by the service). + /// + /// + /// A describing the reaction, or null if the service returned an empty body. + /// + public async Task CreateOrUpdateAsync( + string conversationId, + string activityId, + ReactionType reactionType + ) + { + // Assumed route: + // PUT v3/conversations/{conversationId}/activities/{activityId}/reactions + var url = $"{ServiceUrl}v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}"; + var req = HttpRequest.Put(url); + await _http.SendAsync(req, _cancellationToken); + } + + /// + /// Removes a reaction from an activity in a conversation. + /// + /// The conversation id. + /// The id of the activity the reaction is on. + /// + /// The reaction type to remove (for example: "like", "heart", "laugh", etc.). + /// + /// + /// Optional id of the user whose reaction should be removed (if supported by the service). + /// + public async Task DeleteAsync( + string conversationId, + string activityId, + ReactionType reactionType + ) + { + // Assumed route: + // DELETE v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType} + var url = + $"{ServiceUrl}v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}"; + + var req = HttpRequest.Delete(url); + + await _http.SendAsync(req, _cancellationToken); + } +} From f0178176ea1f03c0a992d246a20eb1454b0e7ebd Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 17 Feb 2026 13:32:53 -0800 Subject: [PATCH 04/11] Add Samples.Reactions bot and refactor reaction APIs Added a new Samples.Reactions project with a sample bot demonstrating Teams message reactions. Removed AddReaction/RemoveReaction helpers from MessageReactionActivity and updated related tests. Exposed IHttpClient via ApiClient.Client. Registered the new sample in the solution and suppressed CS0618 warnings in test projects. Minor formatting and consistency improvements throughout. --- .../Message/MessageReactionActivity.cs | 54 ------------------- .../Microsoft.Teams.Api/Clients/ApiClient.cs | 3 ++ Microsoft.Teams.sln | 17 +++++- Samples/Samples.Reactions/Program.cs | 39 ++++++++++++++ .../Properties/launchSettings.TEMPLATE.json | 16 ++++++ .../Samples.Reactions.csproj | 14 +++++ Samples/Samples.Reactions/appsettings.json | 9 ++++ .../Message/MessageReactionActivityTests.cs | 50 ++--------------- .../Message/MessageReactionActivity.json | 11 +--- .../Messages/MessageReactionActivityTests.cs | 22 ++++---- .../Microsoft.Teams.Apps.Tests.csproj | 45 ++++++++-------- 11 files changed, 137 insertions(+), 143 deletions(-) create mode 100644 Samples/Samples.Reactions/Program.cs create mode 100644 Samples/Samples.Reactions/Properties/launchSettings.TEMPLATE.json create mode 100644 Samples/Samples.Reactions/Samples.Reactions.csproj create mode 100644 Samples/Samples.Reactions/appsettings.json diff --git a/Libraries/Microsoft.Teams.Api/Activities/Message/MessageReactionActivity.cs b/Libraries/Microsoft.Teams.Api/Activities/Message/MessageReactionActivity.cs index 48f83139..e0907628 100644 --- a/Libraries/Microsoft.Teams.Api/Activities/Message/MessageReactionActivity.cs +++ b/Libraries/Microsoft.Teams.Api/Activities/Message/MessageReactionActivity.cs @@ -22,58 +22,4 @@ public class MessageReactionActivity() : Activity(ActivityType.MessageReaction) [JsonPropertyName("reactionsRemoved")] [JsonPropertyOrder(122)] public IList? ReactionsRemoved { get; set; } - - public MessageReactionActivity AddReaction(Messages.Reaction reaction) - { - ReactionsAdded ??= []; - ReactionsAdded.Add(reaction); - return this; - } - - public MessageReactionActivity AddReaction(Messages.ReactionType type) - { - ReactionsAdded ??= []; - ReactionsAdded.Add(new() { Type = type }); - return this; - } - - public MessageReactionActivity RemoveReaction(Messages.Reaction reaction) - { - ReactionsRemoved ??= []; - - if (ReactionsAdded is not null) - { - var i = ReactionsAdded.ToList().FindIndex(r => - r.Type.Equals(reaction.Type) && r.User?.Id == reaction.User?.Id - ); - - if (i > -1) - { - ReactionsAdded.RemoveAt(i); - return this; - } - } - - ReactionsRemoved.Add(reaction); - return this; - } - - public MessageReactionActivity RemoveReaction(Messages.ReactionType type) - { - ReactionsRemoved ??= []; - - if (ReactionsAdded is not null) - { - var i = ReactionsAdded.ToList().FindIndex(r => r.Type.Equals(type)); - - if (i > -1) - { - ReactionsAdded.RemoveAt(i); - return this; - } - } - - ReactionsRemoved.Add(new() { Type = type }); - return this; - } } \ No newline at end of file diff --git a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs index 3a7d9269..0c2f04ec 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs @@ -14,6 +14,8 @@ public class ApiClient : Client public virtual TeamClient Teams { get; } public virtual MeetingClient Meetings { get; } + public IHttpClient Client { get => base._http; } + public ApiClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) { ServiceUrl = serviceUrl; @@ -32,6 +34,7 @@ public ApiClient(string serviceUrl, IHttpClient client, CancellationToken cancel Users = new UserClient(_http, cancellationToken); Teams = new TeamClient(serviceUrl, _http, cancellationToken); Meetings = new MeetingClient(serviceUrl, _http, cancellationToken); + } public ApiClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) diff --git a/Microsoft.Teams.sln b/Microsoft.Teams.sln index a6b950d7..f2566bc6 100644 --- a/Microsoft.Teams.sln +++ b/Microsoft.Teams.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.2.11415.280 d18.0 +VisualStudioVersion = 18.2.11415.280 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{809F86A1-1C4C-B159-0CD4-DF9D33D876CE}" EndProject @@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deprecated.Controllers", "S EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.TargetedMessages", "Samples\Samples.TargetedMessages\Samples.TargetedMessages.csproj", "{A5989DC4-B182-4218-A858-24FA5FE31251}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.Reactions", "Samples\Samples.Reactions\Samples.Reactions.csproj", "{1FCD6490-DD3A-4D76-A07F-A30655365CA8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -543,6 +545,18 @@ Global {A5989DC4-B182-4218-A858-24FA5FE31251}.Release|x64.Build.0 = Release|Any CPU {A5989DC4-B182-4218-A858-24FA5FE31251}.Release|x86.ActiveCfg = Release|Any CPU {A5989DC4-B182-4218-A858-24FA5FE31251}.Release|x86.Build.0 = Release|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Debug|x64.ActiveCfg = Debug|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Debug|x64.Build.0 = Debug|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Debug|x86.ActiveCfg = Debug|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Debug|x86.Build.0 = Debug|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Release|Any CPU.Build.0 = Release|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Release|x64.ActiveCfg = Release|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Release|x64.Build.0 = Release|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Release|x86.ActiveCfg = Release|Any CPU + {1FCD6490-DD3A-4D76-A07F-A30655365CA8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -588,6 +602,7 @@ Global {FCEEF7AD-1E78-4615-8BE4-7B46B034A514} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} {1F0A8B29-3792-4E11-B6A7-43F03F9A591F} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} {A5989DC4-B182-4218-A858-24FA5FE31251} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} + {1FCD6490-DD3A-4D76-A07F-A30655365CA8} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {378263F2-C2B2-4DB1-83CF-CA228AF03ABF} diff --git a/Samples/Samples.Reactions/Program.cs b/Samples/Samples.Reactions/Program.cs new file mode 100644 index 00000000..7b25d830 --- /dev/null +++ b/Samples/Samples.Reactions/Program.cs @@ -0,0 +1,39 @@ +using Microsoft.Teams.Api.Clients; +using Microsoft.Teams.Api.Messages; +using Microsoft.Teams.Apps.Activities; +using Microsoft.Teams.Apps.Extensions; +using Microsoft.Teams.Plugins.AspNetCore.DevTools.Extensions; +using Microsoft.Teams.Plugins.AspNetCore.Extensions; + +var builder = WebApplication.CreateBuilder(args); +builder.AddTeams().AddTeamsDevTools(); +var app = builder.Build(); +var teams = app.UseTeams(); + + +teams.OnMessage(async (context, cancellationToken) => +{ + await context.Send($"you said '{context.Activity.Text}'", cancellationToken); + + + // replace with context.Api.Conversations.Reactions once APX is rolledout to PROD + var api = new ApiClient(context.Activity.ServiceUrl!, context.Api.Client, cancellationToken); + + await api.Conversations.Reactions.CreateOrUpdateAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f44b_wavinghand-tone4")); + + await Task.Delay(2000, cancellationToken); + await api.Conversations.Reactions.CreateOrUpdateAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f601_beamingfacewithsmilingeyes")); + + await Task.Delay(2000, cancellationToken); + await api.Conversations.Reactions.DeleteAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f601_beamingfacewithsmilingeyes")); + +}); + +teams.OnMessageReaction(async (context, cancellationToken) => +{ + context.Log.Info($"Reaction '{context.Activity.ReactionsAdded?.FirstOrDefault()?.Type}' added by {context.Activity.From?.Name}"); + await context.Send($"you reacted with added '{context.Activity.ReactionsAdded?.FirstOrDefault()?.Type}' " + + $"and removed '{context.Activity.ReactionsRemoved?.FirstOrDefault()?.Type}'", cancellationToken); +}); + +app.Run(); \ No newline at end of file diff --git a/Samples/Samples.Reactions/Properties/launchSettings.TEMPLATE.json b/Samples/Samples.Reactions/Properties/launchSettings.TEMPLATE.json new file mode 100644 index 00000000..4fd0838c --- /dev/null +++ b/Samples/Samples.Reactions/Properties/launchSettings.TEMPLATE.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "bot-config": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:3978", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Teams__TenantId": "", + "Teams__ClientId": "", + "Teams__ClientSecret": "" + } + } + } +} \ No newline at end of file diff --git a/Samples/Samples.Reactions/Samples.Reactions.csproj b/Samples/Samples.Reactions/Samples.Reactions.csproj new file mode 100644 index 00000000..682a8f9d --- /dev/null +++ b/Samples/Samples.Reactions/Samples.Reactions.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/Samples/Samples.Reactions/appsettings.json b/Samples/Samples.Reactions/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Samples/Samples.Reactions/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Tests/Microsoft.Teams.Api.Tests/Activities/Message/MessageReactionActivityTests.cs b/Tests/Microsoft.Teams.Api.Tests/Activities/Message/MessageReactionActivityTests.cs index ac52de79..eaff0dd3 100644 --- a/Tests/Microsoft.Teams.Api.Tests/Activities/Message/MessageReactionActivityTests.cs +++ b/Tests/Microsoft.Teams.Api.Tests/Activities/Message/MessageReactionActivityTests.cs @@ -29,15 +29,7 @@ public void JsonSerialize() Name = "test-bot", Role = Role.Bot } - }.AddReaction(new Messages.Reaction() - { - Type = Messages.ReactionType.Like, - User = new Messages.User() - { - Id = "100", - UserIdentityType = Messages.UserIdentityType.AnonymousGuest - } - }); + }; var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() { @@ -74,15 +66,7 @@ public void JsonSerialize_Derived() Name = "test-bot", Role = Role.Bot } - }.AddReaction(new Messages.Reaction() - { - Type = Messages.ReactionType.Like, - User = new Messages.User() - { - Id = "100", - UserIdentityType = Messages.UserIdentityType.AnonymousGuest - } - }); + }; var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() { @@ -119,15 +103,7 @@ public void JsonSerialize_Derived_Interface() Name = "test-bot", Role = Role.Bot } - }.AddReaction(new Messages.Reaction() - { - Type = Messages.ReactionType.Like, - User = new Messages.User() - { - Id = "100", - UserIdentityType = Messages.UserIdentityType.AnonymousGuest - } - }); + }; var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions() { @@ -166,15 +142,7 @@ public void JsonDeserialize() Name = "test-bot", Role = Role.Bot } - }.AddReaction(new Messages.Reaction() - { - Type = Messages.ReactionType.Like, - User = new Messages.User() - { - Id = "100", - UserIdentityType = Messages.UserIdentityType.AnonymousGuest - } - }); + }; Assert.Equivalent(expected, activity); } @@ -204,15 +172,7 @@ public void JsonDeserialize_Derived() Name = "test-bot", Role = Role.Bot } - }.AddReaction(new Messages.Reaction() - { - Type = Messages.ReactionType.Like, - User = new Messages.User() - { - Id = "100", - UserIdentityType = Messages.UserIdentityType.AnonymousGuest - } - }); + }; Assert.Equivalent(expected, activity); } diff --git a/Tests/Microsoft.Teams.Api.Tests/Json/Activity/Message/MessageReactionActivity.json b/Tests/Microsoft.Teams.Api.Tests/Json/Activity/Message/MessageReactionActivity.json index 488ea5f2..f8dd48d9 100644 --- a/Tests/Microsoft.Teams.Api.Tests/Json/Activity/Message/MessageReactionActivity.json +++ b/Tests/Microsoft.Teams.Api.Tests/Json/Activity/Message/MessageReactionActivity.json @@ -15,14 +15,5 @@ "conversation": { "id": "1", "conversationType": "personal" - }, - "reactionsAdded": [ - { - "type": "like", - "user": { - "id": "100", - "userIdentityType": "anonymousGuest" - } - } - ] + } } \ No newline at end of file diff --git a/Tests/Microsoft.Teams.Apps.Tests/Activities/Messages/MessageReactionActivityTests.cs b/Tests/Microsoft.Teams.Apps.Tests/Activities/Messages/MessageReactionActivityTests.cs index b5f9623c..d562e4cd 100644 --- a/Tests/Microsoft.Teams.Apps.Tests/Activities/Messages/MessageReactionActivityTests.cs +++ b/Tests/Microsoft.Teams.Apps.Tests/Activities/Messages/MessageReactionActivityTests.cs @@ -39,12 +39,12 @@ public async Task Should_CallHandler() return Task.CompletedTask; }); - var res = await _app.Process(_token, new MessageReactionActivity().AddReaction(Api.Messages.ReactionType.Angry)); + var res = await _app.Process(_token, new MessageReactionActivity()); Assert.Equal(System.Net.HttpStatusCode.OK, res.Status); Assert.Equal(2, calls); - Assert.Equal(2, _controller.Calls); - Assert.Equal(4, res.Meta.Routes); + Assert.Equal(1, _controller.Calls); + Assert.Equal(3, res.Meta.Routes); } [Fact] @@ -98,12 +98,12 @@ public async Task Should_CallHandler_OnAdd() return Task.CompletedTask; }); - var res = await _app.Process(_token, new MessageReactionActivity().AddReaction(Api.Messages.ReactionType.Angry)); + var res = await _app.Process(_token, new MessageReactionActivity()); Assert.Equal(System.Net.HttpStatusCode.OK, res.Status); - Assert.Equal(3, calls); - Assert.Equal(2, _controller.Calls); - Assert.Equal(5, res.Meta.Routes); + Assert.Equal(2, calls); + Assert.Equal(1, _controller.Calls); + Assert.Equal(3, res.Meta.Routes); } [Fact] @@ -137,12 +137,12 @@ public async Task Should_CallHandler_OnRemove() return Task.CompletedTask; }); - var res = await _app.Process(_token, new MessageReactionActivity().RemoveReaction(Api.Messages.ReactionType.Angry)); + var res = await _app.Process(_token, new MessageReactionActivity()); Assert.Equal(System.Net.HttpStatusCode.OK, res.Status); - Assert.Equal(3, calls); - Assert.Equal(2, _controller.Calls); - Assert.Equal(5, res.Meta.Routes); + Assert.Equal(2, calls); + Assert.Equal(1, _controller.Calls); + Assert.Equal(3, res.Meta.Routes); } [TeamsController] diff --git a/Tests/Microsoft.Teams.Apps.Tests/Microsoft.Teams.Apps.Tests.csproj b/Tests/Microsoft.Teams.Apps.Tests/Microsoft.Teams.Apps.Tests.csproj index 03f2566f..2da9ef9e 100644 --- a/Tests/Microsoft.Teams.Apps.Tests/Microsoft.Teams.Apps.Tests.csproj +++ b/Tests/Microsoft.Teams.Apps.Tests/Microsoft.Teams.Apps.Tests.csproj @@ -1,29 +1,30 @@  - - net8.0;net10.0 - enable - enable - false - + + net8.0;net10.0 + enable + enable + false + $(NoWarn);CS0618 + - - - - - - - - - + + + + + + + + + - - - + + + - - - - + + + + From 48e5d4c2f5ca17d519862c60987473c0aea7b2ac Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:10:59 -0800 Subject: [PATCH 05/11] Address review feedback: fix docs, formatting, and add test coverage (#336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review comments from PR #335 by fixing documentation errors, code formatting issues, and adding missing test coverage. **Documentation fixes:** - Fixed copyright header in `ReactionClient.cs` (added "All rights reserved.") - Removed incorrect `userId` parameter references from XML docs in `CreateOrUpdateAsync` and `DeleteAsync` methods - Completed incomplete XML comment in `Reaction.cs`: "The type of reaction given to the message." - Added comprehensive XML documentation for `ApiClient.Client` property explaining its purpose and usage caveats **Code quality:** - Fixed grammatical error in `Samples.Reactions/Program.cs`: "you reacted with added" → "you added" - Removed inconsistent extra blank line in `ApiClient` constructor **Test coverage:** - Added `ReactionClientTests.cs` with test methods for `CreateOrUpdateAsync` and `DeleteAsync` following existing client test patterns --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> --- .../Microsoft.Teams.Api/Clients/ApiClient.cs | 13 +++- .../Clients/ReactionClient.cs | 13 ++-- .../Microsoft.Teams.Api/Messages/Reaction.cs | 2 +- Samples/Samples.Reactions/Program.cs | 2 +- .../Clients/ReactionClientTests.cs | 74 +++++++++++++++++++ 5 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs diff --git a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs index 0c2f04ec..b1588d2a 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs @@ -14,6 +14,18 @@ public class ApiClient : Client public virtual TeamClient Teams { get; } public virtual MeetingClient Meetings { get; } + /// + /// Gets the underlying instance used by this + /// and its sub-clients to perform HTTP requests. + /// + /// + /// This property is provided for advanced scenarios where you need to issue custom HTTP + /// calls that are not yet covered by the strongly-typed clients exposed by . + /// Prefer using the typed clients (, , + /// , , ) whenever possible. + /// Relying on this property may couple your code to the current HTTP implementation and + /// could limit future refactoring of the underlying client. + /// public IHttpClient Client { get => base._http; } public ApiClient(string serviceUrl, CancellationToken cancellationToken = default) : base(cancellationToken) @@ -34,7 +46,6 @@ public ApiClient(string serviceUrl, IHttpClient client, CancellationToken cancel Users = new UserClient(_http, cancellationToken); Teams = new TeamClient(serviceUrl, _http, cancellationToken); Meetings = new MeetingClient(serviceUrl, _http, cancellationToken); - } public ApiClient(string serviceUrl, IHttpClientOptions options, CancellationToken cancellationToken = default) : base(options, cancellationToken) diff --git a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs index 5759d686..ea166991 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Microsoft.Teams.Api.Messages; @@ -45,11 +45,8 @@ public ReactionClient(string serviceUrl, IHttpClientFactory factory, Cancellatio /// /// The reaction type (for example: "like", "heart", "laugh", etc.). /// - /// - /// Optional id of the user on whose behalf the reaction is added/updated (if supported by the service). - /// /// - /// A describing the reaction, or null if the service returned an empty body. + /// A representing the asynchronous operation. /// public async Task CreateOrUpdateAsync( string conversationId, @@ -72,9 +69,9 @@ ReactionType reactionType /// /// The reaction type to remove (for example: "like", "heart", "laugh", etc.). /// - /// - /// Optional id of the user whose reaction should be removed (if supported by the service). - /// + /// + /// A representing the asynchronous operation. + /// public async Task DeleteAsync( string conversationId, string activityId, diff --git a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs index ec8613c5..1bbb568b 100644 --- a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs +++ b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs @@ -8,7 +8,7 @@ namespace Microsoft.Teams.Api.Messages; /// -/// The type of reaction given to the +/// The type of reaction given to the message. /// [JsonConverter(typeof(JsonConverter))] public class ReactionType(string value) : StringEnum(value) diff --git a/Samples/Samples.Reactions/Program.cs b/Samples/Samples.Reactions/Program.cs index 7b25d830..20be7dc7 100644 --- a/Samples/Samples.Reactions/Program.cs +++ b/Samples/Samples.Reactions/Program.cs @@ -32,7 +32,7 @@ teams.OnMessageReaction(async (context, cancellationToken) => { context.Log.Info($"Reaction '{context.Activity.ReactionsAdded?.FirstOrDefault()?.Type}' added by {context.Activity.From?.Name}"); - await context.Send($"you reacted with added '{context.Activity.ReactionsAdded?.FirstOrDefault()?.Type}' " + + await context.Send($"you added '{context.Activity.ReactionsAdded?.FirstOrDefault()?.Type}' " + $"and removed '{context.Activity.ReactionsRemoved?.FirstOrDefault()?.Type}'", cancellationToken); }); diff --git a/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs b/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs new file mode 100644 index 00000000..cef8b1f8 --- /dev/null +++ b/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs @@ -0,0 +1,74 @@ +using System.Net; + +using Microsoft.Teams.Api.Clients; +using Microsoft.Teams.Api.Messages; +using Microsoft.Teams.Common.Http; + +using Moq; + +namespace Microsoft.Teams.Api.Tests.Clients; + +public class ReactionClientTests +{ + [Fact] + public async Task ReactionClient_CreateOrUpdateAsync() + { + var responseMessage = new HttpResponseMessage(); + responseMessage.Headers.Add("Custom-Header", "HeaderValue"); + var mockHandler = new Mock(); + mockHandler + .Setup(handler => handler.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponse() + { + Headers = responseMessage.Headers, + StatusCode = HttpStatusCode.OK, + Body = string.Empty + }); + + string serviceUrl = "https://serviceurl.com/"; + var reactionClient = new ReactionClient(serviceUrl, mockHandler.Object); + string conversationId = "conversationId"; + string activityId = "activityId"; + var reactionType = ReactionType.Like; + + await reactionClient.CreateOrUpdateAsync(conversationId, activityId, reactionType); + + string expectedUrl = "https://serviceurl.com/v3/conversations/conversationId/activities/activityId/reactions/like"; + HttpMethod expectedMethod = HttpMethod.Put; + mockHandler.Verify(x => x.SendAsync( + It.Is(arg => arg.Url == expectedUrl && arg.Method == expectedMethod), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task ReactionClient_DeleteAsync() + { + var responseMessage = new HttpResponseMessage(); + responseMessage.Headers.Add("Custom-Header", "HeaderValue"); + var mockHandler = new Mock(); + mockHandler + .Setup(handler => handler.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponse() + { + Headers = responseMessage.Headers, + StatusCode = HttpStatusCode.OK, + Body = string.Empty + }); + + string serviceUrl = "https://serviceurl.com/"; + var reactionClient = new ReactionClient(serviceUrl, mockHandler.Object); + string conversationId = "conversationId"; + string activityId = "activityId"; + var reactionType = ReactionType.Heart; + + await reactionClient.DeleteAsync(conversationId, activityId, reactionType); + + string expectedUrl = "https://serviceurl.com/v3/conversations/conversationId/activities/activityId/reactions/heart"; + HttpMethod expectedMethod = HttpMethod.Delete; + mockHandler.Verify(x => x.SendAsync( + It.Is(arg => arg.Url == expectedUrl && arg.Method == expectedMethod), + It.IsAny()), + Times.Once); + } +} From 91159ca775e9b6e9b209de8111be95a91fc95af1 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 17 Feb 2026 14:24:46 -0800 Subject: [PATCH 06/11] Rename CreateOrUpdateAsync to AddAsync in ReactionClient Renamed the CreateOrUpdateAsync method to AddAsync in the ReactionClient class for clarity. Updated XML documentation to specify that the method adds a reaction. Refactored all usages in Program.cs and ReactionClientTests.cs to use the new method name. No changes to method behavior or parameters. --- Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs | 4 ++-- Samples/Samples.Reactions/Program.cs | 4 ++-- .../Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs index ea166991..71390214 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs @@ -38,7 +38,7 @@ public ReactionClient(string serviceUrl, IHttpClientFactory factory, Cancellatio } /// - /// Creates or updates a reaction on an activity in a conversation. + /// Adds a reaction on an activity in a conversation. /// /// The conversation id. /// The id of the activity to react to. @@ -48,7 +48,7 @@ public ReactionClient(string serviceUrl, IHttpClientFactory factory, Cancellatio /// /// A representing the asynchronous operation. /// - public async Task CreateOrUpdateAsync( + public async Task AddAsync( string conversationId, string activityId, ReactionType reactionType diff --git a/Samples/Samples.Reactions/Program.cs b/Samples/Samples.Reactions/Program.cs index 20be7dc7..7c9f589b 100644 --- a/Samples/Samples.Reactions/Program.cs +++ b/Samples/Samples.Reactions/Program.cs @@ -19,10 +19,10 @@ // replace with context.Api.Conversations.Reactions once APX is rolledout to PROD var api = new ApiClient(context.Activity.ServiceUrl!, context.Api.Client, cancellationToken); - await api.Conversations.Reactions.CreateOrUpdateAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f44b_wavinghand-tone4")); + await api.Conversations.Reactions.AddAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f44b_wavinghand-tone4")); await Task.Delay(2000, cancellationToken); - await api.Conversations.Reactions.CreateOrUpdateAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f601_beamingfacewithsmilingeyes")); + await api.Conversations.Reactions.AddAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f601_beamingfacewithsmilingeyes")); await Task.Delay(2000, cancellationToken); await api.Conversations.Reactions.DeleteAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f601_beamingfacewithsmilingeyes")); diff --git a/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs b/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs index cef8b1f8..e0b0234c 100644 --- a/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs +++ b/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs @@ -31,7 +31,7 @@ public async Task ReactionClient_CreateOrUpdateAsync() string activityId = "activityId"; var reactionType = ReactionType.Like; - await reactionClient.CreateOrUpdateAsync(conversationId, activityId, reactionType); + await reactionClient.AddAsync(conversationId, activityId, reactionType); string expectedUrl = "https://serviceurl.com/v3/conversations/conversationId/activities/activityId/reactions/like"; HttpMethod expectedMethod = HttpMethod.Put; From 7c58a953ceb47a8f7ea2c6f8d7b795abf5409262 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 17 Feb 2026 14:30:47 -0800 Subject: [PATCH 07/11] Remove PlusOne reaction type from ReactionType class The PlusOne static field and IsPlusOne property were deleted from the ReactionType class, eliminating support for the "plus-one" reaction. --- Libraries/Microsoft.Teams.Api/Messages/Reaction.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs index 1bbb568b..da8c5a06 100644 --- a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs +++ b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs @@ -73,11 +73,6 @@ public class ReactionType(string value) : StringEnum(value) public static readonly ReactionType Angry = new("angry"); public bool IsAngry => Angry.Equals(Value); - /// - /// plus-one - /// - public static readonly ReactionType PlusOne = new("plusOne"); - public bool IsPlusOne => PlusOne.Equals(Value); } /// From 0ac3b38b938274059c885d29c2e171cee54e59b1 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 17 Feb 2026 14:59:16 -0800 Subject: [PATCH 08/11] Fix JSON structure in launchSettings.TEMPLATE.json Corrected closing brackets to ensure valid JSON formatting. Removed extraneous characters at the end of the file. --- .../Samples.Reactions/Properties/launchSettings.TEMPLATE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/Samples.Reactions/Properties/launchSettings.TEMPLATE.json b/Samples/Samples.Reactions/Properties/launchSettings.TEMPLATE.json index 4fd0838c..6fc6823f 100644 --- a/Samples/Samples.Reactions/Properties/launchSettings.TEMPLATE.json +++ b/Samples/Samples.Reactions/Properties/launchSettings.TEMPLATE.json @@ -13,4 +13,4 @@ } } } -} \ No newline at end of file +} From be9d9101a4f7333d26c63bf5d15a2b0fb27c546c Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 24 Feb 2026 13:58:48 -0800 Subject: [PATCH 09/11] Update reaction handling and types; improve async API usage Added obsolete AddReaction/RemoveReaction methods to MessageReactionActivity for direct manipulation of reactions. ReactionClient's AddAsync and DeleteAsync now accept optional CancellationToken. Refactored ReactionType: removed legacy types, added new Unicode-based types (Eyes, CheckMark, Launch, Pushpin). Updated Program.cs comment to reference Reactions client. These changes modernize reaction management and enhance async operation support. --- .../Message/MessageReactionActivity.cs | 58 +++++++++++++++++++ .../Clients/ReactionClient.cs | 12 ++-- .../Microsoft.Teams.Api/Messages/Reaction.cs | 49 ++++------------ Samples/Samples.Reactions/Program.cs | 4 +- 4 files changed, 80 insertions(+), 43 deletions(-) diff --git a/Libraries/Microsoft.Teams.Api/Activities/Message/MessageReactionActivity.cs b/Libraries/Microsoft.Teams.Api/Activities/Message/MessageReactionActivity.cs index e0907628..e1d40bc3 100644 --- a/Libraries/Microsoft.Teams.Api/Activities/Message/MessageReactionActivity.cs +++ b/Libraries/Microsoft.Teams.Api/Activities/Message/MessageReactionActivity.cs @@ -22,4 +22,62 @@ public class MessageReactionActivity() : Activity(ActivityType.MessageReaction) [JsonPropertyName("reactionsRemoved")] [JsonPropertyOrder(122)] public IList? ReactionsRemoved { get; set; } + + [Obsolete("Use the Reactions client instead.")] + public MessageReactionActivity AddReaction(Messages.Reaction reaction) + { + ReactionsAdded ??= []; + ReactionsAdded.Add(reaction); + return this; + } + + [Obsolete("Use the Reactions client instead.")] + public MessageReactionActivity AddReaction(Messages.ReactionType type) + { + ReactionsAdded ??= []; + ReactionsAdded.Add(new() { Type = type }); + return this; + } + + [Obsolete("Use the Reactions client instead.")] + public MessageReactionActivity RemoveReaction(Messages.Reaction reaction) + { + ReactionsRemoved ??= []; + + if (ReactionsAdded is not null) + { + var i = ReactionsAdded.ToList().FindIndex(r => + r.Type.Equals(reaction.Type) && r.User?.Id == reaction.User?.Id + ); + + if (i > -1) + { + ReactionsAdded.RemoveAt(i); + return this; + } + } + + ReactionsRemoved.Add(reaction); + return this; + } + + [Obsolete("Use the Reactions client instead.")] + public MessageReactionActivity RemoveReaction(Messages.ReactionType type) + { + ReactionsRemoved ??= []; + + if (ReactionsAdded is not null) + { + var i = ReactionsAdded.ToList().FindIndex(r => r.Type.Equals(type)); + + if (i > -1) + { + ReactionsAdded.RemoveAt(i); + return this; + } + } + + ReactionsRemoved.Add(new() { Type = type }); + return this; + } } \ No newline at end of file diff --git a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs index 71390214..1091a916 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs @@ -45,20 +45,22 @@ public ReactionClient(string serviceUrl, IHttpClientFactory factory, Cancellatio /// /// The reaction type (for example: "like", "heart", "laugh", etc.). /// + /// A to observe while waiting for the task to complete. /// /// A representing the asynchronous operation. /// public async Task AddAsync( string conversationId, string activityId, - ReactionType reactionType + ReactionType reactionType, + CancellationToken cancellationToken = default ) { // Assumed route: // PUT v3/conversations/{conversationId}/activities/{activityId}/reactions var url = $"{ServiceUrl}v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}"; var req = HttpRequest.Put(url); - await _http.SendAsync(req, _cancellationToken); + await _http.SendAsync(req, cancellationToken != default ? cancellationToken : _cancellationToken); } /// @@ -69,13 +71,15 @@ ReactionType reactionType /// /// The reaction type to remove (for example: "like", "heart", "laugh", etc.). /// + /// A to observe while waiting for the task to complete. /// /// A representing the asynchronous operation. /// public async Task DeleteAsync( string conversationId, string activityId, - ReactionType reactionType + ReactionType reactionType, + CancellationToken cancellationToken = default ) { // Assumed route: @@ -85,6 +89,6 @@ ReactionType reactionType var req = HttpRequest.Delete(url); - await _http.SendAsync(req, _cancellationToken); + await _http.SendAsync(req, cancellationToken != default ? cancellationToken : _cancellationToken); } } diff --git a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs index da8c5a06..974f0fd4 100644 --- a/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs +++ b/Libraries/Microsoft.Teams.Api/Messages/Reaction.cs @@ -26,53 +26,28 @@ public class ReactionType(string value) : StringEnum(value) public bool IsHeart => Heart.Equals(Value); /// - /// ✅ - /// - public static readonly ReactionType Checkmark = new("checkmark"); - public bool IsCheckmark => Checkmark.Equals(Value); - - /// - /// ⏳ - /// - public static readonly ReactionType Hourglass = new("hourglass"); - public bool IsHourglass => Hourglass.Equals(Value); - - /// - /// 📌 + /// 👀 /// - public static readonly ReactionType Pushpin = new("pushpin"); - public bool IsPushpin => Pushpin.Equals(Value); + public static readonly ReactionType Eyes = new("1f440_eyes"); + public bool IsEyes => Eyes.Equals(Value); /// - /// ❗ - /// - public static readonly ReactionType Exclamation = new("exclamation"); - public bool IsExclamation => Exclamation.Equals(Value); - - /// - /// 😆 - /// - public static readonly ReactionType Laugh = new("laugh"); - public bool IsLaugh => Laugh.Equals(Value); - - /// - /// 😮 + /// ✅ /// - public static readonly ReactionType Surprise = new("surprise"); - public bool IsSurprise => Surprise.Equals(Value); + public static readonly ReactionType CheckMark = new("2705_whiteheavycheckmark"); + public bool IsCheckMark => CheckMark.Equals(Value); /// - /// 🙁 + /// 🚀 /// - public static readonly ReactionType Sad = new("sad"); - public bool IsSad => Sad.Equals(Value); + public static readonly ReactionType Launch = new("launch"); + public bool IsLaunch => Launch.Equals(Value); /// - /// 😠 + /// 📌 /// - public static readonly ReactionType Angry = new("angry"); - public bool IsAngry => Angry.Equals(Value); - + public static readonly ReactionType Pushpin = new("1f4cc_pushpin"); + public bool IsPushpin => Pushpin.Equals(Value); } /// diff --git a/Samples/Samples.Reactions/Program.cs b/Samples/Samples.Reactions/Program.cs index 7c9f589b..d4890117 100644 --- a/Samples/Samples.Reactions/Program.cs +++ b/Samples/Samples.Reactions/Program.cs @@ -15,8 +15,8 @@ { await context.Send($"you said '{context.Activity.Text}'", cancellationToken); - - // replace with context.Api.Conversations.Reactions once APX is rolledout to PROD + + // replace with context.Api.Conversations.Reactions once Reactions client is available in PROD. var api = new ApiClient(context.Activity.ServiceUrl!, context.Api.Client, cancellationToken); await api.Conversations.Reactions.AddAsync(context.Activity.Conversation.Id, context.Activity.Id, new ReactionType("1f44b_wavinghand-tone4")); From f1527a039f8ab3885c140056b9b652aaf3b10bc3 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 24 Feb 2026 14:06:53 -0800 Subject: [PATCH 10/11] Update tests to check for "Like" instead of "Angry" reaction Changed all relevant assertions in MessageReactionActivityTests.cs to verify the "Like" reaction type rather than "Angry". This ensures the tests align with the expected reaction type behavior. --- .../Messages/MessageReactionActivityTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/Microsoft.Teams.Apps.Tests/Activities/Messages/MessageReactionActivityTests.cs b/Tests/Microsoft.Teams.Apps.Tests/Activities/Messages/MessageReactionActivityTests.cs index d562e4cd..d1b6b1b5 100644 --- a/Tests/Microsoft.Teams.Apps.Tests/Activities/Messages/MessageReactionActivityTests.cs +++ b/Tests/Microsoft.Teams.Apps.Tests/Activities/Messages/MessageReactionActivityTests.cs @@ -35,7 +35,7 @@ public async Task Should_CallHandler() calls++; Assert.True(context.Activity.Type.IsMessageReaction); Assert.Single(context.Activity.ReactionsAdded ?? []); - Assert.True(context.Activity.ReactionsAdded!.First().Type.IsAngry); + Assert.True(context.Activity.ReactionsAdded!.First().Type.IsLike); return Task.CompletedTask; }); @@ -84,7 +84,7 @@ public async Task Should_CallHandler_OnAdd() calls++; Assert.True(context.Activity.Type.IsMessageReaction); Assert.Single(context.Activity.ReactionsAdded ?? []); - Assert.True(context.Activity.ReactionsAdded!.First().Type.IsAngry); + Assert.True(context.Activity.ReactionsAdded!.First().Type.IsLike); return context.Next(); }); @@ -94,7 +94,7 @@ public async Task Should_CallHandler_OnAdd() Assert.True(context.Activity.Type.IsMessageReaction); Assert.Single(context.Activity.ReactionsAdded ?? []); Assert.Empty(context.Activity.ReactionsRemoved ?? []); - Assert.True(context.Activity.ReactionsAdded!.First().Type.IsAngry); + Assert.True(context.Activity.ReactionsAdded!.First().Type.IsLike); return Task.CompletedTask; }); @@ -123,7 +123,7 @@ public async Task Should_CallHandler_OnRemove() calls++; Assert.True(context.Activity.Type.IsMessageReaction); Assert.Single(context.Activity.ReactionsRemoved ?? []); - Assert.True(context.Activity.ReactionsRemoved!.First().Type.IsAngry); + Assert.True(context.Activity.ReactionsRemoved!.First().Type.IsLike); return context.Next(); }); @@ -133,7 +133,7 @@ public async Task Should_CallHandler_OnRemove() Assert.True(context.Activity.Type.IsMessageReaction); Assert.Single(context.Activity.ReactionsAdded ?? []); Assert.Empty(context.Activity.ReactionsRemoved ?? []); - Assert.True(context.Activity.ReactionsRemoved!.First().Type.IsAngry); + Assert.True(context.Activity.ReactionsRemoved!.First().Type.IsLike); return Task.CompletedTask; }); From 1caf0dd07373c1ee9f02f1d299e57d196c0ef887 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 24 Feb 2026 16:06:57 -0800 Subject: [PATCH 11/11] Update ReactionClient API route and docs; rename test - Changed reaction API route to include reactionType in the URL path. - Updated XML docs to use "launch" as example reaction type. - Removed outdated comment in Program.cs and used new Reactions client. - Renamed test method to match AddAsync method. --- Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs | 8 ++++---- Samples/Samples.Reactions/Program.cs | 1 - .../Clients/ReactionClientTests.cs | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs index 1091a916..85ae72a2 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/ReactionClient.cs @@ -43,7 +43,7 @@ public ReactionClient(string serviceUrl, IHttpClientFactory factory, Cancellatio /// The conversation id. /// The id of the activity to react to. /// - /// The reaction type (for example: "like", "heart", "laugh", etc.). + /// The reaction type (for example: "like", "heart", "launch", etc.). /// /// A to observe while waiting for the task to complete. /// @@ -57,7 +57,7 @@ public async Task AddAsync( ) { // Assumed route: - // PUT v3/conversations/{conversationId}/activities/{activityId}/reactions + // PUT v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType} var url = $"{ServiceUrl}v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}"; var req = HttpRequest.Put(url); await _http.SendAsync(req, cancellationToken != default ? cancellationToken : _cancellationToken); @@ -69,7 +69,7 @@ public async Task AddAsync( /// The conversation id. /// The id of the activity the reaction is on. /// - /// The reaction type to remove (for example: "like", "heart", "laugh", etc.). + /// The reaction type to remove (for example: "like", "heart", "launch", etc.). /// /// A to observe while waiting for the task to complete. /// @@ -78,7 +78,7 @@ public async Task AddAsync( public async Task DeleteAsync( string conversationId, string activityId, - ReactionType reactionType, + ReactionType reactionType, CancellationToken cancellationToken = default ) { diff --git a/Samples/Samples.Reactions/Program.cs b/Samples/Samples.Reactions/Program.cs index d4890117..248b4819 100644 --- a/Samples/Samples.Reactions/Program.cs +++ b/Samples/Samples.Reactions/Program.cs @@ -15,7 +15,6 @@ { await context.Send($"you said '{context.Activity.Text}'", cancellationToken); - // replace with context.Api.Conversations.Reactions once Reactions client is available in PROD. var api = new ApiClient(context.Activity.ServiceUrl!, context.Api.Client, cancellationToken); diff --git a/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs b/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs index e0b0234c..ce4722ac 100644 --- a/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs +++ b/Tests/Microsoft.Teams.Api.Tests/Clients/ReactionClientTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.Teams.Api.Tests.Clients; public class ReactionClientTests { [Fact] - public async Task ReactionClient_CreateOrUpdateAsync() + public async Task ReactionClient_AddAsync() { var responseMessage = new HttpResponseMessage(); responseMessage.Headers.Add("Custom-Header", "HeaderValue");