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 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)

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 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'))

View File

@@ -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

View File

@@ -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

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_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.

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 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('/<int:room_id>/members', methods=['GET'])
@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 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

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 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('/<int:room_id>/trash', methods=['GET'])
@login_required

View File

@@ -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 = '<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
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
// Initialize event listeners
attachEventListeners();
// Add event listeners for filters
notifTypeFilter.addEventListener('change', () => {
currentPage = 1;
fetchNotifications();
}, 300);
}
updateURL();
});
notifTypeFilter.addEventListener('change', debouncedFetch);
dateRangeFilter.addEventListener('change', debouncedFetch);
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();
});

View File

@@ -32,8 +32,13 @@
</a>
<div class="d-flex align-items-center">
<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>
{% 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>
</div>
<div class="dropdown">

View File

@@ -50,7 +50,6 @@
<th>Timestamp</th>
<th>Type</th>
<th>From</th>
<th>Details</th>
<th>Status</th>
<th>Actions</th>
</tr>
@@ -84,14 +83,6 @@
{% endif %}
</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>
{% if notif.read %}
<span class="badge bg-success">Read</span>
@@ -103,21 +94,39 @@
<div class="btn-group">
{% 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) }}"
class="btn btn-sm btn-primary">
<i class="fas fa-door-open"></i> View Room
class="btn btn-sm btn-primary"
data-bs-toggle="tooltip"
title="View Room">
<i class="fas fa-door-open"></i>
</a>
{% 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) }}"
class="btn btn-sm btn-primary">
<i class="fas fa-comments"></i> View Conversation
class="btn btn-sm btn-primary"
data-bs-toggle="tooltip"
title="View Conversation">
<i class="fas fa-comments"></i>
</a>
{% 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 %}
<button class="btn btn-sm btn-success mark-read" data-notif-id="{{ notif.id }}">
<i class="fas fa-check"></i> Mark as Read
<button class="btn btn-sm btn-success mark-read"
data-notif-id="{{ notif.id }}"
data-bs-toggle="tooltip"
title="Mark as Read">
<i class="fas fa-check"></i>
</button>
{% 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>
</button>
</div>
@@ -126,7 +135,7 @@
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="text-center">No notifications found</td>
<td colspan="5" class="text-center">No notifications found</td>
</tr>
{% endif %}
</tbody>