Repository: uchicago-cs/chigame
Pull request #1003: Live chat/add the ability for users to create chats
Author: login: aflekkas
Labels: name: enhancement
description: New feature or request
name: x/live-chat
Created: 2025-05-15T03:59:46Z | Merged: 2025-05-25T05:35:48Z

This PR closes #921.

Linked issue #921: Add the ability for users to create chats
Users should be able to create custom chats, and they should be admins of the chat and this means we need to add the model support for admins too. We will expand on this after by implementing (in a new issue & PR) the ability for users to assign members to chats as well as send invites (like invite links, perhas).
Linked issue comments: user: login: aflekkas
created_at: 2025-05-15T03:49:57Z
body: Give users the ability to create a chat. - not administrators too.
author_association: CONTRIBUTOR
user: login: aflekkas
created_at: 2025-05-15T03:52:09Z
body: Added `path("live-chat-list/create/", views.create_live_chat, name="create-live-chat"),` to `urls.py`
author_association: CONTRIBUTOR
user: login: aflekkas
created_at: 2025-05-15T03:52:50Z
body: ```
def create_live_chat(request):
    return render(request, "chat/create-live-chat.html")
```

Added the view, now we need to add a base html file for @nathanL534 to edit - then we can proceed and add what is supposed to happen when there is a post request.
author_association: CONTRIBUTOR
user: login: aflekkas
created_at: 2025-05-15T03:54:29Z
body: Added `create-live-chat.html`
author_association: CONTRIBUTOR
user: login: aflekkas
created_at: 2025-05-15T03:55:49Z
body: `forms.py` created

```
from django import forms
from .models import LiveChat

class LiveChatForm(forms.ModelForm):
    class Meta:
        model = LiveChat    
        fields = ["name", "description", "public"]
```

Users can now create chats
        

author_association: CONTRIBUTOR
user: login: aflekkas
created_at: 2025-05-15T03:56:24Z
body: ```
def create_live_chat(request):

    if request.method == "POST":
        form = LiveChatForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("live-chat-list")
    else:
        form = LiveChatForm()
    return render(request, "chat/create-live-chat.html", {"form": form})
```

`POST` request should now be setup, ready for you to do some frontend @nathanL534!
author_association: CONTRIBUTOR
user: login: aflekkas
created_at: 2025-05-15T03:56:51Z
body: Don't worry too much about design, just get the interactivity to work - @quincyleung could step in on a future issue to fix this if needed.
author_association: CONTRIBUTOR
user: login: nathanL534
created_at: 2025-05-16T23:11:55Z
body: 
https://github.com/user-attachments/assets/087c4c5f-0139-463d-902b-e2dcd43f440a

Added frontend functionality for users to create chats
author_association: CONTRIBUTOR
user: login: abargher
created_at: 2025-06-01T17:44:59Z
body: Issue Score: Excellent

Comments:
Great work, excellent status updates!

<!-- {"score": "excellent", "minor": false} -->
author_association: CONTRIBUTOR

Diff:
diff --git a/src/chigame/chat/forms.py b/src/chigame/chat/forms.py
new file mode 100644
index 000000000..9955a8e97
--- /dev/null
+++ b/src/chigame/chat/forms.py
@@ -0,0 +1,9 @@
+from django import forms
+
+from .models import LiveChat
+
+
+class LiveChatForm(forms.ModelForm):
+    class Meta:
+        model = LiveChat
+        fields = ["name", "description", "public"]
diff --git a/src/chigame/chat/migrations/0001_initial.py b/src/chigame/chat/migrations/0001_initial.py
index 5fc04aa12..57c08fd8a 100644
--- a/src/chigame/chat/migrations/0001_initial.py
+++ b/src/chigame/chat/migrations/0001_initial.py
@@ -1,5 +1,4 @@
-# Generated by Django 4.2.20 on 2025-05-21 18:00
-# Generated by Django 4.2.20 on 2025-05-13 04:18
+# Generated by Django 4.2.20 on 2025-05-23 05:22
 
 import chigame.chat.validators
 from django.conf import settings
@@ -29,6 +28,7 @@ class Migration(migrations.Migration):
                 ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
                 ("sent_at", models.DateTimeField(auto_now_add=True)),
                 ("content", models.TextField()),
+                ("edited", models.BooleanField(default=False)),
                 ("live_chat", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="chat.livechat")),
                 (
                     "reply_to",
@@ -39,6 +39,14 @@ class Migration(migrations.Migration):
                 ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
             ],
         ),
+        migrations.CreateModel(
+            name="LiveChatUser",
+            fields=[
+                ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+                ("live_chat", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="chat.livechat")),
+                ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
         migrations.CreateModel(
             name="LiveChatPoll",
             fields=[
@@ -62,21 +70,6 @@ class Migration(migrations.Migration):
                 ("content", models.TextField()),
             ],
         ),
-        migrations.CreateModel(
-            name="LiveChatUser",
-            fields=[
-                ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
-                ("live_chat", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="chat.livechat")),
-                ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-        migrations.AddField(
-            model_name="livechat",
-            name="users",
-            field=models.ManyToManyField(
-                related_name="live_chats", through="chat.LiveChatUser", to=settings.AUTH_USER_MODEL
-            ),
-        ),
         migrations.CreateModel(
             name="LiveChatPollVote",
             fields=[
@@ -103,6 +96,24 @@ class Migration(migrations.Migration):
                 ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
             ],
         ),
+        migrations.AddField(
+            model_name="livechat",
+            name="pinned_message",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name="pinned_message",
+                to="chat.livechatmessage",
+            ),
+        ),
+        migrations.AddField(
+            model_name="livechat",
+            name="users",
+            field=models.ManyToManyField(
+                related_name="live_chats", through="chat.LiveChatUser", to=settings.AUTH_USER_MODEL
+            ),
+        ),
         migrations.AddConstraint(
             model_name="livechatpollvote",
             constraint=models.UniqueConstraint(fields=("poll", "option", "user"), name="unique_poll_option_user"),
@@ -111,4 +122,4 @@ class Migration(migrations.Migration):
             name="livechatmessagereaction",
             unique_together={("user", "message", "content")},
         ),
-    ]
\ No newline at end of file
+    ]
diff --git a/src/chigame/chat/migrations/0002_livechatmessage_edited.py b/src/chigame/chat/migrations/0002_livechat_description.py
similarity index 54%
rename from src/chigame/chat/migrations/0002_livechatmessage_edited.py
rename to src/chigame/chat/migrations/0002_livechat_description.py
index 93cda80d1..e3e4c7e7a 100644
--- a/src/chigame/chat/migrations/0002_livechatmessage_edited.py
+++ b/src/chigame/chat/migrations/0002_livechat_description.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.20 on 2025-05-22 01:58
+# Generated by Django 4.2.20 on 2025-05-16 22:25
 
 from django.db import migrations, models
 
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
 
     operations = [
         migrations.AddField(
-            model_name="livechatmessage",
-            name="edited",
-            field=models.BooleanField(default=False),
+            model_name="livechat",
+            name="description",
+            field=models.TextField(blank=True, null=True),
         ),
     ]
