From 9c296644f7a02670761688ea49de7f5917b423c4 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Thu, 19 Mar 2026 12:18:42 +0100 Subject: [PATCH] feat(messages): add Quill rich text editor with inline image paste - Replace plain textarea with Quill editor in compose and reply forms - Support Ctrl+V paste of screenshots directly into message body - Image toolbar button for file picker upload - New endpoint POST /api/messages/upload-image for inline images - Content sanitized via sanitize_html (bleach) with img tag support - Messages rendered as HTML (|safe) instead of plain text - Links clickable, images displayed inline in message body Co-Authored-By: Claude Opus 4.6 (1M context) --- blueprints/messages/routes.py | 43 +++++++++++- templates/messages/compose.html | 115 +++++++++++++++++++++++++++++- templates/messages/view.html | 120 ++++++++++++++++++++++++++++++-- utils/helpers.py | 4 +- 4 files changed, 270 insertions(+), 12 deletions(-) diff --git a/blueprints/messages/routes.py b/blueprints/messages/routes.py index 6c0af51..030fb95 100644 --- a/blueprints/messages/routes.py +++ b/blueprints/messages/routes.py @@ -14,7 +14,7 @@ from . import bp from sqlalchemy.orm import joinedload from database import SessionLocal, User, Company, UserCompanyPermissions, PrivateMessage, UserNotification, UserBlock, Classified from extensions import limiter -from utils.helpers import sanitize_input +from utils.helpers import sanitize_input, sanitize_html from utils.decorators import member_required from email_service import send_email, build_message_notification_email from message_upload_service import MessageUploadService @@ -169,7 +169,7 @@ def messages_send(): """Wyślij wiadomość""" recipient_id = request.form.get('recipient_id', type=int) subject = sanitize_input(request.form.get('subject', ''), 255) - content = request.form.get('content', '').strip() + content = sanitize_html(request.form.get('content', '').strip()) context_type = request.form.get('context_type') context_id = request.form.get('context_id', type=int) @@ -341,12 +341,49 @@ def messages_view(message_id): db.close() +@bp.route('/api/messages/upload-image', methods=['POST']) +@login_required +@member_required +def messages_upload_image(): + """Upload inline image for Quill editor in messages.""" + import os + import uuid + from datetime import datetime as dt + from werkzeug.utils import secure_filename + + file = request.files.get('image') + if not file or not file.filename: + return jsonify({'error': 'Brak pliku'}), 400 + + filename = secure_filename(file.filename) + ext = os.path.splitext(filename)[1].lower() + if ext not in ('.jpg', '.jpeg', '.png', '.gif', '.webp'): + return jsonify({'error': 'Dozwolone formaty: JPG, PNG, GIF, WebP'}), 400 + + # Read and check size (max 5MB) + data = file.read() + if len(data) > 5 * 1024 * 1024: + return jsonify({'error': 'Maksymalny rozmiar obrazka: 5 MB'}), 400 + + # Save to uploads + now = dt.now() + upload_dir = os.path.join('static', 'uploads', 'messages', str(now.year), f'{now.month:02d}') + os.makedirs(upload_dir, exist_ok=True) + stored_name = f'{uuid.uuid4().hex}{ext}' + filepath = os.path.join(upload_dir, stored_name) + with open(filepath, 'wb') as f: + f.write(data) + + url = f'/{filepath}' + return jsonify({'url': url}) + + @bp.route('/wiadomosci//odpowiedz', methods=['POST']) @login_required @member_required def messages_reply(message_id): """Odpowiedz na wiadomość""" - content = request.form.get('content', '').strip() + content = sanitize_html(request.form.get('content', '').strip()) if not content: flash('Treść jest wymagana.', 'error') diff --git a/templates/messages/compose.html b/templates/messages/compose.html index 1b70239..f18d846 100755 --- a/templates/messages/compose.html +++ b/templates/messages/compose.html @@ -2,8 +2,35 @@ {% block title %}Nowa wiadomosc - Norda Biznes Partner{% endblock %} +{% block head_extra %} + + +{% endblock %} + {% block extra_css %}