Make chat faster
This commit is contained in:
parent
f11d9b4b53
commit
2854ac97e9
11 changed files with 903 additions and 801 deletions
20
chat_box/migrations/0013_alter_message_time.py
Normal file
20
chat_box/migrations/0013_alter_message_time.py
Normal 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
38
chat_box/migrations/0014_userroom_unread_count.py
Normal file
38
chat_box/migrations/0014_userroom_unread_count.py
Normal 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),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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 get_unread_boxes(profile):
|
def encrypt_channel(channel):
|
||||||
ignored_users = Ignore.get_ignored_users(profile)
|
return (
|
||||||
|
hmac.new(
|
||||||
mess = (
|
settings.CHAT_SECRET_KEY.encode(),
|
||||||
Message.objects.filter(room=OuterRef("room"), time__gte=OuterRef("last_seen"))
|
channel.encode(),
|
||||||
.exclude(author=profile)
|
hashlib.sha512,
|
||||||
.exclude(author__in=ignored_users)
|
).hexdigest()[:16]
|
||||||
.order_by()
|
+ "%s" % channel
|
||||||
.values("room")
|
|
||||||
.annotate(unread_count=Count("pk"))
|
|
||||||
.values("unread_count")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_unread_boxes(profile):
|
||||||
|
ignored_rooms = Ignore.get_ignored_rooms(profile)
|
||||||
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()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
)
|
||||||
|
for ur in res:
|
||||||
|
ur["other_user"] = (
|
||||||
|
ur["room__user_one"]
|
||||||
|
if ur["room__user_two"] == user.id
|
||||||
|
else ur["room__user_two"]
|
||||||
)
|
)
|
||||||
.values_list("unread_count", flat=True)
|
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
|
||||||
|
|
|
@ -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="
|
||||||
|
|
28
judge/migrations/0166_display_rank_index.py
Normal file
28
judge/migrations/0166_display_rank_index.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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"),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,132 +1,134 @@
|
||||||
<style>
|
{% compress css %}
|
||||||
footer {
|
<style>
|
||||||
display: none;
|
footer {
|
||||||
}
|
|
||||||
|
|
||||||
#content {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #d6dee1;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 6px solid transparent;
|
|
||||||
background-clip: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #a8bbbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-message img{
|
|
||||||
max-height: 12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:not(.shown) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 4%;
|
|
||||||
}
|
|
||||||
.profile-pic {
|
|
||||||
height: 2.6em;
|
|
||||||
width: 2.6em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
margin-top: 0.1em;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
.body-message {
|
|
||||||
padding-left: 3em;
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
border-bottom: 1px dotted lightgray;
|
|
||||||
}
|
|
||||||
.user-time {
|
|
||||||
margin-bottom: 0.3em;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
.clear {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
.content-message {
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.content-message p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#content-body {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
#page-container {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
.sidebox h3 {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.body-block {
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.05em 0.6em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#search-form {
|
|
||||||
float: inherit;
|
|
||||||
}
|
|
||||||
#search-container {
|
|
||||||
margin-bottom: 0.4em;
|
|
||||||
}
|
|
||||||
#setting {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
#page-container {
|
|
||||||
position:fixed;
|
|
||||||
overflow:hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 799px) {
|
|
||||||
html, body {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
#mobile ul {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.info-pic {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
.active-span {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
#content {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #d6dee1;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #a8bbbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-message img{
|
||||||
|
max-height: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:not(.shown) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
.profile-pic {
|
||||||
|
height: 2.6em;
|
||||||
|
width: 2.6em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
margin-top: 0.1em;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.body-message {
|
||||||
|
padding-left: 3em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
border-bottom: 1px dotted lightgray;
|
||||||
|
}
|
||||||
|
.user-time {
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.content-message {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.content-message p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#content-body {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
#page-container {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.sidebox h3 {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.body-block {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.05em 0.6em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#search-form {
|
||||||
|
float: inherit;
|
||||||
|
}
|
||||||
|
#search-container {
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
#setting {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
#page-container {
|
||||||
|
position:fixed;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 799px) {
|
||||||
|
html, body {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
#mobile ul {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.info-pic {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
.active-span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endcompress %}
|
553
templates/chat/chat_js.html
Normal file
553
templates/chat/chat_js.html
Normal 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>
|
Loading…
Reference in a new issue