{% extends "root.html" %} {% block head %} <title>Chats - {{ config.name }}</title> {% endblock %} {% block body %} {{ macros::nav(selected="chats") }} <nav class="chats_nav"> <button class="flex gap-2 items-center active" onclick="toggle_sidebars(event)" > {{ icon "panel-left" }} {% if community %} <b class="name shorter"> {% if community.context.display_name %} {{ community.context.display_name }} {% else %} {{ community.title }} {% endif %} </b> {% else %} <b>{{ text "chats:label.my_chats" }}</b> {% endif %} </button> </nav> <div class="flex"> <div class="sidebar flex flex-col items-center gap-2" id="community_list" style="width: var(--list-bar-width)" > <a href="/chats/0/0" class="button quaternary channel_icon {% if selected_community == 0 %}selected{% endif %}" data-turbo="false" > {{ icon "message-circle" }} </a> {% for community in communities %} {% if community.id != 0 %} <a href="/chats/{{ community.id }}/0" class="button quaternary channel_icon {% if selected_community == community.id %}selected{% endif %}" data-turbo="false" > {{ components::community_avatar(id=community.id, community=community, size="48px") }} </a> {% endif %} {% endfor %} </div> <div class="sidebar flex flex-col gap-2" id="channels_list"> <div class="title flex justify-between"> {% if community %} <b class="name shorter"> {% if community.context.display_name %} {{ community.context.display_name }} {% else %} {{ community.title }} {% endif %} </b> {% else %} <b>{{ text "chats:label.my_chats" }}</b> {% endif %} <div class="dropdown"> <button class="camo small" onclick="trigger('atto::hooks::dropdown', [event])" exclude="dropdown" > {{ icon "ellipsis" }} </button> <div class="inner"> <a href="/community/{{ selected_community }}"> {{ icon "book-heart" }} <span >{{ text "communities:label.show_community" }}</span > </a> {% if can_manage_channels %} <a href="/community/{{ selected_community }}/manage"> {{ icon "settings" }} <span>{{ text "general:action.manage" }}</span> </a> {% endif %} </div> </div> </div> {% if can_manage_channels %} <a class="button w-full justify-start quaternary" href="/community/{{ selected_community }}/manage#/channels" > {{ icon "plus" }} <span>{{ text "communities:action.create_channel" }}</span> </a> {% endif %} <turbo-frame id="channels_list_frame" src="/chats/{{ selected_community }}/{{ selected_channel }}/_channels" target="_top" ></turbo-frame> </div> {% if channel %} <div class="w-full flex flex-col gap-2" id="stream" style="padding: 1rem"> <turbo-frame id="stream_body_frame" src="/chats/{{ selected_community }}/{{ selected_channel }}/_stream?page={{ page }}" target="_top" ></turbo-frame> <form class="card flex flex-row gap-2" onsubmit="create_message_from_form(event)" > <textarea type="text" name="content" id="content" placeholder="message {{ channel.title }}" required minlength="2" maxlength="2048" style="min-height: 48px !important; height: 48px" ></textarea> <button class="camo send_button" title="Send"> {{ icon "send-horizontal" }} </button> </form> </div> {% endif %} <style> :root { --list-bar-width: 64px; --channels-bar-width: 256px; } html, body { overflow: hidden; } .name.shortest { max-width: 165px; overflow-wrap: normal; } .send_button { width: 48px; height: 48px; } .send_button .icon { width: 2em; height: 2em; } a.channel_icon { width: 48px; height: 48px; min-height: 48px; } a.channel_icon .icon { min-width: 24px; height: 24px; } a.channel_icon.small { width: 24px; height: 24px; min-height: 24px; } a.channel_icon.small .icon { min-width: 12px; height: 12px; } a.channel_icon:has(img) { padding: 0; } a.channel_icon img { min-width: 48px; min-height: 48px; } a.channel_icon img, a.channel_icon:has(.icon) { transition: outline 0.25s, background 0.15s !important; } a.channel_icon:not(.selected):hover img, a.channel_icon:not(.selected):hover:has(.icon) { outline: solid 1px var(--color-text); } a.channel_icon.selected img, a.channel_icon.selected:has(.icon) { outline: solid 2px var(--color-text); } nav { background: var(--color-raised); color: var(--color-text-raised) !important; height: 42px; position: sticky !important; } nav::after { display: block; position: absolute; background: var(--color-super-lowered); height: 1px; width: calc(100% - var(--list-bar-width)); bottom: 0; left: var(--list-bar-width); content: ""; } nav .content_container { max-width: 100% !important; width: 100%; } .chats_nav { display: none; padding: 0; } .chats_nav button { justify-content: flex-start; width: 100% !important; flex-direction: row !important; font-size: 16px !important; margin-top: -4px; } .chats_nav button svg { margin-right: 1rem; } .sidebar { background: var(--color-raised); color: var(--color-text-raised); border-right: solid 1px var(--color-super-lowered); padding: 0.4rem; width: max-content; height: calc(100dvh - 42px); overflow: auto; transition: left 0.15s; } .sidebar .title { padding: 1rem; border-bottom: solid 1px var(--color-super-lowered); } .sidebar#channels_list { width: var(--channels-bar-width); background: var(--color-surface); color: var(--color-text); } #stream { width: calc( 100dvw - var(--list-bar-width) - var(--channels-bar-width) ) !important; height: calc(100dvh - 42px); } .message { transition: background 0.15s; box-shadow: none; } .message:hover { background: var(--color-raised); } .message:hover .hidden, .message:focus .hidden, .message:active .hidden { display: flex !important; } turbo-frame { display: contents; } @media screen and (max-width: 900px) { body:not(.sidebars_shown) .sidebar { position: absolute; left: -200%; } body.sidebars_shown .sidebar { position: absolute; } #stream { width: 100dvw !important; height: calc(100dvh - 42px * 2); } nav::after { width: 100dvw; left: 0; } .chats_nav { display: flex; } } </style> <script> window.CURRENT_PAGE = Number.parseInt("{{ page }}"); window.CHAT_PROPS = { selected_community: "{{ selected_community }}", selected_channel: "{{ selected_channel }}", membership_role: Number.parseInt("{{ membership_role }}"), }; window.SIDEBARS_OPEN = false; if (new URLSearchParams(window.location.search).get("nav") === "true") { window.SIDEBARS_OPEN = true; } if ( window.SIDEBARS_OPEN && !document.body.classList.contains("sidebars_shown") ) { toggle_sidebars(); window.SIDEBARS_OPEN = true; } for (const anchor of document.querySelectorAll("[data-turbo=false]")) { anchor.href += `?nav=${window.SIDEBARS_OPEN}`; } function toggle_sidebars() { window.SIDEBARS_OPEN = !window.SIDEBARS_OPEN; for (const anchor of document.querySelectorAll( "[data-turbo=false]", )) { anchor.href = anchor.href.replace( `?nav=${!window.SIDEBARS_OPEN}`, `?nav=${window.SIDEBARS_OPEN}`, ); } const community_list = document.getElementById("community_list"); const channels_list = document.getElementById("channels_list"); if (document.body.classList.contains("sidebars_shown")) { // hide document.body.classList.remove("sidebars_shown"); community_list.style.left = "-200%"; channels_list.style.left = "-200%"; } else { // show document.body.classList.add("sidebars_shown"); community_list.style.left = "0"; channels_list.style.left = "var(--list-bar-width)"; } } globalThis.kick_member = async (cid, uid) => { if ( !(await trigger("atto::confirm", [ "Are you sure you would like to do this?", ])) ) { return; } fetch(`/api/v1/channels/${cid}/kick`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ member: uid, }), }) .then((res) => res.json()) .then((res) => { trigger("atto::toast", [ res.ok ? "success" : "error", res.message, ]); }); }; globalThis.delete_channel = async (id) => { if ( !(await trigger("atto::confirm", [ "Are you sure you would like to do this?", ])) ) { return; } fetch(`/api/v1/channels/${id}`, { method: "DELETE", }) .then((res) => res.json()) .then((res) => { trigger("atto::toast", [ res.ok ? "success" : "error", res.message, ]); }); }; </script> <script id="socket_init" data-turbo-permanent="true"> globalThis.socket_init = () => { if (window.socket) { window.socket.send("Close"); window.socket.close(); window.socket = undefined; console.log("closed lingering"); } if (window.CHAT_PROPS.selected_community !== "0") { const endpoint = `${window.location.origin.replace("http", "ws")}/api/v1/_connect/${window.CHAT_PROPS.selected_community}`; const socket = new WebSocket(endpoint); window.socket = socket; window.socket_id = window.CHAT_PROPS.selected_community; } else { const endpoint = `${window.location.origin.replace("http", "ws")}/api/v1/_connect/${window.CHAT_PROPS.selected_channel}`; const socket = new WebSocket(endpoint); window.socket = socket; window.socket_id = window.CHAT_PROPS.selected_channel; } window.socket.addEventListener("open", () => { // auth window.socket.send( JSON.stringify({ method: "Headers", data: JSON.stringify({ // SocketHeaders user: "{{ user.id }}", is_channel: window.SUBSCRIBE_CHANNEL, }), }), ); }); setTimeout(() => { window.socket.addEventListener("message", async (event) => { if (event.data === "Ping") { return socket.send("Pong"); } const msg = JSON.parse(event.data); const [channel_id, data] = JSON.parse(msg.data); if (msg.method === "Message" && window.CURRENT_PAGE === 0) { if (channel_id !== window.CHAT_PROPS.selected_channel) { // message not for us... maybe send notification later // something like /api/v1/messages/{id}/mark_unread return; } if (document.getElementById("stream_body")) { const element = document.createElement("div"); element.style.display = "contents"; element.innerHTML = await ( await fetch( `/chats/${window.CHAT_PROPS.selected_community}/${window.CHAT_PROPS.selected_channel}/_render`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ data: msg.data, }), }, ) ).text(); document .getElementById("stream_body") .prepend(element); clean_text(); } else { console.log("abandoned remote"); socket.close(); } } else if (msg.method === "Delete") { if (document.getElementById(`message-${data.id}`)) { document .getElementById(`message-${data.id}`) .remove(); } } }); globalThis.create_message_from_form = async (e) => { e.preventDefault(); await trigger("atto::debounce", ["messages::create"]); fetch("/api/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: e.target.content.value, channel: window.CHAT_PROPS.selected_channel, }), }) .then((res) => res.json()) .then((res) => { if (!res.ok) { trigger("atto::toast", ["error", res.message]); } e.target.reset(); }); }; globalThis.delete_message = async (id) => { if ( !(await trigger("atto::confirm", [ "Are you sure you would like to do this?", ])) ) { return; } fetch(`/api/v1/messages/${id}`, { method: "DELETE", }) .then((res) => res.json()) .then((res) => { trigger("atto::toast", [ res.ok ? "success" : "error", res.message, ]); }); }; const clean_text = () => { trigger("atto::clean_date_codes"); trigger("atto::hooks::online_indicator"); }; document.addEventListener( "turbo:before-frame-render", (event) => { setTimeout(clean_text, 50); }, ); setTimeout(clean_text, 150); }, 250); }; </script> {% if selected_channel %} <script> window.SUBSCRIBE_CHANNEL = "{{ selected_community }}" === "0"; setTimeout(() => { if (!window.SUBSCRIBE_CHANNEL) { // sub community if (window.socket_id !== "{{ selected_community }}") { socket_init(); } } else { // sub channel if (window.socket_id !== "{{ selected_channel }}") { socket_init(); } } }, 100); </script> {% endif %} </div> {% endblock %}