diff --git a/src/chigame/chat/migrations/0002_livechat_pinned_message.py b/src/chigame/chat/migrations/0002_livechat_pinned_message.py
deleted file mode 100644
index 60d083f7f..000000000
--- a/src/chigame/chat/migrations/0002_livechat_pinned_message.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 4.2.20 on 2025-05-22 02:35
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-    dependencies = [
-        ("chat", "0001_initial"),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name="livechat",
-            name="pinned_message",
-            field=models.ForeignKey(
-                blank=True,
-                null=True,
-                on_delete=django.db.models.deletion.SET_NULL,
-                related_name="pinned_message",
-                to="chat.livechatmessage",
-            ),
-        ),
-    ]
diff --git a/src/chigame/chat/migrations/0003_livechatuser_profanity.py b/src/chigame/chat/migrations/0003_livechatuser_profanity.py
new file mode 100644
index 000000000..3f3d9767c
--- /dev/null
+++ b/src/chigame/chat/migrations/0003_livechatuser_profanity.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.20 on 2025-05-23 19:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("chat", "0002_livechat_description"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="livechatuser",
+            name="profanity",
+            field=models.BooleanField(default=True),
+        ),
+    ]
diff --git a/src/chigame/chat/migrations/0003_merge_20250522_1426.py b/src/chigame/chat/migrations/0003_merge_20250522_1426.py
deleted file mode 100644
index afca2b6be..000000000
--- a/src/chigame/chat/migrations/0003_merge_20250522_1426.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# Generated by Django 4.2.20 on 2025-05-22 19:26
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-    dependencies = [
-        ("chat", "0002_livechat_pinned_message"),
-        ("chat", "0002_livechatmessage_edited"),
-    ]
-
-    operations = []
diff --git a/src/chigame/chat/migrations/0004_remove_livechat_pinned_message.py b/src/chigame/chat/migrations/0004_remove_livechat_pinned_message.py
deleted file mode 100644
index fc160b072..000000000
--- a/src/chigame/chat/migrations/0004_remove_livechat_pinned_message.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Generated by Django 4.2.20 on 2025-05-25 00:56
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-    dependencies = [
-        ("chat", "0003_merge_20250522_1426"),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name="livechat",
-            name="pinned_message",
-        ),
-    ]
diff --git a/src/chigame/chat/models.py b/src/chigame/chat/models.py
index 51d701c2f..6ee1b0004 100644
--- a/src/chigame/chat/models.py
+++ b/src/chigame/chat/models.py
@@ -12,7 +12,11 @@ class LiveChat(models.Model):
 
     name = models.TextField(null=False)
     users: models.ManyToManyField = models.ManyToManyField(User, through="LiveChatUser", related_name="live_chats")
+    pinned_message = models.ForeignKey(
+        "LiveChatMessage", null=True, blank=True, on_delete=models.SET_NULL, related_name="pinned_message"
+    )
     public = models.BooleanField(default=False)  # defined for global chats
+    description = models.TextField(null=True, blank=True)
 
     def __str__(self):
         return f"LiveChat with name:'{self.name}'"
@@ -44,6 +48,7 @@ class LiveChatUser(models.Model):
 
     user = models.ForeignKey(User, on_delete=models.CASCADE)
     live_chat = models.ForeignKey(LiveChat, on_delete=models.CASCADE)
+    profanity = models.BooleanField(default=True)
 
     def __str__(self):
         return f"{self.user} in chat {self.live_chat.name}"
diff --git a/src/chigame/chat/urls.py b/src/chigame/chat/urls.py
index c346ddbf8..7f8f0327c 100644
--- a/src/chigame/chat/urls.py
+++ b/src/chigame/chat/urls.py
@@ -5,7 +5,9 @@
 urlpatterns = [
     path("<int:chat_id>/", views.chat, name="chat-page"),
     path("live-chat-list/", views.live_chat_list, name="live-chat-list"),
+    path("live-chat-list/create/", views.create_live_chat, name="create-live-chat"),
     path("message/<int:message_id>/delete", views.delete_message, name="delete-message"),
+    path("message/<int:message_id>/pin", views.pin_message, name="pin-message"),
     path("message/<int:message_id>/edit", views.edit_message, name="edit-message"),
     path("message/<int:message_id>/react", views.react_to_message, name="react-to-message"),
 ]
diff --git a/src/chigame/chat/views.py b/src/chigame/chat/views.py
index f3925c984..8baf1f053 100644
--- a/src/chigame/chat/views.py
+++ b/src/chigame/chat/views.py
@@ -1,9 +1,11 @@
 from django.core.exceptions import ValidationError
+from django.db.models import Count, OuterRef, Subquery
 from django.http import JsonResponse
