(text "{% extends \"root.html\" %} {% block head %}") (title (text "Chats - {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"chats\", hide_user_menu=true) }}") (nav ("class" "chats_nav") (button ("class" "flex gap-2 items-center active") ("onclick" "toggle_sidebars(event)") (text "{{ icon \"panel-left\" }} {% if community -%}") (b ("class" "name shorter") (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) (text "{% else %}") (b (text "{{ text \"chats:label.my_chats\" }}")) (text "{%- endif %}"))) (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") (text "{{ icon \"message-circle\" }}")) (text "{% 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") (text "{{ components::community_avatar(id=community.id, community=community, size=\"48px\") }}")) (text "{%- endif %} {% endfor %}")) (div ("class" "sidebar flex flex-col gap-2 justify-between") ("id" "channels_list") (div ("class" "flex flex-col gap-2 w-full") (div ("class" "title flex items-center justify-between channel_header") (text "{% if community -%}") (b ("class" "name shorter") (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) (text "{% else %}") (b (text "{{ text \"chats:label.my_chats\" }}")) (text "{%- endif %} {% if selected_community != 0 -%}") (div ("class" "dropdown") (button ("class" "camo small") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") (a ("href" "/community/{{ selected_community }}") (text "{{ icon \"book-heart\" }}") (span (text "{{ text \"communities:label.show_community\" }}"))) (text "{% if can_manage_channels -%}") (a ("href" "/community/{{ selected_community }}/manage") (text "{{ icon \"settings\" }}") (span (text "{{ text \"general:action.manage\" }}"))) (text "{%- endif %}"))) (text "{%- endif %}")) (text "{% if can_manage_channels -%}") (a ("class" "button w-full justify-start quaternary") ("href" "/community/{{ selected_community }}/manage#/channels") (text "{{ icon \"plus\" }}") (span (text "{{ text \"communities:action.create_channel\" }}"))) (text "{%- endif %}") (turbo-frame ("id" "channels_list_frame") ("src" "/chats/{{ selected_community }}/{{ selected_channel }}/_channels") ("target" "_top"))) (text "{{ components::user_plate(user=user, show_menu=true) }}")) (text "{% 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 }}&message={{ message }}")) (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")) (button ("class" "camo send_button") ("title" "Send") (text "{{ icon \"send-horizontal\" }}")))) (text "{%- endif %}") (style (text ":root { --list-bar-width: 64px; --channels-bar-width: 256px; --sidebar-height: calc(100dvh - 42px); --channel-header-height: 48px; } 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: var(--sidebar-height); overflow: auto; transition: left 0.15s; z-index: 1; } .sidebar .title:not(.dropdown *) { 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: var(--sidebar-height); } .message { transition: background 0.15s; box-shadow: none; position: relative; } .message:hover { background: var(--color-raised); } .message:hover .hidden, .message:focus .hidden, .message:active .hidden { display: flex !important; } .message.grouped { padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 42px); } turbo-frame { display: contents; } .channel_header { height: var(--channel-header-height); } .members_list_half { padding-top: 1rem; border-top: solid 1px var(--color-super-lowered); } .channels_list_half:not(.no_members), .members_list_half { overflow: auto; height: calc( (var(--sidebar-height) - var(--channel-header-height) - 8rem) / 2 ); } @media screen and (max-width: 900px) { :root { --sidebar-height: calc(100dvh - 42px * 2); } .message.grouped { padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 31px); } body:not(.sidebars_shown) .sidebar { position: absolute; left: -200%; } body.sidebars_shown .sidebar { position: absolute; } #stream { width: 100dvw !important; height: var(--sidebar-height); } nav::after { width: 100dvw; left: 0; } .chats_nav { display: flex; } }")) (script (text "window.CURRENT_PAGE = Number.parseInt(\"{{ page }}\"); window.VIEWING_SINGLE = \"{{ message }}\".length > 0; 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 mention_user(username) { document.getElementById(\"content\").value += ` @${username} `; } 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.add_member = async (id) => { await trigger(\"atto::debounce\", [\"channels::add_member\"]); const member = await trigger(\"atto::prompt\", [\"Member username:\"]); if (!member) { return; } fetch(`/api/v1/channels/${id}/add`, { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ member, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); }); }; globalThis.update_channel_title = async (id) => { await trigger(\"atto::debounce\", [\"channels::update_title\"]); const title = await trigger(\"atto::prompt\", [\"New channel title:\"]); if (!title) { return; } fetch(`/api/v1/channels/${id}/title`, { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ title, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); }); }; 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 ("id" "socket_init") ("data-turbo-permanent" "true") (text "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; } if (window.CHANNEL_NOTIFS_INTERVAL) { window.clearInterval(window.CHANNEL_NOTIFS_INTERVAL); } window.CHANNEL_NOTIFS_INTERVAL = setInterval(() => { if (!window.CHAT_PROPS.selected_channel) { return; } if (!window.location.href.includes(\"{{ selected_channel }}\")) { window.clearInterval(window.CHANNEL_NOTIFS_INTERVAL); return; } fetch( `/api/v1/notifications/tag/chats/${window.CHAT_PROPS.selected_channel}`, { method: \"DELETE\" }, ); }, 10000); 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.LAST_MESSAGE_AUTHOR_ID = null; window.socket.addEventListener(\"message\", async (event) => { if (event.data === \"Ping\") { return socket.send(\"Pong\"); } const msg = JSON.parse(event.data); if ( msg.method === \"Message\" && window.CURRENT_PAGE === 0 && window.VIEWING_SINGLE ) { const [channel_id, data] = JSON.parse(msg.data); 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\"; const message_owner = JSON.parse(msg.data)[1].owner; 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, grouped: message_owner === window.LAST_MESSAGE_AUTHOR_ID, }), }, ) ).text(); document .getElementById(\"stream_body\") .prepend(element); clean_text(); window.LAST_MESSAGE_AUTHOR_ID = message_owner; } else { console.log(\"abandoned remote\"); socket.close(); } } else if (msg.method === \"Delete\") { const data = JSON.parse(msg.data); 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.trim(), 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); };")) (text "{% if selected_channel -%}") (script (text "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);")) (text "{%- endif %}")) (text "{% endblock %}")