recent_view: Use html as scroll container.

Fixes #17933, #27517

Instead of `recent_view_table`, we make `html` as our scroll container.
This fixes an important bug for us where filters sometimes disappear
due to them scrolling under navbar which is unexpected. Since we are
now using separate containers to display rows and
filter (while includes table headers), where filters use sticky
positioning, this bug will be fixed.
This commit is contained in:
Aman Agrawal 2024-06-03 10:05:29 +00:00 committed by Tim Abbott
parent 4750f84ba8
commit 371cd0da6c
10 changed files with 709 additions and 687 deletions

View File

@ -231,6 +231,10 @@
<div id="recent_view"> <div id="recent_view">
<div class="recent_view_container"> <div class="recent_view_container">
<div id="recent_view_table"></div> <div id="recent_view_table"></div>
</div>
<table id="recent-view-content-table">
<tbody data-empty="{{ _('No conversations match your filters.') }}" id="recent-view-content-tbody"></tbody>
</table>
<div id="recent_view_bottom_whitespace"> <div id="recent_view_bottom_whitespace">
<div class="bottom-messages-logo"> <div class="bottom-messages-logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 773.12 773.12"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 773.12 773.12">
@ -240,6 +244,13 @@
</div> </div>
<div id="recent_view_loading_messages_indicator"></div> <div id="recent_view_loading_messages_indicator"></div>
</div> </div>
<!-- Don't show the banner until we have some messages loaded. -->
<div class="recent-view-load-more-container main-view-banner info notvisible">
<div class="last-fetched-message banner_content">{{ _('This view is still loading messages.') }}</div>
<button class="fetch-messages-button main-view-banner-action-button right_edge notvisible">
<div class="loading-indicator"></div>
<span class="button-label">{{ _('Load more') }}</span>
</button>
</div> </div>
</div> </div>
<div id="inbox-view"> <div id="inbox-view">

View File

