This commit is contained in:
retoor 2025-05-23 00:44:18 +02:00
parent b2a4887e23
commit f0545cbf02
22 changed files with 1275 additions and 1232 deletions

View File

@ -7,7 +7,7 @@
// MIT License // MIT License
import { Schedule } from './schedule.js'; import { Schedule } from "./schedule.js";
import { EventHandler } from "./event-handler.js"; import { EventHandler } from "./event-handler.js";
import { Socket } from "./socket.js"; import { Socket } from "./socket.js";
@ -16,11 +16,11 @@ export class RESTClient {
async get(url, params = {}) { async get(url, params = {}) {
const encodedParams = new URLSearchParams(params); const encodedParams = new URLSearchParams(params);
if (encodedParams) url += '?' + encodedParams; if (encodedParams) url += "?" + encodedParams;
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
}); });
const result = await response.json(); const result = await response.json();
@ -32,9 +32,9 @@ export class RESTClient {
async post(url, data) { async post(url, data) {
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
@ -50,7 +50,10 @@ export class RESTClient {
export class Chat extends EventHandler { export class Chat extends EventHandler {
constructor() { constructor() {
super(); super();
this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws'; this._url =
window.location.hostname === "localhost"
? "ws://localhost/chat.ws"
: "wss://" + window.location.hostname + "/chat.ws";
this._socket = null; this._socket = null;
this._waitConnect = null; this._waitConnect = null;
this._promises = {}; this._promises = {};
@ -81,7 +84,7 @@ export class Chat extends EventHandler {
} }
generateUniqueId() { generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9); return "id-" + Math.random().toString(36).substr(2, 9);
} }
call(method, ...args) { call(method, ...args) {
@ -109,7 +112,7 @@ export class Chat extends EventHandler {
this._socket.onclose = () => { this._socket.onclose = () => {
this._waitSocket = null; this._waitSocket = null;
this._socket = null; this._socket = null;
this.emit('close'); this.emit("close");
}; };
} }
@ -127,19 +130,21 @@ export class NotificationAudio {
} }
sounds = { sounds = {
"message": "/audio/soundfx.d_beep3.mp3", message: "/audio/soundfx.d_beep3.mp3",
"mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav", mention: "/audio/750607__deadrobotmusic__notification-sound-1.wav",
"messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav", messageOtherChannel:
"ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav", "/audio/750608__deadrobotmusic__notification-sound-2.wav",
} ping: "/audio/750609__deadrobotmusic__notification-sound-3.wav",
};
play(soundIndex = 0) { play(soundIndex = 0) {
this.schedule.delay(() => { this.schedule.delay(() => {
new Audio(this.sounds[soundIndex]).play() new Audio(this.sounds[soundIndex])
.play()
.then(() => { .then(() => {
console.debug("Gave sound notification"); console.debug("Gave sound notification");
}) })
.catch(error => { .catch((error) => {
console.error("Notification failed:", error); console.error("Notification failed:", error);
}); });
}); });
@ -153,17 +158,17 @@ export class App extends EventHandler {
audio = null; audio = null;
user = {}; user = {};
typeLock = null; typeLock = null;
typeListener = null typeListener = null;
typeEventChannelUid = null typeEventChannelUid = null;
async set_typing(channel_uid) { async set_typing(channel_uid) {
this.typeEventChannel_uid = channel_uid this.typeEventChannel_uid = channel_uid;
} }
async ping(...args) { async ping(...args) {
if (this.is_pinging) return false if (this.is_pinging) return false;
this.is_pinging = true this.is_pinging = true;
await this.rpc.ping(...args); await this.rpc.ping(...args);
this.is_pinging = false this.is_pinging = false;
} }
async forcePing(...arg) { async forcePing(...arg) {
@ -175,30 +180,30 @@ export class App extends EventHandler {
this.ws = new Socket(); this.ws = new Socket();
this.rpc = this.ws.client; this.rpc = this.ws.client;
this.audio = new NotificationAudio(500); this.audio = new NotificationAudio(500);
this.is_pinging = false this.is_pinging = false;
this.ping_interval = setInterval(() => { this.ping_interval = setInterval(() => {
this.ping("active") this.ping("active");
}, 15000) }, 15000);
this.typeEventChannelUid = null this.typeEventChannelUid = null;
this.typeListener = setInterval(() => { this.typeListener = setInterval(() => {
if (this.typeEventChannelUid) { if (this.typeEventChannelUid) {
this.rpc.set_typing(this.typeEventChannelUid) this.rpc.set_typing(this.typeEventChannelUid);
this.typeEventChannelUid = null this.typeEventChannelUid = null;
} }
}) });
const me = this const me = this;
this.ws.addEventListener("connected", (data) => { this.ws.addEventListener("connected", (data) => {
this.ping("online") this.ping("online");
}) });
this.ws.addEventListener("channel-message", (data) => { this.ws.addEventListener("channel-message", (data) => {
me.emit("channel-message", data); me.emit("channel-message", data);
}); });
this.ws.addEventListener("event", (data) => { this.ws.addEventListener("event", (data) => {
console.info("aaaa") console.info("aaaa");
}) });
this.rpc.getUser(null).then(user => { this.rpc.getUser(null).then((user) => {
me.user = user; me.user = user;
}); });
} }
@ -218,31 +223,35 @@ export class App extends EventHandler {
timeAgo(date1, date2) { timeAgo(date1, date2) {
const diffMs = Math.abs(date2 - date1); const diffMs = Math.abs(date2 - date1);
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const hours = Math.floor(
(diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
);
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000); const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
if (days) { if (days) {
return `${days} ${days > 1 ? 'days' : 'day'} ago`; return `${days} ${days > 1 ? "days" : "day"} ago`;
} }
if (hours) { if (hours) {
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`; return `${hours} ${hours > 1 ? "hours" : "hour"} ago`;
} }
if (minutes) { if (minutes) {
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`; return `${minutes} ${minutes > 1 ? "minutes" : "minute"} ago`;
} }
return 'just now'; return "just now";
} }
async benchMark(times = 100, message = "Benchmark Message") { async benchMark(times = 100, message = "Benchmark Message") {
const promises = []; const promises = [];
const me = this; const me = this;
for (let i = 0; i < times; i++) { for (let i = 0; i < times; i++) {
promises.push(this.rpc.getChannels().then(channels => { promises.push(
channels.forEach(channel => { this.rpc.getChannels().then((channels) => {
channels.forEach((channel) => {
me.rpc.sendMessage(channel.uid, `${message} ${i}`); me.rpc.sendMessage(channel.uid, `${message} ${i}`);
}); });
})); }),
);
} }
} }
} }

View File

@ -1,15 +1,10 @@
import { app } from "../app.js";
import { app } from '../app.js';
class ChatInputComponent extends HTMLElement { class ChatInputComponent extends HTMLElement {
autoCompletions = { autoCompletions = {
'example 1': () => { "example 1": () => {},
"example 2": () => {},
}, };
'example 2': () => {
}
}
constructor() { constructor() {
super(); super();
@ -40,8 +35,7 @@ class ChatInputComponent extends HTMLElement {
value = key; value = key;
} }
}); });
if (count == 1) if (count == 1) return value;
return value;
return null; return null;
} }
@ -56,7 +50,8 @@ class ChatInputComponent extends HTMLElement {
async connectedCallback() { async connectedCallback() {
this.user = await app.rpc.getUser(null); this.user = await app.rpc.getUser(null);
this.liveType = this.getAttribute("live-type") === "true"; this.liveType = this.getAttribute("live-type") === "true";
this.liveTypeInterval = parseInt(this.getAttribute("live-type-interval")) || 3; this.liveTypeInterval =
parseInt(this.getAttribute("live-type-interval")) || 3;
this.channelUid = this.getAttribute("channel"); this.channelUid = this.getAttribute("channel");
this.messageUid = null; this.messageUid = null;
@ -79,10 +74,10 @@ class ChatInputComponent extends HTMLElement {
this.appendChild(this.uploadButton); this.appendChild(this.uploadButton);
this.textarea.addEventListener("keyup", (e) => { this.textarea.addEventListener("keyup", (e) => {
if(e.key === 'Enter' && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
this.value = '' this.value = "";
e.target.value = ''; e.target.value = "";
return return;
} }
this.value = e.target.value; this.value = e.target.value;
this.changed = true; this.changed = true;
@ -100,13 +95,13 @@ class ChatInputComponent extends HTMLElement {
return; return;
} }
} }
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
const message = e.target.value; const message = e.target.value;
this.messageUid = null; this.messageUid = null;
this.value = ''; this.value = "";
this.previousValue = ''; this.previousValue = "";
if (!message) { if (!message) {
return; return;
@ -114,15 +109,15 @@ class ChatInputComponent extends HTMLElement {
let autoCompletion = this.autoCompletions[message]; let autoCompletion = this.autoCompletions[message];
if (autoCompletion) { if (autoCompletion) {
this.value = ''; this.value = "";
this.previousValue = ''; this.previousValue = "";
e.target.value = ''; e.target.value = "";
autoCompletion(); autoCompletion();
return; return;
} }
e.target.value = ''; e.target.value = "";
this.value = ''; this.value = "";
this.messageUid = null; this.messageUid = null;
this.sendMessage(this.channelUid, message).then((uid) => { this.sendMessage(this.channelUid, message).then((uid) => {
this.messageUid = uid; this.messageUid = uid;
@ -135,9 +130,12 @@ class ChatInputComponent extends HTMLElement {
return; return;
} }
if (this.value !== this.previousValue) { if (this.value !== this.previousValue) {
if (this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval) { if (
this.value = ''; this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
this.previousValue = ''; this.liveTypeInterval
) {
this.value = "";
this.previousValue = "";
} }
this.lastChange = new Date(); this.lastChange = new Date();
} }
@ -163,7 +161,7 @@ class ChatInputComponent extends HTMLElement {
newMessage() { newMessage() {
if (!this.messageUid) { if (!this.messageUid) {
this.messageUid = '?'; this.messageUid = "?";
} }
this.sendMessage(this.channelUid, this.value).then((uid) => { this.sendMessage(this.channelUid, this.value).then((uid) => {
@ -179,10 +177,14 @@ class ChatInputComponent extends HTMLElement {
this.newMessage(); this.newMessage();
return false; return false;
} }
if (this.messageUid === '?') { if (this.messageUid === "?") {
return false; return false;
} }
if (typeof app !== "undefined" && app.rpc && typeof app.rpc.updateMessageText === "function") { if (
typeof app !== "undefined" &&
app.rpc &&
typeof app.rpc.updateMessageText === "function"
) {
app.rpc.updateMessageText(this.messageUid, this.value); app.rpc.updateMessageText(this.messageUid, this.value);
} }
} }
@ -193,15 +195,21 @@ class ChatInputComponent extends HTMLElement {
} }
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) { if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
this.lastUpdateEvent = new Date(); this.lastUpdateEvent = new Date();
if (typeof app !== "undefined" && app.rpc && typeof app.rpc.set_typing === "function") { if (
typeof app !== "undefined" &&
app.rpc &&
typeof app.rpc.set_typing === "function"
) {
app.rpc.set_typing(this.channelUid, this.user.color); app.rpc.set_typing(this.channelUid, this.user.color);
} }
} }
} }
update() { update() {
const expired = this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval; const expired =
const changed = (this.value !== this.previousValue); this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
this.liveTypeInterval;
const changed = this.value !== this.previousValue;
if (changed || expired) { if (changed || expired) {
this.lastChange = new Date(); this.lastChange = new Date();
@ -232,4 +240,4 @@ class ChatInputComponent extends HTMLElement {
} }
} }
customElements.define('chat-input', ChatInputComponent); customElements.define("chat-input", ChatInputComponent);

View File

@ -13,11 +13,11 @@
class ChatWindowElement extends HTMLElement { class ChatWindowElement extends HTMLElement {
receivedHistory = false; receivedHistory = false;
channel = null channel = null;
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.component = document.createElement('section'); this.component = document.createElement("section");
this.app = app; this.app = app;
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
@ -27,9 +27,9 @@ class ChatWindowElement extends HTMLElement {
} }
async connectedCallback() { async connectedCallback() {
const link = document.createElement('link'); const link = document.createElement("link");
link.rel = 'stylesheet'; link.rel = "stylesheet";
link.href = '/base.css'; link.href = "/base.css";
this.component.appendChild(link); this.component.appendChild(link);
this.component.classList.add("chat-area"); this.component.classList.add("chat-area");
@ -39,7 +39,7 @@ class ChatWindowElement extends HTMLElement {
const chatHeader = document.createElement("div"); const chatHeader = document.createElement("div");
chatHeader.classList.add("chat-header"); chatHeader.classList.add("chat-header");
const chatTitle = document.createElement('h2'); const chatTitle = document.createElement("h2");
chatTitle.classList.add("chat-title"); chatTitle.classList.add("chat-title");
chatTitle.classList.add("no-select"); chatTitle.classList.add("no-select");
chatTitle.innerText = "Loading..."; chatTitle.innerText = "Loading...";
@ -51,11 +51,11 @@ class ChatWindowElement extends HTMLElement {
this.channel = channel; this.channel = channel;
chatTitle.innerText = channel.name; chatTitle.innerText = channel.name;
const channelElement = document.createElement('message-list'); const channelElement = document.createElement("message-list");
channelElement.setAttribute("channel", channel.uid); channelElement.setAttribute("channel", channel.uid);
this.container.appendChild(channelElement); this.container.appendChild(channelElement);
const chatInput = document.createElement('chat-input'); const chatInput = document.createElement("chat-input");
chatInput.chatWindow = this; chatInput.chatWindow = this;
chatInput.addEventListener("submit", (e) => { chatInput.addEventListener("submit", (e) => {
app.rpc.sendMessage(channel.uid, e.detail); app.rpc.sendMessage(channel.uid, e.detail);
@ -65,8 +65,8 @@ class ChatWindowElement extends HTMLElement {
this.component.appendChild(this.container); this.component.appendChild(this.container);
const messages = await app.rpc.getMessages(channel.uid); const messages = await app.rpc.getMessages(channel.uid);
messages.forEach(message => { messages.forEach((message) => {
if (!message['user_nick']) return; if (!message["user_nick"]) return;
channelElement.addMessage(message); channelElement.addMessage(message);
}); });
@ -74,9 +74,9 @@ class ChatWindowElement extends HTMLElement {
channelElement.addEventListener("message", (message) => { channelElement.addEventListener("message", (message) => {
if (me.user.uid !== message.detail.user_uid) app.playSound(0); if (me.user.uid !== message.detail.user_uid) app.playSound(0);
message.detail.element.scrollIntoView({"block": "end"}); message.detail.element.scrollIntoView({ block: "end" });
}); });
} }
} }
customElements.define('chat-window', ChatWindowElement); customElements.define("chat-window", ChatWindowElement);

View File

@ -0,0 +1,150 @@
class DumbTerminal extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host {
--terminal-bg: #111;
--terminal-fg: #0f0;
--terminal-accent: #0ff;
--terminal-font: monospace;
display: block;
background: var(--terminal-bg);
color: var(--terminal-fg);
font-family: var(--terminal-font);
padding: 1rem;
border-radius: 8px;
overflow-y: auto;
max-height: 500px;
}
.output {
white-space: pre-wrap;
margin-bottom: 1em;
}
.input-line {
display: flex;
}
.prompt {
color: var(--terminal-accent);
margin-right: 0.5em;
}
input {
background: transparent;
border: none;
color: var(--terminal-fg);
outline: none;
width: 100%;
font-family: inherit;
font-size: inherit;
}
dialog {
border: none;
background: transparent;
}
.dialog-backdrop {
background: rgba(0, 0, 0, 0.8);
padding: 2rem;
}
</style>
<div class="output" id="output"></div>
<div class="input-line">
<span class="prompt">&gt;</span>
<input type="text" id="input" autocomplete="off" autofocus />
</div>
`;
this.outputEl = this.shadowRoot.getElementById("output");
this.inputEl = this.shadowRoot.getElementById("input");
this.history = [];
this.historyIndex = -1;
this.inputEl.addEventListener("keydown", (e) => this.onKeyDown(e));
}
onKeyDown(event) {
const value = this.inputEl.value;
switch (event.key) {
case "Enter":
this.executeCommand(value);
this.history.push(value);
this.historyIndex = this.history.length;
this.inputEl.value = "";
break;
case "ArrowUp":
if (this.historyIndex > 0) {
this.historyIndex--;
this.inputEl.value = this.history[this.historyIndex];
}
break;
case "ArrowDown":
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
this.inputEl.value = this.history[this.historyIndex];
} else {
this.historyIndex = this.history.length;
this.inputEl.value = "";
}
break;
}
}
executeCommand(command) {
const outputLine = document.createElement("div");
outputLine.textContent = `> ${command}`;
this.outputEl.appendChild(outputLine);
const resultLine = document.createElement("div");
resultLine.textContent = this.mockExecute(command);
this.outputEl.appendChild(resultLine);
this.outputEl.scrollTop = this.outputEl.scrollHeight;
}
mockExecute(command) {
switch (command.trim()) {
case "help":
return "Available commands: help, clear, date";
case "date":
return new Date().toString();
case "clear":
this.outputEl.innerHTML = "";
return "";
default:
return `Unknown command: ${command}`;
}
}
/**
* Static method to create a modal dialog with the terminal
* @returns {HTMLDialogElement}
*/
static createModal() {
const dialog = document.createElement("dialog");
dialog.innerHTML = `
<div class="dialog-backdrop">
<web-terminal></web-terminal>
</div>
`;
document.body.appendChild(dialog);
dialog.showModal();
return dialog;
}
}
customElements.define("web-terminal", WebTerminal);

View File

@ -1,5 +1,3 @@
export class EventHandler { export class EventHandler {
constructor() { constructor() {
this.subscribers = {}; this.subscribers = {};
@ -11,6 +9,7 @@ export class EventHandler {
} }
emit(type, ...data) { emit(type, ...data) {
if (this.subscribers[type]) this.subscribers[type].forEach(handler => handler(...data)); if (this.subscribers[type])
this.subscribers[type].forEach((handler) => handler(...data));
} }
} }

View File

@ -2,25 +2,22 @@
// This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality. // This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality.
// MIT License // MIT License
class FancyButton extends HTMLElement { class FancyButton extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.url = null; this.url = null;
this.type = "button"; this.type = "button";
this.value = null; this.value = null;
} }
connectedCallback() { connectedCallback() {
this.container = document.createElement('span'); this.container = document.createElement("span");
let size = this.getAttribute('size'); let size = this.getAttribute("size");
console.info({ GG: size }); console.info({ GG: size });
size = size === 'auto' ? '1%' : '33%'; size = size === "auto" ? "1%" : "33%";
this.styleElement = document.createElement("style"); this.styleElement = document.createElement("style");
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
@ -50,19 +47,20 @@ class FancyButton extends HTMLElement {
`; `;
this.container.appendChild(this.styleElement); this.container.appendChild(this.styleElement);
this.buttonElement = document.createElement('button'); this.buttonElement = document.createElement("button");
this.container.appendChild(this.buttonElement); this.container.appendChild(this.buttonElement);
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
this.url = this.getAttribute('url'); this.url = this.getAttribute("url");
this.value = this.getAttribute("value");
this.value = this.getAttribute('value'); this.buttonElement.appendChild(
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text"))); document.createTextNode(this.getAttribute("text")),
);
this.buttonElement.addEventListener("click", () => { this.buttonElement.addEventListener("click", () => {
if(this.url == 'submit'){ if (this.url == "submit") {
this.closest('form').submit() this.closest("form").submit();
return return;
} }
if (this.url === "/back" || this.url === "/back/") { if (this.url === "/back" || this.url === "/back/") {

View File

@ -40,19 +40,30 @@ class FileBrowser extends HTMLElement {
<button id="next">Next</button> <button id="next">Next</button>
</nav> </nav>
`; `;
this.shadowRoot.getElementById("up").addEventListener("click", () => this.goUp()); this.shadowRoot
.getElementById("up")
.addEventListener("click", () => this.goUp());
this.shadowRoot.getElementById("prev").addEventListener("click", () => { this.shadowRoot.getElementById("prev").addEventListener("click", () => {
if (this.offset > 0) { this.offset -= this.limit; this.load(); } if (this.offset > 0) {
this.offset -= this.limit;
this.load();
}
}); });
this.shadowRoot.getElementById("next").addEventListener("click", () => { this.shadowRoot.getElementById("next").addEventListener("click", () => {
this.offset += this.limit; this.load(); this.offset += this.limit;
this.load();
}); });
} }
// ---------- Networking ---------------------------------------------- // ---------- Networking ----------------------------------------------
async load() { async load() {
const r = await fetch(`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`); const r = await fetch(
if (!r.ok) { console.error(await r.text()); return; } `/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`,
);
if (!r.ok) {
console.error(await r.text());
return;
}
const data = await r.json(); const data = await r.json();
this.renderTiles(data.items); this.renderTiles(data.items);
this.updateNav(data.pagination); this.updateNav(data.pagination);
@ -62,13 +73,17 @@ class FileBrowser extends HTMLElement {
renderTiles(items) { renderTiles(items) {
const grid = this.shadowRoot.getElementById("grid"); const grid = this.shadowRoot.getElementById("grid");
grid.innerHTML = ""; grid.innerHTML = "";
items.forEach(item => { items.forEach((item) => {
const tile = document.createElement("div"); const tile = document.createElement("div");
tile.className = "tile"; tile.className = "tile";
if (item.type === "directory") { if (item.type === "directory") {
tile.innerHTML = `<div class="icon">📂</div><div>${item.name}</div>`; tile.innerHTML = `<div class="icon">📂</div><div>${item.name}</div>`;
tile.addEventListener("click", () => { this.path = item.path; this.offset = 0; this.load(); }); tile.addEventListener("click", () => {
this.path = item.path;
this.offset = 0;
this.load();
});
} else { } else {
if (item.mimetype?.startsWith("image/")) { if (item.mimetype?.startsWith("image/")) {
tile.innerHTML = `<img class="thumb" src="${item.url}" alt="${item.name}"><div>${item.name}</div>`; tile.innerHTML = `<img class="thumb" src="${item.url}" alt="${item.name}"><div>${item.name}</div>`;

View File

@ -40,7 +40,7 @@ class GenericField extends HTMLElement {
} }
set value(val) { set value(val) {
val = val ?? ''; val = val ?? "";
this.inputElement.value = val; this.inputElement.value = val;
this.inputElement.setAttribute("value", val); this.inputElement.setAttribute("value", val);
} }
@ -62,9 +62,9 @@ class GenericField extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({mode: 'open'}); this.attachShadow({ mode: "open" });
this.container = document.createElement('div'); this.container = document.createElement("div");
this.styleElement = document.createElement('style'); this.styleElement = document.createElement("style");
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
h1 { h1 {
@ -174,7 +174,7 @@ class GenericField extends HTMLElement {
if (this.inputElement == null && this.field) { if (this.inputElement == null && this.field) {
this.inputElement = document.createElement(this.field.tag); this.inputElement = document.createElement(this.field.tag);
if (this.field.tag === 'button' && this.field.value === "submit") { if (this.field.tag === "button" && this.field.value === "submit") {
this.action = this.field.value; this.action = this.field.value;
} }
this.inputElement.name = this.field.name; this.inputElement.name = this.field.name;
@ -182,13 +182,19 @@ class GenericField extends HTMLElement {
const me = this; const me = this;
this.inputElement.addEventListener("keyup", (e) => { this.inputElement.addEventListener("keyup", (e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
const event = new CustomEvent("change", {detail: me, bubbles: true}); const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
me.dispatchEvent(event); me.dispatchEvent(event);
me.dispatchEvent(new Event("submit")); me.dispatchEvent(new Event("submit"));
} else if (me.field.value !== e.target.value) { } else if (me.field.value !== e.target.value) {
const event = new CustomEvent("change", {detail: me, bubbles: true}); const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
me.dispatchEvent(event); me.dispatchEvent(event);
} }
}); });
@ -198,10 +204,17 @@ class GenericField extends HTMLElement {
me.dispatchEvent(event); me.dispatchEvent(event);
}); });
this.inputElement.addEventListener("blur", (e) => { this.inputElement.addEventListener(
const event = new CustomEvent("change", {detail: me, bubbles: true}); "blur",
(e) => {
const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
me.dispatchEvent(event); me.dispatchEvent(event);
}, true); },
true,
);
this.container.appendChild(this.inputElement); this.container.appendChild(this.inputElement);
} }
@ -210,8 +223,8 @@ class GenericField extends HTMLElement {
return; return;
} }
this.inputElement.setAttribute("type", this.field.type ?? 'input'); this.inputElement.setAttribute("type", this.field.type ?? "input");
this.inputElement.setAttribute("name", this.field.name ?? ''); this.inputElement.setAttribute("name", this.field.name ?? "");
if (this.field.text != null) { if (this.field.text != null) {
this.inputElement.innerText = this.field.text; this.inputElement.innerText = this.field.text;
@ -239,14 +252,14 @@ class GenericField extends HTMLElement {
this.inputElement.removeAttribute("required"); this.inputElement.removeAttribute("required");
} }
if (!this.footerElement) { if (!this.footerElement) {
this.footerElement = document.createElement('div'); this.footerElement = document.createElement("div");
this.footerElement.style.clear = 'both'; this.footerElement.style.clear = "both";
this.container.appendChild(this.footerElement); this.container.appendChild(this.footerElement);
} }
} }
} }
customElements.define('generic-field', GenericField); customElements.define("generic-field", GenericField);
class GenericForm extends HTMLElement { class GenericForm extends HTMLElement {
fields = {}; fields = {};
@ -254,7 +267,7 @@ class GenericForm extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({mode: 'open'}); this.attachShadow({ mode: "open" });
this.styleElement = document.createElement("style"); this.styleElement = document.createElement("style");
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
@ -281,27 +294,29 @@ class GenericForm extends HTMLElement {
} }
}`; }`;
this.container = document.createElement('div'); this.container = document.createElement("div");
this.container.appendChild(this.styleElement); this.container.appendChild(this.styleElement);
this.container.classList.add("generic-form-container"); this.container.classList.add("generic-form-container");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
connectedCallback() { connectedCallback() {
const preloadedForm = this.getAttribute('preloaded-structure'); const preloadedForm = this.getAttribute("preloaded-structure");
if (preloadedForm) { if (preloadedForm) {
try { try {
const form = JSON.parse(preloadedForm); const form = JSON.parse(preloadedForm);
this.constructForm(form) this.constructForm(form);
} catch (error) { } catch (error) {
console.error(error, preloadedForm); console.error(error, preloadedForm);
} }
} }
const url = this.getAttribute('url'); const url = this.getAttribute("url");
if (url) { if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get"); const fullUrl = url.startsWith("/")
? window.location.origin + url
: new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) { if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url); fullUrl.searchParams.set("url", url);
} }
this.loadForm(fullUrl.toString()); this.loadForm(fullUrl.toString());
} else { } else {
@ -318,10 +333,10 @@ class GenericForm extends HTMLElement {
let hasAutoFocus = Object.keys(this.fields).length !== 0; let hasAutoFocus = Object.keys(this.fields).length !== 0;
fields.sort((a, b) => a.index - b.index); fields.sort((a, b) => a.index - b.index);
fields.forEach(field => { fields.forEach((field) => {
const updatingField = field.name in this.fields const updatingField = field.name in this.fields;
this.fields[field.name] ??= document.createElement('generic-field'); this.fields[field.name] ??= document.createElement("generic-field");
const fieldElement = this.fields[field.name]; const fieldElement = this.fields[field.name];
@ -362,7 +377,7 @@ class GenericForm extends HTMLElement {
window.location.pathname = saveResult.redirect_url; window.location.pathname = saveResult.redirect_url;
} }
} }
}) });
} }
}); });
} catch (error) { } catch (error) {
@ -374,7 +389,9 @@ class GenericForm extends HTMLElement {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); throw new Error(
`Failed to fetch: ${response.status} ${response.statusText}`,
);
} }
await this.constructForm(await response.json()); await this.constructForm(await response.json());
@ -387,15 +404,15 @@ class GenericForm extends HTMLElement {
const url = this.getAttribute("url"); const url = this.getAttribute("url");
let response = await fetch(url, { let response = await fetch(url, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({"action": "validate", "form": this.form}) body: JSON.stringify({ action: "validate", form: this.form }),
}); });
const form = await response.json(); const form = await response.json();
Object.values(form.fields).forEach(field => { Object.values(form.fields).forEach((field) => {
if (!this.form.fields[field.name]) { if (!this.form.fields[field.name]) {
return; return;
} }
@ -409,23 +426,23 @@ class GenericForm extends HTMLElement {
this.fields[field.name].setAttribute("field", field); this.fields[field.name].setAttribute("field", field);
this.fields[field.name].updateAttributes(); this.fields[field.name].updateAttributes();
}); });
Object.values(form.fields).forEach(field => { Object.values(form.fields).forEach((field) => {
this.fields[field.name].setErrors(field.errors); this.fields[field.name].setErrors(field.errors);
}); });
return form['is_valid']; return form["is_valid"];
} }
async submit() { async submit() {
const url = this.getAttribute("url"); const url = this.getAttribute("url");
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({"action": "submit", "form": this.form}) body: JSON.stringify({ action: "submit", form: this.form }),
}); });
return await response.json(); return await response.json();
} }
} }
customElements.define('generic-form', GenericForm); customElements.define("generic-form", GenericForm);

View File

@ -9,21 +9,23 @@
class HTMLFrame extends HTMLElement { class HTMLFrame extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.container = document.createElement('div'); this.container = document.createElement("div");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
connectedCallback() { connectedCallback() {
this.container.classList.add("html_frame"); this.container.classList.add("html_frame");
let url = this.getAttribute('url'); let url = this.getAttribute("url");
if (!url.startsWith("https")) { if (!url.startsWith("https")) {
url = "https://" + url; url = "https://" + url;
} }
if (url) { if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get"); const fullUrl = url.startsWith("/")
? window.location.origin + url
: new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) { if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url); fullUrl.searchParams.set("url", url);
} }
this.loadAndRender(fullUrl.toString()); this.loadAndRender(fullUrl.toString());
} else { } else {
@ -39,7 +41,7 @@ class HTMLFrame extends HTMLElement {
} }
const html = await response.text(); const html = await response.text();
if (url.endsWith(".md")) { if (url.endsWith(".md")) {
const markdownElement = document.createElement('div'); const markdownElement = document.createElement("div");
markdownElement.innerHTML = html; markdownElement.innerHTML = html;
this.outerHTML = html; this.outerHTML = html;
} else { } else {
@ -51,4 +53,4 @@ class HTMLFrame extends HTMLElement {
} }
} }
customElements.define('html-frame', HTMLFrame); customElements.define("html-frame", HTMLFrame);

View File

@ -12,22 +12,22 @@
class HTMLFrame extends HTMLElement { class HTMLFrame extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.container = document.createElement('div'); this.container = document.createElement("div");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
connectedCallback() { connectedCallback() {
this.container.classList.add('html_frame'); this.container.classList.add("html_frame");
const url = this.getAttribute('url'); const url = this.getAttribute("url");
if (url) { if (url) {
const fullUrl = url.startsWith('/') const fullUrl = url.startsWith("/")
? window.location.origin + url ? window.location.origin + url
: new URL(window.location.origin + '/http-get'); : new URL(window.location.origin + "/http-get");
if (!url.startsWith('/')) fullUrl.searchParams.set('url', url); if (!url.startsWith("/")) fullUrl.searchParams.set("url", url);
this.loadAndRender(fullUrl.toString()); this.loadAndRender(fullUrl.toString());
} else { } else {
this.container.textContent = 'No source URL!'; this.container.textContent = "No source URL!";
} }
} }
@ -45,4 +45,4 @@ class HTMLFrame extends HTMLElement {
} }
} }
customElements.define('markdown-frame', HTMLFrame); customElements.define("markdown-frame", HTMLFrame);

View File

@ -4,7 +4,6 @@
// No external libraries or dependencies are used other than standard web components. // No external libraries or dependencies are used other than standard web components.
// MIT License // MIT License
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
@ -13,19 +12,18 @@
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class TileGridElement extends HTMLElement { class TileGridElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.gridId = this.getAttribute('grid'); this.gridId = this.getAttribute("grid");
this.component = document.createElement('div'); this.component = document.createElement("div");
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
connectedCallback() { connectedCallback() {
console.log('connected'); console.log("connected");
this.styleElement = document.createElement('style'); this.styleElement = document.createElement("style");
this.styleElement.textContent = ` this.styleElement.textContent = `
.grid { .grid {
padding: 10px; padding: 10px;
@ -48,26 +46,26 @@ class TileGridElement extends HTMLElement {
} }
`; `;
this.component.appendChild(this.styleElement); this.component.appendChild(this.styleElement);
this.container = document.createElement('div'); this.container = document.createElement("div");
this.container.classList.add('gallery'); this.container.classList.add("gallery");
this.component.appendChild(this.container); this.component.appendChild(this.container);
} }
addImage(src) { addImage(src) {
const item = document.createElement('img'); const item = document.createElement("img");
item.src = src; item.src = src;
item.classList.add('tile'); item.classList.add("tile");
item.style.width = '100px'; item.style.width = "100px";
item.style.height = '100px'; item.style.height = "100px";
this.container.appendChild(item); this.container.appendChild(item);
} }
addImages(srcs) { addImages(srcs) {
srcs.forEach(src => this.addImage(src)); srcs.forEach((src) => this.addImage(src));
} }
addElement(element) { addElement(element) {
element.classList.add('tile'); element.classList.add("tile");
this.container.appendChild(element); this.container.appendChild(element);
} }
} }
@ -75,14 +73,14 @@ class TileGridElement extends HTMLElement {
class UploadButton extends HTMLElement { class UploadButton extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.component = document.createElement('div'); this.component = document.createElement("div");
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
window.u = this; window.u = this;
} }
get gridSelector() { get gridSelector() {
return this.getAttribute('grid'); return this.getAttribute("grid");
} }
grid = null; grid = null;
@ -91,8 +89,8 @@ class UploadButton extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
console.log('connected'); console.log("connected");
this.styleElement = document.createElement('style'); this.styleElement = document.createElement("style");
this.styleElement.textContent = ` this.styleElement.textContent = `
.upload-button { .upload-button {
display: flex; display: flex;
@ -116,14 +114,14 @@ class UploadButton extends HTMLElement {
} }
`; `;
this.shadowRoot.appendChild(this.styleElement); this.shadowRoot.appendChild(this.styleElement);
this.container = document.createElement('div'); this.container = document.createElement("div");
this.container.classList.add('upload-button'); this.container.classList.add("upload-button");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
const input = document.createElement('input'); const input = document.createElement("input");
input.type = 'file'; input.type = "file";
input.accept = 'image/*'; input.accept = "image/*";
input.multiple = true; input.multiple = true;
input.addEventListener('change', (e) => { input.addEventListener("change", (e) => {
const files = e.target.files; const files = e.target.files;
const urls = []; const urls = [];
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
@ -137,39 +135,39 @@ class UploadButton extends HTMLElement {
reader.readAsDataURL(files[i]); reader.readAsDataURL(files[i]);
} }
}); });
const label = document.createElement('label'); const label = document.createElement("label");
label.textContent = 'Upload Images'; label.textContent = "Upload Images";
label.appendChild(input); label.appendChild(input);
this.container.appendChild(label); this.container.appendChild(label);
} }
} }
customElements.define('upload-button', UploadButton); customElements.define("upload-button", UploadButton);
customElements.define('tile-grid', TileGridElement); customElements.define("tile-grid", TileGridElement);
class MeniaUploadElement extends HTMLElement { class MeniaUploadElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.component = document.createElement("div"); this.component = document.createElement("div");
alert('aaaa'); alert("aaaa");
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
connectedCallback() { connectedCallback() {
this.container = document.createElement("div"); this.container = document.createElement("div");
this.component.style.height = '100%'; this.component.style.height = "100%";
this.component.style.backgroundColor = 'blue'; this.component.style.backgroundColor = "blue";
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
this.tileElement = document.createElement("tile-grid"); this.tileElement = document.createElement("tile-grid");
this.tileElement.style.backgroundColor = 'red'; this.tileElement.style.backgroundColor = "red";
this.tileElement.style.height = '100%'; this.tileElement.style.height = "100%";
this.component.appendChild(this.tileElement); this.component.appendChild(this.tileElement);
this.uploadButton = document.createElement('upload-button'); this.uploadButton = document.createElement("upload-button");
this.component.appendChild(this.uploadButton); this.component.appendChild(this.uploadButton);
} }
} }
customElements.define('menia-upload', MeniaUploadElement); customElements.define("menia-upload", MeniaUploadElement);

View File

@ -22,18 +22,17 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
class MessageListManagerElement extends HTMLElement { class MessageListManagerElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.container = document.createElement("div"); this.container = document.createElement("div");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
async connectedCallback() { async connectedCallback() {
const channels = await app.rpc.getChannels(); const channels = await app.rpc.getChannels();
channels.forEach(channel => { channels.forEach((channel) => {
const messageList = document.createElement("message-list"); const messageList = document.createElement("message-list");
messageList.setAttribute("channel", channel.uid); messageList.setAttribute("channel", channel.uid);
this.container.appendChild(messageList); this.container.appendChild(messageList);

View File

@ -5,50 +5,46 @@
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application. // The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty. // MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
import {app} from '../app.js' import { app } from "../app.js";
class MessageList extends HTMLElement { class MessageList extends HTMLElement {
constructor() { constructor() {
super(); super();
app.ws.addEventListener("update_message_text", (data) => { app.ws.addEventListener("update_message_text", (data) => {
this.updateMessageText(data.data.uid,data.data) this.updateMessageText(data.uid, data);
}) });
app.ws.addEventListener("set_typing", (data) => { app.ws.addEventListener("set_typing", (data) => {
this.triggerGlow(data.data.user_uid) this.triggerGlow(data.user_uid);
});
})
this.items = []; this.items = [];
} }
updateMessageText(uid, message) { updateMessageText(uid, message) {
const messageDiv = this.querySelector("div[data-uid=\""+uid+"\"]") const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
if (!messageDiv) { if (!messageDiv) {
return return;
} }
const receivedHtml = document.createElement("div") const receivedHtml = document.createElement("div");
receivedHtml.innerHTML = message.html receivedHtml.innerHTML = message.html;
const html = receivedHtml.querySelector(".text").innerHTML const html = receivedHtml.querySelector(".text").innerHTML;
const textElement = messageDiv.querySelector(".text") const textElement = messageDiv.querySelector(".text");
textElement.innerHTML = html textElement.innerHTML = html;
textElement.style.display = message.text == '' ? 'none' : 'block' textElement.style.display = message.text == "" ? "none" : "block";
} }
triggerGlow(uid) { triggerGlow(uid) {
let lastElement = null; let lastElement = null;
this.querySelectorAll(".avatar").forEach((el) => { this.querySelectorAll(".avatar").forEach((el) => {
const div = el.closest('a'); const div = el.closest("a");
if (el.href.indexOf(uid) != -1) { if (el.href.indexOf(uid) != -1) {
lastElement = el lastElement = el;
} }
});
})
if (lastElement) { if (lastElement) {
lastElement.classList.add("glow") lastElement.classList.add("glow");
setTimeout(() => { setTimeout(() => {
lastElement.classList.remove("glow") lastElement.classList.remove("glow");
},1000) }, 1000);
} }
} }
set data(items) { set data(items) {
@ -56,175 +52,10 @@
this.render(); this.render();
} }
render() { render() {
this.innerHTML = ''; this.innerHTML = "";
//this.insertAdjacentHTML("beforeend", html); //this.insertAdjacentHTML("beforeend", html);
}
}
customElements.define('message-list', MessageList);
class MessageListElementOLD extends HTMLElement {
static get observedAttributes() {
return ["messages"];
}
messages = [];
room = null;
url = null;
container = null;
messageEventSchedule = null;
observer = null;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component);
}
linkifyText(text) {
const urlRegex = /https?:\/\/[^\s]+/g;
return text.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
}
timeAgo(date1, date2) {
const diffMs = Math.abs(date2 - date1);
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
if (days) {
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
}
if (hours) {
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
}
if (minutes) {
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
}
return 'just now';
}
timeDescription(isoDate) {
const date = new Date(isoDate);
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
return timeStr;
}
createElement(message) {
const element = document.createElement("div");
element.dataset.uid = message.uid;
element.dataset.color = message.color;
element.dataset.channel_uid = message.channel_uid;
element.dataset.user_nick = message.user_nick;
element.dataset.created_at = message.created_at;
element.dataset.user_uid = message.user_uid;
element.dataset.message = message.message;
element.classList.add("message");
if (!this.messages.length || this.messages[this.messages.length - 1].user_uid != message.user_uid) {
element.classList.add("switch-user");
}
const avatar = document.createElement("div");
avatar.classList.add("avatar");
avatar.classList.add("no-select");
avatar.style.backgroundColor = message.color;
avatar.style.color = "black";
avatar.innerText = message.user_nick[0];
const messageContent = document.createElement("div");
messageContent.classList.add("message-content");
const author = document.createElement("div");
author.classList.add("author");
author.style.color = message.color;
author.textContent = message.user_nick;
const text = document.createElement("div");
text.classList.add("text");
if (message.html) text.innerHTML = message.html;
const time = document.createElement("div");
time.classList.add("time");
time.dataset.created_at = message.created_at;
time.textContent = this.timeDescription(message.created_at);
messageContent.appendChild(author);
messageContent.appendChild(text);
messageContent.appendChild(time);
element.appendChild(avatar);
element.appendChild(messageContent);
message.element = element;
return element;
}
addMessage(message) {
const obj = new models.Message(
message.uid,
message.channel_uid,
message.user_uid,
message.user_nick,
message.color,
message.message,
message.html,
message.created_at,
message.updated_at
);
const element = this.createElement(obj);
this.messages.push(obj);
this.container.appendChild(element);
this.messageEventSchedule.delay(() => {
this.dispatchEvent(new CustomEvent("message", { detail: obj, bubbles: true }));
});
return obj;
}
scrollBottom() {
this.container.scrollTop = this.container.scrollHeight;
}
connectedCallback() {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/base.css';
this.component.appendChild(link);
this.component.classList.add("chat-messages");
this.container = document.createElement('div');
this.component.appendChild(this.container);
this.messageEventSchedule = new Schedule(500);
this.messages = [];
this.channel_uid = this.getAttribute("channel");
app.addEventListener(this.channel_uid, (data) => {
this.addMessage(data);
});
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }));
this.timeUpdateInterval = setInterval(() => {
this.messages.forEach((message) => {
const newText = this.timeDescription(message.created_at);
if (newText != message.element.innerText) {
message.element.querySelector(".time").innerText = newText;
}
});
}, 30000);
} }
} }
//customElements.define('message-list', MessageListElement); customElements.define("message-list", MessageList);

View File

@ -7,20 +7,30 @@
// MIT License // MIT License
class MessageModel { class MessageModel {
constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) { constructor(
this.uid = uid uid,
this.message = message channel_uid,
this.html = html user_uid,
this.user_uid = user_uid user_nick,
this.user_nick = user_nick color,
this.color = color message,
this.channel_uid = channel_uid html,
this.created_at = created_at created_at,
this.updated_at = updated_at updated_at,
this.element = null ) {
this.uid = uid;
this.message = message;
this.html = html;
this.user_uid = user_uid;
this.user_nick = user_nick;
this.color = color;
this.channel_uid = channel_uid;
this.created_at = created_at;
this.updated_at = updated_at;
this.element = null;
} }
} }
const models = { const models = {
Message: MessageModel Message: MessageModel,
} };

View File

@ -1 +0,0 @@

View File

@ -12,13 +12,17 @@ this.onpush = (event) => {
const subscriptionObject = { const subscriptionObject = {
endpoint: pushSubscription.endpoint, endpoint: pushSubscription.endpoint,
keys: { keys: {
p256dh: pushSubscription.getKey('p256dh'), p256dh: pushSubscription.getKey("p256dh"),
auth: pushSubscription.getKey('auth'), auth: pushSubscription.getKey("auth"),
}, },
encoding: PushManager.supportedContentEncodings, encoding: PushManager.supportedContentEncodings,
/* other app-specific data, such as user identity */ /* other app-specific data, such as user identity */
}; };
console.log(pushSubscription.endpoint, pushSubscription, subscriptionObject); console.log(
pushSubscription.endpoint,
pushSubscription,
subscriptionObject,
);
// The push subscription details needed by the application // The push subscription details needed by the application
// server are now available, and can be sent to it using, // server are now available, and can be sent to it using,
// for example, the fetch() API. // for example, the fetch() API.

View File

@ -1,33 +1,34 @@
async function requestNotificationPermission() { async function requestNotificationPermission() {
const permission = await Notification.requestPermission(); const permission = await Notification.requestPermission();
return permission === 'granted'; return permission === "granted";
} }
// Subscribe to Push Notifications // Subscribe to Push Notifications
async function subscribeUser() { async function subscribeUser() {
const registration = await navigator.serviceWorker.register('/service-worker.js'); const registration =
await navigator.serviceWorker.register("/service-worker.js");
const subscription = await registration.pushManager.subscribe({ const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY) applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
}); });
// Send subscription to your backend // Send subscription to your backend
await fetch('/subscribe', { await fetch("/subscribe", {
method: 'POST', method: "POST",
body: JSON.stringify(subscription), body: JSON.stringify(subscription),
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}); });
} }
// Service Worker (service-worker.js) // Service Worker (service-worker.js)
self.addEventListener('push', event => { self.addEventListener("push", (event) => {
const data = event.data.json(); const data = event.data.json();
self.registration.showNotification(data.title, { self.registration.showNotification(data.title, {
body: data.message, body: data.message,
icon: data.icon icon: data.icon,
}); });
}); });

View File

@ -4,16 +4,16 @@ export class Socket extends EventHandler {
/** /**
* @type {URL} * @type {URL}
*/ */
url url;
/** /**
* @type {WebSocket|null} * @type {WebSocket|null}
*/ */
ws = null ws = null;
/** /**
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}} * @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
*/ */
connection = null connection = null;
shouldReconnect = true; shouldReconnect = true;
@ -28,10 +28,10 @@ export class Socket extends EventHandler {
constructor() { constructor() {
super(); super();
this.url = new URL('/rpc.ws', window.location.origin); this.url = new URL("/rpc.ws", window.location.origin);
this.url.protocol = this.url.protocol.replace('http', 'ws'); this.url.protocol = this.url.protocol.replace("http", "ws");
this.connect() this.connect();
} }
connect() { connect() {
@ -40,7 +40,7 @@ export class Socket extends EventHandler {
} }
if (!this.connection || this.connection.resolved) { if (!this.connection || this.connection.resolved) {
this.connection = Promise.withResolvers() this.connection = Promise.withResolvers();
} }
this.ws = new WebSocket(this.url); this.ws = new WebSocket(this.url);
@ -52,12 +52,12 @@ export class Socket extends EventHandler {
this.ws.addEventListener("close", () => { this.ws.addEventListener("close", () => {
console.log("Connection closed"); console.log("Connection closed");
this.disconnect() this.disconnect();
}) });
this.ws.addEventListener("error", (e) => { this.ws.addEventListener("error", (e) => {
console.error("Connection error", e); console.error("Connection error", e);
this.disconnect() this.disconnect();
}) });
this.ws.addEventListener("message", (e) => { this.ws.addEventListener("message", (e) => {
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) { if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
console.error("Binary data not supported"); console.error("Binary data not supported");
@ -68,10 +68,9 @@ export class Socket extends EventHandler {
console.error("Failed to parse message", e); console.error("Failed to parse message", e);
} }
} }
}) });
} }
onData(data) { onData(data) {
if (data.success !== undefined && !data.success) { if (data.success !== undefined && !data.success) {
console.error(data); console.error(data);
@ -81,12 +80,11 @@ export class Socket extends EventHandler {
} }
if (data.channel_uid) { if (data.channel_uid) {
this.emit(data.channel_uid, data.data); this.emit(data.channel_uid, data.data);
if(!data['event']) if (!data["event"]) this.emit("channel-message", data);
this.emit("channel-message", data);
} }
this.emit("data", data.data) this.emit("data", data.data);
if(data['event']){ if (data["event"]) {
this.emit(data.event, data) this.emit(data.event, data.data);
} }
} }
@ -94,27 +92,30 @@ export class Socket extends EventHandler {
this.ws?.close(); this.ws?.close();
this.ws = null; this.ws = null;
if (this.shouldReconnect) setTimeout(() => { if (this.shouldReconnect)
setTimeout(() => {
console.log("Reconnecting"); console.log("Reconnecting");
return this.connect(); return this.connect();
}, 0); }, 0);
} }
_camelToSnake(str) { _camelToSnake(str) {
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
} }
get client() { get client() {
const me = this; const me = this;
return new Proxy({}, { return new Proxy(
{},
{
get(_, prop) { get(_, prop) {
return (...args) => { return (...args) => {
const functionName = me._camelToSnake(prop); const functionName = me._camelToSnake(prop);
return me.call(functionName, ...args); return me.call(functionName, ...args);
}; };
}, },
}); },
);
} }
generateCallId() { generateCallId() {
@ -122,7 +123,7 @@ export class Socket extends EventHandler {
} }
async sendJson(data) { async sendJson(data) {
await this.connect().then(api => { await this.connect().then((api) => {
api.ws.send(JSON.stringify(data)); api.ws.send(JSON.stringify(data));
}); });
} }
@ -133,9 +134,9 @@ export class Socket extends EventHandler {
method, method,
args, args,
}; };
const me = this const me = this;
return new Promise((resolve) => { return new Promise((resolve) => {
me.addEventListener(call.callId, data => resolve(data)); me.addEventListener(call.callId, (data) => resolve(data));
me.sendJson(call); me.sendJson(call);
}); });
} }

View File

@ -2,18 +2,17 @@
// This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest. // This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest.
// MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
class UploadButtonElement extends HTMLElement { class UploadButtonElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
} }
chatInput = null chatInput = null;
async uploadFiles() { async uploadFiles() {
const fileInput = this.container.querySelector('.file-input'); const fileInput = this.container.querySelector(".file-input");
const uploadButton = this.container.querySelector('.upload-button'); const uploadButton = this.container.querySelector(".upload-button");
if (!fileInput.files.length) { if (!fileInput.files.length) {
return; return;
@ -22,12 +21,12 @@ class UploadButtonElement extends HTMLElement {
const files = fileInput.files; const files = fileInput.files;
const formData = new FormData(); const formData = new FormData();
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]); formData.append("files[]", files[i]);
} }
const request = new XMLHttpRequest(); const request = new XMLHttpRequest();
request.responseType = 'json'; request.responseType = "json";
request.open('POST', `/channel/${this.channelUid}/attachment.bin`, true); request.open("POST", `/channel/${this.channelUid}/attachment.bin`, true);
request.upload.onprogress = function (event) { request.upload.onprogress = function (event) {
if (event.lengthComputable) { if (event.lengthComputable) {
@ -35,27 +34,29 @@ class UploadButtonElement extends HTMLElement {
uploadButton.innerText = `${Math.round(percentComplete)}%`; uploadButton.innerText = `${Math.round(percentComplete)}%`;
} }
}; };
const me = this const me = this;
request.onload = function () { request.onload = function () {
if (request.status === 200) { if (request.status === 200) {
me.dispatchEvent(new CustomEvent('uploaded', { detail: request.response })); me.dispatchEvent(
uploadButton.innerHTML = '📤'; new CustomEvent("uploaded", { detail: request.response }),
);
uploadButton.innerHTML = "📤";
} else { } else {
alert('Upload failed'); alert("Upload failed");
} }
}; };
request.onerror = function () { request.onerror = function () {
alert('Error while uploading.'); alert("Error while uploading.");
}; };
request.send(formData); request.send(formData);
const uploadEvent = new Event('upload',{}); const uploadEvent = new Event("upload", {});
this.dispatchEvent(uploadEvent); this.dispatchEvent(uploadEvent);
} }
channelUid = null channelUid = null;
connectedCallback() { connectedCallback() {
this.styleElement = document.createElement('style'); this.styleElement = document.createElement("style");
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
@ -98,7 +99,7 @@ class UploadButtonElement extends HTMLElement {
} }
`; `;
this.shadowRoot.appendChild(this.styleElement); this.shadowRoot.appendChild(this.styleElement);
this.container = document.createElement('div'); this.container = document.createElement("div");
this.container.innerHTML = ` this.container.innerHTML = `
<div class="upload-container"> <div class="upload-container">
<button class="upload-button"> <button class="upload-button">
@ -108,16 +109,16 @@ class UploadButtonElement extends HTMLElement {
</div> </div>
`; `;
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
this.channelUid = this.getAttribute('channel'); this.channelUid = this.getAttribute("channel");
this.uploadButton = this.container.querySelector('.upload-button'); this.uploadButton = this.container.querySelector(".upload-button");
this.fileInput = this.container.querySelector('.hidden-input'); this.fileInput = this.container.querySelector(".hidden-input");
this.uploadButton.addEventListener('click', () => { this.uploadButton.addEventListener("click", () => {
this.fileInput.click(); this.fileInput.click();
}); });
this.fileInput.addEventListener('change', () => { this.fileInput.addEventListener("change", () => {
this.uploadFiles(); this.uploadFiles();
}); });
} }
} }
customElements.define('upload-button', UploadButtonElement); customElements.define("upload-button", UploadButtonElement);

View File

@ -18,18 +18,18 @@
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24);
if (days > 0) { if (days > 0) {
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${days} day${days > 1 ? 's' : ''} ago`; return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${days} day${days > 1 ? "s" : ""} ago`;
} else if (hours > 0) { } else if (hours > 0) {
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${hours} hour${hours > 1 ? 's' : ''} ago`; return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${hours} hour${hours > 1 ? "s" : ""} ago`;
} else { } else {
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${minutes} min ago`; return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${minutes} min ago`;
} }
} }
render() { render() {
this.innerHTML = ''; this.innerHTML = "";
this.users.forEach(user => { this.users.forEach((user) => {
const html = ` const html = `
<div class="user-list__item" <div class="user-list__item"
data-uid="${user.uid}" data-uid="${user.uid}"
@ -56,4 +56,4 @@
} }
} }
customElements.define('user-list', UserList); customElements.define("user-list", UserList);

View File

@ -11,6 +11,7 @@
{{ message.html }} {{ message.html }}
{% endautoescape %} {% endautoescape %}
{% endfor %} {% endfor %}
<div class class="message-list-bottom"></div>
</message-list> </message-list>
<chat-input live-type="false" channel="{{ channel.uid.value }}"></chat-input> <chat-input live-type="false" channel="{{ channel.uid.value }}"></chat-input>
</section> </section>