-from django.shortcuts import get_object_or_404, render
+from django.shortcuts import get_object_or_404, redirect, render
 from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.http import require_POST
 
+from .forms import LiveChatForm
 from .models import LiveChat, LiveChatMessage, LiveChatMessageReaction
 
 
@@ -14,13 +16,45 @@ def chat(request, chat_id):
     # if the chat is public, add the request user to the chat
     if request.user.is_authenticated and chat.public and not chat.users.filter(id=request.user.id).exists():
         chat.users.add(request.user)
-
+    if request.user.is_authenticated and not chat.users.filter(id=request.user.id).exists():
+        chat.users.add(request.user)
     return render(request, "chat/index.html", {"chat": chat, "messages": messages})
 
 
 def live_chat_list(request):
-    chats = LiveChat.objects.filter(public=True)
-    return render(request, "chat/live-chat-list.html", {"chats": chats})
+    latest_message = LiveChatMessage.objects.filter(live_chat=OuterRef("pk")).order_by("-sent_at")
+
+    public_chats = LiveChat.objects.filter(public=True).annotate(
+        user_count=Count("users"),
+        last_message=Subquery(latest_message.values("content")[:1]),
+        last_message_time=Subquery(latest_message.values("sent_at")[:1]),
+    )
+
+    private_chats = LiveChat.objects.filter(public=False, users=request.user).annotate(
+        user_count=Count("users"),
+        last_message=Subquery(latest_message.values("content")[:1]),
+        last_message_time=Subquery(latest_message.values("sent_at")[:1]),
+    )
+    return render(
+        request,
+        "chat/live-chat-list.html",
+        {
+            "public_chats": public_chats,
+            "private_chats": private_chats,
+        },
+    )
+
+
+def create_live_chat(request):
+    if request.method == "POST":
+        form = LiveChatForm(request.POST)
+
+        if form.is_valid():
+            form.save()
+            return redirect("live-chat-list")
+    else:
+        form = LiveChatForm()
+    return render(request, "chat/create-live-chat.html", {"form": form})
 
 
 def delete_message(request, message_id):
@@ -47,6 +81,43 @@ def delete_message(request, message_id):
     return JsonResponse({"message": "Message deleted successfully"}, status=200)
 
 
+def pin_message(request, message_id):
+    """
+    Pins a message in the chat
+
+    Args:
+        request: The request object.
+        message_id: The id of the message to delete.
+
+    Returns:
+        A JSON response.
+    """
+    if not request.user.is_authenticated:
+        return JsonResponse({"error": "Unauthorized"}, status=401)
+
+    message = get_object_or_404(LiveChatMessage, id=message_id)
+
+    if not message.live_chat.users.filter(id=request.user.id).exists():
+        return JsonResponse({"error": "Unauthorized"}, status=401)
+
+    chat = message.live_chat
+    chat.pinned_message = message
+    chat.save()
+
+    return JsonResponse(
+        {
+            "message": "Message pinned successfully",
+            "pinned_message": {
+                "id": message.id,
+                "content": message.content,
+                "username": message.user.username or message.user.email,
+                "is_pinned": True,
+            },
+        },
+        status=200,
+    )
+
+
 @csrf_exempt
 @require_POST
 def react_to_message(request, message_id):
diff --git a/src/templates/base.html b/src/templates/base.html
index 812f2a8ea..50cadc576 100644
--- a/src/templates/base.html
+++ b/src/templates/base.html
@@ -245,16 +245,17 @@
     {% block body %}
     {% endblock body %}
     <div class="container">
-      {% comment %}
       {% if messages %}
         {% for message in messages %}
           <div class="alert alert-dismissible {% if message.tags %}alert-{{ message.tags }}{% endif %}">
             {{ message }}
-            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+            <button type="button"
+                    class="btn-close"
+                    data-bs-dismiss="alert"
+                    aria-label="Close"></button>
           </div>
         {% endfor %}
       {% endif %}
-      {% endcomment %}
       {% block content %}
         <p>Coming soon!</p>
       {% endblock content %}
