Initial implementation of notification icon.

This commit is contained in:
Ben Rog-Wilhelm 2023-08-27 10:56:16 -05:00
parent 61cecf5a70
commit 6fccfb2436
5 changed files with 92 additions and 4 deletions

View file

@ -20,6 +20,7 @@ enum ChatHandlers {
TYPING = "typing",
DELETE = "delete",
SPEAK = "speak",
READ = "read",
}
interface ChatProviderContext {
@ -65,6 +66,11 @@ export function ChatProvider({ children }: PropsWithChildren) {
const [notifications, setNotifications] = useState<number>(0);
const [messageLookup, setMessageLookup] = useState({});
const setMessagesAndRead = useCallback((messages: IChatMessage[]) => {
setMessages(messages);
trySendReadMessage();
}, []);
const addMessage = useCallback((message: IChatMessage) => {
if (message.id === OPTIMISTIC_MESSAGE_ID) {
setMessages((prev) => prev.concat(message));
@ -139,6 +145,23 @@ export function ChatProvider({ children }: PropsWithChildren) {
} catch (error) {}
}, []);
const [lastMaxTime, setLastMaxTime] = useState<number | null>(null);
const trySendReadMessage = useCallback(() => {
if (messages.length === 0) {
return; // Exit if the messages array is empty
}
if (document.hasFocus()) {
const maxTime = Math.max(...messages.map(msg => msg.time));
if (maxTime !== lastMaxTime) { // Only emit if there's a new maxTime
setLastMaxTime(maxTime); // Update the stored maxTime
socket.current?.emit(ChatHandlers.READ, maxTime);
}
}
}, [messages, lastMaxTime]);
const context = useMemo<ChatProviderContext>(
() => ({
online,
@ -170,7 +193,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
socket.current = io();
socket.current
.on(ChatHandlers.CATCHUP, setMessages)
.on(ChatHandlers.CATCHUP, setMessagesAndRead)
.on(ChatHandlers.ONLINE, setOnline)
.on(ChatHandlers.TYPING, setTyping)
.on(ChatHandlers.SPEAK, addMessage)
@ -193,6 +216,8 @@ export function ChatProvider({ children }: PropsWithChildren) {
}, [draft]);
useEffect(() => {
trySendReadMessage();
if (focused || document.hasFocus()) {
setNotifications(0);
}

View file

@ -4874,6 +4874,7 @@ img.golden, img[g] {
.fa-x:before{content:"\58"}
.fa-scale-balanced:before{content:"\f24e"}
.fa-hippo:before{content:"\f6ed"}
.fa-booth:before{content:"\f734"}
.awards-wrapper input[type="radio"] {
display: none;

View file

@ -12,6 +12,7 @@ from files.classes.alts import Alt
from files.classes.award import AwardRelationship
from files.classes.badges import Badge
from files.classes.base import CreatedBase
from files.classes.chat_message import ChatMessage
from files.classes.clients import * # note: imports Comment and Submission
from files.classes.follows import Follow
from files.classes.mod_logs import ModAction
@ -162,6 +163,30 @@ class User(CreatedBase):
def can_manage_reports(self):
return self.admin_level > 1
@property
@lazy
def can_access_chat(self):
if self.is_suspended_permanently:
return False
if self.admin_level >= PERMS['CHAT_FULL_CONTROL']:
return True
if self.chat_authorized:
return True
return False
@property
@lazy
def unread_chat_messages_count(self):
if not self.can_access_chat:
return 0 # return 0 if the user can't access chat
# Query for all chat messages that are newer than the user's last seen timestamp
unread_messages_count = g.db.query(ChatMessage)\
.filter(ChatMessage.created_datetimez > self.chat_lastseen)\
.count()
return unread_messages_count
@property
def age_days(self):
return (datetime.now() - datetime.fromtimestamp(self.created_utc)).days

View file

@ -18,9 +18,11 @@ def chat_is_allowed(perm_level: int=0):
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> bool | None:
v = get_logged_in_user()
if not v or v.is_suspended_permanently or v.admin_level < perm_level:
if not v:
return abort(403)
if v.admin_level < PERMS['CHAT_FULL_CONTROL'] and not v.chat_authorized:
if not v.can_access_chat:
return abort(403)
if v.admin_level < perm_level:
return abort(403)
kwargs['v'] = v
return func(*args, **kwargs)
@ -73,7 +75,7 @@ def send_system_reply(text):
"username": "System",
"text": text,
"text_html": sanitize(text),
'time': int(self.created_datetimez.timestamp()),
'time': time.time(),
}
emit('speak', data)
@ -188,6 +190,21 @@ def typing_indicator(data, v):
emit('typing', typing, broadcast=True)
@socketio.on('read')
@chat_is_allowed()
def read(data, v):
limiter.check()
if v.is_banned: return '', 403
# This value gets truncated at some point in the pipeline and I haven't really spent time to figure out where.
# Instead, we just bump it by one.
timestamp = datetime.fromtimestamp(int(data) + 1)
v.chat_lastseen = timestamp
g.db.add(v)
g.db.commit()
@socketio.on('delete')
@chat_is_allowed(PERMS['CHAT_MODERATION'])
def delete(id, v):

View file

@ -25,6 +25,14 @@
<a class="mobile-nav-icon d-md-none" onclick="location.reload()"><i class="fas fa-arrow-rotate-right align-middle text-gray-500 black"></i></a>
{% endif %}
{% if v.can_access_chat %}
{% if v.unread_chat_messages_count > 0 %}
<a class="mobile-nav-icon d-md-none pl-0" href="/chat" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Voting Booth"><i class="fas fa-booth align-middle text-danger"></i><span class="notif-count ml-1" style="padding-left: 4.5px;">{{v.unread_chat_messages_count}}</span></a>
{% else %}
<a class="mobile-nav-icon d-md-none" href="/chat" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Voting Booth"><i class="fas fa-booth align-middle text-gray-500 black"></i></a>
{% endif %}
{% endif %}
{% if v %}
{% if v.notifications_count %}
<a class="mobile-nav-icon d-md-none pl-0" href="/notifications{% if v.do_posts %}/posts{% endif %}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Notifications"><i class="fas fa-bell align-middle text-danger" {% if v.do_posts %}style="color:blue!important"{% endif %}></i><span class="notif-count ml-1" style="padding-left: 4.5px;{% if v.do_posts %}background:blue{% endif %}">{{v.notifications_count}}</span></a>
@ -53,6 +61,18 @@
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto d-none d-md-flex">
{% if v.can_access_chat %}
{% if v.unread_chat_messages_count > 0 %}
<li class="nav-item d-flex align-items-center text-center justify-content-center mx-1">
<a class="nav-link position-relative" href="/chat" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Voting Booth"><i class="fas fa-booth text-danger"></i><span class="notif-count ml-1" style="padding-left: 4.5px;">{{v.unread_chat_messages_count}}</span></a>
</li>
{% else %}
<li class="nav-item d-flex align-items-center text-center justify-content-center mx-1">
<a class="nav-link" href="/chat" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Voting Booth"><i class="fas fa-booth"></i></a>
</li>
{% endif %}
{% endif %}
{% if v %}
{% if v.notifications_count %}