unread notifs

This commit is contained in:
2025-05-31 23:08:38 +02:00
parent 08a11c240d
commit 779e81346b
20 changed files with 153 additions and 67 deletions

View File

@@ -1,9 +1,18 @@
from flask import render_template, request, flash, redirect, url_for from flask import render_template, request, flash, redirect, url_for, Blueprint, jsonify
from flask_login import login_user, logout_user, login_required, current_user from flask_login import login_user, logout_user, login_required, current_user
from models import db, User from models import db, User, Notif
from functools import wraps from functools import wraps
from datetime import datetime from datetime import datetime
from utils import log_event, create_notification from utils import log_event, create_notification, get_unread_count
auth_bp = Blueprint('auth', __name__)
@auth_bp.context_processor
def inject_unread_notifications():
if current_user.is_authenticated:
unread_count = get_unread_count(current_user.id)
return {'unread_notifications': unread_count}
return {'unread_notifications': 0}
def require_password_change(f): def require_password_change(f):
@wraps(f) @wraps(f)

View File

@@ -1,11 +1,11 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from models import db, User from models import db, User, Notif
from forms import UserForm from forms import UserForm
from flask import abort from flask import abort
from sqlalchemy import or_ from sqlalchemy import or_
from routes.auth import require_password_change from routes.auth import require_password_change
from utils import log_event, create_notification from utils import log_event, create_notification, get_unread_count
import json import json
import os import os
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -17,6 +17,13 @@ UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics')
if not os.path.exists(UPLOAD_FOLDER): if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER) os.makedirs(UPLOAD_FOLDER)
@contacts_bp.context_processor
def inject_unread_notifications():
if current_user.is_authenticated:
unread_count = get_unread_count(current_user.id)
return {'unread_notifications': unread_count}
return {'unread_notifications': 0}
def admin_required(): def admin_required():
if not current_user.is_authenticated: if not current_user.is_authenticated:
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))

View File

@@ -3,7 +3,7 @@ from flask_login import login_required, current_user
from models import db, Conversation, User, Message, MessageAttachment from models import db, Conversation, User, Message, MessageAttachment
from forms import ConversationForm from forms import ConversationForm
from routes.auth import require_password_change from routes.auth import require_password_change
from utils import log_event, create_notification from utils import log_event, create_notification, get_unread_count
import os import os
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from datetime import datetime from datetime import datetime
@@ -54,7 +54,8 @@ def conversations():
if search: if search:
query = query.filter(Conversation.name.ilike(f'%{search}%')) query = query.filter(Conversation.name.ilike(f'%{search}%'))
conversations = query.order_by(Conversation.created_at.desc()).all() conversations = query.order_by(Conversation.created_at.desc()).all()
return render_template('conversations/conversations.html', conversations=conversations, search=search) unread_count = get_unread_count(current_user.id)
return render_template('conversations/conversations.html', conversations=conversations, search=search, unread_notifications=unread_count)
@conversations_bp.route('/create', methods=['GET', 'POST']) @conversations_bp.route('/create', methods=['GET', 'POST'])
@login_required @login_required

View File

@@ -10,7 +10,7 @@ import logging
import sys import sys
import time import time
from forms import CompanySettingsForm from forms import CompanySettingsForm
from utils import log_event, create_notification from utils import log_event, create_notification, get_unread_count
# Set up logging to show in console # Set up logging to show in console
logging.basicConfig( logging.basicConfig(
@@ -28,6 +28,13 @@ def init_routes(main_bp):
site_settings = SiteSettings.query.first() site_settings = SiteSettings.query.first()
return dict(site_settings=site_settings) return dict(site_settings=site_settings)
@main_bp.context_processor
def inject_unread_notifications():
if current_user.is_authenticated:
unread_count = get_unread_count(current_user.id)
return {'unread_notifications': unread_count}
return {'unread_notifications': 0}
@main_bp.route('/') @main_bp.route('/')
@login_required @login_required
@require_password_change @require_password_change

View File

@@ -22,14 +22,14 @@ to maintain file metadata and content.
from flask import Blueprint, jsonify, request, abort, send_from_directory, send_file from flask import Blueprint, jsonify, request, abort, send_from_directory, send_file
from flask_login import login_required, current_user from flask_login import login_required, current_user
import os import os
from models import Room, RoomMemberPermission, RoomFile, TrashedFile, db from models import Room, RoomMemberPermission, RoomFile, TrashedFile, db, User, Notif
from werkzeug.utils import secure_filename, safe_join from werkzeug.utils import secure_filename, safe_join
import time import time
import shutil import shutil
import io import io
import zipfile import zipfile
from datetime import datetime from datetime import datetime
from utils import log_event from utils import log_event, create_notification, get_unread_count
# Blueprint for room file operations # Blueprint for room file operations
room_files_bp = Blueprint('room_files', __name__, url_prefix='/api/rooms') room_files_bp = Blueprint('room_files', __name__, url_prefix='/api/rooms')
@@ -61,6 +61,13 @@ ALLOWED_EXTENSIONS = {
'eml', 'msg', 'vcf', 'ics' 'eml', 'msg', 'vcf', 'ics'
} }
@room_files_bp.context_processor
def inject_unread_notifications():
if current_user.is_authenticated:
unread_count = get_unread_count(current_user.id)
return {'unread_notifications': unread_count}
return {'unread_notifications': 0}
def get_room_dir(room_id): def get_room_dir(room_id):
""" """
Get the absolute path to a room's directory. Get the absolute path to a room's directory.

View File

@@ -1,10 +1,18 @@
from flask import Blueprint, jsonify, request, abort from flask import Blueprint, jsonify, request, abort, render_template, redirect, url_for, flash
from flask_login import login_required, current_user from flask_login import login_required, current_user
from models import db, Room, User, RoomMemberPermission from models import db, Room, User, RoomMemberPermission, Notif
from utils import user_has_permission, log_event, create_notification from utils import user_has_permission, log_event, create_notification, get_unread_count
from routes.auth import require_password_change
from datetime import datetime from datetime import datetime
room_members_bp = Blueprint('room_members', __name__) room_members_bp = Blueprint('room_members', __name__, url_prefix='/api/rooms')
@room_members_bp.context_processor
def inject_unread_notifications():
if current_user.is_authenticated:
unread_count = get_unread_count(current_user.id)
return {'unread_notifications': unread_count}
return {'unread_notifications': 0}
@room_members_bp.route('/<int:room_id>/members', methods=['GET']) @room_members_bp.route('/<int:room_id>/members', methods=['GET'])
@login_required @login_required

View File

@@ -1,16 +1,23 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from models import db, Room, User, RoomMemberPermission, RoomFile from models import db, Room, User, RoomMemberPermission, RoomFile, Notif
from forms import RoomForm from forms import RoomForm
from routes.room_files import user_has_permission from routes.room_files import user_has_permission
from routes.auth import require_password_change from routes.auth import require_password_change
from utils import log_event, create_notification from utils import log_event, create_notification, get_unread_count
import os import os
import shutil import shutil
from datetime import datetime from datetime import datetime
rooms_bp = Blueprint('rooms', __name__, url_prefix='/rooms') rooms_bp = Blueprint('rooms', __name__, url_prefix='/rooms')
@rooms_bp.context_processor
def inject_unread_notifications():
if current_user.is_authenticated:
unread_count = get_unread_count(current_user.id)
return {'unread_notifications': unread_count}
return {'unread_notifications': 0}
@rooms_bp.route('/') @rooms_bp.route('/')
@login_required @login_required
@require_password_change @require_password_change

View File

@@ -1,11 +1,19 @@
from flask import Blueprint, jsonify, request, abort from flask import Blueprint, jsonify, request, abort, render_template, redirect, url_for, flash
from flask_login import login_required, current_user from flask_login import login_required, current_user
from models import db, Room, RoomFile, TrashedFile, UserStarredFile from models import db, Room, RoomFile, TrashedFile, UserStarredFile, Notif
from utils import user_has_permission, clean_path, log_event from routes.auth import require_password_change
from utils import user_has_permission, clean_path, log_event, create_notification, get_unread_count
import os import os
from datetime import datetime from datetime import datetime
trash_bp = Blueprint('trash', __name__) trash_bp = Blueprint('trash', __name__, url_prefix='/trash')
@trash_bp.context_processor
def inject_unread_notifications():
if current_user.is_authenticated:
unread_count = get_unread_count(current_user.id)
return {'unread_notifications': unread_count}
return {'unread_notifications': 0}
@trash_bp.route('/<int:room_id>/trash', methods=['GET']) @trash_bp.route('/<int:room_id>/trash', methods=['GET'])
@login_required @login_required

View File

@@ -1,4 +1,10 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Initialize tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize variables // Initialize variables
let currentPage = 1; let currentPage = 1;
let totalPages = parseInt(document.getElementById('totalPages').textContent) || 1; let totalPages = parseInt(document.getElementById('totalPages').textContent) || 1;
@@ -62,6 +68,12 @@ document.addEventListener('DOMContentLoaded', function() {
const data = await response.json(); const data = await response.json();
updateNotificationsTable(data.notifications); updateNotificationsTable(data.notifications);
updatePagination(data.total_pages, data.current_page); updatePagination(data.total_pages, data.current_page);
// Reinitialize tooltips after updating the table
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
} catch (error) { } catch (error) {
console.error('Error fetching notifications:', error); console.error('Error fetching notifications:', error);
notifsTableBody.innerHTML = '<tr><td colspan="6" class="text-center">Error loading notifications</td></tr>'; notifsTableBody.innerHTML = '<tr><td colspan="6" class="text-center">Error loading notifications</td></tr>';
@@ -104,6 +116,11 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Function to update notification counter
function updateNotificationCounter() {
counter.textContent = document.querySelector('.nav-link[href*="notifications"] .badge');
}
// Function to mark notification as read // Function to mark notification as read
async function markAsRead(notifId) { async function markAsRead(notifId) {
try { try {
@@ -138,6 +155,8 @@ document.addEventListener('DOMContentLoaded', function() {
} }
} }
} }
// Update the notification counter
updateNotificationCounter();
} }
} catch (error) { } catch (error) {
console.error('Error marking notification as read:', error); console.error('Error marking notification as read:', error);
@@ -165,6 +184,10 @@ document.addEventListener('DOMContentLoaded', function() {
// Remove the notification row from the table // Remove the notification row from the table
const notifRow = document.querySelector(`tr[data-notif-id="${notifId}"]`); const notifRow = document.querySelector(`tr[data-notif-id="${notifId}"]`);
if (notifRow) { if (notifRow) {
// Only update counter if the notification was unread
if (notifRow.classList.contains('table-warning')) {
updateNotificationCounter();
}
notifRow.remove(); notifRow.remove();
} }
} }
@@ -206,6 +229,11 @@ document.addEventListener('DOMContentLoaded', function() {
} }
} }
}); });
// Remove the notification counter
const counter = document.querySelector('.nav-link[href*="notifications"] .badge');
if (counter) {
counter.remove();
}
} }
} catch (error) { } catch (error) {
console.error('Error marking all notifications as read:', error); console.error('Error marking all notifications as read:', error);
@@ -260,28 +288,30 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Add event listeners for filters with debounce // Initialize event listeners
let filterTimeout; attachEventListeners();
function debouncedFetch() {
clearTimeout(filterTimeout); // Add event listeners for filters
filterTimeout = setTimeout(() => { notifTypeFilter.addEventListener('change', () => {
currentPage = 1; // Reset to first page when filters change currentPage = 1;
fetchNotifications(); fetchNotifications();
}, 300); updateURL();
} });
notifTypeFilter.addEventListener('change', debouncedFetch); dateRangeFilter.addEventListener('change', () => {
dateRangeFilter.addEventListener('change', debouncedFetch); currentPage = 1;
fetchNotifications();
updateURL();
});
// Add event listener for clear filters
clearFiltersBtn.addEventListener('click', () => { clearFiltersBtn.addEventListener('click', () => {
notifTypeFilter.value = ''; notifTypeFilter.value = '';
dateRangeFilter.value = '7d'; dateRangeFilter.value = '7d';
currentPage = 1; currentPage = 1;
fetchNotifications(); fetchNotifications();
updateURL();
}); });
// Add event listener for mark all as read
markAllReadBtn.addEventListener('click', markAllAsRead); markAllReadBtn.addEventListener('click', markAllAsRead);
// Add event listeners for pagination // Add event listeners for pagination
@@ -289,6 +319,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (currentPage > 1) { if (currentPage > 1) {
currentPage--; currentPage--;
fetchNotifications(); fetchNotifications();
updateURL();
} }
}); });
@@ -296,20 +327,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (currentPage < totalPages) { if (currentPage < totalPages) {
currentPage++; currentPage++;
fetchNotifications(); fetchNotifications();
updateURL();
} }
}); });
// Initialize filters from URL parameters
const params = new URLSearchParams(window.location.search);
notifTypeFilter.value = params.get('notif_type') || '';
dateRangeFilter.value = params.get('date_range') || '7d';
currentPage = parseInt(params.get('page')) || 1;
// Initial fetch if filters are set
if (notifTypeFilter.value || dateRangeFilter.value !== '7d') {
fetchNotifications();
}
// Attach initial event listeners
attachEventListeners();
}); });

