mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 12:03:46 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			334 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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[messages.length - 1]);
 | |
| }
 | |
| 
 | |
| 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]);
 | |
| }
 |