Refactor WebSocket message interfaces

Changes:
- Replace AccountTextMessage with an extendable ContactMessage interface
- Add accountId parameter to server-side WebSocket callbacks
- Set the accountId for WebRTC messages on server-side for security
- Rename all WebRTC and SDP variables to proper camelCase or PascalCase

GitLab: #147
Change-Id: I125b5431821b03ef4d46b751eb1c13830017ccff
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
new file mode 100644
index 0000000..3999259
--- /dev/null
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+import { WebRtcIceCandidate, WebRtcSdp, WebSocketMessageType } from 'jami-web-common';
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+
+import { WithChildren } from '../utils/utils';
+import { ConversationContext } from './ConversationProvider';
+import { WebSocketContext } from './WebSocketProvider';
+
+interface IWebRtcContext {
+  isConnected: boolean;
+
+  remoteStreams: readonly MediaStream[] | undefined;
+  webRtcConnection: RTCPeerConnection | undefined;
+
+  sendWebRtcOffer: (sdp: RTCSessionDescriptionInit) => Promise<void>;
+}
+
+const defaultWebRtcContext: IWebRtcContext = {
+  isConnected: false,
+  remoteStreams: undefined,
+  webRtcConnection: undefined,
+  sendWebRtcOffer: async () => {},
+};
+
+export const WebRtcContext = createContext<IWebRtcContext>(defaultWebRtcContext);
+
+export default ({ children }: WithChildren) => {
+  const webSocket = useContext(WebSocketContext);
+  const { conversation } = useContext(ConversationContext);
+  const [webRtcConnection, setWebRtcConnection] = useState<RTCPeerConnection | undefined>();
+  const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
+  const [isConnected, setIsConnected] = useState(false);
+
+  // TODO: This logic will have to change to support multiple people in a call
+  const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
+
+  useEffect(() => {
+    if (!webRtcConnection) {
+      // TODO: Use SFL iceServers
+      const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
+      setWebRtcConnection(new RTCPeerConnection(iceConfig));
+    }
+  }, [webRtcConnection]);
+
+  const sendWebRtcOffer = useCallback(
+    async (sdp: RTCSessionDescriptionInit) => {
+      if (!webRtcConnection || !webSocket) {
+        throw new Error('Could not send WebRTC offer');
+      }
+
+      const webRtcOffer: WebRtcSdp = {
+        contactId: contactUri,
+        sdp,
+      };
+
+      console.info('Sending WebRtcOffer', webRtcOffer);
+      webSocket.send(WebSocketMessageType.WebRtcOffer, webRtcOffer);
+      await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
+    },
+    [webRtcConnection, webSocket, contactUri]
+  );
+
+  const sendWebRtcAnswer = useCallback(
+    (sdp: RTCSessionDescriptionInit) => {
+      if (!webRtcConnection || !webSocket) {
+        throw new Error('Could not send WebRTC answer');
+      }
+
+      const webRtcAnswer: WebRtcSdp = {
+        contactId: contactUri,
+        sdp,
+      };
+
+      console.info('Sending WebRtcAnswer', webRtcAnswer);
+      webSocket.send(WebSocketMessageType.WebRtcAnswer, webRtcAnswer);
+    },
+    [contactUri, webRtcConnection, webSocket]
+  );
+
+  useEffect(() => {
+    if (!webSocket || !webRtcConnection) {
+      return;
+    }
+
+    const webRtcOfferListener = async (data: WebRtcSdp) => {
+      console.info('Received event on WebRtcOffer', data);
+      await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
+
+      const sdp = await webRtcConnection.createAnswer({
+        offerToReceiveAudio: true,
+        offerToReceiveVideo: true,
+      });
+      sendWebRtcAnswer(sdp);
+      await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
+      setIsConnected(true);
+    };
+
+    const webRtcAnswerListener = async (data: WebRtcSdp) => {
+      console.info('Received event on WebRtcAnswer', data);
+      await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
+      setIsConnected(true);
+    };
+
+    const webRtcIceCandidateListener = async (data: WebRtcIceCandidate) => {
+      console.info('Received event on WebRtcIceCandidate', data);
+      await webRtcConnection.addIceCandidate(data.candidate);
+    };
+
+    webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
+    webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
+    webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
+
+    return () => {
+      webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
+      webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
+      webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
+    };
+  }, [webSocket, webRtcConnection, sendWebRtcAnswer]);
+
+  useEffect(() => {
+    if (!webRtcConnection || !webSocket) {
+      return;
+    }
+
+    const iceCandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
+      console.info('Received WebRTC event on icecandidate', event);
+      if (!contactUri) {
+        throw new Error('Could not handle WebRTC event on icecandidate: contactUri is not defined');
+      }
+
+      if (event.candidate) {
+        const webRtcIceCandidate: WebRtcIceCandidate = {
+          contactId: contactUri,
+          candidate: event.candidate,
+        };
+
+        console.info('Sending WebRtcIceCandidate', webRtcIceCandidate);
+        webSocket.send(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidate);
+      }
+    };
+
+    const trackEventListener = (event: RTCTrackEvent) => {
+      console.info('Received WebRTC event on track', event);
+      setRemoteStreams(event.streams);
+    };
+
+    webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
+    webRtcConnection.addEventListener('track', trackEventListener);
+
+    return () => {
+      webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
+      webRtcConnection.removeEventListener('track', trackEventListener);
+    };
+  }, [webRtcConnection, webSocket, contactUri]);
+
+  return (
+    <WebRtcContext.Provider
+      value={{
+        isConnected,
+        remoteStreams,
+        webRtcConnection,
+        sendWebRtcOffer,
+      }}
+    >
+      {children}
+    </WebRtcContext.Provider>
+  );
+};