Краткое руководство. Подключение приложения чата к собранию в Teams

Приступите к работе со Службами коммуникации Azure, подключив решение для чата к Microsoft Teams.

В этом кратком руководстве вы узнаете, как общаться в чате на собрании Teams с помощью пакета SDK для чата Служб коммуникации Azure для JavaScript.

Пример кода

Итоговый код для этого краткого руководства можно найти на сайте GitHub.

Предварительные требования

Присоединение к чату собрания

Пользователь Служб коммуникации может анонимно присоединиться к собранию Teams с помощью пакета SDK для вызовов. Присоединение к собранию также добавляет их в качестве участника в чат собрания, где они могут отправлять и получать сообщения с другими пользователями на собрании. У пользователя не будет доступа к сообщениям чата, отправленным до присоединения к собранию, и он не сможет отправлять или получать сообщения после завершения собрания. Чтобы присоединиться к собранию и начать беседу, можно выполнить следующие действия.

создание приложения Node.js;

Откройте терминал или командное окно, создайте каталог для своего приложения и перейдите к нему.

mkdir chat-interop-quickstart && cd chat-interop-quickstart

Воспользуйтесь командой npm init -y, чтобы создать файл package.json с параметрами по умолчанию.

npm init -y

Установка пакетов чата

Используйте команду npm install, чтобы установить необходимые пакеты SDK Служб коммуникации для JavaScript.

npm install @azure/communication-common --save

npm install @azure/communication-identity --save

npm install @azure/communication-signaling --save

npm install @azure/communication-chat --save

npm install @azure/communication-calling --save

Параметр --save указывает библиотеку как зависимость в файле пакета package.json.

Настройка платформы приложения

В этом кратком руководстве для упаковки ресурсов приложения используется Webpack. Выполните следующую команду, чтобы установить пакеты npm Webpack, webpack-cli и webpack-dev-server и перечислить их как зависимости разработки в файле package.json:

npm install webpack@4.42.0 webpack-cli@3.3.11 webpack-dev-server@3.10.3 --save-dev

Создайте файл index.html в корневом каталоге проекта. Мы используем этот файл для настройки базового макета, который позволит пользователю присоединиться к собранию и начать чат.

Добавление элементов управления пользовательского интерфейса Teams

Замените код в файле index.html приведенным ниже фрагментом кода. В текстовые поля в верхней части страницы будут вводиться контекст собрания группы и идентификатор потока собрания. Кнопка "Присоединиться к собранию 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: 300px;" />
          <input id="thread-id-input" type="text" placeholder="Chat thread id"
        style="margin-bottom:1em; width: 300px;" />
        <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

Замените содержимое файла client.js приведенным ниже фрагментом кода.

Во фрагменте кода замените

  • SECRET_CONNECTION_STRING строкой подключения Служб коммуникации,
  • ENDPOINT_URL URL-адресом конечной точки Служб коммуникации.
import { CallClient, CallAgent } 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 threadIdInput = document.getElementById('thread-id-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 lastMessage = '';
var previousMessage = '';

async function init() {
  const connectionString = "<SECRET_CONNECTION_STRING>";
  const endpointUrl = "<ENDPOINT_URL>";

  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();

callButton.addEventListener("click", async () => {
  // join with meeting link
  call = callAgent.join({meetingLink: meetingLinkInput.value}, {});

  call.on('stateChanged', () => {
      callStateElement.innerText = call.state;
  })
  // toggle button and chat box states
  chatBox.style.display = "block";
  hangUpButton.disabled = false;
  callButton.disabled = true;

  messagesContainer.innerHTML = "";

  console.log(call);

  // 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 (threadIdInput.value != e.threadId) {
      return;
    }

    if (e.sender.communicationUserId != userId) {
       renderReceivedMessage(e.message);
    }
    else {
       renderSentMessage(e.message);
    }
  });

  chatThreadClient = await chatClient.getChatThreadClient(threadIdInput.value);
});

async function renderReceivedMessage(message) {
  previousMessage = lastMessage;
  lastMessage = '<div class="container lighter">' + message + '</div>';
  messagesContainer.innerHTML = previousMessage + lastMessage;
}

async function renderSentMessage(message) {
  previousMessage = lastMessage;
  lastMessage = '<div class="container darker">' + message + '</div>';
  messagesContainer.innerHTML = previousMessage + lastMessage;
}