@ -18,6 +18,7 @@ type ListWidgetMeta<Key, Item = Key> = {
filtered_list: Item[]; filtered_list: Item[];
reverse_mode: boolean; reverse_mode: boolean;
$scroll_container: JQuery; $scroll_container: JQuery;
$scroll_listening_element: JQuery | JQuery<Window>;
}; };
// This type ensures the mutually exclusive nature of the predicate and filterer options. // This type ensures the mutually exclusive nature of the predicate and filterer options.
@ -261,6 +262,17 @@ export function create<Key, Item = Key>(
old_widget.clear_event_handlers(); old_widget.clear_event_handlers();
} }
let $scroll_listening_element: JQuery | JQuery<Window> = opts.$simplebar_container;
if ($scroll_listening_element.is("html")) {
// When `$scroll_container` is the entire page (`html`),
// scroll events are fired on `window/document`, so we need to
// listen for scrolling events on that.
//
// We still keep `html` as `$scroll_container` to use
// its various methods as `HTMLElement`.
$scroll_listening_element = $(window);
}
const meta: ListWidgetMeta<Key, Item> = { const meta: ListWidgetMeta<Key, Item> = {
sorting_function: null, sorting_function: null,
sorting_functions: new Map(), sorting_functions: new Map(),
@ -270,6 +282,7 @@ export function create<Key, Item = Key>(
reverse_mode: false, reverse_mode: false,
filter_value: "", filter_value: "",
$scroll_container: scroll_util.get_scroll_element(opts.$simplebar_container), $scroll_container: scroll_util.get_scroll_element(opts.$simplebar_container),
$scroll_listening_element,
}; };
const widget: ListWidget<Key, Item> = { const widget: ListWidget<Key, Item> = {
@ -417,7 +430,9 @@ export function create<Key, Item = Key>(
set_up_event_handlers() { set_up_event_handlers() {
// on scroll of the nearest scrolling container, if it hits the bottom // on scroll of the nearest scrolling container, if it hits the bottom
// of the container then fetch a new block of items and render them. // of the container then fetch a new block of items and render them.
meta.$scroll_container.on("scroll.list_widget_container", function () { meta.$scroll_listening_element.on(
"scroll.list_widget_container",
function (this: HTMLElement) {
if (opts.post_scroll__pre_render_callback) { if (opts.post_scroll__pre_render_callback) {
opts.post_scroll__pre_render_callback(); opts.post_scroll__pre_render_callback();
} }
@ -430,7 +445,8 @@ export function create<Key, Item = Key>(
if (should_render) { if (should_render) {
widget.render(); widget.render();
} }
}); },
);
if (opts.$parent_container) { if (opts.$parent_container) {
opts.$parent_container.on( opts.$parent_container.on(
@ -450,7 +466,12 @@ export function create<Key, Item = Key>(
}, },
clear_event_handlers() { clear_event_handlers() {
meta.$scroll_container.off("scroll.list_widget_container"); // Since `$scroll_listening_element` is of type `JQuery | JQuery<Window>` instead
// of just `JQuery`, Typescript is expecting `off` to be called on
// TypeEventHandlers<HTMLElement, any, any, any> which is confusing.
//
// @ts-expect-error Maybe JQuery<Window>.TypeEventHandlers is not defined?
meta.$scroll_listening_element.off("scroll.list_widget_container");
if (opts.$parent_container) { if (opts.$parent_container) {
opts.$parent_container.off("click.list_widget_sort", "[data-sort]"); opts.$parent_container.off("click.list_widget_sort", "[data-sort]");

View File

@ -40,7 +40,6 @@ import * as recent_senders from "./recent_senders";
import * as recent_view_data from "./recent_view_data"; import * as recent_view_data from "./recent_view_data";
import type {ConversationData} from "./recent_view_data"; import type {ConversationData} from "./recent_view_data";
import * as recent_view_util from "./recent_view_util"; import * as recent_view_util from "./recent_view_util";
import * as scroll_util from "./scroll_util";
import * as sidebar_ui from "./sidebar_ui"; import * as sidebar_ui from "./sidebar_ui";
import * as stream_data from "./stream_data"; import * as stream_data from "./stream_data";
import * as sub_store from "./sub_store"; import * as sub_store from "./sub_store";
@ -114,6 +113,9 @@ let is_initial_message_fetch_pending = true;
// We wait for rows to render and restore focus before processing // We wait for rows to render and restore focus before processing
// any new events. // any new events.
let is_waiting_for_revive_current_focus = true; let is_waiting_for_revive_current_focus = true;
// Used to store the last scroll position of the recent view before
// it is hidden to avoid scroll jumping when it is shown again.
let last_scroll_offset: number | undefined;
export function set_initial_message_fetch_status(value: boolean): void { export function set_initial_message_fetch_status(value: boolean): void {
is_initial_message_fetch_pending = value; is_initial_message_fetch_pending = value;
@ -206,7 +208,7 @@ function set_oldest_message_date(msg_list_data: MessageListData): void {
// We might be loading messages in another narrow before the recent view // We might be loading messages in another narrow before the recent view
// is shown, so we keep the state updated and update the banner only // is shown, so we keep the state updated and update the banner only
// once it's actually rendered. // once it's actually rendered.
if ($("#recent_view_table table tbody").length) { if ($("#recent-view-content-tbody tr").length) {
update_load_more_banner(); update_load_more_banner();
} }
} }
@ -278,7 +280,7 @@ function get_row_type(row: number): string {
// Return "private" or "stream" // Return "private" or "stream"
// We use CSS method for finding row type until topics_widget gets initialized. // We use CSS method for finding row type until topics_widget gets initialized.
if (!topics_widget) { if (!topics_widget) {
const $topic_rows = $("#recent_view_table table tbody tr"); const $topic_rows = $("#recent-view-content-tbody tr");
const $topic_row = $topic_rows.eq(row); const $topic_row = $topic_rows.eq(row);
const is_private = $topic_row.attr("data-private"); const is_private = $topic_row.attr("data-private");
if (is_private) { if (is_private) {
@ -310,7 +312,7 @@ function set_table_focus(row: number, col: number, using_keyboard = false): bool
return true; return true;
} }
const $topic_rows = $("#recent_view_table table tbody tr"); const $topic_rows = $("#recent-view-content-tbody tr");
if ($topic_rows.length === 0 || row < 0 || row >= $topic_rows.length) { if ($topic_rows.length === 0 || row < 0 || row >= $topic_rows.length) {
row_focus = 0; row_focus = 0;
// return focus back to filters if we cannot focus on the table. // return focus back to filters if we cannot focus on the table.
@ -338,9 +340,7 @@ function set_table_focus(row: number, col: number, using_keyboard = false): bool
$current_focus_elem = "table"; $current_focus_elem = "table";
if (using_keyboard) { if (using_keyboard) {
const scroll_element = $( const scroll_element = $("html")[0]!;
"#recent_view_table .table_fix_head .simplebar-content-wrapper",
)[0]!;
const half_height_of_visible_area = scroll_element.offsetHeight / 2; const half_height_of_visible_area = scroll_element.offsetHeight / 2;
const topic_offset = topic_offset_to_visible_area($topic_row); const topic_offset = topic_offset_to_visible_area($topic_row);
@ -379,7 +379,7 @@ export function get_focused_row_message(): Message | undefined {
return undefined; return undefined;
} }
const $topic_rows = $("#recent_view_table table tbody tr"); const $topic_rows = $("#recent-view-content-tbody tr");
const $topic_row = $topic_rows.eq(row_focus); const $topic_row = $topic_rows.eq(row_focus);
const topic_id = $topic_row.attr("id"); const topic_id = $topic_row.attr("id");
assert(topic_id !== undefined); assert(topic_id !== undefined);
@ -1127,22 +1127,18 @@ function topic_offset_to_visible_area($topic_row: JQuery): string | undefined {
// topic and the callers will take care of undefined being returned. // topic and the callers will take care of undefined being returned.
return undefined; return undefined;
} }
const $scroll_container = $("#recent_view_table .table_fix_head");
const thead_height = $scroll_container.find("thead").outerHeight(true)!;
const scroll_container_props = $scroll_container[0]!.getBoundingClientRect();
// Since user cannot see row under thead, exclude it as part of the scroll container. // Rows are only visible below thead bottom and above compose top.
const scroll_container_top = scroll_container_props.top + thead_height; const thead_bottom = $("#recent-view-table-headers")[0]!.getBoundingClientRect().bottom;
const compose_height = $("#compose").outerHeight(true)!; const compose_top = window.innerHeight - $("#compose").outerHeight(true)!;
const scroll_container_bottom = scroll_container_props.bottom - compose_height;
const topic_props = $topic_row[0]!.getBoundingClientRect(); const topic_props = $topic_row[0]!.getBoundingClientRect();
// Topic is above the visible scroll region. // Topic is above the visible scroll region.
if (topic_props.top < scroll_container_top) { if (topic_props.top < thead_bottom) {
return "above"; return "above";
// Topic is below the visible scroll region. // Topic is below the visible scroll region.
} else if (topic_props.bottom > scroll_container_bottom) { } else if (topic_props.bottom > compose_top) {
return "below"; return "below";
} }
@ -1155,9 +1151,7 @@ function recenter_focus_if_off_screen(): void {
return; return;
} }
const table_wrapper_element = $("#recent_view_table .table_fix_head")[0]!; const $topic_rows = $("#recent-view-content-tbody tr");
const $topic_rows = $("#recent_view_table table tbody tr");
if (row_focus >= $topic_rows.length) { if (row_focus >= $topic_rows.length) {
// User used a filter which reduced // User used a filter which reduced
// the number of visible rows. // the number of visible rows.
@ -1172,9 +1166,10 @@ function recenter_focus_if_off_screen(): void {
if (topic_offset !== "visible") { if (topic_offset !== "visible") {
// Get the element at the center of the table. // Get the element at the center of the table.
const position = table_wrapper_element.getBoundingClientRect(); const thead_props = $("#recent-view-table-headers")[0]!.getBoundingClientRect();
const topic_center_x = (position.left + position.right) / 2; const compose_top = window.innerHeight - $("#compose").outerHeight(true)!;
const topic_center_y = (position.top + position.bottom) / 2; const topic_center_x = (thead_props.left + thead_props.right) / 2;
const topic_center_y = (thead_props.bottom + compose_top) / 2;
const topic_element = document.elementFromPoint(topic_center_x, topic_center_y); const topic_element = document.elementFromPoint(topic_center_x, topic_center_y);
if ( if (
@ -1184,7 +1179,7 @@ function recenter_focus_if_off_screen(): void {
// There are two theoretical reasons that the center // There are two theoretical reasons that the center
// element might be null. One is that we haven't rendered // element might be null. One is that we haven't rendered
// the view yet; but in that case, we should have returned // the view yet; but in that case, we should have returned
// early checking is_waiting_for_revive_current_focus: // early checking is_waiting_for_revive_current_focus.
// //
// The other possibility is that the table is too short // The other possibility is that the table is too short
// for there to be an topic row element at the center of // for there to be an topic row element at the center of
@ -1199,19 +1194,29 @@ function recenter_focus_if_off_screen(): void {
} }
} }
function is_scroll_position_for_render(scroll_container: HTMLElement): boolean { function is_scroll_position_for_render(): boolean {
const table_bottom_margin = 100; // Extra margin at the bottom of table. const scroll_position = window.scrollY;
const table_row_height = 50; const window_height = window.innerHeight;
return ( // We allocate `--max-unexpanded-compose-height` in empty space
scroll_container.scrollTop + // below the last rendered row in recent view.
scroll_container.clientHeight + //
table_bottom_margin + // We don't want user to see this empty space until there are no
table_row_height > // new rows to render when the user is scrolling to the bottom of
scroll_container.scrollHeight // the view. So, we render new rows when user has scrolled 2 / 3
); // of (the total scrollable height - the empty space).
const compose_max_height = $("html").css("--max-unexpanded-compose-height");
assert(typeof compose_max_height === "string");
const scroll_max = document.body.scrollHeight - Number.parseInt(compose_max_height, 10);
return scroll_position + window_height >= (2 / 3) * scroll_max;
} }
function callback_after_render(): void { function callback_after_render(): void {
// It is important to restore the scroll position as soon
// as the rendering is complete to avoid scroll jumping.
if (last_scroll_offset !== undefined) {
window.scrollTo(0, last_scroll_offset);
}
update_load_more_banner(); update_load_more_banner();
setTimeout(() => { setTimeout(() => {
revive_current_focus(); revive_current_focus();
@ -1261,6 +1266,11 @@ export function complete_rerender(): void {
return; return;
} }
// This is the first time we are rendering the Recent Conversations view.
// So, we always scroll to the top to avoid any scroll jumping in case
// user is returning from another view.
window.scrollTo(0, 0);
const rendered_body = render_recent_view_body({ const rendered_body = render_recent_view_body({
search_val: $("#recent_view_search").val() ?? "", search_val: $("#recent_view_search").val() ?? "",
...get_recent_view_filters_params(), ...get_recent_view_filters_params(),
@ -1273,7 +1283,7 @@ export function complete_rerender(): void {
// was not the first view loaded in the app. // was not the first view loaded in the app.
show_selected_filters(); show_selected_filters();
const $container = $("#recent_view_table table tbody"); const $container = $("#recent-view-content-tbody");
$container.empty(); $container.empty();
topics_widget = list_widget.create($container, mapped_topic_values, { topics_widget = list_widget.create($container, mapped_topic_values, {
name: "recent_view_table", name: "recent_view_table",
@ -1296,7 +1306,7 @@ export function complete_rerender(): void {
...list_widget.generic_sort_functions("numeric", ["last_msg_id"]), ...list_widget.generic_sort_functions("numeric", ["last_msg_id"]),
}, },
html_selector: get_topic_row, html_selector: get_topic_row,
$simplebar_container: $("#recent_view_table .table_fix_head"), $simplebar_container: $("html"),
callback_after_render, callback_after_render,
is_scroll_position_for_render, is_scroll_position_for_render,
post_scroll__pre_render_callback() { post_scroll__pre_render_callback() {
@ -1309,6 +1319,10 @@ export function complete_rerender(): void {
} }
export function show(): void { export function show(): void {
// We remove event handler before hiding, so they need to
// be attached again, checking for topics_widget to be defined
// is a reliable solution to check if recent view was displayed earlier.
const reattach_event_handlers = topics_widget !== undefined;
views_util.show({ views_util.show({
highlight_view_in_left_sidebar: left_sidebar_navigation_area.highlight_recent_view, highlight_view_in_left_sidebar: left_sidebar_navigation_area.highlight_recent_view,
$view: $("#recent_view"), $view: $("#recent_view"),
@ -1321,6 +1335,12 @@ export function show(): void {
set_visible: recent_view_util.set_visible, set_visible: recent_view_util.set_visible,
complete_rerender, complete_rerender,
}); });
last_scroll_offset = undefined;
if (reattach_event_handlers) {
assert(topics_widget !== undefined);
topics_widget.set_up_event_handlers();
}
if (onboarding_steps.ONE_TIME_NOTICES_TO_DISPLAY.has("intro_recent_view_modal")) { if (onboarding_steps.ONE_TIME_NOTICES_TO_DISPLAY.has("intro_recent_view_modal")) {
const html_body = render_introduce_zulip_view_modal({ const html_body = render_introduce_zulip_view_modal({
@ -1348,7 +1368,11 @@ function filter_buttons(): JQuery {
} }
export function hide(): void { export function hide(): void {
// Since we have events attached to element (window) which are present in
// views others than recent view, it is important to clear events here.
topics_widget?.clear_event_handlers();
is_waiting_for_revive_current_focus = true; is_waiting_for_revive_current_focus = true;
last_scroll_offset = window.scrollY;
views_util.hide({ views_util.hide({
$view: $("#recent_view"), $view: $("#recent_view"),
set_visible: recent_view_util.set_visible, set_visible: recent_view_util.set_visible,
@ -1433,9 +1457,8 @@ function down_arrow_navigation(): void {
} }
function get_page_up_down_delta(): number { function get_page_up_down_delta(): number {
const table_height = $("#recent_view_table .table_fix_head").height()!; const thead_bottom = $("#recent-view-table-headers")[0]!.getBoundingClientRect().bottom;
const table_header_height = $("#recent_view_table table thead").height()!; const compose_box_top = window.innerHeight - $("#compose").outerHeight(true)!;
const compose_box_height = $("#compose").height()!;
// One usually wants PageDown to move what had been the bottom row // One usually wants PageDown to move what had been the bottom row
// to now be at the top, so one can be confident one will see // to now be at the top, so one can be confident one will see
// every row using it. This offset helps achieve that goal. // every row using it. This offset helps achieve that goal.
@ -1443,38 +1466,45 @@ function get_page_up_down_delta(): number {
// See navigate.amount_to_paginate for similar logic in the message feed. // See navigate.amount_to_paginate for similar logic in the message feed.
const scrolling_reduction_to_maintain_context = 75; const scrolling_reduction_to_maintain_context = 75;
const delta = const delta = compose_box_top - thead_bottom - scrolling_reduction_to_maintain_context;
table_height -
table_header_height -
compose_box_height -
scrolling_reduction_to_maintain_context;
return delta; return delta;
} }
function page_up_navigation(): void { function page_up_navigation(): void {
const $scroll_container = scroll_util.get_scroll_element(
$("#recent_view_table .table_fix_head"),
);
const delta = get_page_up_down_delta(); const delta = get_page_up_down_delta();
const new_scrollTop = $scroll_container.scrollTop()! - delta; const new_scrollTop = window.scrollY - delta;
if (new_scrollTop <= 0) { if (new_scrollTop <= 0) {
row_focus = 0; row_focus = 0;
// If we are already at the scroll top, a scroll event
// is not triggered since the window doesn't actually scroll so
// we need to update `row_focus` manually.
if (window.scrollY === 0) {
set_table_focus(row_focus, col_focus);
return;
} }
$scroll_container.scrollTop(new_scrollTop); }
window.scroll(0, new_scrollTop);
} }
function page_down_navigation(): void { function page_down_navigation(): void {
const $scroll_container = scroll_util.get_scroll_element(
$("#recent_view_table .table_fix_head"),
);
const delta = get_page_up_down_delta(); const delta = get_page_up_down_delta();
const new_scrollTop = $scroll_container.scrollTop()! + delta; const new_scrollTop = window.scrollY + delta;
const table_height = $("#recent_view_table .table_fix_head").height()!; const max_scroll_top = document.body.scrollHeight - window.innerHeight;
if (new_scrollTop >= table_height) {
if (new_scrollTop >= max_scroll_top) {
assert(topics_widget !== undefined); assert(topics_widget !== undefined);
row_focus = topics_widget.get_current_list().length - 1; row_focus = topics_widget.get_current_list().length - 1;
// If we are already at the scroll bottom, a scroll event
// is not triggered since the window doesn't actually scroll so
// we need to update `row_focus` manually.
if (window.scrollY === max_scroll_top) {
set_table_focus(row_focus, col_focus);
return;
} }
$scroll_container.scrollTop(new_scrollTop); }
window.scroll(0, new_scrollTop);
} }
function check_row_type_transition(row: number, col: number): boolean { function check_row_type_transition(row: number, col: number): boolean {
@ -1706,14 +1736,18 @@ export function initialize({
}): void { }): void {
load_filters(); load_filters();
$("body").on("click", "#recent_view_table .recent_view_participant_avatar", function (e) { $("body").on(
"click",
"#recent-view-content-table .recent_view_participant_avatar",
function (e) {
const user_id_string = $(this).parent().attr("data-user-id"); const user_id_string = $(this).parent().attr("data-user-id");
assert(user_id_string !== undefined); assert(user_id_string !== undefined);
const participant_user_id = Number.parseInt(user_id_string, 10); const participant_user_id = Number.parseInt(user_id_string, 10);
e.stopPropagation(); e.stopPropagation();
assert(this instanceof Element); assert(this instanceof Element);
on_click_participant(this, participant_user_id); on_click_participant(this, participant_user_id);
}); },
);
$("body").on( $("body").on(
"keydown", "keydown",
@ -1722,7 +1756,7 @@ export function initialize({
); );
// Mute topic in a unmuted stream // Mute topic in a unmuted stream
$("body").on("click", "#recent_view_table .stream_unmuted.on_hover_topic_mute", (e) => { $("body").on("click", "#recent-view-content-table .stream_unmuted.on_hover_topic_mute", (e) => {
e.stopPropagation(); e.stopPropagation();
assert(e.target instanceof HTMLElement); assert(e.target instanceof HTMLElement);
const $elt = $(e.target); const $elt = $(e.target);
@ -1735,7 +1769,10 @@ export function initialize({
}); });
// Unmute topic in a unmuted stream // Unmute topic in a unmuted stream
$("body").on("click", "#recent_view_table .stream_unmuted.on_hover_topic_unmute", (e) => { $("body").on(
"click",
"#recent-view-content-table .stream_unmuted.on_hover_topic_unmute",
(e) => {
e.stopPropagation(); e.stopPropagation();
assert(e.target instanceof HTMLElement); assert(e.target instanceof HTMLElement);
const $elt = $(e.target); const $elt = $(e.target);
@ -1745,10 +1782,11 @@ export function initialize({
$elt, $elt,
user_topics.all_visibility_policies.INHERIT, user_topics.all_visibility_policies.INHERIT,
); );
}); },
);
// Unmute topic in a muted stream // Unmute topic in a muted stream
$("body").on("click", "#recent_view_table .stream_muted.on_hover_topic_unmute", (e) => { $("body").on("click", "#recent-view-content-table .stream_muted.on_hover_topic_unmute", (e) => {
e.stopPropagation(); e.stopPropagation();
assert(e.target instanceof HTMLElement); assert(e.target instanceof HTMLElement);
const $elt = $(e.target); const $elt = $(e.target);
@ -1761,7 +1799,7 @@ export function initialize({
}); });
// Mute topic in a muted stream // Mute topic in a muted stream
$("body").on("click", "#recent_view_table .stream_muted.on_hover_topic_mute", (e) => { $("body").on("click", "#recent-view-content-table .stream_muted.on_hover_topic_mute", (e) => {
e.stopPropagation(); e.stopPropagation();
assert(e.target instanceof HTMLElement); assert(e.target instanceof HTMLElement);
const $elt = $(e.target); const $elt = $(e.target);
@ -1779,7 +1817,7 @@ export function initialize({
change_focused_element($(e.target), "click"); change_focused_element($(e.target), "click");
}); });
$("body").on("click", "#recent_view_table .on_hover_topic_read", (e) => { $("body").on("click", "#recent-view-content-table .on_hover_topic_read", (e) => {
e.stopPropagation(); e.stopPropagation();
assert(e.currentTarget instanceof HTMLElement); assert(e.currentTarget instanceof HTMLElement);
const $elt = $(e.currentTarget); const $elt = $(e.currentTarget);

View File

@ -263,6 +263,16 @@
--color-buddy-list-highlighted-user: hsl(120deg 12.3% 71.4% / 38%); --color-buddy-list-highlighted-user: hsl(120deg 12.3% 71.4% / 38%);
--color-border-sidebar: hsl(0deg 0% 87%); --color-border-sidebar: hsl(0deg 0% 87%);
/* Recent view */
--color-border-recent-view-row: hsl(0deg 0% 87%);
--color-border-recent-view-table: hsl(0deg 0% 0% / 60%);
--color-background-recent-view-row: hsl(100deg 11% 96%);
--color-background-recent-view-row-hover: hsl(210deg 100% 97%);
--color-background-recent-view-unread-row: hsl(0deg 0% 100%);
--color-background-recent-view-unread-row-hover: hsl(210deg 100% 97%);
--color-recent-view-link: hsl(205deg 47% 42%);
--color-recent-view-link-hover: hsl(214deg 40% 58%);
/* Compose box colors */ /* Compose box colors */
--color-compose-send-button-icon-color: hsl(0deg 0% 100%); --color-compose-send-button-icon-color: hsl(0deg 0% 100%);
--color-compose-send-button-background: hsl(240deg 96% 68%); --color-compose-send-button-background: hsl(240deg 96% 68%);
@ -591,6 +601,16 @@
--color-buddy-list-highlighted-user: hsl(136deg 25% 73% / 20%); --color-buddy-list-highlighted-user: hsl(136deg 25% 73% / 20%);
--color-border-sidebar: hsl(0deg 0% 0% / 20%); --color-border-sidebar: hsl(0deg 0% 0% / 20%);
/* Recent view */
--color-border-recent-view-row: hsl(0deg 0% 0% / 20%);
--color-border-recent-view-table: hsl(0deg 0% 88%);
--color-background-recent-view-row: hsl(0deg 0% 11%);
--color-background-recent-view-row-hover: hsl(208deg 26% 11% / 60%);
--color-background-recent-view-unread-row: hsl(212deg 30% 22% / 40%);
--color-background-recent-view-unread-row-hover: hsl(212deg 30% 22% / 60%);
--color-recent-view-link: hsl(206deg 89% 74%);
--color-recent-view-link-hover: hsl(208deg 64% 52%);
/* Compose box colors */ /* Compose box colors */
--color-compose-send-button-focus-shadow: hsl(0deg 0% 100% / 80%); --color-compose-send-button-focus-shadow: hsl(0deg 0% 100% / 80%);
--color-compose-send-control-button: hsl(240deg 30% 70%); --color-compose-send-control-button: hsl(240deg 30% 70%);

View File

@ -689,22 +689,6 @@
} }
} }
#recent_view_table tr {
background-color: var(--color-background);
&:hover {
background-color: hsl(208deg 26% 11% / 60%);
}
}
#recent_view_table .unread_topic {
background-color: hsl(212deg 30% 22% / 40%);
&:hover {
background-color: hsl(212deg 30% 22% / 60%);
}
}
.btn-recent-selected, .btn-recent-selected,
#recent_view_table thead th { #recent_view_table thead th {
background-color: hsl(228deg 11% 17%) !important; background-color: hsl(228deg 11% 17%) !important;
@ -714,18 +698,7 @@
} }
} }
#recent_view_table td a {
color: hsl(206deg 89% 74%);
text-decoration: none;
&:hover {
color: hsl(208deg 64% 52%);
}
}
#recent_view_table { #recent_view_table {
border-color: hsl(0deg 0% 0% / 60%);
.fa-envelope, .fa-envelope,
.fa-group { .fa-group {
opacity: 0.7; opacity: 0.7;
@ -788,8 +761,7 @@
#settings_page .sidebar-wrapper *, #settings_page .sidebar-wrapper *,
table, table,
table th, table th,
table td, table td {
#recent_view_table table td {
border-color: hsl(0deg 0% 0% / 20%); border-color: hsl(0deg 0% 0% / 20%);
} }

View File

@ -4,27 +4,39 @@
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 100vh; position: sticky;
top: var(--navbar-fixed-height);
z-index: 1;
}
#recent_view_table { .recent_view_container #recent_view_table {
max-width: 100%; max-width: 100%;
overflow: hidden !important; overflow: hidden !important;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-style: solid; border: 0;
border-color: hsl(0deg 0% 88%); }
border-width: 0 1px;
border-radius: 0; #recent_view_table .table,
/* To make the table span full height #recent-view-content-table {
* when rows don't reach bottom of the /* To keep border properties to the thead th. */
* window. This makes the border span border-collapse: separate;
* fully to bottom in that case.
*/ border-spacing: 0;
min-height: 100vh; width: 100%;
}
#recent_view {
display: none;
padding-top: var(--navbar-fixed-height);
/* Add bottom padding equal to `#bottom-whitespace`. This helps us keep #compose visible
at its max-height without overlapping with any visible topics. */
padding-bottom: var(--max-unexpanded-compose-height);
& td { & td {
vertical-align: middle; vertical-align: middle;
padding: 3px 8px; padding: 3px 8px;
border-top: 1px solid var(--color-border-recent-view-row);
} }
.recent_view_focusable { .recent_view_focusable {
@ -63,11 +75,11 @@
} }
& a { & a {
color: hsl(205deg 47% 42%); color: var(--color-recent-view-link);
text-decoration: none; text-decoration: none;
&:hover { &:hover {
color: hsl(214deg 40% 58%); color: var(--color-recent-view-link-hover);
} }
} }
@ -92,23 +104,6 @@
.table_fix_head { .table_fix_head {
padding: 0 !important; padding: 0 !important;
max-height: calc(
100vh - var(--recent-topics-filters-height) -
var(--navbar-fixed-height)
);
}
.recent-view-container {
/*
Add margin bottom equal to `#bottom-whitespace`. This helps us keep
#compose visible at its max-height without overlapping with any visible
topics.
Alternative is to adjust the max-height of `table_fix_head` based on compose height which is an
expensive repaint. The downside of not doing so is that the scrollbar is not visible to user when
user is at the bottom of scroll container when the compose box is open.
*/
margin-bottom: var(--max-unexpanded-compose-height);
} }
.recent-view-load-more-container { .recent-view-load-more-container {
@ -130,46 +125,19 @@
} }
} }
.table_fix_head table { .table_fix_head table th {
/* To keep border properties to the thead th. */
border-collapse: separate;
border-spacing: 0;
width: 100%;
th {
padding: 8px; padding: 8px;
text-align: left; text-align: left;
} }
.unread_sort {
padding-left: 6px;
.zulip-icon-unread {
position: absolute;
right: 30px;
top: 11px;
}
&::after {
right: 15px;
top: 7px;
}
}
td {
border-top: 1px solid hsl(0deg 0% 87%);
}
}
#recent_view_filter_buttons { #recent_view_filter_buttons {
padding-top: 12px; padding: 12px 10px 0;
margin: 0 10px;
display: flex; display: flex;
/* Search box has no height without this in safari. */ /* Search box has no height without this in safari. */
flex: 0 0 auto; flex: 0 0 auto;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
background: var(--color-background);
} }
.search_group { .search_group {
@ -338,10 +306,10 @@
} }
& tr { & tr {
background-color: hsl(100deg 11% 96%); background-color: var(--color-background-recent-view-row);
&:hover { &:hover {
background-color: hsl(210deg 100% 97%); background-color: var(--color-background-recent-view-row-hover);
.change_visibility_policy .zulip-icon-inherit { .change_visibility_policy .zulip-icon-inherit {
opacity: 0.4; opacity: 0.4;
@ -350,7 +318,13 @@
} }
.unread_topic { .unread_topic {
background-color: hsl(0deg 0% 100%); background-color: var(--color-background-recent-view-unread-row);
&:hover {
background-color: var(
--color-background-recent-view-unread-row-hover
);
}
} }
.last_msg_time { .last_msg_time {
@ -363,8 +337,6 @@
color: inherit; color: inherit;
border-top: 1px solid hsl(0deg 0% 0% / 20%) !important; border-top: 1px solid hsl(0deg 0% 0% / 20%) !important;
border-bottom: 1px solid hsl(0deg 0% 0% / 20%) !important; border-bottom: 1px solid hsl(0deg 0% 0% / 20%) !important;
position: sticky;
top: 0;
z-index: 1; z-index: 1;
&.active::after, &.active::after,
@ -398,10 +370,37 @@
} }
} }
.recent_topic_stream,
.recent-view-stream-header {
width: 25%;
}
.recent-view-topic-header {
width: 35%;
}
.recent-view-unread-header {
width: 5%;
.zulip-icon-unread {
position: relative;
top: 3px;
}
}
.recent_topic_users,
.recent-view-participants-header {
width: 20%;
}
.recent_topic_timestamp,
.recent-view-last-msg-time-header {
width: 15%;
}
/* These fixed column widths prevent column widths from being adjusted /* These fixed column widths prevent column widths from being adjusted
as new messages arrive from the server. */ as new messages arrive from the server. */
.recent_topic_stream { .recent_topic_stream {
width: 25%;
padding: 8px 0 8px 8px; padding: 8px 0 8px 8px;
& a { & a {
@ -433,14 +432,6 @@
} }
} }
.recent_topic_users {
width: 20%;
}
.recent_topic_timestamp {
width: 15%;
}
& thead .last_msg_time_header { & thead .last_msg_time_header {
/* The responsive table of bootstrap /* The responsive table of bootstrap
somehow ignores the width of ::after somehow ignores the width of ::after
@ -503,21 +494,15 @@
} }
} }
} }
.stream-privacy .zulip-icon {
position: relative;
left: -1px;
top: 1.5px;
}
} }
#recent_view_bottom_whitespace { #recent_view_bottom_whitespace {
/* For visual reasons, in a message feed, we require a large
* bottom_whitespace to make it convenient to display new
* messages as they come in and prevent occluding the last
* message with an open compose box. Here, the bottom item
* is rarely interesting context for a message one is
* composing, but we do need at least 41px to allow the
* close-compose-box UI element (including border) to not
* overlap content. Add some more margin so that user
* can clearly see the end of the topics.
*/
height: 120px;
#recent_view_loading_messages_indicator, #recent_view_loading_messages_indicator,
.bottom-messages-logo { .bottom-messages-logo {
display: block; display: block;
@ -534,20 +519,6 @@
} }
} }
.stream-privacy {
.zulip-icon {
position: relative;
left: -1px;
top: 1.5px;
}
}
}
#recent_view {
display: none;
position: relative;
}
#recent-view-filter_widget { #recent-view-filter_widget {
display: inline-flex; display: inline-flex;
width: 150px; width: 150px;

View File

@ -323,10 +323,6 @@ p.n-margin {
} }
} }
.recent_view_container #recent_view_table {
margin-top: var(--navbar-fixed-height);
}
.app { .app {
min-width: 100%; min-width: 100%;
min-height: 100%; min-height: 100%;

View File

@ -9,29 +9,20 @@
</button> </button>
</div> </div>
</div> </div>
<div class="table_fix_head" data-simplebar> <div class="table_fix_head">
<div class="recent-view-container"> <div class="recent-view-container">
<table class="table table-responsive"> <table class="table table-responsive">
<tbody data-empty="{{t 'No conversations match your filters.' }}"></tbody> <thead id="recent-view-table-headers">
<thead>
<tr> <tr>
<th data-sort="stream_sort">{{t 'Channel' }}</th> <th class="recent-view-stream-header" data-sort="stream_sort">{{t 'Channel' }}</th>
<th data-sort="topic_sort">{{t 'Topic' }}</th> <th class="recent-view-topic-header" data-sort="topic_sort">{{t 'Topic' }}</th>
<th data-sort="unread_sort" data-tippy-content="{{t 'Sort by unread message count' }}" class="unread_sort tippy-zulip-delayed-tooltip hidden-for-spectators"> <th data-sort="unread_sort" data-tippy-content="{{t 'Sort by unread message count' }}" class="recent-view-unread-header unread_sort tippy-zulip-delayed-tooltip hidden-for-spectators">
<i class="zulip-icon zulip-icon-unread"></i> <i class="zulip-icon zulip-icon-unread"></i>
</th> </th>
<th class='participants_header'>{{t 'Participants' }}</th> <th class='recent-view-participants-header participants_header'>{{t 'Participants' }}</th>
<th data-sort="numeric" data-sort-prop="last_msg_id" class="last_msg_time_header active descend">{{t 'Time' }}</th> <th data-sort="numeric" data-sort-prop="last_msg_id" class="recent-view-last-msg-time-header last_msg_time_header active descend">{{t 'Time' }}</th>
</tr> </tr>
</thead> </thead>
</table> </table>
{{!-- Don't show the banner until we have some messages loaded. --}}
<div class="recent-view-load-more-container main-view-banner info notvisible">
<div class="last-fetched-message banner_content">{{t "This view is still loading messages."}}</div>
<button class="fetch-messages-button main-view-banner-action-button right_edge notvisible">
<div class="loading-indicator"></div>
<span class="button-label">{{t "Load more"}}</span>
</button>
</div>
</div> </div>
</div> </div>

View File

@ -76,6 +76,7 @@ function make_scroll_container() {
assert.equal(ev, "scroll.list_widget_container"); assert.equal(ev, "scroll.list_widget_container");
$scroll_container.cleared = true; $scroll_container.cleared = true;
}; };
$scroll_container.is = () => false;
return $scroll_container; return $scroll_container;
} }

View File

@ -7,6 +7,7 @@ const {run_test, noop} = require("./lib/test");
const $ = require("./lib/zjquery"); const $ = require("./lib/zjquery");
const {page_params} = require("./lib/zpage_params"); const {page_params} = require("./lib/zpage_params");
window.scrollTo = noop;
const test_url = () => "https://www.example.com"; const test_url = () => "https://www.example.com";
// We assign this in our test() wrapper. // We assign this in our test() wrapper.