快速入門:將聊天應用程式加入至 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/。 您應看到應用程式已啟動,如下列螢幕擷取畫面所示:

已完成 JavaScript 應用程式的螢幕快照。

將 Teams 會議連結插入文字框中。 按下 [加入 Teams 會議] 以加入 Teams 會議。 在通訊服務使用者已允許加入會議後,您可從通訊服務應用程式進行聊天。 瀏覽至頁面底部的方塊以開始聊天。 為求簡單明瞭,應用程式僅顯示聊天中的最後兩則訊息。

注意

Teams 的互通性案例不支援特定功能。 如需深入了解支援功能,請參閱 Teams 外部使用者的 Teams 會議功能

在本快速入門中,您將了解如何使用適用於 iOS 的 Azure 通訊服務聊天 SDK 以開始在 Teams 會議中聊天。

範例程式碼

如果您想要直接跳到結尾,您可以在 GitHub \(英文\) 上下載此快速入門作為範例。

必要條件

  • 具有有效訂用帳戶的 Azure 帳戶。 免費建立帳戶
  • 執行 Xcode 的 Mac,以及安裝在您 Keychain 中的有效開發人員憑證。
  • Teams 部署
  • 針對您的 Azure 通訊服務的使用者存取權杖。 您也可以使用 Azure CLI,並搭配您的連接字串執行命令,以建立使用者和存取權杖。
az communication identity token issue --scope voip --connection-string "yourConnectionString"

如需詳細資訊,請參閱使用 Azure CLI 建立和管理存取權杖

設定

建立 XCode 專案

在 Xcode 中,建立新的 iOS 專案,並選取 [單一檢視應用程式] 範本。 此教學課程使用 SwiftUI 架構 \(英文\),因此,您應將 [語言] 設定為 [Swift],並將 [使用者介面] 設定為 [SwiftUI]。 進行本快速入門期間,您不會建立測試。 您可以視需要取消核取 [包含測試]。

顯示 Xcode 內 [新增專案] 視窗的螢幕快照。

安裝 CocoaPods

使用此指南,在 Mac 上安裝 CocoaPods \(英文\)。

使用 CocoaPods 安裝套件和相依性

  1. 若要為應用程式建立 Podfile,請開啟終端,然後瀏覽至專案資料夾並執行 pod init。

  2. 將下列程式代碼新增至 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
  1. 執行 pod install

  2. .xcworkspace使用 Xcode 開啟檔案。

要求存取麥克風

您必須以 NSMicrophoneUsageDescription 更新應用程式的資訊屬性清單,才能存取裝置的麥克風。 您可以將相關聯的值設定為 string,此值會包含在系統用來向使用者要求存取權的對話中。

在目標下,選取索引 Info 標籤並新增 [隱私權 - 麥克風使用描述] 的字串

顯示在 Xcode 中新增麥克風使用量的螢幕快照。

停用使用者腳本沙盒

連結庫內的一些腳本會在建置程式期間寫入檔案。 若要允許此功能,請在 Xcode 中停用使用者腳本沙盒。 在組建設定下,搜尋 sandbox 並設定 User Script SandboxingNo

顯示停用 Xcode 內使用者腳本沙箱的螢幕快照。

加入會議聊天

通訊服務使用者可使用通話 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 ,新增下列程式代碼來初始化 CallClientChatClient

  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 。 這需要我們檢查來自委派的會議狀態,然後在加入會議時使用 初始化 ChatThreadClientthreadId

使用下列程式代碼建立函 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。 回應包含具有 threadIDchatInfo 物件。

執行程式碼

執行應用程式。

若要加入 Teams 會議,請在 UI 中輸入您的 Teams 會議連結。

加入 Teams 會議後,您必須在 Teams 用戶端中允許使用者加入會議。 一旦使用者被接納並加入聊天,您就可以傳送和接收訊息。

已完成 iOS 應用程式的螢幕快照。

注意

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 傳回的訊息清單中取得下列資料:

  • 加入後對話的 texthtml 訊息
  • 對話名冊的變更
  • 對話主題的更新

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() 方法結尾開始。

最後,我們會新增方法以查詢對話上的所有可存取訊息、按訊息類型進行剖析,然後顯示 htmltext 訊息:

    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。 回應包含具有 threadIDchatInfo 物件。

執行程式碼

應用程式現在可以使用工具列上的 [執行應用程式] 按鈕 (Shift+F10) 來啟動。

若要加入 Teams 會議和聊天,請在 UI 中輸入您的 Teams 會議連結和對話識別碼。

加入 Teams 會議後,您必須在 Teams 用戶端中允許使用者加入會議。 一旦使用者被接納並加入聊天,您就可以傳送和接收訊息。

已完成Android應用程式的螢幕快照。

注意

Teams 的互通性案例不支援特定功能。 如需深入了解支援功能,請參閱 Teams 外部使用者的 Teams 會議功能

在本快速入門中,您將瞭解如何使用適用於 C# 的 Azure 通訊服務 Chat SDK 在 Teams 會議中聊天。

範例指令碼

GitHub 上找到此快速入門的完成程式碼。

必要條件

加入會議聊天

通訊服務使用者可使用通話 SDK 以匿名使用者身分加入 Team 會議。 加入會議也會將這些使用者新增為會議交談的參與者,以便可以與會議中的其他使用者傳送及接收訊息。 使用者無法存取在加入會議之前傳送的聊天訊息,而且無法在會議結束後傳送或接收訊息。 若要加入會議並開始聊天,您可以遵循後續步驟。

執行程式碼

在 Visual Studio 中,您可以建置並執行程式碼。 請注意我們支援的解決方案平臺:x64x86、和 ARM64

  1. 開啟 PowerShell 實例、Windows 終端機、命令提示字元或對等專案,並流覽至您想要複製範例的目錄。
  2. git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git
  3. 在 Visual Studio 中開啟專案 ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj。
  4. 安裝下列 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

  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 的「開發人員模式」(開發人員設定)

如果未正確設定,則後續步驟將無法進行

  1. 按下 F5 以在偵錯模式開始專案。
  2. 在 [Teams 會議連結] 方塊上貼上有效 Teams 會議連結 (請參閱下一節)
  3. 按下 [加入 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 stablished
        /// </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;
        }

您可以使用圖形 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 擷取線程標識碼。

已完成 csharp 應用程式的螢幕快照。

注意

Teams 的互通性案例不支援特定功能。 如需深入了解支援功能,請參閱 Teams 外部使用者的 Teams 會議功能

清除資源

如果您想要清除並移除通訊服務訂用帳戶,您可以刪除資源或資源群組。 刪除資源群組也會刪除與其相關聯的任何其他資源。 深入了解如何清除資源

下一步

如需詳細資訊,請參閱下列文章: