Implement personal code rooms (#11)
continuous-integration/drone/push Build is passing Details

Co-authored-by: bastien ollier <bastien.ollier@etu.uca.fr>
Reviewed-on: #11
Reviewed-by: Clément FRÉVILLE <clement.freville2@etu.uca.fr>
Co-authored-by: Bastien OLLIER <bastien.ollier@noreply.codefirst.iut.uca.fr>
Co-committed-by: Bastien OLLIER <bastien.ollier@noreply.codefirst.iut.uca.fr>
chore/angular18
Bastien OLLIER 11 months ago committed by Clément FRÉVILLE
parent 90f83ec698
commit 7dfd83b60a

1
.gitignore vendored

@ -19,6 +19,7 @@ yarn-error.log
*.launch
.settings/
*.sublime-workspace
.nx
# Visual Studio Code
.vscode/*

10
package-lock.json generated

@ -17,6 +17,7 @@
"@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7",
"@codemirror/collab": "^6.1.1",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/state": "^6.4.1",
@ -2447,6 +2448,15 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/collab": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz",
"integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.5.0.tgz",

@ -21,6 +21,7 @@
"@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7",
"@codemirror/collab": "^6.1.1",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/state": "^6.4.1",

@ -1,5 +1,4 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Routes } from '@angular/router';
import { EditorComponent } from './components/editor/editor.component';
import { LandingPageComponent } from './components/landing-page/landing-page.component';
import { DocumentationComponent } from './components/documentation/documentation.component';
@ -9,18 +8,13 @@ import { OurStoryComponent } from './components/our-story/our-story.component';
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
// Toutes les routes de l'application sont définies ici
const routes: Routes = [
export const routes: Routes = [
{ path: '', component: LandingPageComponent },
{ path: 'editor', component: EditorComponent },
{ path: 'editor-live/:idRoom', component: EditorComponent },
{ path: 'documentation', component: DocumentationComponent },
{ path: 'contact', component: FormComponent },
{ path: 'our-story', component: OurStoryComponent },
{ path: 'terms-of-service', component: TermsOfServiceComponent },
{ path: 'privacy-policy', component: PrivacyPolicyComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

@ -59,6 +59,13 @@
</div>
}
<button
class="button-join"
(click)="onCreateRoomButtonClicked()"
>
Créer une salle
</button>
<select id="language" [(ngModel)]="selectedLanguage">
@for (language of languages; track language.name) {
<option [ngValue]="language">{{ language.name }}</option>
@ -68,7 +75,6 @@
<div class="param-editor">
<button
class="button-icon button-run"
type="button"
(click)="onRunButtonClicked()"
[disabled]="isLoaded">
<div>RUN</div>

@ -40,6 +40,16 @@ svg {
border-radius: 10px;
}
.button-join {
background-color: #1c53bf;
border: none;
color: white;
padding: 12px 16px;
font-size: 16px;
cursor: pointer;
border-radius: 10px;
}
select {
background-color: #0000f0;
border: none;

@ -1,11 +1,12 @@
import { Component, ViewChild } from '@angular/core';
import { CodeExecutionService } from 'src/app/services/codeExecution.service';
import { Compartment } from '@codemirror/state';
import { Component, Input, ViewChild } from '@angular/core';
import { BackendService } from 'src/app/services/backendService.service';
import { Compartment, StateEffect } from '@codemirror/state';
import { CodeMirrorComponent } from '@sandkasten/codemirror6-editor';
import { LanguageDescription } from '@codemirror/language';
import { CODE_DEFAULTS, LANGUAGES } from '../languages';
import { SafeHTMLPipe } from '../../safe-html.pipe';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import {
keymap,
highlightSpecialChars,
@ -36,6 +37,11 @@ import {
closeBracketsKeymap,
} from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';
import {
Connection,
getDocument,
peerExtension,
} from '../../services/connection.service';
const basicSetup: Extension = (() => [
highlightActiveLineGutter(),
@ -131,18 +137,47 @@ export class EditorComponent {
this.languageCompartment.of(this.selectedLanguage.support!),
];
constructor(private codeExecutionService: CodeExecutionService) {}
private client: WebSocket | undefined;
@Input()
set idRoom(idRoom: string) {
this.client = new WebSocket(`ws://127.0.0.1:3000/live/${idRoom}`);
this.client.addEventListener('open', async () => {
let conn = new Connection(this.client!);
let { version, doc } = await getDocument(conn);
this.codemirror.editor?.dispatch({
changes: {
from: 0,
to: this.codemirror.editor.state.doc.length,
insert: doc,
},
});
this.codemirror.editor?.dispatch({
effects: StateEffect.appendConfig.of([peerExtension(version, conn)]),
});
});
}
constructor(
private router: Router,
private backendService: BackendService
) {}
// Efface le contenu de l'éditeur
clear(): void {
this.editorContent = '';
}
async onCreateRoomButtonClicked() {
const idRoom = await this.backendService.createRoom();
await this.router.navigate([`./editor-live/${idRoom}`]);
}
onRunButtonClicked() {
// Le code à exécuter est le contenu de l'éditeur
const codeToExecute = this.editorContent;
this.codeExecutionService.executeCode(
this.backendService.executeCode(
codeToExecute,
this.selectedLanguage.name
);

@ -10,14 +10,19 @@ export type ExecutionMessage = {
@Injectable({
providedIn: 'root',
})
export class CodeExecutionService {
private apiUrl = 'http://localhost:3000/run';
export class BackendService {
private apiUrl = 'http://localhost:3000';
private resultSubject = new Subject<ExecutionMessage>();
constructor() {}
async createRoom() {
const reponse = await fetch(`${this.apiUrl}/live`, { method: 'POST' });
return reponse.text();
}
executeCode(code: string, language: string) {
const sse = new SSE(this.apiUrl, {
const sse = new SSE(`${this.apiUrl}/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

@ -0,0 +1,120 @@
import { ChangeSet, Text } from '@codemirror/state';
import { EditorView } from 'codemirror';
import {
Update,
collab,
getSyncedVersion,
receiveUpdates,
sendableUpdates,
} from '@codemirror/collab';
import { ViewPlugin, ViewUpdate } from '@codemirror/view';
export class Connection {
private requestId = 0;
private resolves: Record<number, (value: any) => void> = {};
constructor(private client: WebSocket) {
client.addEventListener('message', (event) => {
const response = JSON.parse(event.data);
if ('_request' in response) {
const resolve = this.resolves[response._request];
if (resolve) {
resolve(response.payload);
} else {
console.error(
'Received response for unknown or already used request',
response._request
);
}
} else {
console.error('Received invalid response', response._request);
}
});
}
request(body: Record<string, unknown>): Promise<any> {
body['_request'] = this.requestId;
this.client.send(JSON.stringify(body));
return new Promise(
(resolve) => (this.resolves[this.requestId++] = resolve)
);
}
}
function pushUpdates(
connection: Connection,
version: number,
fullUpdates: readonly Update[]
): Promise<boolean> {
// Strip off transaction data
let updates = fullUpdates.map((u) => ({
clientID: u.clientID,
changes: u.changes.toJSON(),
}));
return connection.request({ type: 'pushUpdates', version, updates });
}
function pullUpdates(
connection: Connection,
version: number
): Promise<readonly Update[]> {
return connection.request({ type: 'pullUpdates', version }).then((updates) =>
updates.map((u: any) => ({
changes: ChangeSet.fromJSON(u.changes),
clientID: u.clientID,
}))
);
}
export function getDocument(
connection: Connection
): Promise<{ version: number; doc: Text }> {
return connection.request({ type: 'getDocument' }).then((data) => ({
version: data.version,
doc: Text.of(data.doc.split('\n')),
}));
}
export function peerExtension(startVersion: number, connection: Connection) {
let plugin = ViewPlugin.fromClass(
class {
private pushing = false;
private done = false;
constructor(private view: EditorView) {
this.pull();
}
update(update: ViewUpdate) {
if (update.docChanged) this.push();
}
async push() {
let updates = sendableUpdates(this.view.state);
if (this.pushing || !updates.length) return;
this.pushing = true;
let version = getSyncedVersion(this.view.state);
await pushUpdates(connection, version, updates);
this.pushing = false;
// Regardless of whether the push failed or new updates came in
// while it was running, try again if there's updates remaining
if (sendableUpdates(this.view.state).length)
setTimeout(() => this.push(), 100);
}
async pull() {
while (!this.done) {
let version = getSyncedVersion(this.view.state);
let updates = await pullUpdates(connection, version);
this.view.dispatch(receiveUpdates(this.view.state, updates));
}
}
destroy() {
this.done = true;
}
}
);
return [collab({ startVersion }), plugin];
}

@ -8,15 +8,16 @@ import {
HttpClient,
} from '@angular/common/http';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app/app-routing.module';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { TranslationService } from './app/services/translation.service';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding()),
importProvidersFrom(
BrowserModule,
AppRoutingModule,
ReactiveFormsModule,
FormsModule,
TranslateModule.forRoot({

Loading…
Cancel
Save