mirror of
https://github.com/rudy3333/eversync.git
synced 2025-07-01 08:36:02 +00:00
Compare commits
2 commits
7b1ac19a4e
...
e85ed60e20
Author | SHA1 | Date | |
---|---|---|---|
e85ed60e20 | |||
97ef207ba9 |
5 changed files with 164 additions and 15 deletions
|
@ -127,6 +127,28 @@ textarea#message-content {
|
||||||
.dark #ai-assist-button i {
|
.dark #ai-assist-button i {
|
||||||
color: #e4e6eb;
|
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>
|
</style>
|
||||||
</head>
|
</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;">
|
<div id="chat-log" style="flex: 1; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 15px;">
|
||||||
{% for msg in messages %}
|
{% 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;">
|
<div style="width: 32px; height: 32px; flex-shrink: 0;">
|
||||||
{% if msg.sender.profile.profile_picture %}
|
{% 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;">
|
<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>
|
<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>
|
</form>
|
||||||
{% endif %}
|
{% 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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -449,5 +487,30 @@ const toggle = document.getElementById("themeToggle");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
29
eversyncc/migrations/0033_messagereaction.py
Normal file
29
eversyncc/migrations/0033_messagereaction.py
Normal 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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -150,4 +150,16 @@ def create_user_profile(sender, instance, created, **kwargs):
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
def save_user_profile(sender, instance, **kwargs):
|
def save_user_profile(sender, instance, **kwargs):
|
||||||
instance.profile.save()
|
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}"
|
|
@ -78,6 +78,8 @@ urlpatterns = [
|
||||||
path('web_archive/view/<int:archive_id>', views.view_web_archive, name='view_web_archive'),
|
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('update_profile_picture/', views.update_profile_picture, name='update_profile_picture'),
|
||||||
path('delete_profile_picture/', views.delete_profile_picture, name='delete_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:
|
if settings.DEBUG:
|
||||||
|
|
|
@ -12,7 +12,7 @@ from django.contrib.auth import logout
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.views import PasswordChangeView
|
from django.contrib.auth.views import PasswordChangeView
|
||||||
from .forms import UsernameChangeForm, DocumentForm, EventForm, NoteForm, TaskForm
|
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 django.contrib import messages
|
||||||
from allauth.account.views import LoginView as AllauthLoginView
|
from allauth.account.views import LoginView as AllauthLoginView
|
||||||
import os
|
import os
|
||||||
|
@ -52,6 +52,20 @@ import selenium
|
||||||
import pyclamd
|
import pyclamd
|
||||||
from datetime import datetime, timedelta
|
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):
|
def scan_file_with_clamav(file_path):
|
||||||
try:
|
try:
|
||||||
cd = pyclamd.ClamdNetworkSocket(host='127.0.0.1', port=3310)
|
cd = pyclamd.ClamdNetworkSocket(host='127.0.0.1', port=3310)
|
||||||
|
@ -176,8 +190,7 @@ def upload_file(request):
|
||||||
file_size = document.file.size
|
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()
|
request.session.flush()
|
||||||
logout(request)
|
logout(request)
|
||||||
return HttpResponse("<html><body><script>alert('Uploading possibly malicious files is forbidden. You have been logged out.'); location.reload();</script></body></html>")
|
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(
|
messages = Message.objects.filter(
|
||||||
Q(sender=request.user, receiver=other_user) |
|
Q(sender=request.user, receiver=other_user) |
|
||||||
Q(sender=other_user, receiver=request.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()
|
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())
|
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:
|
try:
|
||||||
# Set up Chrome options
|
# Set up Chrome options
|
||||||
chrome_options = Options()
|
chrome_options = Options()
|
||||||
chrome_options.add_argument('--headless=new')
|
for arg in SELENIUM_CHROME_ARGS:
|
||||||
chrome_options.add_argument('--no-sandbox')
|
chrome_options.add_argument(arg)
|
||||||
chrome_options.add_argument('--disable-dev-shm-usage')
|
chrome_options.add_experimental_option("prefs", SELENIUM_CHROME_PREFS)
|
||||||
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,
|
|
||||||
})
|
|
||||||
# Initialize Chrome driver
|
# Initialize Chrome driver
|
||||||
driver = webdriver.Chrome(options=chrome_options)
|
driver = webdriver.Chrome(options=chrome_options)
|
||||||
url = form.cleaned_data['url']
|
url = form.cleaned_data['url']
|
||||||
|
@ -1371,3 +1378,39 @@ def reorder_embeds(request):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
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)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue