Make chat faster

This commit is contained in:
cuom1999 2023-08-28 14:20:35 -05:00
parent f11d9b4b53
commit 2854ac97e9
11 changed files with 903 additions and 801 deletions

View file

@ -0,0 +1,20 @@
# Generated by Django 3.2.18 on 2023-08-28 01:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0012_auto_20230308_1417"),
]
operations = [
migrations.AlterField(
model_name="message",
name="time",
field=models.DateTimeField(
auto_now_add=True, db_index=True, verbose_name="posted time"
),
),
]

View file

@ -0,0 +1,38 @@
# Generated by Django 3.2.18 on 2023-08-28 06:02
from django.db import migrations, models
def migrate(apps, schema_editor):
UserRoom = apps.get_model("chat_box", "UserRoom")
Message = apps.get_model("chat_box", "Message")
for ur in UserRoom.objects.all():
if not ur.room:
continue
messages = ur.room.message_set
last_msg = messages.first()
try:
if last_msg and last_msg.author != ur.user:
ur.unread_count = messages.filter(time__gte=ur.last_seen).count()
else:
ur.unread_count = 0
ur.save()
except:
continue
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0013_alter_message_time"),
]
operations = [
migrations.AddField(
model_name="userroom",
name="unread_count",
field=models.IntegerField(db_index=True, default=0),
),
migrations.RunPython(migrate, migrations.RunPython.noop, atomic=True),
]

View file

@ -1,9 +1,10 @@
from django.db import models from django.db import models
from django.db.models import CASCADE from django.db.models import CASCADE, Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["Message", "Room", "UserRoom", "Ignore"] __all__ = ["Message", "Room", "UserRoom", "Ignore"]
@ -29,7 +30,9 @@ class Room(models.Model):
class Message(models.Model): class Message(models.Model):
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE) author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) time = models.DateTimeField(
verbose_name=_("posted time"), auto_now_add=True, db_index=True
)
body = models.TextField(verbose_name=_("body of comment"), max_length=8192) body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
hidden = models.BooleanField(verbose_name="is hidden", default=False) hidden = models.BooleanField(verbose_name="is hidden", default=False)
room = models.ForeignKey( room = models.ForeignKey(
@ -56,6 +59,7 @@ class UserRoom(models.Model):
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
) )
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True) last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
unread_count = models.IntegerField(default=0, db_index=True)
class Meta: class Meta:
unique_together = ("user", "room") unique_together = ("user", "room")
@ -74,11 +78,9 @@ class Ignore(models.Model):
@classmethod @classmethod
def is_ignored(self, current_user, new_friend): def is_ignored(self, current_user, new_friend):
try: try:
return ( return current_user.ignored_chat_users.ignored_users.filter(
current_user.ignored_chat_users.get() id=new_friend.id
.ignored_users.filter(id=new_friend.id) ).exists()
.exists()
)
except: except:
return False return False
@ -89,6 +91,16 @@ class Ignore(models.Model):
except: except:
return Profile.objects.none() return Profile.objects.none()
@classmethod
def get_ignored_rooms(self, user):
try:
ignored_users = self.objects.get(user=user).ignored_users.all()
return Room.objects.filter(Q(user_one=user) | Q(user_two=user)).filter(
Q(user_one__in=ignored_users) | Q(user_two__in=ignored_users)
)
except:
return Room.objects.none()
@classmethod @classmethod
def add_ignore(self, current_user, friend): def add_ignore(self, current_user, friend):
ignore, created = self.objects.get_or_create(user=current_user) ignore, created = self.objects.get_or_create(user=current_user)

View file

@ -1,10 +1,12 @@
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
import hmac
import hashlib
from django.conf import settings from django.conf import settings
from django.db.models import OuterRef, Count, Subquery, IntegerField from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from chat_box.models import Ignore, Message, UserRoom from chat_box.models import Ignore, Message, UserRoom, Room
secret_key = settings.CHAT_SECRET_KEY secret_key = settings.CHAT_SECRET_KEY
fernet = Fernet(secret_key) fernet = Fernet(secret_key)
@ -24,25 +26,22 @@ def decrypt_url(message_encrypted):
return None, None return None, None
def encrypt_channel(channel):
return (
hmac.new(
settings.CHAT_SECRET_KEY.encode(),
channel.encode(),
hashlib.sha512,
).hexdigest()[:16]
+ "%s" % channel
)
def get_unread_boxes(profile): def get_unread_boxes(profile):
ignored_users = Ignore.get_ignored_users(profile) ignored_rooms = Ignore.get_ignored_rooms(profile)
mess = (
Message.objects.filter(room=OuterRef("room"), time__gte=OuterRef("last_seen"))
.exclude(author=profile)
.exclude(author__in=ignored_users)
.order_by()
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
unread_boxes = ( unread_boxes = (
UserRoom.objects.filter(user=profile, room__isnull=False) UserRoom.objects.filter(user=profile, unread_count__gt=0)
.annotate( .exclude(room__in=ignored_rooms)
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
)
.filter(unread_count__gte=1)
.count() .count()
) )

View file

@ -21,6 +21,7 @@ from django.db.models import (
Exists, Exists,
Count, Count,
IntegerField, IntegerField,
F,
) )
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
@ -34,7 +35,7 @@ from judge.jinja2.gravatar import gravatar
from judge.models import Friend from judge.models import Friend
from chat_box.models import Message, Profile, Room, UserRoom, Ignore from chat_box.models import Message, Profile, Room, UserRoom, Ignore
from chat_box.utils import encrypt_url, decrypt_url from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel
import json import json
@ -49,7 +50,8 @@ class ChatView(ListView):
self.room_id = None self.room_id = None
self.room = None self.room = None
self.messages = None self.messages = None
self.page_size = 20 self.first_page_size = 20 # only for first request
self.follow_up_page_size = 50
def get_queryset(self): def get_queryset(self):
return self.messages return self.messages
@ -63,10 +65,12 @@ class ChatView(ListView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
request_room = kwargs["room_id"] request_room = kwargs["room_id"]
page_size = self.follow_up_page_size
try: try:
last_id = int(request.GET.get("last_id")) last_id = int(request.GET.get("last_id"))
except Exception: except Exception:
last_id = 1e15 last_id = 1e15
page_size = self.first_page_size
only_messages = request.GET.get("only_messages") only_messages = request.GET.get("only_messages")
if request_room: if request_room:
@ -80,11 +84,12 @@ class ChatView(ListView):
request_room = None request_room = None
self.room_id = request_room self.room_id = request_room
self.messages = Message.objects.filter( self.messages = (
hidden=False, room=self.room_id, id__lt=last_id Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
)[: self.page_size] .select_related("author", "author__user")
.defer("author__about", "author__user_script")[:page_size]
)
if not only_messages: if not only_messages:
update_last_seen(request, **kwargs)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
return render( return render(
@ -101,10 +106,14 @@ class ChatView(ListView):
context["title"] = self.title context["title"] = self.title
context["last_msg"] = event.last() context["last_msg"] = event.last()
context["status_sections"] = get_status_context(self.request) context["status_sections"] = get_status_context(self.request.profile)
context["room"] = self.room_id context["room"] = self.room_id
context["has_next"] = self.has_next() context["has_next"] = self.has_next()
context["unread_count_lobby"] = get_unread_count(None, self.request.profile) context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
context["chat_channel"] = encrypt_channel(
"chat_" + str(self.request.profile.id)
)
context["chat_lobby_channel"] = encrypt_channel("chat_lobby")
if self.room: if self.room:
users_room = [self.room.user_one, self.room.user_two] users_room = [self.room.user_one, self.room.user_two]
users_room.remove(self.request.profile) users_room.remove(self.request.profile)
@ -187,7 +196,7 @@ def post_message(request):
if not room: if not room:
event.post( event.post(
"chat_lobby", encrypt_channel("chat_lobby"),
{ {
"type": "lobby", "type": "lobby",
"author_id": request.profile.id, "author_id": request.profile.id,
@ -199,7 +208,7 @@ def post_message(request):
else: else:
for user in room.users(): for user in room.users():
event.post( event.post(
"chat_" + str(user.id), encrypt_channel("chat_" + str(user.id)),
{ {
"type": "private", "type": "private",
"author_id": request.profile.id, "author_id": request.profile.id,
@ -208,6 +217,10 @@ def post_message(request):
"tmp_id": request.POST.get("tmp_id"), "tmp_id": request.POST.get("tmp_id"),
}, },
) )
if user != request.profile:
UserRoom.objects.filter(user=user, room=room).update(
unread_count=F("unread_count") + 1
)
return JsonResponse(ret) return JsonResponse(ret)
@ -254,35 +267,33 @@ def update_last_seen(request, **kwargs):
room_id = request.POST.get("room") room_id = request.POST.get("room")
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
profile = request.profile profile = request.profile
room = None room = None
if room_id: if room_id:
room = Room.objects.get(id=int(room_id)) room = Room.objects.filter(id=int(room_id)).first()
except Room.DoesNotExist: except Room.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
except Exception as e:
return HttpResponseBadRequest()
if room and not room.contain(profile): if room and not room.contain(profile):
return HttpResponseBadRequest() return HttpResponseBadRequest()
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room) user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
user_room.last_seen = timezone.now() user_room.last_seen = timezone.now()
user_room.unread_count = 0
user_room.save() user_room.save()
return JsonResponse({"msg": "updated"}) return JsonResponse({"msg": "updated"})
def get_online_count(): def get_online_count():
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2) last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
return Profile.objects.filter(last_access__gte=last_two_minutes).count() return Profile.objects.filter(last_access__gte=last_5_minutes).count()
def get_user_online_status(user): def get_user_online_status(user):
time_diff = timezone.now() - user.last_access time_diff = timezone.now() - user.last_access
is_online = time_diff <= timezone.timedelta(minutes=2) is_online = time_diff <= timezone.timedelta(minutes=5)
return is_online return is_online
@ -319,47 +330,51 @@ def user_online_status_ajax(request):
) )
def get_online_status(request_user, queryset, rooms=None): def get_online_status(profile, other_profile_ids, rooms=None):
if not queryset: if not other_profile_ids:
return None return None
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2) joined_ids = ",".join([str(id) for id in other_profile_ids])
other_profiles = Profile.objects.raw(
f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})"
)
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
ret = [] ret = []
if rooms: if rooms:
unread_count = get_unread_count(rooms, request_user) unread_count = get_unread_count(rooms, profile)
count = {} count = {}
for i in unread_count: for i in unread_count:
count[i["other_user"]] = i["unread_count"] count[i["other_user"]] = i["unread_count"]
for other_profile in other_profiles:
for user in queryset:
is_online = False is_online = False
if user.last_access >= last_two_minutes: if other_profile.last_access >= last_5_minutes:
is_online = True is_online = True
user_dict = {"user": user, "is_online": is_online} user_dict = {"user": other_profile, "is_online": is_online}
if rooms and user.id in count: if rooms and other_profile.id in count:
user_dict["unread_count"] = count[user.id] user_dict["unread_count"] = count[other_profile.id]
user_dict["url"] = encrypt_url(request_user.id, user.id) user_dict["url"] = encrypt_url(profile.id, other_profile.id)
ret.append(user_dict) ret.append(user_dict)
return ret return ret
def get_status_context(request, include_ignored=False): def get_status_context(profile, include_ignored=False):
if include_ignored: if include_ignored:
ignored_users = Profile.objects.none() ignored_users = []
queryset = Profile.objects queryset = Profile.objects
else: else:
ignored_users = Ignore.get_ignored_users(request.profile) ignored_users = list(
Ignore.get_ignored_users(profile).values_list("id", flat=True)
)
queryset = Profile.objects.exclude(id__in=ignored_users) queryset = Profile.objects.exclude(id__in=ignored_users)
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2) last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
recent_profile = ( recent_profile = (
Room.objects.filter(Q(user_one=request.profile) | Q(user_two=request.profile)) Room.objects.filter(Q(user_one=profile) | Q(user_two=profile))
.annotate( .annotate(
last_msg_time=Subquery( last_msg_time=Subquery(
Message.objects.filter(room=OuterRef("pk")).values("time")[:1] Message.objects.filter(room=OuterRef("pk")).values("time")[:1]
), ),
other_user=Case( other_user=Case(
When(user_one=request.profile, then="user_two"), When(user_one=profile, then="user_two"),
default="user_one", default="user_one",
), ),
) )
@ -369,50 +384,49 @@ def get_status_context(request, include_ignored=False):
.values("other_user", "id")[:20] .values("other_user", "id")[:20]
) )
recent_profile_id = [str(i["other_user"]) for i in recent_profile] recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
joined_id = ",".join(recent_profile_id)
recent_rooms = [int(i["id"]) for i in recent_profile] recent_rooms = [int(i["id"]) for i in recent_profile]
recent_list = None
if joined_id:
recent_list = Profile.objects.raw(
f"SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})"
)
friend_list = ( friend_list = (
Friend.get_friend_profiles(request.profile) Friend.get_friend_profiles(profile)
.exclude(id__in=recent_profile_id) .exclude(id__in=recent_profile_ids)
.exclude(id__in=ignored_users) .exclude(id__in=ignored_users)
.order_by("-last_access") .order_by("-last_access")
.values_list("id", flat=True)
) )
admin_list = ( admin_list = (
queryset.filter(display_rank="admin") queryset.filter(display_rank="admin")
.exclude(id__in=friend_list) .exclude(id__in=friend_list)
.exclude(id__in=recent_profile_id) .exclude(id__in=recent_profile_ids)
.values_list("id", flat=True)
) )
all_user_status = ( all_user_status = (
queryset.filter(display_rank="user", last_access__gte=last_two_minutes) queryset.filter(last_access__gte=last_5_minutes)
.annotate(is_online=Case(default=True, output_field=BooleanField())) .annotate(is_online=Case(default=True, output_field=BooleanField()))
.order_by("-rating") .order_by("-rating")
.exclude(id__in=friend_list) .exclude(id__in=friend_list)
.exclude(id__in=admin_list) .exclude(id__in=admin_list)
.exclude(id__in=recent_profile_id)[:30] .exclude(id__in=recent_profile_ids)
.values_list("id", flat=True)[:30]
) )
return [ return [
{ {
"title": "Recent", "title": "Recent",
"user_list": get_online_status(request.profile, recent_list, recent_rooms), "user_list": get_online_status(profile, recent_profile_ids, recent_rooms),
}, },
{ {
"title": "Following", "title": "Following",
"user_list": get_online_status(request.profile, friend_list), "user_list": get_online_status(profile, friend_list),
}, },
{ {
"title": "Admin", "title": "Admin",
"user_list": get_online_status(request.profile, admin_list), "user_list": get_online_status(profile, admin_list),
}, },
{ {
"title": "Other", "title": "Other",
"user_list": get_online_status(request.profile, all_user_status), "user_list": get_online_status(profile, all_user_status),
}, },
] ]
@ -423,7 +437,7 @@ def online_status_ajax(request):
request, request,
"chat/online_status.html", "chat/online_status.html",
{ {
"status_sections": get_status_context(request), "status_sections": get_status_context(request.profile),
"unread_count_lobby": get_unread_count(None, request.profile), "unread_count_lobby": get_unread_count(None, request.profile),
}, },
) )
@ -447,7 +461,6 @@ def get_or_create_room(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
request_id, other_id = decrypt_url(decrypted_other_id) request_id, other_id = decrypt_url(decrypted_other_id)
if not other_id or not request_id or request_id != request.profile.id: if not other_id or not request_id or request_id != request.profile.id:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -475,48 +488,31 @@ def get_or_create_room(request):
def get_unread_count(rooms, user): def get_unread_count(rooms, user):
if rooms: if rooms:
mess = (
Message.objects.filter(
room=OuterRef("room"), time__gte=OuterRef("last_seen")
)
.exclude(author=user)
.order_by()
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
return (
UserRoom.objects.filter(user=user, room__in=rooms)
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
other_user=Case(
When(room__user_one=user, then="room__user_two"),
default="room__user_one",
),
)
.filter(unread_count__gte=1)
.values("other_user", "unread_count")
)
else: # lobby
mess = (
Message.objects.filter(room__isnull=True, time__gte=OuterRef("last_seen"))
.exclude(author=user)
.order_by()
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
res = ( res = (
UserRoom.objects.filter(user=user, room__isnull=True) UserRoom.objects.filter(user=user, room__in=rooms, unread_count__gt=0)
.annotate( .select_related("room__user_one", "room__user_two")
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0), .values("unread_count", "room__user_one", "room__user_two")
) )
.values_list("unread_count", flat=True) for ur in res:
ur["other_user"] = (
ur["room__user_one"]
if ur["room__user_two"] == user.id
else ur["room__user_two"]
)
return res
else: # lobby
user_room = UserRoom.objects.filter(user=user, room__isnull=True).first()
if not user_room:
return 0
last_seen = user_room.last_seen
res = (
Message.objects.filter(room__isnull=True, time__gte=last_seen)
.exclude(author=user)
.exclude(hidden=True)
.count()
) )
return res[0] if len(res) else 0 return res
@login_required @login_required

View file

@ -484,3 +484,6 @@ except IOError:
pass pass
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Chat
CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U="

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.18 on 2023-08-28 01:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0165_drop_output_prefix_override"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="display_rank",
field=models.CharField(
choices=[
("user", "Normal User"),
("setter", "Problem Setter"),
("admin", "Admin"),
],
db_index=True,
default="user",
max_length=10,
verbose_name="display rank",
),
),
]

