From a2c89301d5c63f573ac6680f9c11acda69043a78 Mon Sep 17 00:00:00 2001 From: Tyler Murphy <=> Date: Wed, 26 Jul 2023 01:04:39 -0400 Subject: start dms --- public/js/chat.js | 37 +++++++++++++++++++++++++++++++++++++ public/js/components.js | 18 ++++++++++++++---- public/js/home.js | 21 ++++++--------------- public/js/main.js | 8 ++++++-- public/js/people.js | 15 ++------------- public/js/profile.js | 6 +++--- 6 files changed, 68 insertions(+), 37 deletions(-) create mode 100644 public/js/chat.js (limited to 'public/js') diff --git a/public/js/chat.js b/public/js/chat.js new file mode 100644 index 0000000..41ba2ba --- /dev/null +++ b/public/js/chat.js @@ -0,0 +1,37 @@ +import { div, pfp, p, parse, button, body, a, textarea, span, crawl } from './main.js' +import { loadself, loadpostspage, createpost, loadusers } from './api.js' +import { parsePost, header } from './components.js' + +function render() { + + let new_body = + body({}, + ...header(false, false, true, data.self.user_id), + ) + + document.body.replaceWith(new_body) + +} + +const data = { + self: {}, + users: [] +} + +async function init() { + + let request = (await loadself()); + data.self = request.json + + if (request.json == undefined) { + location.href = '/login' + return + } + + data.users[data.self.user_id] = data.self + + render() +} + + +init() diff --git a/public/js/components.js b/public/js/components.js index 7e2a268..f247875 100644 --- a/public/js/components.js +++ b/public/js/components.js @@ -1,9 +1,9 @@ -import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth } from './main.js' +import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth, g } from './main.js' import { postlike, postcomment, loadcommentspage } from './api.js'; window.parse = parse; -export function header(home, people, user_id) { +export function header(home, people, chat, user_id) { return [ div({id: 'header'}, span({class: 'logo'}, @@ -21,6 +21,16 @@ export function header(home, people, user_id) { svg({viewBox: '0 0 28 28', fill: 'currentColor', height: '28', width: '28'}, path({d: "M10.5 4.5c-2.272 0-2.75 1.768-2.75 3.25C7.75 9.542 8.983 11 10.5 11s2.75-1.458 2.75-3.25c0-1.482-.478-3.25-2.75-3.25zm0 8c-2.344 0-4.25-2.131-4.25-4.75C6.25 4.776 7.839 3 10.5 3s4.25 1.776 4.25 4.75c0 2.619-1.906 4.75-4.25 4.75zm9.5-6c-1.41 0-2.125.841-2.125 2.5 0 1.378.953 2.5 2.125 2.5 1.172 0 2.125-1.122 2.125-2.5 0-1.659-.715-2.5-2.125-2.5zm0 6.5c-1.999 0-3.625-1.794-3.625-4 0-2.467 1.389-4 3.625-4 2.236 0 3.625 1.533 3.625 4 0 2.206-1.626 4-3.625 4zm4.622 8a.887.887 0 00.878-.894c0-2.54-2.043-4.606-4.555-4.606h-1.86c-.643 0-1.265.148-1.844.413a6.226 6.226 0 011.76 4.336V21h5.621zm-7.122.562v-1.313a4.755 4.755 0 00-4.749-4.749H8.25A4.755 4.755 0 003.5 20.249v1.313c0 .518.421.938.937.938h12.125c.517 0 .938-.42.938-.938zM20.945 14C24.285 14 27 16.739 27 20.106a2.388 2.388 0 01-2.378 2.394h-5.81a2.44 2.44 0 01-2.25 1.5H4.437A2.44 2.44 0 012 21.562v-1.313A6.256 6.256 0 018.25 14h4.501a6.2 6.2 0 013.218.902A5.932 5.932 0 0119.084 14h1.861z"}) ) + ), + a({id: 'chat', class: chat ? 'selected' : '', href: '/chat', 'aria-label': 'xssbook chat page'}, + svg({viewBox: '0 0 512 512', fill: 'currentColor', height: '28', width: '28'}, + g({transform: "translate(0.000000,512.000000) scale(0.100000,-0.100000)", fill: "#fffffff", stroke: "none"}, + path({d: "M1731 4799 c-240 -27 -467 -93 -687 -199 -992 -481 -1340 -1619 -768 -2512 l43 -66 -150 -469 c-82 -257 -149 -481 -149 -496 0 -73 75 -147 150 -147 31 0 215 89 725 350 l230 118 90 -35 c109 -42 279 -87 395 -104 83 -12 86 -14 147 -70 172 -159 313 -256 514 -354 507 -245 1103 -270 1644 -68 l81 30 449 -229 c291 -148 464 -232 491 -235 80 -10 164 63 164 143 0 15 -67 238 -149 496 l-150 469 43 67 c330 511 364 1151 90 1689 -268 524 -818 913 -1421 1003 -43 7 -83 15 -89 18 -7 4 -54 45 -106 92 -143 128 -266 212 -443 299 -215 107 -352 152 -580 191 -139 25 -430 34 -564 19z m407 -300 c123 -13 261 -43 377 -80 100 -33 300 -127 385 -182 l54 -35 -39 -7 c-273 -43 -442 -94 -645 -191 -911 -439 -1295 -1442 -887 -2317 25 -53 41 -97 36 -97 -6 0 -67 22 -136 50 -78 31 -141 50 -166 50 -32 0 -104 -33 -363 -165 -178 -91 -325 -165 -327 -165 -3 0 42 145 100 323 57 177 104 340 105 362 1 47 -6 63 -84 178 -107 157 -180 326 -220 510 -29 135 -31 396 -5 530 119 596 612 1070 1253 1206 186 40 380 50 562 30z m1220 -600 c223 -24 404 -78 607 -179 436 -217 742 -607 832 -1059 24 -119 24 -384 0 -504 -39 -194 -130 -405 -244 -563 -31 -43 -60 -94 -64 -112 -9 -42 -4 -61 114 -429 52 -161 93 -293 91 -293 -2 0 -149 74 -327 165 -263 134 -331 165 -365 165 -26 0 -82 -17 -149 -44 -528 -216 -1130 -170 -1608 124 -163 100 -335 258 -452 417 -115 155 -211 374 -250 570 -24 122 -24 384 0 506 106 530 514 974 1062 1155 239 79 508 108 753 81z"}), + path({d: "M2488 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 75 -38 157 -14 198 58 27 49 28 91 2 142 -37 73 -127 99 -202 59z"}), + path({d: "M3088 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 75 -38 157 -14 198 58 27 49 28 91 2 142 -37 73 -127 99 -202 59z"}), + path({d: "M3688 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 49 -25 90 -25 138 -1 43 22 82 84 82 131 0 47 -39 109 -82 131 -47 24 -93 24 -140 -2z"}) + ) + ) ) ), a({class: 'pfp', id: 'profile', href: '/profile', 'aria-label': 'your xssbook profile'}, @@ -246,7 +256,7 @@ export function parseUser(user) { parse('Joined ' + parseDate(new Date(user.date))) ), span({class: 'gtext'}, - parse('Gender :' + user.gender) + parse('Gender:' + user.gender) ), span({class: 'gtext'}, parse('Birthday: ' + parseMonth(user.month) + ' ' + user.day + ', ' + user.year) @@ -257,4 +267,4 @@ export function parseUser(user) { ) ) ) -} \ No newline at end of file +} diff --git a/public/js/home.js b/public/js/home.js index 9f0398f..7d64eab 100644 --- a/public/js/home.js +++ b/public/js/home.js @@ -36,7 +36,7 @@ function render() { let new_body = body({}, - ...header(true, false, data.self.user_id), + ...header(true, false, false, data.self.user_id), div({id: 'create'}, div({class: 'create'}, a({class: 'pfp', href: '/profile'}, @@ -44,7 +44,7 @@ function render() { ), button({class: 'pfp'}, p({class: 'gtext', onclick: () => document.getElementById('popup').classList.remove('hidden')}, - parse(`What' on your mind, ${data.self.firstname}`) + parse(`What's on your mind, ${data.self.firstname}`) ) ) ) @@ -113,7 +113,9 @@ async function load() { const posts = (await loadpostspage(page)).json if (posts.length === 0) { - document.getElementById('load').remove() + let load = document.getElementById('load') + if (load) + load.remove() return [] } else { page++ @@ -134,17 +136,6 @@ async function load() { async function init() { let request = (await loadself()); - - if (request.status === 429) { - let new_body = - body({}, - ...header(true, false) - ) - - document.body.replaceWith(new_body) - throw new Error("Rate limited"); - } - data.self = request.json if (request.json == undefined) { @@ -161,4 +152,4 @@ async function init() { } -init() \ No newline at end of file +init() diff --git a/public/js/main.js b/public/js/main.js index b49474c..80ee48b 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -83,6 +83,10 @@ export function path(attrs, ...children) { return createElementNS("path", attrs, ...children) } +export function g(attrs, ...children) { + return createElementNS("g", attrs, ...children) +} + export function svg(attrs, ...children) { return createElementNS("svg", attrs, ...children) } @@ -129,7 +133,7 @@ export function parseMonth(month) { } export function parseDate(date) { - return parseMonth(date.getUTCMonth()) + ' ' + date.getUTCDate() + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString(); + return parseMonth(date.getUTCMonth()) + ' ' + (date.getUTCDate()-1) + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString(); } export function crawl(key, object) { @@ -142,4 +146,4 @@ export function crawl(key, object) { } } return data -} \ No newline at end of file +} diff --git a/public/js/people.js b/public/js/people.js index 99890d9..431359c 100644 --- a/public/js/people.js +++ b/public/js/people.js @@ -6,7 +6,7 @@ function render() { let new_body = body({}, - ...header(false, true, data.self.user_id), + ...header(false, true, false, data.self.user_id), div({id: 'users'}, ...data.users.map(u => parseUser(u)) ), @@ -49,17 +49,6 @@ async function load() { async function init() { let request = (await loadself()); - - if (request.status === 429) { - let new_body = - body({}, - ...header(true, false, data.self.user_id) - ) - - document.body.replaceWith(new_body) - throw new Error("Rate limited"); - } - if (request.json == undefined) { location.href = '/login' return @@ -73,4 +62,4 @@ async function init() { render() } -init() \ No newline at end of file +init() diff --git a/public/js/profile.js b/public/js/profile.js index a9990fb..b000425 100644 --- a/public/js/profile.js +++ b/public/js/profile.js @@ -107,7 +107,7 @@ async function render() { let new_body = body({}, - ...header(false, false, data.self.user_id), + ...header(false, false, false, data.self.user_id), div({id: 'top'}, div({id: 'banner'}, div({class: 'bg'}, @@ -223,7 +223,7 @@ async function render() { parse('Email: ' + data.user.email) ), span({class: 'gtext bold'}, - parse('Gender ' + data.user.gender) + parse('Gender: ' + data.user.gender) ), span({class: 'gtext bold'}, parse('Birthday: ' + parseMonth(data.user.month) + ' ' + data.user.day + ', ' + data.user.year) @@ -362,4 +362,4 @@ async function init() { render() } -init() \ No newline at end of file +init() -- cgit v1.2.3-freya From c4c26f42b6b06dbc75578314a483db66bf7ddb4c Mon Sep 17 00:00:00 2001 From: Tyler Murphy <=> Date: Mon, 21 Aug 2023 23:19:53 -0400 Subject: dms --- public/chat.html | 1 + public/css/chat.css | 172 ++++++++++++++++++++++++++++++++++++++++ public/css/home.css | 2 +- public/css/main.css | 6 +- public/js/api.js | 26 +++++- public/js/chat.js | 207 ++++++++++++++++++++++++++++++++++++++++++++++-- public/js/components.js | 163 +++++++++++++++++++++++++++++++++++++- public/js/main.js | 6 +- src/api/chat.rs | 17 ++-- src/database/chat.rs | 30 +++---- src/main.rs | 2 +- src/types/chat.rs | 4 +- 12 files changed, 598 insertions(+), 38 deletions(-) create mode 100644 public/css/chat.css (limited to 'public/js') diff --git a/public/chat.html b/public/chat.html index e293210..ecd2f5c 100644 --- a/public/chat.html +++ b/public/chat.html @@ -17,6 +17,7 @@ + diff --git a/public/css/chat.css b/public/css/chat.css new file mode 100644 index 0000000..1092a39 --- /dev/null +++ b/public/css/chat.css @@ -0,0 +1,172 @@ +.spacer { + margin-bottom: 3.5em !important; +} + +#cent { + display: flex; + width: 100%; + height: calc(100vh - 3.5em); + flex-direction: row; +} + +#sidebar { + height: 100%; + width: 20%; + min-width: 25em; + display: flex; + flex-direction: column; +} + +#sidebar>span { + display: block; + width: 100%; + text-align: center; + font-size: 1.5em; + margin: .5em 0; +} + +#center { + height: 100%; + width: calc(100vw - 20%); + max-width: calc(100vw - 25em); +} + +.room { + width: calc (100% - 1rem); + height: 3rem; + display: flex; + position: relative; + flex-direction: row; + padding: .5rem 1rem; +} + +.room:hover, .current { + background-color: var(--hover); +} + +.room-icon { + border-radius: 3rem; + height: 3rem; + width: 3rem; + display: flex; + justify-content: center; + align-items: center; + font-weight: 1000; + font-size: 1.5rem; +} + +.room-name { + display: flex; + height: 3rem; + margin-left: 1rem; + align-items: center; +} + +.roomDisplay { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + position: relative; +} + +.roomDisplayCenter { + display: flex; + width: calc(100% - 25em); + height: 100%; + flex-direction: column; +} + +.roomDisplayPeople { + width: 25em; + height: 100%; + overflow-y: scroll; + display: flex; + align-items: center; + flex-direction: column; +} + +.roomDisplayPeople>span { + display: block; + width: 100%; + text-align: center; + font-size: 1.5em; + margin: .5em 0em; +} + +.person { + width: 20em; +} + +.person img, .person .profile { + height: 7em; + width: 7em; +} + +.person .info { + margin: 5px; +} + +.person .ltext { + font-size: 18px; +} + +.person .gtext { + font-size: 12px; +} + +.roomDisplay .messages { + flex: 1; + margin-left: 1rem; + display: flex; + flex-direction: column-reverse; + overflow-y: scroll; +} + +.roomDisplay .messageContent { + flex-grow: 0; + width: auto; + margin-bottom: .5rem; + display: block; + height: fit-content; + overflow: none; +} + +.roomDisplay .messageContent[contenteditable]:empty::before { + content: "Send an unencrypted message"; + color: gray; +} + +.addUser[contenteditable]:empty::before { + content: "Type email to add user"; + color: gray; +} + +.addRoom[contenteditable]:empty::before { + content: "Type name to create room"; + color: gray; +} + +.message { + display: flex; + flex-direction: row; + height: fit-content; + padding: .5rem; +} + +.message-pfp { + flex-grow: 0; + height: 3rem; + width: 3rem; + margin-right: 1rem; +} + +.message-pfp img { + width: 100%; + height: 100%; + border-radius: 3rem; +} + +.message-content { + flex-grow: 0; +} diff --git a/public/css/home.css b/public/css/home.css index f467aba..f21d201 100644 --- a/public/css/home.css +++ b/public/css/home.css @@ -184,4 +184,4 @@ body { width: calc(100% - 20px); background-color: var(--secondary); font-family: sfpro; -} \ No newline at end of file +} diff --git a/public/css/main.css b/public/css/main.css index 352a3b4..c8ff1d7 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -14,6 +14,10 @@ body { --popup: #ffffffcc; } +textarea { + resize: none +} + body { background-color: var(--primary); width: 100%; @@ -130,7 +134,7 @@ footer { background-color: var(--primary); } -input { +input, .input { flex: 1; font-family: sfpro; background-color: var(--primary); diff --git a/public/js/api.js b/public/js/api.js index 5a55460..9ef5e4c 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -141,4 +141,28 @@ export const updateavatar = async (file) => { export const updatebanner = async (file) => { return await fileRequest('/users/banner', file, 'PUT') -} \ No newline at end of file +} + +export const chatlist = async () => { + return await request('/chat/list', {}) +} + +export const chatcreate = async (name) => { + return await request('/chat/create', {name}) +} + +export const chatadd = async (email, room_id) => { + return await request('/chat/add', {email, room_id}, 'PATCH') +} + +export const chatleave = async (room_id) => { + return await request('/chat/leave', {room_id}, 'DELETE') +} + +export const chatsend = async (content, room_id) => { + return await request('/chat/send', {content, room_id}) +} + +export const chatload = async (newest_msg, page, room_id) => { + return await request('/chat/load', {newest_msg, page, room_id}) +} diff --git a/public/js/chat.js b/public/js/chat.js index 41ba2ba..33f21cb 100644 --- a/public/js/chat.js +++ b/public/js/chat.js @@ -1,21 +1,202 @@ -import { div, pfp, p, parse, button, body, a, textarea, span, crawl } from './main.js' -import { loadself, loadpostspage, createpost, loadusers } from './api.js' -import { parsePost, header } from './components.js' +import { body, div, span, p, parse } from './main.js' +import { loadself, chatlist, chatload, loadusers, chatcreate } from './api.js' +import { createRoomDisplay, header, parseMessage, parseRoom, parseUser, createSingleLineInput } from './components.js' -function render() { +async function getUser(user_id) { + if (data.users[user_id]) { + return data.users[user_id] + } else { + let request = (await loadusers([user_id])) + if (request.status != 200) { + location.href = 'login' + } else { + data.users[user_id] = request.json[0] + return request.json[0] + } + } +} + +async function parseMessageImpl(message) { + let user = await getUser(message.user_id) + return parseMessage(message, user) +} + +async function onRoomClick(room) { + for (const room of Object.values(data.rooms)) { + room.display.style.display = 'none' + room.button.classList.remove('current') + } + room.display.style.display = '' + room.button.classList.add('current') +} +async function render() { let new_body = body({}, ...header(false, false, true, data.self.user_id), + div({id: 'cent'}, + div({id: 'sidebar'}, + span({class: 'ltext'}, + parse("Rooms") + ), + createSingleLineInput( + { + type: 'text', + name: 'addRoom', + class: 'addRoom input', + style: 'flex-grow: 0; width: 80%; margin-left: auto; margin-right: auto;' + }, + async (text) => { + let result = (await chatcreate(text)) + if (result.status != 201) { + alert(result.msg) + return false + } else { + return true + } + } + ), + ), + div({id: 'center'}) + ) ) document.body.replaceWith(new_body) - } const data = { self: {}, - users: [] + users: [], + rooms: {}, +} + +async function loadRoomPage(room_id) { + let room = data.rooms[room_id] + let request = (await chatload ( + room.newest_msg, + room.page, + room_id + )) + + if (request.json == undefined) { + alert(request.msg) + return + } + + for (const msg of request.json) { + room.messages.push(msg) + } + + room.page++ +} + +async function loadRoom(room_id) { + let room = data.rooms[room_id] + + let request = (await loadusers(room.users)) + if (request.status != 200) { + location.href = '/login' + } else { + for (const user of request.json) { + data.users[user.user_id] = user + } + } + + room.page = 0 + room.messages = [] + if (room.newest_msg == undefined || room.newest_msg < 0) + room.newest_msg = Number.MAX_SAFE_INTEGER + await loadRoomPage(room_id) + room.newest_msg = Math.max( + ...room.messages.map(m => m.message_id) + ) + room.page = 0 + + room.display = createRoomDisplay(room) + + let displays = document.getElementById("center") + displays.appendChild(room.display) + + let button = parseRoom(room, onRoomClick) + + if (displays.children.length > 1) { + room.display.style.display = 'none' + } else { + button.classList.add('current') + } + + let sidebar = document.getElementById("sidebar") + sidebar.appendChild(button) + room.button = button + + let messages = room.display.getElementsByClassName('messages')[0] + for (const message of room.messages) { + messages.appendChild(await parseMessageImpl(message)) + } + + if (!room.people) room.people = room.people = {} + + let people = room.display.getElementsByClassName("roomDisplayPeople")[0] + for (const user_id of room.users) { + if (room.people[user_id]) continue + let user = await getUser(user_id) + let el = parseUser(user) + people.appendChild(el) + room.people[user_id] = el + } + +} + +async function onMessage(message) { + let event = JSON.parse(message.data) + switch (event.type) { + case "message": { + let room = data.rooms[event.room_id] + if (!room) return + let display = room.display + let messages = display.getElementsByClassName('messages')[0] + messages.prepend(await parseMessageImpl(event)) + break; + } + case "add": { + let room = data.rooms[event.room.room_id] + if (!room) { + // we are the user being added + data.rooms[event.room.room_id] = event.room + loadRoom(event.room.room_id) + } else { + let display = room.display + let people = display.getElementsByClassName('roomDisplayPeople')[0] + if (!room.people[event.user_id]) { + let user = await getUser(event.user_id) + let el = parseUser(user) + people.appendChild(el) + room.people[event.user_id] = el + } + } + break; + } + case "leave": { + let room = data.rooms[event.room_id] + if (!room) return + if (room.people[event.user_id]) { + room.people[event.user_id].remove() + delete room.people[event.user_id] + } + if (event.user_id == data.self.user_id) { + room.display.remove() + room.button.remove() + } + break; + } + case "typing": { + break; + } + default: { + console.warn("unhandled event: " + message.data) + break; + } + } } async function init() { @@ -31,6 +212,20 @@ async function init() { data.users[data.self.user_id] = data.self render() + + let rooms = (await chatlist()); + if (rooms.json === undefined) { + alert(rooms.msg) + } else { + for (const room of rooms.json) { + data.rooms[room.room_id] = room + loadRoom(room.room_id) + } + } + + let socket = new WebSocket(window.location.protocol.replace("http", "ws") + "//" + location.host + "/api/chat/connect") + socket.addEventListener("message", onMessage); + } diff --git a/public/js/components.js b/public/js/components.js index f247875..38af787 100644 --- a/public/js/components.js +++ b/public/js/components.js @@ -1,5 +1,5 @@ import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth, g } from './main.js' -import { postlike, postcomment, loadcommentspage } from './api.js'; +import { postlike, postcomment, loadcommentspage, chatsend, chatadd, chatleave } from './api.js'; window.parse = parse; @@ -268,3 +268,164 @@ export function parseUser(user) { ) ) } + +const stringToColor = (str) => { + let hash = 0; + str.split('').forEach(char => { + hash = char.charCodeAt(0) + ((hash << 5) - hash) + }) + let color = '#' + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff + color += value.toString(16).padStart(2, '0') + } + return color +} + +export function parseRoom(room, callback) { + + let dspName = room.name[0].toUpperCase() + let color = stringToColor(room.room_id + room.name + room.room_id) + + return ( + div({class: 'room', onclick: () => { + callback(room) + }}, + div({class: 'room-icon ltext', style: `background-color: ${color}`}, + span({}, parse(dspName)) + ), + div({class: 'room-name ltext'}, + span({}, parse(room.name)) + ), + div({ + class: 'close', + onclick: async () => { + let request = (await chatleave(room.room_id)) + + if (request.status != 200) { + alert(request.msg) + } + } + }) + ) + ) +} + +export function parseRooms(rooms, callback) { + let ret = [] + + for (const room of rooms) { + ret.push(parseRoom(room, callback)) + } + + return ret +} + +export function createMultiLineInput(attributes, onSubmit) { + let area = span({ + ...attributes, + role: 'textbox', + contenteditable: '', + onkeydown: async (event) => { + if (event.keyCode == 13 && !event.shiftKey) { + event.preventDefault() + let text = area.innerHTML.trim() + text = text.replaceAll("\n", "
") + if (text.length < 1) return + if (await onSubmit(text)) { + area.textContent = '' + } + } + }, + }) + return area +} + +export function createSingleLineInput(attributes, onSubmit) { + let area = span({ + ...attributes, + role: 'textbox', + contenteditable: '', + onkeydown: async (event) => { + if (event.keyCode == 13 && !event.shiftKey) { + event.preventDefault() + let text = area.innerHTML.trim() + text = text.replaceAll("\n", "
") + if (text.length < 1) return + if (await onSubmit(text)) { + area.textContent = '' + } + } else if (event.keyCode == 13) { + event.preventDefault() + } + }, + }) + return area +} + +export function createRoomDisplay(room) { + return ( + div({class: 'roomDisplay'}, + div({class: 'roomDisplayCenter'}, + div({class: 'messages'}), + createMultiLineInput( + { + type: 'text', + name: 'messageContent', + class: 'messageContent input', + }, + async (text) => { + let result = (await chatsend(text, room.room_id)) + + if (result.status != 201) { + alert(result.msg) + return false + } else { + return true + } + } + ), + ), + div({class: 'roomDisplayPeople'}, + span({class: 'ltext'}, + parse("People"), + ), + createSingleLineInput( + { + type: 'text', + name: 'addUser', + class: 'addUser input', + style: 'flex-grow: 0; width: 80%' + }, + async (text) => { + let result = (await chatadd(text, room.room_id)) + if (result.status != 200) { + alert(result.msg) + return false + } else { + return true + } + } + ) + ) + ) + ) +} + +export function parseMessage(message, user) { + return ( + div({class: 'message'}, + a({class: 'message-pfp', href: `/profile?id=${message.user_id}`}, + pfp(message.user_id) + ), + div({class: 'message-content'}, + span({class: 'message-name ltext'}, + parse(user.firstname + ' ' + user.lastname) + ), + p({class: 'message-text ltext'}, + parse(message.content) + ) + ) + ) + ) +} diff --git a/public/js/main.js b/public/js/main.js index 80ee48b..a20ef10 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -103,10 +103,6 @@ export function parse(html) { return document.createRange().createContextualFragment(html); } -export function pfpl(id) { - -} - export function pfp(id) { return img('pfp', {src: `/image/avatar?user_id=${id}`}) } @@ -133,7 +129,7 @@ export function parseMonth(month) { } export function parseDate(date) { - return parseMonth(date.getUTCMonth()) + ' ' + (date.getUTCDate()-1) + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString(); + return parseMonth(date.getUTCMonth()) + ' ' + date.getUTCDate() + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString(); } export function crawl(key, object) { diff --git a/src/api/chat.rs b/src/api/chat.rs index 1e56c3e..02bdfbd 100644 --- a/src/api/chat.rs +++ b/src/api/chat.rs @@ -6,7 +6,7 @@ use tokio::sync::{Mutex, mpsc::{Sender, self}}; use crate::{ public::docs::{EndpointDocumentation, EndpointMethod}, types::{ - extract::{AuthorizedUser, Check, CheckResult, Database, Json}, + extract::{AuthorizedUser, Check, CheckResult, Database, Json, Log}, http::ResponseCode, chat::{ChatRoom, ChatEvent}, user::User, }, @@ -75,7 +75,8 @@ pub const CHAT_LIST: EndpointDocumentation = EndpointDocumentation { async fn list ( AuthorizedUser(user): AuthorizedUser, - Database(db): Database + Database(db): Database, + _: Log ) -> Response { let Ok(rooms) = ChatRoom::from_user_id(&db, user.user_id) else { return ResponseCode::InternalServerError.text("Failed to retrieve rooms") @@ -137,7 +138,7 @@ async fn create ( for user in &room.users { send_event(ChatEvent::Add { user_id: *user, - room_id: room.room_id + room: room.clone() }, &room).await; } @@ -191,10 +192,14 @@ async fn add ( return ResponseCode::BadRequest.text("User does not exist") }; - let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else { + let Ok(mut room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else { return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it") }; + if room.users.contains(&to_add.user_id) { + return ResponseCode::BadRequest.text("User is already in the room") + } + let Ok(success) = room.add_user(&db, to_add.user_id) else { return ResponseCode::InternalServerError.text("Failed to add user") }; @@ -202,10 +207,12 @@ async fn add ( if !success { return ResponseCode::BadRequest.text("User is already in the room") } + + room.users.push(to_add.user_id); send_event(ChatEvent::Add { user_id: to_add.user_id, - room_id: room.room_id + room: room.clone() }, &room).await; ResponseCode::Success.text("Successfully added user") diff --git a/src/database/chat.rs b/src/database/chat.rs index 7364211..99ec86c 100644 --- a/src/database/chat.rs +++ b/src/database/chat.rs @@ -61,9 +61,10 @@ impl Database { ); ", )?; + let row = stmt.query_map([user_id], |row| { - let room_id = row.get(0)?; - let name = row.get(1)?; + let room_id: u64 = row.get(0)?; + let name: String = row.get(1)?; let mut stmt2 = self.0.prepare( " @@ -72,20 +73,19 @@ impl Database { " )?; - let mut users = Vec::new(); - let _ = stmt2.query_map([room_id], |row2| { - let user_id = row2.get(0)?; - users.push(user_id); - Ok(()) - })?; + let users = stmt2.query_map([room_id], |row2| { + Ok(row2.get(0)?) + })?.into_iter().flatten().collect(); let room = ChatRoom { room_id, users, name }; + Ok(room) })?; + Ok(row.into_iter().flatten().collect()) } @@ -158,8 +158,8 @@ impl Database { let msg = stmt.query_row((user_id, room_id, date, content), |row| { let message_id = row.get(0)?; - let room_id = row.get(1)?; - let user_id = row.get(2)?; + let user_id = row.get(1)?; + let room_id = row.get(2)?; let date = row.get(3)?; let content = row.get(4)?; @@ -182,17 +182,17 @@ impl Database { " SELECT * FROM chat_messages WHERE room_id = ? - AND message_id < newest_message - ORDER BY message_id ASC + AND message_id < ? + ORDER BY message_id DESC LIMIT ? OFFSET ? " )?; - let messages = stmt.query_map((room_id, 20, 20 * page), |row| { + let messages = stmt.query_map((room_id, newest_message, 20, 20 * page), |row| { let message_id = row.get(0)?; - let room_id = row.get(1)?; - let user_id = row.get(2)?; + let user_id = row.get(1)?; + let room_id = row.get(2)?; let date = row.get(3)?; let content = row.get(4)?; diff --git a/src/main.rs b/src/main.rs index 817f8ac..cc8a61e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ async fn main() { tracing_subscriber::registry() .with( fmt_layer - .with_filter(LevelFilter::TRACE) + .with_filter(LevelFilter::INFO) .with_filter(filter_fn(|metadata| { metadata.target().starts_with("xssbook") })), diff --git a/src/types/chat.rs b/src/types/chat.rs index ab3390c..8413f77 100644 --- a/src/types/chat.rs +++ b/src/types/chat.rs @@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize}; use tracing::instrument; use crate::{types::http::{ResponseCode, Result}, database::Database}; -#[derive(Serialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct ChatRoom { pub room_id: u64, pub users: Vec, @@ -33,7 +33,7 @@ pub enum ChatEvent { #[serde(rename = "add")] Add { user_id: u64, - room_id: u64 + room: ChatRoom }, #[serde(rename = "leave")] -- cgit v1.2.3-freya From 909d47f3316b3593a375bfb52535003c6bc9dd4b Mon Sep 17 00:00:00 2001 From: Tyler Murphy <=> Date: Mon, 21 Aug 2023 23:43:01 -0400 Subject: fuck you you stupid browsers stop making thing safe >:( --- public/js/components.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'public/js') diff --git a/public/js/components.js b/public/js/components.js index 38af787..0bf0ed1 100644 --- a/public/js/components.js +++ b/public/js/components.js @@ -330,6 +330,13 @@ export function createMultiLineInput(attributes, onSubmit) { if (event.keyCode == 13 && !event.shiftKey) { event.preventDefault() let text = area.innerHTML.trim() + .replaceAll("&", '&') + .replaceAll("<", '<') + .replaceAll(">", '>') + .replaceAll(""", '"') + .replaceAll("'", "'") + + text = text.replaceAll("\n", "
") if (text.length < 1) return if (await onSubmit(text)) { -- cgit v1.2.3-freya From fabc1e51cb6b21b19f00f155adb0ee329c9abdb2 Mon Sep 17 00:00:00 2001 From: Tyler Murphy <=> Date: Tue, 22 Aug 2023 00:15:10 -0400 Subject: finish dms --- public/css/chat.css | 10 ++++++++- public/js/chat.js | 60 ++++++++++++++++++++++++++++--------------------- public/js/components.js | 17 ++++++++++++-- 3 files changed, 58 insertions(+), 29 deletions(-) (limited to 'public/js') diff --git a/public/css/chat.css b/public/css/chat.css index 1092a39..d654679 100644 --- a/public/css/chat.css +++ b/public/css/chat.css @@ -53,7 +53,8 @@ align-items: center; font-weight: 1000; font-size: 1.5rem; -} + +right: 0;} .room-name { display: flex; @@ -71,6 +72,7 @@ } .roomDisplayCenter { + position: relative; display: flex; width: calc(100% - 25em); height: 100%; @@ -170,3 +172,9 @@ .message-content { flex-grow: 0; } + +.loadMessages { + position: absolute; + right: 0; + cursor: pointer; +} diff --git a/public/js/chat.js b/public/js/chat.js index 33f21cb..ed92bb6 100644 --- a/public/js/chat.js +++ b/public/js/chat.js @@ -70,12 +70,12 @@ const data = { rooms: {}, } -async function loadRoomPage(room_id) { - let room = data.rooms[room_id] +async function loadRoomPage(room) { + let request = (await chatload ( room.newest_msg, room.page, - room_id + room.room_id )) if (request.json == undefined) { @@ -83,36 +83,38 @@ async function loadRoomPage(room_id) { return } + let messages = room.display.getElementsByClassName('messages')[0] for (const msg of request.json) { room.messages.push(msg) + messages.appendChild(await parseMessageImpl(msg)) } room.page++ + + return request.json.length > 0 } async function loadRoom(room_id) { let room = data.rooms[room_id] - let request = (await loadusers(room.users)) - if (request.status != 200) { - location.href = '/login' - } else { - for (const user of request.json) { - data.users[user.user_id] = user + let batch = [] + for (const user_id of room.users) { + if (data.users[user_id]) continue + batch.push(user_id) + } + + if (batch.length > 1) { + let request = (await loadusers(batch)) + if (request.status != 200) { + location.href = '/login' + } else { + for (const user of request.json) { + data.users[user.user_id] = user + } } } - room.page = 0 - room.messages = [] - if (room.newest_msg == undefined || room.newest_msg < 0) - room.newest_msg = Number.MAX_SAFE_INTEGER - await loadRoomPage(room_id) - room.newest_msg = Math.max( - ...room.messages.map(m => m.message_id) - ) - room.page = 0 - - room.display = createRoomDisplay(room) + room.display = createRoomDisplay(room, loadRoomPage) let displays = document.getElementById("center") displays.appendChild(room.display) @@ -125,15 +127,20 @@ async function loadRoom(room_id) { button.classList.add('current') } + room.page = 0 + room.messages = [] + if (room.newest_msg == undefined || room.newest_msg < 0) + room.newest_msg = Number.MAX_SAFE_INTEGER + await loadRoomPage(room) + room.newest_msg = Math.min( + ...room.messages.map(m => m.message_id) + ) + room.page = 0 + let sidebar = document.getElementById("sidebar") sidebar.appendChild(button) room.button = button - let messages = room.display.getElementsByClassName('messages')[0] - for (const message of room.messages) { - messages.appendChild(await parseMessageImpl(message)) - } - if (!room.people) room.people = room.people = {} let people = room.display.getElementsByClassName("roomDisplayPeople")[0] @@ -186,6 +193,7 @@ async function onMessage(message) { if (event.user_id == data.self.user_id) { room.display.remove() room.button.remove() + delete data.rooms[event.room_id] } break; } @@ -202,13 +210,13 @@ async function onMessage(message) { async function init() { let request = (await loadself()); - data.self = request.json if (request.json == undefined) { location.href = '/login' return } + data.self = request.json data.users[data.self.user_id] = data.self render() diff --git a/public/js/components.js b/public/js/components.js index 0bf0ed1..88895c1 100644 --- a/public/js/components.js +++ b/public/js/components.js @@ -1,4 +1,4 @@ -import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth, g } from './main.js' +import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth, g, button } from './main.js' import { postlike, postcomment, loadcommentspage, chatsend, chatadd, chatleave } from './api.js'; window.parse = parse; @@ -370,10 +370,23 @@ export function createSingleLineInput(attributes, onSubmit) { return area } -export function createRoomDisplay(room) { +export function createRoomDisplay(room, loadMessageCallback) { + let buttonEl = button({ + class: 'loadMessages input', + style: 'flex-grow: 0', + onclick: async () => { + if (!await loadMessageCallback(room)) { + buttonEl.remove() + } + } + }, + parse('Load Previous') + ) + return ( div({class: 'roomDisplay'}, div({class: 'roomDisplayCenter'}, + buttonEl, div({class: 'messages'}), createMultiLineInput( { -- cgit v1.2.3-freya