hangUpButton.addEventListener("click", async () => 
  {
    // end the current call
    await call.hangUp();

    // toggle button states
    hangUpButton.disabled = true;
    callButton.disabled = false;
    callStateElement.innerText = '-';

    // toggle chat states
    chatBox.style.display = "none";
    messages = "";
  });

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. Имена возвращаются как null в API для перечисления участников, в событии participantsAdded и в событии participantsRemoved . Отображаемые имена участников чата можно получить из поля remoteParticipants объекта call. При получении уведомления об изменении состава можно использовать этот код для получения имени пользователя, который был добавлен или удален:

var displayName = call.remoteParticipants.find(p => p.identifier.communicationUserId == '<REMOTE_USER_ID>').displayName;

Получение данных о беседе в чате для пользователя Служб коммуникации

Сведения о собрании Teams можно получить с помощью API-интерфейсов Graph. Подробности см. в документации по Graph. Пакет SDK вызовов Служб коммуникации принимает полную ссылку на собрание Teams или идентификатор собрания. Они возвращаются как часть onlineMeeting ресурса, доступного в свойствеjoinWebUrl

С помощью API Graph можно также получить threadID. Ответ содержит chatInfo объект с threadID.

Вы также можете получить необходимую информацию о собрании и идентификатор потока по URL-адресу Присоединиться к собранию в самом приглашении на собрание в Teams. Ссылка на собрание в Teams выглядит следующим образом: https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here. — threadId это , где meeting_chat_thread_id находится в ссылке. Перед использованием убедитесь, что объект meeting_chat_thread_id не экранирован. Он должен иметь следующий формат: 19:meeting_ZWRhZDY4ZGUtYmRlNS00OWZaLTlkZTgtZWRiYjIxOWI2NTQ4@thread.v2.

Выполнение кода

Пользователи Webpack могут использовать webpack-dev-server для сборки и запуска приложения. Выполните следующую команду, чтобы создать пакет узла приложения на локальном веб-сервере:

npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map

Откройте веб-браузер и перейдите по адресу http://localhost:8080/. Приложение должно быть запущено, как показано на следующем снимке экрана:

Снимок экрана: готовое приложение JavaScript.

В текстовые поля вставьте ссылку на собрание в Teams и идентификатор потока. Чтобы присоединиться к собранию в Teams, нажмите кнопку Join Teams Meeting (Присоединиться к собранию в Teams). После того, как пользователь Службы коммуникации был допущен к встрече, вы можете общаться в чате из приложения служб связи. Чтобы начать беседу, перейдите к полю в нижней части страницы. Для простоты приложение отображает в чате только два последних сообщения.

Примечание

Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в статье Возможности собраний Teams для внешних пользователей Teams.

В этом кратком руководстве описывается, как общаться в чате конференции Teams с помощью пакета SDK для чата Служб коммуникации Azure для iOS.

Предварительные требования

Присоединение к чату собрания

Пользователь Служб коммуникации может анонимно присоединиться к собранию Teams с помощью пакета SDK для вызовов. Присоединение к собранию также добавляет их в качестве участника в чат собрания, где они могут отправлять и получать сообщения с другими пользователями на собрании. У пользователя не будет доступа к сообщениям чата, которые были отправлены до того, как он присоединился к собранию, и он не сможет отправлять или получать сообщения после завершения собрания. Чтобы присоединиться к собранию и начать беседу, можно выполнить следующие действия.

Добавление чата в вызывающее приложение Teams

Удалите Podfile.lock и замените содержимое файла Podfile следующим кодом.

platform :ios, '13.0'
use_frameworks!

target 'AzureCommunicationCallingSample' do
  pod 'AzureCommunicationCalling', '~> 1.0.0-beta.12'
  pod 'AzureCommunicationChat', '~> 1.0.0-beta.11'
end

Установка пакетов чата

Запустите pod install для установки пакета AzureCommunicationChat. После установки откройте .xcworkspace файл .

Настройка платформы приложения

Импортируйте пакет AzureCommunicationChat в ContentView.swift, добавив следующий фрагмент кода.

import AzureCommunicationChat

Добавление элементов управления пользовательского интерфейса Teams

Добавьте следующий фрагмент кода под существующие переменные состояния в ContentView.swift.

    // Chat
    @State var chatClient: ChatClient?
    @State var chatThreadClient: ChatThreadClient?
    @State var chatMessage: String = ""
    @State var meetingMessages: [MeetingMessage] = []

    let displayName: String = "<YOUR_DISPLAY_NAME_HERE>"

Замените <YOUR_DISPLAY_NAME_HERE> на отображаемое имя, которое хотите использовать в чате.

Далее мы изменим форму Section для отображения сообщений чата и добавим элементы управления пользовательского интерфейса для чата.

Добавьте следующий фрагмент кода в существующую форму. Сразу после текстового представления Text(recordingStatus) для состояния записи.

VStack(alignment: .leading) {
    ForEach(meetingMessages, id: \.id) { message in
        let currentUser: Bool = (message.displayName == self.displayName)
        let foregroundColor = currentUser ? Color.white : Color.black
        let background = currentUser ? Color.blue : Color(.systemGray6)
        let alignment = currentUser ? HorizontalAlignment.trailing : HorizontalAlignment.leading
        VStack {
            Text(message.displayName).font(Font.system(size: 10))
            Text(message.content)
        }
        .alignmentGuide(.leading) { d in d[alignment] }
        .padding(10)
        .foregroundColor(foregroundColor)
        .background(background)
        .cornerRadius(10)
    }
}.frame(minWidth: 0, maxWidth: .infinity)
TextField("Enter your message...", text: $chatMessage)
Button(action: sendMessage) {
    Text("Send Message")
}.disabled(chatThreadClient == nil)

Далее измените заголовок на Chat Teams Quickstart. Измените следующую строку с помощью этого заголовка.

.navigationBarTitle("Chat Teams Quickstart")

Включение элементов управления пользовательского интерфейса Teams

Инициализация ChatClient

Создайте экземпляр ChatClient и включите уведомления в реальном времени. Мы используем уведомления в режиме реального времени для получения сообщений чата.

В добавьте NavigationView.onAppearфрагмент кода ниже после существующего кода, который инициализирует CallAgent.

// Initialize the ChatClient
do {
    let endpoint = "COMMUNICATION_SERVICES_RESOURCE_ENDPOINT_HERE>"
    let credential = try CommunicationTokenCredential(token: "<USER_ACCESS_TOKEN_HERE>")

    self.chatClient = try ChatClient(
        endpoint: endpoint,
        credential: credential,
        withOptions: AzureCommunicationChatClientOptions()
    )

    self.message = "ChatClient successfully created"

    // Start real-time notifications
    self.chatClient?.startRealTimeNotifications() { result in
        switch result {
        case .success:
            print("Real-time notifications started")
            // Receive chat messages
            self.chatClient?.register(event: ChatEventId.chatMessageReceived, handler: receiveMessage)
        case .failure:
            print("Failed to start real-time notifications")
            self.message = "Failed to enable chat notifications"
        }
    }
} catch {
    print("Unable to create ChatClient")
    self.message = "Please enter a valid endpoint and Chat token in source code"
    return
}

Замените <COMMUNICATION_SERVICES_RESOURCE_ENDPOINT_HERE> значением конечной точки для ресурса Служб коммуникации. Замените <USER_ACCESS_TOKEN_HERE> маркером доступа с областью чата.

Дополнительные сведения о маркерах доступа пользователей: маркер доступа пользователя

Инициализация ChatThreadClient

После того как пользователь присоединится к конференции, внутри существующей функции joinTeamsMeeting() будет выполнена инициализация ChatThreadClient.

Внутри обработчика завершения для вызова self.callAgent?.join()добавьте код под комментарием // Initialize the ChatThreadClient. Полный код показан ниже.

self.callAgent?.join(with: teamsMeetingLinkLocator, joinCallOptions: joinCallOptions) { (call, error) in
    if (error == nil) {
        self.call = call
        self.callObserver = CallObserver(self)
        self.call!.delegate = self.callObserver
        self.message = "Teams meeting joined successfully"

        // Initialize the ChatThreadClient
        do {
            guard let threadId = getThreadId(from: self.meetingLink) else {
                self.message = "Failed to join meeting chat"
                return
            }
            self.chatThreadClient = try chatClient?.createClient(forThread: threadId)
            self.message = "Joined meeting chat successfully"
        } catch {
            print("Failed to create ChatThreadClient")
            self.message = "Failed to join meeting chat"
            return
        }
    } else {
        print("Failed to join Teams meeting")
    }
}

Добавьте в ContentView следующую вспомогательную функцию, которая используется для извлечения идентификатора беседы чата из ссылки на конференцию Teams.

