Compare commits

...

2 commits

5 changed files with 164 additions and 15 deletions

View file

@ -127,6 +127,28 @@ textarea#message-content {
.dark #ai-assist-button i {
color: #e4e6eb;
}
.reaction-bubble {
padding: 2px 6px;
border-radius: 12px;
background: #eee;
margin-right: 2px;
cursor: pointer;
border: 1px solid #ccc;
display: inline-block;
font-size: 16px;
transition: background 0.2s, border 0.2s;
}
.reaction-bubble.my-reaction {
background: #ffd966;
border: 1px solid #ffc107;
}
.add-reaction-row {
display: none;
}
.message-container:hover .add-reaction-row {
display: flex;
}
</style>
</head>
@ -171,7 +193,7 @@ textarea#message-content {
<div id="chat-log" style="flex: 1; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 15px;">
{% for msg in messages %}
<div style="margin: 10px 0; display: flex; align-items: start; {% if msg.sender == request.user %}flex-direction: row-reverse;{% endif %} gap: 8px;">
<div class="message-container" style="margin: 10px 0; display: flex; align-items: start; {% if msg.sender == request.user %}flex-direction: row-reverse;{% endif %} gap: 8px;">
<div style="width: 32px; height: 32px; flex-shrink: 0;">
{% if msg.sender.profile.profile_picture %}
<img src="{{ msg.sender.profile.profile_picture.url }}" alt="Profile Picture" style="width: 32px; height: 32px; border-radius: 4px; object-fit: cover; border: 1px solid #ccc;">
@ -203,6 +225,22 @@ textarea#message-content {
<button type="submit" class="btn btn-danger btn-sm" style="margin-top:5px; background-color: #dc3545; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer;">Delete</button>
</form>
{% endif %}
{% if msg.reactions.all %}
{% with last_reaction=msg.reactions.all.last %}
<div class="reactions-row" style="margin: 4px 0; display: flex; gap: 4px;">
<span class="reaction-bubble{% if last_reaction.user == request.user %} my-reaction{% endif %}" data-message-id="{{ msg.id }}" data-reaction="{{ last_reaction.reaction_type }}">
{{ last_reaction.reaction_type }}
</span>
</div>
{% endwith %}
{% endif %}
<div class="add-reaction-row" style="margin: 2px 0; gap: 4px;">
<button type="button" class="add-reaction-btn" data-message-id="{{ msg.id }}" data-emoji="👍" style="background: none; border: none; cursor: pointer; font-size: 18px;">👍</button>
<button type="button" class="add-reaction-btn" data-message-id="{{ msg.id }}" data-emoji="❤️" style="background: none; border: none; cursor: pointer; font-size: 18px;">❤️</button>
<button type="button" class="add-reaction-btn" data-message-id="{{ msg.id }}" data-emoji="😂" style="background: none; border: none; cursor: pointer; font-size: 18px;">😂</button>
<button type="button" class="add-reaction-btn" data-message-id="{{ msg.id }}" data-emoji="😮" style="background: none; border: none; cursor: pointer; font-size: 18px;">😮</button>
<button type="button" class="add-reaction-btn" data-message-id="{{ msg.id }}" data-emoji="😢" style="background: none; border: none; cursor: pointer; font-size: 18px;">😢</button>
</div>
</div>
</div>
{% endfor %}
@ -449,5 +487,30 @@ const toggle = document.getElementById("themeToggle");
}
});
</script>
<script>
$(document).on('click', '.add-reaction-btn', function() {
var messageId = $(this).data('message-id');
var emoji = $(this).data('emoji');
var csrfToken = $('input[name="csrfmiddlewaretoken"]').val();
$.post(`/message/${messageId}/add_reaction/`, {
reaction_type: emoji,
csrfmiddlewaretoken: csrfToken
}, function(response) {
location.reload(); // For now, reload to show updated reactions
});
});
$(document).on('click', '.reaction-bubble', function() {
var messageId = $(this).data('message-id');
var emoji = $(this).data('reaction');
var csrfToken = $('input[name="csrfmiddlewaretoken"]').val();
$.post(`/message/${messageId}/remove_reaction/`, {
reaction_type: emoji,
csrfmiddlewaretoken: csrfToken
}, function(response) {
location.reload();
});
});
</script>
</body>
</html>

View file

@ -0,0 +1,29 @@
# Generated by Django 5.2.1 on 2025-06-24 21:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eversyncc', '0032_embed_order'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='MessageReaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reaction_type', models.CharField(max_length=16)),
('created_at', models.DateTimeField(auto_now_add=True)),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='eversyncc.message')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('message', 'user', 'reaction_type')},
},
),
]

View file

@ -151,3 +151,15 @@ def create_user_profile(sender, instance, created, **kwargs):
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
class MessageReaction(models.Model):
message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='reactions')
user = models.ForeignKey(User, on_delete=models.CASCADE)
reaction_type = models.CharField(max_length=16)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('message', 'user', 'reaction_type')
def __str__(self):
return f"{self.user.username} reacted {self.reaction_type} to message {self.message.id}"