diff --git a/src/templates/chat/components/_chat_list.html b/src/templates/chat/components/_chat_list.html
new file mode 100644
index 000000000..7e99a5d8a
--- /dev/null
+++ b/src/templates/chat/components/_chat_list.html
@@ -0,0 +1,19 @@
+<div class="list-group">
+  {% for chat in chats %}
+    <a href="{% url 'chat-page' chat.id %}"
+       class="list-group-item list-group-item-action d-flex justify-content-between align-items-start">
+      <div class="flex-grow-1">
+        <div class="d-flex align-items-center gap-2 mb-1">
+          <h5 class="mb-1">{{ chat.name }}</h5>
+          <span class="badge bg-primary rounded-pill extra-small-text">
+            {{ chat.user_count }} user{{ chat.user_count|pluralize }}
+          </span>
+        </div>
+        <div class="text-muted small">{{ chat.last_message|default:"No messages yet." }}</div>
+      </div>
+      {% if chat.last_message_time %}
+        <div class="text-muted small">{{ chat.last_message_time|date:"g:i A" }}</div>
+      {% endif %}
+    </a>
+  {% endfor %}
+</div>
diff --git a/src/templates/chat/components/_context_menu.html b/src/templates/chat/components/_context_menu.html
index 18fb13bac..d24de8505 100644
--- a/src/templates/chat/components/_context_menu.html
+++ b/src/templates/chat/components/_context_menu.html
@@ -1,6 +1,6 @@
 <div id="contextMenu" class="context-menu">
   <div class="small text-muted mb-1" id="replyPreview"></div>
-  <button class="btn btn-sm btn-light w-100" onclick="pinningMessage()">Pinning Messages</button>
+  <button class="btn btn-sm btn-light w-100" onclick="pinMessage()">Pinning Messages</button>
   <button class="btn btn-sm btn-light w-100" onclick="replyToMessage()">Reply</button>
   <button class="btn btn-sm btn-light w-100"
           onclick="editMessage(replyToMessageId, replyToContent)">Edit Message</button>
@@ -165,6 +165,62 @@
     hideContextMenu();
   }
 
+  function pinMessage() {
+    const id = replyToMessageId;
+    const token = document.querySelector('[name=csrfmiddlewaretoken]').value;
+
+    fetch(`/chat/message/${id}/pin`, {
+        method: 'POST',
+        headers: {
+          'X-CSRFToken': token
+        }
+      })
+      .then(res => res.json())
+      .then(data => {
+        const pm = data.pinned_message;
+
+        const banner = document.getElementById('pinned-message-container');
+        if (pm.is_pinned) {
+          banner.innerHTML = `📌 <strong>${pm.username}</strong>: ${pm.content}`;
+          banner.style.display = 'block'; // ← this is KEY!
+        } else {
+          banner.innerHTML = '';
+          banner.style.display = 'none';
+        }
+        document.querySelectorAll('.pinned-label').forEach(el => el.remove());
+
+        if (pm.is_pinned) {
+          const messageEl = document.querySelector(
+            `[data-message-id="${pm.id}"]`
+          );
+
+          // check for .message-meta
+          let meta = messageEl?.querySelector('.message-meta');
+
+          if (!meta) {
+            // create it if missing
+            meta = document.createElement('div');
+            meta.className = 'message-meta text-end text-muted small mt-1';
+
+
+
+            messageEl?.querySelector('.rounded')?.appendChild(meta);
+          }
+          meta.insertAdjacentHTML(
+            'beforeend',
+            `<span class="text-warning small ms-1 pinned-label">📌 Pinned</span>`
+          );
+        }
+      })
+      .catch(err => console.error('Pin failed:', err));
+
+    hideContextMenu();
+  }
+
+
+
+
+
   function addReactionToMessage(messageId, content) {
     const container = document.getElementById(`reactions-${messageId}`);
     if (!container) return;
@@ -185,6 +241,9 @@
     }
   }
 
+
+
+
   function removeReactionFromMessage(messageId, content) {
     const container = document.getElementById(`reactions-${messageId}`);
     if (!container) return;
diff --git a/src/templates/chat/components/_header.html b/src/templates/chat/components/_header.html
index 71ad71741..cc334defc 100644
--- a/src/templates/chat/components/_header.html
+++ b/src/templates/chat/components/_header.html
@@ -18,3 +18,11 @@ <h2 class="chigame-text mb-0">{{ chat.name }}</h2>
     </button>
   </div>
 </div>
+<div id="pinned-message-container"
+     class="text-bg-warning p-2 rounded-bottom shadow-sm"
+     style="{% if not chat.pinned_message %}display: none;
+            {% endif %}">
+  {% if chat.pinned_message %}
+    📌 <strong>{{ chat.pinned_message.user.username|default:chat.pinned_message.user.email }}</strong>: {{ chat.pinned_message.content }}
+  {% endif %}
+</div>
diff --git a/src/templates/chat/components/_messages.html b/src/templates/chat/components/_messages.html
index d5b84372c..978e21536 100644
--- a/src/templates/chat/components/_messages.html
+++ b/src/templates/chat/components/_messages.html
@@ -143,6 +143,11 @@
             {% endif %}
             <div class="fw-bold small text-danger">{{ message.user.username }}</div>
             <div class="mt-1">{{ message.content }}</div>
+            <div class="message-meta text-end text-muted small mt-1">
+              {% if message.id == chat.pinned_message.id %}
+                <span class="text-warning small ms-1 pinned-label">📌 Pinned</span>
+              {% endif %}
+            </div>
             <div class="text-end text-muted small mt-1">{{ message.sent_at|date:"g:i A" }}</div>
             <div class="message-reactions mt-1" id="reactions-{{ message.id }}"></div>
           </div>
diff --git a/src/templates/chat/create-live-chat.html b/src/templates/chat/create-live-chat.html
new file mode 100644
index 000000000..d4c8e560b
--- /dev/null
+++ b/src/templates/chat/create-live-chat.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block content %}
+  <div class="container mt-5" id="chat-form-container">
+    <h1 class="mb-4">Create New Live Chat</h1>
+    <form method="post" novalidate>
+      {% csrf_token %}
+      <div class="mb-3">
+        <label for="id_name" class="form-label">Chat Name</label>
+        <input type="text" name="name" class="form-control" id="id_name" required />
+      </div>
+      <div class="mb-3">
+        <label for="id_description" class="form-label">Description</label>
+        <textarea name="description" class="form-control" id="id_description" rows="3"></textarea>
+      </div>
+      <div class="form-check mb-3">
+        <input type="checkbox" name="public" class="form-check-input" id="id_public" />
+        <label class="form-check-label" for="id_public">Make chat public</label>
+      </div>
+      <button type="submit" class="btn btn-primary">Create Chat</button>
+    </form>
+  </div>
+{% endblock content %}
diff --git a/src/templates/chat/live-chat-list.html b/src/templates/chat/live-chat-list.html
index 91416eb3d..15a10aa3a 100644
--- a/src/templates/chat/live-chat-list.html
+++ b/src/templates/chat/live-chat-list.html
@@ -2,21 +2,66 @@
 
 {% block content %}
   <div class="container">
-    <h1>Live Chats</h1>
-    <div class="search-container mb-3">
-      <input type="text"
-             id="search-input"
-             class="form-control"
-             placeholder="Search public chats..."
-             value="{{ request.GET.q|default:'' }}" />
-    </div>
-    <ul class="list-group">
-      {% for chat in chats %}
-        <li class="list-group-item">
-          <a href="{% url 'chat-page' chat.id %}">{{ chat.name }}</a>
+    <div class="container mt-4">
+      <h1 class="mb-4">Chats</h1>
+      <a href="{% url 'create-live-chat' %}" class="btn btn-primary">+ create new chat</a>
+      <!-- Nav Tabs -->
+      <ul class="nav nav-tabs" id="chatTabs" role="tablist">
+        <li class="nav-item" role="presentation">
+          <button class="nav-link active"
+                  id="public-tab"
+                  data-bs-toggle="tab"
+                  data-bs-target="#public"
+                  type="button"
+                  role="tab">Public</button>
+        </li>
+        <li class="nav-item" role="presentation">
+          <button class="nav-link"
+                  id="private-tab"
+                  data-bs-toggle="tab"
+                  data-bs-target="#private"
+                  type="button"
+                  role="tab">Private</button>
         </li>
