add: working chat ui

This commit is contained in:
trisua 2025-08-27 20:22:12 -04:00
parent f53eb3d367
commit b360c5e737
14 changed files with 319 additions and 53 deletions

153
app/public/messages.js Normal file
View file

@ -0,0 +1,153 @@
const STATE = {
id: 0,
chat_id: "",
observer: null,
is_loading: false,
stream_element: null,
first_message_time: 0,
};
function create_streamer(chat_id, hook_element) {
STATE.chat_id = chat_id;
STATE.stream_element = hook_element.parentElement;
STATE.observer = new IntersectionObserver(
() => {
load_messages();
},
{
root: STATE.stream_element,
rootMargin: "0px",
scrollMargin: "0px",
threshold: 1.0,
},
);
STATE.observer.observe(hook_element);
}
function load_messages() {
if (STATE.is_loading) {
return;
}
STATE.is_loading = true;
STATE.id += 1;
fetch(
`/chats/_templates/chat/${STATE.chat_id}/messages/before/${STATE.first_message_time}?use_id=${STATE.id}`,
)
.then((res) => res.text())
.then((res) => {
setTimeout(() => {
STATE.is_loading = false;
}, 2000);
STATE.stream_element.innerHTML += res;
STATE.first_message_time = Number.parseInt(
document
.getElementById(`msgs_data_${STATE.id}`)
.getAttribute("data-first-message-time"),
);
// STATE.stream_element.scrollTo(0, STATE.stream_element.scrollHeight);
if (document.getElementById(`msgs_quit_${STATE.id}`)) {
STATE.observer.disconnect();
console.log("quit");
} else {
STATE.observer.unobserve(
STATE.stream_element.querySelector(
"[ui_ident=data_marker]",
),
);
const element = document.createElement("div");
element.setAttribute("ui_ident", "data_marker");
STATE.stream_element.append(element);
STATE.observer.observe(element);
}
});
}
function render_message(id) {
STATE.is_loading = true;
fetch(`/chats/_templates/message/${id}`)
.then((res) => res.text())
.then((res) => {
STATE.is_loading = false;
STATE.stream_element.innerHTML = `${res}${STATE.stream_element.innerHTML}`;
mark_message_read();
read_receipt();
STATE.stream_element.scrollTo(0, STATE.stream_element.scrollHeight);
});
}
function sock_con() {
const socket = new WebSocket(
`//${window.location.origin.split("//")[1]}/api/v1/chats/${STATE.chat_id}/_connect`,
);
socket.addEventListener("message", async (event) => {
if (event.data === "Ping") {
return socket.send("Pong");
}
const msg = JSON.parse(event.data);
if (msg.method === "MessageCreate") {
render_message(msg.body);
} else if (msg.method === "MessageDelete") {
if (document.getElementById(`message_${msg.body}`)) {
document.getElementById(`message_${msg.body}`).remove();
}
}
});
}
function create_message(e) {
e.preventDefault();
const body = new FormData();
body.append(
"body",
JSON.stringify({
content: e.target.content.value,
}),
);
fetch(`/api/v1/messages/${STATE.chat_id}`, { method: "POST", body })
.then((res) => res.json())
.then((res) => {
if (res.ok) {
e.target.reset();
} else {
show_message(res.message, res.ok);
}
});
}
function mark_message_read() {
fetch(`/api/v1/chats/${STATE.chat_id}/read_message`, {
method: "POST",
})
.then((res) => res.json())
.then((res) => {
if (!res.ok) {
show_message(res.message, res.ok);
}
});
}
function read_receipt() {
if (document.getElementById("delivered_read_status")) {
document.getElementById("delivered_read_status").remove();
}
fetch(`/chats/_templates/chat/${STATE.chat_id}/read_receipt`)
.then((res) => res.text())
.then((res) => {
STATE.stream_element.innerHTML = `${res}${STATE.stream_element.innerHTML}`;
});
}

View file

@ -746,3 +746,24 @@ menu.col {
width: 25rem;
max-width: 100%;
}
/* messages */
.message {
align-items: flex-end;
}
.message:not(.mine) {
flex-direction: row-reverse;
justify-content: flex-end;
}
.message .inner {
padding: var(--pad-2) var(--pad-3);
background: var(--color-surface);
color: var(--color-text);
}
.message.mine .inner {
background: var(--color-primary);
color: var(--color-text-primary);
}

View file

@ -19,30 +19,16 @@
("style" "flex: 1 0 auto")
(div
("class" "card flex flex_rev_col gap_2")
("style" "flex: 1 0 auto")
("style" "flex: 1 0 auto; max-height: 80dvh; overflow: auto")
("id" "messages_stream")
(text "{% if chat.last_message_created > 0 -%}")
(div
("class" "flex gap_ch items_center")
("id" "delivered_read_status")
(text "{% if chat.last_message_read_by|length <= 1 -%}")
; just delivered
(text "{{ icon \"check\" }}")
(text "{{ chat.last_message_created|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC }}")
(text "{%- else -%}")
; delivered and read by at least two people
(text "{{ icon \"check-check\" }}")
(text "{% for uid in chat.last_message_read_by -%}")
(text "{{ components::avatar(id=uid) }}")
(text "{%- endfor %}")
(text "{%- endif %}"))
(text "{%- endif %}")
(text "{% if messages|length == 0 -%}")
(i ("class" "flex gap_ch items_center fade") (text "{{ icon \"star\" }} This is the start of the chat!"))
(text "{%- endif %}"))
(text "{%- endif %}")
(div ("ui_ident" "data_marker")))
(form
("class" "card flex flex_row items_center gap_2")
("onsubmit" "create_message(event)")
(input
("type" "text")
("class" "w_full")
@ -53,6 +39,10 @@
("class" "button")
(text "{{ icon \"send\" }}"))))
(script ("src" "/public/messages.js"))
(script
(text ""))
(text "create_streamer(\"{{ chat.id }}\", document.querySelector(\"[ui_ident=data_marker]\"));
sock_con();
mark_message_read();
read_receipt();"))
(text "{% endblock %}")

View file

@ -22,18 +22,18 @@
; advanced
(text "{% if chat.style == \"Direct\" -%} {% for member in members -%} {% if member.id != user.id -%}")
; direct message; user that ISN'T the current user
(text "{{ components::avatar(id=member.id, size=avatar_size) }}")
(text "{{ components::username(user=member) }}")
(text "{{ self::avatar(id=member.id, size=avatar_size) }}")
(text "{{ self::username(user=member) }}")
(text "{%- endif %} {%- endfor %} {%- else -%}")
; group chat
(text "{% for member in members -%} {{ components::avatar(id=member.id, size=avatar_size) }} {%- endfor %}")
(text "{% for member in members -%} {{ self::avatar(id=member.id, size=avatar_size) }} {%- endfor %}")
(b (text "{{ chat.style.Group.name }}"))
(text "{%- endif %}")
(text "{%- else -%}")
; NOT advanced
(text "{% if chat.style == \"Direct\" -%} {% for member in members -%} {% if member.id != user.id -%}")
; direct message; user that ISN'T the current user
(text "{{ user.username }}")
(text "{{ member.username }}")
(text "{%- endif %} {%- endfor %} {%- else -%}")
; group chat
(text "{{ chat.style.Group.name }}")
@ -44,8 +44,9 @@
(text "{% macro message(message) -%}")
(div
("class" "flex w_full gap_ch message {%- if user.id == message.owner %} justify_right mine {%- endif %}")
("id" "message_{{ message.id }}")
(div
("class" "inner no_p_margin")
(text "{{ message.content|markdown|safe }}"))
(text "{{ components::avatar(id=uid) }}"))
(text "{{ self::avatar(id=message.owner) }}"))
(text "{%- endmacro %}")

View file

@ -101,7 +101,7 @@
// redirect
setTimeout(() => {
window.location.href = \"/app\";
window.location.href = \"/chats\";
}, 150);
}
});

View file

@ -1,2 +1,2 @@
(text "{%- import \"components.lisp\" as components -%}")
(text "{{ components::message(message) }}")
(text "{{ components::message(message=message) }}")

View file

@ -1,4 +1,15 @@
(text "{%- import \"components.lisp\" as components -%}")
(text "{% for message in messages -%}")
(text "{{ components::message(message) }}")
(text "{{ components::message(message=message) }}")
(text "{%- endfor %}")
(div
("class" "hidden")
("id" "msgs_data_{{ id }}")
("data-first-message-time" "{{ first_message_time }}"))
(text "{% if messages|length == 0 -%}")
(div
("class" "hidden")
("id" "msgs_quit_{{ id }}"))
(text "{%- endif %}")

View file

@ -0,0 +1,17 @@
(text "{%- import \"components.lisp\" as components -%}")
(text "{% if chat.last_message_created > 0 -%}")
(div
("class" "flex gap_ch items_center")
("id" "delivered_read_status")
(text "{% if chat.last_message_read_by|length <= 1 -%}")
; just delivered
(text "{{ icon \"check\" }}")
(text "{{ chat.last_message_created|int|date(format=\"%H:%M\", timezone=\"Etc/UTC\") }} UTC")
(text "{%- else -%}")
; delivered and read by at least two people
(text "{{ icon \"check-check\" }}")
(text "{% for uid in chat.last_message_read_by -%}")
(text "{{ components::avatar(id=uid) }}")
(text "{%- endfor %}")
(text "{%- endif %}"))
(text "{%- endif %}")