View file

@ -78,6 +78,8 @@ urlpatterns = [
path('web_archive/view/<int:archive_id>', views.view_web_archive, name='view_web_archive'),
path('update_profile_picture/', views.update_profile_picture, name='update_profile_picture'),
path('delete_profile_picture/', views.delete_profile_picture, name='delete_profile_picture'),
path('message/<int:message_id>/add_reaction/', views.add_reaction, name='add_reaction'),
path('message/<int:message_id>/remove_reaction/', views.remove_reaction, name='remove_reaction'),
]
if settings.DEBUG:

View file

@ -12,7 +12,7 @@ from django.contrib.auth import logout
from django.contrib.auth.models import User
from django.contrib.auth.views import PasswordChangeView
from .forms import UsernameChangeForm, DocumentForm, EventForm, NoteForm, TaskForm
from .models import Document, Event, Notes, Embed, Task, RichDocument, Message, WebArchive
from .models import Document, Event, Notes, Embed, Task, RichDocument, Message, WebArchive, MessageReaction
from django.contrib import messages
from allauth.account.views import LoginView as AllauthLoginView
import os
@ -52,6 +52,20 @@ import selenium
import pyclamd
from datetime import datetime, timedelta
# Constants
FORBIDDEN_EXTENSIONS = ['.html', '.htm', '.php', '.exe', '.js', '.sh', '.bat']
SELENIUM_CHROME_ARGS = [
'--headless=new',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--lang=en-US',
]
SELENIUM_CHROME_PREFS = {
'intl.accept_languages': 'en,en_US',
'profile.default_content_setting_values.geolocation': 2,
}
def scan_file_with_clamav(file_path):
try:
cd = pyclamd.ClamdNetworkSocket(host='127.0.0.1', port=3310)
@ -176,8 +190,7 @@ def upload_file(request):
file_size = document.file.size
forbidden_extensions=['.html','.htm','.php','.exe','.js','.sh','.bat']
if ext in forbidden_extensions:
if ext in FORBIDDEN_EXTENSIONS:
request.session.flush()
logout(request)
return HttpResponse("<html><body><script>alert('Uploading possibly malicious files is forbidden. You have been logged out.'); location.reload();</script></body></html>")
@ -902,7 +915,7 @@ def chat_with_user(request, username):
messages = Message.objects.filter(
Q(sender=request.user, receiver=other_user) |
Q(sender=other_user, receiver=request.user)
).select_related('sender', 'receiver').order_by('timestamp')
).select_related('sender', 'receiver').prefetch_related('reactions__user').order_by('timestamp')
pinned_message = Message.objects.filter(pinned=True, sender=request.user).order_by('-timestamp').first()
Message.objects.filter(sender=other_user, receiver=request.user, seen=False).update(seen=True, seen_at=now())
@ -1212,15 +1225,9 @@ def save_web_archive(request):
try:
# Set up Chrome options
chrome_options = Options()
chrome_options.add_argument('--headless=new')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument("--lang=en-US")
chrome_options.add_experimental_option("prefs", {
"intl.accept_languages": "en,en_US",
"profile.default_content_setting_values.geolocation": 2,
})
for arg in SELENIUM_CHROME_ARGS:
chrome_options.add_argument(arg)
chrome_options.add_experimental_option("prefs", SELENIUM_CHROME_PREFS)
# Initialize Chrome driver
driver = webdriver.Chrome(options=chrome_options)
url = form.cleaned_data['url']
@ -1371,3 +1378,39 @@ def reorder_embeds(request):
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
@email_verified_required
@login_required
def add_reaction(request, message_id):
if request.method == 'POST':
reaction_type = request.POST.get('reaction_type')
if not reaction_type:
return JsonResponse({'error': 'No reaction_type provided'}, status=400)
message = get_object_or_404(Message, id=message_id)
reaction, created = MessageReaction.objects.get_or_create(
message=message,
user=request.user,
reaction_type=reaction_type
)
return JsonResponse({'success': True, 'created': created})
return JsonResponse({'error': 'Invalid request method'}, status=405)
@email_verified_required
@login_required
def remove_reaction(request, message_id):
if request.method == 'POST':
reaction_type = request.POST.get('reaction_type')
if not reaction_type:
return JsonResponse({'error': 'No reaction_type provided'}, status=400)
message = get_object_or_404(Message, id=message_id)
try:
reaction = MessageReaction.objects.get(
message=message,
user=request.user,
reaction_type=reaction_type
)
reaction.delete()
return JsonResponse({'success': True})
except MessageReaction.DoesNotExist:
return JsonResponse({'error': 'Reaction not found'}, status=404)
return JsonResponse({'error': 'Invalid request method'}, status=405)