New chat message notification Django Channels

django, django-channels, django-models, django-views, python

Solution

One easy way to implement a notification system can be:

When you want to show a new message, manipulate HTML using JS as soon as you get a message on the websocket. And whenever the element has been interacted with, which means the user has read the notification, send a message back to server using the websocket.

Your `Notification` can have `ForeignKeys` to user and the message along with a `BooleanField` for read status. Whenever you are sending the message to the user, you should append the notification_id along the message,

#consumer.py
async def websocket_receive(self, event):
        # when a message is received from the websocket
        print("receive", event)

        message_type = event.get('type', None)  #check message type, act accordingly
        if message_type == "notification_read":
             # Update the notification read status flag in Notification model.
             notification = Notification.object.get(id=notification_id)
             notification.notification_read = True
             notification.save()  #commit to DB
             print("notification read")

        front_text = event.get('text', None)
        if front_text is not None:
            loaded_dict_data = json.loads(front_text)
            msg =  loaded_dict_data.get('message')
            user = self.scope['user']
            username = 'default'
            if user.is_authenticated:
                username = user.username
            myResponse = {
                'message': msg,
                'username': username,
                'notification': notification_id  # send a unique identifier for the notification
            }
            ...

On the client side,

// thread.html
socket.onmessage = function(e) {
    var data = JSON.parse(event.data);
    // Find the notification icon/button/whatever and show a red dot, add the notification_id to element as id or data attribute.
}
...

$(#notification-element).on("click", function(){
    data = {"type":"notification_read", "username": username, "notification_id": notification_id};
    socket.send(JSON.stringify(data));
});

You can mark individual/all unread notifications as read according to your need.

I did something similar for a training project, you can check that out for ideas. Github link.

Problem

