Implement attachment view
This commit is contained in:
@@ -8,7 +8,13 @@ from asgiref.sync import sync_to_async
|
||||
from django.core import signing
|
||||
|
||||
from core.models import ChatSession, Message, PersonIdentifier
|
||||
from core.views.compose import COMPOSE_WS_TOKEN_SALT
|
||||
from core.realtime.typing_state import get_person_typing_state
|
||||
from core.views.compose import (
|
||||
COMPOSE_WS_TOKEN_SALT,
|
||||
_image_urls_from_text,
|
||||
_is_url_only_text,
|
||||
_looks_like_image_url,
|
||||
)
|
||||
|
||||
|
||||
def _safe_int(value, default=0):
|
||||
@@ -28,11 +34,24 @@ def _fmt_ts(ts_value):
|
||||
|
||||
def _serialize_message(msg):
|
||||
author = str(msg.custom_author or "").strip()
|
||||
text_value = str(msg.text or "")
|
||||
image_urls = _image_urls_from_text(text_value)
|
||||
image_url = image_urls[0] if image_urls else ""
|
||||
hide_text = bool(
|
||||
image_urls
|
||||
and _is_url_only_text(text_value)
|
||||
and all(_looks_like_image_url(url) for url in image_urls)
|
||||
)
|
||||
display_text = text_value if text_value.strip() else ("(no text)" if not image_url else "")
|
||||
return {
|
||||
"id": str(msg.id),
|
||||
"ts": int(msg.ts or 0),
|
||||
"display_ts": _fmt_ts(msg.ts),
|
||||
"text": str(msg.text or ""),
|
||||
"text": text_value,
|
||||
"display_text": display_text,
|
||||
"image_url": image_url,
|
||||
"image_urls": image_urls,
|
||||
"hide_text": hide_text,
|
||||
"author": author,
|
||||
"outgoing": author.upper() in {"USER", "BOT"},
|
||||
}
|
||||
@@ -59,14 +78,18 @@ def _load_since(user_id, service, identifier, person_id, after_ts, limit):
|
||||
identifier=identifier,
|
||||
).first()
|
||||
if person_identifier is None:
|
||||
return {"messages": [], "last_ts": after_ts}
|
||||
return {"messages": [], "last_ts": after_ts, "person_id": 0}
|
||||
|
||||
session = ChatSession.objects.filter(
|
||||
user_id=user_id,
|
||||
identifier=person_identifier,
|
||||
).first()
|
||||
if session is None:
|
||||
return {"messages": [], "last_ts": after_ts}
|
||||
return {
|
||||
"messages": [],
|
||||
"last_ts": after_ts,
|
||||
"person_id": int(person_identifier.person_id),
|
||||
}
|
||||
|
||||
qs = Message.objects.filter(
|
||||
user_id=user_id,
|
||||
@@ -85,6 +108,7 @@ def _load_since(user_id, service, identifier, person_id, after_ts, limit):
|
||||
return {
|
||||
"messages": [_serialize_message(row) for row in rows],
|
||||
"last_ts": int(newest or after_ts or 0),
|
||||
"person_id": int(person_identifier.person_id),
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +133,7 @@ async def compose_ws_application(scope, receive, send):
|
||||
service = str(payload.get("s") or "").strip()
|
||||
identifier = str(payload.get("i") or "").strip()
|
||||
person_id = str(payload.get("p") or "").strip()
|
||||
resolved_person_id = _safe_int(person_id)
|
||||
|
||||
if user_id <= 0 or (not identifier and not person_id):
|
||||
await send({"type": "websocket.close", "code": 4401})
|
||||
@@ -117,6 +142,7 @@ async def compose_ws_application(scope, receive, send):
|
||||
await send({"type": "websocket.accept"})
|
||||
last_ts = 0
|
||||
limit = 100
|
||||
last_typing_key = ""
|
||||
|
||||
while True:
|
||||
event = None
|
||||
@@ -145,18 +171,33 @@ async def compose_ws_application(scope, receive, send):
|
||||
)
|
||||
messages = payload.get("messages") or []
|
||||
latest = _safe_int(payload.get("last_ts"), last_ts)
|
||||
if resolved_person_id <= 0:
|
||||
resolved_person_id = _safe_int(payload.get("person_id"), 0)
|
||||
typing_state = get_person_typing_state(
|
||||
user_id=user_id,
|
||||
person_id=resolved_person_id,
|
||||
)
|
||||
typing_key = json.dumps(typing_state, sort_keys=True)
|
||||
typing_changed = typing_key != last_typing_key
|
||||
if typing_changed:
|
||||
last_typing_key = typing_key
|
||||
|
||||
outgoing_payload = {}
|
||||
if messages:
|
||||
last_ts = max(last_ts, latest)
|
||||
outgoing_payload["messages"] = messages
|
||||
outgoing_payload["last_ts"] = last_ts
|
||||
else:
|
||||
last_ts = max(last_ts, latest)
|
||||
outgoing_payload["last_ts"] = last_ts
|
||||
|
||||
if typing_changed:
|
||||
outgoing_payload["typing"] = typing_state
|
||||
|
||||
if messages or typing_changed:
|
||||
await send(
|
||||
{
|
||||
"type": "websocket.send",
|
||||
"text": json.dumps(
|
||||
{
|
||||
"messages": messages,
|
||||
"last_ts": last_ts,
|
||||
}
|
||||
),
|
||||
"text": json.dumps(outgoing_payload),
|
||||
}
|
||||
)
|
||||
else:
|
||||
last_ts = max(last_ts, latest)
|
||||
|
||||
74
core/realtime/typing_state.py
Normal file
74
core/realtime/typing_state.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
TYPING_TTL_SECONDS = 12
|
||||
|
||||
|
||||
def _person_key(user_id, person_id):
|
||||
return f"compose:typing:user:{int(user_id)}:person:{int(person_id)}"
|
||||
|
||||
|
||||
def set_person_typing_state(
|
||||
*,
|
||||
user_id,
|
||||
person_id,
|
||||
started,
|
||||
source_service="",
|
||||
display_name="",
|
||||
):
|
||||
if not user_id or not person_id:
|
||||
return
|
||||
now_ms = int(time.time() * 1000)
|
||||
state = {
|
||||
"typing": bool(started),
|
||||
"source_service": str(source_service or ""),
|
||||
"display_name": str(display_name or ""),
|
||||
"updated_ts": now_ms,
|
||||
"expires_ts": (
|
||||
now_ms + (TYPING_TTL_SECONDS * 1000) if started else now_ms
|
||||
),
|
||||
}
|
||||
cache.set(
|
||||
_person_key(user_id, person_id),
|
||||
state,
|
||||
timeout=max(TYPING_TTL_SECONDS * 2, 30),
|
||||
)
|
||||
|
||||
|
||||
def get_person_typing_state(*, user_id, person_id):
|
||||
if not user_id or not person_id:
|
||||
return {
|
||||
"typing": False,
|
||||
"source_service": "",
|
||||
"display_name": "",
|
||||
"updated_ts": 0,
|
||||
"expires_ts": 0,
|
||||
}
|
||||
|
||||
key = _person_key(user_id, person_id)
|
||||
state = dict(cache.get(key) or {})
|
||||
if not state:
|
||||
return {
|
||||
"typing": False,
|
||||
"source_service": "",
|
||||
"display_name": "",
|
||||
"updated_ts": 0,
|
||||
"expires_ts": 0,
|
||||
}
|
||||
|
||||
now_ms = int(time.time() * 1000)
|
||||
is_typing = bool(state.get("typing"))
|
||||
expires_ts = int(state.get("expires_ts") or 0)
|
||||
if is_typing and expires_ts and now_ms > expires_ts:
|
||||
state["typing"] = False
|
||||
state["updated_ts"] = now_ms
|
||||
cache.set(key, state, timeout=max(TYPING_TTL_SECONDS * 2, 30))
|
||||
|
||||
return {
|
||||
"typing": bool(state.get("typing")),
|
||||
"source_service": str(state.get("source_service") or ""),
|
||||
"display_name": str(state.get("display_name") or ""),
|
||||
"updated_ts": int(state.get("updated_ts") or 0),
|
||||
"expires_ts": int(state.get("expires_ts") or 0),
|
||||
}
|
||||
Reference in New Issue
Block a user