notifications: Collapse blockquotes and "user said" paragraphs.

Since notifications have limited space for the contents of a message,
a quote from a previous message, or elsewhere, can take up most of the
notification, leaving little room for the actual message, and reducing
the usefulness of the notification.

To fix this, we collapse blockquotes and "user said" paragraphs to make
space for the actual message.
This commit is contained in:
N-Shar-ma 2024-01-23 00:33:18 +05:30 committed by Tim Abbott
parent 727774a0e5
commit adddb3d54f
6 changed files with 174 additions and 0 deletions

View File

@ -34,6 +34,18 @@ export function get_current_hash_section(): string {
return get_hash_section(window.location.hash);
}
export function is_same_server_message_link(url: string): boolean {
// A same server message link always has category `narrow`,
// section `stream` or `dm`, and ends with `/near/<message_id>`,
// where <message_id> is a sequence of digits.
return (
get_hash_category(url) === "narrow" &&
(get_hash_section(url) === "stream" || get_hash_section(url) === "dm") &&
get_nth_hash_section(url, -2) === "near" &&
/^\d+$/.test(get_nth_hash_section(url, -1))
);
}
export function is_overlay_hash(hash: string): boolean {
// Hash changes within this list are overlays and should not unnarrow (etc.)
const overlay_list = [

View File

@ -20,6 +20,7 @@ function get_notification_content(message) {
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 (

View File

@ -1,6 +1,7 @@
import $ from "jquery";
import * as blueslip from "./blueslip";
import * as hash_parser from "./hash_parser";
import * as keydown_util from "./keydown_util";
// Add functions to this that have no non-trivial
@ -44,6 +45,60 @@ export function change_katex_to_raw_latex($element: JQuery): void {
});
}
export function is_user_said_paragraph($element: JQuery): boolean {
// Irrespective of language, the user said paragraph has these exact elements:
// 1. A user mention
// 2. A same server message link ("said")
// 3. A colon (:)
const $user_mention = $element.find(".user-mention");
if ($user_mention.length !== 1) {
return false;
}
const $message_link = $element.find("a[href]").filter((_index, element) => {
const href = $(element).attr("href")!;
return href ? hash_parser.is_same_server_message_link(href) : false;
});
if ($message_link.length !== 1) {
return false;
}
const remaining_text = $element
.text()
.replace($user_mention.text(), "")
.replace($message_link.text(), "");
return remaining_text.trim() === ":";
}
export function get_collapsible_status_array($elements: JQuery): boolean[] {
return [...$elements].map(
(element) => $(element).is("blockquote") || is_user_said_paragraph($(element)),
);
}
export function potentially_collapse_quotes($element: JQuery): boolean {
const $children = $element.children();
const collapsible_status = get_collapsible_status_array($children);
if (collapsible_status.every(Boolean) || collapsible_status.every((x) => !x)) {
// If every element is collapsible or none of them is collapsible,
// we don't collapse any element.
return false;
}
for (const [index, element] of [...$children].entries()) {
if (collapsible_status[index]) {
if (index > 0 && collapsible_status[index - 1]) {
// If the previous element was also collapsible, remove its text
// to have a single collapsed block instead of multiple in a row.
$(element).text("");
} else {
// Else, collapse this element.
$(element).text("[…]");
}
}
}
return true;
}
export function blur_active_element(): void {
// this blurs anything that may perhaps be actively focused on.
if (document.activeElement instanceof HTMLElement) {

View File

@ -87,6 +87,42 @@ run_test("get_current_nth_hash_section", () => {
assert.equal(hash_parser.get_current_nth_hash_section(3), "");
});
run_test("test_is_same_server_message_link", () => {
const dm_message_link = "#narrow/dm/9,15-dm/near/43";
assert.equal(hash_parser.is_same_server_message_link(dm_message_link), true);
const group_message_link = "#narrow/dm/9,16,15-group/near/68";
assert.equal(hash_parser.is_same_server_message_link(group_message_link), true);
const stream_message_link = "#narrow/stream/8-design/topic/desktop/near/82";
assert.equal(hash_parser.is_same_server_message_link(stream_message_link), true);
const stream_link = "#narrow/stream/8-design";
assert.equal(hash_parser.is_same_server_message_link(stream_link), false);
const topic_link = "#narrow/stream/8-design/topic/desktop";
assert.equal(hash_parser.is_same_server_message_link(topic_link), false);
const dm_link = "#narrow/dm/15-John";
assert.equal(hash_parser.is_same_server_message_link(dm_link), false);
const search_link = "#narrow/search/database";
assert.equal(hash_parser.is_same_server_message_link(search_link), false);
const different_server_message_link =
"https://fakechat.zulip.org/#narrow/dm/8,1848,2369-group/near/1717378";
assert.equal(hash_parser.is_same_server_message_link(different_server_message_link), false);
const drafts_link = "#drafts";
assert.equal(hash_parser.is_same_server_message_link(drafts_link), false);
const empty_link = "#";
assert.equal(hash_parser.is_same_server_message_link(empty_link), false);
const non_zulip_link = "https://www.google.com";
assert.equal(hash_parser.is_same_server_message_link(non_zulip_link), false);
});
run_test("build_reload_url", () => {
window.location.hash = "#settings/profile";
assert.equal(hash_util.build_reload_url(), "+oldhash=settings%2Fprofile");

View File

@ -341,6 +341,7 @@ test("message_is_notifiable", () => {
test("basic_notifications", () => {
$("<div>").set_find_results(".emoji", {replaceWith() {}});
$("<div>").set_find_results("span.katex", {each() {}});
$("<div>").children = () => [];
let n; // Object for storing all notification data for assertions.
let last_closed_message_id = null;

69
web/tests/ui_util.test.js Normal file
View File

@ -0,0 +1,69 @@
"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("./lib/namespace");
const {run_test} = require("./lib/test");
const $ = require("./lib/zjquery");
const ui_util = zrequire("ui_util");
run_test("potentially_collapse_quotes", ({override_rewire}) => {
const $element = $.create("message-content");
let children = [];
$element.children = () => children;
children = [
$.create("normal paragraph 1"),
$.create("blockquote"),
$.create("normal paragraph 2"),
$.create("user said paragraph"),
$.create("message quote"),
$.create("normal paragraph 3"),
];
override_rewire(ui_util, "get_collapsible_status_array", () => [
false,
true,
false,
true,
true,
false,
]);
// When there are both collapsible and non-collapsible elements, for
// multiple collapsible elements in a row, only the first element
// should be collapsed, and the rest's text should be removed. Non-
// collapsible elements should not be touched.
let collapsed = ui_util.potentially_collapse_quotes($element);
assert.equal(collapsed, true);
let expected_texts = ["never-been-set", "[…]", "never-been-set", "[…]", "", "never-been-set"];
assert.deepEqual(
$element.children().map(($el) => $el.text()),
expected_texts,
);
children = [
$.create("normal paragraph 4"),
$.create("normal paragraph 5"),
$.create("normal paragraph 6"),
];
override_rewire(ui_util, "get_collapsible_status_array", () => [false, false, false]);
// For all non-collapsible elements, none should be collapsed.
collapsed = ui_util.potentially_collapse_quotes($element);
assert.equal(collapsed, false);
expected_texts = ["never-been-set", "never-been-set", "never-been-set"];
assert.deepEqual(
$element.children().map(($el) => $el.text()),
expected_texts,
);
children = [$.create("blockquote 1"), $.create("blockquote 2"), $.create("blockquote 3")];
override_rewire(ui_util, "get_collapsible_status_array", () => [true, true, true]);
// For all collapsible elements, none should be collapsed.
collapsed = ui_util.potentially_collapse_quotes($element);
assert.equal(collapsed, false);
expected_texts = ["never-been-set", "never-been-set", "never-been-set"];
assert.deepEqual(
$element.children().map(($el) => $el.text()),
expected_texts,
);
});