你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
教程:在聊天应用中启用内联图像支持
聊天 SDK 旨在无缝地与 Microsoft Teams 配合使用。 具体而言,聊天 SDK 提供了一种解决方案,用于接收内联图像并将内联图像从 Microsoft Teams 发送给用户。
本教程介绍如何使用用于 JavaScript 的 Azure 通信服务聊天 SDK 启用内联图像支持。
内联图像是直接复制并粘贴到 Teams 客户端的发送框中的图像。 对于通过“从此设备上传”菜单或通过拖放上传的图像(例如直接拖动到 Teams 中的发送框的图像),需要参考本教程作为文件共享功能的一部分。 (请参阅“处理图像附件”不分。)
若要复制图像,Teams 用户有两个选项:
- 使用其操作系统的上下文菜单复制图像文件,然后将其粘贴到 Teams 客户端的发送框中。
- 使用键盘快捷方式。
在本教程中,你将了解在以下情况下需要执行的操作:
注意
发送内联图像的功能目前以公共预览版提供。 该功能仅适用于 JavaScript。 接收内联图像的功能目前已正式发布。 它适用于 Teams 互操作性聊天中的 JavaScript 和 C#。
先决条件
- 查看快速入门:将聊天应用加入 Teams 会议。
- 创建 Azure 通信服务资源。 有关详细信息,请参阅创建 Azure 通信服务资源。 需要为此教程记录连接字符串。
- 使用企业帐户设置 Teams 会议,并准备好会议 URL。
- 使用适用于 JavaScript 的聊天 SDK (@azure/communication-chat) 1.4.0 或最新版本。 有关详细信息,请参阅适用于 JavaScript 的 Azure 通信聊天客户端库。
代码示例
在 GitHub 上找到本教程的最终代码。
处理新消息事件中收到的内联图像
在本部分中,你将了解如何呈现嵌入在新消息接收事件的消息内容中的内联图像。
在快速入门中,你为 chatMessageReceived
事件创建了事件处理程序,当你收到来自 Teams 用户的新消息时,会触发该事件处理程序。 你还会在收到来自 chatClient
的 chatMessageReceived
事件后直接将传入消息内容追加到 messageContainer
,如下所示:
chatClient.on("chatMessageReceived", (e) => {
console.log("Notification chatMessageReceived!");
// Check whether the notification is intended for the current thread
if (threadIdInput.value != e.threadId) {
return;
}
if (e.sender.communicationUserId != userId) {
renderReceivedMessage(e.message);
}
else {
renderSentMessage(e.message);
}
});
async function renderReceivedMessage(message) {
messages += '<div class="container lighter">' + message + '</div>';
messagesContainer.innerHTML = messages;
}
在类型为 ChatMessageReceivedEvent
的传入事件中,名为 attachments
的属性包含有关内联图像的信息。 只需在 UI 中呈现内联图像:
export interface ChatMessageReceivedEvent extends BaseChatMessageEvent {
/**
* Content of the message.
*/
message: string;
/**
* Metadata of the message.
*/
metadata: Record<string, string>;
/**
* Chat message attachment.
*/
attachments?: ChatAttachment[];
}
export interface ChatAttachment {
/** Id of the attachment */
id: string;
/** The type of attachment. */
attachmentType: ChatAttachmentType;
/** The name of the attachment content. */
name?: string;
/** The URL where the attachment can be downloaded */
url?: string;
/** The URL where the preview of attachment can be downloaded */
previewUrl?: string;
}
export type ChatAttachmentType = "image" | "unknown";
现在,回到前面的代码,添加一些额外的逻辑,如以下代码片段所示:
chatClient.on("chatMessageReceived", (e) => {
console.log("Notification chatMessageReceived!");
// Check whether the notification is intended for the current thread
if (threadIdInput.value != e.threadId) {
return;
}
const isMyMessage = e.sender.communicationUserId === userId;
renderReceivedMessage(e, isMyMessage);
});
function renderReceivedMessage(e, isMyMessage) {
const messageContent = e.message;
const card = document.createElement('div');
card.className = isMyMessage ? "container darker" : "container lighter";
card.innerHTML = messageContent;
messagesContainer.appendChild(card);
// Filter out inline images from attachments
const imageAttachments = e.attachments.filter((e) =>
e.attachmentType.toLowerCase() === 'image');
// Fetch and render preview images
fetchPreviewImages(imageAttachments);
// Set up onclick event handler to fetch full-scale image
setImgHandler(card, imageAttachments);
}
function setImgHandler(element, imageAttachments) {
// Do nothing if there are no image attachments
if (!imageAttachments.length > 0) {
return;
}
const imgs = element.getElementsByTagName('img');
for (const img of imgs) {
img.addEventListener('click', (e) => {
// Fetch full-scale image upon click
fetchFullScaleImage(e, imageAttachments);
});
}
}
async function fetchPreviewImages(attachments) {
if (!attachments.length > 0) {
return;
}
// Since each message could contain more than one inline image
// we need to fetch them individually
const result = await Promise.all(
attachments.map(async (attachment) => {
// Fetch preview image from its 'previewURL'
const response = await fetch(attachment.previewUrl, {
method: 'GET',
headers: {
// The token here should be the same one from chat initialization
'Authorization': 'Bearer ' + tokenString,
},
});
// The response would be in an image blob, so we can render it directly
return {
id: attachment.id,
content: await response.blob(),
};
}),
);
result.forEach((imageResult) => {
const urlCreator = window.URL || window.webkitURL;
const url = urlCreator.createObjectURL(imageResult.content);
// Look up the image ID and replace its 'src' with object URL
document.getElementById(imageResult.id).src = url;
});
}
在此示例中,你创建了两个帮助程序函数,fetchPreviewImages
和 setImgHandler
。 第一个直接从每个 ChatAttachment
对象中提供的带有 auth 标头的 previewURL
获取预览图像。 按照同样的方式,为函数 setImgHandler
中的每个图像设置 onclick
事件。 在事件处理程序中,可以使用 auth 标头从 ChatAttachment
对象的属性 url
中获取全尺寸图像。
现在,你需要将令牌公开到全局级别,因为你需要用它来构造一个 auth 标头。 你需要修改以下代码:
// New variable for token string
var tokenString = '';
async function init() {
....
let tokenResponse = await identityClient.getToken(identityResponse, [
"voip",
"chat"
]);
const { token, expiresOn } = tokenResponse;
// Save to token string
tokenString = token;
...
}
若要在覆盖层中显示全尺寸图像,你还需要添加新组件:
<div class="overlay" id="overlay-container">
<div class="content">
<img id="full-scale-image" src="" alt="" />
</div>
</div>
使用一些 CSS:
/* let's make chat popup scrollable */
.chat-popup {
...
max-height: 650px;
overflow-y: scroll;
}
.overlay {
position: fixed;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, .7);
top: 0;
left: 0;
z-index: 100;
}
.overlay .content {
position: fixed;
width: 100%;
height: 100%;
text-align: center;
overflow: hidden;
z-index: 100;
margin: auto;
background-color: rgba(0, 0, 0, .7);
}
.overlay img {
position: absolute;
display: block;
max-height: 90%;
max-width: 90%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#overlay-container {
display: none
}
现在,你已经设置了覆盖层,接下来可以处理逻辑来呈现全尺寸图像。 回想一下,你创建了一个 onClick
事件处理程序来调用函数 fetchFullScaleImage
:
const overlayContainer = document.getElementById('overlay-container');
const loadingImageOverlay = document.getElementById('full-scale-image');
function fetchFullScaleImage(e, imageAttachments) {
// Get the image ID from the clicked image element
const link = imageAttachments.filter((attachment) =>
attachment.id === e.target.id)[0].url;
loadingImageOverlay.src = '';
// Fetch the image
fetch(link, {
method: 'GET',
headers: {'Authorization': 'Bearer ' + tokenString},
}).then(async (result) => {
// Now we set image blob to our overlay element
const content = await result.blob();
const urlCreator = window.URL || window.webkitURL;
const url = urlCreator.createObjectURL(content);
loadingImageOverlay.src = url;
});
// Show overlay
overlayContainer.style.display = 'block';
}
最后要添加的是在单击图像时消除覆盖层的功能:
loadingImageOverlay.addEventListener('click', () => {
overlayContainer.style.display = 'none';
});
现在,你已经完成了为来自实时通知的消息呈现内联图像所需的所有更改。
运行代码
Webpack 用户可以使用 webpack-dev-server
生成并运行应用。 运行以下命令,在本地 Web 服务器上捆绑应用程序主机:
npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map
演示
打开浏览器并转到http://localhost:8080/
。 输入会议 URL 和会话 ID。 从 Teams 客户端发送一些内联图像。
然后,应会看到呈现的新消息以及预览图像。
Azure 通信服务用户选择预览图像后,将出现一个覆盖层,其中包含 Teams 用户发送的全尺寸图像。
处理在新消息请求中发送内联图像
重要
Azure 通信服务的这一功能目前以预览版提供。
预览版 API 和 SDK 在没有服务级别协议的情况下提供。 建议不要将它们用于生产工作负荷。 某些功能可能不受支持或者已受限。
有关详细信息,请参阅 Microsoft Azure 预览版补充使用条款。
除了处理内联图像的消息外,适用于 JavaScript 的聊天 SDK 还提供一种解决方案,允许通信用户通过互操作性聊天将内联图像发送到 Microsoft Teams 用户。
来看看 ChatThreadClient
中的新 API:
var imageAttachment = await chatThreadClient.uploadImage(blob, file.name, {
"onUploadProgress": reportProgressCallback
});
API 采用图像 blob、文件名字符串和报告上传进度的函数回调。
若要向其他聊天参与者发送图像,则需要:
- 通过
uploadImage
API 从ChatThreadClient
上传图像,然后保存返回的对象。 - 撰写邮件内容,并将附件设置为在上一步中保存的返回对象。
- 通过
sendMessage
API 从ChatThreadClient
发送新消息
创建一个接受图像的新文件选取器:
<label for="myfile">Attach images:</label>
<input id="upload" type="file" id="myfile" name="myfile" accept="image/*" multiple>
<input style="display: none;" id="upload-result"></input>
现在,在发生状态更改时设置事件侦听器:
document.getElementById("upload").addEventListener("change", uploadImages);
需要为状态更改时创建一个新函数:
var uploadedImageModels = [];
async function uploadImages(e) {
const files = e.target.files;
if (files.length === 0) {
return;
}
for (let key in files) {
if (files.hasOwnProperty(key)) {
await uploadImage(files[key]);
}
}
}
async function uploadImage(file) {
const buffer = await file.arrayBuffer();
const blob = new Blob([new Uint8Array(buffer)], {type: file.type });
const url = window.URL.createObjectURL(blob);
document.getElementById("upload-result").innerHTML += `<img src="${url}" height="auto" width="100" />`;
let uploadedImageModel = await chatThreadClient.uploadImage(blob, file.name, {
imageBytesLength: file.size
});
uploadedImageModels.push(uploadedImageModel);
}
在此示例中,你创建了一个 FileReader
,用于将每个图像作为 base64
编码图像读取,然后在调用 ChatSDK API 上传图像之前创建一个 Blob
。 你创建了一个全局 uploadedImageModels
,以保存来自聊天 SDK 的上传图像的数据模型。
最后,需要修改之前创建的 sendMessageButton
事件侦听器以附加上传的图像。
sendMessageButton.addEventListener("click", async () => {
let message = messagebox.value;
let attachments = uploadedImageModels;
// Inject image tags for images we have selected
// so they can be treated as inline images
// Alternatively, we can use some third-party libraries
// to have a rich text editor with inline image support
message += attachments.map((attachment) => `<img id="${attachment.id}" />`).join("");
let sendMessageRequest = {
content: message,
attachments: attachments,
};
let sendMessageOptions = {
senderDisplayName: "Jack",
type: "html"
};
let sendChatMessageResult = await chatThreadClient.sendMessage(
sendMessageRequest,
sendMessageOptions
);
let messageId = sendChatMessageResult.id;
uploadedImageModels = [];
messagebox.value = "";
document.getElementById("upload").value = "";
console.log(`Message sent!, message id:${messageId}`);
});
这就可以了。 现在,运行代码,亲眼看看效果。
运行代码
Webpack 用户可以使用 webpack-dev-server
生成并运行应用。 运行以下命令,在本地 Web 服务器上捆绑应用程序主机:
npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map
演示
打开浏览器并转到http://localhost:8080/
。 发送框中有一个用于附加图像的新部分。
接下来,可以选择要附加的图像。
现在,Teams 用户在选择“发送”按钮时,应该会收到你刚刚发送的图像。
本教程介绍如何使用用于 C# 的 Azure 通信服务聊天 SDK 启用内联图像支持。
本教程介绍如何执行下列操作:
- 处理新消息的内联图像。
先决条件
- 查看快速入门:将聊天应用加入 Teams 会议。
- 创建 Azure 通信服务资源。 有关详细信息,请参阅创建 Azure 通信服务资源。 需要为此教程记录连接字符串。
- 使用企业帐户设置 Teams 会议,并准备好会议 URL。
- 使用用于 C# (Azure.Communication.Chat) 1.3.0 或更高版本的聊天 SDK。 有关详细信息,请参阅适用于 .NET 的 Azure 通信聊天客户端库。
目标
- 获取内联图像附件的
previewUri
属性。
处理新消息的内联图像
在快速入门中,你将轮询消息并将新消息追加到 messageList
属性。 稍后,你将基于此功能进行构建,添加内联图像的解析和提取。
CommunicationUserIdentifier currentUser = new(user_Id_);
AsyncPageable<ChatMessage> allMessages = chatThreadClient.GetMessagesAsync();
SortedDictionary<long, string> messageList = [];
int textMessages = 0;
await foreach (ChatMessage message in allMessages)
{
if (message.Type == ChatMessageType.Html || message.Type == ChatMessageType.Text)
{
textMessages++;
var userPrefix = message.Sender.Equals(currentUser) ? "[you]:" : "";
var strippedMessage = StripHtml(message.Content.Message);
messageList.Add(long.Parse(message.SequenceId), $"{userPrefix}{strippedMessage}");
}
}
在类型为 ChatMessageReceivedEvent
的传入事件中,名为 attachments
的属性包含有关内联图像的信息。 只需在 UI 中呈现内联图像。
public class ChatAttachment
{
public ChatAttachment(string id, ChatAttachmentType attachmentType)
public ChatAttachmentType AttachmentType { get }
public string Id { get }
public string Name { get }
public System.Uri PreviewUrl { get }
public System.Uri Url { get }
}
public struct ChatAttachmentType : System.IEquatable<AttachmentType>
{
public ChatAttachmentType(string value)
public static File { get }
public static Image { get }
}
以下 JSON 示例展示了图像附件的 ChatAttachment
的外观:
"attachments": [
{
"id": "9d89acb2-c4e4-4cab-b94a-7c12a61afe30",
"attachmentType": "image",
"name": "Screenshot.png",
"url": "https://contoso.communication.azure.com/chat/threads/19:9d89acb29d89acb2@thread.v2/images/9d89acb2-c4e4-4cab-b94a-7c12a61afe30/views/original?api-version=2023-11-03",
"previewUrl": "https://contoso.communication.azure.com/chat/threads/19:9d89acb29d89acb2@thread.v2/images/9d89acb2-c4e4-4cab-b94a-7c12a61afe30/views/small?api-version=2023-11-03"
}
]
现在,返回并替换代码,以添加额外的逻辑来分析和提取图像附件:
CommunicationUserIdentifier currentUser = new(user_Id_);
AsyncPageable<ChatMessage> allMessages = chatThreadClient.GetMessagesAsync();
SortedDictionary<long, string> messageList = [];
int textMessages = 0;
await foreach (ChatMessage message in allMessages)
{
// Get message attachments that are of type 'image'
IEnumerable<ChatAttachment> imageAttachments = message.Content.Attachments.Where(x => x.AttachmentType == ChatAttachmentType.Image);
// Fetch image and render
var chatAttachmentImageUris = new List<Uri>();
foreach (ChatAttachment imageAttachment in imageAttachments)
{
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", communicationTokenCredential.GetToken().Token);
var response = await client.GetAsync(imageAttachment.PreviewUri);
var randomAccessStream = await response.Content.ReadAsStreamAsync();
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
var bitmapImage = new BitmapImage();
await bitmapImage.SetSourceAsync(randomAccessStream.AsRandomAccessStream());
InlineImage.Source = bitmapImage;
});
chatAttachmentImageUris.Add(imageAttachment.PreviewUri);
}
// Build message list
if (message.Type == ChatMessageType.Html || message.Type == ChatMessageType.Text)
{
textMessages++;
var userPrefix = message.Sender.Equals(currentUser) ? "[you]:" : "";
var strippedMessage = StripHtml(message.Content.Message);
var chatAttachments = chatAttachmentImageUris.Count > 0 ? "[Attachments]:\n" + string.Join(",\n", chatAttachmentImageUris) : "";
messageList.Add(long.Parse(message.SequenceId), $"{userPrefix}{strippedMessage}\n{chatAttachments}");
}
在此示例中,从 Image
类型的消息中获取所有附件,然后提取每个图像。 出于授权目的,必须在请求头的 Bearer
部分中使用 Token
。 下载图像后,可以将其分配给视图的 InlineImage
元素。
你还包括一个附件 URI 列表,这些 URI 将与短信列表中的消息一起显示。
演示
- 从集成开发环境 (IDE) 中运行应用程序。
- 输入 Teams 会议链接。
- 加入会议。
- 允许 Teams 端的用户。
- 使用图像从 Teams 端发送消息。
消息中包含的 URL 将显示在消息列表中。 接收的最后一个图像呈现在窗口底部。
后续步骤
- 详细了解其他受支持的互操作性功能。
- 查看我们的出色聊天示例。
- 详细了解聊天的工作原理。