diff --git a/backend/app/crud.py b/backend/app/crud.py index cf1c8753dfac16c55edc31752e9fb394befca67d..1e92bcb259a832ac7109153189d16cb091394998 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -144,6 +144,10 @@ def create_session_satisfy( return db_satisfy +def get_message(db: Session, message_id: int): + return db.query(models.Message).filter(models.Message.id == message_id).first() + + def get_messages(db: Session, session_id: int, skip: int = 0, limit: int = 100): return ( db.query(models.Message) @@ -188,6 +192,26 @@ def create_message_metadata( return db_message_metadata +def create_message_spellcheck( + db: Session, + message_id: int, + message: str, + spellcheck: schemas.MessageSpellCheckCreate, +): + message = ( + message[: spellcheck.start] + + "¤µ" + + message[spellcheck.start : spellcheck.end] + + "µ¤" + + message[spellcheck.end :] + ) + + db.query(models.Message).filter(models.Message.id == message_id).update( + {"content": message} + ) + db.commit() + + def create_test_typing(db: Session, test: schemas.TestTypingCreate, user: schemas.User): db_test = models.TestTyping(user_id=user.id) db.add(db_test) diff --git a/backend/app/main.py b/backend/app/main.py index 1260eea590edede2285f53b9779eee854d214782..9c7f18d0b92d74877529dc1b5898746133b54417 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -511,13 +511,14 @@ def read_sessions( return crud.get_sessions(db, current_user, skip=skip, limit=limit) -@sessionsRouter.post('/{session_id}/satisfy', status_code=status.HTTP_204_NO_CONTENT) + +@sessionsRouter.post("/{session_id}/satisfy", status_code=status.HTTP_204_NO_CONTENT) def create_session_satisfy( - session_id: int, - satisfy: schemas.SessionSatisfyCreate, - db: Session = Depends(get_db), - current_user: schemas.User = Depends(get_jwt_user), - ): + session_id: int, + satisfy: schemas.SessionSatisfyCreate, + db: Session = Depends(get_db), + current_user: schemas.User = Depends(get_jwt_user), +): db_session = crud.get_session(db, session_id) if db_session is None: raise HTTPException(status_code=404, detail="Session not found") @@ -611,6 +612,37 @@ def create_message( return {"id": message.id, "message_id": message.message_id} +@sessionsRouter.post( + "/{session_id}/messages/{message_id}/spellcheck", + status_code=status.HTTP_204_NO_CONTENT, +) +def spellcheck_message( + session_id: int, + message_id: int, + spellcheck: schemas.MessageSpellCheckCreate, + db: Session = Depends(get_db), + current_user: schemas.User = Depends(get_jwt_user), +): + db_session = crud.get_session(db, session_id) + if db_session is None: + raise HTTPException(status_code=404, detail="Session not found") + + if ( + not check_user_level(current_user, models.UserType.ADMIN) + and current_user not in db_session.users + ): + raise HTTPException( + status_code=401, + detail="You do not have permission to spellcheck a message in this session", + ) + + message = crud.get_message(db, message_id) + if message is None: + raise HTTPException(status_code=404, detail="Message not found") + + crud.create_message_spellcheck(db, message_id, message.content, spellcheck) + + async def send_websoket_typing(session_id: int, user_id: int): content = json.dumps( {"type": "message", "action": "typing", "data": {"user": user_id}} diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 7697f91d76c3b7dc3766812e38d43375145a12bd..f5725ed215a6fe7b6719b738903e980d5cd18b6e 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -128,6 +128,11 @@ class MessageCreate(BaseModel): from_attributes = True +class MessageSpellCheckCreate(BaseModel): + start: int + end: int + + class TestTypingEntryCreate(BaseModel): exerciceId: int position: int diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4dee433a2ce14baf9913c32d5343297d2519b18a..240f04474e8e326b7e881cf5ba246dd33fbb296d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "emoji-picker-element": "^1.21.3", + "sanitize-html": "^2.13.0", "svelte-gravatar": "^1.0.3", "svelte-i18n": "^4.0.0", "svelte-material-icons": "^3.0.5", @@ -1744,12 +1745,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2130,7 +2132,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2203,6 +2204,61 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2226,6 +2282,18 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -2348,7 +2416,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2701,10 +2768,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2974,6 +3042,25 @@ "node": ">= 0.4" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3157,6 +3244,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -3170,6 +3258,15 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -3602,7 +3699,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -3764,6 +3860,12 @@ "node": ">= 0.10" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3844,8 +3946,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3882,7 +3983,6 @@ "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4308,6 +4408,20 @@ "rimraf": "bin.js" } }, + "node_modules/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, "node_modules/sax": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", @@ -5421,6 +5535,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/frontend/package.json b/frontend/package.json index 352ca2baa2fc12a76287e6564349d05d9ae5f9d9..5548b0f003d7d324068776f5e0ee41fcd3ecce70 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,7 @@ "type": "module", "dependencies": { "emoji-picker-element": "^1.21.3", + "sanitize-html": "^2.13.0", "svelte-gravatar": "^1.0.3", "svelte-i18n": "^4.0.0", "svelte-material-icons": "^3.0.5", diff --git a/frontend/src/lib/api/sessions.ts b/frontend/src/lib/api/sessions.ts index f0d5898b8a58e14f73fb27f7c20a428fb4df278a..c62ad1ebf68da53698de8b5378243a148a80bf88 100644 --- a/frontend/src/lib/api/sessions.ts +++ b/frontend/src/lib/api/sessions.ts @@ -75,6 +75,23 @@ export async function updateMessageAPI( return response.data; } +export async function addMessageSpellCheckAPI( + id: number, + message_id: number, + start: number, + end: number +): Promise<boolean> { + const response = await axiosInstance.post(`/sessions/${id}/messages/${message_id}/spellcheck`, { + start, + end + }); + if (response.status !== 204) { + toastAlert('Failed to add spellcheck'); + return false; + } + return true; +} + export async function patchLanguageAPI(id: number, language: string) { const response = await axiosInstance.patch(`/sessions/${id}`, { language }); diff --git a/frontend/src/lib/components/icons/spellCheck.svelte b/frontend/src/lib/components/icons/spellCheck.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c215e280141ffe9c934f8a22924606386987e2de --- /dev/null +++ b/frontend/src/lib/components/icons/spellCheck.svelte @@ -0,0 +1,10 @@ +<svg + fill="currentColor" + width="20" + height="20" + viewBox="0 -32 576 576" + xmlns="http://www.w3.org/2000/svg" + ><path + d="M272 256h91.36c43.2 0 82-32.2 84.51-75.34a79.82 79.82 0 0 0-25.26-63.07 79.81 79.81 0 0 0 9.06-44.91C427.9 30.57 389.3 0 347 0h-75a16 16 0 0 0-16 16v224a16 16 0 0 0 16 16zm40-200h40a24 24 0 0 1 0 48h-40zm0 96h56a24 24 0 0 1 0 48h-56zM155.12 22.25A32 32 0 0 0 124.64 0H99.36a32 32 0 0 0-30.48 22.25L.59 235.73A16 16 0 0 0 16 256h24.93a16 16 0 0 0 15.42-11.73L68.29 208h87.42l11.94 36.27A16 16 0 0 0 183.07 256H208a16 16 0 0 0 15.42-20.27zM89.37 144L112 75.3l22.63 68.7zm482 132.48l-45.21-45.3a15.88 15.88 0 0 0-22.59 0l-151.5 151.5-55.41-55.5a15.88 15.88 0 0 0-22.59 0l-45.3 45.3a16 16 0 0 0 0 22.59l112 112.21a15.89 15.89 0 0 0 22.6 0l208-208.21a16 16 0 0 0-.02-22.59z" + /></svg +> diff --git a/frontend/src/lib/components/sessions/message.svelte b/frontend/src/lib/components/sessions/message.svelte index e504be6081b2c9f2c0e18d6850010dee4c942cf8..f056d1450df621b0d81482b44a0f0283313fcd76 100644 --- a/frontend/src/lib/components/sessions/message.svelte +++ b/frontend/src/lib/components/sessions/message.svelte @@ -1,10 +1,20 @@ <script lang="ts"> import type Message from '$lib/types/message'; import { displayTime } from '$lib/utils/date'; - import { Check, Icon, Pencil } from 'svelte-hero-icons'; + import { + ChatBubbleBottomCenter, + ChatBubbleBottomCenterText, + Check, + Icon, + Pencil, + PencilSquare + } from 'svelte-hero-icons'; import { user } from '$lib/types/user'; import Gravatar from 'svelte-gravatar'; import { t } from '$lib/services/i18n'; + import { onMount } from 'svelte'; + import SpellCheck from '$lib/components/icons/spellCheck.svelte'; + import { sanitize } from '$lib/utils/sanitize'; export let message: Message; @@ -48,6 +58,85 @@ } } + let hightlight: HTMLDivElement; + + onMount(() => { + document.addEventListener('selectionchange', onTextSelect); + }); + + function getSelectionCharacterOffsetWithin() { + var start = 0; + var end = 0; + var doc = contentDiv.ownerDocument; + var win = doc.defaultView; + if (!doc || !win) return { start: 0, end: 0 }; + var sel; + if (typeof win.getSelection === 'undefined') { + return { start: 0, end: 0 }; + } + sel = win.getSelection(); + if (!sel) return { start: 0, end: 0 }; + if (sel.rangeCount <= 0) return { start: 0, end: 0 }; + + var range = sel.getRangeAt(0); + var preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(contentDiv); + preCaretRange.setEnd(range.startContainer, range.startOffset); + start = preCaretRange.toString().length; + preCaretRange.setEnd(range.endContainer, range.endOffset); + end = preCaretRange.toString().length; + + return { start: start, end: end }; + } + + function onTextSelect() { + const selection = window.getSelection(); + if (!selection) return; + const range = selection.getRangeAt(0); + const start = range.startOffset; + const end = range.endOffset; + if (range.commonAncestorContainer.parentElement === contentDiv && end - start > 0) { + const rects = range.getClientRects(); + if (!rects.length) { + hightlight.style.visibility = 'hidden'; + return; + } + const rect = rects[rects.length - 1]; + if (!rect) { + hightlight.style.visibility = 'hidden'; + return; + } + hightlight.style.top = rect.bottom + 'px'; + hightlight.style.left = rect.right + 'px'; + hightlight.style.visibility = 'visible'; + } else { + hightlight.style.visibility = 'hidden'; + } + } + + async function onSpellSelect() { + const selection = window.getSelection(); + if (!selection) { + hightlight.style.visibility = 'hidden'; + return; + } + const range = getSelectionCharacterOffsetWithin(); + + const start = range.start; + const end = range.end; + console.log(start, end); + + const res = await message.addSpellCheck(start, end); + + if (res) { + selection.removeAllRanges(); + hightlight.style.visibility = 'hidden'; + contentDiv.innerHTML = sanitize(message.content) + .replaceAll('¤µ', '<span class="decoration-wavy decoration-orange-500 underline">') + .replaceAll('µ¤', '</span>'); + } + } + const isSender = message.user.id == $user?.id; </script> @@ -68,7 +157,9 @@ class:text-white={isSender} > <div contenteditable={isEdit} bind:this={contentDiv} class:bg-blue-900={isEdit}> - {message.content} + {@html sanitize(message.content) + .replaceAll('¤µ', '<span class="decoration-wavy decoration-orange-500 underline">') + .replaceAll('µ¤', '</span>')} </div> {#if isEdit} <button @@ -106,7 +197,14 @@ <div> {#each $messageVersions as version} <div class="flex justify-between items-center border-b border-gray-300 py-1"> - <div>{version.content}</div> + <div> + {@html sanitize(version.content) + .replaceAll( + '¤µ', + '<span class="decoration-wavy decoration-orange-500 underline">' + ) + .replaceAll('µ¤', '</span>')} + </div> <div class="whitespace-nowrap">{displayTime(version.date)}</div> </div> {/each} @@ -121,3 +219,17 @@ {/if} </div> </div> + +<div class="absolute invisible rounded-full border-black border bg-white" bind:this={hightlight}> + <button + on:click={onSpellSelect} + class="bg-opacity-0 bg-blue-500 hover:bg-opacity-50 p-2 pl-4 rounded-l-full" + > + <SpellCheck /> + </button><!--- + --><button + class="bg-opacity-0 bg-blue-500 hover:bg-opacity-50 p-2 pr-4 rounded-r-full hover:cursor-not-allowed" + > + <Icon src={ChatBubbleBottomCenterText} size="20" /> + </button> +</div> diff --git a/frontend/src/lib/types/message.ts b/frontend/src/lib/types/message.ts index fefb5d925631246bb92e75aca4970e08005643e7..87acfd307db6cfe4807a45d9428ca93911d85f52 100644 --- a/frontend/src/lib/types/message.ts +++ b/frontend/src/lib/types/message.ts @@ -1,6 +1,6 @@ import Session from './session'; import User from './user'; -import { updateMessageAPI } from '$lib/api/sessions'; +import { updateMessageAPI, addMessageSpellCheckAPI } from '$lib/api/sessions'; import { toastAlert } from '$lib/utils/toasts'; import { writable, type Writable } from 'svelte/store'; @@ -81,6 +81,28 @@ export default class Message { return true; } + async addSpellCheck(start: number, end: number): Promise<boolean> { + for (let i = 0; i < start + 1; i++) { + if (this._content[i] == '¤' || this._content[i] == 'µ') { + start++; + end++; + } + } + + const response = await addMessageSpellCheckAPI(this._session.id, this._id, start, end); + + if (!response) return false; + + this._content = + this._content.slice(0, start) + + '¤µ' + + this._content.slice(start, end) + + 'µ¤' + + this._content.slice(end); + + return true; + } + static parse( // eslint-disable-next-line @typescript-eslint/no-explicit-any json: any, @@ -148,6 +170,7 @@ export default class Message { if (prev.created_at < m.created_at) { prev._versions.update((v) => [...v, { content: m.content, date: m.created_at }]); prev._content = m.content; + prev._id = m.id; prev._edited = true; } } diff --git a/frontend/src/lib/utils/sanitize.ts b/frontend/src/lib/utils/sanitize.ts new file mode 100644 index 0000000000000000000000000000000000000000..38111abc688bb323dff5d0898963584a30f5b6ea --- /dev/null +++ b/frontend/src/lib/utils/sanitize.ts @@ -0,0 +1,8 @@ +import sanitizeHtml from 'sanitize-html'; + +export function sanitize(html: string): string { + return sanitizeHtml(html, { + allowedTags: [], + allowedAttributes: {} + }); +}