-      {% endfor %}
-    </ul>
+      </ul>
+      <!-- Tab Content -->
+      <div class="tab-content mt-3">
+        <!-- Public Chats -->
+        <div class="tab-pane fade show active" id="public" role="tabpanel">
+          <div class="search-container mb-3">
+            <input type="text"
+                   id="search-input"
+                   class="form-control"
+                   placeholder="Search public chats..."
+                   value="{{ request.GET.q|default:'' }}" />
+          </div>
+          {% include "chat/components/_chat_list.html" with chats=public_chats %}
+        </div>
+        <!-- Private Chats -->
+        <div class="tab-pane fade" id="private" role="tabpanel">
+          {% include "chat/components/_chat_list.html" with chats=private_chats %}
+          <div class="list-group">
+            {% for chat in chats %}
+              <a href="{% url 'chat-page' chat.id %}"
+                 class="list-group-item list-group-item-action d-flex justify-content-between align-items-start">
+                <div class="flex-grow-1">
+                  <div class="d-flex align-items-center gap-2 mb-1">
+                    <h5 class="mb-1">{{ chat.name }}</h5>
+                    <span class="badge bg-primary rounded-pill extra-small-text">
+                      {{ chat.user_count }} user{{ chat.user_count|pluralize }}
+                    </span>
+                  </div>
+                  <div class="text-muted small">{{ chat.last_message|default:"No messages yet." }}</div>
+                </div>
+                {% if chat.last_message_time %}
+                  <div class="text-muted small">{{ chat.last_message_time|date:"g:i A" }}</div>
+                {% endif %}
+              </a>
+            {% endfor %}
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
   <script>
     document.addEventListener('DOMContentLoaded', function() {
@@ -54,6 +99,7 @@ <h1>Live Chats</h1>
         });
       });
       searchInput.addEventListener('input', handleSearchInput);
+      xw
     });
   </script>
 {% endblock content %}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces live chat functionality by adding new views, templates, and URL routes for listing and creating live chats as well as updating the chat model to support public chats.
- Added a live chat list template with client-side chat filtering.
- Introduced a live chat creation template and corresponding view and URL.
- Updated models and migrations to include the "public" field and new reaction functionality.

### Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/templates/chat/live-chat-list.html | Adds a template for listing live chats with a search input (missing event binding on the search input). |
| src/templates/chat/create-live-chat.html | Introduces a basic template for creating a live chat. |
| src/templates/base.html | Updates the navigation bar to include a link to live chats. |
| src/chigame/knowledge_base/migrations/0004_alter_reviewfeedback_guide_id.py | Updates migration for review feedback guide ID. |
| src/chigame/chat/views.py | Adds views for displaying a chat, listing live chats, and creating a live chat. |
| src/chigame/chat/urls.py | Registers URL routes for live chat list and creation. |
| src/chigame/chat/models.py | Adds a boolean 'public' field to the LiveChat model. |
| src/chigame/chat/migrations/0001_initial.py | Modifies model creation order and adds a new reactions model. |
| src/chigame/chat/forms.py | Introduces a form for creating live chats with a reference to a non-existent 'description' field. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/chigame/chat/migrations/0001_initial.py:25**
* The migration swaps the names for the models 'LiveChatUser' and 'LiveChatMessage' compared to the original design, which may lead to unintended consequences. Verify that the model naming and relationships are correctly aligned with the intended functionality.
```
name="LiveChatMessage",
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T04:00:32Z
user: login: nathanL534
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T21:57:16Z
user: login: quincyleung
body: I tested the feature locally and it works great — nice job!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T19:40:28Z
user: login: abargher
body: Looks great!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T05:33:56Z

Review comments:
diff_hunk: @@ -0,0 +1,8 @@
+from django import forms
+from .models import LiveChat
+
+class LiveChatForm(forms.ModelForm):
+    class Meta:
+        model = LiveChat    
+        fields = ["name", "description", "public"]
path: src/chigame/chat/forms.py
user: login: Copilot
body: The LiveChat model does not include a 'description' field, which could cause errors when using this form. Either remove 'description' from the form fields or add the field to the model.
created_at: 2025-05-15T04:00:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,58 @@
+{% extends "base.html" %}
+
+{% block content %}
+  <div class="container">
+    <h1>Live Chats</h1>
+    <div class="search-container mb-3">
+      <input type="text"
+             id="search-input"
+             class="form-control"
+             placeholder="Search public chats..."
+             value="{{ request.GET.q|default:'' }}" />
+    </div>
+    <ul class="list-group">
+      {% for chat in chats %}
+        <li class="list-group-item">
+          <a href="{% url 'chat-page' chat.id %}">{{ chat.name }}</a>
+        </li>
+      {% endfor %}
+    </ul>
+  </div>
+  <script>
+    document.addEventListener('DOMContentLoaded', function() {
+      const searchInput = document.getElementById('search-input');
+
+      const debounce = (func, delay) => {
+        let timeout;
+        return function(...args) {
+          clearTimeout(timeout);
+          timeout = setTimeout(() => func.apply(this, args), delay);
+        };
+      };
+
+      const handleSearchInput = debounce(function() {
+        const searchQuery = this.value.trim();
+        const currentUrl = new URL(window.location.href);
+
+        if (searchQuery) {
+          currentUrl.searchParams.set('q', searchQuery);
+        } else {
+          currentUrl.searchParams.delete('q');
+        }
+
+        window.history.pushState({}, '', currentUrl);
+
+        // Filter the chat list
+        const chatItems = document.querySelectorAll('.list-group-item');
+        chatItems.forEach(item => {
+          const chatName = item.textContent.toLowerCase();
+          if (chatName.includes(searchQuery.toLowerCase())) {
+            item.style.display = '';
+          } else {
+            item.style.display = 'none';
+          }
+        });
path: src/templates/chat/live-chat-list.html
user: login: Copilot
body: The debounce function 'handleSearchInput' is defined but never attached to the input element. Add an event listener (e.g., searchInput.addEventListener('input', handleSearchInput)) to ensure the search functionality works as intended.
```suggestion
        });
        searchInput.addEventListener('input', handleSearchInput);
```
created_at: 2025-05-15T04:00:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+from django import forms
+from .models import LiveChat
+
+class LiveChatForm(forms.ModelForm):
+    class Meta:
+        model = LiveChat    
+        fields = ["name", "description", "public"]
path: src/chigame/chat/forms.py
user: login: nathanL534
body: I added the description to models!
created_at: 2025-05-16T21:57:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: aflekkas
created_at: 2025-05-15T04:01:30Z
body: @nathanL534 do you think you could address these bugs?
author_association: CONTRIBUTOR
user: login: nathanL534
created_at: 2025-05-16T21:57:46Z
body: > @nathanL534 do you think you could address these bugs?

Yes Ill work on them right now!
author_association: CONTRIBUTOR
user: login: nathanL534
created_at: 2025-05-16T23:11:33Z
body: 
https://github.com/user-attachments/assets/087c4c5f-0139-463d-902b-e2dcd43f440a

Added frontend functionality for users to create chats
author_association: CONTRIBUTOR
user: login: abargher
created_at: 2025-06-01T17:58:52Z
body: Issue Score: Excellent

Comments:
Great job, yet another good demo video!

<!-- {"score": "excellent", "minor": false} -->
author_association: CONTRIBUTOR