View file

@ -183,6 +183,7 @@ class Profile(models.Model):
("setter", "Problem Setter"), ("setter", "Problem Setter"),
("admin", "Admin"), ("admin", "Admin"),
), ),
db_index=True,
) )
mute = models.BooleanField( mute = models.BooleanField(
verbose_name=_("comment mute"), verbose_name=_("comment mute"),

View file

@ -7,21 +7,11 @@
<script type="text/javascript" src="{{ static('mathjax3_config.js') }}"></script> <script type="text/javascript" src="{{ static('mathjax3_config.js') }}"></script>
<script type="text/javascript" src="{{ static('event.js') }}"></script> <script type="text/javascript" src="{{ static('event.js') }}"></script>
<script src="https://unpkg.com/@popperjs/core@2"></script>
<script type="module" src="https://unpkg.com/emoji-picker-element@1"></script> <script type="module" src="https://unpkg.com/emoji-picker-element@1"></script>
<script type="text/javascript"> {% compress js %}
let message_template = ` {% include "chat/chat_js.html" %}
{% with message=message_template %} {% endcompress %}
{% include "chat/message.html" %}
{% endwith %}
`;
let META_HEADER = [
"{{_('Recent')}}",
"{{_('Following')}}",
"{{_('Admin')}}",
"{{_('Other')}}",
];
</script>
<script type="text/javascript"> <script type="text/javascript">
window.limit_time = 24; window.limit_time = 24;
window.room_id = "{{room if room else ''}}"; window.room_id = "{{room if room else ''}}";
@ -32,73 +22,10 @@
window.pushed_messages = new Set(); window.pushed_messages = new Set();
let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches; let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches;
function load_next_page(last_id, refresh_html=false) {
var param = {
'last_id': last_id,
'only_messages': true,
}
$.get("{{ url('chat', '') }}" + window.room_id, param)
.fail(function() {
console.log("Fail to load page, last_id = " + last_id);
})
.done(function(data) {
if (refresh_html) {
$('#chat-log').html('');
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
window.lock = true;
}
var time = refresh_html ? 0 : 200;
setTimeout(function() {
$(".has_next").remove();
let $chat_box = $('#chat-box');
let lastMsgPos = scrollTopOfBottom($chat_box)
$('#loader').hide();
if (refresh_html) {
$('#chat-log').append(data);
}
else {
$('#chat-log').prepend(data);
}
register_time($('.time-with-rel'));
merge_authors();
if (!refresh_html) {
$chat_box.scrollTop(scrollTopOfBottom($chat_box) - lastMsgPos);
}
else {
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
}
window.lock = false;
window.has_next = parseInt($(".has_next").attr("value"));
}, time);
})
}
function scrollTopOfBottom(container) {
return container[0].scrollHeight - container.innerHeight()
}
function scrollContainer(container, loader) {
container.scroll(function() {
if (container.scrollTop() == 0) {
if (!window.lock && window.has_next) {
loader.show();
var message_ids = $('.message').map(function() {
return parseInt($(this).attr('message-id'));
}).get();
load_next_page(Math.min(...message_ids));
}
}
})}
window.load_dynamic_update = function (last_msg) { window.load_dynamic_update = function (last_msg) {
var receiver = new EventReceiver( var receiver = new EventReceiver(
"{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}", "{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
['chat_lobby', 'chat_{{request.profile.id}}'], last_msg, function (message) { ['{{chat_lobby_channel}}', '{{chat_channel}}'], last_msg, function (message) {
if (window.pushed_messages.has(message.message)) { if (window.pushed_messages.has(message.message)) {
return; return;
} }
@ -115,484 +42,8 @@
return receiver; return receiver;
} }
function refresh_status() {
$.get("{{url('online_status_ajax')}}")
.fail(function() {
console.log("Fail to get online status");
})
.done(function(data) {
if (data.status == 403) {
console.log("Fail to retrieve data");
}
else {
$('#chat-online-list').html(data).find('.toggle').each(function () {
register_toggle($(this));
});
register_click_space();
}
})
var data = {
'user': window.other_user_id,
};
$.get("{{url('user_online_status_ajax')}}", data)
.fail(function() {
console.log("Fail to get user online status");
})
.done(function(data) {
$('#chat-info').html(data);
register_time($('.time-with-rel'));
register_setting();
})
}
function add_message(data) {
var $data = $(data);
$('#chat-log').append($data);
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
register_time($('.time-with-rel'));
MathJax.typeset();
merge_authors();
}
function add_new_message(message, room, is_self_author) {
function callback(update) {
if (!document['hidden']) {
if (update) update_last_seen();
refresh_status();
}
else if (!is_self_author) {
window.unread_message++;
document.title = "(" + window.unread_message + ") " + "{{ _('New message(s)') }}";
}
}
if (room == window.room_id) {
$.get({
url: "{{ url('chat_message_ajax') }}",
data: {
message: message,
},
success: function (data) {
add_message(data);
callback(true);
},
error: function (data) {
console.log('Could not add new message');
}
});
}
else {
callback(false);
}
}
function check_new_message(message, tmp_id, room) {
if (room == room_id) {
$.get({
url: "{{ url('chat_message_ajax') }}",
data: {
message: message,
},
success: function (data) {
var $body_block = $(data).find('.body-block');
if ($('#message-'+tmp_id).length) {
$('#message-'+tmp_id).replaceWith(data);
}
else if ($('#body-block-'+tmp_id).length) {
$('#body-block-'+tmp_id).replaceWith($body_block);
}
else {
add_new_message(message, room, true);
}
MathJax.typeset();
register_time($('.time-with-rel'));
remove_unread_current_user();
merge_authors();
},
error: function (data) {
console.log('Fail to check message');
var $body = $('#body-block-'+tmp_id + ' p');
$body.css('text-decoration', 'line-through');
$body.css('text-decoration-color', 'red');
}
});
}
}
function merge_authors() {
var time_limit = 5; // minutes
var last = {
username: null,
time: null,
$content: null
};
$('.body-message').each(function() {
var username = $(this).find(".username a").text().trim();
var $body = $(this).find(".content-message .body-block");
var time = moment($(this).find(".time-with-rel").attr('data-iso'));
var $content = $(this).children('.content-message');
if (username == window.user.name) {
$(this).find('.message-text').each(function() {
$(this).removeClass('message-text-other').addClass('message-text-myself');
});
}
if (username == last.username && time.diff(last.time, 'minutes') <= time_limit) {
last.$content.append($body);
$(this).parent().remove();
}
else {
last.username = username;
last.time = time;
last.$content = $content;
}
});
}
function add_message_from_template(body, tmp_id) {
var html = message_template;
html = html.replaceAll('$body', body).replaceAll('$id', tmp_id);
var $html = $(html);
$html.find('.time-with-rel').attr('data-iso', (new Date()).toISOString());
add_message($html[0].outerHTML);
}
function submit_chat() {
{% if last_msg and not request.profile.mute %}
if ($("#chat-input").val().trim()) {
var body = $('#chat-input').val().trim();
// body = body.split('\n').join('\n\n');
var message = {
body: body,
room: window.room_id,
tmp_id: Date.now(),
};
$('#chat-input').val('');
add_message_from_template(body, message.tmp_id);
$.post("{{ url('post_chat_message') }}", message)
.fail(function(res) {
console.log('Fail to send message');
})
.done(function(res, status) {
$('#empty_msg').hide();
$('#chat-input').focus();
})
}
{% endif %}
}
function resize_emoji(element) {
var html = element.html();
html = html.replace(/(\p{Extended_Pictographic})/ug, `<span class="big-emoji">$1</span>`);
element.html(html);
}
function insert_char_after_cursor(elem, char) {
var val = elem.value;
if (typeof elem.selectionStart == "number" && typeof elem.selectionEnd == "number") {
var start = elem.selectionStart;
var prefix = elem.value.slice(0, start);
var prefix_added = prefix + char;
var chars = [...val];
chars.splice([...prefix].length, 0, char);
elem.value = chars.join('');
elem.selectionStart = elem.selectionEnd = prefix_added.length;
} else if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
elem.focus();
range.text = char;
range.collapse(false);
range.select();
}
}
function load_room(encrypted_user) {
if (window.lock_click_space) return;
function callback() {
history.replaceState(null, '', "{{url('chat', '')}}" + window.room_id);
load_next_page(null, true);
update_last_seen();
refresh_status();
$('#chat-input').focus();
}
window.lock_click_space = true;
if (encrypted_user) {
$.get("{{url('get_or_create_room')}}" + `?other=${encrypted_user}`)
.done(function(data) {
window.room_id = data.room;
window.other_user_id = data.other_user_id;
callback();
})
.fail(function() {
console.log('Fail to get_or_create_room');
})
}
else {
window.room_id = '';
window.other_user_id = '';
callback();
}
window.lock_click_space = false;
}
function register_click_space() {
$('.click_space').on('click', function(e) {
if ($(this).attr('id') == 'click_space_' + window.other_user_id) {
return;
}
var other_user = $(this).attr('value');
load_room(other_user);
});
$('#lobby_row').on('click', function(e) {
if (window.room_id) {
load_room(null);
}
});
if (isMobile) {
$('#chat-tab a').click();
}
}
function update_last_seen() {
var data = {
room: window.room_id
};
$.post("{{ url('update_last_seen') }}", data)
.fail(function(data) {
console.log('Fail to update last seen');
})
.done(function(data) {
})
}
function remove_unread_current_user() {
if (window.other_user_id) {
$("#unread-count-" + window.other_user_id).hide();
}
else {
$('#unread-count-lobby').hide();
}
}
function register_setting() {
$('#setting-button').on('click', function() {
$('#setting-content').toggle();
});
$('#setting-content li').on('click', function() {
$(this).children('a')[0].click();
})
$('#setting-content a').on('click', function() {
var href = $(this).attr('href');
href += '?next=' + window.location.pathname;
$(this).attr('href', href);
})
}
$(function() { $(function() {
$('#loader').hide();
merge_authors();
window.has_next = parseInt($(".has_next").attr("value"));
scrollContainer($('#chat-box'), $('#loader'))
{% if request.user.is_staff %}
$(document).on("click", ".chat_remove", function() {
var elt = $(this);
$.ajax({
url: "{{ url('delete_chat_message') }}",
type: 'post',
data: {
message: elt.attr('value'),
},
dataType: 'json',
success: function(data){
var $block = elt.parent();
if ($block.parent().find('.body-block').length > 1) {
$block.remove();
}
else {
elt.closest('li').remove();
}
},
fail: function(data) {
console.log('Fail to delete');
},
});
});
$(document).on("click", ".chat_mute", function() {
if (confirm("{{_('Mute this user and delete all messages?')}}")) {
var elt = $(this);
$.ajax({
url: "{{ url('mute_chat_message') }}",
type: 'post',
data: {
message: elt.attr('value'),
},
dataType: 'json',
success: function(data){
window.location.reload();
},
fail: function(data) {
console.log('Fail to delete');
},
});
}
});
{% endif %}
$("#chat-log").show();
$("#chat-log").change(function() {
$('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
});
$('#chat-input').focus();
$('#chat-input').keydown(function(e) {
if (e.keyCode === 13) {
if (e.ctrlKey || e.shiftKey) {
insert_char_after_cursor(this, "\n");
}
else {
e.preventDefault();
submit_chat();
}
return false
}
return true
});
$('.chat-right-panel').hide();
$('#chat-tab').find('a').click(function (e) {
e.preventDefault();
$('#chat-tab').addClass('active');
$('#online-tab').removeClass('active');
$('.chat-left-panel').show();
$('.chat-right-panel').hide();
});
$('#online-tab').find('a').click(function (e) {
e.preventDefault();
$('#online-tab').addClass('active');
$('#chat-tab').removeClass('active');
$('.chat-left-panel').hide();
$('.chat-right-panel').show();
});
$('#refresh-button').on('click', function(e) {
e.preventDefault();
refresh_status();
});
setInterval(refresh_status, 2 * 60 * 1000);
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
load_dynamic_update({{last_msg}}); load_dynamic_update({{last_msg}});
const button = document.querySelector('#emoji-button');
const tooltip = document.querySelector('.tooltip');
Popper.createPopper(button, tooltip, {
placement: 'left-end',
});
function toggleEmoji() {
tooltip.classList.toggle('shown')
}
$('#emoji-button').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggleEmoji();
});
// Đóng bảng emoji khi click bất kỳ chỗ nào trên màn hình
document.addEventListener("click", function(e) {
if (!tooltip.contains(e.target)) {
tooltip.classList.remove('shown'); // Ẩn bảng emoji
}
});
$('emoji-picker').on('emoji-click', function(e) {
var $chat = $('#chat-input').get(0);
insert_char_after_cursor($chat, e.detail.unicode);
$chat.focus();
})
register_click_space();
document.addEventListener('keydown', function(e) {
if (e.keyCode === 27 && $('.tooltip').hasClass('shown')) {
toggleEmoji();
}
})
$('#search-handle').replaceWith($('<select>').attr({
id: 'search-handle',
name: 'other',
onchange: 'form.submit()'
}));
var in_user_redirect = false;
$('#search-handle').select2({
placeholder: '{{ _('Search by handle...') }}',
ajax: {
url: '{{ url('chat_user_search_select2_ajax') }}'
},
minimumInputLength: 1,
escapeMarkup: function (markup) {
return markup;
},
templateResult: function (data, container) {
return $('<span>')
.append($('<img>', {
'class': 'user-search-image', src: data.gravatar_url,
width: 24, height: 24
}))
.append($('<span>', {'class': data.display_rank + ' user-search-name'}).text(data.text))
.append($('<a>', {href: '/user/' + data.text, 'class': 'user-redirect'})
.append($('<i>', {'class': 'fa fa-mail-forward'}))
.mouseover(function () {
in_user_redirect = true;
}).mouseout(function () {
in_user_redirect = false;
}));
}
}).on('select2:selecting', function () {
return !in_user_redirect;
});
$("#chat-input").on("keyup", function() {
$("#chat-input").scrollTop($("#chat-input")[0].scrollHeight);
});
// https://stackoverflow.com/questions/42121565/detecting-class-change-without-setinterval
if (typeof(MutationObserver) !== undefined) {
var observer = new MutationObserver(function (event) {
if (!document['hidden'] && window.unread_message > 0) {
update_last_seen();
refresh_status();
window.unread_message = 0;
document.title = "{{_('Chat Box')}}";
}
})
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class'],
childList: false,
characterData: false
})
}
register_setting();
}); });
</script> </script>
@ -638,8 +89,7 @@
{% include 'chat/user_online_status.html' %} {% include 'chat/user_online_status.html' %}
</div> </div>
<div id="chat-box"> <div id="chat-box">
<img src="{{static('loading.gif')}}" id="loader">
<img src="{{static('loading.gif')}}" id="loader" height="2em">
<ul id="chat-log" style="display: none"> <ul id="chat-log" style="display: none">
{% include 'chat/message_list.html' %} {% include 'chat/message_list.html' %}
</ul> </ul>

View file

@ -1,3 +1,4 @@
{% compress css %}
<style> <style>
footer { footer {
display: none; display: none;
@ -51,7 +52,7 @@
display: block; display: block;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
width: 4%; width: 80px;
} }
.profile-pic { .profile-pic {
height: 2.6em; height: 2.6em;
@ -130,3 +131,4 @@
} }
} }
</style> </style>
{% endcompress %}

