Add direct message
This commit is contained in:
parent
259cb95b43
commit
2f8ef1b524
20 changed files with 1066 additions and 195 deletions
28
chat_box/migrations/0005_auto_20211011_0714.py
Normal file
28
chat_box/migrations/0005_auto_20211011_0714.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 2.2.17 on 2021-10-11 00:14
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0116_auto_20211011_0645'),
|
||||
('chat_box', '0004_auto_20200505_2336'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Room',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('user_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_one', to='judge.Profile', verbose_name='user 1')),
|
||||
('user_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_two', to='judge.Profile', verbose_name='user 2')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='room',
|
||||
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id'),
|
||||
),
|
||||
]
|
24
chat_box/migrations/0006_userroom.py
Normal file
24
chat_box/migrations/0006_userroom.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.17 on 2021-11-12 05:27
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0116_auto_20211011_0645'),
|
||||
('chat_box', '0005_auto_20211011_0714'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserRoom',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_seen', models.DateTimeField(verbose_name='last seen')),
|
||||
('room', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')),
|
||||
],
|
||||
),
|
||||
]
|
18
chat_box/migrations/0007_auto_20211112_1255.py
Normal file
18
chat_box/migrations/0007_auto_20211112_1255.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.17 on 2021-11-12 05:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('chat_box', '0006_userroom'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userroom',
|
||||
name='last_seen',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='last seen'),
|
||||
),
|
||||
]
|
23
chat_box/migrations/0008_ignore.py
Normal file
23
chat_box/migrations/0008_ignore.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.17 on 2021-11-18 10:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0116_auto_20211011_0645'),
|
||||
('chat_box', '0007_auto_20211112_1255'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Ignore',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ignored_users', models.ManyToManyField(to='judge.Profile')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ignored_chat_users', to='judge.Profile', verbose_name='user')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -8,12 +8,23 @@ from judge.models.profile import Profile
|
|||
|
||||
__all__ = ['Message']
|
||||
|
||||
class Room(models.Model):
|
||||
user_one = models.ForeignKey(Profile, related_name="user_one", verbose_name='user 1', on_delete=CASCADE)
|
||||
user_two = models.ForeignKey(Profile, related_name="user_two", verbose_name='user 2', on_delete=CASCADE)
|
||||
|
||||
def contain(self, profile):
|
||||
return self.user_one == profile or self.user_two == profile
|
||||
def other_user(self, profile):
|
||||
return self.user_one if profile == self.user_two else self.user_two
|
||||
def users(self):
|
||||
return [self.user_one, self.user_two]
|
||||
|
||||
class Message(models.Model):
|
||||
author = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
|
||||
body = models.TextField(verbose_name=_('body of comment'), max_length=8192)
|
||||
hidden = models.BooleanField(verbose_name='is hidden', default=False)
|
||||
room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
new_message = self.id
|
||||
|
@ -25,3 +36,46 @@ class Message(models.Model):
|
|||
verbose_name = 'message'
|
||||
verbose_name_plural = 'messages'
|
||||
ordering = ('-time',)
|
||||
|
||||
class UserRoom(models.Model):
|
||||
user = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE)
|
||||
room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True)
|
||||
last_seen = models.DateTimeField(verbose_name=_('last seen'), auto_now_add=True)
|
||||
|
||||
|
||||
class Ignore(models.Model):
|
||||
user = models.ForeignKey(Profile, related_name="ignored_chat_users", verbose_name=_('user'), on_delete=CASCADE)
|
||||
ignored_users = models.ManyToManyField(Profile)
|
||||
|
||||
@classmethod
|
||||
def is_ignored(self, current_user, new_friend):
|
||||
try:
|
||||
return current_user.ignored_chat_users.get().ignored_users \
|
||||
.filter(id=new_friend.id).exists()
|
||||
except:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_ignored_users(self, user):
|
||||
return self.objects.get(user=user).ignored_users.all()
|
||||
|
||||
@classmethod
|
||||
def add_ignore(self, current_user, friend):
|
||||
ignore, created = self.objects.get_or_create(
|
||||
user = current_user
|
||||
)
|
||||
ignore.ignored_users.add(friend)
|
||||
|
||||
@classmethod
|
||||
def remove_ignore(self, current_user, friend):
|
||||
ignore, created = self.objects.get_or_create(
|
||||
user = current_user
|
||||
)
|
||||
ignore.ignored_users.remove(friend)
|
||||
|
||||
@classmethod
|
||||
def toggle_ignore(self, current_user, friend):
|
||||
if (self.is_ignored(current_user, friend)):
|
||||
self.remove_ignore(current_user, friend)
|
||||
else:
|
||||
self.add_ignore(current_user, friend)
|
18
chat_box/utils.py
Normal file
18
chat_box/utils.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from cryptography.fernet import Fernet
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
secret_key = settings.CHAT_SECRET_KEY
|
||||
fernet = Fernet(secret_key)
|
||||
|
||||
def encrypt_url(creator_id, other_id):
|
||||
message = str(creator_id) + '_' + str(other_id)
|
||||
return fernet.encrypt(message.encode()).decode()
|
||||
|
||||
def decrypt_url(message_encrypted):
|
||||
try:
|
||||
dec_message = fernet.decrypt(message_encrypted.encode()).decode()
|
||||
creator_id, other_id = dec_message.split('_')
|
||||
return int(creator_id), int(other_id)
|
||||
except Exception as e:
|
||||
return None, None
|
|
@ -1,19 +1,25 @@
|
|||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView
|
||||
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
|
||||
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponsePermanentRedirect, HttpResponseRedirect
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import render
|
||||
from django.forms.models import model_to_dict
|
||||
from django.db.models import Case, BooleanField
|
||||
from django.db.models import Case, BooleanField, When, Q, Subquery, OuterRef, Exists, Count, IntegerField
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
|
||||
import datetime
|
||||
|
||||
from judge import event_poster as event
|
||||
from judge.jinja2.gravatar import gravatar
|
||||
from .models import Message, Profile
|
||||
from judge.models import Friend
|
||||
|
||||
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
|
||||
from chat_box.utils import encrypt_url, decrypt_url
|
||||
|
||||
import json
|
||||
|
||||
|
||||
|
@ -21,22 +27,46 @@ class ChatView(ListView):
|
|||
context_object_name = 'message'
|
||||
template_name = 'chat/chat.html'
|
||||
title = _('Chat Box')
|
||||
paginate_by = 50
|
||||
messages = Message.objects.filter(hidden=False)
|
||||
paginator = Paginator(messages, paginate_by)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.room_id = None
|
||||
self.room = None
|
||||
self.paginate_by = 50
|
||||
self.messages = None
|
||||
self.paginator = None
|
||||
|
||||
def get_queryset(self):
|
||||
return self.messages
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
request_room = kwargs['room_id']
|
||||
page = request.GET.get('page')
|
||||
|
||||
if request_room:
|
||||
try:
|
||||
self.room = Room.objects.get(id=request_room)
|
||||
if not can_access_room(request, self.room):
|
||||
return HttpResponseBadRequest()
|
||||
except Room.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
request_room = None
|
||||
|
||||
if request_room != self.room_id or not self.messages:
|
||||
self.room_id = request_room
|
||||
self.messages = Message.objects.filter(hidden=False, room=self.room_id)
|
||||
self.paginator = Paginator(self.messages, self.paginate_by)
|
||||
|
||||
if page == None:
|
||||
update_last_seen(request, **kwargs)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
cur_page = self.paginator.get_page(page)
|
||||
|
||||
return render(request, 'chat/message_list.html', {
|
||||
'object_list': cur_page.object_list,
|
||||
'num_pages': self.paginator.num_pages
|
||||
})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -45,7 +75,22 @@ class ChatView(ListView):
|
|||
context['title'] = self.title
|
||||
context['last_msg'] = event.last()
|
||||
context['status_sections'] = get_status_context(self.request)
|
||||
context['today'] = timezone.now().strftime("%d-%m-%Y")
|
||||
context['room'] = self.room_id
|
||||
context['unread_count_lobby'] = get_unread_count(None, self.request.profile)
|
||||
if self.room:
|
||||
users_room = [self.room.user_one, self.room.user_two]
|
||||
users_room.remove(self.request.profile)
|
||||
context['other_user'] = users_room[0]
|
||||
context['other_online'] = get_user_online_status(context['other_user'])
|
||||
context['is_ignored'] = Ignore.is_ignored(self.request.profile, context['other_user'])
|
||||
else:
|
||||
context['online_count'] = get_online_count()
|
||||
context['message_template'] = {
|
||||
'author': self.request.profile,
|
||||
'id': '$id',
|
||||
'time': timezone.now(),
|
||||
'body': '$body'
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
|
@ -73,21 +118,46 @@ def delete_message(request):
|
|||
@login_required
|
||||
def post_message(request):
|
||||
ret = {'msg': 'posted'}
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if request.method == 'GET':
|
||||
return JsonResponse(ret)
|
||||
room = None
|
||||
if request.POST['room']:
|
||||
room = Room.objects.get(id=request.POST['room'])
|
||||
|
||||
if not can_access_room(request, room) or request.profile.mute:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
new_message = Message(author=request.profile,
|
||||
body=request.POST['body'])
|
||||
body=request.POST['body'],
|
||||
room=room)
|
||||
new_message.save()
|
||||
|
||||
event.post('chat', {
|
||||
'type': 'new_message',
|
||||
if not room:
|
||||
event.post('chat_lobby', {
|
||||
'type': 'lobby',
|
||||
'author_id': request.profile.id,
|
||||
'message': new_message.id,
|
||||
'room': 'None',
|
||||
'tmp_id': request.POST.get('tmp_id')
|
||||
})
|
||||
else:
|
||||
for user in room.users():
|
||||
event.post('chat_' + str(user.id), {
|
||||
'type': 'private',
|
||||
'author_id': request.profile.id,
|
||||
'message': new_message.id,
|
||||
'room': room.id,
|
||||
'tmp_id': request.POST.get('tmp_id')
|
||||
})
|
||||
|
||||
return JsonResponse(ret)
|
||||
|
||||
|
||||
def can_access_room(request, room):
|
||||
return not room or room.user_one == request.profile or room.user_two == request.profile
|
||||
|
||||
|
||||
@login_required
|
||||
def chat_message_ajax(request):
|
||||
if request.method != 'GET':
|
||||
|
@ -100,6 +170,9 @@ def chat_message_ajax(request):
|
|||
|
||||
try:
|
||||
message = Message.objects.filter(hidden=False).get(id=message_id)
|
||||
room = message.room
|
||||
if room and not room.contain(request.profile):
|
||||
return HttpResponse('Unauthorized', status=401)
|
||||
except Message.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
return render(request, 'chat/message.html', {
|
||||
|
@ -107,53 +180,156 @@ def chat_message_ajax(request):
|
|||
})
|
||||
|
||||
|
||||
def get_user_online_status():
|
||||
last_five_minutes = timezone.now()-timezone.timedelta(minutes=5)
|
||||
return Profile.objects \
|
||||
.filter(display_rank='user',
|
||||
last_access__gte = last_five_minutes)\
|
||||
.annotate(is_online=Case(default=True,output_field=BooleanField()))\
|
||||
.order_by('-rating')
|
||||
@login_required
|
||||
def update_last_seen(request, **kwargs):
|
||||
if 'room_id' in kwargs:
|
||||
room_id = kwargs['room_id']
|
||||
elif request.method == 'GET':
|
||||
room_id = request.GET.get('room')
|
||||
elif request.method == 'POST':
|
||||
room_id = request.POST.get('room')
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
profile = request.profile
|
||||
room = None
|
||||
if room_id:
|
||||
room = Room.objects.get(id=int(room_id))
|
||||
except Room.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
except Exception as e:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if room and not room.contain(profile):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
|
||||
user_room.last_seen = timezone.now()
|
||||
user_room.save()
|
||||
|
||||
return JsonResponse({'msg': 'updated'})
|
||||
|
||||
|
||||
def get_admin_online_status():
|
||||
all_admin = Profile.objects.filter(display_rank='admin')
|
||||
last_five_minutes = timezone.now()-timezone.timedelta(minutes=5)
|
||||
def get_online_count():
|
||||
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
|
||||
return Profile.objects.filter(last_access__gte=last_two_minutes).count()
|
||||
|
||||
|
||||
def get_user_online_status(user):
|
||||
time_diff = timezone.now() - user.last_access
|
||||
is_online = time_diff <= timezone.timedelta(minutes=2)
|
||||
return is_online
|
||||
|
||||
|
||||
def user_online_status_ajax(request):
|
||||
if request.method != 'GET':
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
user_id = request.GET.get('user')
|
||||
|
||||
if user_id:
|
||||
try:
|
||||
user_id = int(user_id)
|
||||
user = Profile.objects.get(id=user_id)
|
||||
except Exception as e:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
is_online = get_user_online_status(user)
|
||||
return render(request, 'chat/user_online_status.html', {
|
||||
'other_user': user,
|
||||
'other_online': is_online,
|
||||
'is_ignored': Ignore.is_ignored(request.profile, user)
|
||||
})
|
||||
else:
|
||||
return render(request, 'chat/user_online_status.html', {
|
||||
'online_count': get_online_count(),
|
||||
})
|
||||
|
||||
|
||||
def get_online_status(request_user, queryset, rooms=None):
|
||||
if not queryset:
|
||||
return None
|
||||
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
|
||||
ret = []
|
||||
|
||||
for admin in all_admin:
|
||||
is_online = False
|
||||
if (admin.last_access >= last_five_minutes):
|
||||
is_online = True
|
||||
ret.append({'user': admin, 'is_online': is_online})
|
||||
if rooms:
|
||||
unread_count = get_unread_count(rooms, request_user)
|
||||
count = {}
|
||||
for i in unread_count:
|
||||
count[i['other_user']] = i['unread_count']
|
||||
|
||||
for user in queryset:
|
||||
is_online = False
|
||||
if (user.last_access >= last_two_minutes):
|
||||
is_online = True
|
||||
user_dict = {'user': user, 'is_online': is_online}
|
||||
if rooms and user.id in count:
|
||||
user_dict['unread_count'] = count[user.id]
|
||||
user_dict['url'] = encrypt_url(request_user.id, user.id)
|
||||
ret.append(user_dict)
|
||||
return ret
|
||||
|
||||
|
||||
def get_status_context(request):
|
||||
friend_list = request.profile.get_friends()
|
||||
all_user_status = get_user_online_status()
|
||||
friend_status = []
|
||||
user_status = []
|
||||
|
||||
for user in all_user_status:
|
||||
if user.username in friend_list:
|
||||
friend_status.append(user)
|
||||
def get_status_context(request, include_ignored=False):
|
||||
if include_ignored:
|
||||
ignored_users = Profile.objects.none()
|
||||
queryset = Profile.objects
|
||||
else:
|
||||
user_status.append(user)
|
||||
ignored_users = Ignore.get_ignored_users(request.profile)
|
||||
queryset = Profile.objects.exclude(id__in=ignored_users)
|
||||
|
||||
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
|
||||
recent_profile = Room.objects.filter(
|
||||
Q(user_one=request.profile) | Q(user_two=request.profile)
|
||||
).annotate(
|
||||
last_msg_time=Subquery(
|
||||
Message.objects.filter(room=OuterRef('pk')).values('time')[:1]
|
||||
),
|
||||
other_user=Case(
|
||||
When(user_one=request.profile, then='user_two'),
|
||||
default='user_one',
|
||||
)
|
||||
).filter(last_msg_time__isnull=False)\
|
||||
.exclude(other_user__in=ignored_users)\
|
||||
.order_by('-last_msg_time').values('other_user', 'id')[:20]
|
||||
|
||||
recent_profile_id = [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_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.get_friend_profiles(request.profile).exclude(id__in=recent_profile_id)\
|
||||
.exclude(id__in=ignored_users)\
|
||||
.order_by('-last_access')
|
||||
admin_list = queryset.filter(display_rank='admin')\
|
||||
.exclude(id__in=friend_list).exclude(id__in=recent_profile_id)
|
||||
all_user_status = queryset\
|
||||
.filter(display_rank='user',
|
||||
last_access__gte = last_two_minutes)\
|
||||
.annotate(is_online=Case(default=True,output_field=BooleanField()))\
|
||||
.order_by('-rating').exclude(id__in=friend_list).exclude(id__in=admin_list)\
|
||||
.exclude(id__in=recent_profile_id)[:30]
|
||||
|
||||
return [
|
||||
{
|
||||
'title': 'Admins',
|
||||
'user_list': get_admin_online_status(),
|
||||
'title': 'Recent',
|
||||
'user_list': get_online_status(request.profile, recent_list, recent_rooms),
|
||||
},
|
||||
{
|
||||
'title': 'Following',
|
||||
'user_list': friend_status,
|
||||
'user_list': get_online_status(request.profile, friend_list),
|
||||
},
|
||||
{
|
||||
'title': 'Users',
|
||||
'user_list': user_status,
|
||||
'title': 'Admins',
|
||||
'user_list': get_online_status(request.profile, admin_list),
|
||||
},
|
||||
{
|
||||
'title': 'Other',
|
||||
'user_list': get_online_status(request.profile, all_user_status),
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -162,4 +338,80 @@ def get_status_context(request):
|
|||
def online_status_ajax(request):
|
||||
return render(request, 'chat/online_status.html', {
|
||||
'status_sections': get_status_context(request),
|
||||
'unread_count_lobby': get_unread_count(None, request.profile),
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def get_room(user_one, user_two):
|
||||
if user_one.id > user_two.id:
|
||||
user_one, user_two = user_two, user_one
|
||||
room, created = Room.objects.get_or_create(user_one=user_one, user_two=user_two)
|
||||
return room
|
||||
|
||||
|
||||
@login_required
|
||||
def get_or_create_room(request):
|
||||
decrypted_other_id = request.GET.get('other')
|
||||
request_id, other_id = decrypt_url(decrypted_other_id)
|
||||
|
||||
if not other_id or not request_id or request_id != request.profile.id:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
other_user = Profile.objects.get(id=int(other_id))
|
||||
except Exception:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
user = request.profile
|
||||
|
||||
if not other_user or not user:
|
||||
return HttpResponseBadRequest()
|
||||
# TODO: each user can only create <= 300 rooms
|
||||
room = get_room(other_user, user)
|
||||
return JsonResponse({'room': room.id, 'other_user_id': other_user.id})
|
||||
|
||||
|
||||
def get_unread_count(rooms, user):
|
||||
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)\
|
||||
.annotate(unread_count=Count('pk')).values('unread_count')
|
||||
|
||||
return UserRoom.objects\
|
||||
.filter(user=user, room__isnull=True)\
|
||||
.annotate(
|
||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
||||
).values_list('unread_count', flat=True)[0]
|
||||
|
||||
|
||||
@login_required
|
||||
def toggle_ignore(request, **kwargs):
|
||||
user_id = kwargs['user_id']
|
||||
if not user_id:
|
||||
return HttpResponseBadRequest()
|
||||
try:
|
||||
other_user = Profile.objects.get(id=user_id)
|
||||
except:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
Ignore.toggle_ignore(request.profile, other_user)
|
||||
next_url = request.GET.get('next', '/')
|
||||
return HttpResponseRedirect(next_url)
|
17
dmoj/urls.py
17
dmoj/urls.py
|
@ -1,4 +1,4 @@
|
|||
from chat_box.views import ChatView, delete_message, post_message, chat_message_ajax, online_status_ajax
|
||||
from chat_box.views import *
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
|
@ -24,8 +24,8 @@ from judge.views import TitledTemplateView, about, api, blog, comment, contests,
|
|||
from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \
|
||||
problem_data_file, problem_init_view
|
||||
from judge.views.register import ActivationView, RegistrationView
|
||||
from judge.views.select2 import AssigneeSelect2View, CommentSelect2View, ContestSelect2View, \
|
||||
ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \
|
||||
from judge.views.select2 import AssigneeSelect2View, ChatUserSearchSelect2View, CommentSelect2View, \
|
||||
ContestSelect2View, ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \
|
||||
UserSearchSelect2View, UserSelect2View
|
||||
|
||||
admin.autodiscover()
|
||||
|
@ -288,6 +288,7 @@ urlpatterns = [
|
|||
|
||||
url(r'^select2/', include([
|
||||
url(r'^user_search$', UserSearchSelect2View.as_view(), name='user_search_select2_ajax'),
|
||||
url(r'^user_search_chat$', ChatUserSearchSelect2View.as_view(), name='chat_user_search_select2_ajax'),
|
||||
url(r'^contest_users/(?P<contest>\w+)$', ContestUserSearchSelect2View.as_view(),
|
||||
name='contest_user_search_select2_ajax'),
|
||||
url(r'^ticket_user$', TicketUserSelect2View.as_view(), name='ticket_user_select2_ajax'),
|
||||
|
@ -373,13 +374,15 @@ urlpatterns = [
|
|||
url(r'^custom_checker_sample/', about.custom_checker_sample, name='custom_checker_sample'),
|
||||
|
||||
url(r'^chat/', include([
|
||||
url(r'^$',
|
||||
login_required(ChatView.as_view()),
|
||||
name='chat'),
|
||||
url(r'^(?P<room_id>\d*)$', login_required(ChatView.as_view()), name='chat'),
|
||||
url(r'^delete/$', delete_message, name='delete_chat_message'),
|
||||
url(r'^post/$', post_message, name='post_chat_message'),
|
||||
url(r'^ajax$', chat_message_ajax, name='chat_message_ajax'),
|
||||
url(r'^online_status/ajax$', online_status_ajax, name='online_status_ajax')
|
||||
url(r'^online_status/ajax$', online_status_ajax, name='online_status_ajax'),
|
||||
url(r'^get_or_create_room$', get_or_create_room, name='get_or_create_room'),
|
||||
url(r'^update_last_seen$', update_last_seen, name='update_last_seen'),
|
||||
url(r'^online_status/user/ajax$', user_online_status_ajax, name='user_online_status_ajax'),
|
||||
url(r'^toggle_ignore/(?P<user_id>\d+)$', toggle_ignore, name='toggle_ignore'),
|
||||
])),
|
||||
|
||||
url(r'^notifications/',
|
||||
|
|
18
judge/migrations/0116_auto_20211011_0645.py
Normal file
18
judge/migrations/0116_auto_20211011_0645.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.17 on 2021-10-10 23:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0115_auto_20210525_0222'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='contest',
|
||||
name='format_name',
|
||||
field=models.CharField(choices=[('atcoder', 'AtCoder'), ('default', 'Default'), ('ecoo', 'ECOO'), ('icpc', 'ICPC'), ('ioi', 'IOI')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format'),
|
||||
),
|
||||
]
|
|
@ -257,5 +257,13 @@ class Friend(models.Model):
|
|||
else:
|
||||
self.make_friend(current_user, new_friend)
|
||||
|
||||
@classmethod
|
||||
def get_friend_profiles(self, current_user):
|
||||
try:
|
||||
ret = self.objects.get(current_user=current_user).users.all()
|
||||
except Friend.DoesNotExist:
|
||||
ret = []
|
||||
return ret
|
||||
|
||||
def __str__(self):
|
||||
return str(self.current_user)
|
||||
|
|
|
@ -4,6 +4,8 @@ from django.shortcuts import get_object_or_404
|
|||
from django.utils.encoding import smart_text
|
||||
from django.views.generic.list import BaseListView
|
||||
|
||||
from chat_box.utils import encrypt_url
|
||||
|
||||
from judge.jinja2.gravatar import gravatar
|
||||
from judge.models import Comment, Contest, Organization, Problem, Profile
|
||||
|
||||
|
@ -121,3 +123,35 @@ class AssigneeSelect2View(UserSearchSelect2View):
|
|||
def get_queryset(self):
|
||||
return Profile.objects.filter(assigned_tickets__isnull=False,
|
||||
user__username__icontains=self.term).distinct()
|
||||
|
||||
|
||||
class ChatUserSearchSelect2View(BaseListView):
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self): # TODO: add block
|
||||
return _get_user_queryset(self.term)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
self.kwargs = kwargs
|
||||
self.term = kwargs.get('term', request.GET.get('term', ''))
|
||||
self.gravatar_size = request.GET.get('gravatar_size', 128)
|
||||
self.gravatar_default = request.GET.get('gravatar_default', None)
|
||||
|
||||
self.object_list = self.get_queryset().values_list('pk', 'user__username', 'user__email', 'display_rank')
|
||||
|
||||
context = self.get_context_data()
|
||||
|
||||
return JsonResponse({
|
||||
'results': [
|
||||
{
|
||||
'text': username,
|
||||
'id': encrypt_url(request.profile.id, pk),
|
||||
'gravatar_url': gravatar(email, self.gravatar_size, self.gravatar_default),
|
||||
'display_rank': display_rank,
|
||||
} for pk, username, email, display_rank in context['object_list']],
|
||||
'more': context['page_obj'].has_next(),
|
||||
})
|
||||
|
||||
def get_name(self, obj):
|
||||
return str(obj)
|
|
@ -55,8 +55,7 @@
|
|||
overflow-y: scroll;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
max-height: 85%;
|
||||
height: auto;
|
||||
height: 75%;
|
||||
}
|
||||
|
||||
#chat-input {
|
||||
|
@ -90,6 +89,88 @@
|
|||
display: block !important;
|
||||
}
|
||||
}
|
||||
#chat-input, #chat-log .content-message {
|
||||
font-family: "Segoe UI", "Lucida Grande", Arial, sans-serif;
|
||||
}
|
||||
.info-pic {
|
||||
height: 90%;
|
||||
border-radius: 50%;
|
||||
padding: 0.05em;
|
||||
border: 0.1px solid #ccc;
|
||||
margin-left: 3em;
|
||||
margin-bottom: 1.5px;
|
||||
}
|
||||
.info-circle {
|
||||
position: absolute;
|
||||
cx: 86%;
|
||||
cy: 80%;
|
||||
r: 6px;
|
||||
stroke: white;
|
||||
stroke-width: 1;
|
||||
}
|
||||
.info-name {
|
||||
margin-left: 10px;
|
||||
font-size: 2em;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.info-name a {
|
||||
display: table-caption;
|
||||
}
|
||||
#chat-info {
|
||||
border-bottom: 2px solid darkgray;
|
||||
display: flex;
|
||||
}
|
||||
#refresh-button {
|
||||
padding: 0;
|
||||
margin-left: auto;
|
||||
margin-right: 0.3em;
|
||||
background: transparent;
|
||||
border: none;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
}
|
||||
#refresh-button:hover {
|
||||
background: lightgreen;
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
transition: 1.5s ease-in-out;
|
||||
}
|
||||
.status-pic {
|
||||
height: 1.3em;
|
||||
width: 1.3em;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
.status-container {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
.status-circle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cx: 18px;
|
||||
cy: 18px;
|
||||
r: 4.5px;
|
||||
stroke: white;
|
||||
stroke-width: 1;
|
||||
}
|
||||
.status-row {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
padding: 0.2em 0.2em 0.2em 1em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.status-row:hover {
|
||||
background: lightgray;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-list {
|
||||
padding: 0;
|
||||
}
|
||||
.status-section-title {
|
||||
cursor: pointer;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
@media (max-width: 799px) {
|
||||
#chat-area {
|
||||
height: 500px;
|
||||
|
|
|
@ -523,3 +523,13 @@ details {
|
|||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
background: lightgray;
|
||||
color: black !important;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: gray;
|
||||
}
|
|
@ -8,24 +8,53 @@
|
|||
<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="text/javascript">
|
||||
let message_template = `
|
||||
{% with message=message_template %}
|
||||
{% include "chat/message.html" %}
|
||||
{% endwith %}
|
||||
`;
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
window.currentPage = 1;
|
||||
window.limit_time = 0;
|
||||
window.limit_time = 24;
|
||||
window.messages_per_page = 50;
|
||||
window.room_id = "{{room if room else ''}}";
|
||||
window.unread_message = 0;
|
||||
window.other_user_id = "{{other_user.id if other_user else ''}}";
|
||||
window.num_pages = {{paginator.num_pages}};
|
||||
window.lock = false;
|
||||
|
||||
function load_page(page) {
|
||||
$.get('?page=' + page)
|
||||
function load_page(page, refresh_html=false) {
|
||||
var param = {
|
||||
'page': page,
|
||||
}
|
||||
$.get("{{ url('chat', '') }}" + window.room_id, param)
|
||||
.fail(function() {
|
||||
console.log("Fail to load page " + page);
|
||||
})
|
||||
.done(function(data) {
|
||||
if (refresh_html) {
|
||||
$('#chat-log').html('');
|
||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||
window.lock = true;
|
||||
}
|
||||
|
||||
window.num_pages = parseInt($('<div>' + data + '</div>').find('#num_pages').html());
|
||||
var time = refresh_html ? 0 : 500;
|
||||
|
||||
setTimeout(function() {
|
||||
let container = $('#chat-box');
|
||||
let lastMsgPos = scrollTopOfBottom(container)
|
||||
let $chat_box = $('#chat-box');
|
||||
let lastMsgPos = scrollTopOfBottom($chat_box)
|
||||
|
||||
$('#loader').hide();
|
||||
|
||||
if (refresh_html) {
|
||||
$('#chat-log').append(data);
|
||||
}
|
||||
else {
|
||||
$('#chat-log').prepend(data);
|
||||
}
|
||||
|
||||
$('.body-block').slice(0, window.messages_per_page).each(function() {
|
||||
resize_emoji($(this));
|
||||
|
@ -34,8 +63,14 @@
|
|||
register_time($('.time-with-rel'));
|
||||
merge_authors();
|
||||
|
||||
container.scrollTop(scrollTopOfBottom(container) - lastMsgPos);
|
||||
}, 500);
|
||||
if (!refresh_html) {
|
||||
$chat_box.scrollTop(scrollTopOfBottom($chat_box) - lastMsgPos);
|
||||
}
|
||||
else {
|
||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||
}
|
||||
window.lock = false;
|
||||
}, time);
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -46,7 +81,7 @@
|
|||
function scrollContainer(container, loader) {
|
||||
container.scroll(function() {
|
||||
if (container.scrollTop() == 0) {
|
||||
if (currentPage < {{paginator.num_pages}}) {
|
||||
if (currentPage < window.num_pages && !window.lock) {
|
||||
currentPage++;
|
||||
loader.show();
|
||||
load_page(currentPage);
|
||||
|
@ -57,42 +92,122 @@
|
|||
window.load_dynamic_update = function (last_msg) {
|
||||
return new EventReceiver(
|
||||
"{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
|
||||
['chat'], last_msg, function (message) {
|
||||
switch (message.type) {
|
||||
case 'new_message':
|
||||
add_new_message(message.message);
|
||||
break;
|
||||
['chat_lobby', 'chat_{{request.profile.id}}'], last_msg, function (message) {
|
||||
var room = (message.type == 'lobby') ? '' : message.room;
|
||||
if (message.author_id == {{request.profile.id}}) {
|
||||
check_new_message(message.message, message.tmp_id, room);
|
||||
}
|
||||
else {
|
||||
add_new_message(message.message, room);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function add_new_message(message) {
|
||||
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);
|
||||
resize_emoji($data.find('.body-block'));
|
||||
|
||||
$('#chat-log').append($data);
|
||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||
register_time($('.time-with-rel'));
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
|
||||
merge_authors();
|
||||
}
|
||||
|
||||
function add_new_message(message, room) {
|
||||
function callback(update) {
|
||||
if (!document['hidden']) {
|
||||
if (update) update_last_seen();
|
||||
refresh_status();
|
||||
}
|
||||
else {
|
||||
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) {
|
||||
var $data = $(data);
|
||||
resize_emoji($data.find('.body-block'));
|
||||
|
||||
$('#chat-log').append($data);
|
||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
|
||||
register_time($('.time-with-rel'));
|
||||
merge_authors();
|
||||
add_message(data);
|
||||
callback(true);
|
||||
},
|
||||
error: function (data) {
|
||||
if (data.status === 403)
|
||||
console.log('No right to see: ' + message);
|
||||
else {
|
||||
console.log('Could not load chat message:');
|
||||
console.log(data.responseText);
|
||||
}
|
||||
console.log('Could not add new message');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
|
||||
function check_new_message(message, tmp_id, room) {
|
||||
if (room == "{{room}}") {
|
||||
$.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 {
|
||||
$('#body-block-'+tmp_id).replaceWith($body_block);
|
||||
}
|
||||
resize_emoji($body_block);
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
|
||||
register_time($('.time-with-rel'));
|
||||
remove_unread_current_user();
|
||||
},
|
||||
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
|
||||
|
@ -119,25 +234,40 @@
|
|||
});
|
||||
}
|
||||
|
||||
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, false);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -165,6 +295,77 @@
|
|||
}
|
||||
}
|
||||
|
||||
function register_click_space() {
|
||||
function callback() {
|
||||
history.replaceState(null, '', "{{url('chat', '')}}" + window.room_id);
|
||||
load_page(window.currentPage, true, refresh_status);
|
||||
update_last_seen();
|
||||
refresh_status();
|
||||
$('#chat-input').focus();
|
||||
}
|
||||
$('.click_space').on('click', function(e) {
|
||||
if ($(this).attr('id') == 'click_space_' + window.other_user_id) {
|
||||
return;
|
||||
}
|
||||
var other_user = $(this).attr('value');
|
||||
$.get("{{url('get_or_create_room')}}" + `?other=${other_user}`)
|
||||
.done(function(data) {
|
||||
window.currentPage = 1;
|
||||
window.room_id = data.room;
|
||||
window.other_user_id = data.other_user_id;
|
||||
callback();
|
||||
})
|
||||
.fail(function() {
|
||||
console.log('Fail to get_or_create_room');
|
||||
})
|
||||
});
|
||||
$('#lobby_row').on('click', function(e) {
|
||||
if (window.room_id) {
|
||||
window.currentPage = 1;
|
||||
window.room_id = '';
|
||||
window.other_user_id = '';
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
merge_authors();
|
||||
|
@ -191,7 +392,7 @@
|
|||
}
|
||||
},
|
||||
fail: function(data) {
|
||||
alert('Fail to delete');
|
||||
console.log('Fail to delete');
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -201,6 +402,7 @@
|
|||
resize_emoji($(this));
|
||||
});
|
||||
|
||||
$("#chat-log").show();
|
||||
$("#chat-log").change(function() {
|
||||
$('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
|
||||
});
|
||||
|
@ -237,26 +439,9 @@
|
|||
$('.chat-right-panel').show();
|
||||
});
|
||||
|
||||
$('#refresh-button').on('click', function() {
|
||||
$.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-content').html(data).find('.toggle').each(function () {
|
||||
register_toggle($(this));
|
||||
});;
|
||||
}
|
||||
})
|
||||
})
|
||||
$('#refresh-button').on('click', refresh_status)
|
||||
|
||||
setInterval(function() {
|
||||
$('#refresh-button').click();
|
||||
}, 5 * 60 * 1000);
|
||||
setInterval(refresh_status, 2 * 60 * 1000);
|
||||
|
||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||
load_dynamic_update({{last_msg}});
|
||||
|
@ -279,11 +464,67 @@
|
|||
$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;
|
||||
});
|
||||
|
||||
// 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>
|
||||
|
||||
|
@ -314,13 +555,27 @@
|
|||
</button>
|
||||
</h3>
|
||||
<div id="chat-online-content">
|
||||
<div id="search-container">
|
||||
<center>
|
||||
<form id="search-form" name="form" action="{{ url('get_or_create_room') }}" method="get">
|
||||
<input id="search-handle" type="text" name="search"
|
||||
placeholder="{{ _('Search by handle...') }}">
|
||||
</form>
|
||||
</center>
|
||||
</div>
|
||||
<div id="chat-online-list">
|
||||
{% include "chat/online_status.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-area" class="chat-left-panel" style="width:100%">
|
||||
<div id="chat-info" style="height: 8%">
|
||||
{% include 'chat/user_online_status.html' %}
|
||||
</div>
|
||||
<div id="chat-box">
|
||||
<img src="http://opengraphicdesign.com/wp-content/uploads/2009/01/loader64.gif" id="loader">
|
||||
<ul id="chat-log">
|
||||
|
||||
<img src="{{static('loading.gif')}}" id="loader">
|
||||
<ul id="chat-log" style="display: none">
|
||||
{% include 'chat/message_list.html' %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -2,53 +2,7 @@
|
|||
#content {
|
||||
margin: -1em 1em 0 0;
|
||||
}
|
||||
#refresh-button {
|
||||
padding: 0;
|
||||
margin-left: auto;
|
||||
margin-right: 0.3em;
|
||||
background: transparent;
|
||||
border: none;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
}
|
||||
#refresh-button:hover {
|
||||
background: lightgreen;
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
transition: 1.5s ease-in-out;
|
||||
}
|
||||
.status-pic {
|
||||
height: 1.3em;
|
||||
width: 1.3em;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
.status-container {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
.status-circle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cx: 18px;
|
||||
cy: 18px;
|
||||
r: 4.5px;
|
||||
stroke: white;
|
||||
stroke-width: 1;
|
||||
}
|
||||
.status-row {
|
||||
display: flex;
|
||||
margin-bottom: 0.5em;
|
||||
font-size: 15px;
|
||||
}
|
||||
.status-list {
|
||||
padding: 0;
|
||||
padding-left: 1em;
|
||||
}
|
||||
.status-section-title {
|
||||
cursor: pointer;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
}
|
||||
|
@ -143,14 +97,55 @@
|
|||
.body-block:hover {
|
||||
background: #eee;
|
||||
}
|
||||
#chat-input, #chat-log .content-message {
|
||||
font-family: "Apple Color Emoji", "Segoe UI", "Lucida Grande", Arial, sans-serif;
|
||||
.active-span {
|
||||
margin-top: 1em;
|
||||
margin-right: 1em;
|
||||
color: #636363;
|
||||
}
|
||||
@media (min-width: 800px) {
|
||||
|
||||
.unread-count {
|
||||
float: right;
|
||||
color: white;
|
||||
background-color: darkcyan;
|
||||
border-radius: 2px;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
#search-form {
|
||||
float: inherit;
|
||||
}
|
||||
#search-container {
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
#setting {
|
||||
position: relative;
|
||||
}
|
||||
#setting-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f1f1f1;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
}
|
||||
#setting-content li {
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
#setting-content li:hover {
|
||||
background-color: #ddd;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#page-container {
|
||||
position:fixed;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
}
|
||||
@media (max-width: 799px) {
|
||||
html, body {
|
||||
|
@ -160,5 +155,11 @@
|
|||
#mobile ul {
|
||||
width: 100%;
|
||||
}
|
||||
.info-pic {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.active-span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div class="user-time">
|
||||
<span class="username {{ message.author.css_class }}">
|
||||
<a href="{{ url('user_page', message.author.user.username) }}">
|
||||
{{message.author}}
|
||||
{{ message.author }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="time">
|
||||
|
@ -14,7 +14,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<span class="content-message">
|
||||
<div class="body-block" title="{{ message.time|date('g:i a') }}">
|
||||
<div class="body-block" id="body-block-{{ message.id }}" title="{{ message.time|date('g:i a') }}">
|
||||
{% if request.user.is_staff %}
|
||||
<a class="chatbtn_remove_mess" value="{{message.id}}" style="color:red; cursor: pointer;">
|
||||
{{_('Delete')}}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
{% if object_list %}
|
||||
<div style="display: none" id="num_pages">{{num_pages}}</div>
|
||||
{% for message in object_list | reverse%}
|
||||
{% include "chat/message.html" %}
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
<center id="empty_msg">{{_('You are connect now. Say something to start the conversation.')}}</center>
|
||||
{% endif %}
|
||||
{% if REQUIRE_JAX %}
|
||||
{% include "mathjax-load.html" %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
<li class="status-row" id="lobby_row">
|
||||
<div class="status-container">
|
||||
<img src="{{ static('icons/logo.png') }}" style="height:1.3em">
|
||||
</div>
|
||||
<span style="padding-left:0.5em">
|
||||
<b>{{_('Lobby')}}</b>
|
||||
</span>
|
||||
<span class="spacer">
|
||||
<span class="unread-count" id="unread-count-lobby">{{unread_count_lobby if unread_count_lobby}}</span>
|
||||
</span>
|
||||
</li>
|
||||
{% for section in status_sections %}
|
||||
{% if section.user_list %}
|
||||
<div class="status-section-title toggle open">
|
||||
|
@ -8,7 +19,7 @@
|
|||
</div>
|
||||
<ul class="status-list toggled">
|
||||
{% for user in section.user_list %}
|
||||
<li style="padding-left: 0.1em" class="status-row">
|
||||
<li class="status-row">
|
||||
<div class="status-container">
|
||||
<img src="{{ gravatar(user.user, 135) }}" class="status-pic">
|
||||
<svg style="position:absolute;" height="32" width="32">
|
||||
|
@ -19,6 +30,9 @@
|
|||
<span style="padding-left:0.3em" class="username {{ user.user.css_class }}">
|
||||
{{ link_user(user.user) }}
|
||||
</span>
|
||||
<span class="click_space spacer" value="{{user.url}}" id="click_space_{{user.user.id}}">
|
||||
<span class="unread-count" id="unread-count-{{user.user.id}}">{{user.unread_count if user.unread_count}}</span>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
35
templates/chat/user_online_status.html
Normal file
35
templates/chat/user_online_status.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% if other_user %}
|
||||
<div class="status-container" style="height: 100%">
|
||||
<img src="{{ gravatar(other_user.user, 135) }}" class="info-pic">
|
||||
<svg style="position:absolute; height:100%; width: 110%">
|
||||
<circle class="info-circle"
|
||||
fill="{{'green' if other_online else 'red'}}"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="info-name username">
|
||||
{% if other_user %}
|
||||
<a href="{{url('user_page', other_user)}}">{{other_user.user.username}}</a>
|
||||
{% else%}
|
||||
<a href="#" style="margin-left: 3em">{{ _('Lobby') }}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
{% if other_user and not other_online %}
|
||||
<span class="active-span">{{ relative_time(other_user.last_access, abs=_('Last online on {time}'), rel=_('Online {time}'), format=_('g:i a d/m/Y')) }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if other_user %}
|
||||
<span style="margin-right: 0.3em" id="setting">
|
||||
<button class="control-button" style="height:100%;" id="setting-button">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div id="setting-content">
|
||||
<li>
|
||||
<a href="{{url('toggle_ignore', other_user.id)}}" style="color: {{'green' if is_ignored else 'red'}}">{{_('Unignore') if is_ignored else _('Ignore')}}</a>
|
||||
</li>
|
||||
</div>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="active-span">{{online_count}} {{_('users are online')}}</span>
|
||||
{% endif %}
|
|
@ -2,15 +2,6 @@
|
|||
|
||||
{% block media %}
|
||||
<style>
|
||||
#control-button {
|
||||
margin-left: 0.8em;
|
||||
background: lightgray;
|
||||
color: black !important;
|
||||
border: 0;
|
||||
}
|
||||
#control-button:hover {
|
||||
background: gray;
|
||||
}
|
||||
{% if request.user.is_authenticated and is_member %}
|
||||
#control-panel {
|
||||
display: none;
|
||||
|
@ -63,7 +54,7 @@
|
|||
$('.blog-content').hide();
|
||||
$('.blog-sidebar').show();
|
||||
});
|
||||
$('#control-button').click(function(e) {
|
||||
$('.control-button').click(function(e) {
|
||||
e.preventDefault();
|
||||
$('#control-panel').toggle("fast");
|
||||
})
|
||||
|
@ -92,7 +83,7 @@
|
|||
class="unselectable button">{{ _('Request membership') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button id="control-button"><i class="fa fa-ellipsis-h"></i></button>
|
||||
<button class="control-button"><i class="fa fa-ellipsis-h"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue