Snabbstart: Ansluta din chattapp till ett Teams-möte

Kom igång med Azure Communication Services genom att ansluta din chattlösning till Microsoft Teams.

I den här snabbstarten får du lära dig hur du chattar i ett Teams-möte med hjälp av Azure Communication Services Chat SDK för JavaScript.

Exempelkod

Hitta den färdiga koden för den här snabbstarten på GitHub.

Förutsättningar

Ansluta till möteschatten

En Communication Services-användare kan ansluta till ett Teams-möte som en anonym användare med hjälp av den anropande SDK:t. När de ansluter till mötet läggs de också till som deltagare i möteschatten, där de kan skicka och ta emot meddelanden med andra användare i mötet. Användaren har inte åtkomst till chattmeddelanden som skickades innan de gick med i mötet och de kommer inte att kunna skicka eller ta emot meddelanden när mötet har avslutats. Om du vill ansluta till mötet och börja chatta kan du följa nästa steg.

Skapa ett nytt Node.js-program

Öppna terminalen eller kommandofönstret, skapa en ny katalog för din app och navigera till den.

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

Kör npm init -y för att skapa en package.json fil med standardinställningar.

npm init -y

Installera chattpaketen

npm install Använd kommandot för att installera nödvändiga Communication Services-SDK:er för JavaScript.

npm install @azure/communication-common --save

npm install @azure/communication-identity --save

npm install @azure/communication-chat --save

npm install @azure/communication-calling --save

Alternativet --save visar biblioteket som ett beroende i din package.json-fil .

Konfigurera appramverket

Den här snabbstarten använder Webpack för att paketera programtillgångarna. Kör följande kommando för att installera npm-paketen Webpack, webpack-cli och webpack-dev-server och lista dem som utvecklingsberoenden i din package.json:

npm install webpack@5.89.0 webpack-cli@5.1.4 webpack-dev-server@4.15.1 --save-dev

Skapa en index.html fil i rotkatalogen för projektet. Vi använder den här filen för att konfigurera en grundläggande layout som gör att användaren kan ansluta till ett möte och börja chatta.

Lägga till teams användargränssnittskontroller

Ersätt koden i index.html med följande kodfragment. Textrutan överst på sidan används för att ange möteskontexten för Teams. Knappen Anslut till Teams-möte används för att ansluta till det angivna mötet. Ett popup-fönster för chatt visas längst ned på sidan. Den kan användas för att skicka meddelanden i mötestråden och visas i realtid alla meddelanden som skickas i tråden medan Communication Services-användaren är medlem.

<!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>

Aktivera Teams användargränssnittskontroller

Ersätt innehållet i client.js-filen med följande kodfragment.