553
templates/chat/chat_js.html Normal file
View file

@ -0,0 +1,553 @@
<script type="text/javascript">
let message_template = `
{% with message=message_template %}
{% include "chat/message.html" %}
{% endwith %}
`;
let META_HEADER = [
"{{_('Recent')}}",
"{{_('Following')}}",
"{{_('Admin')}}",
"{{_('Other')}}",
];
function load_next_page(last_id, refresh_html=false) {
var param = {
'last_id': last_id,
'only_messages': true,
}
$.get("{{ url('chat', '') }}" + window.room_id, param)
.fail(function() {
console.log("Fail to load page, last_id = " + last_id);
})
.done(function(data) {
if (refresh_html) {
$('#chat-log').html('');
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
window.lock = true;
}
var time = refresh_html ? 0 : 200;
setTimeout(function() {
$(".has_next").remove();
let $chat_box = $('#chat-box');
let lastMsgPos = scrollTopOfBottom($chat_box)
$('#loader').hide();
if (refresh_html) {
$('#chat-log').append(data);
}
else {
$('#chat-log').prepend(data);
}
register_time($('.time-with-rel'));
merge_authors();
if (!refresh_html) {
$chat_box.scrollTop(scrollTopOfBottom($chat_box) - lastMsgPos);
}
else {
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
}
window.lock = false;
window.has_next = parseInt($(".has_next").attr("value"));
}, time);
})
}
function scrollTopOfBottom(container) {
return container[0].scrollHeight - container.innerHeight()
}
function scrollContainer(container, loader) {
container.scroll(function() {
if (container.scrollTop() == 0) {
if (!window.lock && window.has_next) {
loader.show();
var message_ids = $('.message').map(function() {
return parseInt($(this).attr('message-id'));
}).get();
load_next_page(Math.min(...message_ids));
}
}
})}
function refresh_status() {
$.get("{{url('online_status_ajax')}}")
.fail(function() {
console.log("Fail to get online status");
})
.done(function(data) {
if (data.status == 403) {
console.log("Fail to retrieve data");
}
else {
$('#chat-online-list').html(data).find('.toggle').each(function () {
register_toggle($(this));
});
register_click_space();
}
})
var data = {
'user': window.other_user_id,
};
$.get("{{url('user_online_status_ajax')}}", data)
.fail(function() {
console.log("Fail to get user online status");
})
.done(function(data) {
$('#chat-info').html(data);
register_time($('.time-with-rel'));
register_setting();
})
}
function add_message(data) {
var $data = $(data);
$('#chat-log').append($data);
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
register_time($('.time-with-rel'));
MathJax.typeset();
merge_authors();
}
function add_new_message(message, room, is_self_author) {
function callback(update) {
if (!document['hidden']) {
if (update) update_last_seen();
refresh_status();
}
else if (!is_self_author) {
window.unread_message++;
document.title = "(" + window.unread_message + ") " + "{{ _('New message(s)') }}";
}
}
if (room == window.room_id) {
$.get({
url: "{{ url('chat_message_ajax') }}",
data: {
message: message,
},
success: function (data) {
add_message(data);
callback(true);
},
error: function (data) {
console.log('Could not add new message');
}
});
}
else {
callback(false);
}
}
function check_new_message(message, tmp_id, room) {
if (room == room_id) {
$.get({
url: "{{ url('chat_message_ajax') }}",
data: {
message: message,
},
success: function (data) {
var $body_block = $(data).find('.body-block');
if ($('#message-'+tmp_id).length) {
$('#message-'+tmp_id).replaceWith(data);
}
else if ($('#body-block-'+tmp_id).length) {
$('#body-block-'+tmp_id).replaceWith($body_block);
}
else {
add_new_message(message, room, true);
}
MathJax.typeset();
register_time($('.time-with-rel'));
remove_unread_current_user();
merge_authors();
},
error: function (data) {
console.log('Fail to check message');
var $body = $('#body-block-'+tmp_id + ' p');
$body.css('text-decoration', 'line-through');
$body.css('text-decoration-color', 'red');
}
});
}
}
function merge_authors() {
var time_limit = 5; // minutes
var last = {
username: null,
time: null,
$content: null
};
$('.body-message').each(function() {
var username = $(this).find(".username a").text().trim();
var $body = $(this).find(".content-message .body-block");
var time = moment($(this).find(".time-with-rel").attr('data-iso'));
var $content = $(this).children('.content-message');
if (username == window.user.name) {
$(this).find('.message-text').each(function() {
$(this).removeClass('message-text-other').addClass('message-text-myself');
});
}
if (username == last.username && time.diff(last.time, 'minutes') <= time_limit) {
last.$content.append($body);
$(this).parent().remove();
}
else {
last.username = username;
last.time = time;
last.$content = $content;
}
});
}
function add_message_from_template(body, tmp_id) {
var html = message_template;
html = html.replaceAll('$body', body).replaceAll('$id', tmp_id);
var $html = $(html);
$html.find('.time-with-rel').attr('data-iso', (new Date()).toISOString());
add_message($html[0].outerHTML);
}
function submit_chat() {
{% if last_msg and not request.profile.mute %}
if ($("#chat-input").val().trim()) {
var body = $('#chat-input').val().trim();
// body = body.split('\n').join('\n\n');
var message = {
body: body,
room: window.room_id,
tmp_id: Date.now(),
};
$('#chat-input').val('');
add_message_from_template(body, message.tmp_id);
$.post("{{ url('post_chat_message') }}", message)
.fail(function(res) {
console.log('Fail to send message');
})
.done(function(res, status) {
$('#empty_msg').hide();
$('#chat-input').focus();
})
}
{% endif %}
}
function resize_emoji(element) {
var html = element.html();
html = html.replace(/(\p{Extended_Pictographic})/ug, `<span class="big-emoji">$1</span>`);
element.html(html);
}
function insert_char_after_cursor(elem, char) {
var val = elem.value;
if (typeof elem.selectionStart == "number" && typeof elem.selectionEnd == "number") {
var start = elem.selectionStart;
var prefix = elem.value.slice(0, start);
var prefix_added = prefix + char;
var chars = [...val];
chars.splice([...prefix].length, 0, char);
elem.value = chars.join('');
elem.selectionStart = elem.selectionEnd = prefix_added.length;
} else if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
elem.focus();
range.text = char;
range.collapse(false);
range.select();
}
}
function load_room(encrypted_user) {
if (window.lock_click_space) return;
function callback() {
history.replaceState(null, '', "{{url('chat', '')}}" + window.room_id);
load_next_page(null, true);
update_last_seen();
refresh_status();
$('#chat-input').focus();
}
window.lock_click_space = true;
if (encrypted_user) {
$.get("{{url('get_or_create_room')}}" + `?other=${encrypted_user}`)
.done(function(data) {
window.room_id = data.room;
window.other_user_id = data.other_user_id;
callback();
})
.fail(function() {
console.log('Fail to get_or_create_room');
})
}
else {
window.room_id = '';
window.other_user_id = '';
callback();
}
window.lock_click_space = false;
}
function register_click_space() {
$('.click_space').on('click', function(e) {
if ($(this).attr('id') == 'click_space_' + window.other_user_id) {
return;
}
var other_user = $(this).attr('value');
load_room(other_user);
});
$('#lobby_row').on('click', function(e) {
if (window.room_id) {
load_room(null);
}
});
if (isMobile) {
$('#chat-tab a').click();
}
}
function update_last_seen() {
var data = {
room: window.room_id
};
$.post("{{ url('update_last_seen') }}", data)
.fail(function(data) {
console.log('Fail to update last seen');
})
.done(function(data) {
})
}
function remove_unread_current_user() {
if (window.other_user_id) {
$("#unread-count-" + window.other_user_id).hide();
}
else {
$('#unread-count-lobby').hide();
}
}
function register_setting() {
$('#setting-button').on('click', function() {
$('#setting-content').toggle();
});
$('#setting-content li').on('click', function() {
$(this).children('a')[0].click();
})
$('#setting-content a').on('click', function() {
var href = $(this).attr('href');
href += '?next=' + window.location.pathname;
$(this).attr('href', href);
})
}
$(function() {
$('#loader').hide();
update_last_seen();
merge_authors();
window.has_next = parseInt($(".has_next").attr("value"));
scrollContainer($('#chat-box'), $('#loader'))
{% if request.user.is_staff %}
$(document).on("click", ".chat_remove", function() {
var elt = $(this);
$.ajax({
url: "{{ url('delete_chat_message') }}",
type: 'post',
data: {
message: elt.attr('value'),
},
dataType: 'json',
success: function(data){
var $block = elt.parent();
if ($block.parent().find('.body-block').length > 1) {
$block.remove();
}
else {
elt.closest('li').remove();
}
},
fail: function(data) {
console.log('Fail to delete');
},
});
});
$(document).on("click", ".chat_mute", function() {
if (confirm("{{_('Mute this user and delete all messages?')}}")) {
var elt = $(this);
$.ajax({
url: "{{ url('mute_chat_message') }}",
type: 'post',
data: {
message: elt.attr('value'),
},
dataType: 'json',
success: function(data){
window.location.reload();
},
fail: function(data) {
console.log('Fail to delete');
},
});
}
});
{% endif %}
$("#chat-log").show();
$("#chat-log").change(function() {
$('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
});
$('#chat-input').focus();
$('#chat-input').keydown(function(e) {
if (e.keyCode === 13) {
if (e.ctrlKey || e.shiftKey) {
insert_char_after_cursor(this, "\n");
}
else {
e.preventDefault();
submit_chat();
}
return false
}
return true
});
$('.chat-right-panel').hide();
$('#chat-tab').find('a').click(function (e) {
e.preventDefault();
$('#chat-tab').addClass('active');
$('#online-tab').removeClass('active');
$('.chat-left-panel').show();
$('.chat-right-panel').hide();
});
$('#online-tab').find('a').click(function (e) {
e.preventDefault();
$('#online-tab').addClass('active');
$('#chat-tab').removeClass('active');
$('.chat-left-panel').hide();
$('.chat-right-panel').show();
});
$('#refresh-button').on('click', function(e) {
e.preventDefault();
refresh_status();
});
setInterval(refresh_status, 2 * 60 * 1000);
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
const button = document.querySelector('#emoji-button');
const tooltip = document.querySelector('.tooltip');
Popper.createPopper(button, tooltip, {
placement: 'left-end',
});
function toggleEmoji() {
tooltip.classList.toggle('shown')
}
$('#emoji-button').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggleEmoji();
});
// Đóng bảng emoji khi click bất kỳ chỗ nào trên màn hình
document.addEventListener("click", function(e) {
if (!tooltip.contains(e.target)) {
tooltip.classList.remove('shown'); // Ẩn bảng emoji
}
});
$('emoji-picker').on('emoji-click', function(e) {
var $chat = $('#chat-input').get(0);
insert_char_after_cursor($chat, e.detail.unicode);
$chat.focus();
})
register_click_space();
document.addEventListener('keydown', function(e) {
if (e.keyCode === 27 && $('.tooltip').hasClass('shown')) {
toggleEmoji();
}
})
$('#search-handle').replaceWith($('<select>').attr({
id: 'search-handle',
name: 'other',
onchange: 'form.submit()'
}));
var in_user_redirect = false;
$('#search-handle').select2({
placeholder: '{{ _('Search by handle...') }}',
ajax: {
url: '{{ url('chat_user_search_select2_ajax') }}'
},
minimumInputLength: 1,
escapeMarkup: function (markup) {
return markup;
},
templateResult: function (data, container) {
return $('<span>')
.append($('<img>', {
'class': 'user-search-image', src: data.gravatar_url,
width: 24, height: 24
}))
.append($('<span>', {'class': data.display_rank + ' user-search-name'}).text(data.text))
.append($('<a>', {href: '/user/' + data.text, 'class': 'user-redirect'})
.append($('<i>', {'class': 'fa fa-mail-forward'}))
.mouseover(function () {
in_user_redirect = true;
}).mouseout(function () {
in_user_redirect = false;
}));
}
}).on('select2:selecting', function () {
return !in_user_redirect;
});
$("#chat-input").on("keyup", function() {
$("#chat-input").scrollTop($("#chat-input")[0].scrollHeight);
});
// https://stackoverflow.com/questions/42121565/detecting-class-change-without-setinterval
if (typeof(MutationObserver) !== undefined) {
var observer = new MutationObserver(function (event) {
if (!document['hidden'] && window.unread_message > 0) {
update_last_seen();
refresh_status();
window.unread_message = 0;
document.title = "{{_('Chat Box')}}";
}
})
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class'],
childList: false,
characterData: false
})
}
register_setting();
});
</script>