zulip/web/src/message_notifications.js

399 lines
13 KiB
JavaScript

import $ from "jquery";
import * as alert_words from "./alert_words";
import * as blueslip from "./blueslip";
import * as desktop_notifications from "./desktop_notifications";
import {$t} from "./i18n";
import * as message_parser from "./message_parser";
import * as narrow from "./narrow";
import * as people from "./people";
import * as spoilers from "./spoilers";
import * as stream_data from "./stream_data";
import * as ui_util from "./ui_util";
import * as unread from "./unread";
import {user_settings} from "./user_settings";
import * as user_topics from "./user_topics";
function get_notification_content(message) {
let content;
// Convert the content to plain text, replacing emoji with their alt text
const $content = $("<div>").html(message.content);
ui_util.replace_emoji_with_text($content);
ui_util.change_katex_to_raw_latex($content);
ui_util.potentially_collapse_quotes($content);
spoilers.hide_spoilers_in_notification($content);
if (
$content.text().trim() === "" &&
(message_parser.message_has_image(message) ||
message_parser.message_has_attachment(message))
) {
content = $t({defaultMessage: "(attached file)"});
} else {
content = $content.text();
}
if (message.is_me_message) {
content = message.sender_full_name + content.slice(3);
}
if (
(message.type === "private" || message.type === "test-notification") &&
!user_settings.pm_content_in_desktop_notifications
) {
content = $t(
{defaultMessage: "New direct message from {sender_full_name}"},
{sender_full_name: message.sender_full_name},
);
}
return content;
}
function debug_notification_source_value(message) {
let notification_source;
if (message.type === "private" || message.type === "test-notification") {
notification_source = "pm";
} else if (message.mentioned) {
notification_source = "mention";
} else if (message.alerted) {
notification_source = "alert";
} else {
notification_source = "stream";
}
blueslip.debug("Desktop notification from source " + notification_source);
}
function get_notification_key(message) {
let key;
if (message.type === "private" || message.type === "test-notification") {
key = message.display_reply_to;
} else {
const stream_name = stream_data.get_stream_name_from_id(message.stream_id);
key = message.sender_full_name + " to " + stream_name + " > " + message.topic;
}
return key;
}
function remove_sender_from_list_of_recipients(message) {
return `, ${message.display_reply_to}, `
.replace(`, ${message.sender_full_name}, `, ", ")
.slice(", ".length, -", ".length);
}
function get_notification_title(message, msg_count) {
let title = message.sender_full_name;
let other_recipients;
if (msg_count > 1) {
title = msg_count + " messages from " + title;
}
switch (message.type) {
case "test-notification":
other_recipients = remove_sender_from_list_of_recipients(message);
break;
case "private":
other_recipients = remove_sender_from_list_of_recipients(message);
if (message.display_recipient.length > 2) {
// Character limit taken from https://www.pushengage.com/push-notification-character-limits
// We use a higher character limit so that the 3rd sender can at least be partially visible so that
// the user can distinguish the group DM.
// If the message has too many recipients to list them all...
if (title.length + other_recipients.length > 50) {
// Then count how many people are in the conversation and summarize
other_recipients = message.display_recipient.length - 2 + " more";
}
title += " (to you and " + other_recipients + ")";
} else {
title += " (to you)";
}
break;
case "stream": {
const stream_name = stream_data.get_stream_name_from_id(message.stream_id);
title += " (#" + stream_name + " > " + message.topic + ")";
break;
}
}
return title;
}
export function process_notification(notification) {
const message = notification.message;
const content = get_notification_content(message);
const key = get_notification_key(message);
let notification_object;
let msg_count = 1;
debug_notification_source_value(message);
if (desktop_notifications.notice_memory.has(key)) {
msg_count = desktop_notifications.notice_memory.get(key).msg_count + 1;
notification_object = desktop_notifications.notice_memory.get(key).obj;
notification_object.close();
}
const title = get_notification_title(message, msg_count);
if (notification.desktop_notify) {
const icon_url = people.small_avatar_url(message);
notification_object = new desktop_notifications.NotificationAPI(title, {
icon: icon_url,
body: content,
tag: message.id,
});
desktop_notifications.notice_memory.set(key, {
obj: notification_object,
msg_count,
message_id: message.id,
});
if (typeof notification_object.addEventListener === "function") {
// Sadly, some third-party Electron apps like Franz/Ferdi
// misimplement the Notification API not inheriting from
// EventTarget. This results in addEventListener being
// unavailable for them.
notification_object.addEventListener("click", () => {
notification_object.close();
if (message.type !== "test-notification") {
narrow.by_topic(message.id, {trigger: "notification"});
}
window.focus();
});
notification_object.addEventListener("close", () => {
desktop_notifications.notice_memory.delete(key);
});
}
}
}
export function message_is_notifiable(message) {
// Independent of the user's notification settings, are there
// properties of the message that unconditionally mean we
// shouldn't notify about it.
if (message.sent_by_me) {
return false;
}
// If a message is edited multiple times, we want to err on the side of
// not spamming notifications.
if (message.notification_sent) {
return false;
}
// @-<username> mentions take precedence over muted-ness. Note
// that @all mentions are still suppressed by muting.
if (message.mentioned_me_directly) {
return true;
}
// Messages to followed topics take precedence over muted-ness.
if (
message.type === "stream" &&
user_topics.is_topic_followed(message.stream_id, message.topic)
) {
return true;
}
// Messages to unmuted topics in muted streams may generate desktop notifications.
if (
message.type === "stream" &&
stream_data.is_muted(message.stream_id) &&
!user_topics.is_topic_unmuted(message.stream_id, message.topic)
) {
return false;
}
if (message.type === "stream" && user_topics.is_topic_muted(message.stream_id, message.topic)) {
return false;
}
// Everything else is on the table; next filter based on notification
// settings.
return true;
}
export function should_send_desktop_notification(message) {
// Always notify for testing notifications.
if (message.type === "test-notification") {
return true;
}
// For streams, send if desktop notifications are enabled for all
// message on this stream.
if (
message.type === "stream" &&
stream_data.receives_notifications(message.stream_id, "desktop_notifications")
) {
return true;
}
// enable_followed_topic_desktop_notifications determines whether we pop up
// a notification for messages in followed topics.
if (
message.type === "stream" &&
user_topics.is_topic_followed(message.stream_id, message.topic) &&
user_settings.enable_followed_topic_desktop_notifications
) {
return true;
}
// enable_desktop_notifications determines whether we pop up a
// notification for direct messages, mentions, and/or alerts.
if (!user_settings.enable_desktop_notifications) {
return false;
}
// And then we need to check if the message is a direct message,
// mention, wildcard mention with wildcard_mentions_notify, or alert.
if (message.type === "private") {
return true;
}
if (alert_words.notifies(message)) {
return true;
}
if (message.mentioned_me_directly) {
return true;
}
// The following blocks for 'wildcard mentions' and 'Followed topic wildcard mentions'
// should be placed below (as they are right now) the 'user_settings.enable_desktop_notifications'
// block because the global, stream-specific, and followed topic wildcard mention
// settings are wrappers around the personal-mention setting.
// wildcard mentions
if (
message.mentioned &&
stream_data.receives_notifications(message.stream_id, "wildcard_mentions_notify")
) {
return true;
}
// Followed topic wildcard mentions
if (
message.mentioned &&
user_topics.is_topic_followed(message.stream_id, message.topic) &&
user_settings.enable_followed_topic_wildcard_mentions_notify
) {
return true;
}
return false;
}
export function should_send_audible_notification(message) {
// If `None` is selected as the notification sound, never send
// audible notifications regardless of other configuration.
if (user_settings.notification_sound === "none") {
return false;
}
// For streams, ding if sounds are enabled for all messages on
// this stream.
if (
message.type === "stream" &&
stream_data.receives_notifications(message.stream_id, "audible_notifications")
) {
return true;
}
// enable_followed_topic_audible_notifications determines whether we ding
// for messages in followed topics.
if (
message.type === "stream" &&
user_topics.is_topic_followed(message.stream_id, message.topic) &&
user_settings.enable_followed_topic_audible_notifications
) {
return true;
}
// enable_sounds determines whether we ding for direct messages,
// mentions, and/or alerts.
if (!user_settings.enable_sounds) {
return false;
}
// And then we need to check if the message is a direct message,
// mention, wildcard mention with wildcard_mentions_notify, or alert.
if (message.type === "private" || message.type === "test-notification") {
return true;
}
if (alert_words.notifies(message)) {
return true;
}
if (message.mentioned_me_directly) {
return true;
}
// The following blocks for 'wildcard mentions' and 'Followed topic wildcard mentions'
// should be placed below (as they are right now) the 'user_settings.enable_sounds'
// block because the global, stream-specific, and followed topic wildcard mention
// settings are wrappers around the personal-mention setting.
// wildcard mentions
if (
message.mentioned &&
stream_data.receives_notifications(message.stream_id, "wildcard_mentions_notify")
) {
return true;
}
// Followed topic wildcard mentions
if (
message.mentioned &&
user_topics.is_topic_followed(message.stream_id, message.topic) &&
user_settings.enable_followed_topic_wildcard_mentions_notify
) {
return true;
}
return false;
}
export function received_messages(messages) {
for (const message of messages) {
if (!message_is_notifiable(message)) {
continue;
}
if (!unread.message_unread(message)) {
// The message is already read; Zulip is currently in focus.
continue;
}
message.notification_sent = true;
if (should_send_desktop_notification(message)) {
process_notification({
message,
desktop_notify: desktop_notifications.granted_desktop_notifications_permission(),
});
}
if (should_send_audible_notification(message)) {
ui_util.play_audio($("#user-notification-sound-audio")[0]);
}
}
}
export function send_test_notification(content) {
received_messages([
{
id: Math.random(),
type: "test-notification",
sender_email: "notification-bot@zulip.com",
sender_full_name: "Notification Bot",
display_reply_to: "Notification Bot",
content,
unread: true,
},
]);
}