From cdb4dab47ac7fb50281442c013e03fefca1b0262 Mon Sep 17 00:00:00 2001 From: Xamora Date: Mon, 15 Jan 2024 16:29:18 +0100 Subject: [PATCH] Start invitation; notification; add profile avatar in me --- chat/{consumers.py => consumersChat.py} | 13 +- chat/consumersNotice.py | 177 ++++++++++++++++++++++++ chat/routing.py | 8 +- frontend/static/css/index.css | 23 ++- frontend/static/js/api/chat/notice.js | 44 ++++-- frontend/static/js/index.js | 4 - frontend/static/js/utils/noticeUtils.js | 28 +++- frontend/static/js/views/Search.js | 36 +++-- frontend/templates/index.html | 2 +- 9 files changed, 300 insertions(+), 35 deletions(-) rename chat/{consumers.py => consumersChat.py} (94%) create mode 100644 chat/consumersNotice.py diff --git a/chat/consumers.py b/chat/consumersChat.py similarity index 94% rename from chat/consumers.py rename to chat/consumersChat.py index 43ca834..2de3c68 100644 --- a/chat/consumers.py +++ b/chat/consumersChat.py @@ -118,6 +118,10 @@ class ChatNoticeConsumer(WebsocketConsumer): self.channel_layer.users_channels = {} self.channel_layer.users_channels[user.pk] = self.channel_name + if (not hasattr(self.channel_layer, "invite")): + self.channel_layer.invite = {} + self.channel_layer.invite[user.pk] = []; + async_to_sync(self.channel_layer.group_add)( self.room_group_name, self.channel_name @@ -184,9 +188,9 @@ class ChatNoticeConsumer(WebsocketConsumer): #print("receive" + str(user.pk)) - if targets == "all": targets = list(self.channel_layer.users_channels.keys()) + for target in targets: channel = self.channel_layer.users_channels.get(target) if (channel == None or target == user.pk): @@ -198,6 +202,7 @@ class ChatNoticeConsumer(WebsocketConsumer): 'author_id':user.pk, 'content':content, 'time':message_time, + 'status': 200, }) async_to_sync(self.channel_layer.send)(self.channel_layer.users_channels.get(user.pk), { 'type':type_notice, @@ -215,11 +220,16 @@ class ChatNoticeConsumer(WebsocketConsumer): if (user.is_anonymous or not user.is_authenticated): return + if (self.channel_layer.invite[event["author_id"]].get(user.pk)): + return + self.channel_layer.invite[event["author_id"]].append(user.pl) + self.send(text_data=json.dumps({ 'type':event['type'], 'author_id':event['author_id'], 'content':event['content'], 'time': event['time'], + 'status':event['status'], })) def online_users(self, event): @@ -237,4 +247,5 @@ class ChatNoticeConsumer(WebsocketConsumer): 'author_id':event['author_id'], 'content':event['content'], 'time': event['time'], + 'status':event['status'], })) diff --git a/chat/consumersNotice.py b/chat/consumersNotice.py new file mode 100644 index 0000000..37267a2 --- /dev/null +++ b/chat/consumersNotice.py @@ -0,0 +1,177 @@ +from channels.generic.websocket import WebsocketConsumer +from asgiref.sync import async_to_sync + +import time +import json + +class ChatNoticeConsumer(WebsocketConsumer): + + def connect(self): + + user = self.scope["user"] + #if (user.is_anonymous or not user.is_authenticated): + #return + + if (self.channel_layer == None): + return + + self.room_group_name = f'chatNotice{user.pk}' + if (not hasattr(self.channel_layer, "users_channels")): + self.channel_layer.users_channels = {} + self.channel_layer.users_channels[user.pk] = self.channel_name + + if (not hasattr(self.channel_layer, "invite")): + self.channel_layer.invite = {} + self.channel_layer.invite[user.pk] = []; + + async_to_sync(self.channel_layer.group_add)( + self.room_group_name, + self.channel_name + ) + + self.accept() + + message_time: int = int(time.time() * 1000) + targets = list(self.channel_layer.users_channels.keys()) + for target in targets: + channel = self.channel_layer.users_channels.get(target) + if (channel == None or target == user.pk): + continue + async_to_sync(self.channel_layer.send)(channel, { + 'type':"online_users", + 'author_id':user.pk, + 'targets': targets, + 'time':message_time, + 'status': 200, + }) + + def disconnect(self, code): + + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return + + self.channel_layer.users_channels.pop(user.pk) + + message_time: int = int(time.time() * 1000) + + targets = list(self.channel_layer.users_channels.keys()) + for target in targets: + channel = self.channel_layer.users_channels.get(target) + if (channel == None or target == user.pk): + continue + async_to_sync(self.channel_layer.send)(channel, { + 'type':"online_users", + 'author_id':user.pk, + 'targets': targets, + 'time':message_time, + 'status': 200, + }) + + def receive(self, text_data=None, bytes_data=None): + + if text_data == None: + return + + user = self.scope["user"] + #if (user.is_anonymous or not user.is_authenticated): + #return + + text_data_json = json.loads(text_data) + + type_notice = text_data_json.get('type') + targets : list = text_data_json.get('targets') + content : dict = text_data_json.get('content') + + if (type_notice == None or targets == None): + return + + if (self.channel_layer == None): + return + + message_time: int = int(time.time() * 1000) + status = 200; + + #print("receive" + str(user.pk)) + + try: + status = getattr(self, "pre_" + type_notice)(user, targets) + except AttributeError: + print(f"La fonction pre_{type_notice} n'existe pas.") + + if targets == "all": + targets = list(self.channel_layer.users_channels.keys()) + + for target in targets: + channel = self.channel_layer.users_channels.get(target) + if (channel == None or target == user.pk): + if (channel == None): + status = 404 + continue + async_to_sync(self.channel_layer.send)(channel, { + 'type':type_notice, + 'author_id':user.pk, + 'content':content, + 'targets': targets, + 'time':message_time, + 'status': 200, + }) + async_to_sync(self.channel_layer.send)(self.channel_layer.users_channels.get(user.pk), { + 'type':type_notice, + 'author_id':user.pk, + 'content':"notice return", + 'targets': targets, + 'time':message_time, + 'status':status, + }) + + + def pre_invite(self, user, targets): + status = 200 + + for target in targets: + if (target in self.channel_layer.invite[user.pk]): + status = 409 + return status + + def invite(self, event): + + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return + + if (user.pk in self.channel_layer.invite[event["author_id"]]): + return + + if (user.pk != event["author_id"]): + self.channel_layer.invite[event["author_id"]].append(user.pk) + + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'content':event['content'], + 'targets': event['targets'], + 'time': event['time'], + 'status':event['status'], + })) + + def pre_online_users(self, user, targets): + pass + + def online_users(self, event): + + user = self.scope["user"] + #if (user.is_anonymous or not user.is_authenticated): + #return + + #print("online_users" + str(user.pk)) + + event['content'] = self.channel_layer.users_channels + + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'content':event['content'], + 'time': event['time'], + 'status':event['status'], + })) diff --git a/chat/routing.py b/chat/routing.py index 26f3fc7..6b8fc6f 100644 --- a/chat/routing.py +++ b/chat/routing.py @@ -1,7 +1,9 @@ from django.urls import re_path -from . import consumers + +from . import consumersChat +from . import consumersNotice websocket_urlpatterns = [ - re_path(r'ws/chat/(?P\d+)$', consumers.ChatConsumer.as_asgi()), - re_path(r'ws/chat/notice$', consumers.ChatNoticeConsumer.as_asgi()), + re_path(r'ws/chat/(?P\d+)$', consumersChat.ChatConsumer.as_asgi()), + re_path(r'ws/chat/notice$', consumersNotice.ChatNoticeConsumer.as_asgi()), ] diff --git a/frontend/static/css/index.css b/frontend/static/css/index.css index 12a9efb..7e35fb0 100644 --- a/frontend/static/css/index.css +++ b/frontend/static/css/index.css @@ -7,11 +7,30 @@ body { } -#app #avatar -{ +#app #avatar { max-height: 10em; max-width: 10em; min-height: 6em; min-width: 6em; } + +#popup { + position: fixed; + font-size: 1.2em; + z-index: 1; /* foreground */ + + top:calc(1% + 0.1em); + left:50%; + transform: translate(-50%, 50%); + + border: 1em solid #1a1a1a; + color: #1a1a1a; + background-color: #cccccc; + + padding: 5px; + border-width: 0.1em; + + opacity: 0; + transition: opacity 0.25s; +} diff --git a/frontend/static/js/api/chat/notice.js b/frontend/static/js/api/chat/notice.js index be92ab1..1ae4d23 100644 --- a/frontend/static/js/api/chat/notice.js +++ b/frontend/static/js/api/chat/notice.js @@ -1,7 +1,16 @@ +import {create_popup} from "../../utils/noticeUtils.js"; + class Notice { constructor(client) { this.client = client; - this.online_users = {}; + this.data = {}; + + // users online, invited by, asked friend by, + let data_variable = ["online", "invited", "asked"]; + for (let i in data_variable) { + this.data[data_variable[i]] = []; + } + this.connect(); } @@ -19,7 +28,6 @@ class Notice { this.receiveOnlineUser(data); } this.chatSocket.onopen = (event) => { - this.online_users = {}; this.getOnlineUser(); } } @@ -47,14 +55,22 @@ class Notice { async receiveInvite(data) { if (data.content === "notice return") { - if (data.status == 200) - return - // Notification pour dire que la notif a été bien envoyé + if (data.status == 200) { + for (let target in data.targets) + this.data["invited"].push(target); + return create_popup("Invitation send"); + } else if (data.status == 404) - return - // Pas connecté + return create_popup("User not connected"); + else if (data.status == 409) + return create_popup("Already invited"); } else { + let sender = await this.client.profiles.getProfile(data.author_id); + + this.inviter.push(data.author_id); + create_popup("Invitation received by " + sender.username); + // Géré la reception de l'invitation } } @@ -75,7 +91,19 @@ class Notice { async receiveOnlineUser(data) { if (data.content !== undefined) { - this.online_users = data.content; + + if (this.online_users.length > 0) { + // get all disconnect user + let disconnects = this.online_users.filter(id => !Object.keys(data.content).includes(id)); + + // delete invite + this.data["invited"] = this.data["invited"].filter(id => !disconnects.includes(id)); + + //console.log(this.data["invited"]); + } + + this.data["online"] = Object.keys(data.content); + if (this.rewrite_usernames !== undefined) this.rewrite_usernames(); } diff --git a/frontend/static/js/index.js b/frontend/static/js/index.js index 5b5247e..385b838 100644 --- a/frontend/static/js/index.js +++ b/frontend/static/js/index.js @@ -19,8 +19,6 @@ import TournamentPageView from "./views/tournament/TournamentPageView.js"; import TournamentsView from "./views/tournament/TournamentsListView.js"; import TournamentCreateView from "./views/tournament/TournamentCreateView.js"; -import {manage_popup} from "./utils/noticeUtils.js"; - let client = new Client(location.protocol + "//" + location.host) let lastView = undefined @@ -118,8 +116,6 @@ const router = async(uri) => { if (await renderView(view)) return 1; - manage_popup(client) - return 0; }; diff --git a/frontend/static/js/utils/noticeUtils.js b/frontend/static/js/utils/noticeUtils.js index 65ad8cf..f5b77bc 100644 --- a/frontend/static/js/utils/noticeUtils.js +++ b/frontend/static/js/utils/noticeUtils.js @@ -1,4 +1,28 @@ -function manage_popup(client) { +// timer in milliseconds +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); } -export {manage_popup} +async function create_popup(text = undefined, timer = undefined) { + + if (text == undefined) + text = "notice undefined"; + + let popup = document.getElementById("popup"); + popup.textContent = "Notice: " + text; + popup.style.opacity = 0.95; + popup.onclick = async () => { + popup.style.opacity = 0; + return ; + } + + if (timer == undefined) + timer = 5000; + await sleep(timer); + + popup.style.opacity = 0; + //popup.style.visibility = "hidden" + +} + +export {create_popup} diff --git a/frontend/static/js/views/Search.js b/frontend/static/js/views/Search.js index 11ba26e..5f84845 100644 --- a/frontend/static/js/views/Search.js +++ b/frontend/static/js/views/Search.js @@ -10,7 +10,8 @@ export default class extends AbstractView { async wait_get_online_users() { return new Promise((resolve) => { const checkInterval = setInterval(() => { - if (Object.keys(client.notice.online_users).length > 0) { + //console.log(client.notice.data["online"].length); + if (client.notice.data["online"].length > 0) { clearInterval(checkInterval); resolve(); } @@ -23,10 +24,11 @@ export default class extends AbstractView { let logged = await client.isAuthentificate(); let profiles = await client.profiles.all(); - if (client.notice == undefined || client.notice.online_users == undefined) + //console.log(client.notice.data); + if (client.notice.data == undefined || client.notice.data["online"] == undefined) return console.log("Error"); - await client.notice.getOnlineUser(); + //await client.notice.getOnlineUser(); await this.wait_get_online_users(); client.notice.rewrite_usernames = this.rewrite_usernames; @@ -64,7 +66,7 @@ export default class extends AbstractView { username.setAttribute('data-link', ''); username.id = `username${user.id}` username.href = `/profiles/${user.id}`; - username.style.color = client.notice.online_users[user.id] !== undefined ? "green" : "red"; + username.style.color = client.notice.data["online"].includes(user.id.toString()) ? "green" : "red"; username.appendChild(document.createTextNode(user.username)); new_user.appendChild(username); @@ -134,7 +136,7 @@ export default class extends AbstractView { profiles.filter(user => user.username.toLowerCase().startsWith(search) == true).forEach((user) => { let username = document.getElementById(`username${user.id}`); if (username !== null) - username.style.color = client.notice.online_users[user.id] !== undefined ? "green" : "red"; + username.style.color = client.notice.data["online"].includes(user.id.toString()) ? "green" : "red"; }); } @@ -194,15 +196,7 @@ export default class extends AbstractView { // Scroll to the bottom of messages messages.scrollTop = messages.scrollHeight; - // Button to send invite to play - let invite = document.getElementById("invite") || document.createElement("button"); - invite.id = "invite"; - invite.innerText = "invite"; - invite.onclick = async () => { - await client.notice.sendInvite(client.me.id, - client.channels.channel.members_id.filter(id => id !== client.me.id)); - }; - chat.appendChild(invite); + this.display_invite(chat); } @@ -264,6 +258,20 @@ export default class extends AbstractView { return members } + async display_invite(chat, profiles) { + + // Button to send invite to play + let invite = document.getElementById("invite") || document.createElement("button"); + invite.id = "invite"; + invite.innerText = "invite"; + invite.onclick = async () => { + await client.notice.sendInvite(client.me.id, + client.channels.channel.members_id.filter(id => id !== client.me.id)); + }; + chat.appendChild(invite); + + } + async hide_chat() { let closes = ["chat", "invite"] diff --git a/frontend/templates/index.html b/frontend/templates/index.html index e7c319a..534ce9a 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -24,7 +24,7 @@
- Test +