diff --git a/matrix_iframe/__init__.py b/matrix_iframe/__init__.py index fa98511..6e47493 100644 --- a/matrix_iframe/__init__.py +++ b/matrix_iframe/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, current_app, redirect, url_for, request +from flask import Blueprint, current_app, redirect, url_for, request blueprint = Blueprint( 'matrix_iframe', @@ -8,29 +8,35 @@ blueprint = Blueprint( static_url_path='/static/matrix_iframe', ) - -def init_app(app, server_url='', room_alias='', base_template='layout.html', login_endpoint=None): +def init_app(app, server_url='', room_alias='', login_endpoint=None): """ Register the Matrix chat blueprint with a Flask app. + The blueprint serves static assets (JS + CSS) and injects + matrix_chat_available, matrix_server_url, and matrix_room_alias + into every template context so the host app's layout can mount + the chat overlay widget. + Args: server_url: Matrix homeserver base URL, e.g. "https://matrix.sticknife.com" room_alias: Matrix room alias, e.g. "#sticknife-library:matrix.sticknife.com" - base_template: Jinja2 base template to extend, default "layout.html" login_endpoint: Flask endpoint to redirect unauthenticated users to, e.g. "web.login". If None, no auth guard is applied. """ app.config.setdefault('MATRIX_SERVER_URL', server_url) app.config.setdefault('MATRIX_ROOM_ALIAS', room_alias) - app.config.setdefault('MATRIX_CHAT_BASE', base_template) app.config.setdefault('MATRIX_CHAT_LOGIN_ENDPOINT', login_endpoint) app.register_blueprint(blueprint) @blueprint.app_context_processor def _inject_matrix_chat(): - """Makes matrix_chat_available=True visible to all templates when the blueprint is registered.""" - return {'matrix_chat_available': True} + """Injects matrix chat config into every template when the blueprint is registered.""" + return { + 'matrix_chat_available': True, + 'matrix_server_url': current_app.config.get('MATRIX_SERVER_URL', ''), + 'matrix_room_alias': current_app.config.get('MATRIX_ROOM_ALIAS', ''), + } @blueprint.before_request @@ -46,12 +52,3 @@ def _auth_guard(): pass -@blueprint.route('/chat') -def chat(): - return render_template( - 'matrix_iframe/chat.html', - matrix_server=current_app.config.get('MATRIX_SERVER_URL', ''), - matrix_room_alias=current_app.config.get('MATRIX_ROOM_ALIAS', ''), - base_template=current_app.config.get('MATRIX_CHAT_BASE', 'layout.html'), - page='chat', - ) diff --git a/matrix_iframe/static/matrix_iframe/matrix-chat.css b/matrix_iframe/static/matrix_iframe/matrix-chat.css index 2579ee3..4949315 100644 --- a/matrix_iframe/static/matrix_iframe/matrix-chat.css +++ b/matrix_iframe/static/matrix_iframe/matrix-chat.css @@ -1,144 +1,261 @@ -#matrix-chat-root { - display: flex; - flex-direction: column; - height: calc(100vh - 120px); - padding: 1rem; - gap: 0.75rem; +/* ── Launch button ──────────────────────────────────────────────────────── */ + +.sk-chat-launch { + position: fixed; + left: 18px; + bottom: 18px; + z-index: 1020; + min-height: 46px; + display: inline-flex; + align-items: center; + gap: 10px; + border: 1px solid rgba(126, 207, 255, 0.40); + border-radius: 999px; + color: #0e1f2b; + background: #7ecfff; + box-shadow: 0 16px 38px rgba(0, 0, 0, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.38); + padding: 0 16px 0 12px; + font-weight: 800; + cursor: pointer; + font-size: 0.9rem; + font-family: inherit; + transition: background 140ms ease, box-shadow 140ms ease; } -.matrix-status { - font-size: 0.875rem; - color: #aaa; - min-height: 1.25rem; +.sk-chat-launch:hover { + background: #a8dcff; + box-shadow: 0 20px 44px rgba(0, 0, 0, 0.38), inset 0 1px 0 rgba(255, 255, 255, 0.42); } -.matrix-status-error { - color: #e74c3c; +.sk-chat-launch-icon { + width: 28px; + height: 28px; + border-radius: 50%; + background: rgba(14, 31, 43, 0.14); + display: inline-grid; + place-items: center; + flex-shrink: 0; +} + +.sk-chat-launch svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +/* ── Backdrop ───────────────────────────────────────────────────────────── */ + +.sk-chat-backdrop { + position: fixed; + inset: 0; + z-index: 2000; + display: grid; + place-items: center; + padding: 28px; + background: rgba(5, 10, 12, 0.58); + backdrop-filter: blur(10px); +} + +.sk-chat-backdrop[hidden] { + display: none; +} + +/* ── Shell (modal) ──────────────────────────────────────────────────────── */ + +.sk-chat-shell { + position: relative; + width: min(680px, 100%); + height: min(700px, calc(100vh - 56px)); + display: flex; + flex-direction: column; + gap: 10px; + overflow: hidden; + border: 1px solid rgba(126, 207, 255, 0.18); + border-radius: 18px; + color: #e8f4ff; + background: rgba(10, 20, 30, 0.94); + box-shadow: 0 28px 88px rgba(0, 0, 0, 0.52), inset 0 1px 0 rgba(255, 255, 255, 0.06); + padding: 14px; +} + +/* ── Top bar ────────────────────────────────────────────────────────────── */ + +.sk-chat-topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 40px; + flex-shrink: 0; +} + +.sk-chat-title { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 700; + font-size: 1rem; + color: #7ecfff; +} + +.sk-chat-title svg { + width: 18px; + height: 18px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + flex-shrink: 0; +} + +.sk-chat-close { + background: none; + border: none; + color: #7ecfff; + font-size: 1.4rem; + line-height: 1; + cursor: pointer; + padding: 4px 8px; + border-radius: 6px; + transition: background 120ms ease; + font-family: inherit; +} + +.sk-chat-close:hover { + background: rgba(126, 207, 255, 0.12); +} + +/* ── Status bar ─────────────────────────────────────────────────────────── */ + +.sk-chat-status { + font-size: 0.8rem; + color: #7aa8c4; + min-height: 1rem; + flex-shrink: 0; +} + +.sk-chat-status-error { + color: #e74c3c; } /* ── Message list ───────────────────────────────────────────────────────── */ -.matrix-messages { - flex: 1; - overflow-y: auto; - background: rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 8px; - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.4rem; +.sk-chat-messages { + flex: 1; + overflow-y: auto; + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 207, 255, 0.08); + border-radius: 10px; + padding: 12px; + display: flex; + flex-direction: column; + gap: 6px; + min-height: 0; } -.matrix-message { - display: flex; - align-items: baseline; - gap: 0.5rem; - flex-wrap: nowrap; - font-size: 0.9rem; - line-height: 1.5; - min-width: 0; +.sk-chat-message { + display: flex; + align-items: baseline; + gap: 6px; + font-size: 0.875rem; + line-height: 1.5; + min-width: 0; } -.matrix-sender { - font-weight: 600; - color: #7ecfff; - white-space: nowrap; - flex-shrink: 0; +.sk-chat-sender { + font-weight: 600; + color: #7ecfff; + white-space: nowrap; + flex-shrink: 0; } -.matrix-sender::after { - content: ':'; +.sk-chat-sender::after { + content: ':'; } -.matrix-body { - color: #ddd; - word-break: break-word; - flex: 1; - min-width: 0; +.sk-chat-body { + color: #cce4f5; + word-break: break-word; + flex: 1; + min-width: 0; } -.matrix-time { - font-size: 0.72rem; - color: #555; - white-space: nowrap; - flex-shrink: 0; - align-self: flex-end; - padding-bottom: 1px; +.sk-chat-time { + font-size: 0.7rem; + color: #3a5568; + white-space: nowrap; + flex-shrink: 0; + align-self: flex-end; + padding-bottom: 1px; } /* ── Input bar ──────────────────────────────────────────────────────────── */ -.matrix-input-bar { - display: flex; - gap: 0.5rem; - flex-shrink: 0; +.sk-chat-inputbar { + display: flex; + gap: 8px; + flex-shrink: 0; } -.matrix-input { - flex: 1; - background: rgba(0, 0, 0, 0.3); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 6px; - color: #ddd; - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - outline: none; - transition: border-color 0.15s; +.sk-chat-input { + flex: 1; + background: rgba(0, 0, 0, 0.35); + border: 1px solid rgba(126, 207, 255, 0.18); + border-radius: 8px; + color: #cce4f5; + padding: 8px 12px; + font-size: 0.875rem; + outline: none; + transition: border-color 0.15s; + font-family: inherit; } -.matrix-input:focus { - border-color: rgba(126, 207, 255, 0.4); +.sk-chat-input:focus { + border-color: rgba(126, 207, 255, 0.45); } -.matrix-input::placeholder { - color: #555; +.sk-chat-input::placeholder { + color: #2e4a5e; } -.matrix-send-btn { - background: rgba(126, 207, 255, 0.12); - border: 1px solid rgba(126, 207, 255, 0.3); - border-radius: 6px; - color: #7ecfff; - padding: 0.5rem 1.25rem; - font-size: 0.9rem; - cursor: pointer; - transition: background 0.15s, border-color 0.15s; - white-space: nowrap; +.sk-chat-send { + background: rgba(126, 207, 255, 0.14); + border: 1px solid rgba(126, 207, 255, 0.32); + border-radius: 8px; + color: #7ecfff; + padding: 8px 18px; + font-size: 0.875rem; + cursor: pointer; + font-weight: 600; + transition: background 0.14s, border-color 0.14s; + font-family: inherit; + white-space: nowrap; } -.matrix-send-btn:hover { - background: rgba(126, 207, 255, 0.22); - border-color: rgba(126, 207, 255, 0.5); +.sk-chat-send:hover { + background: rgba(126, 207, 255, 0.24); + border-color: rgba(126, 207, 255, 0.5); } -.matrix-send-btn:active { - background: rgba(126, 207, 255, 0.3); -} +/* ── Responsive ─────────────────────────────────────────────────────────── */ -/* ── Light theme fallback (when host app uses a light base) ─────────────── */ - -@media (prefers-color-scheme: light) { - .matrix-messages { - background: rgba(0, 0, 0, 0.04); - border-color: rgba(0, 0, 0, 0.1); - } - .matrix-sender { color: #1a6fa8; } - .matrix-body { color: #222; } - .matrix-time { color: #aaa; } - .matrix-input { - background: #fff; - border-color: #ccc; - color: #222; - } - .matrix-input:focus { border-color: #1a6fa8; } - .matrix-input::placeholder { color: #aaa; } - .matrix-send-btn { - background: rgba(26, 111, 168, 0.1); - border-color: rgba(26, 111, 168, 0.4); - color: #1a6fa8; - } - .matrix-send-btn:hover { - background: rgba(26, 111, 168, 0.18); - } +@media (max-width: 600px) { + .sk-chat-backdrop { + padding: 0; + align-items: flex-end; + } + .sk-chat-shell { + width: 100%; + height: 85vh; + border-radius: 18px 18px 0 0; + } + .sk-chat-launch { + left: 12px; + bottom: 12px; + } } diff --git a/matrix_iframe/static/matrix_iframe/matrix-chat.js b/matrix_iframe/static/matrix_iframe/matrix-chat.js index 0fe7dd3..bd8915b 100644 --- a/matrix_iframe/static/matrix_iframe/matrix-chat.js +++ b/matrix_iframe/static/matrix_iframe/matrix-chat.js @@ -1,9 +1,11 @@ 'use strict'; (function () { - const root = document.getElementById('matrix-chat-root'); - const SERVER = (root.dataset.matrixServer || '').replace(/\/$/, ''); - const ROOM_ALIAS = root.dataset.matrixRoom || ''; + const cfg = window._matrixChat || {}; + const SERVER = (cfg.server || '').replace(/\/$/, ''); + const ROOM_ALIAS = cfg.room || ''; + if (!SERVER || !ROOM_ALIAS) return; + const CREDS_KEY = 'matrix_chat_creds_' + SERVER; let accessToken = null; @@ -12,30 +14,84 @@ let syncToken = null; let syncActive = false; let txnCounter = 0; + let initialized = false; - const messagesEl = document.getElementById('matrix-messages'); - const statusEl = document.getElementById('matrix-chat-status'); - const inputEl = document.getElementById('matrix-input'); - const sendBtn = document.getElementById('matrix-send'); + // ── DOM bootstrap ───────────────────────────────────────────────────────── - // ── Utilities ──────────────────────────────────────────────────────────── + function mount() { + const css = document.createElement('link'); + css.rel = 'stylesheet'; + css.href = cfg.cssUrl || ''; + if (cfg.cssUrl) document.head.appendChild(css); + + document.body.insertAdjacentHTML('beforeend', ` + + +