View File

@@ -32,8 +32,13 @@
</a> </a>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="d-none d-lg-flex align-items-center me-3"> <div class="d-none d-lg-flex align-items-center me-3">
<a class="nav-link text-white" href="{{ url_for('main.notifications') }}"> <a class="nav-link text-white position-relative" href="{{ url_for('main.notifications') }}">
<i class="fas fa-bell text-xl" style="width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center;"></i> <i class="fas fa-bell text-xl" style="width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center;"></i>
{% if unread_notifications > 0 %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
{{ unread_notifications }}
</span>
{% endif %}
</a> </a>
</div> </div>
<div class="dropdown"> <div class="dropdown">

View File

@@ -50,7 +50,6 @@
<th>Timestamp</th> <th>Timestamp</th>
<th>Type</th> <th>Type</th>
<th>From</th> <th>From</th>
<th>Details</th>
<th>Status</th> <th>Status</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
@@ -84,14 +83,6 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ notif.sender.username + ' ' + notif.sender.last_name if notif.sender else 'System' }}</td> <td>{{ notif.sender.username + ' ' + notif.sender.last_name if notif.sender else 'System' }}</td>
<td>
<button class="btn btn-sm btn-secondary"
data-bs-toggle="modal"
data-bs-target="#notifDetailsModal"
data-notif-id="{{ notif.id }}">
<i class="fas fa-info-circle"></i> View Details
</button>
</td>
<td> <td>
{% if notif.read %} {% if notif.read %}
<span class="badge bg-success">Read</span> <span class="badge bg-success">Read</span>
@@ -103,21 +94,39 @@
<div class="btn-group"> <div class="btn-group">
{% if notif.notif_type in ['room_invite', 'room_invite_removed'] and notif.details and notif.details.room_id %} {% if notif.notif_type in ['room_invite', 'room_invite_removed'] and notif.details and notif.details.room_id %}
<a href="{{ url_for('rooms.room', room_id=notif.details.room_id) }}" <a href="{{ url_for('rooms.room', room_id=notif.details.room_id) }}"
class="btn btn-sm btn-primary"> class="btn btn-sm btn-primary"
<i class="fas fa-door-open"></i> View Room data-bs-toggle="tooltip"
title="View Room">
<i class="fas fa-door-open"></i>
</a> </a>
{% elif notif.notif_type in ['conversation_invite', 'conversation_invite_removed', 'conversation_message'] and notif.details and notif.details.conversation_id %} {% elif notif.notif_type in ['conversation_invite', 'conversation_invite_removed', 'conversation_message'] and notif.details and notif.details.conversation_id %}
<a href="{{ url_for('conversations.conversation', conversation_id=notif.details.conversation_id) }}" <a href="{{ url_for('conversations.conversation', conversation_id=notif.details.conversation_id) }}"
class="btn btn-sm btn-primary"> class="btn btn-sm btn-primary"
<i class="fas fa-comments"></i> View Conversation data-bs-toggle="tooltip"
title="View Conversation">
<i class="fas fa-comments"></i>
</a> </a>
{% endif %} {% endif %}
<button class="btn btn-sm btn-secondary"
data-bs-toggle="modal"
data-bs-target="#notifDetailsModal"
data-notif-id="{{ notif.id }}"
data-bs-toggle="tooltip"
title="View Details">
<i class="fas fa-info-circle"></i>
</button>
{% if not notif.read %} {% if not notif.read %}
<button class="btn btn-sm btn-success mark-read" data-notif-id="{{ notif.id }}"> <button class="btn btn-sm btn-success mark-read"
<i class="fas fa-check"></i> Mark as Read data-notif-id="{{ notif.id }}"
data-bs-toggle="tooltip"
title="Mark as Read">
<i class="fas fa-check"></i>
</button> </button>
{% endif %} {% endif %}
<button class="btn btn-sm btn-danger delete-notif" data-notif-id="{{ notif.id }}"> <button class="btn btn-sm btn-danger delete-notif"
data-notif-id="{{ notif.id }}"
data-bs-toggle="tooltip"
title="Delete">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</div> </div>
@@ -126,7 +135,7 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
<tr> <tr>
<td colspan="6" class="text-center">No notifications found</td> <td colspan="5" class="text-center">No notifications found</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>