Compare commits

...

5 Commits

Author SHA1 Message Date
lebaudantoine d9b9f32e44 💩(frontend) call '/invite' endpoint on modal submission
Quick and dirty front and back connection.
2024-07-18 01:18:58 +02:00
lebaudantoine b25a0065c2 💩(mail) prototype an invitation template
heavily inspired by Google one.
It's ugly, but contains all required information.
I'll iterate.
2024-07-18 01:18:09 +02:00
lebaudantoine bc9143096b 💩(backend) support sending email invitations
Totally WIP. would be better having an @action define on room viewset.
2024-07-18 01:17:14 +02:00
lebaudantoine 45908042c6 💩(frontend) prototype an email invitation modal
Basic input to type an email and submit it.
Lacks everything to make it acceptable (e.g. form validation, etc...)
2024-07-17 23:52:36 +02:00
lebaudantoine ed3b9ad50d 💩(frontend) prototype an invitation modal
Display an invitation pop-in to all user joining a meeting.
Quick and very dirty code to prototype the feature discussed.
2024-07-17 23:22:31 +02:00
5 changed files with 249 additions and 34 deletions
+47 -1
View File
@@ -1,11 +1,17 @@
"""API endpoints"""
import json
import smtplib
import uuid
from django.conf import settings
from django.contrib.sites.models import Site
from django.core import mail
from django.db.models import Q
from django.http import Http404
from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from rest_framework import (
decorators,
@@ -276,3 +282,43 @@ class ResourceAccessViewSet(
permission_classes = [permissions.ResourceAccessPermission]
queryset = models.ResourceAccess.objects.all()
serializer_class = serializers.ResourceAccessSerializer
def invite(request):
"""PoC of sending email invitation."""
if request.method != "POST":
return JsonResponse({"error": "Method not allowed"}, status=405)
if not request.user.is_authenticated:
return JsonResponse({"error": "Authentication required"}, status=401)
data = json.loads(request.body)
emails = data.get("emails")
room = data.get("room")
if not emails or not room:
return JsonResponse(
{"error": "Emails and room are required fields."}, status=400
)
try:
template_vars = {
"title": _("Invitation to join a room!"),
"site": Site.objects.get_current(),
"room": room,
"sender": str(request.user),
}
msg_html = render_to_string("mail/html/invitation.html", template_vars)
msg_plain = render_to_string("mail/text/invitation.txt", template_vars)
mail.send_mail(
_("Invitation to join a room!"),
msg_plain,
settings.EMAIL_FROM,
emails,
html_message=msg_html,
fail_silently=False,
)
except smtplib.SMTPException:
return JsonResponse({"error": "Failed to send invitation emails"}, status=500)
return JsonResponse({"msg": "invitation sent."}, status=200)
+1
View File
@@ -22,6 +22,7 @@ urlpatterns = [
[
*router.urls,
*oidc_urls,
path("invite/", viewsets.invite, name="invite")
]
),
),
@@ -0,0 +1,22 @@
import {fetchApi} from '@/api/fetchApi'
export const inviteToRoom = (roomId, emails) => {
return fetchApi(
`/invite/`,
{
method: 'POST',
body: JSON.stringify(
{
emails: emails,
room: roomId
}
)
}
).then((ee) => {
console.log(ee)
alert("invitation envoyée")
}).catch((ee) => {
console.log(ee)
alert("invitation failed")
})
}
@@ -9,6 +9,162 @@ import { keys } from '@/api/queryKeys'
import { QueryAware } from '@/layout/QueryAware'
import { navigateToHome } from '@/navigation/navigateToHome'
import { fetchRoom } from '../api/fetchRoom'
import {Box, Button, Div, H, P, Text} from "@/primitives";
import { cva } from '@/styled-system/css'
import {styled} from "@/styled-system/jsx";
import {useRef, useState} from "react";
import {inviteToRoom} from "@/features/rooms/api/inviteToRoom";
const popin = cva({
base: {
display: "flex",
flexDirection: "column",
alignItems: "start",
justifyContent: "space-between",
borderWidth: '1px',
borderStyle: 'solid',
borderColor: 'box.border',
backgroundColor: 'box.bg',
color: 'box.text',
boxShadow: 'box',
borderRadius: 8,
padding: 'boxPadding',
flex: 1,
position: "absolute",
zIndex: 100,
}
})
const Popin = styled('div', popin)
const fakeInput = cva({
base: {
height: "40px",
display: "flex",
borderRadius: "4px",
background: "rgb(241,243,244)",
alignItems: "center",
marginTop: ".5rem",
paddingLeft: ".75rem",
width: "100%",
justifyContent: "space-between"
}
})
const FakeInput = styled('div', fakeInput)
const getLocation = () => {
return window.location.hostname + window.location.pathname;
}
const CopyIcon = ({size = 16}) => (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} fill="currentColor" className="bi bi-copy" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z"/>
</svg>
)
const CloseIcon = ({size = 16}) => (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} fill="currentColor" className="bi bi-x" viewBox="0 0 16 16">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708"/>
</svg>
)
const Invitation = ({onClose, onOpenEmailModal}) => (
<Popin style={{ bottom: "100px", left: "24px", maxWidth: "350px" }}>
<Div style={{display: "flex", justifyContent: "space-between", width: '100%'}}>
<H lvl={2} style={{ marginBottom: "0.75rem"}}>Votre réunion est prête</H>
<Div style={{cursor: "pointer"}} onClick={onClose}>
<CloseIcon size={25} />
</Div>
</Div>
<Button variant="primary" onClick={onOpenEmailModal}>Ajouter des participants</Button>
<P style={{fontSize: "14px", marginBottom: "0", marginTop: "0.5rem"}}>
Ou partagez ce lien avec les personnes que vous souhaitez inviter à la réunion
</P>
<FakeInput>
<Text as="span" style={{fontSize: '0.75rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '70%'}}>{getLocation()}</Text>
<Div style={{paddingRight: ".75rem", cursor: "pointer"}} onClick={() => navigator.clipboard.writeText(getLocation())}><CopyIcon size={20} /></Div>
</FakeInput>
<P style={{fontSize: "12px", marginBottom: "0", marginTop: "0.5rem"}}>
Les personnes utilisant le lien de cette réunion pourront rejoindre la réunion sans demander votre autorisation.
</P>
</Popin>
)
const modalContainer = cva({
base: {
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
backgroundColor: 'rgba(0, 0, 0, 0.5)',
height: "100%",
width: "100%",
position: "absolute",
top: 0,
bottom: 0,
zIndex: 200,
}
})
const ModalContainer = styled('div', modalContainer)
const EmailModal = ({onClose, onSubmit, closeOnSubmit = false}) => {
const emailRef = useRef(null);
return (
<ModalContainer>
<InnerModal>
<Div style={{display: "flex", justifyContent: "space-between", width: '100%'}}>
<H lvl={2} style={{ marginBottom: "0.75rem"}}>Inviter des participants</H>
<Div style={{cursor: "pointer"}} onClick={onClose}>
<CloseIcon size={25} />
</Div>
</Div>
<P>
Inviter un participant par email.
</P>
<Div style={{width: "100%"}}>
<input ref={emailRef} type="email" placeholder="sam@meet.co" name="email" style={{border: '1px solid black', borderRadius: '4px', minWidth: '250px', padding: '0 .5rem'}}/>
<Button onClick={() => {
const emailValue = emailRef.current.value;
if(!emailValue || !/\S+@\S+\.\S+/.test(emailValue)) {
return
}
onSubmit(emailValue)
if (closeOnSubmit) {
onClose()
}
}}>inviter</Button>
</Div>
</InnerModal>
</ModalContainer>
)
}
const innerModal = cva({
base: {
display: "flex",
flexDirection: "column",
alignItems: "start",
justifyContent: "space-between",
borderWidth: '1px',
borderStyle: 'solid',
borderColor: 'box.border',
backgroundColor: 'box.bg',
color: 'box.text',
boxShadow: 'box',
borderRadius: 8,
padding: 'boxPadding',
flex: 1,
maxWidth: "400px"
}
})
const InnerModal = styled('div', innerModal)
export const Conference = ({
userConfig,
@@ -25,8 +181,23 @@ export const Conference = ({
}),
})
const [isInvitationOpened, setIsInvitationOpened] = useState(true)
const [isEmailModalOpened, setIsEmailModalOpened] = useState(false)
return (
<QueryAware status={status}>
{
isInvitationOpened && (
<Invitation onClose={() => setIsInvitationOpened(false)} onOpenEmailModal={() => setIsEmailModalOpened(true)} />
)
}
{
isEmailModalOpened && (
<EmailModal onClose={() => setIsEmailModalOpened(false)} onSubmit={(email) => {
inviteToRoom(roomId, [email])
}} closeOnSubmit />
)
}
<LiveKitRoom
serverUrl={data?.livekit?.url}
token={data?.livekit?.token}
+8 -33
View File
@@ -11,39 +11,14 @@
<mj-section mj-class="bg--white-100" padding="30px 20px 60px 20px">
<mj-column>
<mj-text font-size="14px">
<p>{% trans "Invitation to join a team" %}</p>
</mj-text>
<!-- Welcome Message -->
<mj-text>
<h1>{% blocktrans %}Welcome to <strong>Meet</strong>{% endblocktrans %}</h1>
</mj-text>
<mj-divider border-width="1px" border-style="solid" border-color="#DDDDDD" width="30%" align="left"/>
<mj-image src="{% base64_static 'images/logo.svg' %}" width="157px" align="left" alt="{%trans 'Logo' %}" />
<!-- Main Message -->
<mj-text>{% trans "We are delighted to welcome you to our community on Meet, your new companion to collaborate on documents efficiently, intuitively, and securely." %}</mj-text>
<mj-text>{% trans "Our application is designed to help you organize, collaborate, and manage permissions." %}</mj-text>
<mj-text>
{% trans "With Meet, you will be able to:" %}
<ul>
<li>{% trans "Create documents."%}</li>
<li>{% trans "Invite members of your document or community in just a few clicks."%}</li>
</ul>
</mj-text>
<mj-button href="//{{site.domain}}" background-color="#000091" color="white" padding-bottom="30px">
{% trans "Visit Meet"%}
</mj-button>
<mj-text>{% trans "We are confident that Meet will help you increase efficiency and productivity while strengthening the bond among members." %}</mj-text>
<mj-text>{% trans "Feel free to explore all the features of the application and share your feedback and suggestions with us. Your feedback is valuable to us and will enable us to continually improve our service." %}</mj-text>
<mj-text>{% trans "Once again, welcome aboard! We are eager to accompany you on you collaboration adventure." %}</mj-text>
<!-- Signature -->
<mj-text>
<p>{% trans "Sincerely," %}</p>
<p>{% trans "The La Suite Numérique Team" %}</p>
</mj-text>
<p>{{sender}} {% trans "invite you to join a meeting" %}</p>
</mj-text>
<mj-button href="//{{site.domain}}/{{room}}" background-color="#000091" color="white" padding-bottom="30px">
{% trans "Join meeting"%}
</mj-button>
<mj-text font-size="14px">
<p>{{site.domain}}/{{room}}</p>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>