Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poc invitation #43

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 change: 1 addition & 0 deletions src/backend/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
[
*router.urls,
*oidc_urls,
path("invite/", viewsets.invite, name="invite")
]
),
),
Expand Down
22 changes: 22 additions & 0 deletions src/frontend/src/features/rooms/api/inviteToRoom.ts
Original file line number Diff line number Diff line change
@@ -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")
})
}
171 changes: 171 additions & 0 deletions src/frontend/src/features/rooms/components/Conference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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="[email protected]" 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,
Expand All @@ -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}
Expand Down
41 changes: 8 additions & 33 deletions src/mail/mjml/invitation.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading