diff --git a/routes/__pycache__/admin.cpython-313.pyc b/routes/__pycache__/admin.cpython-313.pyc index c055c5f..b5da3be 100644 Binary files a/routes/__pycache__/admin.cpython-313.pyc and b/routes/__pycache__/admin.cpython-313.pyc differ diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index 608081d..a08b475 100644 Binary files a/routes/__pycache__/auth.cpython-313.pyc and b/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index e9f119d..5033b2b 100644 Binary files a/routes/__pycache__/contacts.cpython-313.pyc and b/routes/__pycache__/contacts.cpython-313.pyc differ diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index 732b9f5..719e543 100644 Binary files a/routes/__pycache__/conversations.cpython-313.pyc and b/routes/__pycache__/conversations.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 45114d4..eeb0d92 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/__pycache__/room_files.cpython-313.pyc b/routes/__pycache__/room_files.cpython-313.pyc index 4e92dc2..32e3c33 100644 Binary files a/routes/__pycache__/room_files.cpython-313.pyc and b/routes/__pycache__/room_files.cpython-313.pyc differ diff --git a/routes/__pycache__/room_members.cpython-313.pyc b/routes/__pycache__/room_members.cpython-313.pyc index 1a7cb40..1f599c7 100644 Binary files a/routes/__pycache__/room_members.cpython-313.pyc and b/routes/__pycache__/room_members.cpython-313.pyc differ diff --git a/routes/__pycache__/rooms.cpython-313.pyc b/routes/__pycache__/rooms.cpython-313.pyc index c2ec82d..4dc7618 100644 Binary files a/routes/__pycache__/rooms.cpython-313.pyc and b/routes/__pycache__/rooms.cpython-313.pyc differ diff --git a/routes/__pycache__/trash.cpython-313.pyc b/routes/__pycache__/trash.cpython-313.pyc index 56e026c..dc05ee3 100644 Binary files a/routes/__pycache__/trash.cpython-313.pyc and b/routes/__pycache__/trash.cpython-313.pyc differ diff --git a/routes/auth.py b/routes/auth.py index 940b815..44b6db4 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -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 models import db, User +from models import db, User, Notif from functools import wraps 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): @wraps(f) diff --git a/routes/contacts.py b/routes/contacts.py index 1c8c7c1..bf2a3a5 100644 --- a/routes/contacts.py +++ b/routes/contacts.py @@ -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 models import db, User +from models import db, User, Notif from forms import UserForm from flask import abort from sqlalchemy import or_ 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 os 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): 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(): if not current_user.is_authenticated: return redirect(url_for('auth.login')) diff --git a/routes/conversations.py b/routes/conversations.py index e411d6c..12031f3 100644 --- a/routes/conversations.py +++ b/routes/conversations.py @@ -3,7 +3,7 @@ from flask_login import login_required, current_user from models import db, Conversation, User, Message, MessageAttachment from forms import ConversationForm 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 from werkzeug.utils import secure_filename from datetime import datetime @@ -54,7 +54,8 @@ def conversations(): if search: query = query.filter(Conversation.name.ilike(f'%{search}%')) 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']) @login_required diff --git a/routes/main.py b/routes/main.py index c945934..fbcc644 100644 --- a/routes/main.py +++ b/routes/main.py @@ -10,7 +10,7 @@ import logging import sys import time 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 logging.basicConfig( @@ -28,6 +28,13 @@ def init_routes(main_bp): site_settings = SiteSettings.query.first() 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('/') @login_required @require_password_change diff --git a/routes/room_files.py b/routes/room_files.py index 72849b0..1a5b194 100644 --- a/routes/room_files.py +++ b/routes/room_files.py @@ -22,14 +22,14 @@ to maintain file metadata and content. from flask import Blueprint, jsonify, request, abort, send_from_directory, send_file from flask_login import login_required, current_user 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 import time import shutil import io import zipfile from datetime import datetime -from utils import log_event +from utils import log_event, create_notification, get_unread_count # Blueprint for room file operations room_files_bp = Blueprint('room_files', __name__, url_prefix='/api/rooms') @@ -61,6 +61,13 @@ ALLOWED_EXTENSIONS = { '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): """ Get the absolute path to a room's directory. diff --git a/routes/room_members.py b/routes/room_members.py index bbeec56..5ac1d23 100644 --- a/routes/room_members.py +++ b/routes/room_members.py @@ -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 models import db, Room, User, RoomMemberPermission -from utils import user_has_permission, log_event, create_notification +from models import db, Room, User, RoomMemberPermission, Notif +from utils import user_has_permission, log_event, create_notification, get_unread_count +from routes.auth import require_password_change 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('//members', methods=['GET']) @login_required diff --git a/routes/rooms.py b/routes/rooms.py index 42c799b..80a4a04 100644 --- a/routes/rooms.py +++ b/routes/rooms.py @@ -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 models import db, Room, User, RoomMemberPermission, RoomFile +from models import db, Room, User, RoomMemberPermission, RoomFile, Notif from forms import RoomForm from routes.room_files import user_has_permission 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 shutil from datetime import datetime 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('/') @login_required @require_password_change diff --git a/routes/trash.py b/routes/trash.py index 9acaa0b..3b20d0a 100644 --- a/routes/trash.py +++ b/routes/trash.py @@ -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 models import db, Room, RoomFile, TrashedFile, UserStarredFile -from utils import user_has_permission, clean_path, log_event +from models import db, Room, RoomFile, TrashedFile, UserStarredFile, Notif +from routes.auth import require_password_change +from utils import user_has_permission, clean_path, log_event, create_notification, get_unread_count import os 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('//trash', methods=['GET']) @login_required diff --git a/static/js/notifications.js b/static/js/notifications.js index 933eeee..428f339 100644 --- a/static/js/notifications.js +++ b/static/js/notifications.js @@ -1,4 +1,10 @@ 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 let currentPage = 1; let totalPages = parseInt(document.getElementById('totalPages').textContent) || 1; @@ -62,6 +68,12 @@ document.addEventListener('DOMContentLoaded', function() { const data = await response.json(); updateNotificationsTable(data.notifications); 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) { console.error('Error fetching notifications:', error); notifsTableBody.innerHTML = 'Error loading notifications'; @@ -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 async function markAsRead(notifId) { try { @@ -138,6 +155,8 @@ document.addEventListener('DOMContentLoaded', function() { } } } + // Update the notification counter + updateNotificationCounter(); } } catch (error) { console.error('Error marking notification as read:', error); @@ -165,6 +184,10 @@ document.addEventListener('DOMContentLoaded', function() { // Remove the notification row from the table const notifRow = document.querySelector(`tr[data-notif-id="${notifId}"]`); if (notifRow) { + // Only update counter if the notification was unread + if (notifRow.classList.contains('table-warning')) { + updateNotificationCounter(); + } 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) { console.error('Error marking all notifications as read:', error); @@ -260,28 +288,30 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // Add event listeners for filters with debounce - let filterTimeout; - function debouncedFetch() { - clearTimeout(filterTimeout); - filterTimeout = setTimeout(() => { - currentPage = 1; // Reset to first page when filters change - fetchNotifications(); - }, 300); - } + // Initialize event listeners + attachEventListeners(); - notifTypeFilter.addEventListener('change', debouncedFetch); - dateRangeFilter.addEventListener('change', debouncedFetch); + // Add event listeners for filters + notifTypeFilter.addEventListener('change', () => { + currentPage = 1; + fetchNotifications(); + updateURL(); + }); + + dateRangeFilter.addEventListener('change', () => { + currentPage = 1; + fetchNotifications(); + updateURL(); + }); - // Add event listener for clear filters clearFiltersBtn.addEventListener('click', () => { notifTypeFilter.value = ''; dateRangeFilter.value = '7d'; currentPage = 1; fetchNotifications(); + updateURL(); }); - // Add event listener for mark all as read markAllReadBtn.addEventListener('click', markAllAsRead); // Add event listeners for pagination @@ -289,6 +319,7 @@ document.addEventListener('DOMContentLoaded', function() { if (currentPage > 1) { currentPage--; fetchNotifications(); + updateURL(); } }); @@ -296,20 +327,7 @@ document.addEventListener('DOMContentLoaded', function() { if (currentPage < totalPages) { currentPage++; 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(); }); \ No newline at end of file diff --git a/templates/common/base.html b/templates/common/base.html index 184d44a..49f27b6 100644 --- a/templates/common/base.html +++ b/templates/common/base.html @@ -32,8 +32,13 @@