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 class="recent_view_container">
<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 class="bottom-messages-logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 773.12 773.12">
@ -240,6 +244,13 @@
</div>
<div id="recent_view_loading_messages_indicator"></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 id="inbox-view">

View File

@ -18,6 +18,7 @@ type ListWidgetMeta<Key, Item = Key> = {
filtered_list: Item[];
reverse_mode: boolean;
$scroll_container: JQuery;
$scroll_listening_element: JQuery | JQuery<Window>;
};
// 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();
}
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> = {
sorting_function: null,
sorting_functions: new Map(),
@ -270,6 +282,7 @@ export function create<Key, Item = Key>(
reverse_mode: false,
filter_value: "",
$scroll_container: scroll_util.get_scroll_element(opts.$simplebar_container),
$scroll_listening_element,
};
const widget: ListWidget<Key, Item> = {
@ -417,7 +430,9 @@ export function create<Key, Item = Key>(
set_up_event_handlers() {
// 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.
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) {
opts.post_scroll__pre_render_callback();
}
@ -430,7 +445,8 @@ export function create<Key, Item = Key>(
if (should_render) {
widget.render();
}
});
},
);
if (opts.$parent_container) {
opts.$parent_container.on(
@ -450,7 +466,12 @@ export function create<Key, Item = Key>(
},
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) {
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 type {ConversationData} from "./recent_view_data";
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 stream_data from "./stream_data";
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
// any new events.
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 {
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
// is shown, so we keep the state updated and update the banner only
// once it's actually rendered.
if ($("#recent_view_table table tbody").length) {
if ($("#recent-view-content-tbody tr").length) {
update_load_more_banner();
}
}
@ -278,7 +280,7 @@ function get_row_type(row: number): string {
// Return "private" or "stream"
// We use CSS method for finding row type until topics_widget gets initialized.
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 is_private = $topic_row.attr("data-private");
if (is_private) {
@ -310,7 +312,7 @@ function set_table_focus(row: number, col: number, using_keyboard = false): bool
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) {
row_focus = 0;
// 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";
if (using_keyboard) {
const scroll_element = $(
"#recent_view_table .table_fix_head .simplebar-content-wrapper",
)[0]!;
const scroll_element = $("html")[0]!;
const half_height_of_visible_area = scroll_element.offsetHeight / 2;
const topic_offset = topic_offset_to_visible_area($topic_row);
@ -379,7 +379,7 @@ export function get_focused_row_message(): Message | 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_id = $topic_row.attr("id");
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.
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.
const scroll_container_top = scroll_container_props.top + thead_height;
const compose_height = $("#compose").outerHeight(true)!;
const scroll_container_bottom = scroll_container_props.bottom - compose_height;
// Rows are only visible below thead bottom and above compose top.
const thead_bottom = $("#recent-view-table-headers")[0]!.getBoundingClientRect().bottom;
const compose_top = window.innerHeight - $("#compose").outerHeight(true)!;
const topic_props = $topic_row[0]!.getBoundingClientRect();
// Topic is above the visible scroll region.
if (topic_props.top < scroll_container_top) {
if (topic_props.top < thead_bottom) {
return "above";
// Topic is below the visible scroll region.
} else if (topic_props.bottom > scroll_container_bottom) {
} else if (topic_props.bottom > compose_top) {
return "below";
}
@ -1155,9 +1151,7 @@ function recenter_focus_if_off_screen(): void {
return;
}
const table_wrapper_element = $("#recent_view_table .table_fix_head")[0]!;
const $topic_rows = $("#recent_view_table table tbody tr");
const $topic_rows = $("#recent-view-content-tbody tr");
if (row_focus >= $topic_rows.length) {
// User used a filter which reduced
// the number of visible rows.
@ -1172,9 +1166,10 @@ function recenter_focus_if_off_screen(): void {
if (topic_offset !== "visible") {
// Get the element at the center of the table.
const position = table_wrapper_element.getBoundingClientRect();
const topic_center_x = (position.left + position.right) / 2;
const topic_center_y = (position.top + position.bottom) / 2;
const thead_props = $("#recent-view-table-headers")[0]!.getBoundingClientRect();
const compose_top = window.innerHeight - $("#compose").outerHeight(true)!;
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);
if (
@ -1184,7 +1179,7 @@ function recenter_focus_if_off_screen(): void {
// There are two theoretical reasons that the center
// element might be null. One is that we haven't rendered
// 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
// 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 {
const table_bottom_margin = 100; // Extra margin at the bottom of table.
const table_row_height = 50;
return (
scroll_container.scrollTop +
scroll_container.clientHeight +
table_bottom_margin +
table_row_height >
scroll_container.scrollHeight
);
function is_scroll_position_for_render(): boolean {
const scroll_position = window.scrollY;
const window_height = window.innerHeight;
// We allocate `--max-unexpanded-compose-height` in empty space
// below the last rendered row in recent view.
//
// We don't want user to see this empty space until there are no
// new rows to render when the user is scrolling to the bottom of
// 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 {
// 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();
setTimeout(() => {
revive_current_focus();
@ -1261,6 +1266,11 @@ export function complete_rerender(): void {
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({
search_val: $("#recent_view_search").val() ?? "",
...get_recent_view_filters_params(),
@ -1273,7 +1283,7 @@ export function complete_rerender(): void {
// was not the first view loaded in the app.
show_selected_filters();
const $container = $("#recent_view_table table tbody");
const $container = $("#recent-view-content-tbody");
$container.empty();
topics_widget = list_widget.create($container, mapped_topic_values, {
name: "recent_view_table",
@ -1296,7 +1306,7 @@ export function complete_rerender(): void {
...list_widget.generic_sort_functions("numeric", ["last_msg_id"]),
},
html_selector: get_topic_row,
$simplebar_container: $("#recent_view_table .table_fix_head"),
$simplebar_container: $("html"),
callback_after_render,
is_scroll_position_for_render,
post_scroll__pre_render_callback() {
@ -1309,6 +1319,10 @@ export function complete_rerender(): 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({
highlight_view_in_left_sidebar: left_sidebar_navigation_area.highlight_recent_view,
$view: $("#recent_view"),
@ -1321,6 +1335,12 @@ export function show(): void {
set_visible: recent_view_util.set_visible,
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")) {
const html_body = render_introduce_zulip_view_modal({
@ -1348,7 +1368,11 @@ function filter_buttons(): JQuery {
}
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;
last_scroll_offset = window.scrollY;
views_util.hide({
$view: $("#recent_view"),
set_visible: recent_view_util.set_visible,
@ -1433,9 +1457,8 @@ function down_arrow_navigation(): void {
}
function get_page_up_down_delta(): number {
const table_height = $("#recent_view_table .table_fix_head").height()!;
const table_header_height = $("#recent_view_table table thead").height()!;
const compose_box_height = $("#compose").height()!;
const thead_bottom = $("#recent-view-table-headers")[0]!.getBoundingClientRect().bottom;
const compose_box_top = window.innerHeight - $("#compose").outerHeight(true)!;
// 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
// 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.
const scrolling_reduction_to_maintain_context = 75;
const delta =
table_height -
table_header_height -
compose_box_height -
scrolling_reduction_to_maintain_context;
const delta = compose_box_top - thead_bottom - scrolling_reduction_to_maintain_context;
return delta;
}
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 new_scrollTop = $scroll_container.scrollTop()! - delta;
const new_scrollTop = window.scrollY - delta;
if (new_scrollTop <= 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 {
const $scroll_container = scroll_util.get_scroll_element(
$("#recent_view_table .table_fix_head"),
);
const delta = get_page_up_down_delta();
const new_scrollTop = $scroll_container.scrollTop()! + delta;
const table_height = $("#recent_view_table .table_fix_head").height()!;
if (new_scrollTop >= table_height) {
const new_scrollTop = window.scrollY + delta;
const max_scroll_top = document.body.scrollHeight - window.innerHeight;
if (new_scrollTop >= max_scroll_top) {
assert(topics_widget !== undefined);
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 {
@ -1706,14 +1736,18 @@ export function initialize({
}): void {
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");
assert(user_id_string !== undefined);
const participant_user_id = Number.parseInt(user_id_string, 10);
e.stopPropagation();
assert(this instanceof Element);
on_click_participant(this, participant_user_id);
});
},
);
$("body").on(
"keydown",
@ -1722,7 +1756,7 @@ export function initialize({
);
// 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();
assert(e.target instanceof HTMLElement);
const $elt = $(e.target);
@ -1735,7 +1769,10 @@ export function initialize({
});
// 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();
assert(e.target instanceof HTMLElement);
const $elt = $(e.target);
@ -1745,10 +1782,11 @@ export function initialize({
$elt,
user_topics.all_visibility_policies.INHERIT,
);
});
},
);
// 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();
assert(e.target instanceof HTMLElement);
const $elt = $(e.target);
@ -1761,7 +1799,7 @@ export function initialize({
});
// 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();
assert(e.target instanceof HTMLElement);
const $elt = $(e.target);
@ -1779,7 +1817,7 @@ export function initialize({
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();
assert(e.currentTarget instanceof HTMLElement);
const $elt = $(e.currentTarget);

View File

@ -263,6 +263,16 @@
--color-buddy-list-highlighted-user: hsl(120deg 12.3% 71.4% / 38%);
--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 */
--color-compose-send-button-icon-color: hsl(0deg 0% 100%);
--color-compose-send-button-background: hsl(240deg 96% 68%);
@ -591,6 +601,16 @@
--color-buddy-list-highlighted-user: hsl(136deg 25% 73% / 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 */
--color-compose-send-button-focus-shadow: hsl(0deg 0% 100% / 80%);
--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,
#recent_view_table thead th {
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 {
border-color: hsl(0deg 0% 0% / 60%);
.fa-envelope,
.fa-group {
opacity: 0.7;
@ -788,8 +761,7 @@
#settings_page .sidebar-wrapper *,
table,
table th,
table td,
#recent_view_table table td {
table td {
border-color: hsl(0deg 0% 0% / 20%);
}

View File

@ -4,27 +4,39 @@
overflow: hidden;
display: flex;
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%;
overflow: hidden !important;
display: flex;
flex-direction: column;
border-style: solid;
border-color: hsl(0deg 0% 88%);
border-width: 0 1px;
border-radius: 0;
/* To make the table span full height
* when rows don't reach bottom of the
* window. This makes the border span
* fully to bottom in that case.
*/
min-height: 100vh;
border: 0;
}
#recent_view_table .table,
#recent-view-content-table {
/* To keep border properties to the thead th. */
border-collapse: separate;
border-spacing: 0;
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 {
vertical-align: middle;
padding: 3px 8px;
border-top: 1px solid var(--color-border-recent-view-row);
}
.recent_view_focusable {
@ -63,11 +75,11 @@
}
& a {
color: hsl(205deg 47% 42%);
color: var(--color-recent-view-link);
text-decoration: none;
&:hover {
color: hsl(214deg 40% 58%);
color: var(--color-recent-view-link-hover);
}
}
@ -92,23 +104,6 @@
.table_fix_head {
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 {
@ -130,46 +125,19 @@
}
}
.table_fix_head table {
/* To keep border properties to the thead th. */
border-collapse: separate;
border-spacing: 0;
width: 100%;
th {
.table_fix_head table th {
padding: 8px;
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 {
padding-top: 12px;
margin: 0 10px;
padding: 12px 10px 0;
display: flex;
/* Search box has no height without this in safari. */
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: flex-start;
background: var(--color-background);
}
.search_group {
@ -338,10 +306,10 @@
}
& tr {
background-color: hsl(100deg 11% 96%);
background-color: var(--color-background-recent-view-row);
&:hover {
background-color: hsl(210deg 100% 97%);
background-color: var(--color-background-recent-view-row-hover);
.change_visibility_policy .zulip-icon-inherit {
opacity: 0.4;
@ -350,7 +318,13 @@
}
.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 {
@ -363,8 +337,6 @@
color: inherit;
border-top: 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;
&.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
as new messages arrive from the server. */
.recent_topic_stream {
width: 25%;
padding: 8px 0 8px 8px;
& a {
@ -433,14 +432,6 @@
}
}
.recent_topic_users {
width: 20%;
}
.recent_topic_timestamp {
width: 15%;
}
& thead .last_msg_time_header {
/* The responsive table of bootstrap
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 {
/* 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,
.bottom-messages-logo {
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 {
display: inline-flex;
width: 150px;

View File

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

View File

@ -9,29 +9,20 @@
</button>
</div>
</div>
<div class="table_fix_head" data-simplebar>
<div class="table_fix_head">
<div class="recent-view-container">
<table class="table table-responsive">
<tbody data-empty="{{t 'No conversations match your filters.' }}"></tbody>
<thead>
<thead id="recent-view-table-headers">
<tr>
<th data-sort="stream_sort">{{t 'Channel' }}</th>
<th 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 class="recent-view-stream-header" data-sort="stream_sort">{{t 'Channel' }}</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="recent-view-unread-header unread_sort tippy-zulip-delayed-tooltip hidden-for-spectators">
<i class="zulip-icon zulip-icon-unread"></i>
</th>
<th class='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 class='recent-view-participants-header participants_header'>{{t 'Participants' }}</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>
</thead>
</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>

View File

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

View File

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