mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-05-17 16:52:48 +00:00
Compare commits
No commits in common. "f817c71043d76130670626dc3be07b2dc9be8cda" and "b820306e2e66fa897a3ec7d0318f5a348397b5a1" have entirely different histories.
f817c71043
...
b820306e2e
@ -19,4 +19,4 @@ class MessageAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('channel', 'author', 'created_at', 'updated_at', 'content',)
|
list_display = ('channel', 'author', 'created_at', 'updated_at', 'content',)
|
||||||
list_filter = ('channel', 'created_at', 'updated_at',)
|
list_filter = ('channel', 'created_at', 'updated_at',)
|
||||||
search_fields = ('author__username', 'author__first_name', 'author__last_name', 'content',)
|
search_fields = ('author__username', 'author__first_name', 'author__last_name', 'content',)
|
||||||
autocomplete_fields = ('channel', 'author', 'users_read',)
|
autocomplete_fields = ('channel', 'author',)
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Count, Exists, OuterRef, Q
|
|
||||||
from registration.models import Registration
|
from registration.models import Registration
|
||||||
|
|
||||||
from .models import Channel, Message
|
from .models import Channel, Message
|
||||||
@ -35,10 +34,8 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Accept the connection
|
# Accept the connection
|
||||||
await self.accept()
|
await self.accept()
|
||||||
|
|
||||||
self.read_channels = await Channel.get_accessible_channels(user, 'read')
|
channels = await Channel.get_accessible_channels(user, 'read')
|
||||||
self.write_channels = await Channel.get_accessible_channels(user, 'write')
|
async for channel in channels.all():
|
||||||
|
|
||||||
async for channel in self.read_channels.all():
|
|
||||||
await self.channel_layer.group_add(f"chat-{channel.id}", self.channel_name)
|
await self.channel_layer.group_add(f"chat-{channel.id}", self.channel_name)
|
||||||
await self.channel_layer.group_add(f"user-{user.id}", self.channel_name)
|
await self.channel_layer.group_add(f"user-{user.id}", self.channel_name)
|
||||||
|
|
||||||
@ -51,7 +48,8 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# User is not authenticated
|
# User is not authenticated
|
||||||
return
|
return
|
||||||
|
|
||||||
async for channel in self.read_channels.all():
|
channels = await Channel.get_accessible_channels(self.scope['user'], 'read')
|
||||||
|
async for channel in channels.all():
|
||||||
await self.channel_layer.group_discard(f"chat-{channel.id}", self.channel_name)
|
await self.channel_layer.group_discard(f"chat-{channel.id}", self.channel_name)
|
||||||
await self.channel_layer.group_discard(f"user-{self.scope['user'].id}", self.channel_name)
|
await self.channel_layer.group_discard(f"user-{self.scope['user'].id}", self.channel_name)
|
||||||
|
|
||||||
@ -71,8 +69,6 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
await self.delete_message(**content)
|
await self.delete_message(**content)
|
||||||
case 'fetch_messages':
|
case 'fetch_messages':
|
||||||
await self.fetch_messages(**content)
|
await self.fetch_messages(**content)
|
||||||
case 'mark_read':
|
|
||||||
await self.mark_read(**content)
|
|
||||||
case 'start_private_chat':
|
case 'start_private_chat':
|
||||||
await self.start_private_chat(**content)
|
await self.start_private_chat(**content)
|
||||||
case unknown:
|
case unknown:
|
||||||
@ -81,6 +77,8 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
async def fetch_channels(self) -> None:
|
async def fetch_channels(self) -> None:
|
||||||
user = self.scope['user']
|
user = self.scope['user']
|
||||||
|
|
||||||
|
read_channels = await Channel.get_accessible_channels(user, 'read')
|
||||||
|
write_channels = await Channel.get_accessible_channels(user, 'write')
|
||||||
message = {
|
message = {
|
||||||
'type': 'fetch_channels',
|
'type': 'fetch_channels',
|
||||||
'channels': [
|
'channels': [
|
||||||
@ -89,11 +87,9 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'name': channel.get_visible_name(user),
|
'name': channel.get_visible_name(user),
|
||||||
'category': channel.category,
|
'category': channel.category,
|
||||||
'read_access': True,
|
'read_access': True,
|
||||||
'write_access': await self.write_channels.acontains(channel),
|
'write_access': await write_channels.acontains(channel),
|
||||||
'unread_messages': channel.unread_messages,
|
|
||||||
}
|
}
|
||||||
async for channel in self.read_channels.prefetch_related('invited')
|
async for channel in read_channels.prefetch_related('invited').all()
|
||||||
.annotate(unread_messages=Count('messages', filter=~Q(messages__users_read=user))).all()
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
await self.send_json(message)
|
await self.send_json(message)
|
||||||
@ -102,7 +98,8 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
user = self.scope['user']
|
user = self.scope['user']
|
||||||
channel = await Channel.objects.prefetch_related('tournament__pools__juries', 'pool', 'team', 'invited') \
|
channel = await Channel.objects.prefetch_related('tournament__pools__juries', 'pool', 'team', 'invited') \
|
||||||
.aget(id=channel_id)
|
.aget(id=channel_id)
|
||||||
if not await self.write_channels.acontains(channel):
|
write_channels = await Channel.get_accessible_channels(user, 'write')
|
||||||
|
if not await write_channels.acontains(channel):
|
||||||
return
|
return
|
||||||
|
|
||||||
message = await Message.objects.acreate(
|
message = await Message.objects.acreate(
|
||||||
@ -153,16 +150,13 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
|
|
||||||
async def fetch_messages(self, channel_id: int, offset: int = 0, limit: int = 50, **_kwargs) -> None:
|
async def fetch_messages(self, channel_id: int, offset: int = 0, limit: int = 50, **_kwargs) -> None:
|
||||||
channel = await Channel.objects.aget(id=channel_id)
|
channel = await Channel.objects.aget(id=channel_id)
|
||||||
if not await self.read_channels.acontains(channel):
|
read_channels = await Channel.get_accessible_channels(self.scope['user'], 'read')
|
||||||
|
if not await read_channels.acontains(channel):
|
||||||
return
|
return
|
||||||
|
|
||||||
limit = min(limit, 200) # Fetch only maximum 200 messages at the time
|
limit = min(limit, 200) # Fetch only maximum 200 messages at the time
|
||||||
|
|
||||||
messages = Message.objects \
|
messages = Message.objects.filter(channel=channel).order_by('-created_at')[offset:offset + limit].all()
|
||||||
.filter(channel=channel) \
|
|
||||||
.annotate(read=Exists(User.objects.filter(pk=self.scope['user'].pk)
|
|
||||||
.filter(pk=OuterRef('users_read')))) \
|
|
||||||
.order_by('-created_at')[offset:offset + limit].all()
|
|
||||||
await self.send_json({
|
await self.send_json({
|
||||||
'type': 'fetch_messages',
|
'type': 'fetch_messages',
|
||||||
'channel_id': channel_id,
|
'channel_id': channel_id,
|
||||||
@ -173,27 +167,11 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'author_id': message.author_id,
|
'author_id': message.author_id,
|
||||||
'author': await message.aget_author_name(),
|
'author': await message.aget_author_name(),
|
||||||
'content': message.content,
|
'content': message.content,
|
||||||
'read': message.read,
|
|
||||||
}
|
}
|
||||||
async for message in messages
|
async for message in messages
|
||||||
]))
|
]))
|
||||||
})
|
})
|
||||||
|
|
||||||
async def mark_read(self, message_ids: list[int], **_kwargs) -> None:
|
|
||||||
messages = Message.objects.filter(id__in=message_ids)
|
|
||||||
async for message in messages.all():
|
|
||||||
await message.users_read.aadd(self.scope['user'])
|
|
||||||
|
|
||||||
unread_messages_by_channel = Message.objects.exclude(users_read=self.scope['user']).values('channel_id') \
|
|
||||||
.annotate(unread_messages=Count('channel_id'))
|
|
||||||
|
|
||||||
await self.send_json({
|
|
||||||
'type': 'mark_read',
|
|
||||||
'messages': [{'id': message.id, 'channel_id': message.channel_id} async for message in messages.all()],
|
|
||||||
'unread_messages': {group['channel_id']: group['unread_messages']
|
|
||||||
async for group in unread_messages_by_channel.all()},
|
|
||||||
})
|
|
||||||
|
|
||||||
async def start_private_chat(self, user_id: int, **kwargs) -> None:
|
async def start_private_chat(self, user_id: int, **kwargs) -> None:
|
||||||
user = self.scope['user']
|
user = self.scope['user']
|
||||||
other_user = await User.objects.aget(id=user_id)
|
other_user = await User.objects.aget(id=user_id)
|
||||||
@ -205,6 +183,7 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
private=True,
|
private=True,
|
||||||
)
|
)
|
||||||
await channel.invited.aset([user, other_user])
|
await channel.invited.aset([user, other_user])
|
||||||
|
await channel.asave()
|
||||||
|
|
||||||
await self.channel_layer.group_add(f"chat-{channel.id}", self.channel_name)
|
await self.channel_layer.group_add(f"chat-{channel.id}", self.channel_name)
|
||||||
|
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
# Generated by Django 5.0.3 on 2024-04-28 18:52
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("chat", "0002_alter_channel_options_channel_category"),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="message",
|
|
||||||
name="users_read",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Users who have read the message.",
|
|
||||||
related_name="+",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
verbose_name="users read",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -200,14 +200,6 @@ class Message(models.Model):
|
|||||||
verbose_name=_("content"),
|
verbose_name=_("content"),
|
||||||
)
|
)
|
||||||
|
|
||||||
users_read = models.ManyToManyField(
|
|
||||||
'auth.User',
|
|
||||||
verbose_name=_("users read"),
|
|
||||||
related_name='+',
|
|
||||||
blank=True,
|
|
||||||
help_text=_("Users who have read the message."),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_author_name(self):
|
def get_author_name(self):
|
||||||
registration = self.author.registration
|
registration = self.author.registration
|
||||||
|
|
||||||
|
@ -33,7 +33,8 @@ function selectChannel(channel_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selected_channel_id = channel_id
|
selected_channel_id = channel_id
|
||||||
localStorage.setItem('chat.last-channel-id', channel_id)
|
|
||||||
|
window.history.replaceState({}, null, `#channel-${channel['id']}`)
|
||||||
|
|
||||||
let channelTitle = document.getElementById('channel-title')
|
let channelTitle = document.getElementById('channel-title')
|
||||||
channelTitle.innerText = channel['name']
|
channelTitle.innerText = channel['name']
|
||||||
@ -69,53 +70,43 @@ function setChannels(new_channels) {
|
|||||||
categoryLists[category].parentElement.classList.add('d-none')
|
categoryLists[category].parentElement.classList.add('d-none')
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let channel of new_channels)
|
for (let channel of new_channels) {
|
||||||
addChannel(channel, categoryLists)
|
channels[channel['id']] = channel
|
||||||
|
if (!messages[channel['id']])
|
||||||
|
messages[channel['id']] = new Map()
|
||||||
|
|
||||||
|
let categoryList = categoryLists[channel['category']]
|
||||||
|
categoryList.parentElement.classList.remove('d-none')
|
||||||
|
|
||||||
|
let navItem = document.createElement('li')
|
||||||
|
navItem.classList.add('list-group-item')
|
||||||
|
navItem.id = `tab-channel-${channel['id']}`
|
||||||
|
navItem.setAttribute('data-bs-dismiss', 'offcanvas')
|
||||||
|
navItem.onclick = () => selectChannel(channel['id'])
|
||||||
|
categoryList.appendChild(navItem)
|
||||||
|
|
||||||
|
let channelButton = document.createElement('button')
|
||||||
|
channelButton.classList.add('nav-link')
|
||||||
|
channelButton.type = 'button'
|
||||||
|
channelButton.innerText = channel['name']
|
||||||
|
navItem.appendChild(channelButton)
|
||||||
|
|
||||||
|
fetchMessages(channel['id'])
|
||||||
|
}
|
||||||
|
|
||||||
if (new_channels && (!selected_channel_id || !channels[selected_channel_id])) {
|
if (new_channels && (!selected_channel_id || !channels[selected_channel_id])) {
|
||||||
let last_channel_id = parseInt(localStorage.getItem('chat.last-channel-id'))
|
if (window.location.hash) {
|
||||||
if (last_channel_id && channels[last_channel_id])
|
let channel_id = parseInt(window.location.hash.substring(9))
|
||||||
selectChannel(last_channel_id)
|
if (channels[channel_id])
|
||||||
|
selectChannel(channel_id)
|
||||||
|
else
|
||||||
|
selectChannel(Object.keys(channels)[0])
|
||||||
|
}
|
||||||
else
|
else
|
||||||
selectChannel(Object.keys(channels)[0])
|
selectChannel(Object.keys(channels)[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addChannel(channel, categoryLists) {
|
|
||||||
channels[channel['id']] = channel
|
|
||||||
if (!messages[channel['id']])
|
|
||||||
messages[channel['id']] = new Map()
|
|
||||||
|
|
||||||
let categoryList = categoryLists[channel['category']]
|
|
||||||
categoryList.parentElement.classList.remove('d-none')
|
|
||||||
|
|
||||||
let navItem = document.createElement('li')
|
|
||||||
navItem.classList.add('list-group-item', 'tab-channel')
|
|
||||||
navItem.id = `tab-channel-${channel['id']}`
|
|
||||||
navItem.setAttribute('data-bs-dismiss', 'offcanvas')
|
|
||||||
navItem.onclick = () => selectChannel(channel['id'])
|
|
||||||
categoryList.appendChild(navItem)
|
|
||||||
|
|
||||||
let channelButton = document.createElement('button')
|
|
||||||
channelButton.classList.add('nav-link')
|
|
||||||
channelButton.type = 'button'
|
|
||||||
channelButton.innerText = channel['name']
|
|
||||||
navItem.appendChild(channelButton)
|
|
||||||
|
|
||||||
let unreadBadge = document.createElement('span')
|
|
||||||
unreadBadge.classList.add('badge', 'rounded-pill', 'text-bg-light', 'ms-2')
|
|
||||||
unreadBadge.id = `unread-messages-${channel['id']}`
|
|
||||||
unreadBadge.innerText = channel.unread_messages || 0
|
|
||||||
if (!channel.unread_messages)
|
|
||||||
unreadBadge.classList.add('d-none')
|
|
||||||
channelButton.appendChild(unreadBadge)
|
|
||||||
|
|
||||||
if (document.getElementById('sort-by-unread-switch').checked)
|
|
||||||
navItem.style.order = `${-channel.unread_messages}`
|
|
||||||
|
|
||||||
fetchMessages(channel['id'])
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiveMessage(message) {
|
function receiveMessage(message) {
|
||||||
let scrollableContent = document.getElementById('chat-messages')
|
let scrollableContent = document.getElementById('chat-messages')
|
||||||
let isScrolledToBottom = scrollableContent.scrollHeight - scrollableContent.clientHeight <= scrollableContent.scrollTop + 1
|
let isScrolledToBottom = scrollableContent.scrollHeight - scrollableContent.clientHeight <= scrollableContent.scrollTop + 1
|
||||||
@ -174,36 +165,6 @@ function receiveFetchedMessages(data) {
|
|||||||
redrawMessages()
|
redrawMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
function markMessageAsRead(data) {
|
|
||||||
for (let message of data['messages']) {
|
|
||||||
let stored_message = messages[message['channel_id']].get(message['id'])
|
|
||||||
if (stored_message)
|
|
||||||
stored_message['read'] = true
|
|
||||||
}
|
|
||||||
redrawMessages()
|
|
||||||
updateUnreadBadges(data['unread_messages'])
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUnreadBadges(unreadMessages) {
|
|
||||||
const sortByUnread = document.getElementById('sort-by-unread-switch').checked
|
|
||||||
|
|
||||||
for (let channel of Object.values(channels)) {
|
|
||||||
let unreadMessagesChannel = unreadMessages[channel['id']] || 0
|
|
||||||
console.log(channel, unreadMessagesChannel)
|
|
||||||
channel.unread_messages = unreadMessagesChannel
|
|
||||||
|
|
||||||
let unreadBadge = document.getElementById(`unread-messages-${channel['id']}`)
|
|
||||||
unreadBadge.innerText = unreadMessagesChannel
|
|
||||||
if (unreadMessagesChannel)
|
|
||||||
unreadBadge.classList.remove('d-none')
|
|
||||||
else
|
|
||||||
unreadBadge.classList.add('d-none')
|
|
||||||
|
|
||||||
if (sortByUnread)
|
|
||||||
document.getElementById(`tab-channel-${channel['id']}`).style.order = `${-unreadMessagesChannel}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startPrivateChat(data) {
|
function startPrivateChat(data) {
|
||||||
let channel = data['channel']
|
let channel = data['channel']
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
@ -233,14 +194,12 @@ function redrawMessages() {
|
|||||||
let newTimestamp = new Date(message['timestamp'])
|
let newTimestamp = new Date(message['timestamp'])
|
||||||
if ((newTimestamp - lastTimestamp) / 1000 < 60 * 10) {
|
if ((newTimestamp - lastTimestamp) / 1000 < 60 * 10) {
|
||||||
let messageContentDiv = document.createElement('div')
|
let messageContentDiv = document.createElement('div')
|
||||||
messageContentDiv.classList.add('message')
|
|
||||||
messageContentDiv.setAttribute('data-message-id', message['id'])
|
|
||||||
lastContentDiv.appendChild(messageContentDiv)
|
lastContentDiv.appendChild(messageContentDiv)
|
||||||
let messageContentSpan = document.createElement('span')
|
let messageContentSpan = document.createElement('span')
|
||||||
messageContentSpan.innerText = message['content']
|
messageContentSpan.innerText = message['content']
|
||||||
messageContentDiv.appendChild(messageContentSpan)
|
messageContentDiv.appendChild(messageContentSpan)
|
||||||
|
|
||||||
registerMessageContextMenu(message, messageContentDiv, messageContentSpan)
|
registerMessageContextMenu(message, messageContentSpan)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,7 +216,7 @@ function redrawMessages() {
|
|||||||
authorSpan.innerText = message['author']
|
authorSpan.innerText = message['author']
|
||||||
authorDiv.appendChild(authorSpan)
|
authorDiv.appendChild(authorSpan)
|
||||||
|
|
||||||
registerSendPrivateMessageContextMenu(message, authorDiv, authorSpan)
|
registerSendPrivateMessageContextMenu(message, authorSpan)
|
||||||
|
|
||||||
let dateSpan = document.createElement('span')
|
let dateSpan = document.createElement('span')
|
||||||
dateSpan.classList.add('text-muted', 'float-end')
|
dateSpan.classList.add('text-muted', 'float-end')
|
||||||
@ -268,14 +227,12 @@ function redrawMessages() {
|
|||||||
messageElement.appendChild(contentDiv)
|
messageElement.appendChild(contentDiv)
|
||||||
|
|
||||||
let messageContentDiv = document.createElement('div')
|
let messageContentDiv = document.createElement('div')
|
||||||
messageContentDiv.classList.add('message')
|
|
||||||
messageContentDiv.setAttribute('data-message-id', message['id'])
|
|
||||||
contentDiv.appendChild(messageContentDiv)
|
contentDiv.appendChild(messageContentDiv)
|
||||||
let messageContentSpan = document.createElement('span')
|
let messageContentSpan = document.createElement('span')
|
||||||
messageContentSpan.innerText = message['content']
|
messageContentSpan.innerText = message['content']
|
||||||
messageContentDiv.appendChild(messageContentSpan)
|
messageContentDiv.appendChild(messageContentSpan)
|
||||||
|
|
||||||
registerMessageContextMenu(message, messageContentDiv, messageContentSpan)
|
registerMessageContextMenu(message, messageContentSpan)
|
||||||
|
|
||||||
lastMessage = message
|
lastMessage = message
|
||||||
lastContentDiv = contentDiv
|
lastContentDiv = contentDiv
|
||||||
@ -286,8 +243,6 @@ function redrawMessages() {
|
|||||||
fetchMoreButton.classList.add('d-none')
|
fetchMoreButton.classList.add('d-none')
|
||||||
else
|
else
|
||||||
fetchMoreButton.classList.remove('d-none')
|
fetchMoreButton.classList.remove('d-none')
|
||||||
|
|
||||||
messageList.dispatchEvent(new CustomEvent('updatemessages'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAllPopovers() {
|
function removeAllPopovers() {
|
||||||
@ -298,11 +253,11 @@ function removeAllPopovers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerSendPrivateMessageContextMenu(message, div, span) {
|
function registerSendPrivateMessageContextMenu(message, element) {
|
||||||
div.addEventListener('contextmenu', (menu_event) => {
|
element.addEventListener('contextmenu', (menu_event) => {
|
||||||
menu_event.preventDefault()
|
menu_event.preventDefault()
|
||||||
removeAllPopovers()
|
removeAllPopovers()
|
||||||
const popover = bootstrap.Popover.getOrCreateInstance(span, {
|
const popover = bootstrap.Popover.getOrCreateInstance(element, {
|
||||||
'title': message['author'],
|
'title': message['author'],
|
||||||
'content': `<a id="send-private-message-link-${message['id']}" class="nav-link" href="#" tabindex="0">Envoyer un message privé</a>`,
|
'content': `<a id="send-private-message-link-${message['id']}" class="nav-link" href="#" tabindex="0">Envoyer un message privé</a>`,
|
||||||
'html': true,
|
'html': true,
|
||||||
@ -320,8 +275,8 @@ function registerSendPrivateMessageContextMenu(message, div, span) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerMessageContextMenu(message, div, span) {
|
function registerMessageContextMenu(message, element) {
|
||||||
div.addEventListener('contextmenu', (menu_event) => {
|
element.addEventListener('contextmenu', (menu_event) => {
|
||||||
menu_event.preventDefault()
|
menu_event.preventDefault()
|
||||||
removeAllPopovers()
|
removeAllPopovers()
|
||||||
let content = `<a id="send-private-message-link-msg-${message['id']}" class="nav-link" href="#" tabindex="0">Envoyer un message privé</a>`
|
let content = `<a id="send-private-message-link-msg-${message['id']}" class="nav-link" href="#" tabindex="0">Envoyer un message privé</a>`
|
||||||
@ -333,7 +288,7 @@ function registerMessageContextMenu(message, div, span) {
|
|||||||
content += `<a id="delete-message-${message['id']}" class="nav-link" href="#" tabindex="0">Supprimer</a>`
|
content += `<a id="delete-message-${message['id']}" class="nav-link" href="#" tabindex="0">Supprimer</a>`
|
||||||
}
|
}
|
||||||
|
|
||||||
const popover = bootstrap.Popover.getOrCreateInstance(span, {
|
const popover = bootstrap.Popover.getOrCreateInstance(element, {
|
||||||
'content': content,
|
'content': content,
|
||||||
'html': true,
|
'html': true,
|
||||||
'placement': 'bottom',
|
'placement': 'bottom',
|
||||||
@ -366,7 +321,7 @@ function registerMessageContextMenu(message, div, span) {
|
|||||||
document.getElementById('delete-message-' + message['id']).addEventListener('click', event => {
|
document.getElementById('delete-message-' + message['id']).addEventListener('click', event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
popover.dispose()
|
popover.dispose()
|
||||||
if (confirm(`Supprimer le message ?\n${message['content']}`)) {
|
if (confirm("Supprimer le message ?")) {
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
'type': 'delete_message',
|
'type': 'delete_message',
|
||||||
'message_id': message['id'],
|
'message_id': message['id'],
|
||||||
@ -382,36 +337,18 @@ function toggleFullscreen() {
|
|||||||
if (!chatContainer.getAttribute('data-fullscreen')) {
|
if (!chatContainer.getAttribute('data-fullscreen')) {
|
||||||
chatContainer.setAttribute('data-fullscreen', 'true')
|
chatContainer.setAttribute('data-fullscreen', 'true')
|
||||||
chatContainer.classList.add('position-absolute', 'top-0', 'start-0', 'vh-100', 'z-3')
|
chatContainer.classList.add('position-absolute', 'top-0', 'start-0', 'vh-100', 'z-3')
|
||||||
window.history.replaceState({}, null, `?fullscreen=1`)
|
window.history.replaceState({}, null, `?fullscreen=1#channel-${selected_channel_id}`)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
chatContainer.removeAttribute('data-fullscreen')
|
chatContainer.removeAttribute('data-fullscreen')
|
||||||
chatContainer.classList.remove('position-absolute', 'top-0', 'start-0', 'vh-100', 'z-3')
|
chatContainer.classList.remove('position-absolute', 'top-0', 'start-0', 'vh-100', 'z-3')
|
||||||
window.history.replaceState({}, null, `?fullscreen=0`)
|
window.history.replaceState({}, null, `?fullscreen=0#channel-${selected_channel_id}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.addEventListener('click', removeAllPopovers)
|
document.addEventListener('click', removeAllPopovers)
|
||||||
|
|
||||||
document.getElementById('sort-by-unread-switch').addEventListener('change', event => {
|
|
||||||
const sortByUnread = event.target.checked
|
|
||||||
for (let channel of Object.values(channels)) {
|
|
||||||
let item = document.getElementById(`tab-channel-${channel['id']}`)
|
|
||||||
if (sortByUnread)
|
|
||||||
item.style.order = `${-channel.unread_messages}`
|
|
||||||
else
|
|
||||||
item.style.removeProperty('order')
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('chat.sort-by-unread', sortByUnread)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (localStorage.getItem('chat.sort-by-unread') === 'true') {
|
|
||||||
document.getElementById('sort-by-unread-switch').checked = true
|
|
||||||
document.getElementById('sort-by-unread-switch').dispatchEvent(new Event('change'))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the received data from the server.
|
* Process the received data from the server.
|
||||||
* @param data The received message
|
* @param data The received message
|
||||||
@ -433,9 +370,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
case 'fetch_messages':
|
case 'fetch_messages':
|
||||||
receiveFetchedMessages(data)
|
receiveFetchedMessages(data)
|
||||||
break
|
break
|
||||||
case 'mark_read':
|
|
||||||
markMessageAsRead(data)
|
|
||||||
break
|
|
||||||
case 'start_private_chat':
|
case 'start_private_chat':
|
||||||
startPrivateChat(data)
|
startPrivateChat(data)
|
||||||
break
|
break
|
||||||
@ -501,51 +435,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupReadTracker() {
|
|
||||||
const scrollableContent = document.getElementById('chat-messages')
|
|
||||||
const messagesList = document.getElementById('message-list')
|
|
||||||
let markReadBuffer = []
|
|
||||||
let markReadTimeout = null
|
|
||||||
|
|
||||||
scrollableContent.addEventListener('scroll', () => {
|
|
||||||
if (scrollableContent.clientHeight - scrollableContent.scrollTop === scrollableContent.scrollHeight
|
|
||||||
&& !document.getElementById('fetch-previous-messages').classList.contains('d-none')) {
|
|
||||||
// If the user is at the top of the chat, fetch previous messages
|
|
||||||
fetchPreviousMessages()}
|
|
||||||
|
|
||||||
markVisibleMessagesAsRead()
|
|
||||||
})
|
|
||||||
|
|
||||||
messagesList.addEventListener('updatemessages', () => markVisibleMessagesAsRead())
|
|
||||||
|
|
||||||
function markVisibleMessagesAsRead() {
|
|
||||||
let viewport = scrollableContent.getBoundingClientRect()
|
|
||||||
|
|
||||||
for (let item of messagesList.querySelectorAll('.message')) {
|
|
||||||
let message = messages[selected_channel_id].get(parseInt(item.getAttribute('data-message-id')))
|
|
||||||
if (!message.read) {
|
|
||||||
let rect = item.getBoundingClientRect()
|
|
||||||
if (rect.top >= viewport.top && rect.bottom <= viewport.bottom) {
|
|
||||||
message.read = true
|
|
||||||
markReadBuffer.push(message['id'])
|
|
||||||
if (markReadTimeout)
|
|
||||||
clearTimeout(markReadTimeout)
|
|
||||||
markReadTimeout = setTimeout(() => {
|
|
||||||
socket.send(JSON.stringify({
|
|
||||||
'type': 'mark_read',
|
|
||||||
'message_ids': markReadBuffer,
|
|
||||||
}))
|
|
||||||
markReadBuffer = []
|
|
||||||
markReadTimeout = null
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
markVisibleMessagesAsRead()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupPWAPrompt() {
|
function setupPWAPrompt() {
|
||||||
let deferredPrompt = null
|
let deferredPrompt = null
|
||||||
|
|
||||||
@ -571,6 +460,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
setupSocket()
|
setupSocket()
|
||||||
setupSwipeOffscreen()
|
setupSwipeOffscreen()
|
||||||
setupReadTracker()
|
|
||||||
setupPWAPrompt()
|
setupPWAPrompt()
|
||||||
})
|
})
|
||||||
|
@ -9,10 +9,6 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="offcanvas-body">
|
<div class="offcanvas-body">
|
||||||
<div class="form-switch form-switch">
|
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="sort-by-unread-switch">
|
|
||||||
<label class="form-check-label" for="sort-by-unread-switch">{% trans "Sort by unread messages" %}</label>
|
|
||||||
</div>
|
|
||||||
<ul class="list-group list-group-flush" id="nav-channels-tab">
|
<ul class="list-group list-group-flush" id="nav-channels-tab">
|
||||||
<li class="list-group-item d-none">
|
<li class="list-group-item d-none">
|
||||||
<h4>{% trans "General channels" %}</h4>
|
<h4>{% trans "General channels" %}</h4>
|
||||||
|
@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TFJM\n"
|
"Project-Id-Version: TFJM\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-28 23:37+0200\n"
|
"POT-Creation-Date: 2024-04-28 13:08+0200\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n"
|
"Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -21,19 +21,19 @@ msgstr ""
|
|||||||
msgid "API"
|
msgid "API"
|
||||||
msgstr "API"
|
msgstr "API"
|
||||||
|
|
||||||
#: chat/models.py:17 chat/templates/chat/content.html:18
|
#: chat/models.py:17
|
||||||
msgid "General channels"
|
msgid "General channels"
|
||||||
msgstr "Canaux généraux"
|
msgstr "Canaux généraux"
|
||||||
|
|
||||||
#: chat/models.py:18 chat/templates/chat/content.html:22
|
#: chat/models.py:18
|
||||||
msgid "Tournament channels"
|
msgid "Tournament channels"
|
||||||
msgstr "Canaux de tournois"
|
msgstr "Canaux de tournois"
|
||||||
|
|
||||||
#: chat/models.py:19 chat/templates/chat/content.html:26
|
#: chat/models.py:19
|
||||||
msgid "Team channels"
|
msgid "Team channels"
|
||||||
msgstr "Canaux d'équipes"
|
msgstr "Canaux d'équipes"
|
||||||
|
|
||||||
#: chat/models.py:20 chat/templates/chat/content.html:30
|
#: chat/models.py:20
|
||||||
msgid "Private channels"
|
msgid "Private channels"
|
||||||
msgstr "Messages privés"
|
msgstr "Messages privés"
|
||||||
|
|
||||||
@ -126,48 +126,40 @@ msgstr ""
|
|||||||
"Utilisateur⋅rices supplémentaires qui ont été invité⋅es au canal, en plus du "
|
"Utilisateur⋅rices supplémentaires qui ont été invité⋅es au canal, en plus du "
|
||||||
"groupe autorisé du canal."
|
"groupe autorisé du canal."
|
||||||
|
|
||||||
#: chat/models.py:102
|
#: chat/models.py:95
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Channel {name}"
|
msgid "Channel {name}"
|
||||||
msgstr "Canal {name}"
|
msgstr "Canal {name}"
|
||||||
|
|
||||||
#: chat/models.py:168 chat/models.py:177
|
#: chat/models.py:161 chat/models.py:170
|
||||||
msgid "channel"
|
msgid "channel"
|
||||||
msgstr "canal"
|
msgstr "canal"
|
||||||
|
|
||||||
#: chat/models.py:169
|
#: chat/models.py:162
|
||||||
msgid "channels"
|
msgid "channels"
|
||||||
msgstr "canaux"
|
msgstr "canaux"
|
||||||
|
|
||||||
#: chat/models.py:183
|
#: chat/models.py:176
|
||||||
msgid "author"
|
msgid "author"
|
||||||
msgstr "auteur⋅rice"
|
msgstr "auteur⋅rice"
|
||||||
|
|
||||||
#: chat/models.py:190
|
#: chat/models.py:183
|
||||||
msgid "created at"
|
msgid "created at"
|
||||||
msgstr "créé le"
|
msgstr "créé le"
|
||||||
|
|
||||||
#: chat/models.py:195
|
#: chat/models.py:188
|
||||||
msgid "updated at"
|
msgid "updated at"
|
||||||
msgstr "modifié le"
|
msgstr "modifié le"
|
||||||
|
|
||||||
#: chat/models.py:200
|
#: chat/models.py:193
|
||||||
msgid "content"
|
msgid "content"
|
||||||
msgstr "contenu"
|
msgstr "contenu"
|
||||||
|
|
||||||
#: chat/models.py:205
|
#: chat/models.py:256
|
||||||
msgid "users read"
|
|
||||||
msgstr "utilisateur⋅rices ayant lu"
|
|
||||||
|
|
||||||
#: chat/models.py:208
|
|
||||||
msgid "Users who have read the message."
|
|
||||||
msgstr "Utilisateur⋅rices qui ont lu le message."
|
|
||||||
|
|
||||||
#: chat/models.py:271
|
|
||||||
msgid "message"
|
msgid "message"
|
||||||
msgstr "message"
|
msgstr "message"
|
||||||
|
|
||||||
#: chat/models.py:272
|
#: chat/models.py:257
|
||||||
msgid "messages"
|
msgid "messages"
|
||||||
msgstr "messages"
|
msgstr "messages"
|
||||||
|
|
||||||
@ -179,11 +171,7 @@ msgstr "JavaScript doit être activé sur votre navigateur pour accéder au chat
|
|||||||
msgid "Chat channels"
|
msgid "Chat channels"
|
||||||
msgstr "Canaux de chat"
|
msgstr "Canaux de chat"
|
||||||
|
|
||||||
#: chat/templates/chat/content.html:14
|
#: chat/templates/chat/content.html:17
|
||||||
msgid "Sort by unread messages"
|
|
||||||
msgstr "Trier par messages non lus"
|
|
||||||
|
|
||||||
#: chat/templates/chat/content.html:38
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can install a shortcut to the chat on your home screen using the "
|
"You can install a shortcut to the chat on your home screen using the "
|
||||||
"download button on the header."
|
"download button on the header."
|
||||||
@ -191,23 +179,23 @@ msgstr ""
|
|||||||
"Vous pouvez installer un raccourci vers le chat sur votre écran d'accueil en "
|
"Vous pouvez installer un raccourci vers le chat sur votre écran d'accueil en "
|
||||||
"utilisant le bouton de téléchargement dans l'en-tête."
|
"utilisant le bouton de téléchargement dans l'en-tête."
|
||||||
|
|
||||||
#: chat/templates/chat/content.html:56
|
#: chat/templates/chat/content.html:35
|
||||||
msgid "Toggle fullscreen mode"
|
msgid "Toggle fullscreen mode"
|
||||||
msgstr "Inverse le mode plein écran"
|
msgstr "Inverse le mode plein écran"
|
||||||
|
|
||||||
#: chat/templates/chat/content.html:60 tfjm/templates/navbar.html:117
|
#: chat/templates/chat/content.html:39 tfjm/templates/navbar.html:117
|
||||||
msgid "Log out"
|
msgid "Log out"
|
||||||
msgstr "Déconnexion"
|
msgstr "Déconnexion"
|
||||||
|
|
||||||
#: chat/templates/chat/content.html:64
|
#: chat/templates/chat/content.html:43
|
||||||
msgid "Install app on home screen"
|
msgid "Install app on home screen"
|
||||||
msgstr "Installer l'application sur l'écran d'accueil"
|
msgstr "Installer l'application sur l'écran d'accueil"
|
||||||
|
|
||||||
#: chat/templates/chat/content.html:76
|
#: chat/templates/chat/content.html:55
|
||||||
msgid "Fetch previous messages…"
|
msgid "Fetch previous messages…"
|
||||||
msgstr "Récupérer les messages précédents…"
|
msgstr "Récupérer les messages précédents…"
|
||||||
|
|
||||||
#: chat/templates/chat/content.html:87
|
#: chat/templates/chat/content.html:66
|
||||||
msgid "Send message…"
|
msgid "Send message…"
|
||||||
msgstr "Envoyer un message…"
|
msgstr "Envoyer un message…"
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user