I kodfragmentet ersätter du

  • SECRET_CONNECTION_STRINGmed kommunikationstjänstens anslutningssträng
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}`);
});

Visningsnamn för chatttrådsdeltagarna anges inte av Teams-klienten. Namnen returneras som null i API:et för att visa deltagare, i participantsAdded händelsen och i participantsRemoved händelsen. Visningsnamnen för chattdeltagarna kan hämtas från remoteParticipants objektets call fält. När du får ett meddelande om en ändring av listan kan du använda den här koden för att hämta namnet på den användare som lades till eller togs bort:

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

Kör koden

Webpack-användare kan använda webpack-dev-server för att skapa och köra din app. Kör följande kommando för att paketera programvärden på en lokal webbserver:

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

Öppna din webbläsare och gå till http://localhost:8080/. Du bör se att appen startas enligt följande skärmbild:

Skärmbild av det färdiga JavaScript-programmet.

Infoga teams-möteslänken i textrutan. Tryck på Anslut till Teams-möte för att ansluta till Teams-mötet. När Communication Services-användaren har blivit antagen till mötet kan du chatta inifrån ditt Communication Services-program. Gå till rutan längst ned på sidan för att börja chatta. För enkelhetens skull visar programmet bara de två sista meddelandena i chatten.

Kommentar

Vissa funktioner stöds för närvarande inte för samverkansscenarier med Teams. Läs mer om de funktioner som stöds i Teams mötesfunktioner för externa Teams-användare

I den här snabbstarten får du lära dig hur du chattar i ett Teams-möte med hjälp av Azure Communication Services Chat SDK för iOS.

Exempelkod

Om du vill gå vidare till slutet kan du ladda ned den här snabbstarten som ett exempel på GitHub.

Förutsättningar

  • Ett Azure-konto med en aktiv prenumeration. Skapa ett konto kostnadsfritt
  • En Mac som kör Xcode, tillsammans med ett giltigt utvecklarcertifikat installerat i nyckelringen.
  • En Teams-distribution.
  • En användaråtkomsttoken för azure-kommunikationstjänsten. Du kan också använda Azure CLI och köra kommandot med din anslutningssträng för att skapa en användare och en åtkomsttoken.
az communication identity token issue --scope voip --connection-string "yourConnectionString"

Mer information finns i Använda Azure CLI för att skapa och hantera åtkomsttoken.

Konfigurera

Skapa Xcode-projektet

I Xcode skapar du ett nytt iOS-projekt och väljer mallen Enkel vyapp. I den här självstudien används SwiftUI-ramverket, så du bör ange Språket till Swift och användargränssnittet till SwiftUI. Du kommer inte att skapa tester under den här snabbstarten. Avmarkera Inkludera tester.

Skärmbild som visar fönstret Nytt projekt i Xcode.

Installera CocoaPods

Använd den här guiden för att installera CocoaPods på din Mac.

Installera paketet och beroenden med CocoaPods

  1. Om du vill skapa en Podfile för ditt program öppnar du terminalen och navigerar till projektmappen och kör podd-init.

  2. Lägg till följande kod Podfile under målet och spara.

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. Kör pod install.

  2. .xcworkspace Öppna filen med Xcode.

Begär åtkomst till mikrofonen

För att få åtkomst till enhetens mikrofon måste du uppdatera appens informationsegenskapslista med en NSMicrophoneUsageDescription. Du anger det associerade värdet till en string som ingick i den dialogruta som systemet använder för att begära åtkomst från användaren.

Under målet väljer du fliken Info och lägger till en sträng för "Sekretess – Beskrivning av mikrofonanvändning"

Skärmbild som visar hur du lägger till mikrofonanvändning i Xcode.

Inaktivera sandbox-miljö för användarskript

Några av skripten i de länkade biblioteken skriver filer under byggprocessen. Om du vill tillåta detta inaktiverar du sandbox-miljön för användarskript i Xcode. Under bygginställningarna söker sandbox du efter och anger User Script Sandboxing till No.

Skärmbild som visar inaktivering av sandbox-miljö för användarskript i Xcode.

Ansluta till möteschatten

En Communication Services-användare kan ansluta till ett Teams-möte som en anonym användare med hjälp av den anropande SDK:t. När en användare har anslutit till Teams-mötet kan de skicka och ta emot meddelanden med andra mötesdeltagare. Användaren kommer inte att ha åtkomst till chattmeddelanden som skickats före anslutningen, och de kommer inte heller att kunna skicka eller ta emot meddelanden när de inte är med i mötet. Om du vill ansluta till mötet och börja chatta kan du följa nästa steg.

Konfigurera appramverket

Importera Azure Communication-paketen genom ContentView.swift att lägga till följande kodfragment:

import AVFoundation
import SwiftUI

import AzureCommunicationCalling
import AzureCommunicationChat

Lägg ContentView.swift till följande kodfragment precis ovanför deklarationen struct ContentView: View :

let endpoint = "<ADD_YOUR_ENDPOINT_URL_HERE>"
let token = "<ADD_YOUR_USER_TOKEN_HERE>"
let displayName: String = "Quickstart User"

Ersätt <ADD_YOUR_ENDPOINT_URL_HERE> med slutpunkten för din Communication Services-resurs. Ersätt <ADD_YOUR_USER_TOKEN_HERE> med den token som genererades ovan via Azure-klientkommandoraden. Läs mer om användaråtkomsttoken: Användaråtkomsttoken

Ersätt Quickstart User med det visningsnamn som du vill använda i chatten.

Om du vill lagra tillståndet lägger du till följande variabler i structen 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] = []

Nu ska vi lägga till huvudtexten var för att lagra användargränssnittselementen. Vi kopplar affärslogik till dessa kontroller i den här snabbstarten. Lägg till följande kod i structen 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
    }
  }

Initiera ChatClient

Instansiera ChatClient och aktivera meddelandemeddelanden. Vi använder realtidsmeddelanden för att ta emot chattmeddelanden.

Med huvudtexten konfigurerad lägger vi till funktionerna för att hantera konfigurationen av samtals- och chattklienter.

onAppear I funktionen lägger du till följande kod för att initiera CallClient och ChatClient:

  if let threadIdFromMeetingLink = getThreadId(from: self.meetingLink) {
    self.chatThreadId = threadIdFromMeetingLink
  }
  // Authenticate
  do {
    let credentials = try CommunicationTokenCredential(token: token)
    self.callClient = CallClient()
    self.callClient?.createCallAgent(
      userCredential: credentials
    ) { agent, error in
      if let e = error {
        self.message = "ERROR: It was not possible to create a call agent."
        print(e)
        return
      } else {
        self.callAgent = agent
      }
    }
  
    // Start the chat client
    self.chatClient = try ChatClient(
      endpoint: endpoint,
      credential: credentials,
      withOptions: AzureCommunicationChatClientOptions()
    )
    // Register for real-time notifications
    self.chatClient?.startRealTimeNotifications { result in
      switch result {
      case .success:
        self.chatClient?.register(
          event: .chatMessageReceived,
          handler: receiveMessage
      )
      case let .failure(error):
        self.message = "Could not register for message notifications: " + error.localizedDescription
        print(error)
      }
    }
  } catch {
    print(error)
    self.message = error.localizedDescription
  }

Lägg till möteskopplingsfunktion

Lägg till följande funktion i structen ContentView för att hantera anslutningen till mötet.

  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"
      }
    }
  }

Initiera ChatThreadClient

Vi initierar ChatThreadClient när användaren har anslutit till mötet. Detta kräver att vi kontrollerar mötesstatusen från ombudet och sedan initierar ChatThreadClient med när det är anslutet threadId till mötet.

connectChat() Skapa funktionen med följande kod:

  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
    }
  }

Lägg till följande hjälpfunktion i , ContentViewsom används för att parsa chatttråds-ID:t från teamets möteslänk, om möjligt. Om den här extraheringen misslyckas måste användaren manuellt ange chatttråds-ID:t med graph-API:er för att hämta tråd-ID:t.

 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
}

Aktivera sändning av meddelanden

Lägg till funktionen i sendMessage()ContentView. Den här funktionen använder ChatThreadClient för att skicka meddelanden från användaren.

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?"
    }
  }
}

Aktivera mottagande meddelanden

För att ta emot meddelanden implementerar vi hanteraren för ChatMessageReceived händelser. När nya meddelanden skickas till tråden lägger den här hanteraren till meddelandena i variabeln meetingMessages så att de kan visas i användargränssnittet.

Lägg först till följande struct i ContentView.swift. Användargränssnittet använder data i struct för att visa våra chattmeddelanden.

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
    )
  }
}

Lägg sedan till funktionen i receiveMessage()ContentView. Detta anropas när en meddelandehändelse inträffar. Observera att du måste registrera dig för alla händelser som du vill hantera i -instruktionen switchchatClient?.register() via -metoden.

  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
    }
  }

Slutligen måste vi implementera ombudshanteraren för anropsklienten. Den här hanteraren används för att kontrollera samtalsstatusen och initiera chattklienten när användaren ansluter till mötet.

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"
    }
  }
}

Lämna chatten

När användaren lämnar teamets möte rensar vi chattmeddelandena från användargränssnittet och lägger på samtalet. Den fullständiga koden visas nedan.

  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"
    }
  }

Hämta en Teams-möteschatttråd för en Communication Services-användare

Mötesinformationen för Teams kan hämtas med graph-API:er, som beskrivs i Graph-dokumentationen. Communication Services Calling SDK accepterar en fullständig Teams-möteslänk eller ett mötes-ID. De returneras som en del av resursenonlineMeeting, som är tillgänglig under egenskapen joinWebUrl

Med Graph-API:erna kan du också hämta threadID. Svaret har ett chatInfo objekt som innehåller threadID.

Kör koden

Kör programmet.

Om du vill ansluta till Teams-mötet anger du din teams möteslänk i användargränssnittet.

När du har anslutit till teamets möte måste du släppa in användaren till mötet i teamets klient. När användaren är antagen och har anslutit till chatten kan du skicka och ta emot meddelanden.

Skärmbild av det slutförda iOS-programmet.

Kommentar

Vissa funktioner stöds för närvarande inte för samverkansscenarier med Teams. Läs mer om de funktioner som stöds i Teams mötesfunktioner för externa Teams-användare

I den här snabbstarten får du lära dig hur du chattar i ett Teams-möte med hjälp av Azure Communication Services Chat SDK för Android.

Exempelkod

Om du vill gå vidare till slutet kan du ladda ned den här snabbstarten som ett exempel på GitHub.

Förutsättningar

Aktivera Teams-samverkan

En Communication Services-användare som ansluter till ett Teams-möte som gästanvändare kan endast komma åt mötets chatt när de har anslutit till Teams-mötessamtalet. Se Teams interop-dokumentationen för att lära dig hur du lägger till en Communication Services-användare i ett Teams-mötessamtal.

Du måste vara medlem i den ägande organisationen för båda entiteterna för att kunna använda den här funktionen.

Ansluta till möteschatten

När Teams-samverkan har aktiverats kan en Communication Services-användare ansluta teams-anropet som en extern användare med hjälp av Calling SDK. När du ansluter till samtalet läggs de också till som deltagare i möteschatten, där de kan skicka och ta emot meddelanden med andra användare i samtalet. Användaren har inte åtkomst till chattmeddelanden som skickades innan de anslöt till samtalet. Om du vill ansluta till mötet och börja chatta kan du följa nästa steg.

Lägg till chatt i Teams-samtalsappen

På modulnivån build.gradlelägger du till beroendet för chatt-SDK:n.

Viktigt!

Känt problem: När du använder Android Chat och Calling SDK tillsammans i samma program fungerar inte funktionen för chatt-SDK:ns realtidsaviseringar. Du får ett problem med beroendelösning. När vi arbetar med en lösning kan du inaktivera funktionen för realtidsaviseringar genom att lägga till följande undantag i Chat SDK-beroendet i appens build.gradle fil:

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

Lägga till teams användargränssnittslayout

Ersätt koden i activity_main.xml med följande kodfragment. Den lägger till indata för tråd-ID:t och för att skicka meddelanden, en knapp för att skicka det inskrivna meddelandet och en grundläggande chattlayout.

<?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>

Aktivera Teams användargränssnittskontroller

Importera paket och definiera tillståndsvariabler

Lägg till följande importer i innehållet MainActivity.javai :

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;

Lägg till följande variabler i MainActivity klassen:

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

Ersätt <USER_ID> med ID:t för användaren som initierar chatten. Ersätt <COMMUNICATION_SERVICES_RESOURCE_ENDPOINT> med slutpunkten för din Communication Services-resurs.

Initiera ChatThreadClient

När du har anslutit till mötet instansierar ChatThreadClient du och gör chattkomponenterna synliga.

Uppdatera slutet av MainActivity.joinTeamsMeeting() metoden med koden nedan:

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

Aktivera sändning av meddelanden

Lägg till metoden i sendMessage()MainActivity. Den använder ChatThreadClient för att skicka meddelanden för användarens räkning.

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

Aktivera avsökning för meddelanden och återge dem i programmet

Viktigt!

Känt problem: Eftersom chatt-SDK:ns realtidsaviseringar inte fungerar tillsammans med anropande SDK:er måste vi avsöka API:et GetMessages med fördefinierade intervall. I vårt exempel använder vi 3-sekundersintervall.

Vi kan hämta följande data från meddelandelistan som returneras av API:et GetMessages :

  • Meddelandena text och html i tråden sedan anslutningen
  • Ändringar i trådlistan
  • Uppdateringar till trådavsnittet

MainActivity I klassen lägger du till en hanterare och en körningsbar uppgift som ska köras med 3 sekunders intervall:

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

Observera att aktiviteten redan har startats i slutet av MainActivity.joinTeamsMeeting() metoden som uppdaterades i initieringssteget.

Slutligen lägger vi till metoden för att köra frågor mot alla tillgängliga meddelanden i tråden, parsa dem efter meddelandetyp och visa och 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);
        }
    }

Visningsnamn för chatttrådsdeltagarna anges inte av Teams-klienten. Namnen returneras som null i API:et för att visa deltagare, i participantsAdded händelsen och i participantsRemoved händelsen. Visningsnamnen för chattdeltagarna kan hämtas från remoteParticipants objektets call fält.

Hämta en Teams-möteschatttråd för en Communication Services-användare

Mötesinformationen för Teams kan hämtas med graph-API:er, som beskrivs i Graph-dokumentationen. Communication Services Calling SDK accepterar en fullständig Teams-möteslänk eller ett mötes-ID. De returneras som en del av resursenonlineMeeting, som är tillgänglig under egenskapen joinWebUrl

Med Graph-API:erna kan du också hämta threadID. Svaret har ett chatInfo objekt som innehåller threadID.

Kör koden

Appen kan nu startas med knappen "Kör app" i verktygsfältet (Skift+F10).

Om du vill ansluta till Teams-mötet och chatten anger du din teams möteslänk och tråd-ID:t i användargränssnittet.

När du har anslutit till teamets möte måste du släppa in användaren till mötet i teamets klient. När användaren är antagen och har anslutit till chatten kan du skicka och ta emot meddelanden.

Skärmbild av det färdiga Android-programmet.

Kommentar

Vissa funktioner stöds för närvarande inte för samverkansscenarier med Teams. Läs mer om de funktioner som stöds i Teams mötesfunktioner för externa Teams-användare

I den här snabbstarten lär du dig att chatta i ett Teams-möte med hjälp av Azure Communication Services Chat SDK för C#.

Exempelkod

Hitta koden för den här snabbstarten på GitHub.

Förutsättningar

Ansluta till möteschatten

En Communication Services-användare kan ansluta till ett Teams-möte som en anonym användare med hjälp av den anropande SDK:t. När de ansluter till mötet läggs de också till som deltagare i möteschatten, där de kan skicka och ta emot meddelanden med andra användare i mötet. Användaren har inte åtkomst till chattmeddelanden som skickades innan de gick med i mötet och de kommer inte att kunna skicka eller ta emot meddelanden när mötet har avslutats. Om du vill ansluta till mötet och börja chatta kan du följa nästa steg.

Kör koden

Du kan skapa och köra koden i Visual Studio. Observera de lösningsplattformar som vi stöder: x64,x86och ARM64.

  1. Öppna en instans av PowerShell, Windows-terminal, Kommandotolken eller motsvarande och navigera till den katalog som du vill klona exemplet till.
  2. git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git
  3. Öppna projektet ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj i Visual Studio.
  4. Installera följande NuGet-paketversioner (eller senare):
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. När Communication Services-resursen anskaffas i förhandskraven lägger du till anslutningssträngen i filen 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_ = "";

Viktigt!

  • Välj rätt plattform i listrutan "Lösningsplattformar" i Visual Studio innan du kör koden, d.v.s. x64
  • Kontrollera att utvecklarläget i Windows 10 är aktiverat (developer Inställningar)

Nästa steg fungerar inte om detta inte är korrekt konfigurerat

  1. Tryck på F5 för att starta projektet i felsökningsläge.
  2. Klistra in en giltig möteslänk för teams i rutan Möteslänk för Teams (se nästa avsnitt)
  3. Tryck på "Join Teams meeting" (Anslut till Teams-mötet) för att börja chatta.

Viktigt!

När den anropande SDK:n upprättar anslutningen till teammötet Se Kommunikationstjänster som anropar Windows-appen är de viktigaste funktionerna för att hantera chattåtgärder: StartPollingForChatMessages och SendMessageButton_Click. Båda kodfragmenten finns i 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;
        }

Teams möteslänk kan hämtas med graph-API:er, som beskrivs i Graph-dokumentationen. Den här länken returneras som en del av resursen onlineMeeting som är tillgänglig under egenskapenjoinWebUrl.

Du kan också hämta den nödvändiga möteslänken från URL:en för att ansluta till mötet i själva Teams-mötesbjudningen. En Teams-möteslänk ser ut så här: https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here. Om din teams-länk har ett annat format än detta måste du hämta tråd-ID:t med hjälp av Graph-API:et.

Skärmbild av det slutförda csharp-programmet.

Kommentar

Vissa funktioner stöds för närvarande inte för samverkansscenarier med Teams. Läs mer om de funktioner som stöds i Teams mötesfunktioner för externa Teams-användare

Rensa resurser

Om du vill rensa och ta bort en Communication Services-prenumeration kan du ta bort resursen eller resursgruppen. Om du tar bort resursgruppen tas även alla andra resurser som är associerade med den bort. Läs mer om att rensa resurser.

Nästa steg

Mer information finns i följande artiklar: