Compare commits
No commits in common. "main" and "bugfix/multiple-issues-with-new-chat" have entirely different histories.
main
...
bugfix/mul
@ -39,8 +39,7 @@ dependencies = [
|
|||||||
"Pillow",
|
"Pillow",
|
||||||
"pillow-heif",
|
"pillow-heif",
|
||||||
"IP2Location",
|
"IP2Location",
|
||||||
"bleach",
|
"bleach"
|
||||||
"sentry-sdk"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
@ -9,8 +9,6 @@ from snek.shell import Shell
|
|||||||
from snek.app import Application
|
from snek.app import Application
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
@ -124,12 +122,6 @@ def shell(db_path):
|
|||||||
Shell(db_path).run()
|
Shell(db_path).run()
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
|
||||||
import sentry_sdk
|
|
||||||
sentry_sdk.init("https://ab6147c2f3354c819768c7e89455557b@gt.molodetz.nl/1")
|
|
||||||
except ImportError:
|
|
||||||
print("Could not import sentry_sdk")
|
|
||||||
|
|
||||||
cli()
|
cli()
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ import uuid
|
|||||||
import signal
|
import signal
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import aiohttp_debugtoolbar
|
|
||||||
|
|
||||||
from snek import snode
|
from snek import snode
|
||||||
from snek.view.threads import ThreadsView
|
from snek.view.threads import ThreadsView
|
||||||
@ -177,8 +176,6 @@ class Application(BaseApplication):
|
|||||||
self.on_startup.append(self.start_ssh_server)
|
self.on_startup.append(self.start_ssh_server)
|
||||||
self.on_startup.append(self.prepare_database)
|
self.on_startup.append(self.prepare_database)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def prepare_stats(self, app):
|
async def prepare_stats(self, app):
|
||||||
app['stats'] = create_stats_structure()
|
app['stats'] = create_stats_structure()
|
||||||
print("Stats prepared", flush=True)
|
print("Stats prepared", flush=True)
|
||||||
@ -290,9 +287,9 @@ class Application(BaseApplication):
|
|||||||
self.router.add_view("/login.json", LoginView)
|
self.router.add_view("/login.json", LoginView)
|
||||||
self.router.add_view("/register.html", RegisterView)
|
self.router.add_view("/register.html", RegisterView)
|
||||||
self.router.add_view("/register.json", RegisterView)
|
self.router.add_view("/register.json", RegisterView)
|
||||||
# self.router.add_view("/drive/{rel_path:.*}", DriveView)
|
self.router.add_view("/drive/{rel_path:.*}", DriveView)
|
||||||
## self.router.add_view("/drive.bin", UploadView)
|
self.router.add_view("/drive.bin", UploadView)
|
||||||
# self.router.add_view("/drive.bin/{uid}.{ext}", UploadView)
|
self.router.add_view("/drive.bin/{uid}.{ext}", UploadView)
|
||||||
self.router.add_view("/search-user.html", SearchUserView)
|
self.router.add_view("/search-user.html", SearchUserView)
|
||||||
self.router.add_view("/search-user.json", SearchUserView)
|
self.router.add_view("/search-user.json", SearchUserView)
|
||||||
self.router.add_view("/avatar/{uid}.svg", AvatarView)
|
self.router.add_view("/avatar/{uid}.svg", AvatarView)
|
||||||
@ -300,25 +297,25 @@ class Application(BaseApplication):
|
|||||||
self.router.add_get("/http-photo", self.handle_http_photo)
|
self.router.add_get("/http-photo", self.handle_http_photo)
|
||||||
self.router.add_get("/rpc.ws", RPCView)
|
self.router.add_get("/rpc.ws", RPCView)
|
||||||
self.router.add_get("/c/{channel:.*}", ChannelView)
|
self.router.add_get("/c/{channel:.*}", ChannelView)
|
||||||
#self.router.add_view(
|
self.router.add_view(
|
||||||
# "/channel/{channel_uid}/attachment.bin", ChannelAttachmentView
|
"/channel/{channel_uid}/attachment.bin", ChannelAttachmentView
|
||||||
#)
|
)
|
||||||
#self.router.add_view(
|
self.router.add_view(
|
||||||
# "/channel/{channel_uid}/drive.json", ChannelDriveApiView
|
"/channel/{channel_uid}/drive.json", ChannelDriveApiView
|
||||||
#)
|
)
|
||||||
self.router.add_view(
|
self.router.add_view(
|
||||||
"/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView
|
"/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView
|
||||||
)
|
)
|
||||||
self.router.add_view(
|
self.router.add_view(
|
||||||
"/channel/attachment/{relative_url:.*}", ChannelAttachmentView
|
"/channel/attachment/{relative_url:.*}", ChannelAttachmentView
|
||||||
)#
|
)
|
||||||
self.router.add_view("/channel/{channel}.html", WebView)
|
self.router.add_view("/channel/{channel}.html", WebView)
|
||||||
self.router.add_view("/threads.html", ThreadsView)
|
self.router.add_view("/threads.html", ThreadsView)
|
||||||
self.router.add_view("/terminal.ws", TerminalSocketView)
|
self.router.add_view("/terminal.ws", TerminalSocketView)
|
||||||
self.router.add_view("/terminal.html", TerminalView)
|
self.router.add_view("/terminal.html", TerminalView)
|
||||||
#self.router.add_view("/drive.json", DriveApiView)
|
self.router.add_view("/drive.json", DriveApiView)
|
||||||
#self.router.add_view("/drive.html", DriveView)
|
self.router.add_view("/drive.html", DriveView)
|
||||||
#self.router.add_view("/drive/{drive}.json", DriveView)
|
self.router.add_view("/drive/{drive}.json", DriveView)
|
||||||
self.router.add_get("/stats.html", stats_handler)
|
self.router.add_get("/stats.html", stats_handler)
|
||||||
self.router.add_view("/stats.json", StatsView)
|
self.router.add_view("/stats.json", StatsView)
|
||||||
self.router.add_view("/user/{user}.html", UserView)
|
self.router.add_view("/user/{user}.html", UserView)
|
||||||
@ -499,7 +496,6 @@ class Application(BaseApplication):
|
|||||||
raise raised_exception
|
raise raised_exception
|
||||||
|
|
||||||
app = Application(db_path="sqlite:///snek.db")
|
app = Application(db_path="sqlite:///snek.db")
|
||||||
#aiohttp_debugtoolbar.setup(app)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
@ -13,17 +13,13 @@ class ChannelModel(BaseModel):
|
|||||||
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
|
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
|
||||||
history_start = ModelField(name="history_start", required=False, kind=str)
|
history_start = ModelField(name="history_start", required=False, kind=str)
|
||||||
|
|
||||||
@property
|
|
||||||
def is_dm(self):
|
|
||||||
return 'dm' in self['tag'].lower()
|
|
||||||
|
|
||||||
async def get_last_message(self) -> ChannelMessageModel:
|
async def get_last_message(self) -> ChannelMessageModel:
|
||||||
history_start_filter = ""
|
history_start_filter = ""
|
||||||
if self["history_start"]:
|
if self["history_start"]:
|
||||||
history_start_filter = f" AND created_at > '{self['history_start']}' "
|
history_start_filter = f" AND created_at > '{self['history_start']}' "
|
||||||
try:
|
try:
|
||||||
async for model in self.app.services.channel_message.query(
|
async for model in self.app.services.channel_message.query(
|
||||||
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY id DESC LIMIT 1",
|
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY created_at DESC LIMIT 1",
|
||||||
{"channel_uid": self["uid"]},
|
{"channel_uid": self["uid"]},
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -1,21 +1,6 @@
|
|||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
from snek.system.template import sanitize_html
|
from snek.system.template import whitelist_attributes
|
||||||
import time
|
import time
|
||||||
import asyncio
|
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
import json
|
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
|
||||||
global jinja2_env
|
|
||||||
import pathlib
|
|
||||||
template_path = pathlib.Path(__file__).parent.parent.joinpath("templates")
|
|
||||||
|
|
||||||
|
|
||||||
def render(context):
|
|
||||||
template =jinja2_env.get_template("message.html")
|
|
||||||
return sanitize_html(template.render(**context))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelMessageService(BaseService):
|
class ChannelMessageService(BaseService):
|
||||||
mapper_name = "channel_message"
|
mapper_name = "channel_message"
|
||||||
@ -23,19 +8,6 @@ class ChannelMessageService(BaseService):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._configured_indexes = False
|
self._configured_indexes = False
|
||||||
self._executor_pools = {}
|
|
||||||
global jinja2_env
|
|
||||||
jinja2_env = self.app.jinja2_env
|
|
||||||
self._max_workers = 1
|
|
||||||
def get_or_create_executor(self, uid):
|
|
||||||
if not uid in self._executor_pools:
|
|
||||||
self._executor_pools[uid] = ProcessPoolExecutor(max_workers=5)
|
|
||||||
return self._executor_pools[uid]
|
|
||||||
|
|
||||||
def delete_executor(self, uid):
|
|
||||||
if uid in self._executor_pools:
|
|
||||||
self._executor_pools[uid].shutdown()
|
|
||||||
del self._executor_pools[uid]
|
|
||||||
|
|
||||||
async def maintenance(self):
|
async def maintenance(self):
|
||||||
args = {}
|
args = {}
|
||||||
@ -97,14 +69,10 @@ class ChannelMessageService(BaseService):
|
|||||||
"color": user["color"],
|
"color": user["color"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
try:
|
try:
|
||||||
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
context = json.loads(json.dumps(context, default=str))
|
model["html"] = template.render(**context)
|
||||||
|
model["html"] = whitelist_attributes(model["html"])
|
||||||
|
|
||||||
model["html"] = await loop.run_in_executor(self.get_or_create_executor(model["uid"]), render,context)
|
|
||||||
#model['html'] = await loop.run_in_executor(self.get_or_create_executor(user["uid"]), sanitize_html,model['html'])
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex, flush=True)
|
print(ex, flush=True)
|
||||||
|
|
||||||
@ -123,8 +91,6 @@ class ChannelMessageService(BaseService):
|
|||||||
["deleted_at"], unique=False
|
["deleted_at"], unique=False
|
||||||
)
|
)
|
||||||
self._configured_indexes = True
|
self._configured_indexes = True
|
||||||
if model['is_final']:
|
|
||||||
self.delete_executor(model['uid'])
|
|
||||||
return model
|
return model
|
||||||
raise Exception(f"Failed to create channel message: {model.errors}.")
|
raise Exception(f"Failed to create channel message: {model.errors}.")
|
||||||
|
|
||||||
@ -161,15 +127,10 @@ class ChannelMessageService(BaseService):
|
|||||||
"color": user["color"],
|
"color": user["color"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
context = json.loads(json.dumps(context, default=str))
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
loop = asyncio.get_event_loop()
|
model["html"] = template.render(**context)
|
||||||
model["html"] = await loop.run_in_executor(self.get_or_create_executor(model["uid"]), render, context)
|
model["html"] = whitelist_attributes(model["html"])
|
||||||
#model['html'] = await loop.run_in_executor(self.get_or_create_executor(user["uid"]), sanitize_html,model['html'])
|
return await super().save(model)
|
||||||
|
|
||||||
result = await super().save(model)
|
|
||||||
if model['is_final']:
|
|
||||||
self.delete_executor(model['uid'])
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||||
channel = await self.services.channel.get(uid=channel_uid)
|
channel = await self.services.channel.get(uid=channel_uid)
|
||||||
|
@ -44,29 +44,19 @@ class SocketService(BaseService):
|
|||||||
|
|
||||||
async def user_availability_service(self):
|
async def user_availability_service(self):
|
||||||
logger.info("User availability update service started.")
|
logger.info("User availability update service started.")
|
||||||
logger.debug("Entering the main loop.")
|
|
||||||
while True:
|
while True:
|
||||||
logger.info("Updating user availability...")
|
logger.info("Updating user availability...")
|
||||||
logger.debug("Initializing users_updated list.")
|
|
||||||
users_updated = []
|
users_updated = []
|
||||||
logger.debug("Iterating over sockets.")
|
|
||||||
for s in self.sockets:
|
for s in self.sockets:
|
||||||
logger.debug(f"Checking connection status for socket: {s}.")
|
|
||||||
if not s.is_connected:
|
if not s.is_connected:
|
||||||
logger.debug("Socket is not connected, continuing to next socket.")
|
|
||||||
continue
|
continue
|
||||||
logger.debug(f"Checking if user {s.user} is already updated.")
|
|
||||||
if s.user not in users_updated:
|
if s.user not in users_updated:
|
||||||
logger.debug(f"Updating last_ping for user: {s.user}.")
|
|
||||||
s.user["last_ping"] = now()
|
s.user["last_ping"] = now()
|
||||||
logger.debug(f"Saving user {s.user} to the database.")
|
|
||||||
await self.app.services.user.save(s.user)
|
await self.app.services.user.save(s.user)
|
||||||
logger.debug(f"Adding user {s.user} to users_updated list.")
|
|
||||||
users_updated.append(s.user)
|
users_updated.append(s.user)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Updated user availability for {len(users_updated)} online users."
|
f"Updated user availability for {len(users_updated)} online users."
|
||||||
)
|
)
|
||||||
logger.debug("Sleeping for 60 seconds before the next update.")
|
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
async def add(self, ws, user_uid):
|
async def add(self, ws, user_uid):
|
||||||
@ -87,7 +77,7 @@ class SocketService(BaseService):
|
|||||||
|
|
||||||
async def send_to_user(self, user_uid, message):
|
async def send_to_user(self, user_uid, message):
|
||||||
count = 0
|
count = 0
|
||||||
for s in list(self.users.get(user_uid, [])):
|
for s in self.users.get(user_uid, []):
|
||||||
if await s.send_json(message):
|
if await s.send_json(message):
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
@ -1,494 +1,226 @@
|
|||||||
import { NjetComponent } from "/njet.js"
|
import { NjetComponent} from "/njet.js"
|
||||||
|
|
||||||
class NjetEditor extends NjetComponent {
|
class NjetEditor extends NjetComponent {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
:host {
|
#editor {
|
||||||
display: block;
|
padding: 1rem;
|
||||||
position: relative;
|
outline: none;
|
||||||
height: 100%;
|
white-space: pre-wrap;
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
line-height: 1.5;
|
||||||
}
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
#command-line {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.2rem 1rem;
|
||||||
|
background: #333;
|
||||||
|
color: #0f0;
|
||||||
|
display: none;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
#editor {
|
this.editor = document.createElement('div');
|
||||||
padding: 1rem;
|
this.editor.id = 'editor';
|
||||||
outline: none;
|
this.editor.contentEditable = true;
|
||||||
white-space: pre-wrap;
|
this.editor.innerText = `Welcome to VimEditor Component
|
||||||
line-height: 1.5;
|
|
||||||
height: calc(100% - 30px);
|
|
||||||
overflow-y: auto;
|
|
||||||
background: #1e1e1e;
|
|
||||||
color: #d4d4d4;
|
|
||||||
font-size: 14px;
|
|
||||||
caret-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor.insert-mode {
|
|
||||||
caret-color: #4ec9b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor.visual-mode {
|
|
||||||
caret-color: #c586c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor::selection {
|
|
||||||
background: #264f78;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status-bar {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 30px;
|
|
||||||
background: #007acc;
|
|
||||||
color: #fff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 1rem;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mode-indicator {
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-right: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#command-line {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 30px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.3rem 1rem;
|
|
||||||
background: #2d2d2d;
|
|
||||||
color: #d4d4d4;
|
|
||||||
display: none;
|
|
||||||
font-family: inherit;
|
|
||||||
border-top: 1px solid #3e3e3e;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#command-input {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: inherit;
|
|
||||||
outline: none;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.visual-selection {
|
|
||||||
background: #264f78 !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.editor = document.createElement('div');
|
|
||||||
this.editor.id = 'editor';
|
|
||||||
this.editor.contentEditable = true;
|
|
||||||
this.editor.spellcheck = false;
|
|
||||||
this.editor.innerText = `Welcome to VimEditor Component
|
|
||||||
Line 2 here
|
Line 2 here
|
||||||
Another line
|
Another line
|
||||||
Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
|
Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
|
||||||
|
|
||||||
this.cmdLine = document.createElement('div');
|
this.cmdLine = document.createElement('div');
|
||||||
this.cmdLine.id = 'command-line';
|
this.cmdLine.id = 'command-line';
|
||||||
|
this.shadowRoot.append(style, this.editor, this.cmdLine);
|
||||||
|
|
||||||
const cmdPrompt = document.createElement('span');
|
this.mode = 'normal'; // normal | insert | visual | command
|
||||||
cmdPrompt.textContent = ':';
|
this.keyBuffer = '';
|
||||||
|
this.lastDeletedLine = '';
|
||||||
|
this.yankedLine = '';
|
||||||
|
|
||||||
this.cmdInput = document.createElement('input');
|
this.editor.addEventListener('keydown', this.handleKeydown.bind(this));
|
||||||
this.cmdInput.id = 'command-input';
|
}
|
||||||
this.cmdInput.type = 'text';
|
|
||||||
|
|
||||||
this.cmdLine.append(cmdPrompt, this.cmdInput);
|
connectedCallback() {
|
||||||
|
|
||||||
this.statusBar = document.createElement('div');
|
|
||||||
this.statusBar.id = 'status-bar';
|
|
||||||
|
|
||||||
this.modeIndicator = document.createElement('span');
|
|
||||||
this.modeIndicator.id = 'mode-indicator';
|
|
||||||
this.modeIndicator.textContent = 'NORMAL';
|
|
||||||
|
|
||||||
this.statusBar.appendChild(this.modeIndicator);
|
|
||||||
|
|
||||||
this.shadowRoot.append(style, this.editor, this.cmdLine, this.statusBar);
|
|
||||||
|
|
||||||
this.mode = 'normal';
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.lastDeletedLine = '';
|
|
||||||
this.yankedLine = '';
|
|
||||||
this.visualStartOffset = null;
|
|
||||||
this.visualEndOffset = null;
|
|
||||||
|
|
||||||
// Bind event handlers
|
|
||||||
this.handleKeydown = this.handleKeydown.bind(this);
|
|
||||||
this.handleCmdKeydown = this.handleCmdKeydown.bind(this);
|
|
||||||
this.updateVisualSelection = this.updateVisualSelection.bind(this);
|
|
||||||
|
|
||||||
this.editor.addEventListener('keydown', this.handleKeydown);
|
|
||||||
this.cmdInput.addEventListener('keydown', this.handleCmdKeydown);
|
|
||||||
this.editor.addEventListener('beforeinput', this.handleBeforeInput.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.editor.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
setMode(mode) {
|
|
||||||
this.mode = mode;
|
|
||||||
this.modeIndicator.textContent = mode.toUpperCase();
|
|
||||||
|
|
||||||
// Update editor classes
|
|
||||||
this.editor.classList.remove('insert-mode', 'visual-mode', 'normal-mode');
|
|
||||||
this.editor.classList.add(`${mode}-mode`);
|
|
||||||
|
|
||||||
if (mode === 'visual') {
|
|
||||||
this.visualStartOffset = this.getCaretOffset();
|
|
||||||
this.editor.addEventListener('selectionchange', this.updateVisualSelection);
|
|
||||||
} else {
|
|
||||||
this.clearVisualSelection();
|
|
||||||
this.editor.removeEventListener('selectionchange', this.updateVisualSelection);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'command') {
|
|
||||||
this.cmdLine.style.display = 'block';
|
|
||||||
this.cmdInput.value = '';
|
|
||||||
this.cmdInput.focus();
|
|
||||||
} else {
|
|
||||||
this.cmdLine.style.display = 'none';
|
|
||||||
if (mode !== 'insert') {
|
|
||||||
// Keep focus on editor for all non-insert modes
|
|
||||||
this.editor.focus();
|
this.editor.focus();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVisualSelection() {
|
getCaretOffset() {
|
||||||
if (this.mode !== 'visual') return;
|
let caretOffset = 0;
|
||||||
this.visualEndOffset = this.getCaretOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearVisualSelection() {
|
|
||||||
const sel = this.shadowRoot.getSelection();
|
|
||||||
if (sel) sel.removeAllRanges();
|
|
||||||
this.visualStartOffset = null;
|
|
||||||
this.visualEndOffset = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCaretOffset() {
|
|
||||||
const sel = this.shadowRoot.getSelection();
|
|
||||||
if (!sel || sel.rangeCount === 0) return 0;
|
|
||||||
|
|
||||||
const range = sel.getRangeAt(0);
|
|
||||||
const preCaretRange = range.cloneRange();
|
|
||||||
preCaretRange.selectNodeContents(this.editor);
|
|
||||||
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
|
||||||
return preCaretRange.toString().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCaretOffset(offset) {
|
|
||||||
const textContent = this.editor.innerText;
|
|
||||||
offset = Math.max(0, Math.min(offset, textContent.length));
|
|
||||||
|
|
||||||
const range = document.createRange();
|
|
||||||
const sel = this.shadowRoot.getSelection();
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
this.editor,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
null,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
let currentOffset = 0;
|
|
||||||
let node;
|
|
||||||
|
|
||||||
while ((node = walker.nextNode())) {
|
|
||||||
const nodeLength = node.textContent.length;
|
|
||||||
if (currentOffset + nodeLength >= offset) {
|
|
||||||
range.setStart(node, offset - currentOffset);
|
|
||||||
range.collapse(true);
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(range);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentOffset += nodeLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we couldn't find the position, set to end
|
|
||||||
if (this.editor.lastChild) {
|
|
||||||
range.selectNodeContents(this.editor.lastChild);
|
|
||||||
range.collapse(false);
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(range);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBeforeInput(e) {
|
|
||||||
if (this.mode !== 'insert') {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCmdKeydown(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.executeCommand(this.cmdInput.value);
|
|
||||||
this.setMode('normal');
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setMode('normal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executeCommand(cmd) {
|
|
||||||
const trimmedCmd = cmd.trim();
|
|
||||||
|
|
||||||
// Handle basic vim commands
|
|
||||||
if (trimmedCmd === 'w' || trimmedCmd === 'write') {
|
|
||||||
console.log('Save command (not implemented)');
|
|
||||||
} else if (trimmedCmd === 'q' || trimmedCmd === 'quit') {
|
|
||||||
console.log('Quit command (not implemented)');
|
|
||||||
} else if (trimmedCmd === 'wq' || trimmedCmd === 'x') {
|
|
||||||
console.log('Save and quit command (not implemented)');
|
|
||||||
} else if (/^\d+$/.test(trimmedCmd)) {
|
|
||||||
// Go to line number
|
|
||||||
const lineNum = parseInt(trimmedCmd, 10) - 1;
|
|
||||||
this.goToLine(lineNum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
goToLine(lineNum) {
|
|
||||||
const lines = this.editor.innerText.split('\n');
|
|
||||||
if (lineNum < 0 || lineNum >= lines.length) return;
|
|
||||||
|
|
||||||
let offset = 0;
|
|
||||||
for (let i = 0; i < lineNum; i++) {
|
|
||||||
offset += lines[i].length + 1;
|
|
||||||
}
|
|
||||||
this.setCaretOffset(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentLineInfo() {
|
|
||||||
const text = this.editor.innerText;
|
|
||||||
const caretPos = this.getCaretOffset();
|
|
||||||
const lines = text.split('\n');
|
|
||||||
|
|
||||||
let charCount = 0;
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
if (caretPos <= charCount + lines[i].length) {
|
|
||||||
return {
|
|
||||||
lineIndex: i,
|
|
||||||
lines: lines,
|
|
||||||
lineStartOffset: charCount,
|
|
||||||
positionInLine: caretPos - charCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
charCount += lines[i].length + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
lineIndex: lines.length - 1,
|
|
||||||
lines: lines,
|
|
||||||
lineStartOffset: charCount - lines[lines.length - 1].length - 1,
|
|
||||||
positionInLine: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeydown(e) {
|
|
||||||
if (this.mode === 'insert') {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setMode('normal');
|
|
||||||
// Move cursor one position left (vim behavior)
|
|
||||||
const offset = this.getCaretOffset();
|
|
||||||
if (offset > 0) {
|
|
||||||
this.setCaretOffset(offset - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mode === 'command') {
|
|
||||||
return; // Command mode input is handled by cmdInput
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mode === 'visual') {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setMode('normal');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow movement in visual mode
|
|
||||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
|
||||||
return; // Let default behavior handle selection
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'y') {
|
|
||||||
e.preventDefault();
|
|
||||||
// Yank selected text
|
|
||||||
const sel = this.shadowRoot.getSelection();
|
const sel = this.shadowRoot.getSelection();
|
||||||
if (sel && sel.rangeCount > 0) {
|
if (!sel || sel.rangeCount === 0) return 0;
|
||||||
this.yankedLine = sel.toString();
|
|
||||||
}
|
const range = sel.getRangeAt(0);
|
||||||
this.setMode('normal');
|
const preCaretRange = range.cloneRange();
|
||||||
return;
|
preCaretRange.selectNodeContents(this.editor);
|
||||||
|
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
||||||
|
caretOffset = preCaretRange.toString().length;
|
||||||
|
return caretOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'd' || e.key === 'x') {
|
setCaretOffset(offset) {
|
||||||
e.preventDefault();
|
const range = document.createRange();
|
||||||
// Delete selected text
|
|
||||||
const sel = this.shadowRoot.getSelection();
|
const sel = this.shadowRoot.getSelection();
|
||||||
if (sel && sel.rangeCount > 0) {
|
const walker = document.createTreeWalker(this.editor, NodeFilter.SHOW_TEXT, null, false);
|
||||||
this.lastDeletedLine = sel.toString();
|
|
||||||
document.execCommand('delete');
|
|
||||||
}
|
|
||||||
this.setMode('normal');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal mode handling
|
let currentOffset = 0;
|
||||||
e.preventDefault();
|
let node;
|
||||||
|
while ((node = walker.nextNode())) {
|
||||||
// Special keys that should be handled immediately
|
if (currentOffset + node.length >= offset) {
|
||||||
if (e.key === 'Escape') {
|
range.setStart(node, offset - currentOffset);
|
||||||
this.keyBuffer = '';
|
range.collapse(true);
|
||||||
this.setMode('normal');
|
sel.removeAllRanges();
|
||||||
return;
|
sel.addRange(range);
|
||||||
}
|
return;
|
||||||
|
|
||||||
// Build key buffer for commands
|
|
||||||
this.keyBuffer += e.key;
|
|
||||||
|
|
||||||
const lineInfo = this.getCurrentLineInfo();
|
|
||||||
const { lineIndex, lines, lineStartOffset, positionInLine } = lineInfo;
|
|
||||||
|
|
||||||
// Process commands
|
|
||||||
switch (this.keyBuffer) {
|
|
||||||
case 'i':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setMode('insert');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'a':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setCaretOffset(this.getCaretOffset() + 1);
|
|
||||||
this.setMode('insert');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'v':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setMode('visual');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ':':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setMode('command');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'yy':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.yankedLine = lines[lineIndex];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'dd':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.lastDeletedLine = lines[lineIndex];
|
|
||||||
lines.splice(lineIndex, 1);
|
|
||||||
if (lines.length === 0) lines.push('');
|
|
||||||
this.editor.innerText = lines.join('\n');
|
|
||||||
this.setCaretOffset(lineStartOffset);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'p':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
const lineToPaste = this.yankedLine || this.lastDeletedLine;
|
|
||||||
if (lineToPaste) {
|
|
||||||
lines.splice(lineIndex + 1, 0, lineToPaste);
|
|
||||||
this.editor.innerText = lines.join('\n');
|
|
||||||
this.setCaretOffset(lineStartOffset + lines[lineIndex].length + 1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '0':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setCaretOffset(lineStartOffset);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '$':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setCaretOffset(lineStartOffset + lines[lineIndex].length);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'gg':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setCaretOffset(0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'G':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setCaretOffset(this.editor.innerText.length);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'h':
|
|
||||||
case 'ArrowLeft':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
const currentOffset = this.getCaretOffset();
|
|
||||||
if (currentOffset > 0) {
|
|
||||||
this.setCaretOffset(currentOffset - 1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'l':
|
|
||||||
case 'ArrowRight':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
this.setCaretOffset(this.getCaretOffset() + 1);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'j':
|
|
||||||
case 'ArrowDown':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
if (lineIndex < lines.length - 1) {
|
|
||||||
const nextLineStart = lineStartOffset + lines[lineIndex].length + 1;
|
|
||||||
const nextLineLength = lines[lineIndex + 1].length;
|
|
||||||
const newPosition = Math.min(positionInLine, nextLineLength);
|
|
||||||
this.setCaretOffset(nextLineStart + newPosition);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'k':
|
|
||||||
case 'ArrowUp':
|
|
||||||
this.keyBuffer = '';
|
|
||||||
if (lineIndex > 0) {
|
|
||||||
let prevLineStart = 0;
|
|
||||||
for (let i = 0; i < lineIndex - 1; i++) {
|
|
||||||
prevLineStart += lines[i].length + 1;
|
|
||||||
}
|
}
|
||||||
const prevLineLength = lines[lineIndex - 1].length;
|
currentOffset += node.length;
|
||||||
const newPosition = Math.min(positionInLine, prevLineLength);
|
|
||||||
this.setCaretOffset(prevLineStart + newPosition);
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
|
||||||
default:
|
handleKeydown(e) {
|
||||||
// Clear buffer if it gets too long or contains invalid sequences
|
const key = e.key;
|
||||||
if (this.keyBuffer.length > 2 ||
|
|
||||||
(this.keyBuffer.length === 2 && !['dd', 'yy', 'gg'].includes(this.keyBuffer))) {
|
if (this.mode === 'insert') {
|
||||||
this.keyBuffer = '';
|
if (key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'normal';
|
||||||
|
this.editor.blur();
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
if (this.mode === 'command') {
|
||||||
|
if (key === 'Enter' || key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.cmdLine.style.display = 'none';
|
||||||
|
this.mode = 'normal';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mode === 'visual') {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'normal';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle normal mode
|
||||||
|
this.keyBuffer += key;
|
||||||
|
|
||||||
|
const text = this.editor.innerText;
|
||||||
|
const caretPos = this.getCaretOffset();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
let charCount = 0, lineIdx = 0;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (caretPos <= charCount + lines[i].length) {
|
||||||
|
lineIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
charCount += lines[i].length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetToLine = idx =>
|
||||||
|
text.split('\n').slice(0, idx).reduce((acc, l) => acc + l.length + 1, 0);
|
||||||
|
|
||||||
|
switch (this.keyBuffer) {
|
||||||
|
case 'i':
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'insert';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'v':
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'visual';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ':':
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'command';
|
||||||
|
this.cmdLine.style.display = 'block';
|
||||||
|
this.cmdLine.textContent = ':';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'yy':
|
||||||
|
e.preventDefault();
|
||||||
|
this.yankedLine = lines[lineIdx];
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dd':
|
||||||
|
e.preventDefault();
|
||||||
|
this.lastDeletedLine = lines[lineIdx];
|
||||||
|
lines.splice(lineIdx, 1);
|
||||||
|
this.editor.innerText = lines.join('\n');
|
||||||
|
this.setCaretOffset(offsetToLine(lineIdx));
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'p':
|
||||||
|
e.preventDefault();
|
||||||
|
const lineToPaste = this.yankedLine || this.lastDeletedLine;
|
||||||
|
if (lineToPaste) {
|
||||||
|
lines.splice(lineIdx + 1, 0, lineToPaste);
|
||||||
|
this.editor.innerText = lines.join('\n');
|
||||||
|
this.setCaretOffset(offsetToLine(lineIdx + 1));
|
||||||
|
}
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '0':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setCaretOffset(offsetToLine(lineIdx));
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setCaretOffset(offsetToLine(lineIdx) + lines[lineIdx].length);
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gg':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setCaretOffset(0);
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'G':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setCaretOffset(text.length);
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'normal';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
this.cmdLine.style.display = 'none';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// allow up to 2 chars for combos
|
||||||
|
if (this.keyBuffer.length > 2) this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('njet-editor', NjetEditor);
|
customElements.define('njet-editor', NjetEditor);
|
||||||
export { NjetEditor }
|
export {NjetEditor}
|
||||||
|
@ -3,15 +3,8 @@ export class EventHandler {
|
|||||||
this.subscribers = {};
|
this.subscribers = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(type, handler, { once = false } = {}) {
|
addEventListener(type, handler) {
|
||||||
if (!this.subscribers[type]) this.subscribers[type] = [];
|
if (!this.subscribers[type]) this.subscribers[type] = [];
|
||||||
if (once) {
|
|
||||||
const originalHandler = handler;
|
|
||||||
handler = (...args) => {
|
|
||||||
originalHandler(...args);
|
|
||||||
this.removeEventListener(type, handler);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
this.subscribers[type].push(handler);
|
this.subscribers[type].push(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,15 +12,4 @@ export class EventHandler {
|
|||||||
if (this.subscribers[type])
|
if (this.subscribers[type])
|
||||||
this.subscribers[type].forEach((handler) => handler(...data));
|
this.subscribers[type].forEach((handler) => handler(...data));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEventListener(type, handler) {
|
|
||||||
if (!this.subscribers[type]) return;
|
|
||||||
this.subscribers[type] = this.subscribers[type].filter(
|
|
||||||
(h) => h !== handler
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.subscribers[type].length === 0) {
|
|
||||||
delete this.subscribers[type];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,65 +5,75 @@
|
|||||||
// 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";
|
||||||
|
|
||||||
const LONG_TIME = 1000 * 60 * 20;
|
const LONG_TIME = 1000 * 60 * 20
|
||||||
|
|
||||||
export class ReplyEvent extends Event {
|
export class ReplyEvent extends Event {
|
||||||
constructor(messageTextTarget) {
|
constructor(messageTextTarget) {
|
||||||
super('reply', { bubbles: true, composed: true });
|
super('reply', { bubbles: true, composed: true });
|
||||||
this.messageTextTarget = messageTextTarget;
|
this.messageTextTarget = messageTextTarget;
|
||||||
|
|
||||||
// Clone and sanitize message node to text-only reply
|
const newMessage = messageTextTarget.cloneNode(true);
|
||||||
const newMessage = messageTextTarget.cloneNode(true);
|
newMessage.style.maxHeight = "0"
|
||||||
newMessage.style.maxHeight = "0";
|
messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget);
|
||||||
messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget);
|
|
||||||
|
|
||||||
// Remove all .embed-url-link
|
newMessage.querySelectorAll('.embed-url-link').forEach(link => {
|
||||||
newMessage.querySelectorAll('.embed-url-link').forEach(link => link.remove());
|
link.remove()
|
||||||
|
})
|
||||||
|
|
||||||
// Replace <picture> with their <img>
|
newMessage.querySelectorAll('picture').forEach(picture => {
|
||||||
newMessage.querySelectorAll('picture').forEach(picture => {
|
const img = picture.querySelector('img');
|
||||||
const img = picture.querySelector('img');
|
if (img) {
|
||||||
if (img) picture.replaceWith(img);
|
picture.replaceWith(img);
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Replace <img> with just their src
|
newMessage.querySelectorAll('img').forEach(img => {
|
||||||
newMessage.querySelectorAll('img').forEach(img => {
|
const src = img.src || img.currentSrc;
|
||||||
const src = img.src || img.currentSrc;
|
img.replaceWith(document.createTextNode(src));
|
||||||
img.replaceWith(document.createTextNode(src));
|
})
|
||||||
});
|
|
||||||
|
|
||||||
// Replace <iframe> with their src
|
newMessage.querySelectorAll('iframe').forEach(iframe => {
|
||||||
newMessage.querySelectorAll('iframe').forEach(iframe => {
|
const src = iframe.src || iframe.currentSrc;
|
||||||
const src = iframe.src || iframe.currentSrc;
|
iframe.replaceWith(document.createTextNode(src));
|
||||||
iframe.replaceWith(document.createTextNode(src));
|
})
|
||||||
});
|
|
||||||
|
|
||||||
// Replace <a> with href or markdown
|
newMessage.querySelectorAll('a').forEach(a => {
|
||||||
newMessage.querySelectorAll('a').forEach(a => {
|
const href = a.getAttribute('href');
|
||||||
const href = a.getAttribute('href');
|
const text = a.innerText || a.textContent;
|
||||||
const text = a.innerText || a.textContent;
|
if (text === href || text === '') {
|
||||||
if (text === href || text === '') {
|
a.replaceWith(document.createTextNode(href));
|
||||||
a.replaceWith(document.createTextNode(href));
|
} else {
|
||||||
} else {
|
a.replaceWith(document.createTextNode(`[${text}](${href})`));
|
||||||
a.replaceWith(document.createTextNode(`[${text}](${href})`));
|
}
|
||||||
}
|
})
|
||||||
});
|
|
||||||
|
|
||||||
this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim();
|
this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim();
|
||||||
newMessage.remove();
|
newMessage.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageElement extends HTMLElement {
|
class MessageElement extends HTMLElement {
|
||||||
|
// static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
||||||
|
|
||||||
|
isVisible() {
|
||||||
|
if (!this) return false;
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateUI() {
|
updateUI() {
|
||||||
if (this._originalChildren === undefined) {
|
if (this._originalChildren === undefined) {
|
||||||
const { color, user_nick, created_at, user_uid } = this.dataset;
|
const { color, user_nick, created_at, user_uid} = this.dataset;
|
||||||
this.classList.add('message');
|
this.classList.add('message');
|
||||||
this.style.maxWidth = '100%';
|
this.style.maxWidth = '100%';
|
||||||
this._originalChildren = Array.from(this.children);
|
this._originalChildren = Array.from(this.children);
|
||||||
|
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<a class="avatar" style="background-color: ${color || ''}; color: black;" href="/user/${user_uid || ''}.html">
|
<a class="avatar" style="background-color: ${color || ''}; color: black;" href="/user/${user_uid || ''}.html">
|
||||||
<img class="avatar-img" width="40" height="40" src="/avatar/${user_uid || ''}.svg" alt="${user_nick || ''}" loading="lazy">
|
<img class="avatar-img" width="40" height="40" src="/avatar/${user_uid || ''}.svg" alt="${user_nick || ''}" loading="lazy">
|
||||||
@ -73,30 +83,29 @@ class MessageElement extends HTMLElement {
|
|||||||
<div class="text"></div>
|
<div class="text"></div>
|
||||||
<div class="time no-select" data-created_at="${created_at || ''}">
|
<div class="time no-select" data-created_at="${created_at || ''}">
|
||||||
<span></span>
|
<span></span>
|
||||||
<a href="#reply">reply</a>
|
<a href="#reply">reply</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`;
|
||||||
`;
|
|
||||||
|
|
||||||
this.messageDiv = this.querySelector('.text');
|
this.messageDiv = this.querySelector('.text');
|
||||||
|
|
||||||
if (this._originalChildren && this._originalChildren.length > 0) {
|
if (this._originalChildren && this._originalChildren.length > 0) {
|
||||||
this._originalChildren.forEach(child => {
|
this._originalChildren.forEach(child => {
|
||||||
this.messageDiv.appendChild(child);
|
this.messageDiv.appendChild(child);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timeDiv = this.querySelector('.time span');
|
this.timeDiv = this.querySelector('.time span');
|
||||||
this.replyDiv = this.querySelector('.time a');
|
this.replyDiv = this.querySelector('.time a');
|
||||||
|
|
||||||
this.replyDiv.addEventListener('click', (e) => {
|
this.replyDiv.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.dispatchEvent(new ReplyEvent(this.messageDiv));
|
this.dispatchEvent(new ReplyEvent(this.messageDiv));
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sibling logic for user switches and long time gaps
|
if (!this.siblingGenerated && this.nextElementSibling) {
|
||||||
if ((!this.siblingGenerated || this.siblingGenerated !== this.nextElementSibling) && this.nextElementSibling) {
|
this.siblingGenerated = true;
|
||||||
this.siblingGenerated = this.nextElementSibling;
|
|
||||||
if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) {
|
if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) {
|
||||||
this.classList.add('switch-user');
|
this.classList.add('switch-user');
|
||||||
} else {
|
} else {
|
||||||
@ -104,7 +113,7 @@ class MessageElement extends HTMLElement {
|
|||||||
const siblingTime = new Date(this.nextElementSibling.dataset.created_at);
|
const siblingTime = new Date(this.nextElementSibling.dataset.created_at);
|
||||||
const currentTime = new Date(this.dataset.created_at);
|
const currentTime = new Date(this.dataset.created_at);
|
||||||
|
|
||||||
if (currentTime.getTime() - siblingTime.getTime() > LONG_TIME) {
|
if (currentTime.getTime() - siblingTime.getTime() > LONG_TIME) {
|
||||||
this.classList.add('long-time');
|
this.classList.add('long-time');
|
||||||
} else {
|
} else {
|
||||||
this.classList.remove('long-time');
|
this.classList.remove('long-time');
|
||||||
@ -117,7 +126,7 @@ class MessageElement extends HTMLElement {
|
|||||||
|
|
||||||
updateMessage(...messages) {
|
updateMessage(...messages) {
|
||||||
if (this._originalChildren) {
|
if (this._originalChildren) {
|
||||||
this.messageDiv.replaceChildren(...messages);
|
this.messageDiv.replaceChildren(...messages)
|
||||||
this._originalChildren = messages;
|
this._originalChildren = messages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,26 +135,38 @@ class MessageElement extends HTMLElement {
|
|||||||
this.updateUI();
|
this.updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {}
|
disconnectedCallback() {
|
||||||
connectedMoveCallback() {}
|
}
|
||||||
|
|
||||||
|
connectedMoveCallback() {
|
||||||
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
this.updateUI();
|
this.updateUI()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageList extends HTMLElement {
|
class MessageList extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
app.ws.addEventListener("update_message_text", (data) => {
|
||||||
|
if (this.messageMap.has(data.uid)) {
|
||||||
|
this.upsertMessage(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.ws.addEventListener("set_typing", (data) => {
|
||||||
|
this.triggerGlow(data.user_uid,data.color);
|
||||||
|
});
|
||||||
|
|
||||||
this.messageMap = new Map();
|
this.messageMap = new Map();
|
||||||
this.visibleSet = new Set();
|
this.visibleSet = new Set();
|
||||||
|
|
||||||
this._observer = new IntersectionObserver((entries) => {
|
this._observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.visibleSet.add(entry.target);
|
this.visibleSet.add(entry.target);
|
||||||
if (entry.target instanceof MessageElement) {
|
const messageElement = entry.target;
|
||||||
entry.target.updateUI();
|
if (messageElement instanceof MessageElement) {
|
||||||
|
messageElement.updateUI();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.visibleSet.delete(entry.target);
|
this.visibleSet.delete(entry.target);
|
||||||
@ -154,42 +175,28 @@ class MessageList extends HTMLElement {
|
|||||||
}, {
|
}, {
|
||||||
root: this,
|
root: this,
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
});
|
})
|
||||||
|
|
||||||
// End-of-messages marker
|
for(const c of this.children) {
|
||||||
this.endOfMessages = document.createElement('div');
|
|
||||||
this.endOfMessages.classList.add('message-list-bottom');
|
|
||||||
this.prepend(this.endOfMessages);
|
|
||||||
|
|
||||||
// Observe existing children and index by uid
|
|
||||||
for (const c of this.children) {
|
|
||||||
this._observer.observe(c);
|
this._observer.observe(c);
|
||||||
if (c instanceof MessageElement) {
|
if (c instanceof MessageElement) {
|
||||||
this.messageMap.set(c.dataset.uid, c);
|
this.messageMap.set(c.dataset.uid, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire up socket events
|
this.endOfMessages = document.createElement('div');
|
||||||
app.ws.addEventListener("update_message_text", (data) => {
|
this.endOfMessages.classList.add('message-list-bottom');
|
||||||
if (this.messageMap.has(data.uid)) {
|
this.prepend(this.endOfMessages);
|
||||||
this.upsertMessage(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.ws.addEventListener("set_typing", (data) => {
|
|
||||||
this.triggerGlow(data.user_uid, data.color);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.scrollToBottom(true);
|
this.scrollToBottom(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.addEventListener('click', (e) => {
|
this.addEventListener('click', (e) => {
|
||||||
if (
|
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
|
||||||
e.target.tagName !== 'IMG' ||
|
|
||||||
e.target.classList.contains('avatar-img')
|
|
||||||
) return;
|
|
||||||
|
|
||||||
const img = e.target;
|
const img = e.target;
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:pointer;';
|
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:pointer;';
|
||||||
|
|
||||||
@ -210,11 +217,12 @@ class MessageList extends HTMLElement {
|
|||||||
|
|
||||||
overlay.appendChild(fullImg);
|
overlay.appendChild(fullImg);
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
overlay.addEventListener('click', () => {
|
overlay.addEventListener('click', () => {
|
||||||
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
if (overlay.parentNode) {
|
||||||
|
overlay.parentNode.removeChild(overlay);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// ESC to close
|
// Optional: ESC key closes overlay
|
||||||
const escListener = (evt) => {
|
const escListener = (evt) => {
|
||||||
if (evt.key === 'Escape') {
|
if (evt.key === 'Escape') {
|
||||||
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||||
@ -222,9 +230,8 @@ class MessageList extends HTMLElement {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', escListener);
|
document.addEventListener('keydown', escListener);
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isElementVisible(element) {
|
isElementVisible(element) {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
@ -235,16 +242,14 @@ class MessageList extends HTMLElement {
|
|||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isScrolledToBottom() {
|
isScrolledToBottom() {
|
||||||
return this.visibleSet.has(this.endOfMessages);
|
return this.isElementVisible(this.endOfMessages);
|
||||||
}
|
}
|
||||||
|
scrollToBottom(force = false, behavior= 'instant') {
|
||||||
scrollToBottom(force = false, behavior = 'instant') {
|
|
||||||
if (force || !this.isScrolledToBottom()) {
|
if (force || !this.isScrolledToBottom()) {
|
||||||
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
|
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
|
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,9 +261,7 @@ class MessageList extends HTMLElement {
|
|||||||
this.querySelectorAll('.avatar').forEach((el) => {
|
this.querySelectorAll('.avatar').forEach((el) => {
|
||||||
const anchor = el.closest('a');
|
const anchor = el.closest('a');
|
||||||
if (anchor && typeof anchor.href === 'string' && anchor.href.includes(uid)) {
|
if (anchor && typeof anchor.href === 'string' && anchor.href.includes(uid)) {
|
||||||
if(!lastElement)
|
lastElement = el;
|
||||||
lastElement = el;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (lastElement) {
|
if (lastElement) {
|
||||||
@ -269,48 +272,40 @@ class MessageList extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateTimes() {
|
updateTimes() {
|
||||||
this.visibleSet.forEach((messageElement) => {
|
this.visibleSet.forEach((messageElement) => {
|
||||||
if (messageElement instanceof MessageElement) {
|
if (messageElement instanceof MessageElement) {
|
||||||
messageElement.updateUI();
|
messageElement.updateUI();
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
upsertMessage(data) {
|
upsertMessage(data) {
|
||||||
let message = this.messageMap.get(data.uid);
|
let message = this.messageMap.get(data.uid);
|
||||||
if (message && (data.is_final || !data.message)) {
|
if (message) {
|
||||||
//message.parentElement?.removeChild(message);
|
message.parentElement?.removeChild(message);
|
||||||
// TO force insert
|
}
|
||||||
//message = null;
|
|
||||||
|
|
||||||
}
|
if (!data.message) return
|
||||||
if(message && !data.message){
|
|
||||||
message.parentElement?.removeChild(message);
|
|
||||||
message = null;
|
|
||||||
}
|
|
||||||
if (!data.message) return;
|
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
|
|
||||||
wrapper.innerHTML = data.html;
|
wrapper.innerHTML = data.html;
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
// If the old element is already custom, only update its message children
|
|
||||||
message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children));
|
message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children));
|
||||||
} else {
|
} else {
|
||||||
// If not, insert the new one and observe
|
|
||||||
message = wrapper.firstElementChild;
|
message = wrapper.firstElementChild;
|
||||||
this.messageMap.set(data.uid, message);
|
this.messageMap.set(data.uid, message);
|
||||||
this._observer.observe(message);
|
this._observer.observe(message);
|
||||||
this.endOfMessages.after(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrolledToBottom = this.isScrolledToBottom();
|
const scrolledToBottom = this.isScrolledToBottom();
|
||||||
|
this.prepend(message);
|
||||||
if (scrolledToBottom) this.scrollToBottom(true);
|
if (scrolledToBottom) this.scrollToBottom(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("chat-message", MessageElement);
|
customElements.define("chat-message", MessageElement);
|
||||||
customElements.define("message-list", MessageList);
|
customElements.define("message-list", MessageList);
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
class RestClient {
|
class RestClient {
|
||||||
constructor({ baseURL = '', headers = {} } = {}) {
|
constructor({ baseURL = '', headers = {} } = {}) {
|
||||||
this.baseURL = baseURL;
|
this.baseURL = baseURL;
|
||||||
@ -208,52 +210,27 @@ class Njet extends HTMLElement {
|
|||||||
customElements.define(name, component);
|
customElements.define(name, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(config) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// Store the config for use in render and other methods
|
|
||||||
this.config = config || {};
|
|
||||||
|
|
||||||
if (!Njet._root) {
|
if (!Njet._root) {
|
||||||
Njet._root = this
|
Njet._root = this
|
||||||
Njet._rest = new RestClient({ baseURL: '/' || null })
|
Njet._rest = new RestClient({ baseURL: '/' || null })
|
||||||
}
|
}
|
||||||
this.root._elements.push(this)
|
this.root._elements.push(this)
|
||||||
this.classList.add('njet');
|
this.classList.add('njet');
|
||||||
|
|
||||||
// Initialize properties from config before rendering
|
|
||||||
this.initProps(this.config);
|
|
||||||
|
|
||||||
// Call render after properties are initialized
|
|
||||||
this.render.call(this);
|
this.render.call(this);
|
||||||
|
//this.initProps(config);
|
||||||
// Call construct if defined
|
//if (typeof this.config.construct === 'function')
|
||||||
if (typeof this.config.construct === 'function') {
|
// this.config.construct.call(this)
|
||||||
this.config.construct.call(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initProps(config) {
|
initProps(config) {
|
||||||
const props = Object.keys(config)
|
const props = Object.keys(config)
|
||||||
props.forEach(prop => {
|
props.forEach(prop => {
|
||||||
// Skip special properties that are handled separately
|
if (config[prop] !== undefined) {
|
||||||
if (['construct', 'items', 'classes'].includes(prop)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there's a setter for this property
|
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), prop);
|
|
||||||
if (descriptor && descriptor.set) {
|
|
||||||
// Use the setter
|
|
||||||
this[prop] = config[prop];
|
this[prop] = config[prop];
|
||||||
} else if (prop in this) {
|
|
||||||
// Property exists, set it directly
|
|
||||||
this[prop] = config[prop];
|
|
||||||
} else {
|
|
||||||
// Set as attribute for unknown properties
|
|
||||||
this.setAttribute(prop, config[prop]);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.classes) {
|
if (config.classes) {
|
||||||
this.classList.add(...config.classes);
|
this.classList.add(...config.classes);
|
||||||
}
|
}
|
||||||
@ -365,7 +342,7 @@ class NjetDialog extends Component {
|
|||||||
const buttonContainer = document.createElement('div');
|
const buttonContainer = document.createElement('div');
|
||||||
buttonContainer.style.marginTop = '20px';
|
buttonContainer.style.marginTop = '20px';
|
||||||
buttonContainer.style.display = 'flex';
|
buttonContainer.style.display = 'flex';
|
||||||
buttonContainer.style.justifyContent = 'flex-end';
|
buttonContainer.style.justifyContent = 'flenjet-end';
|
||||||
buttonContainer.style.gap = '10px';
|
buttonContainer.style.gap = '10px';
|
||||||
if (secondaryButton) {
|
if (secondaryButton) {
|
||||||
const secondary = new NjetButton(secondaryButton);
|
const secondary = new NjetButton(secondaryButton);
|
||||||
@ -395,9 +372,8 @@ class NjetWindow extends Component {
|
|||||||
header.textContent = title;
|
header.textContent = title;
|
||||||
this.appendChild(header);
|
this.appendChild(header);
|
||||||
}
|
}
|
||||||
if (this.config.items) {
|
this.config.items.forEach(item => this.appendChild(item));
|
||||||
this.config.items.forEach(item => this.appendChild(item));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show(){
|
show(){
|
||||||
@ -432,8 +408,7 @@ class NjetGrid extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Njet.registerComponent('njet-grid', NjetGrid);
|
Njet.registerComponent('njet-grid', NjetGrid);
|
||||||
|
/*
|
||||||
/* Example usage:
|
|
||||||
const button = new NjetButton({
|
const button = new NjetButton({
|
||||||
classes: ['my-button'],
|
classes: ['my-button'],
|
||||||
text: 'Shared',
|
text: 'Shared',
|
||||||
@ -518,7 +493,7 @@ document.body.appendChild(dialog);
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class NjetComponent extends Component {}
|
class NjetComponent extends Component {}
|
||||||
const njet = Njet
|
const njet = Njet
|
||||||
njet.showDialog = function(args){
|
njet.showDialog = function(args){
|
||||||
const dialog = new NjetDialog(args)
|
const dialog = new NjetDialog(args)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
@ -570,16 +545,15 @@ njet.showWindow = function(args) {
|
|||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
njet.publish = function(event, data) {
|
njet.publish = function(event, data) {
|
||||||
if (this.root && this.root._subscriptions && this.root._subscriptions[event]) {
|
if (this.root._subscriptions[event]) {
|
||||||
this.root._subscriptions[event].forEach(callback => callback(data))
|
this.root._subscriptions[event].forEach(callback => callback(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
njet.subscribe = function(event, callback) {
|
njet.subscribe = function(event, callback) {
|
||||||
if (!this.root) return;
|
|
||||||
if (!this.root._subscriptions[event]) {
|
if (!this.root._subscriptions[event]) {
|
||||||
this.root._subscriptions[event] = []
|
this.root._subscriptions[event] = []
|
||||||
}
|
}
|
||||||
this.root._subscriptions[event].push(callback)
|
this.root._subscriptions[event].push(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow, eventBus };
|
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow,eventBus };
|
||||||
|
@ -142,9 +142,10 @@ export class Socket extends EventHandler {
|
|||||||
method,
|
method,
|
||||||
args,
|
args,
|
||||||
};
|
};
|
||||||
|
const me = this;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.addEventListener(call.callId, (data) => resolve(data), { once: true});
|
me.addEventListener(call.callId, (data) => resolve(data));
|
||||||
this.sendJson(call);
|
me.sendJson(call);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,38 +79,44 @@ emoji.EMOJI_DATA[
|
|||||||
] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]}
|
] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]}
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + ["picture"]
|
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
|
||||||
|
"img",
|
||||||
|
"video",
|
||||||
|
"audio",
|
||||||
|
"source",
|
||||||
|
"iframe",
|
||||||
|
"picture",
|
||||||
|
"span",
|
||||||
|
]
|
||||||
|
ALLOWED_ATTRIBUTES = {
|
||||||
|
**bleach.sanitizer.ALLOWED_ATTRIBUTES,
|
||||||
|
"img": ["src", "alt", "title", "width", "height"],
|
||||||
|
"a": ["href", "title", "target", "rel", "referrerpolicy", "class"],
|
||||||
|
"iframe": [
|
||||||
|
"src",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"frameborder",
|
||||||
|
"allow",
|
||||||
|
"allowfullscreen",
|
||||||
|
"title",
|
||||||
|
"referrerpolicy",
|
||||||
|
"style",
|
||||||
|
],
|
||||||
|
"video": ["src", "controls", "width", "height"],
|
||||||
|
"audio": ["src", "controls"],
|
||||||
|
"source": ["src", "type"],
|
||||||
|
"span": ["class"],
|
||||||
|
"picture": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def sanitize_html(value):
|
def sanitize_html(value):
|
||||||
|
|
||||||
soup = BeautifulSoup(value, 'html.parser')
|
|
||||||
|
|
||||||
for script in soup.find_all('script'):
|
|
||||||
script.decompose()
|
|
||||||
|
|
||||||
#for iframe in soup.find_all('iframe'):
|
|
||||||
#iframe.decompose()
|
|
||||||
|
|
||||||
for tag in soup.find_all(['object', 'embed']):
|
|
||||||
tag.decompose()
|
|
||||||
|
|
||||||
for tag in soup.find_all():
|
|
||||||
event_attributes = ['onclick', 'onerror', 'onload', 'onmouseover', 'onfocus']
|
|
||||||
for attr in event_attributes:
|
|
||||||
if attr in tag.attrs:
|
|
||||||
del tag[attr]
|
|
||||||
|
|
||||||
for img in soup.find_all('img'):
|
|
||||||
if 'onerror' in img.attrs:
|
|
||||||
img.decompose()
|
|
||||||
|
|
||||||
return soup.prettify()
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_html2(value):
|
|
||||||
return bleach.clean(
|
return bleach.clean(
|
||||||
value,
|
value,
|
||||||
protocols=list(bleach.sanitizer.ALLOWED_PROTOCOLS) + ["data"],
|
tags=ALLOWED_TAGS,
|
||||||
|
attributes=ALLOWED_ATTRIBUTES,
|
||||||
|
protocols=bleach.sanitizer.ALLOWED_PROTOCOLS + ["data"],
|
||||||
strip=True,
|
strip=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -126,8 +132,50 @@ def set_link_target_blank(text):
|
|||||||
|
|
||||||
return str(soup)
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
|
SAFE_ATTRIBUTES = {
|
||||||
|
"href",
|
||||||
|
"src",
|
||||||
|
"alt",
|
||||||
|
"title",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"style",
|
||||||
|
"id",
|
||||||
|
"class",
|
||||||
|
"rel",
|
||||||
|
"type",
|
||||||
|
"name",
|
||||||
|
"value",
|
||||||
|
"placeholder",
|
||||||
|
"aria-hidden",
|
||||||
|
"aria-label",
|
||||||
|
"srcset",
|
||||||
|
"target",
|
||||||
|
"rel",
|
||||||
|
"referrerpolicy",
|
||||||
|
"controls",
|
||||||
|
"frameborder",
|
||||||
|
"allow",
|
||||||
|
"allowfullscreen",
|
||||||
|
"referrerpolicy",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def whitelist_attributes(html):
|
def whitelist_attributes(html):
|
||||||
return sanitize_html(html)
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
|
||||||
|
for tag in soup.find_all():
|
||||||
|
if hasattr(tag, "attrs"):
|
||||||
|
if tag.name in ["script", "form", "input"]:
|
||||||
|
tag.replace_with("")
|
||||||
|
continue
|
||||||
|
attrs = dict(tag.attrs)
|
||||||
|
for attr in list(attrs):
|
||||||
|
# Check if attribute is in the safe list or is a data-* attribute
|
||||||
|
if not (attr in SAFE_ATTRIBUTES or attr.startswith("data-")):
|
||||||
|
del tag.attrs[attr]
|
||||||
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
def embed_youtube(text):
|
def embed_youtube(text):
|
||||||
|
@ -12,7 +12,7 @@ function showTerm(options){
|
|||||||
|
|
||||||
|
|
||||||
class StarField {
|
class StarField {
|
||||||
constructor({ count = 50, container = document.body } = {}) {
|
constructor({ count = 200, container = document.body } = {}) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.starCount = count;
|
this.starCount = count;
|
||||||
this.stars = [];
|
this.stars = [];
|
||||||
@ -567,7 +567,7 @@ const count = Array.from(messages).filter(el => el.textContent.trim() === text).
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const starField = new StarField({starCount: 50});
|
const starField = new StarField({starCount: 200});
|
||||||
app.starField = starField;
|
app.starField = starField;
|
||||||
|
|
||||||
class DemoSequence {
|
class DemoSequence {
|
||||||
|
@ -29,7 +29,7 @@ class ChannelDriveApiView(DriveApiView):
|
|||||||
|
|
||||||
class ChannelAttachmentView(BaseView):
|
class ChannelAttachmentView(BaseView):
|
||||||
|
|
||||||
login_required=False
|
login_required=True
|
||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
relative_path = self.request.match_info.get("relative_url")
|
relative_path = self.request.match_info.get("relative_url")
|
||||||
|
@ -11,7 +11,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import random
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from snek.system.model import now
|
from snek.system.model import now
|
||||||
@ -529,8 +529,8 @@ class RPCView(BaseView):
|
|||||||
try:
|
try:
|
||||||
await self.ws.send_str(json.dumps(obj, default=str))
|
await self.ws.send_str(json.dumps(obj, default=str))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("THIS IS THE DeAL>",str(ex), flush=True)
|
|
||||||
await self.services.socket.delete(self.ws)
|
await self.services.socket.delete(self.ws)
|
||||||
|
await self.ws.close()
|
||||||
|
|
||||||
async def get_online_users(self, channel_uid):
|
async def get_online_users(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
@ -638,7 +638,7 @@ class RPCView(BaseView):
|
|||||||
try:
|
try:
|
||||||
await rpc(msg.json())
|
await rpc(msg.json())
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("XXXXXXXXXX Deleting socket", ex, flush=True)
|
print("Deleting socket", ex, flush=True)
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
await self.services.socket.delete(ws)
|
await self.services.socket.delete(ws)
|
||||||
break
|
break
|
||||||
|
@ -55,7 +55,7 @@ class WebView(BaseView):
|
|||||||
user_uid=self.session.get("uid"), channel_uid=channel["uid"]
|
user_uid=self.session.get("uid"), channel_uid=channel["uid"]
|
||||||
)
|
)
|
||||||
if not channel_member:
|
if not channel_member:
|
||||||
if not channel["is_private"] and not channel.is_dm:
|
if not channel["is_private"]:
|
||||||
channel_member = await self.app.services.channel_member.create(
|
channel_member = await self.app.services.channel_member.create(
|
||||||
channel_uid=channel["uid"],
|
channel_uid=channel["uid"],
|
||||||
user_uid=self.session.get("uid"),
|
user_uid=self.session.get("uid"),
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user