diff --git a/backend/api/__init__.py b/backend/api/__init__.py
index 65e0290d0..1dd4d1686 100644
--- a/backend/api/__init__.py
+++ b/backend/api/__init__.py
@@ -99,6 +99,7 @@ def create_app():
masters,
translation,
events,
+ meeting,
)
# why blueprints http://flask.pocoo.org/docs/1.0/blueprints/
@@ -118,6 +119,7 @@ def create_app():
app.register_blueprint(mentee.mentee, url_prefix="/api/mentee")
app.register_blueprint(messages.messages, url_prefix="/api/messages")
app.register_blueprint(notifications.notifications, url_prefix="/api/notifications")
+ app.register_blueprint(meeting.meeting, url_prefix="/api/meeting")
app.register_blueprint(masters.masters, url_prefix="/api/masters")
app.register_blueprint(translation.translation, url_prefix="/api/translation")
diff --git a/backend/api/utils/jaas_jwt_builder.py b/backend/api/utils/jaas_jwt_builder.py
new file mode 100644
index 000000000..b70e5ee4f
--- /dev/null
+++ b/backend/api/utils/jaas_jwt_builder.py
@@ -0,0 +1,169 @@
+import time, uuid
+from authlib.jose import jwt
+
+class JaaSJwtBuilder:
+ """
+ The JaaSJwtBuilder class helps with the generation of the JaaS JWT.
+ """
+
+ EXP_TIME_DELAY_SEC = 7200
+ # Used as a delay for the exp claim value.
+
+ NBF_TIME_DELAY_SEC = 10
+ # Used as a delay for the nbf claim value.
+
+ def __init__(self) -> None:
+ self.header = { 'alg' : 'RS256' }
+ self.userClaims = {}
+ self.featureClaims = {}
+ self.payloadClaims = {}
+
+ def withDefaults(self):
+ """Returns the JaaSJwtBuilder with default valued claims."""
+ return self.withExpTime(int(time.time() + JaaSJwtBuilder.EXP_TIME_DELAY_SEC)) \
+ .withNbfTime(int(time.time() - JaaSJwtBuilder.NBF_TIME_DELAY_SEC)) \
+ .withLiveStreamingEnabled(True) \
+ .withRecordingEnabled(True) \
+ .withOutboundCallEnabled(True) \
+ .withTranscriptionEnabled(True) \
+ .withModerator(True) \
+ .withRoomName('*') \
+ .withUserId(str(uuid.uuid4()))
+
+ def withApiKey(self, apiKey):
+ """
+ Returns the JaaSJwtBuilder with the kid claim(apiKey) set.
+
+ :param apiKey A string as the API Key https://jaas.8x8.vc/#/apikeys
+ """
+ self.header['kid'] = apiKey
+ return self
+
+ def withUserAvatar(self, avatarUrl):
+ """
+ Returns the JaaSJwtBuilder with the avatar claim set.
+
+ :param avatarUrl A string representing the url to get the user avatar.
+ """
+ self.userClaims['avatar'] = avatarUrl
+ return self
+
+ def withModerator(self, isModerator):
+ """
+ Returns the JaaSJwtBuilder with the moderator claim set.
+
+ :param isModerator A boolean if set to True, user is moderator and False otherwise.
+ """
+ self.userClaims['moderator'] = 'true' if isModerator == True else 'false'
+ return self
+
+ def withUserName(self, userName):
+ """
+ Returns the JaaSJwtBuilder with the name claim set.
+
+ :param userName A string representing the user's name.
+ """
+ self.userClaims['name'] = userName
+ return self
+
+ def withUserEmail(self, userEmail):
+ """
+ Returns the JaaSJwtBuilder with the email claim set.
+
+ :param userEmail A string representing the user's email address.
+ """
+ self.userClaims['email'] = userEmail
+ return self
+
+ def withLiveStreamingEnabled(self, isEnabled):
+ """
+ Returns the JaaSJwtBuilder with the livestreaming claim set.
+
+ :param isEnabled A boolean if set to True, live streaming is enabled and False otherwise.
+ """
+ self.featureClaims['livestreaming'] = 'true' if isEnabled == True else 'false'
+ return self
+
+ def withRecordingEnabled(self, isEnabled):
+ """
+ Returns the JaaSJwtBuilder with the recording claim set.
+
+ :param isEnabled A boolean if set to True, recording is enabled and False otherwise.
+ """
+ self.featureClaims['recording'] = 'true' if isEnabled == True else 'false'
+ return self
+
+ def withTranscriptionEnabled(self, isEnabled):
+ """
+ Returns the JaaSJwtBuilder with the transcription claim set.
+
+ :param isEnabled A boolean if set to True, transcription is enabled and False otherwise.
+ """
+ self.featureClaims['transcription'] = 'true' if isEnabled == True else 'false'
+ return self
+
+ def withOutboundCallEnabled(self, isEnabled):
+ """
+ Returns the JaaSJwtBuilder with the outbound-call claim set.
+
+ :param isEnabled A boolean if set to True, outbound calls are enabled and False otherwise.
+ """
+ self.featureClaims['outbound-call'] = 'true' if isEnabled == True else 'false'
+ return self
+
+ def withExpTime(self, expTime):
+ """
+ Returns the JaaSJwtBuilder with exp claim set. Use the defaults, you won't have to change this value too much.
+
+ :param expTime Unix time in seconds since epochs plus a delay. Expiration time of the JWT.
+ """
+ self.payloadClaims['exp'] = expTime
+ return self
+
+ def withNbfTime(self, nbfTime):
+ """
+ Returns the JaaSJwtBuilder with nbf claim set. Use the defaults, you won't have to change this value too much.
+
+ :param nbfTime Unix time in seconds since epochs.
+ """
+ self.payloadClaims['nbfTime'] = nbfTime
+ return self
+
+ def withRoomName(self, roomName):
+ """
+ Returns the JaaSJwtBuilder with room claim set.
+
+ :param roomName A string representing the room to join.
+ """
+ self.payloadClaims['room'] = roomName
+ return self
+
+ def withAppID(self, AppId):
+ """
+ Returns the JaaSJwtBuilder with the sub claim set.
+
+ :param AppId A string representing the unique AppID (previously tenant).
+ """
+ self.payloadClaims['sub'] = AppId
+ return self
+
+ def withUserId(self, userId):
+ """
+ Returns the JaaSJwtBuilder with the id claim set.
+
+ :param A string representing the user, should be unique from your side.
+ """
+ self.userClaims['id'] = userId
+ return self
+
+ def signWith(self, key):
+ """
+ Returns a signed JWT.
+
+ :param key A string representing the private key in PEM format.
+ """
+ context = { 'user': self.userClaims, 'features': self.featureClaims }
+ self.payloadClaims['context'] = context
+ self.payloadClaims['iss'] = 'chat'
+ self.payloadClaims['aud'] = 'jitsi'
+ return jwt.encode(self.header, self.payloadClaims, key)
diff --git a/backend/api/views/meeting.py b/backend/api/views/meeting.py
new file mode 100644
index 000000000..c7d96d70e
--- /dev/null
+++ b/backend/api/views/meeting.py
@@ -0,0 +1,39 @@
+from __future__ import print_function
+
+import os.path, json
+from datetime import datetime, timedelta
+from flask import Blueprint
+from api.core import create_response
+import sys, os
+
+from api.utils.jaas_jwt_builder import JaaSJwtBuilder
+
+
+meeting = Blueprint("meeting", __name__)
+
+API_KEY = os.environ.get("EIGHT_X_EIGHT_API_KEY")
+APP_ID = os.environ.get("REACT_APP_EIGHT_X_EIGHT_APP_ID")
+
+@meeting.route("/generateToken", methods=["GET"])
+def generateToken():
+ try:
+ scriptDir = os.path.dirname('/')
+ fp = os.path.join(scriptDir, 'rsa-private.pem')
+
+ with open(fp, 'r') as reader:
+ PRIVATE_KEY = reader.read()
+ jaasJwt = JaaSJwtBuilder()
+ token = jaasJwt.withDefaults() \
+ .withApiKey(API_KEY) \
+ .withUserName("User Name") \
+ .withUserEmail("email_address@email.com") \
+ .withModerator(False) \
+ .withAppID(APP_ID) \
+ .withUserAvatar("https://asda.com/avatar") \
+ .signWith(PRIVATE_KEY)
+
+ return create_response(data={"token": token.decode('utf-8'), "appID": APP_ID})
+
+ except Exception as error:
+ print(error)
+ return create_response(status=422, message=f'Failed to generate token for meeting')
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index e268f0c36..a6beb6034 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -5,6 +5,7 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@emotion/css": "^11.11.2",
+ "@jitsi/react-sdk": "^1.4.0",
"@reduxjs/toolkit": "^1.9.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
@@ -24,10 +25,12 @@
"pretty-quick": "^3.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-draggable": "^4.4.6",
"react-i18next": "^12.3.1",
"react-json-to-table": "^0.1.7",
"react-player": "^2.11.2",
"react-redux": "^8.1.1",
+ "react-resizable": "^3.0.5",
"react-responsive": "^9.0.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
diff --git a/frontend/public/locales/ar/translation.json b/frontend/public/locales/ar/translation.json
index b74b047ac..530d8ffa4 100644
--- a/frontend/public/locales/ar/translation.json
+++ b/frontend/public/locales/ar/translation.json
@@ -93,7 +93,7 @@
"mentor": "مُرشِد",
"message": "رسالة",
"messages": "رسائل",
- "group_message":"رسائل المجموعة",
+ "group_message":"الدردشة الجماعية",
"name": "الاسم",
"no": "لا",
"other": "آخر",
@@ -593,13 +593,18 @@
"resend": "إعادة إرسال"
},
"meeting": {
- "title": "توليد رابط الاجتماع",
- "generateButton": "توليد",
+ "title": "إنشاء/الانضمام إلى الاجتماع",
+ "generateButton": "إنشاء غرفة",
"cancelButton": "إلغاء",
- "generatedURL": "الرابط المُولَد",
- "errorGenerating": "فشل في توليد رابط الاجتماع.",
- "errorCopy": "فشل في نسخ الرابط إلى الحافظة.",
- "copyMessage": "تم نسخ الرابط إلى الحافظة!",
- "limitWarning": "يرجى ملاحظة أن الاجتماع سيكون له حد أقصى من 100 مشارك، ولا يوجد حد زمني."
- }
+ "generatedURL": "اسم الغرفة",
+ "errorGenerating": "فشل في إنشاء الاجتماع.",
+ "errorCopy": "فشل في نسخ اسم الغرفة إلى الحافظة.",
+ "copyMessage": "تم نسخ اسم الغرفة إلى الحافظة!",
+ "limitWarning": "يرجى ملاحظة أن الاجتماع سيقتصر على 100 مشارك، ولن يكون هناك حد زمني.",
+ "placeHolder": "إنشاء أو لصق اسم الغرفة للانضمام",
+ "roomName": "يرجى تقديم اسم الغرفة للانضمام.",
+ "getToken": "غير قادر على الانضمام إلى الاجتماع",
+ "generateToken": "غير قادر على توليد رمز",
+ "joinMeeting": "الانضمام إلى الاجتماع"
+ }
}
\ No newline at end of file
diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json
index 932c77603..83a3fd591 100644
--- a/frontend/public/locales/en-US/translation.json
+++ b/frontend/public/locales/en-US/translation.json
@@ -93,7 +93,7 @@
"mentor": "Mentor",
"message": "Message",
"messages": "Messages",
- "group_message":"Group Messages",
+ "group_message":"Group Chat",
"name": "Name",
"no": "No",
"confirm": "Confirm",
@@ -594,13 +594,18 @@
"resend": "Resend"
},
"meeting": {
- "title": "Generate Meeting Link",
- "generateButton": "Generate",
+ "title": "Create/Join Meeting",
+ "generateButton": "Create Room",
"cancelButton": "Cancel",
- "generatedURL": "Generated URL",
- "errorGenerating": "Failed to generate meeting link.",
- "errorCopy": "Failed to copy URL to clipboard.",
- "copyMessage": "URL copied to clipboard!",
- "limitWarning": "Please note that the meeting will have a limit of 100 attendees, and no time limit."
+ "generatedURL": "Room Name",
+ "errorGenerating": "Failed to generate meeting.",
+ "errorCopy": "Failed to copy room name to clipboard.",
+ "copyMessage": "Room name copied to clipboard!",
+ "limitWarning": "Please note that the meeting will have a limit of 100 attendees, and no time limit.",
+ "placeHolder": "Generate or Paste Room Name to Join",
+ "roomName": "Please provide a room name to join.",
+ "getToken": "Unable to join meeting",
+ "generateToken": "Unable to generate token",
+ "joinMeeting": "Join Meeting"
}
}
\ No newline at end of file
diff --git a/frontend/public/locales/es-US/translation.json b/frontend/public/locales/es-US/translation.json
index c4ff69627..f4d148f63 100644
--- a/frontend/public/locales/es-US/translation.json
+++ b/frontend/public/locales/es-US/translation.json
@@ -93,7 +93,7 @@
"mentor": "Mentor",
"message": "Mensaje",
"messages": "Mensajes",
- "group_message":"Mensajes grupales",
+ "group_message":"Chat de Grupo",
"name": "Nombre",
"no": "No",
"other": "Otro",
@@ -593,13 +593,18 @@
"resend": "Reenviar correo electrónico"
},
"meeting": {
- "title": "Generar Enlace de Reunión",
- "generateButton": "Generar",
+ "title": "Crear/Unirse a la Reunión",
+ "generateButton": "Crear Sala",
"cancelButton": "Cancelar",
- "generatedURL": "URL Generada",
- "errorGenerating": "Error al generar el enlace de reunión.",
- "errorCopy": "Error al copiar la URL al portapapeles.",
- "copyMessage": "¡URL copiada al portapapeles!",
- "limitWarning": "Por favor, tenga en cuenta que la reunión tendrá un límite de 100 asistentes, y sin límite de tiempo."
+ "generatedURL": "Nombre de la Sala",
+ "errorGenerating": "Error al generar la reunión.",
+ "errorCopy": "Error al copiar el nombre de la sala al portapapeles.",
+ "copyMessage": "¡Nombre de la sala copiado al portapapeles!",
+ "limitWarning": "Tenga en cuenta que la reunión tendrá un límite de 100 asistentes y no habrá límite de tiempo.",
+ "placeHolder": "Genere o pegue el nombre de la sala para unirse",
+ "roomName": "Por favor proporcione un nombre de sala para unirse.",
+ "getToken": "No se puede unir a la reunión",
+ "generateToken": "No se puede generar el token",
+ "joinMeeting": "Unirse a la Reunión"
}
}
\ No newline at end of file
diff --git a/frontend/public/locales/fa-AF/translation.json b/frontend/public/locales/fa-AF/translation.json
index 47119f2d6..5b25bbfe4 100644
--- a/frontend/public/locales/fa-AF/translation.json
+++ b/frontend/public/locales/fa-AF/translation.json
@@ -49,9 +49,6 @@
"traing":{
"completed":"تکمیل شد"
},
- "traing":{
- "completed":"تکمیل شد"
- },
"appointmentStatus": {
"accepted": "تایید شده",
"denied": "رد شده",
@@ -96,7 +93,7 @@
"mentor": "مربی",
"message": "پیام",
"messages": "پیام ها",
- "group_message":"پیام های گروهی",
+ "group_message":"گپ گروهی",
"name": "نام",
"no": "نه",
"other": "سایر",
@@ -596,13 +593,18 @@
"resend": "ارسال مجدد ایمیل"
},
"meeting": {
- "title": "تولید لینک جلسه",
- "generateButton": "تولید",
+ "title": "ایجاد/پیوستن به جلسه",
+ "generateButton": "ایجاد اتاق",
"cancelButton": "لغو",
- "generatedURL": "لینک تولید شده",
- "errorGenerating": "امکان تولید لینک جلسه وجود ندارد.",
- "errorCopy": "امکان کپی کردن لینک به کلیپبورد وجود ندارد.",
- "copyMessage": "لینک به کلیپبورد کپی شد!",
- "limitWarning": "لطفا توجه داشته باشید که جلسه محدودیت ۱۰۰ شرکتکننده دارد و محدودیت زمانی ندارد."
- }
+ "generatedURL": "نام اتاق",
+ "errorGenerating": "ایجاد جلسه ناموفق بود.",
+ "errorCopy": "کپی نام اتاق به کلیپ بورد ناموفق بود.",
+ "copyMessage": "نام اتاق به کلیپ بورد کپی شد!",
+ "limitWarning": "لطفا توجه کنید که جلسه حداکثر ۱۰۰ شرکت کننده خواهد داشت و محدودیت زمانی ندارد.",
+ "placeHolder": "تولید یا چسباندن نام اتاق برای پیوستن",
+ "roomName": "لطفا نام اتاق را برای پیوستن وارد کنید.",
+ "getToken": "نمی توان به جلسه پیوست",
+ "generateToken": "تولید توکن ناموفق بود",
+ "joinMeeting": "پیوستن به جلسه"
+ }
}
\ No newline at end of file
diff --git a/frontend/public/locales/pa-AR/translation.json b/frontend/public/locales/pa-AR/translation.json
index aca081e9b..972427ce6 100644
--- a/frontend/public/locales/pa-AR/translation.json
+++ b/frontend/public/locales/pa-AR/translation.json
@@ -94,7 +94,7 @@
"mentor": "ښوونکی ",
"message": "پیغام ",
"messages": "يیغامونه",
- "group_message":"ګروپ پیغامونه",
+ "group_message":"ډله ییز چیټ",
"name": "نوم",
"no": "نه",
"other": "بل",
@@ -594,13 +594,18 @@
"resend": "بیا لیږنه"
},
"meeting": {
- "title": "د اړوندو لینک جوړول",
- "generateButton": "جوړول",
- "cancelButton": "لغوه",
- "generatedURL": "جوړ شوي URL",
- "errorGenerating": "د اړوندو لینک جوړول ناکامه شو.",
- "errorCopy": "URL په کلپ بورډ کاپی کول ناکامه شو.",
- "copyMessage": "URL په کلپ بورډ کاپی شو!",
- "limitWarning": "مهرباني وکړئ د دوی جوړول له دواړو شمیرې د تر ۱۰۰ کسانو حد او د مهال وقت نلري."
- }
+ "title": "د غونډې جوړول/ګډون کول",
+ "generateButton": "خونه جوړول",
+ "cancelButton": "لغوه کول",
+ "generatedURL": "د خونې نوم",
+ "errorGenerating": "په غونډه کې ناکام شو.",
+ "errorCopy": "د خونې نوم په کلېپ بورډ کې کاپي کولو کې ناکام شو.",
+ "copyMessage": "د خونې نوم په کلېپ بورډ کې کاپي شو!",
+ "limitWarning": "مهرباني وکړئ په ياد ولرئ چې غونډه به ۱۰۰ ګډونوالو ته محدود وي، او وخت به يې محدود نه وي.",
+ "placeHolder": "د ګډون لپاره د خونې نوم جوړول یا پیسټ کول",
+ "roomName": "د ګډون لپاره مهرباني وکړئ د خونې نوم ورکړئ.",
+ "getToken": "په غونډه کې ګډون نشي کولای",
+ "generateToken": "ټوکن جوړول ناممکن دي",
+ "joinMeeting": "په غونډه کې ګډون کول"
+ }
}
\ No newline at end of file
diff --git a/frontend/public/locales/pt-BR/translation.json b/frontend/public/locales/pt-BR/translation.json
index 896a603d5..70a583977 100644
--- a/frontend/public/locales/pt-BR/translation.json
+++ b/frontend/public/locales/pt-BR/translation.json
@@ -93,7 +93,7 @@
"mentor": "Mentor",
"message": "Mensagem",
"messages": "Mensagens",
- "group_message":"Mensagens de grupo",
+ "group_message":"Chat em Grupo",
"name": "Nome",
"no": "Não",
"other": "Outro(s)",
@@ -593,13 +593,18 @@
"resend": "Reenviar"
},
"meeting": {
- "title": "Gerar Link da Reunião",
- "generateButton": "Gerar",
+ "title": "Criar/Entrar na Reunião",
+ "generateButton": "Criar Sala",
"cancelButton": "Cancelar",
- "generatedURL": "URL Gerado",
- "errorGenerating": "Falha ao gerar o link da reunião.",
- "errorCopy": "Falha ao copiar URL para a área de transferência.",
- "copyMessage": "URL copiado para a área de transferência!",
- "limitWarning": "Por favor, note que a reunião terá um limite de 100 participantes e sem limite de tempo."
- }
+ "generatedURL": "Nome da Sala",
+ "errorGenerating": "Falha ao gerar a reunião.",
+ "errorCopy": "Falha ao copiar o nome da sala para a área de transferência.",
+ "copyMessage": "Nome da sala copiado para a área de transferência!",
+ "limitWarning": "Por favor, note que a reunião terá um limite de 100 participantes, sem limite de tempo.",
+ "placeHolder": "Gerar ou colar o nome da sala para entrar",
+ "roomName": "Por favor, forneça um nome de sala para entrar.",
+ "getToken": "Não foi possível entrar na reunião",
+ "generateToken": "Não foi possível gerar o token",
+ "joinMeeting": "Entrar na Reunião"
+ }
}
\ No newline at end of file
diff --git a/frontend/src/app/App.js b/frontend/src/app/App.js
index 14abfd4c9..e8016db9b 100644
--- a/frontend/src/app/App.js
+++ b/frontend/src/app/App.js
@@ -48,6 +48,7 @@ import { useSelector } from "react-redux";
import { ACCOUNT_TYPE } from "utils/consts";
import { fetchAccounts } from "utils/api";
import CreateMeetingLink from "components/CreateMeetingLink";
+import MeetingPanel from 'components/MeetingPanel';
const { Content } = Layout;
@@ -568,6 +569,7 @@ function App() {
+