I've got `Django Channels 2.1.2` set up in my Django app by following a tutorial and now need to set up a notification system for new messages. I want to do this in the simplest way possible. I can do it via browser push notifications, but I don't want to do it like that. I want it to be like Stack Overflow, where there is a red number representing the instance of a new message. One answer on here said For notifications you only need two models: `User` and `Notification`. On connect set the scope to the currently authenticated user. Set up a `post_save` signal on your `Notification` model to trigger a consumer method to message the notification object's user. – I am struggling to wrap my head around what this would look like, I already have a `User` model but no `Notification` one. The chat is between only 2 users, it is not a chat room but more of a chat thread. The 2 html templates are `inbox.html` and `thread.html` Appreciate any help! My Django Channels code is below! consumers.py ``` class ChatConsumer(AsyncConsumer): async def websocket_connect(self, event): print('connected', event) other_user = self.scope['url_route']['kwargs']['username'] me = self.scope['user'] #print(other_user, me) thread_obj = await self.get_thread(me, other_user) self.thread_obj = thread_obj chat_room = f"thread_{thread_obj.id}" self.chat_room = chat_room # below creates the chatroom await self.channel_layer.group_add( chat_room, self.channel_name ) await self.send({ "type": "websocket.accept" }) async def websocket_receive(self, event): # when a message is recieved from the websocket print("receive", event) message_type = event.get('type', None) #check message type, act accordingly if message_type == "notification_read": # Update the notification read status flag in Notification model. notification = Notification.object.get(id=notification_id) notification.notification_read = True notification.save() #commit to DB print("notification read") front_text = event.get('text', None) if front_text is not None: loaded_dict_data = json.loads(front_text) msg = loaded_dict_data.get('message') user = self.scope['user'] username = 'default' if user.is_authenticated: username = user.username myResponse = { 'message': msg, 'username': username, } await self.create_chat_message(user, msg) # broadcasts the message event to be sent, the group send layer # triggers the chat_message function for all of the group (chat_room) await self.channel_layer.group_send( self.chat_room, { 'type': 'chat_message', 'text': json.dumps(myResponse) } ) # chat_method is a custom method name that we made async def chat_message(self, event): # sends the actual message await self.send({ 'type': 'websocket.send', 'text': event['text'] }) async def websocket_disconnect(self, event): # when the socket disconnects print('disconnected', event) @database_sync_to_async def get_thread(self, user, other_username): return Thread.objects.get_or_new(user, other_username)[0] @database_sync_to_async def create_chat_message(self, me, msg): thread_obj = self.thread_obj return ChatMessage.objects.create(thread=thread_obj, user=me, message=msg) ``` manager ``` class ThreadManager(models.Manager): def by_user(self, user): qlookup = Q(first=user) | Q(second=user) qlookup2 = Q(first=user) & Q(second=user) qs = self.get_queryset().filter(qlookup).exclude(qlookup2).distinct() return qs # method to grab the thread for the 2 users def get_or_new(self, user, other_username): # get_or_create username = user.username if username == other_username: return None, None # looks based off of either username qlookup1 = Q(first__username=username) & Q(second__username=other_username) qlookup2 = Q(first__username=other_username) & Q(second__username=username) qs = self.get_queryset().filter(qlookup1 | qlookup2).distinct() if qs.count() == 1: return qs.first(), False elif qs.count() > 1: return qs.order_by('timestamp').first(), False else: Klass = user.__class__ try: user2 = Klass.objects.get(username=other_username) except Klass.DoesNotExist: user2 = None if user != user2: obj = self.model( first=user, second=user2 ) obj.save() return obj, True return None, False ``` models.py ``` class Thread(models.Model): first = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_first') second = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_second') updated = models.DateTimeField(auto_now=True) timestamp = models.DateTimeField(auto_now_add=True) objects = ThreadManager() def __str__(self): return f'{self.id}' @property def room_group_name(self): return f'chat_{self.id}' def broadcast(self, msg=None): if msg is not None: broadcast_msg_to_chat(msg, group_name=self.room_group_name, user='admin') return True return False class ChatMessage(models.Model): thread = models.ForeignKey(Thread, null=True, blank=True, on_delete=models.SET_NULL) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='sender', on_delete=models.CASCADE) message = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) def __str__(self): return f'{self.id}' class Notification(models.Model): notification_user = models.ForeignKey(User, on_delete=models.CASCADE) notification_chat = models.ForeignKey(ChatMessage, on_delete=models.CASCADE) notification_read = models.BooleanField(default=False) def __str__(self): return f'{self.id}' ``` views.py ``` class InboxView(LoginRequiredMixin, ListView): template_name = 'chat/inbox.html' context_object_name = 'threads' def get_queryset(self): return Thread.objects.by_user(self.request.user).exclude(chatmessage__isnull=True).order_by('timestamp') # by_user(self.request.user) class ThreadView(LoginRequiredMixin, FormMixin, DetailView): template_name = 'chat/thread.html' form_class = ComposeForm success_url = '#' def get_queryset(self): return Thread.objects.by_user(self.request.user) def get_object(self): other_username = self.kwargs.get("username") obj, created = Thread.objects.get_or_new(self.request.user, other_username) if obj == None: raise Http404 return obj def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['form'] = self.get_form() return context def post(self, request, *args, **kwargs): if not request.user.is_authenticated: return HttpResponseForbidden() self.object = self.get_object() form = self.get_form() if form.is_valid(): return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): thread = self.get_object() user = self.request.user message = form.cleaned_data.get("message") ChatMessage.objects.create(user=user, thread=thread, message=message) return super().form_valid(form) ``` thread.html ``` {% block head %} <title>Chat</title> <script src="{% static '/channels/js/websocketbridge.js' %}" type="text/javascript"></script> {% endblock %} {% block content %} <script> $(#notification-element).on("click", function(){ data = {"type":"notification_read", "username": username, "notification_id": notification_id}; socket.send(JSON.stringify(data)); }); </script> <!-- back to inbox button with notification example --> <a class="btn btn-light" id="notification_id" href="{% url 'chat:inbox' %}">Back to Inbox</a> <div class="msg_history"> {% for chat in object.chatmessage_set.all %} {% if chat.user == user %} <div class="outgoing_msg"> <div class="outgoing_msg_img"> <img src="{{ chat.user.profile.image.url }}"> </div> <div class="sent_msg"> <p>{{ chat.message }}</p> <span class="time_date"> {{ chat.timestamp }}</span> </div> </div> {% else %} <div class="incoming_msg"> <div class="incoming_msg_img"> <img src="{{ chat.user.profile.image.url }}"> </div> <div class="received_msg"> <div class="received_withd_msg"> <p>{{ chat.message }}</p> <span class="time_date"> {{ chat.timestamp }}</span> </div> </div> </div> {% endif %} {% endfor %} </div> <div class="type_msg"> <div class="input_msg_write"> <!-- text input / write message form --> <form id='form' method='POST'> {% csrf_token %} <input type='hidden' id='myUsername' value='{{ user.username }}' /> {{ form.as_p }} <center><button type="submit" class='btn btn-success disabled' value="Send">Send</button></center> </form> </div> </div> </div> </div> </div> </div> {% endblock %} {% block script %} <script src='https://cdnjs.cloudflare.com/ajax/libs/reconnecting-websocket/1.0.0/reconnecting-websocket.js'></script> <script> // websocket scripts - client side* var loc = window.location var formData = $("#form") var msgInput = $("#id_message") var chatHolder = $('#chat-items') var me = $('#myUsername').val() var wsStart = 'ws://' if (loc.protocol == 'https:') { wsStart = 'wss://' } var endpoint = wsStart + loc.host + loc.pathname var socket = new ReconnectingWebSocket(endpoint) // below is the message I am receiving socket.onmessage = function(e) { console.log("message", e) var data = JSON.parse(event.data); // Find the notification icon/button/whatever and show a red dot, add the notification_id to element as id or data attribute. var chatDataMsg = JSON.parse(e.data) chatHolder.append('<li>' + chatDataMsg.message + ' from ' + chatDataMsg.username + '</li>') } // below is the message I am sending socket.onopen = function(e) { console.log("open", e) formData.submit(function(event) { event.preventDefault() var msgText = msgInput.val() var finalData = { 'message': msgText } socket.send(JSON.stringify(finalData)) formData[0].reset() }) } socket.onerror = function(e) { console.log("error", e) } socket.onclose = function(e) { console.log("close", e) } </script> <script> document.addEventListener('DOMContentLoaded', function() { const webSocketBridge = new channels.WebSocketBridge(); webSocketBridge.connect('/ws'); webSocketBridge.listen(function(action, stream) { console.log("RESPONSE:", action); }) document.ws = webSocketBridge; /* for debugging */ }) </script> {% endblock %} ```

Original source

Related problems