zulip/static/js/floating_recipient_bar.js

334 lines
10 KiB
JavaScript

import $ from "jquery";
import * as blueslip from "./blueslip";
import * as message_lists from "./message_lists";
import * as message_store from "./message_store";
import * as rows from "./rows";
import * as timerender from "./timerender";
let is_floating_recipient_bar_showing = false;
function top_offset($elem) {
return (
$elem.offset().top -
$("#message_view_header").safeOuterHeight() -
$("#navbar_alerts_wrapper").height()
);
}
export function first_visible_message($bar) {
// The first truly visible message would be computed using the
// bottom of the floating recipient bar; but we want the date from
// the first visible message were the floating recipient bar not
// displayed, which will always be the first messages whose bottom
// overlaps the floating recipient bar's space (since you ).
const $messages = $bar.children(".message_row");
const $frb = $("#floating_recipient_bar");
const frb_top = top_offset($frb);
const frb_bottom = frb_top + $frb.safeOuterHeight();
let $result;
for (const message_element of $messages) {
// The details of this comparison function are sensitive, since we're
// balancing between three possible bugs:
//
// * If we compare against the bottom of the floating
// recipient bar, we end up with a bug where if the floating
// recipient bar is just above a normal recipient bar while
// overlapping a series of 1-line messages, there might be 2
// messages occluded by the recipient bar, and we want the
// second one, not the first.
//
// * If we compare the message bottom against the top of the
// floating recipient bar, and the floating recipient bar is
// over a "Yesterday/Today" message date row, we might
// confusingly have the floating recipient bar display
// e.g. "Yesterday" even though all messages in view were
// actually sent "Today".
//
// * If the the floating recipient bar is over a
// between-message groups date separator or similar widget,
// there might be no message overlap with the floating
// recipient bar.
//
// Careful testing of these two corner cases with
// message_viewport.scrollTop() to set precise scrolling
// positions determines the value for date_bar_height_offset.
let $message = $(message_element);
const message_bottom = top_offset($message) + $message.safeOuterHeight();
const date_bar_height_offset = 10;
if (message_bottom > frb_top) {
$result = $message;
}
// Important: This will break if we ever have things that are
// not message rows inside a recipient_row block.
$message = $message.next(".message_row");
if (
$message.length > 0 &&
$result &&
// Before returning a result, we check whether the next
// message's top is actually below the bottom of the
// floating recipient bar; this is different from the
// bottom of our current message because there may be a
// between-messages date separator row in between.
top_offset($message) < frb_bottom - date_bar_height_offset
) {
$result = $message;
}
if ($result) {
return $result;
}
}
// If none of the messages are visible, just take the last message.
return $messages.last();
}
export function get_date($elem) {
const message_row = first_visible_message($elem);
if (!message_row || !message_row.length) {
return undefined;
}
const msg_id = rows.id(message_row);
if (msg_id === undefined) {
return undefined;
}
const message = message_store.get(msg_id);
if (!message) {
return undefined;
}
const time = new Date(message.timestamp * 1000);
const today = new Date();
const rendered_date = timerender.render_date(time, undefined, today)[0].outerHTML;
return rendered_date;
}
export function relevant_recipient_bars() {
let elems = [];
// This line of code does a reverse traversal
// from the selected message, which should be
// in the visible part of the feed, but is sometimes
// not exactly where we want. The value we get
// may be be too far up in the feed, but we can
// deal with that later.
let $first_elem = candidate_recipient_bar();
if (!$first_elem) {
$first_elem = $(".focused_table").find(".recipient_row").first();
}
if ($first_elem.length === 0) {
return [];
}
elems.push($first_elem);
const max_offset = top_offset($("#compose"));
let header_height = $first_elem.find(".message_header").safeOuterHeight();
// It's okay to overestimate header_height a bit, as we don't
// really need an FRB for a section that barely shows.
header_height += 10;
function next($elem) {
$elem = $elem.next();
while ($elem.length !== 0 && !$elem.hasClass("recipient_row")) {
$elem = $elem.next();
}
return $elem;
}
// Now start the forward traversal of recipient bars.
// We'll stop when we go below the fold.
let $elem = next($first_elem);
while ($elem.length) {
if (top_offset($elem) < header_height) {
// If we are close to the top, then the prior
// elements we found are no longer relevant,
// because either the selected item we started
// with in our reverse traversal was too high,
// or there's simply not enough room to draw
// a recipient bar without it being ugly.
elems = [];
}
if (top_offset($elem) > max_offset) {
// Out of sight, out of mind!
// (The element is below the fold, so we stop the
// traversal.)
break;
}
elems.push($elem);
$elem = next($elem);
}
if (elems.length === 0) {
blueslip.warn("Unexpected situation--maybe viewport height is very short.");
return [];
}
const items = elems.map(($elem, i) => {
let date_html;
let need_frb;
if (i === 0) {
date_html = get_date($elem);
need_frb = top_offset($elem) < 0;
} else {
date_html = $elem.find(".recipient_row_date").html();
need_frb = false;
}
const date_text = $(date_html).text();
// Add title here to facilitate troubleshooting.
const title = $elem.find(".message_label_clickable").last().attr("title");
const item = {
$elem,
title,
date_html,
date_text,
need_frb,
};
return item;
});
items[0].show_date = true;
for (let i = 1; i < items.length; i += 1) {
items[i].show_date = items[i].date_text !== items[i - 1].date_text;
}
for (const item of items) {
if (!item.need_frb) {
delete item.date_html;
}
}
return items;
}
export function candidate_recipient_bar() {
// Find a recipient bar that is close to being onscreen
// but above the "top". This function is guaranteed to
// return **some** recipient bar that is above the fold,
// if there is one, but it may not be the optimal one if
// our pointer is messed up. Starting with the pointer
// is just an optimization here, and our caller will do
// a forward traversal and clean up as necessary.
// In most cases we find the bottom-most of recipient
// bars that is still above the fold.
// Start with the pointer's current location.
const $selected_row = message_lists.current.selected_row();
if ($selected_row === undefined || $selected_row.length === 0) {
return undefined;
}
let $candidate = rows.get_message_recipient_row($selected_row);
if ($candidate === undefined) {
return undefined;
}
while ($candidate.length) {
if ($candidate.hasClass("recipient_row") && top_offset($candidate) < 0) {
return $candidate;
}
// We cannot use .prev(".recipient_row") here, because that
// returns nothing if the previous element is not a recipient
// row, rather than finding the first recipient_row.
$candidate = $candidate.prev();
}
return undefined;
}
function show_floating_recipient_bar() {
if (!is_floating_recipient_bar_showing) {
$("#floating_recipient_bar").css("visibility", "visible");
is_floating_recipient_bar_showing = true;
}
}
let $old_source;
function replace_floating_recipient_bar(source_info) {
const $source_recipient_bar = source_info.$elem;
let $new_label;
let $other_label;
let $header;
if ($source_recipient_bar !== $old_source) {
if ($source_recipient_bar.children(".message_header_stream").length !== 0) {
$new_label = $("#current_label_stream");
$other_label = $("#current_label_private_message");
$header = $source_recipient_bar.children(".message_header_stream");
} else {
$new_label = $("#current_label_private_message");
$other_label = $("#current_label_stream");
$header = $source_recipient_bar.children(".message_header_private_message");
}
$new_label.find(".message_header").replaceWith($header.clone());
$other_label.css("display", "none");
$new_label.css("display", "block");
$new_label.attr("zid", rows.id(rows.first_message_in_group($source_recipient_bar)));
$new_label.toggleClass("message-fade", $source_recipient_bar.hasClass("message-fade"));
$old_source = $source_recipient_bar;
}
const rendered_date = source_info.date_html || "";
$("#floating_recipient_bar").find(".recipient_row_date").html(rendered_date);
show_floating_recipient_bar();
}
export function hide() {
if (is_floating_recipient_bar_showing) {
$("#floating_recipient_bar").css("visibility", "hidden");
is_floating_recipient_bar_showing = false;
}
}
export function de_clutter_dates(items) {
for (const item of items) {
item.$elem.find(".recipient_row_date").toggle(item.show_date);
}
}
export function update() {
const items = relevant_recipient_bars();
if (!items || items.length === 0) {
hide();
return;
}
de_clutter_dates(items);
if (!items[0].need_frb) {
hide();
return;
}
replace_floating_recipient_bar(items[0]);
}