func getThreadId(from meetingLink: String) -> String? {
    if let range = self.meetingLink.range(of: "meetup-join/") {
        let thread = self.meetingLink[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: self.displayName
    )
    
    self.chatThreadClient?.send(message: message) { result, _ in
        switch result {
        case .success:
            print("Chat message sent")
        case .failure:
            print("Failed to send chat message")
        }

        self.chatMessage = ""
    }
}

Включение получения сообщений

Для получения сообщений мы реализуем обработчик событий ChatMessageReceived . При отправке новых сообщений в поток этот обработчик добавляет сообщения в meetingMessages переменную, чтобы их можно было отобразить в пользовательском интерфейсе.

Сначала добавьте следующую структуру в ContentView.swift. Пользовательский интерфейс использует данные в структуре для отображения сообщений чата.

struct MeetingMessage {
    let id: String
    let content: String
    let displayName: String
}

Затем добавьте функцию receiveMessage() в ContentView. Это обработчик, который вызывается при каждом возникновении события ChatMessageReceived.

func receiveMessage(response: Any, eventId: ChatEventId) {
    let chatEvent: ChatMessageReceivedEvent = response as! ChatMessageReceivedEvent

    let displayName: String = chatEvent.senderDisplayName ?? "Unknown User"
    let content: String = chatEvent.message.replacingOccurrences(of: "<[^>]+>", with: "", options: String.CompareOptions.regularExpression)

    self.meetingMessages.append(
        MeetingMessage(
            id: chatEvent.id,
            content: content,
            displayName: displayName
        )
    )
}

Выход из чата

Когда пользователь покидает собрание команды, мы очищаем чат из пользовательского интерфейса. Полный код показан ниже.

func leaveMeeting() {
    if let call = call {
        call.hangUp(options: nil, completionHandler: { (error) in
            if error == nil {
                self.message = "Leaving Teams meeting was successful"
                // Clear the chat
                self.meetingMessages.removeAll()
            } else {
                self.message = "Leaving Teams meeting failed"
            }
        })
    } else {
        self.message = "No active call to hanup"
    }
}

Получение данных о беседе в чате для пользователя Служб коммуникации

Сведения о собрании Teams можно получить с помощью API-интерфейсов Graph. Подробности см. в документации по Graph. Пакет SDK вызовов Служб коммуникации принимает полную ссылку на собрание Teams или идентификатор собрания. Они предоставляются как часть ресурса onlineMeeting, доступного под свойством joinWebUrl.

С помощью API Graph можно также получить threadID. Ответ содержит chatInfo объект , содержащий threadID.

Вы также можете получить необходимую информацию о собрании и идентификатор потока по URL-адресу Присоединиться к собранию в самом приглашении на собрание в Teams. Ссылка на собрание в Teams выглядит следующим образом: https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here. — threadID это расположение meeting_chat_thread_id в ссылке. Перед использованием убедитесь, что объект meeting_chat_thread_id не экранирован. Он должен иметь следующий формат: 19:meeting_ZWRhZDY4ZGUtYmRlNS00OWZaLTlkZTgtZWRiYjIxOWI2NTQ4@thread.v2.

Выполнение кода

Запустите приложение.

Чтобы присоединиться к собранию Teams, введите ссылку на собрание команды в пользовательском интерфейсе.

После присоединения к собранию команды необходимо допустить пользователя к собранию в клиенте вашей команды. После того как пользователь будет допущен и присоединился к чату, вы сможете отправлять и получать сообщения.

Снимок экрана: готовое приложение iOS

Примечание

Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в статье Возможности собраний Teams для внешних пользователей Teams.

В этом кратком руководстве описывается, как общаться в чате собрании Teams с помощью пакета SDK для чата Служб коммуникации Azure для Android.

Предварительные требования

Обеспечение взаимодействия с Teams

Пользователь Служб коммуникации, который присоединяется к собранию в Teams в качестве гостевого пользователя, может получить доступ к чату собрания, только когда он присоединится к вызову собрания в Teams. Дополнительные сведения о добавлении пользователя Служб коммуникации в вызов собрания в Teams см. в документации по взаимодействию с Teams.

Для использования этой функции пользователь должен быть членом организации-владельца обеих сущностей.

Присоединение к чату собрания

Включив взаимодействие с Teams, пользователь Служб коммуникации может присоединиться в качестве внешнего пользователя к вызову в Teams с помощью пакета SDK для вызовов. Присоединение к звонку добавляет их в качестве участника в чат собрания, где они могут отправлять и получать сообщения с другими пользователями во время звонка. Пользователь не будет видеть сообщения в чате, отправленные до его присоединения к вызову. Чтобы присоединиться к собранию и начать беседу, можно выполнить следующие действия.

Добавление чата в вызывающее приложение Teams

На уровне build.gradleмодуля добавьте зависимость от пакета SDK для чата.

Важно!

Известная проблема: при использовании в Android пакетов SDK для вызовов и чатов вместе в одном приложении функция уведомлений в режиме реального времени пакета SDK для чата не работает. Возникает проблема с разрешением зависимости. Пока мы работаем над решением, вы можете отключить функцию уведомлений в режиме реального времени, добавив следующие исключения в зависимость от SDK для чата в файле приложения build.gradle:

implementation ("com.azure.android:azure-communication-chat:1.0.0") {
    exclude group: 'com.microsoft', module: 'trouter-client-android'
}

Добавление макета пользовательского интерфейса Teams

Замените код в 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

Импорт пакетов и определение переменных состояния

К содержимому 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 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 для вызовов, нам придется опрашивать API GetMessages через определенные интервалы. В нашем примере мы будем использовать 3-секундные интервалы.

Из списка сообщений, возвращенного API GetMessages, можно получить следующие данные:

  • сообщения 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 не задает отображаемые имена участников потока чата. Имена возвращаются как null в API для перечисления участников, в событии participantsAdded и в событии participantsRemoved . Отображаемые имена участников чата можно получить из поля remoteParticipants объекта call.

Получение данных о беседе в чате для пользователя Служб коммуникации

Сведения о собрании Teams можно получить с помощью API-интерфейсов Graph. Подробности см. в документации по Graph. Пакет SDK вызовов Служб коммуникации принимает полную ссылку на собрание Teams или идентификатор собрания. Они предоставляются как часть ресурса onlineMeeting, доступного под свойством joinWebUrl.

С помощью API Graph можно также получить threadID. Ответ содержит chatInfo объект , содержащий threadID.

Вы также можете получить необходимую информацию о собрании и идентификатор потока по URL-адресу Присоединиться к собранию в самом приглашении на собрание в Teams. Ссылка на собрание в Teams выглядит следующим образом: https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here. — threadId это расположение meeting_chat_thread_id в ссылке. Перед использованием убедитесь, что объект meeting_chat_thread_id не экранирован. Он должен иметь следующий формат: 19:meeting_ZWRhZDY4ZGUtYmRlNS00OWZaLTlkZTgtZWRiYjIxOWI2NTQ4@thread.v2.

Выполнение кода

Теперь вы можете запустить приложение с помощью кнопки Run App (Запустить приложение) на панели инструментов или нажав клавиши SHIFT+F10.

Чтобы присоединиться к собранию и чату Teams, введите ссылку на собрание Teams и идентификатор потока в пользовательском интерфейсе.

После присоединения к собранию команды необходимо допустить пользователя к собранию в клиенте вашей команды. После того как пользователь будет допущен и присоединился к чату, вы сможете отправлять и получать сообщения.

Снимок экрана: готовое приложение Android

Примечание

Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в статье Возможности собраний Teams для внешних пользователей Teams.

В этом кратком руководстве описывается, как общаться в чате на собрании Teams с помощью пакета SDK для чата Служб коммуникации Azure для C#.

Образец кода

Код для этого краткого руководства можно найти на сайте GitHub.

Предварительные требования

Присоединение к чату собрания

Пользователь Служб коммуникации может анонимно присоединиться к собранию Teams с помощью пакета SDK для вызовов. При этом пользователь будет также добавлен в чат собрания как его участник, где он сможет обмениваться сообщениями с другими участниками собрания. У пользователя не будет доступа к сообщениям чата, которые были отправлены до того, как он присоединился к собранию, и он не сможет отправлять или получать сообщения после завершения собрания. Чтобы присоединиться к собранию и начать беседу, можно выполнить следующие действия.

Выполнение кода

Вы можете выполнить сборку и запустить код в Visual Studio. Учтите, что поддерживаются такие платформы решения: x64, x86 и ARM64.

  1. Откройте экземпляр PowerShell, Терминал Windows, командную строку или аналогичную среду и перейдите к каталогу, в который вы хотите клонировать пример.
  2. git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git
  3. Откройте проект ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj в Visual Studio.
  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. Используя ресурс Служб коммуникации, полученный на этапе выполнения предварительных требований, добавьте строку подключения в файл 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 в соответствующем поле (см. следующий раздел).
  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 ocurred 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 Graph. Подробности см. в документации по Graph. Эта ссылка возвращается как часть ресурса onlineMeeting, доступного под свойством joinWebUrl.

Вы также можете получить необходимую ссылку на собрание из URL-адреса Присоединиться к собранию в самом приглашении на собрание Teams. Ссылка на собрание в Teams выглядит следующим образом: https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here.

Снимок экрана: готовое приложение C#.

Примечание

Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в статье Возможности собраний Teams для внешних пользователей Teams.

Очистка ресурсов

Если вы хотите очистить и удалить подписку на Службы коммуникации, вы можете удалить ресурс или группу ресурсов. При этом удаляются все ресурсы, связанные с ней. См. сведения об очистке ресурсов.

Дальнейшие действия

Дополнительные сведения см. в следующих статьях: