Allow linking chats

This commit is contained in:
2026-02-19 17:13:34 +00:00
parent bac2841298
commit 0816687c71
15 changed files with 369 additions and 461 deletions

View File

@@ -38,6 +38,7 @@ from core.models import (
PatternMitigationPlan,
Person,
PersonIdentifier,
PlatformChatLink,
WorkspaceConversation,
)
from core.realtime.typing_state import get_person_typing_state
@@ -1469,11 +1470,31 @@ def _context_base(user, service, identifier, person):
identifier = person_identifier.identifier
person = person_identifier.person
if person_identifier is None and identifier:
bare_id = identifier.split("@", 1)[0].strip()
group_link = PlatformChatLink.objects.filter(
user=user,
service=service,
chat_identifier=bare_id,
is_group=True,
).first()
if group_link:
return {
"person_identifier": None,
"service": service,
"identifier": f"{bare_id}@g.us",
"person": None,
"is_group": True,
"group_name": group_link.chat_name or bare_id,
}
return {
"person_identifier": person_identifier,
"service": service,
"identifier": identifier,
"person": person,
"is_group": False,
"group_name": "",
}
@@ -2095,6 +2116,8 @@ def _panel_context(
"typing_state_json": json.dumps(typing_state),
"platform_options": platform_options,
"recent_contacts": recent_contacts,
"is_group": base.get("is_group", False),
"group_name": base.get("group_name", ""),
}

View File

@@ -9,7 +9,7 @@ from django.views import View
from mixins.views import ObjectList, ObjectRead
from core.clients import transport
from core.models import Chat, PersonIdentifier
from core.models import Chat, PersonIdentifier, PlatformChatLink
from core.views.manage.permissions import SuperUserRequiredMixin
@@ -199,6 +199,31 @@ class SignalChatsList(SuperUserRequiredMixin, ObjectList):
),
}
)
for link in PlatformChatLink.objects.filter(
user=self.request.user,
service="signal",
is_group=True,
):
group_id = str(link.chat_identifier or "").strip()
if not group_id:
continue
rows.append(
{
"chat": None,
"compose_page_url": "",
"compose_widget_url": "",
"ai_url": reverse("ai_workspace"),
"person_name": "",
"manual_icon_class": "fa-solid fa-users",
"can_compose": False,
"match_url": "",
"is_group": True,
"name": link.chat_name or group_id,
"identifier": group_id,
}
)
return rows

View File

@@ -7,7 +7,7 @@ from django.views import View
from mixins.views import ObjectList, ObjectRead
from core.clients import transport
from core.models import PersonIdentifier
from core.models import PersonIdentifier, PlatformChatLink
from core.util import logs
from core.views.compose import _compose_urls, _service_icon_class
from core.views.manage.permissions import SuperUserRequiredMixin
@@ -288,7 +288,10 @@ class WhatsAppChatsList(WhatsAppContactsList):
identifier = str(key or "").strip()
if not identifier:
continue
if "@newsletter" in identifier:
continue
identifier = identifier.split("@", 1)[0].strip() or identifier
identifier = identifier.lstrip("+")
if identifier in seen:
continue
seen.add(identifier)
@@ -327,50 +330,88 @@ class WhatsAppChatsList(WhatsAppContactsList):
if rows:
rows.sort(key=lambda row: row.get("last_ts", 0), reverse=True)
return rows
else:
# Fallback: if no anchors yet, surface the runtime contacts (best effort live state)
for item in combined_contacts:
raw_item_id = str(
item.get("identifier") or item.get("jid") or item.get("chat") or ""
).strip()
if "@newsletter" in raw_item_id:
continue
identifier = raw_item_id
if not identifier:
continue
identifier = identifier.split("@", 1)[0].strip()
if not identifier or identifier in seen:
continue
seen.add(identifier)
jid = str(item.get("jid") or "").strip()
linked = self._linked_identifier(identifier, jid)
urls = _compose_urls(
"whatsapp",
identifier,
linked.person_id if linked else None,
)
name = (
str(item.get("name") or item.get("chat") or "").strip()
or (linked.person.name if linked else "")
or jid
or identifier
or "WhatsApp Chat"
)
rows.append(
{
"identifier": identifier,
"jid": jid or identifier,
"name": name,
"is_group": False,
"service_icon_class": _service_icon_class("whatsapp"),
"person_name": linked.person.name if linked else "",
"compose_page_url": urls["page_url"],
"compose_widget_url": urls["widget_url"],
"match_url": (
f"{reverse('compose_contact_match')}?"
f"{urlencode({'service': 'whatsapp', 'identifier': identifier})}"
),
"last_ts": 0,
}
)
# Fallback: if no anchors yet, surface the runtime contacts (best effort live state)
for item in combined_contacts:
identifier = str(
item.get("identifier") or item.get("jid") or item.get("chat") or ""
).strip()
if not identifier:
db_group_ids = set()
db_groups = PlatformChatLink.objects.filter(
user=self.request.user,
service="whatsapp",
is_group=True,
)
for link in db_groups:
bare_id = str(link.chat_identifier or "").strip()
if not bare_id:
continue
identifier = identifier.split("@", 1)[0].strip()
if not identifier or identifier in seen:
continue
seen.add(identifier)
jid = str(item.get("jid") or "").strip()
linked = self._linked_identifier(identifier, jid)
urls = _compose_urls(
"whatsapp",
identifier,
linked.person_id if linked else None,
)
name = (
str(item.get("name") or item.get("chat") or "").strip()
or (linked.person.name if linked else "")
or jid
or identifier
or "WhatsApp Chat"
)
rows.append(
{
"identifier": identifier,
"jid": jid or identifier,
"name": name,
"service_icon_class": _service_icon_class("whatsapp"),
"person_name": linked.person.name if linked else "",
"compose_page_url": urls["page_url"],
"compose_widget_url": urls["widget_url"],
"match_url": (
f"{reverse('compose_contact_match')}?"
f"{urlencode({'service': 'whatsapp', 'identifier': identifier})}"
),
"last_ts": 0,
}
)
return rows
db_group_ids.add(bare_id)
if bare_id not in seen:
seen.add(bare_id)
jid = link.chat_jid or f"{bare_id}@g.us"
urls = _compose_urls("whatsapp", bare_id, None)
rows.append(
{
"identifier": bare_id,
"jid": jid,
"name": link.chat_name or bare_id,
"is_group": True,
"service_icon_class": _service_icon_class("whatsapp"),
"person_name": "",
"compose_page_url": urls["page_url"],
"compose_widget_url": urls["widget_url"],
"match_url": "",
"last_ts": int(link.updated_at.timestamp()),
}
)
for row in rows:
if not row.get("is_group") and row.get("identifier") in db_group_ids:
row["is_group"] = True
return [row for row in rows if row.get("is_group")]
class WhatsAppAccountAdd(SuperUserRequiredMixin, ObjectRead):

View File

@@ -34,6 +34,7 @@ from core.models import (
PatternMitigationRule,
Person,
PersonIdentifier,
PlatformChatLink,
QueuedMessage,
WorkspaceConversation,
WorkspaceMetricSnapshot,
@@ -3538,6 +3539,24 @@ class AIWorkspaceContactsWidget(LoginRequiredMixin, View):
}
)
rows.sort(key=lambda row: row["last_ts"] or 0, reverse=True)
for link in PlatformChatLink.objects.filter(user=user, is_group=True).order_by(
"service", "chat_name"
):
rows.append(
{
"person": None,
"is_group": True,
"chat_name": link.chat_name or link.chat_identifier,
"service": link.service,
"chat_identifier": link.chat_identifier,
"message_count": 0,
"last_text": "",
"last_ts": None,
"last_ts_label": "",
}
)
return rows
def get(self, request, type):