快速入門:將聊天應用程式加入至 Teams 會議
將您的聊天解決方案連線到 Microsoft Teams,以開始使用 Azure 通訊服務。
在本快速入門中,您將了解如何使用適用於 JavaScript 的 Azure 通訊服務聊天 SDK 以開始在 Teams 會議中聊天。
範例程式碼
在 GitHub 上尋找本快速入門的最終程式碼。
必要條件
加入會議聊天
通訊服務使用者可使用通話 SDK 以匿名使用者身分加入 Team 會議。 加入會議也會將這些使用者新增為會議交談的參與者,以便可以與會議中的其他使用者傳送及接收訊息。 使用者無法存先前加入會議前傳送的取聊天訊息,而在會議結束時也無法傳送或接收訊息。 若要加入會議並開始聊天,您可以遵循後續步驟。
建立新的 Node.js 應用程式
開啟您的終端機或命令視窗,為您的應用程式建立新的目錄,並瀏覽至該目錄。
mkdir chat-interop-quickstart && cd chat-interop-quickstart
執行 npm init -y
以使用預設設定建立 package.json 檔案。
npm init -y
安裝聊天套件
使用 npm install
命令以安裝所需適用於 JavaScript 的通訊服務 SDK。
npm install @azure/communication-common --save
npm install @azure/communication-identity --save
npm install @azure/communication-chat --save
npm install @azure/communication-calling --save
--save
選項會在您的 package.json 檔案中,將程式庫列為相依性。
設定應用程式架構
本快速入門會使用 Webpack 來組合應用程式資產。 執行下列命令來安裝 Webpack、webpack-cli 和 webpack-dev-server 套件,並將這些套件列為 package.json 中的開發相依性:
npm install webpack@5.89.0 webpack-cli@5.1.4 webpack-dev-server@4.15.1 --save-dev
在專案的根目錄中建立 index.html 檔案。 我們使用此檔案來設定基本配置,讓使用者可加入會議和開始聊天。
新增 Teams UI 控制項
使用下列程式碼片段取代 index.html 中的程式碼。 頁面頂端的文字框將用來輸入Teams會議內容。 [加入 Teams 會議] 按鈕用於加入特定會議。 聊天快顯視窗顯示在頁面的底部。 該視窗可用於在會議對話上傳送訊息,並在通訊服務使用者為成員時即時顯示對話上傳送的任何訊息。
<!DOCTYPE html>
<html>
<head>
<title>Communication Client - Calling and Chat Sample</title>
<style>
body {box-sizing: border-box;}
/* The popup chat - hidden by default */
.chat-popup {
display: none;
position: fixed;
bottom: 0;
left: 15px;
border: 3px solid #f1f1f1;
z-index: 9;
}
.message-box {
display: none;
position: fixed;
bottom: 0;
left: 15px;
border: 3px solid #FFFACD;
z-index: 9;
}
.form-container {
max-width: 300px;
padding: 10px;
background-color: white;
}
.form-container textarea {
width: 90%;
padding: 15px;
margin: 5px 0 22px 0;
border: none;
background: #e1e1e1;
resize: none;
min-height: 50px;
}
.form-container .btn {
background-color: #4CAF40;
color: white;
padding: 14px 18px;
margin-bottom:10px;
opacity: 0.6;
border: none;
cursor: pointer;
width: 100%;
}
.container {
border: 1px solid #dedede;
background-color: #F1F1F1;
border-radius: 3px;
padding: 8px;
margin: 8px 0;
}
.darker {
border-color: #ccc;
background-color: #ffdab9;
margin-left: 25px;
margin-right: 3px;
}
.lighter {
margin-right: 20px;
margin-left: 3px;
}
.container::after {
content: "";
clear: both;
display: table;
}
</style>
</head>
<body>
<h4>Azure Communication Services</h4>
<h1>Calling and Chat Quickstart</h1>
<input id="teams-link-input" type="text" placeholder="Teams meeting link"
style="margin-bottom:1em; width: 400px;" />
<p>Call state <span style="font-weight: bold" id="call-state">-</span></p>
<div>
<button id="join-meeting-button" type="button">
Join Teams Meeting
</button>
<button id="hang-up-button" type="button" disabled="true">
Hang Up
</button>
</div>
<div class="chat-popup" id="chat-box">
<div id="messages-container"></div>
<form class="form-container">
<textarea placeholder="Type message.." name="msg" id="message-box" required></textarea>
<button type="button" class="btn" id="send-message">Send</button>
</form>
</div>
<script src="./bundle.js"></script>
</body>
</html>
啟用 Teams UI 控制項
使用下列程式碼片段取代 client.js file 檔案的內容。
在程式碼片段中,
- 將通訊服務的連接字串取代為
SECRET_CONNECTION_STRING
import { CallClient } from "@azure/communication-calling";
import { AzureCommunicationTokenCredential } from "@azure/communication-common";
import { CommunicationIdentityClient } from "@azure/communication-identity";
import { ChatClient } from "@azure/communication-chat";
let call;
let callAgent;
let chatClient;
let chatThreadClient;
const meetingLinkInput = document.getElementById("teams-link-input");
const callButton = document.getElementById("join-meeting-button");
const hangUpButton = document.getElementById("hang-up-button");
const callStateElement = document.getElementById("call-state");
const messagesContainer = document.getElementById("messages-container");
const chatBox = document.getElementById("chat-box");
const sendMessageButton = document.getElementById("send-message");
const messageBox = document.getElementById("message-box");
var userId = "";
var messages = "";
var chatThreadId = "";
async function init() {
const connectionString = "<SECRET_CONNECTION_STRING>";
const endpointUrl = connectionString.split(";")[0].replace("endpoint=", "");
const identityClient = new CommunicationIdentityClient(connectionString);
let identityResponse = await identityClient.createUser();
userId = identityResponse.communicationUserId;
console.log(`\nCreated an identity with ID: ${identityResponse.communicationUserId}`);
let tokenResponse = await identityClient.getToken(identityResponse, ["voip", "chat"]);
const { token, expiresOn } = tokenResponse;
console.log(`\nIssued an access token that expires at: ${expiresOn}`);
console.log(token);
const callClient = new CallClient();
const tokenCredential = new AzureCommunicationTokenCredential(token);
callAgent = await callClient.createCallAgent(tokenCredential);
callButton.disabled = false;
chatClient = new ChatClient(endpointUrl, new AzureCommunicationTokenCredential(token));
console.log("Azure Communication Chat client created!");
}
init();
const joinCall = (urlString, callAgent) => {
const url = new URL(urlString);
console.log(url);
if (url.pathname.startsWith("/meet")) {
// Short teams URL, so for now call meetingID and pass code API
return callAgent.join({
meetingId: url.pathname.split("/").pop(),
passcode: url.searchParams.get("p"),
});
} else {
return callAgent.join({ meetingLink: urlString }, {});
}
};
callButton.addEventListener("click", async () => {
// join with meeting link
try {
call = joinCall(meetingLinkInput.value, callAgent);
} catch {
throw new Error("Could not join meeting - have you set your connection string?");
}
// Chat thread ID is provided from the call info, after connection.
call.on("stateChanged", async () => {
callStateElement.innerText = call.state;
if (call.state === "Connected" && !chatThreadClient) {
chatThreadId = call.info?.threadId;
chatThreadClient = chatClient.getChatThreadClient(chatThreadId);
chatBox.style.display = "block";
messagesContainer.innerHTML = messages;
// open notifications channel
await chatClient.startRealtimeNotifications();
// subscribe to new message notifications
chatClient.on("chatMessageReceived", (e) => {
console.log("Notification chatMessageReceived!");
// check whether the notification is intended for the current thread
if (chatThreadId != e.threadId) {
return;
}
if (e.sender.communicationUserId != userId) {
renderReceivedMessage(e.message);
} else {
renderSentMessage(e.message);
}
});
}
});
// toggle button and chat box states
hangUpButton.disabled = false;
callButton.disabled = true;
console.log(call);
});
async function renderReceivedMessage(message) {
messages += '<div class="container lighter">' + message + "</div>";
messagesContainer.innerHTML = messages;
}
async function renderSentMessage(message) {
messages += '<div class="container darker">' + message + "</div>";
messagesContainer.innerHTML = messages;
}
hangUpButton.addEventListener("click", async () => {
// end the current call
await call.hangUp();
// Stop notifications
chatClient.stopRealtimeNotifications();
// toggle button states
hangUpButton.disabled = true;
callButton.disabled = false;
callStateElement.innerText = "-";
// toggle chat states
chatBox.style.display = "none";
messages = "";
// Remove local ref
chatThreadClient = undefined;
});
sendMessageButton.addEventListener("click", async () => {
let message = messageBox.value;
let sendMessageRequest = { content: message };
let sendMessageOptions = { senderDisplayName: "Jack" };
let sendChatMessageResult = await chatThreadClient.sendMessage(
sendMessageRequest,
sendMessageOptions
);
let messageId = sendChatMessageResult.id;
messageBox.value = "";
console.log(`Message sent!, message id:${messageId}`);
});
Teams 用戶端不會設定聊天對話參與者的顯示名稱。 在 participantsAdded
事件和 participantsRemoved
事件中,若要列出參與者,這些名稱在 API 中會以 Null 傳回。 聊天參與者的顯示名稱可以從 call
物件的 remoteParticipants
欄位中擷取。 在收到關於名冊變更的通知時,您可以使用此程式碼以接收已新增或移除的使用者名稱:
var displayName = call.remoteParticipants.find(p => p.identifier.communicationUserId == '<REMOTE_USER_ID>').displayName;
執行程式碼
Webpack 使用者可以使用 webpack-dev-server
來建置及執行您的應用程式。 執行下列命令,在本機 Web 伺服器上組合應用程式主機:
npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map
請開啟瀏覽器,然後瀏覽至 http://localhost:8080/
。 您應看到應用程式已啟動,如下列螢幕擷取畫面所示:
將 Teams 會議連結插入文字框中。 按下 [加入 Teams 會議] 以加入 Teams 會議。 在通訊服務使用者已允許加入會議後,您可從通訊服務應用程式進行聊天。 瀏覽至頁面底部的方塊以開始聊天。 為求簡單明瞭,應用程式僅顯示聊天中的最後兩則訊息。
注意
Teams 的互通性案例不支援特定功能。 如需深入了解支援功能,請參閱 Teams 外部使用者的 Teams 會議功能
在本快速入門中,您將了解如何使用適用於 iOS 的 Azure 通訊服務聊天 SDK 以開始在 Teams 會議中聊天。
範例程式碼
如果您想要直接跳到結尾,您可以在 GitHub \(英文\) 上下載此快速入門作為範例。
必要條件
- 具有有效訂用帳戶的 Azure 帳戶。 免費建立帳戶
- 執行 Xcode 的 Mac,以及安裝在您 Keychain 中的有效開發人員憑證。
- Teams 部署。
- 針對您的 Azure 通訊服務的使用者存取權杖。 您也可以使用 Azure CLI,並搭配您的連接字串執行命令,以建立使用者和存取權杖。
az communication user-identity token issue --scope voip chat --connection-string "yourConnectionString"
如需詳細資訊,請參閱使用 Azure CLI 建立和管理存取權杖。
設定
建立 XCode 專案
在 Xcode 中,建立新的 iOS 專案,並選取 [單一檢視應用程式] 範本。 此教學課程使用 SwiftUI 架構 \(英文\),因此,您應將 [語言] 設定為 [Swift],並將 [使用者介面] 設定為 [SwiftUI]。 進行本快速入門期間,您不會建立測試。 您可以視需要取消核取 [包含測試]。
安裝 CocoaPods
使用此指南,在 Mac 上安裝 CocoaPods \(英文\)。
使用 CocoaPods 安裝套件和相依性
若要為應用程式建立
Podfile
,請開啟終端,然後瀏覽至專案資料夾並執行 pod init。將下列程式代碼新增至
Podfile
目標底下的 ,然後儲存。
target 'Chat Teams Interop' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for Chat Teams Interop
pod 'AzureCommunicationCalling'
pod 'AzureCommunicationChat'
end
執行
pod install
。.xcworkspace
使用 Xcode 開啟檔案。
要求存取麥克風
您必須以 NSMicrophoneUsageDescription
更新應用程式的資訊屬性清單,才能存取裝置的麥克風。 您可以將相關聯的值設定為 string
,此值會包含在系統用來向使用者要求存取權的對話中。
在目標下,選取索引 Info
標籤並新增 [隱私權 - 麥克風使用描述] 的字串
停用使用者腳本沙盒
連結庫內的一些腳本會在建置程式期間寫入檔案。 若要允許此功能,請在 Xcode 中停用使用者腳本沙盒。
在組建設定下,搜尋 sandbox
並設定 User Script Sandboxing
為 No
。
加入會議聊天
通訊服務使用者可使用通話 SDK 以匿名使用者身分加入 Team 會議。 一旦使用者加入 Teams 會議,他們可以與其他會議出席者一起傳送和接收訊息。 使用者在加入之前將無法存取傳送的聊天訊息,也無法在不在會議時傳送或接收訊息。 若要加入會議並開始聊天,您可以遵循後續步驟。
設定應用程式架構
藉由新增下列代碼段,在 中 ContentView.swift
匯入 Azure 通訊套件:
import AVFoundation
import SwiftUI
import AzureCommunicationCalling
import AzureCommunicationChat
在 ContentView.swift
[新增下列代碼段] 中 struct ContentView: View
,就在宣告的正上方:
let endpoint = "<ADD_YOUR_ENDPOINT_URL_HERE>"
let token = "<ADD_YOUR_USER_TOKEN_HERE>"
let displayName: String = "Quickstart User"
將 <ADD_YOUR_ENDPOINT_URL_HERE>
取代為通訊服務資源的端點。
透過 Azure 用戶端命令行,將 取代 <ADD_YOUR_USER_TOKEN_HERE>
為上述產生的令牌。
深入了解使用者存取權杖:使用者存取權杖
將 Quickstart User
取代為您要在聊天中使用的顯示名稱。
若要保存狀態,請將下列變數新增至 ContentView
結構:
@State var message: String = ""
@State var meetingLink: String = ""
@State var chatThreadId: String = ""
// Calling state
@State var callClient: CallClient?
@State var callObserver: CallDelegate?
@State var callAgent: CallAgent?
@State var call: Call?
// Chat state
@State var chatClient: ChatClient?
@State var chatThreadClient: ChatThreadClient?
@State var chatMessage: String = ""
@State var meetingMessages: [MeetingMessage] = []
現在讓我們新增主體 var 來保存 UI 元素。 在本快速入門中,我們會將商務邏輯附加至這些控制項。 將下列程式代碼新增至 ContentView
結構:
var body: some View {
NavigationView {
Form {
Section {
TextField("Teams Meeting URL", text: $meetingLink)
.onChange(of: self.meetingLink, perform: { value in
if let threadIdFromMeetingLink = getThreadId(from: value) {
self.chatThreadId = threadIdFromMeetingLink
}
})
TextField("Chat thread ID", text: $chatThreadId)
}
Section {
HStack {
Button(action: joinMeeting) {
Text("Join Meeting")
}.disabled(
chatThreadId.isEmpty || callAgent == nil || call != nil
)
Spacer()
Button(action: leaveMeeting) {
Text("Leave Meeting")
}.disabled(call == nil)
}
Text(message)
}
Section {
ForEach(meetingMessages, id: \.id) { message in
let currentUser: Bool = (message.displayName == displayName)
let foregroundColor = currentUser ? Color.white : Color.black
let background = currentUser ? Color.blue : Color(.systemGray6)
let alignment = currentUser ? HorizontalAlignment.trailing : .leading
HStack {
if currentUser {
Spacer()
}
VStack(alignment: alignment) {
Text(message.displayName).font(Font.system(size: 10))
Text(message.content)
.frame(maxWidth: 200)
}
.padding(8)
.foregroundColor(foregroundColor)
.background(background)
.cornerRadius(8)
if !currentUser {
Spacer()
}
}
}
.frame(maxWidth: .infinity)
}
TextField("Enter your message...", text: $chatMessage)
Button(action: sendMessage) {
Text("Send Message")
}.disabled(chatThreadClient == nil)
}
.navigationBarTitle("Teams Chat Interop")
}
.onAppear {
// Handle initialization of the call and chat clients
}
}
初始化 ChatClient
具現化 ChatClient
並啟用訊息通知。 我們使用即時通知來接收聊天訊息。
設定主體后,讓我們新增函式來處理通話和聊天客戶端的設定。
在函式中 onAppear
,新增下列程式代碼來初始化 CallClient
和 ChatClient
:
if let threadIdFromMeetingLink = getThreadId(from: self.meetingLink) {
self.chatThreadId = threadIdFromMeetingLink
}
// Authenticate
do {
let credentials = try CommunicationTokenCredential(token: token)
self.callClient = CallClient()
self.callClient?.createCallAgent(
userCredential: credentials
) { agent, error in
if let e = error {
self.message = "ERROR: It was not possible to create a call agent."
print(e)
return
} else {
self.callAgent = agent
}
}
// Start the chat client
self.chatClient = try ChatClient(
endpoint: endpoint,
credential: credentials,
withOptions: AzureCommunicationChatClientOptions()
)
// Register for real-time notifications
self.chatClient?.startRealTimeNotifications { result in
switch result {
case .success:
self.chatClient?.register(
event: .chatMessageReceived,
handler: receiveMessage
)
case let .failure(error):
self.message = "Could not register for message notifications: " + error.localizedDescription
print(error)
}
}
} catch {
print(error)
self.message = error.localizedDescription
}
新增會議加入函式
將下列函式新增至 ContentView
結構,以處理加入會議。
func joinMeeting() {
// Ask permissions
AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
if granted {
let teamsMeetingLink = TeamsMeetingLinkLocator(
meetingLink: self.meetingLink
)
self.callAgent?.join(
with: teamsMeetingLink,
joinCallOptions: JoinCallOptions()
) {(call, error) in
if let e = error {
self.message = "Failed to join call: " + e.localizedDescription
print(e.localizedDescription)
return
}
self.call = call
self.callObserver = CallObserver(self)
self.call?.delegate = self.callObserver
self.message = "Teams meeting joined successfully"
}
} else {
self.message = "Not authorized to use mic"
}
}
}
初始化 ChatThreadClient
在使用者加入會議之後,我們會初始化 ChatThreadClient
。 這需要我們檢查來自委派的會議狀態,然後在加入會議時使用 初始化 ChatThreadClient
threadId
。
使用下列程式代碼建立函 connectChat()
式:
func connectChat() {
do {
self.chatThreadClient = try chatClient?.createClient(
forThread: self.chatThreadId
)
self.message = "Joined meeting chat successfully"
} catch {
self.message = "Failed to join the chat thread: " + error.localizedDescription
}
}
將下列協助程式函式新增至 ContentView
,以盡可能剖析小組會議連結中的聊天對話標識碼。 如果擷取失敗,用戶必須使用 Graph API 手動輸入聊天對話標識碼,才能擷取線程標識碼。
func getThreadId(from teamsMeetingLink: String) -> String? {
if let range = teamsMeetingLink.range(of: "meetup-join/") {
let thread = teamsMeetingLink[range.upperBound...]
if let endRange = thread.range(of: "/")?.lowerBound {
return String(thread.prefix(upTo: endRange))
}
}
return nil
}
啟用傳送訊息
將 sendMessage()
函式新增至 ContentView
。 此函式使用 ChatThreadClient
以透過使用者傳送訊息。
func sendMessage() {
let message = SendChatMessageRequest(
content: self.chatMessage,
senderDisplayName: displayName,
type: .text
)
self.chatThreadClient?.send(message: message) { result, _ in
switch result {
case .success:
print("Chat message sent")
self.chatMessage = ""
case let .failure(error):
self.message = "Failed to send message: " + error.localizedDescription + "\n Has your token expired?"
}
}
}
啟用接收訊息
若要接收訊息,我們會實作 ChatMessageReceived
事件的處理常式。 當新的訊息傳送至對話時,此處理常式會將訊息新增至 meetingMessages
變數以便可在 UI 中顯示。
首先,將下列結構新增至 ContentView.swift
。 UI 使用結構中的資料以顯示聊天訊息。
struct MeetingMessage: Identifiable {
let id: String
let date: Date
let content: String
let displayName: String
static func fromTrouter(event: ChatMessageReceivedEvent) -> MeetingMessage {
let displayName: String = event.senderDisplayName ?? "Unknown User"
let content: String = event.message.replacingOccurrences(
of: "<[^>]+>", with: "",
options: String.CompareOptions.regularExpression
)
return MeetingMessage(
id: event.id,
date: event.createdOn?.value ?? Date(),
content: content,
displayName: displayName
)
}
}
接下來,將 receiveMessage()
函式新增至 ContentView
。 這會在傳訊事件發生時呼叫。 請注意,您必須註冊您想要透過 chatClient?.register()
方法在 語句中switch
處理的所有事件。
func receiveMessage(event: TrouterEvent) -> Void {
switch event {
case let .chatMessageReceivedEvent(messageEvent):
let message = MeetingMessage.fromTrouter(event: messageEvent)
self.meetingMessages.append(message)
/// OTHER EVENTS
// case .realTimeNotificationConnected:
// case .realTimeNotificationDisconnected:
// case .typingIndicatorReceived(_):
// case .readReceiptReceived(_):
// case .chatMessageEdited(_):
// case .chatMessageDeleted(_):
// case .chatThreadCreated(_):
// case .chatThreadPropertiesUpdated(_):
// case .chatThreadDeleted(_):
// case .participantsAdded(_):
// case .participantsRemoved(_):
default:
break
}
}
最後,我們需要實作呼叫用戶端的委派處理程式。 此處理程式是用來檢查通話狀態,並在使用者加入會議時初始化聊天用戶端。
class CallObserver : NSObject, CallDelegate {
private var owner: ContentView
init(_ view: ContentView) {
owner = view
}
func call(
_ call: Call,
didChangeState args: PropertyChangedEventArgs
) {
owner.message = CallObserver.callStateToString(state: call.state)
if call.state == .disconnected {
owner.call = nil
owner.message = "Left Meeting"
} else if call.state == .inLobby {
owner.message = "Waiting in lobby (go let them in!)"
} else if call.state == .connected {
owner.message = "Connected"
owner.connectChat()
}
}
private static func callStateToString(state: CallState) -> String {
switch state {
case .connected: return "Connected"
case .connecting: return "Connecting"
case .disconnected: return "Disconnected"
case .disconnecting: return "Disconnecting"
case .earlyMedia: return "EarlyMedia"
case .none: return "None"
case .ringing: return "Ringing"
case .inLobby: return "InLobby"
default: return "Unknown"
}
}
}
離開聊天
當使用者離開小組會議時,我們會清除UI中的聊天訊息並掛斷通話。 完整程式碼如下所示。
func leaveMeeting() {
if let call = self.call {
self.chatClient?.unregister(event: .chatMessageReceived)
self.chatClient?.stopRealTimeNotifications()
call.hangUp(options: nil) { (error) in
if let e = error {
self.message = "Leaving Teams meeting failed: " + e.localizedDescription
} else {
self.message = "Leaving Teams meeting was successful"
}
}
self.meetingMessages.removeAll()
} else {
self.message = "No active call to hangup"
}
}
為通訊服務使用者取得 Teams 會議交談的對話
您可以使用圖形 API 擷取 Teams 會議詳細資料,如 Graph 文件所詳述。 通訊服務通話 SDK 接受完整 Teams 會議連結或會議識別碼。 這些連結或識別碼會作為 onlineMeeting
資源的一部分傳回,可在 joinWebUrl
屬性下存取
您也可使用圖形 API 取得 threadID
。 回應包含具有 threadID
的 chatInfo
物件。
執行程式碼
執行應用程式。
若要加入 Teams 會議,請在 UI 中輸入您的 Teams 會議連結。
加入 Teams 會議後,您必須在 Teams 用戶端中允許使用者加入會議。 一旦使用者被接納並加入聊天,您就可以傳送和接收訊息。
注意
Teams 的互通性案例不支援特定功能。 如需深入了解支援功能,請參閱 Teams 外部使用者的 Teams 會議功能
在本快速入門中,您將了解如何使用適用於 Android 的 Azure 通訊服務聊天 SDK 以開始在 Teams 會議中聊天。
範例程式碼
如果您想要直接跳到結尾,您可以在 GitHub \(英文\) 上下載此快速入門作為範例。
必要條件
啟用 Teams 互通性
以來賓使用者身分加入 Teams 會議的通訊服務使用者,只有在加入 Teams 會議通話時,才能存取會議的交談內容。 請參閱 Teams 互通性文件,以了解如何將通訊服務使用者新增至 Teams 會議通話。
您必須是這兩個實體的擁有組織成員,才能使用這項功能。
加入會議聊天
一旦啟用 Teams 的互通性之後,通訊服務使用者就可以使用通話 SDK,以外部使用者身分加入 Teams 通話。 加入通話也會將這些使用者新增為會議交談的參與者,以便可以與通話中的其他使用者傳送及接收訊息。 用戶無法存取在加入通話之前所傳送的聊天訊息。 若要加入會議並開始聊天,您可以遵循後續步驟。
將聊天新增至 Teams 通話應用程式
在您的模組層級中 build.gradle
,新增聊天 SDK 的相依性。
重要
已知問題:在相同應用程式中同時使用 Android 聊天和通話 SDK 時,聊天 SDK 的即時通知功能無法運作。 您將取得相依性解決問題。 當我們正研究解決方案時,您可將下列排除項目新增至應用程式 build.gradle
檔案中的聊天 SDK 相依性,以關閉即時通知功能:
implementation ("com.azure.android:azure-communication-chat:2.0.3") {
exclude group: 'com.microsoft', module: 'trouter-client-android'
}
新增 Team UI 配置
使用下列程式碼片段取代 activity_main.xml 中的程式碼。 這會新增對話識別碼和傳送訊息的輸入、傳送輸入訊息的按鈕,以及基本聊天配置。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/teams_meeting_thread_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="128dp"
android:ems="10"
android:hint="Meeting Thread Id"
android:inputType="textUri"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/teams_meeting_link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="64dp"
android:ems="10"
android:hint="Teams meeting link"
android:inputType="textUri"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/button_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/teams_meeting_thread_id">
<Button
android:id="@+id/join_meeting_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Join Meeting" />
<Button
android:id="@+id/hangup_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hangup" />
</LinearLayout>
<TextView
android:id="@+id/call_status_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/recording_status_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ScrollView
android:id="@+id/chat_box"
android:layout_width="374dp"
android:layout_height="294dp"
android:layout_marginTop="40dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toTopOf="@+id/send_message_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_layout"
android:orientation="vertical"
android:gravity="bottom"
android:layout_gravity="bottom"
android:fillViewport="true">
<LinearLayout
android:id="@+id/chat_box_layout"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="bottom"
android:layout_gravity="top"
android:layout_alignParentBottom="true"/>
</ScrollView>
<EditText
android:id="@+id/message_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="588dp"
android:ems="10"
android:inputType="textUri"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Type your message here..."
tools:visibility="invisible" />
<Button
android:id="@+id/send_message_button"
android:layout_width="138dp"
android:layout_height="45dp"
android:layout_marginStart="133dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="133dp"
android:text="Send Message"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/recording_status_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.428"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chat_box" />
</androidx.constraintlayout.widget.ConstraintLayout>
啟用 Teams UI 控制項
匯入套件並定義狀態變數
在 MainActivity.java
內容中,新增下列匯入:
import android.graphics.Typeface;
import android.graphics.Color;
import android.text.Html;
import android.os.Handler;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.List;
import com.azure.android.communication.chat.ChatThreadAsyncClient;
import com.azure.android.communication.chat.ChatThreadClientBuilder;
import com.azure.android.communication.chat.models.ChatMessage;
import com.azure.android.communication.chat.models.ChatMessageType;
import com.azure.android.communication.chat.models.ChatParticipant;
import com.azure.android.communication.chat.models.ListChatMessagesOptions;
import com.azure.android.communication.chat.models.SendChatMessageOptions;
import com.azure.android.communication.common.CommunicationIdentifier;
import com.azure.android.communication.common.CommunicationUserIdentifier;
import com.azure.android.core.rest.util.paging.PagedAsyncStream;
import com.azure.android.core.util.AsyncStreamHandler;
在 MainActivity
類別中,新增下列變數:
// InitiatorId is used to differentiate incoming messages from outgoing messages
private static final String InitiatorId = "<USER_ID>";
private static final String ResourceUrl = "<COMMUNICATION_SERVICES_RESOURCE_ENDPOINT>";
private String threadId;
private ChatThreadAsyncClient chatThreadAsyncClient;
// The list of ids corresponsding to messages which have already been processed
ArrayList<String> chatMessages = new ArrayList<>();
將 <USER_ID>
取代為起始聊天的使用者識別碼。
將 <COMMUNICATION_SERVICES_RESOURCE_ENDPOINT>
取代為通訊服務資源的端點。
初始化 ChatThreadClient
加入會議後,便會具現化 ChatThreadClient
並顯示聊天元件。
使用下列程式碼更新 MainActivity.joinTeamsMeeting()
方法的結尾:
private void joinTeamsMeeting() {
...
EditText threadIdView = findViewById(R.id.teams_meeting_thread_id);
threadId = threadIdView.getText().toString();
// Initialize Chat Thread Client
chatThreadAsyncClient = new ChatThreadClientBuilder()
.endpoint(ResourceUrl)
.credential(new CommunicationTokenCredential(UserToken))
.chatThreadId(threadId)
.buildAsyncClient();
Button sendMessageButton = findViewById(R.id.send_message_button);
EditText messageBody = findViewById(R.id.message_body);
// Register the method for sending messages and toggle the visibility of chat components
sendMessageButton.setOnClickListener(l -> sendMessage());
sendMessageButton.setVisibility(View.VISIBLE);
messageBody.setVisibility(View.VISIBLE);
// Start the polling for chat messages immediately
handler.post(runnable);
}
啟用傳送訊息
將 sendMessage()
方法新增至 MainActivity
。 其使用 ChatThreadClient
代表使用者傳送訊息。
private void sendMessage() {
// Retrieve the typed message content
EditText messageBody = findViewById(R.id.message_body);
// Set request options and send message
SendChatMessageOptions options = new SendChatMessageOptions();
options.setContent(messageBody.getText().toString());
options.setSenderDisplayName("Test User");
chatThreadAsyncClient.sendMessage(options);
// Clear the text box
messageBody.setText("");
}
在應用程式中,啟用訊息輪詢並進行轉譯
重要
已知問題:由於聊天 SDK 的即時通知功能無法與通話 SDK 的功能同時運作,因此我們必須在預先定義的間隔輪詢 GetMessages
API。 在我們的範例中,我們將使用 3 秒間隔。
我們可以從 GetMessages
API 傳回的訊息清單中取得下列資料:
- 加入後對話的
text
和html
訊息 - 對話名冊的變更
- 對話主題的更新
在 MainActivity
類別中,新增處理常式和可執行工作,以在 3 秒間隔執行:
private Handler handler = new Handler();
private Runnable runnable = new Runnable() {
@Override
public void run() {
try {
retrieveMessages();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Repeat every 3 seconds
handler.postDelayed(runnable, 3000);
}
};
請注意,工作已在初始化步驟中更新的 MainActivity.joinTeamsMeeting()
方法結尾開始。
最後,我們會新增方法以查詢對話上的所有可存取訊息、按訊息類型進行剖析,然後顯示 html
和 text
訊息:
private void retrieveMessages() throws InterruptedException {
// Initialize the list of messages not yet processed
ArrayList<ChatMessage> newChatMessages = new ArrayList<>();
// Retrieve all messages accessible to the user
PagedAsyncStream<ChatMessage> messagePagedAsyncStream
= this.chatThreadAsyncClient.listMessages(new ListChatMessagesOptions(), null);
// Set up a lock to wait until all returned messages have been inspected
CountDownLatch latch = new CountDownLatch(1);
// Traverse the returned messages
messagePagedAsyncStream.forEach(new AsyncStreamHandler<ChatMessage>() {
@Override
public void onNext(ChatMessage message) {
// Messages that should be displayed in the chat
if ((message.getType().equals(ChatMessageType.TEXT)
|| message.getType().equals(ChatMessageType.HTML))
&& !chatMessages.contains(message.getId())) {
newChatMessages.add(message);
chatMessages.add(message.getId());
}
if (message.getType().equals(ChatMessageType.PARTICIPANT_ADDED)) {
// Handle participants added to chat operation
List<ChatParticipant> participantsAdded = message.getContent().getParticipants();
CommunicationIdentifier participantsAddedBy = message.getContent().getInitiatorCommunicationIdentifier();
}
if (message.getType().equals(ChatMessageType.PARTICIPANT_REMOVED)) {
// Handle participants removed from chat operation
List<ChatParticipant> participantsRemoved = message.getContent().getParticipants();
CommunicationIdentifier participantsRemovedBy = message.getContent().getInitiatorCommunicationIdentifier();
}
if (message.getType().equals(ChatMessageType.TOPIC_UPDATED)) {
// Handle topic updated
String newTopic = message.getContent().getTopic();
CommunicationIdentifier topicUpdatedBy = message.getContent().getInitiatorCommunicationIdentifier();
}
}
@Override
public void onError(Throwable throwable) {
latch.countDown();
}
@Override
public void onComplete() {
latch.countDown();
}
});
// Wait until the operation completes
latch.await(1, TimeUnit.MINUTES);
// Returned messages should be ordered by the createdOn field to be guaranteed a proper chronological order
// For the purpose of this demo we will just reverse the list of returned messages
Collections.reverse(newChatMessages);
for (ChatMessage chatMessage : newChatMessages)
{
LinearLayout chatBoxLayout = findViewById(R.id.chat_box_layout);
// For the purpose of this demo UI, we don't need to use HTML formatting for displaying messages
// The Teams client always sends html messages in meeting chats
String message = Html.fromHtml(chatMessage.getContent().getMessage(), Html.FROM_HTML_MODE_LEGACY).toString().trim();
TextView messageView = new TextView(this);
messageView.setText(message);
// Compare with sender identifier and align LEFT/RIGHT accordingly
// Azure Communication Services users are of type CommunicationUserIdentifier
CommunicationIdentifier senderId = chatMessage.getSenderCommunicationIdentifier();
if (senderId instanceof CommunicationUserIdentifier
&& InitiatorId.equals(((CommunicationUserIdentifier) senderId).getId())) {
messageView.setTextColor(Color.GREEN);
messageView.setGravity(Gravity.RIGHT);
} else {
messageView.setTextColor(Color.BLUE);
messageView.setGravity(Gravity.LEFT);
}
// Note: messages with the deletedOn property set to a timestamp, should be marked as deleted
// Note: messages with the editedOn property set to a timestamp, should be marked as edited
messageView.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD);
chatBoxLayout.addView(messageView);
}
}
Teams 用戶端不會設定聊天對話參與者的顯示名稱。 在 participantsAdded
事件和 participantsRemoved
事件中,若要列出參與者,這些名稱在 API 中會以 Null 傳回。 聊天參與者的顯示名稱可以從 call
物件的 remoteParticipants
欄位中擷取。
為通訊服務使用者取得 Teams 會議交談的對話
您可以使用圖形 API 擷取 Teams 會議詳細資料,如 Graph 文件所詳述。 通訊服務通話 SDK 接受完整 Teams 會議連結或會議識別碼。 這些連結或識別碼會作為 onlineMeeting
資源的一部分傳回,可在 joinWebUrl
屬性下存取
您也可使用圖形 API 取得 threadID
。 回應包含具有 threadID
的 chatInfo
物件。
執行程式碼
應用程式現在可以使用工具列上的 [執行應用程式] 按鈕 (Shift+F10) 來啟動。
若要加入 Teams 會議和聊天,請在 UI 中輸入您的 Teams 會議連結和對話識別碼。
加入 Teams 會議後,您必須在 Teams 用戶端中允許使用者加入會議。 一旦使用者被接納並加入聊天,您就可以傳送和接收訊息。
注意
Teams 的互通性案例不支援特定功能。 如需深入了解支援功能,請參閱 Teams 外部使用者的 Teams 會議功能
在本快速入門中,您將瞭解如何使用適用於 C# 的 Azure 通訊服務 Chat SDK 在 Teams 會議中聊天。
範例指令碼
在 GitHub 上找到此快速入門的完成程式碼。
必要條件
- Teams 部署。
- 具有有效訂用帳戶的 Azure 帳戶。 免費建立帳戶。
- 安裝包含通用 Windows 平台開發工作負載的 Visual Studio 2019。
- 已部署通訊服務資源。 建立通訊服務資源。
- Teams 會議連結。
加入會議聊天
通訊服務使用者可使用通話 SDK 以匿名使用者身分加入 Team 會議。 加入會議也會將這些使用者新增為會議交談的參與者,以便可以與會議中的其他使用者傳送及接收訊息。 使用者無法存取在加入會議之前傳送的聊天訊息,而且無法在會議結束後傳送或接收訊息。 若要加入會議並開始聊天,您可以遵循後續步驟。
執行程式碼
在 Visual Studio 中,您可以建置並執行程式碼。 請注意我們支援的解決方案平臺:x64
x86
、和 ARM64
。
- 開啟 PowerShell 實例、Windows 終端機、命令提示字元或對等專案,並流覽至您想要複製範例的目錄。
git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git
- 在 Visual Studio 中開啟專案 ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj。
- 安裝下列 NuGet 套件版本 (或更高版本):
Install-Package Azure.Communication.Calling -Version 1.0.0-beta.29
Install-Package Azure.Communication.Chat -Version 1.1.0
Install-Package Azure.Communication.Common -Version 1.0.1
Install-Package Azure.Communication.Identity -Version 1.0.1
- 在必要條件中採購通訊服務資源之後,將 connectionstring 新增至 ChatTeamsInteropQuickStart/MainPage.xaml.cs 檔案。
//Azure Communication Services resource connection string, i.e., = "endpoint=https://your-resource.communication.azure.net/;accesskey=your-access-key";
private const string connectionString_ = "";
重要
- 在執行程式代碼之前,請先從 Visual Studio 中的 [解決方案平臺] 下拉式清單中選取適當的平臺,例如
x64
- 請確保您已啟用 Windows 10 的「開發人員模式」(開發人員設定)
如果未正確設定,則後續步驟將無法進行
- 按下 F5 以在偵錯模式開始專案。
- 在 [Teams 會議連結] 方塊上貼上有效 Teams 會議連結 (請參閱下一節)
- 按下 [加入 Teams 會議] 以開始聊天。
重要
通話 SDK 建立與 Teams 會議的連線後,請參閱通訊服務通話 Windows 應用程式,處理聊天作業的主要功能如下:StartPollingForChatMessages 和 SendMessageButton_Click。 這兩個程式碼片段位於 ChatTeamsInteropQuickStart\MainPage.xaml.cs
/// <summary>
/// Background task that keeps polling for chat messages while the call connection is established
/// </summary>
private async Task StartPollingForChatMessages()
{
CommunicationTokenCredential communicationTokenCredential = new(user_token_);
chatClient_ = new ChatClient(EndPointFromConnectionString(), communicationTokenCredential);
await Task.Run(async () =>
{
keepPolling_ = true;
ChatThreadClient chatThreadClient = chatClient_.GetChatThreadClient(thread_Id_);
int previousTextMessages = 0;
while (keepPolling_)
{
try
{
CommunicationUserIdentifier currentUser = new(user_Id_);
AsyncPageable<ChatMessage> allMessages = chatThreadClient.GetMessagesAsync();
SortedDictionary<long, string> messageList = new();
int textMessages = 0;
string userPrefix;
await foreach (ChatMessage message in allMessages)
{
if (message.Type == ChatMessageType.Html || message.Type == ChatMessageType.Text)
{
textMessages++;
userPrefix = message.Sender.Equals(currentUser) ? "[you]:" : "";
messageList.Add(long.Parse(message.SequenceId), $"{userPrefix}{StripHtml(message.Content.Message)}");
}
}
//Update UI just when there are new messages
if (textMessages > previousTextMessages)
{
previousTextMessages = textMessages;
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
TxtChat.Text = string.Join(Environment.NewLine, messageList.Values.ToList());
});
}
if (!keepPolling_)
{
return;
}
await SetInCallState(true);
await Task.Delay(3000);
}
catch (Exception e)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
_ = new MessageDialog($"An error occurred while fetching messages in PollingChatMessagesAsync(). The application will shutdown. Details : {e.Message}").ShowAsync();
throw e;
});
await SetInCallState(false);
}
}
});
}
private async void SendMessageButton_Click(object sender, RoutedEventArgs e)
{
SendMessageButton.IsEnabled = false;
ChatThreadClient chatThreadClient = chatClient_.GetChatThreadClient(thread_Id_);
_ = await chatThreadClient.SendMessageAsync(TxtMessage.Text);
TxtMessage.Text = "";
SendMessageButton.IsEnabled = true;
}
取得 Teams 會議連結
您可以使用圖形 API 擷取 Teams 會議連結,如 Graph 文件所詳述。 此連結會作為 onlineMeeting
資源的一部分傳回,可在 joinWebUrl
屬性下存取。
您也可以從 Teams 會議邀請本身的加入會議 URL 取得所需的會議連結。
Teams 會議連結看起來像這樣:https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here
。
如果您的小組連結具有與這個不同的格式,您必須使用圖形 API 擷取線程標識碼。
注意
Teams 的互通性案例不支援特定功能。 如需深入了解支援功能,請參閱 Teams 外部使用者的 Teams 會議功能
清除資源
如果您想要清除並移除通訊服務訂用帳戶,您可以刪除資源或資源群組。 刪除資源群組也會刪除與其相關聯的任何其他資源。 深入了解如何清除資源。
下一步
如需詳細資訊,請參閱下列文章:
- 查看我們的聊天 Hero 範例
- 深入了解聊天的運作方式