import $ from "jquery"; import * as message_flags from "./message_flags"; import * as message_lists from "./message_lists"; import * as message_viewport from "./message_viewport"; import * as recent_topics_util from "./recent_topics_util"; import * as rows from "./rows"; /* This library implements two related, similar concepts: - condensing, i.e. cutting off messages taller than about a half screen so that they aren't distractingly tall (and offering a button to uncondense them). - Collapsing, i.e. taking a message and reducing its height to a single line, with a button to see the content. */ const _message_content_height_cache = new Map(); function show_more_link($row) { $row.find(".message_condenser").hide(); $row.find(".message_expander").show(); } function show_condense_link($row) { $row.find(".message_expander").hide(); $row.find(".message_condenser").show(); } function condense_row($row) { const $content = $row.find(".message_content"); $content.addClass("condensed"); show_more_link($row); } function uncondense_row($row) { const $content = $row.find(".message_content"); $content.removeClass("condensed"); show_condense_link($row); } export function uncollapse($row) { // Uncollapse a message, restoring the condensed message [More] or // [Show less] link if necessary. const message = message_lists.current.get(rows.id($row)); message.collapsed = false; message_flags.save_uncollapsed(message); const process_row = function process_row($row) { const $content = $row.find(".message_content"); $content.removeClass("collapsed"); if (message.condensed === true) { // This message was condensed by the user, so re-show the // [More] link. condense_row($row); } else if (message.condensed === false) { // This message was un-condensed by the user, so re-show the // [Show less] link. uncondense_row($row); } else if ($content.hasClass("could-be-condensed")) { // By default, condense a long message. condense_row($row); } else { // This was a short message, no more need for a [More] link. $row.find(".message_expander").hide(); } }; // We also need to collapse this message in the home view const $home_row = message_lists.home.get_row(rows.id($row)); process_row($row); process_row($home_row); } export function collapse($row) { // Collapse a message, hiding the condensed message [More] or // [Show less] link if necessary. const message = message_lists.current.get(rows.id($row)); message.collapsed = true; if (message.locally_echoed) { // Trying to collapse a locally echoed message is // very rare, and in our current implementation the // server response overwrites the flag, so we just // punt for now. return; } message_flags.save_collapsed(message); const process_row = function process_row($row) { $row.find(".message_content").addClass("collapsed"); show_more_link($row); }; // We also need to collapse this message in the home view const $home_row = message_lists.home.get_row(rows.id($row)); process_row($row); process_row($home_row); } export function toggle_collapse(message) { if (message.is_me_message) { // Disabled temporarily because /me messages don't have a // styling for collapsing /me messages (they only recently // added multi-line support). See also popovers.js. return; } // This function implements a multi-way toggle, to try to do what // the user wants for messages: // // * If the message is currently showing any [More] link, either // because it was previously condensed or collapsed, fully display it. // * If the message is fully visible, either because it's too short to // condense or because it's already uncondensed, collapse it const $row = message_lists.current.get_row(message.id); if (!$row) { return; } const $content = $row.find(".message_content"); const is_condensable = $content.hasClass("could-be-condensed"); const is_condensed = $content.hasClass("condensed"); if (message.collapsed) { if (is_condensable) { message.condensed = true; $content.addClass("condensed"); show_message_expander($row); $row.find(".message_condenser").hide(); } uncollapse($row); } else { if (is_condensed) { message.condensed = false; $content.removeClass("condensed"); hide_message_expander($row); $row.find(".message_condenser").show(); } else { collapse($row); } } } export function clear_message_content_height_cache() { _message_content_height_cache.clear(); } export function un_cache_message_content_height(message_id) { _message_content_height_cache.delete(message_id); } function get_message_height(elem, message_id) { if (_message_content_height_cache.has(message_id)) { return _message_content_height_cache.get(message_id); } // shown to be ~2.5x faster than Node.getBoundingClientRect(). const height = elem.offsetHeight; if (!recent_topics_util.is_visible()) { _message_content_height_cache.set(message_id, height); } return height; } export function hide_message_expander($row) { if ($row.find(".could-be-condensed").length !== 0) { $row.find(".message_expander").hide(); } } export function hide_message_condenser($row) { if ($row.find(".could-be-condensed").length !== 0) { $row.find(".message_condenser").hide(); } } export function show_message_expander($row) { if ($row.find(".could-be-condensed").length !== 0) { $row.find(".message_expander").show(); } } export function show_message_condenser($row) { if ($row.find(".could-be-condensed").length !== 0) { $row.find(".message_condenser").show(); } } export function condense_and_collapse(elems) { const height_cutoff = message_viewport.height() * 0.65; for (const elem of elems) { const $content = $(elem).find(".message_content"); if ($content.length !== 1) { // We could have a "/me did this" message or something // else without a `message_content` div. continue; } const message_id = rows.id($(elem)); if (!message_id) { continue; } const message = message_lists.current.get(message_id); if (message === undefined) { continue; } const message_height = get_message_height(elem, message.id); const long_message = message_height > height_cutoff; if (long_message) { // All long messages are flagged as such. $content.addClass("could-be-condensed"); } else { $content.removeClass("could-be-condensed"); } // If message.condensed is defined, then the user has manually // specified whether this message should be expanded or condensed. if (message.condensed === true) { condense_row($(elem)); continue; } if (message.condensed === false) { uncondense_row($(elem)); continue; } if (long_message) { // By default, condense a long message. condense_row($(elem)); } else { $content.removeClass("condensed"); $(elem).find(".message_expander").hide(); } // Completely hide the message and replace it with a [More] // link if the user has collapsed it. if (message.collapsed) { $content.addClass("collapsed"); $(elem).find(".message_expander").show(); } } } export function initialize() { $("#message_feed_container").on("click", ".message_expander", function (e) { // Expanding a message can mean either uncollapsing or // uncondensing it. const $row = $(this).closest(".message_row"); const message = message_lists.current.get(rows.id($row)); const $content = $row.find(".message_content"); if (message.collapsed) { // Uncollapse. uncollapse($row); } else if ($content.hasClass("condensed")) { // Uncondense (show the full long message). message.condensed = false; $content.removeClass("condensed"); $(this).hide(); $row.find(".message_condenser").show(); } e.stopPropagation(); e.preventDefault(); }); $("#message_feed_container").on("click", ".message_condenser", function (e) { const $row = $(this).closest(".message_row"); message_lists.current.get(rows.id($row)).condensed = true; condense_row($row); e.stopPropagation(); e.preventDefault(); }); }