Update live type.
This commit is contained in:
parent
af1cf4f5ae
commit
db6d6c0106
src/snek
123
src/snek/balancer.py
Normal file
123
src/snek/balancer.py
Normal file
@ -0,0 +1,123 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
class LoadBalancer:
|
||||
def __init__(self, backend_ports):
|
||||
self.backend_ports = backend_ports
|
||||
self.backend_processes = []
|
||||
self.client_counts = [0] * len(backend_ports)
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
async def start_backend_servers(self,port,workers):
|
||||
for x in range(workers):
|
||||
port += 1
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
sys.executable,
|
||||
sys.argv[0],
|
||||
'backend',
|
||||
str(port),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
port += 1
|
||||
self.backend_processes.append(process)
|
||||
print(f"Started backend server on port {(port-1)/port} with PID {process.pid}")
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
async with self.lock:
|
||||
min_clients = min(self.client_counts)
|
||||
server_index = self.client_counts.index(min_clients)
|
||||
self.client_counts[server_index] += 1
|
||||
backend = ('127.0.0.1', self.backend_ports[server_index])
|
||||
try:
|
||||
backend_reader, backend_writer = await asyncio.open_connection(*backend)
|
||||
|
||||
async def forward(r, w):
|
||||
try:
|
||||
while True:
|
||||
data = await r.read(1024)
|
||||
if not data:
|
||||
break
|
||||
w.write(data)
|
||||
await w.drain()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
w.close()
|
||||
|
||||
task1 = asyncio.create_task(forward(reader, backend_writer))
|
||||
task2 = asyncio.create_task(forward(backend_reader, writer))
|
||||
await asyncio.gather(task1, task2)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
writer.close()
|
||||
async with self.lock:
|
||||
self.client_counts[server_index] -= 1
|
||||
|
||||
async def monitor(self):
|
||||
while True:
|
||||
await asyncio.sleep(5)
|
||||
print("Connected clients per server:")
|
||||
for i, count in enumerate(self.client_counts):
|
||||
print(f"Server {self.backend_ports[i]}: {count} clients")
|
||||
|
||||
async def start(self, host='0.0.0.0', port=8081,workers=5):
|
||||
await self.start_backend_servers(port,workers)
|
||||
server = await asyncio.start_server(self.handle_client, host, port)
|
||||
monitor_task = asyncio.create_task(self.monitor())
|
||||
|
||||
# Handle shutdown gracefully
|
||||
try:
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
# Terminate backend processes
|
||||
for process in self.backend_processes:
|
||||
process.terminate()
|
||||
await asyncio.gather(*(p.wait() for p in self.backend_processes))
|
||||
print("Backend processes terminated.")
|
||||
|
||||
async def backend_echo_server(port):
|
||||
async def handle_echo(reader, writer):
|
||||
try:
|
||||
while True:
|
||||
data = await reader.read(1024)
|
||||
if not data:
|
||||
break
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
writer.close()
|
||||
|
||||
server = await asyncio.start_server(handle_echo, '127.0.0.1', port)
|
||||
print(f"Backend echo server running on port {port}")
|
||||
await server.serve_forever()
|
||||
|
||||
async def main():
|
||||
backend_ports = [8001, 8003, 8005, 8006]
|
||||
# Launch backend echo servers
|
||||
# Wait a moment for servers to start
|
||||
lb = LoadBalancer(backend_ports)
|
||||
await lb.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == 'backend':
|
||||
port = int(sys.argv[2])
|
||||
from snek.app import Application
|
||||
snek = Application(port=port)
|
||||
web.run_app(snek, port=port, host='127.0.0.1')
|
||||
elif sys.argv[1] == 'sync':
|
||||
from snek.sync import app
|
||||
web.run_app(snek, port=port, host='127.0.0.1')
|
||||
else:
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("Shutting down...")
|
||||
|
@ -30,7 +30,7 @@ class ChannelMessageService(BaseService):
|
||||
except Exception as ex:
|
||||
print(ex, flush=True)
|
||||
|
||||
if await self.save(model):
|
||||
if await super().save(model):
|
||||
return model
|
||||
raise Exception(f"Failed to create channel message: {model.errors}.")
|
||||
|
||||
@ -50,6 +50,12 @@ class ChannelMessageService(BaseService):
|
||||
"username": user["username"],
|
||||
}
|
||||
|
||||
async def save(self, model):
|
||||
context = model.record
|
||||
template = self.app.jinja2_env.get_template("message.html")
|
||||
model["html"] = template.render(**context)
|
||||
return await super().save(model)
|
||||
|
||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||
results = []
|
||||
offset = page * page_size
|
||||
|
@ -36,4 +36,4 @@ class ChatService(BaseService):
|
||||
self.services.notification.create_channel_message(channel_message_uid)
|
||||
)
|
||||
|
||||
return True
|
||||
return channel_message
|
||||
|
@ -5,8 +5,64 @@
|
||||
// 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.
|
||||
import {app} from '../app.js'
|
||||
class MessageList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
app.ws.addEventListener("update_message_text",(data)=>{
|
||||
this.updateMessageText(data.data.message_uid,data.data.text)
|
||||
})
|
||||
app.ws.addEventListener("set_typing",(data)=>{
|
||||
this.triggerGlow(data.data.user_uid)
|
||||
|
||||
class MessageListElement extends HTMLElement {
|
||||
})
|
||||
|
||||
this.items = [];
|
||||
}
|
||||
updateMessageText(uid,text){
|
||||
const messageDiv = this.querySelector("div[data-uid=\""+uid+"\"]")
|
||||
if(!messageDiv){
|
||||
return
|
||||
}
|
||||
const textElement = messageDiv.querySelector(".text")
|
||||
textElement.innerText = text
|
||||
textElement.style.display = text == '' ? 'none' : 'block'
|
||||
|
||||
}
|
||||
triggerGlow(uid) {
|
||||
let lastElement = null;
|
||||
this.querySelectorAll(".avatar").forEach((el)=>{
|
||||
const div = el.closest('a');
|
||||
if(el.href.indexOf(uid)!=-1){
|
||||
lastElement = el
|
||||
}
|
||||
|
||||
})
|
||||
if(lastElement){
|
||||
lastElement.classList.add("glow")
|
||||
setTimeout(()=>{
|
||||
lastElement.classList.remove("glow")
|
||||
},1000)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
set data(items) {
|
||||
this.items = items;
|
||||
this.render();
|
||||
}
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
|
||||
//this.insertAdjacentHTML("beforeend", html);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define('message-list', MessageList);
|
||||
|
||||
class MessageListElementOLD extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ["messages"];
|
||||
}
|
||||
@ -167,4 +223,4 @@ class MessageListElement extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('message-list', MessageListElement);
|
||||
//customElements.define('message-list', MessageListElement);
|
||||
|
1
src/snek/static/online-users.js
Normal file
1
src/snek/static/online-users.js
Normal file
@ -0,0 +1 @@
|
||||
|
28
src/snek/static/user-list.css
Normal file
28
src/snek/static/user-list.css
Normal file
@ -0,0 +1,28 @@
|
||||
.user-list__item {
|
||||
display: flex;
|
||||
margin-bottom: 1em;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.user-list__item-avatar {
|
||||
margin-right: 10px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: block;
|
||||
}
|
||||
.user-list__item-content {
|
||||
flex: 1;
|
||||
}
|
||||
.user-list__item-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.user-list__item-text {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.user-list__item-time {
|
||||
font-size: 0.8em;
|
||||
color: gray;
|
||||
}
|
59
src/snek/static/user-list.js
Normal file
59
src/snek/static/user-list.js
Normal file
@ -0,0 +1,59 @@
|
||||
class UserList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.users = [];
|
||||
}
|
||||
|
||||
set data(userArray) {
|
||||
this.users = userArray;
|
||||
this.render();
|
||||
}
|
||||
|
||||
formatRelativeTime(timestamp) {
|
||||
const now = new Date();
|
||||
const msgTime = new Date(timestamp);
|
||||
const diffMs = now - msgTime;
|
||||
const minutes = Math.floor(diffMs / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${days} day${days > 1 ? 's' : ''} ago`;
|
||||
} else if (hours > 0) {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
} else {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${minutes} min ago`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
|
||||
this.users.forEach(user => {
|
||||
const html = `
|
||||
<div class="user-list__item"
|
||||
data-uid="${user.uid}"
|
||||
data-color="${user.color}"
|
||||
data-user_nick="${user.nick}"
|
||||
data-created_at="${user.created_at}"
|
||||
data-user_uid="${user.user_uid}">
|
||||
|
||||
<a class="user-list__item-avatar" style="background-color: ${user.color}; color: black;" href="/user/${user.uid}.html">
|
||||
<img width="40px" height="40px" src="/avatar/${user.uid}.svg" alt="${user.nick}">
|
||||
</a>
|
||||
|
||||
<div class="user-list__item-content">
|
||||
<div class="user-list__item-name" style="color: ${user.color};">${user.nick}</div>
|
||||
<div class="user-list__item-time" data-created_at="${user.last_ping}">
|
||||
<a href="/user/${user.uid}.html">profile</a>
|
||||
<a href="/channel/${user.uid}.html">dm</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.insertAdjacentHTML("beforeend", html);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('user-list', UserList);
|
135
src/snek/sync.py
Normal file
135
src/snek/sync.py
Normal file
@ -0,0 +1,135 @@
|
||||
|
||||
|
||||
|
||||
|
||||
class DatasetWebSocketView:
|
||||
def __init__(self):
|
||||
self.ws = None
|
||||
self.db = dataset.connect('sqlite:///snek.db')
|
||||
self.setattr(self, "db", self.get)
|
||||
self.setattr(self, "db", self.set)
|
||||
)
|
||||
super()
|
||||
|
||||
def format_result(self, result):
|
||||
|
||||
try:
|
||||
return dict(result)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
return [dict(row) for row in result]
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
async def send_str(self, msg):
|
||||
return await self.ws.send_str(msg)
|
||||
|
||||
def get(self, key):
|
||||
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
|
||||
|
||||
def set(self, key, value):
|
||||
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
|
||||
|
||||
|
||||
|
||||
async def handle(self, request):
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
self.ws = ws
|
||||
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
try:
|
||||
data = json.loads(msg.data)
|
||||
call_uid = data.get("call_uid")
|
||||
method = data.get("method")
|
||||
table_name = data.get("table")
|
||||
args = data.get("args", {})
|
||||
kwargs = data.get("kwargs", {})
|
||||
|
||||
|
||||
function = getattr(self.db, method, None)
|
||||
if table_name:
|
||||
function = getattr(self.db[table_name], method, None)
|
||||
|
||||
print(method, table_name, args, kwargs,flush=True)
|
||||
|
||||
if function:
|
||||
response = {}
|
||||
try:
|
||||
result = function(*args, **kwargs)
|
||||
print(result)
|
||||
response['result'] = self.format_result(result)
|
||||
response["call_uid"] = call_uid
|
||||
response["success"] = True
|
||||
except Exception as e:
|
||||
response["call_uid"] = call_uid
|
||||
response["success"] = False
|
||||
response["error"] = str(e)
|
||||
response["traceback"] = traceback.format_exc()
|
||||
|
||||
if call_uid:
|
||||
await self.send_str(json.dumps(response,default=str))
|
||||
else:
|
||||
await self.send_str(json.dumps({"status": "error", "error":"Method not found.","call_uid": call_uid}))
|
||||
except Exception as e:
|
||||
await self.send_str(json.dumps({"success": False,"call_uid": call_uid, "error": str(e), "error": str(e), "traceback": traceback.format_exc()},default=str))
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
print('ws connection closed with exception %s' % ws.exception())
|
||||
|
||||
return ws
|
||||
|
||||
class BroadCastSocketView:
|
||||
def __init__(self):
|
||||
self.ws = None
|
||||
super()
|
||||
|
||||
def format_result(self, result):
|
||||
|
||||
try:
|
||||
return dict(result)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
return [dict(row) for row in result]
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
async def send_str(self, msg):
|
||||
return await self.ws.send_str(msg)
|
||||
|
||||
def get(self, key):
|
||||
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
|
||||
|
||||
def set(self, key, value):
|
||||
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
|
||||
|
||||
|
||||
|
||||
async def handle(self, request):
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
self.ws = ws
|
||||
app = request.app
|
||||
app['broadcast_clients'].append(ws)
|
||||
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
print(msg.data)
|
||||
for client in app['broadcast_clients'] if not client == ws:
|
||||
await client.send_str(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
print('ws connection closed with exception %s' % ws.exception())
|
||||
app['broadcast_clients'].remove(ws)
|
||||
return ws
|
||||
|
||||
|
||||
app = web.Application()
|
||||
view = DatasetWebSocketView()
|
||||
app['broadcast_clients'] = []
|
||||
app.router.add_get('/db', view.handle)
|
||||
app.router.add_get('/broadcast', sync_view.handle)
|
||||
|
@ -16,6 +16,10 @@
|
||||
<script src="/html-frame.js" type="module"></script>
|
||||
<script src="/app.js" type="module"></script>
|
||||
<script src="/file-manager.js" type="module"></script>
|
||||
<script src="/user-list.js"></script>
|
||||
<script src="/message-list.js" type="module"></script>
|
||||
<link rel="stylesheet" href="/user-list.css">
|
||||
|
||||
<link rel="stylesheet" href="/base.css">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
|
117
src/snek/templates/online.html
Normal file
117
src/snek/templates/online.html
Normal file
@ -0,0 +1,117 @@
|
||||
<style>
|
||||
/* Ensure the dialog appears centered when open as modal */
|
||||
#online-users {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
background-color: #111; /* Deep black */
|
||||
color: #f1f1f1;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.8);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
|
||||
animation: fadeIn 0.3s ease-out, scaleIn 0.3s ease-out;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Backdrop styling */
|
||||
#online-users::backdrop {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Title and content */
|
||||
#online-users .dialog-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#online-users .dialog-content {
|
||||
font-size: 1rem;
|
||||
color: #ccc;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Button layout */
|
||||
#online-users .dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
#online-users .dialog-button {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.95rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
#online-users .dialog-button.primary {
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#online-users .dialog-button.primary:hover {
|
||||
background-color: #4338ca;
|
||||
}
|
||||
|
||||
#online-users .dialog-button.secondary {
|
||||
background-color: #333;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
#online-users .dialog-button.secondary:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from { transform: scale(0.95) translate(-50%, -50%); opacity: 0; }
|
||||
to { transform: scale(1) translate(-50%, -50%); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<dialog id="online-users">
|
||||
<div class="dialog-backdrop">
|
||||
<div class="dialog-box">
|
||||
<div class="dialog-title"><h2>Currently online</h2></div>
|
||||
<div class="dialog-content"><user-list></user-list></div>
|
||||
<div class="dialog-actions">
|
||||
<button class="dialog-button primary">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
const onlineDialog = document.getElementById("online-users");
|
||||
const dialogButton = onlineDialog.querySelector('.dialog-button.primary');
|
||||
|
||||
dialogButton.addEventListener('click', () => {
|
||||
onlineDialog.close();
|
||||
});
|
||||
|
||||
async function showOnlineUsers() {
|
||||
const users = await app.rpc.getOnlineUsers('{{ channel.uid.value }}');
|
||||
onlineDialog.querySelector('user-list').data = users;
|
||||
onlineDialog.showModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -9,19 +9,19 @@
|
||||
|
||||
|
||||
<section class="chat-area">
|
||||
<div class="chat-messages">
|
||||
{% for message in messages %}
|
||||
{% autoescape false %}
|
||||
{{ message.html }}
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<message-list class="chat-messages">
|
||||
{% for message in messages %}
|
||||
{% autoescape false %}
|
||||
{{ message.html }}
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
</message-list>
|
||||
<div class="chat-input">
|
||||
<textarea placeholder="Type a message..." rows="2"></textarea>
|
||||
<textarea list="chat-input-autocomplete-items" placeholder="Type a message..." rows="2" autocomplete="on"></textarea>
|
||||
<upload-button channel="{{ channel.uid.value }}"></upload-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include "online.html" %}
|
||||
<script type="module">
|
||||
import { app } from "/app.js";
|
||||
import { Schedule } from "/schedule.js";
|
||||
@ -30,18 +30,95 @@
|
||||
function getInputField(){
|
||||
return document.querySelector("textarea")
|
||||
}
|
||||
|
||||
getInputField().autoComplete = {
|
||||
"/online": () =>{
|
||||
showOnlineUsers();
|
||||
},
|
||||
"/clear": () => {
|
||||
document.querySelector(".chat-messages").innerHTML = '';
|
||||
},
|
||||
"/live": () =>{
|
||||
getInputField().liveType = !getInputField().liveType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function initInputField(textBox) {
|
||||
textBox.addEventListener('keydown', (e) => {
|
||||
if(textBox.liveType == undefined){
|
||||
textBox.liveType = false
|
||||
}
|
||||
textBox.addEventListener('keydown',async (e) => {
|
||||
if(e.key === "ArrowUp"){
|
||||
const value = findDivAboveText(e.target.value).querySelector('.text')
|
||||
e.target.value = value.textContent
|
||||
console.info("HIERR")
|
||||
return
|
||||
}
|
||||
if (e.key === "Tab") {
|
||||
|
||||
const message = e.target.value.trim();
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
let autoCompleteHandler = null;
|
||||
Object.keys(e.target.autoComplete).forEach((key)=>{
|
||||
if(key.startsWith(message)){
|
||||
if(autoCompleteHandler){
|
||||
return
|
||||
}
|
||||
autoCompleteHandler = key
|
||||
}
|
||||
})
|
||||
if(autoCompleteHandler){
|
||||
e.preventDefault();
|
||||
e.target.value = autoCompleteHandler;
|
||||
return
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const message = e.target.value.trim();
|
||||
if (message) {
|
||||
app.rpc.sendMessage(channelUid, message);
|
||||
e.target.value = '';
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
let autoCompleteHandler = e.target.autoComplete[message]
|
||||
if(autoCompleteHandler){
|
||||
const value = message;
|
||||
e.target.value = '';
|
||||
autoCompleteHandler(value)
|
||||
return
|
||||
}
|
||||
|
||||
e.target.value = '';
|
||||
if(textBox.liveType && textBox.lastMessageUid && textBox.lastMessageUid != '?'){
|
||||
|
||||
|
||||
app.rpc.updateMessageText(textBox.lastMessageUid, message)
|
||||
textBox.lastMessageUid = null
|
||||
return
|
||||
}
|
||||
|
||||
const messageResponse = await app.rpc.sendMessage(channelUid, message);
|
||||
|
||||
}else{
|
||||
app.rpc.set_typing(channelUid)
|
||||
if(textBox.liveType){
|
||||
|
||||
if(e.target.value[0] == "/"){
|
||||
return
|
||||
}
|
||||
if(!textBox.lastMessageUid){
|
||||
textBox.lastMessageUid = '?'
|
||||
app.rpc.sendMessage(channelUid, e.target.value).then((messageResponse)=>{
|
||||
textBox.lastMessageUid = messageResponse
|
||||
})
|
||||
}
|
||||
if(textBox.lastMessageUid == '?'){
|
||||
return;
|
||||
}
|
||||
app.rpc.updateMessageText(textBox.lastMessageUid, e.target.value)
|
||||
}
|
||||
app.rpc.set_typing(channelUid)
|
||||
|
||||
}
|
||||
});
|
||||
document.querySelector("upload-button").addEventListener("upload",function(e){
|
||||
@ -81,29 +158,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
function triggerGlow(uid) {
|
||||
document.querySelectorAll(".avatar").forEach((el)=>{
|
||||
const div = el.closest('a');
|
||||
if(el.href.indexOf(uid)!=-1){
|
||||
el.classList.add('glow')
|
||||
let originalColor = el.style.backgroundColor
|
||||
//console.error(originalColor)
|
||||
//el.style.backgroundColor = 'black'
|
||||
setTimeout(()=>{
|
||||
// el.style.backgroundColor = originalColor
|
||||
// console.error(el.style.backgroundColor)
|
||||
el.classList.remove('glow')
|
||||
},1200)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
app.ws.addEventListener("set_typing",(data)=>{
|
||||
triggerGlow(data.data.user_uid)
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
const chatInput = document.querySelector(".chat-area")
|
||||
chatInput.addEventListener("drop", async (e) => {
|
||||
|
@ -16,6 +16,9 @@ from snek.system.model import now
|
||||
from snek.system.profiler import Profiler
|
||||
from snek.system.view import BaseView
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RPCView(BaseView):
|
||||
|
||||
@ -170,10 +173,33 @@ class RPCView(BaseView):
|
||||
)
|
||||
return channels
|
||||
|
||||
async def update_message_text(self,message_uid, text):
|
||||
self._require_login()
|
||||
message = await self.services.channel_message.get(message_uid)
|
||||
if message["user_uid"] != self.user_uid:
|
||||
raise Exception("Not allowed")
|
||||
await self.services.socket.broadcast(message["channel_uid"], {
|
||||
"channel_uid": message["channel_uid"],
|
||||
"event": "update_message_text",
|
||||
"data": {
|
||||
|
||||
"message_uid": message_uid,
|
||||
"text": text
|
||||
}
|
||||
})
|
||||
message["message"] = text
|
||||
if not text:
|
||||
message['deleted_at'] = now()
|
||||
else:
|
||||
message['deleted_at'] = None
|
||||
await self.services.channel_message.save(message)
|
||||
return True
|
||||
|
||||
async def send_message(self, channel_uid, message):
|
||||
self._require_login()
|
||||
await self.services.chat.send(self.user_uid, channel_uid, message)
|
||||
return True
|
||||
message = await self.services.chat.send(self.user_uid, channel_uid, message)
|
||||
|
||||
return message["uid"]
|
||||
|
||||
async def echo(self, *args):
|
||||
self._require_login()
|
||||
@ -243,12 +269,14 @@ class RPCView(BaseView):
|
||||
except Exception as ex:
|
||||
result = {"exception": str(ex), "traceback": traceback.format_exc()}
|
||||
success = False
|
||||
logger.exception(ex)
|
||||
if result != "noresponse":
|
||||
await self._send_json(
|
||||
{"callId": call_id, "success": success, "data": result}
|
||||
)
|
||||
except Exception as ex:
|
||||
print(str(ex), flush=True)
|
||||
logger.exception(ex)
|
||||
await self._send_json(
|
||||
{"callId": call_id, "success": False, "data": str(ex)}
|
||||
)
|
||||
@ -259,15 +287,15 @@ class RPCView(BaseView):
|
||||
async def get_online_users(self, channel_uid):
|
||||
self._require_login()
|
||||
|
||||
return [
|
||||
{
|
||||
"uid": record["uid"],
|
||||
"username": record["username"],
|
||||
"nick": record["nick"],
|
||||
"last_ping": record["last_ping"],
|
||||
}
|
||||
async for record in self.services.channel.get_online_users(channel_uid)
|
||||
results = [
|
||||
record.record async for record in self.services.channel.get_online_users(channel_uid)
|
||||
]
|
||||
for result in results:
|
||||
del result['email']
|
||||
del result['password']
|
||||
del result['deleted_at']
|
||||
del result['updated_at']
|
||||
return results
|
||||
|
||||
async def echo(self, obj):
|
||||
await self.ws.send_json(obj)
|
||||
@ -314,6 +342,7 @@ class RPCView(BaseView):
|
||||
await rpc(msg.json())
|
||||
except Exception as ex:
|
||||
print("Deleting socket", ex, flush=True)
|
||||
logger.exception(ex)
|
||||
await self.services.socket.delete(ws)
|
||||
break
|
||||
elif msg.type == web.WSMsgType.ERROR:
|
||||
|
Loading…
Reference in New Issue
Block a user