diff --git a/templates/zerver/app/index.html b/templates/zerver/app/index.html
index d50a8e967f..a04bdf0640 100644
--- a/templates/zerver/app/index.html
+++ b/templates/zerver/app/index.html
@@ -231,15 +231,26 @@
-
+
+
+
+
+
{{ _('This view is still loading messages.') }}
+
diff --git a/web/src/list_widget.ts b/web/src/list_widget.ts
index cd39b26fc5..45005dacc6 100644
--- a/web/src/list_widget.ts
+++ b/web/src/list_widget.ts
@@ -18,6 +18,7 @@ type ListWidgetMeta = {
filtered_list: Item[];
reverse_mode: boolean;
$scroll_container: JQuery;
+ $scroll_listening_element: JQuery | JQuery;
};
// This type ensures the mutually exclusive nature of the predicate and filterer options.
@@ -261,6 +262,17 @@ export function create(
old_widget.clear_event_handlers();
}
+ let $scroll_listening_element: JQuery | JQuery = 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 = {
sorting_function: null,
sorting_functions: new Map(),
@@ -270,6 +282,7 @@ export function create(
reverse_mode: false,
filter_value: "",
$scroll_container: scroll_util.get_scroll_element(opts.$simplebar_container),
+ $scroll_listening_element,
};
const widget: ListWidget = {
@@ -417,20 +430,23 @@ export function create(
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 () {
- if (opts.post_scroll__pre_render_callback) {
- opts.post_scroll__pre_render_callback();
- }
+ 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();
+ }
- if (opts.is_scroll_position_for_render === undefined) {
- opts.is_scroll_position_for_render = is_scroll_position_for_render;
- }
+ if (opts.is_scroll_position_for_render === undefined) {
+ opts.is_scroll_position_for_render = is_scroll_position_for_render;
+ }
- const should_render = opts.is_scroll_position_for_render(this);
- if (should_render) {
- widget.render();
- }
- });
+ const should_render = opts.is_scroll_position_for_render(this);
+ if (should_render) {
+ widget.render();
+ }
+ },
+ );
if (opts.$parent_container) {
opts.$parent_container.on(
@@ -450,7 +466,12 @@ export function create(
},
clear_event_handlers() {
- meta.$scroll_container.off("scroll.list_widget_container");
+ // Since `$scroll_listening_element` is of type `JQuery | JQuery` instead
+ // of just `JQuery`, Typescript is expecting `off` to be called on
+ // TypeEventHandlers which is confusing.
+ //
+ // @ts-expect-error Maybe JQuery.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]");
diff --git a/web/src/recent_view_ui.ts b/web/src/recent_view_ui.ts
index 2d4763d715..b94098acae 100644
--- a/web/src/recent_view_ui.ts
+++ b/web/src/recent_view_ui.ts
@@ -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) {
- 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(
+ "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,20 +1769,24 @@ export function initialize({
});
// Unmute topic in a unmuted stream
- $("body").on("click", "#recent_view_table .stream_unmuted.on_hover_topic_unmute", (e) => {
- e.stopPropagation();
- assert(e.target instanceof HTMLElement);
- const $elt = $(e.target);
- const topic_row_index = $elt.closest("tr").index();
- focus_clicked_element(topic_row_index, COLUMNS.mute);
- user_topics.set_visibility_policy_for_element(
- $elt,
- user_topics.all_visibility_policies.INHERIT,
- );
- });
+ $("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);
+ const topic_row_index = $elt.closest("tr").index();
+ focus_clicked_element(topic_row_index, COLUMNS.mute);
+ user_topics.set_visibility_policy_for_element(
+ $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);
diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css
index 6f889420d8..f36ffc33ba 100644
--- a/web/styles/app_variables.css
+++ b/web/styles/app_variables.css
@@ -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%);
diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css
index fb348d27ef..d16a6114da 100644
--- a/web/styles/dark_theme.css
+++ b/web/styles/dark_theme.css
@@ -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%);
}
diff --git a/web/styles/recent_view.css b/web/styles/recent_view.css
index 87cd84ad0c..f78a188373 100644
--- a/web/styles/recent_view.css
+++ b/web/styles/recent_view.css
@@ -4,548 +4,519 @@
overflow: hidden;
display: flex;
flex-direction: column;
- max-height: 100vh;
-
- #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;
-
- & td {
- vertical-align: middle;
- padding: 3px 8px;
- }
-
- .recent_view_focusable {
- /* Use flexbox to align icons vertically */
- display: flex;
- align-items: center;
-
- .filter-icon {
- /* Maintain righthand space between icon
- and stream name. */
- margin-right: 3px;
- }
-
- & > * {
- outline: 0;
- }
-
- &:focus-within {
- /* Use the same color as the message feed pointer */
- box-shadow: 0 3px 0 var(--color-outline-focus);
- }
-
- &.change_visibility_policy.visibility-policy-popover-visible {
- .zulip-icon-inherit {
- opacity: 0.4;
- }
- }
-
- &.change_visibility_policy .zulip-icon-inherit {
- opacity: 0;
-
- &:focus {
- opacity: 0.2;
- }
- }
- }
-
- & a {
- color: hsl(205deg 47% 42%);
- text-decoration: none;
-
- &:hover {
- color: hsl(214deg 40% 58%);
- }
- }
-
- .empty-table-message {
- background-color: var(--color-background);
- padding: 3em 1em;
- }
-
- .fa-check-square-o,
- .fa-square-o {
- padding: 0 2px;
- width: 10px;
- }
-
- .fa-envelope {
- font-size: 0.7rem;
- margin-right: 2px;
- position: relative;
- top: -1px;
- opacity: 0.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 {
- margin: 20px 10px;
- align-items: center;
- }
-
- .fetch-messages-button {
- display: grid;
- justify-items: center;
-
- .loading_indicator_spinner {
- height: 20px;
- width: 20px;
- }
-
- path {
- fill: var(--color-recent-view-loading-spinner);
- }
- }
-
- .table_fix_head table {
- /* To keep border properties to the thead th. */
- border-collapse: separate;
-
- border-spacing: 0;
- width: 100%;
-
- 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;
- display: flex;
- /* Search box has no height without this in safari. */
- flex: 0 0 auto;
- flex-wrap: wrap;
- justify-content: flex-start;
- }
-
- .search_group {
- display: flex;
- flex-grow: 1;
- margin: 0 -27px 10px 0;
- }
-
- #recent_view_search {
- flex-grow: 1;
- padding-right: 20px;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- }
-
- .clear_search_button {
- /* Overrides app_components.css property. */
- padding-top: 4px !important;
- }
-
- .btn-recent-filters {
- border-radius: 40px;
- margin: 0 5px 10px 0;
-
- &:focus {
- background-color: hsl(0deg 0% 80%);
- outline: 0;
- }
-
- &.fake_disabled_button {
- cursor: not-allowed;
- opacity: 0.5;
-
- &:hover {
- background-color: hsl(0deg 0% 100%);
- border-color: hsl(0deg 0% 80%);
- }
- }
- }
-
- .btn-recent-selected {
- background-color: hsl(0deg 11% 93%);
- }
-
- .unread_count {
- /* Focus underline can only occupy the total length of the unread count */
- margin-right: 1px;
- margin-left: 1px;
- align-self: center;
- background-color: hsl(105deg 2% 50%);
- }
-
- .unread_mention_info:not(:empty) {
- /* Zero out right margin from left sidebar presentation. */
- margin-right: 0;
- /* Match with its font-size. */
- line-height: 14px;
- /* Present a default/arrow cursor */
- cursor: default;
- }
-
- .unread_hidden {
- visibility: hidden;
- }
-
- .flex_container_pm {
- /* Flex container to fit in user circle and group icon */
- display: flex;
- justify-content: space-between;
-
- .tippy-content {
- font-weight: 400;
- }
- }
-
- .flex_container {
- display: flex;
- align-items: center;
- }
-
- .flex_container .right_part {
- margin-left: auto;
- display: inline-flex;
- align-items: center;
- }
-
- .recent_topic_actions {
- /* Add spacing between mention marker, unread count
- and mute icon */
- margin-left: 5px;
- display: flex;
- flex-flow: row nowrap;
- }
-
- .mention_in_unread {
- opacity: 0.7;
- }
-
- .recent_topic_actions.dummy_action_button {
- visibility: hidden;
- }
-
- .recent_topic_actions .recent_view_focusable {
- /* Keep a uniform distance from the focus-within
- indicator at bottom. */
- padding-bottom: 3px;
- /* But push down with margin by the same amount,
- so as to preserve vertical alignment introduced
- by the parent flexbox. */
- margin-top: 3px;
- }
-
- .recent_topic_actions .recipient_bar_icon {
- /* Zero out padding used in recipient bar. */
- padding-right: 0;
- padding-left: 0;
- }
-
- .recent_view_participants {
- display: inline-flex; /* Causes LI items to display in row. */
- list-style-type: none;
- margin: auto; /* Centers vertically / horizontally in flex container. */
- height: 24px;
- padding: 4px;
- border-radius: 6px;
- overflow: hidden;
-
- /*
- By using the row-reverse layout, the visual ordering will be opposite of
- the DOM ordering. This will allows us to stack the items in the opposite
- direction of the natural stacking order without having to mess with the
- zIndex value. The MAJOR DOWNSIDE is that the HTML itself now reads
- backwards, which super janky.
- */
- flex-direction: row-reverse;
- }
-
- .recent_view_participant_item {
- height: 24px;
- margin: 0;
- padding: 0 1.5px;
- position: relative;
- min-width: 24px;
- cursor: pointer;
-
- .fa-user {
- opacity: 0.7;
- }
- }
-
- .recent_view_participant_avatar,
- .recent_view_participant_overflow {
- border: 0;
- border-radius: 6px;
- color: hsl(0deg 0% 100%);
- display: block;
- height: 24px;
- text-align: center;
- background-color: hsl(0deg 0% 88%);
- }
-
- .recent_view_participant_overflow {
- color: hsl(0deg 0% 0%);
- line-height: 24px;
- }
-
- & tr {
- background-color: hsl(100deg 11% 96%);
-
- &:hover {
- background-color: hsl(210deg 100% 97%);
-
- .change_visibility_policy .zulip-icon-inherit {
- opacity: 0.4;
- }
- }
- }
-
- .unread_topic {
- background-color: hsl(0deg 0% 100%);
- }
-
- .last_msg_time {
- float: left;
- margin-right: 5px;
- }
-
- & thead th {
- background-color: hsl(0deg 0% 100%);
- 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,
- &[data-sort]:hover::after {
- content: " \f0d8";
- white-space: pre;
- display: inline-block;
- position: absolute;
- padding-top: 3px;
- font: normal normal normal 12px/1 FontAwesome;
- font-size: inherit;
- }
-
- &.active {
- opacity: 1;
- transition: opacity 100ms ease-out;
-
- &.descend::after {
- content: " \f0d7";
- }
- }
-
- &[data-sort]:hover {
- cursor: pointer;
- background-color: hsl(0deg 0% 95%);
- transition: background-color 100ms ease-in-out;
-
- &:not(.active)::after {
- opacity: 0.3;
- }
- }
- }
-
- /* 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 {
- word-break: break-word;
- hyphens: auto;
- }
- }
-
- .recent_topic_name {
- width: 40%;
-
- & a {
- word-break: break-word;
- /* No hyphes for word break since it caused hyphens to appear before
- the ellipsis `longText-...` which is not desirable. Ellipsis appears due
- to the line clamp applied below.
- */
- }
-
- .line_clamp {
- /* This -webkit-box display property is webkit-specific, but
- it appears that line clamping works fine for this component
- on Firefox anyway. */
- /* stylelint-disable-next-line value-no-vendor-prefix */
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- }
-
- .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
- element. This ensures it is always visible.
- 20px = space occupied by ::after (icon) +
- some extra padding.
- */
- padding-right: 20px;
- }
-
- @media (width < $md_min) {
- /* Hide participants and last message time
- on smaller screens. This ensures user always
- has a nice UI experience. */
- .recent_topic_users,
- .recent_topic_timestamp,
- thead .participants_header,
- thead .last_msg_time_header {
- display: none;
- }
-
- .recent_topic_actions {
- margin-right: 5px;
- font-size: 15px;
- }
- }
-
- .private_conversation_row {
- .recent_topic_stream {
- /* Reduce padding of stream section so that user status
- icon can have more padding without impacting height of the row */
- padding: 5px 0 5px 8px;
- }
-
- .pm_status_icon {
- display: flex;
- justify-content: center;
- align-items: center;
- /* Increasing vertical padding any further will increase
- the height of the row. */
- padding: 8px;
- position: relative;
- right: -8px; /* To cancel padding-right */
- /* To accommodate fa-group icon */
- width: 14px;
- height: 14px;
-
- .fa-group,
- .zulip-icon.zulip-icon-bot {
- font-size: 0.8rem;
- opacity: 0.6;
- }
-
- .user_circle {
- /* Shrink the user activity circle for the Recent Conversations context. */
- min-width: 7px;
- height: 7px;
- float: left;
- position: unset;
- }
- }
- }
- }
-
- #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;
- position: absolute;
- top: 200px;
- left: 0;
- right: 0;
- margin: auto;
-
- .loading_indicator_spinner {
- position: relative;
- top: -7px;
- }
- }
- }
-
- .stream-privacy {
- .zulip-icon {
- position: relative;
- left: -1px;
- top: 1.5px;
- }
- }
+ position: sticky;
+ top: var(--navbar-fixed-height);
+ z-index: 1;
+}
+
+.recent_view_container #recent_view_table {
+ max-width: 100%;
+ overflow: hidden !important;
+ display: flex;
+ flex-direction: column;
+ 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;
- position: relative;
+ 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 {
+ /* Use flexbox to align icons vertically */
+ display: flex;
+ align-items: center;
+
+ .filter-icon {
+ /* Maintain righthand space between icon
+ and stream name. */
+ margin-right: 3px;
+ }
+
+ & > * {
+ outline: 0;
+ }
+
+ &:focus-within {
+ /* Use the same color as the message feed pointer */
+ box-shadow: 0 3px 0 var(--color-outline-focus);
+ }
+
+ &.change_visibility_policy.visibility-policy-popover-visible {
+ .zulip-icon-inherit {
+ opacity: 0.4;
+ }
+ }
+
+ &.change_visibility_policy .zulip-icon-inherit {
+ opacity: 0;
+
+ &:focus {
+ opacity: 0.2;
+ }
+ }
+ }
+
+ & a {
+ color: var(--color-recent-view-link);
+ text-decoration: none;
+
+ &:hover {
+ color: var(--color-recent-view-link-hover);
+ }
+ }
+
+ .empty-table-message {
+ background-color: var(--color-background);
+ padding: 3em 1em;
+ }
+
+ .fa-check-square-o,
+ .fa-square-o {
+ padding: 0 2px;
+ width: 10px;
+ }
+
+ .fa-envelope {
+ font-size: 0.7rem;
+ margin-right: 2px;
+ position: relative;
+ top: -1px;
+ opacity: 0.6;
+ }
+
+ .table_fix_head {
+ padding: 0 !important;
+ }
+
+ .recent-view-load-more-container {
+ margin: 20px 10px;
+ align-items: center;
+ }
+
+ .fetch-messages-button {
+ display: grid;
+ justify-items: center;
+
+ .loading_indicator_spinner {
+ height: 20px;
+ width: 20px;
+ }
+
+ path {
+ fill: var(--color-recent-view-loading-spinner);
+ }
+ }
+
+ .table_fix_head table th {
+ padding: 8px;
+ text-align: left;
+ }
+
+ #recent_view_filter_buttons {
+ 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 {
+ display: flex;
+ flex-grow: 1;
+ margin: 0 -27px 10px 0;
+ }
+
+ #recent_view_search {
+ flex-grow: 1;
+ padding-right: 20px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ .clear_search_button {
+ /* Overrides app_components.css property. */
+ padding-top: 4px !important;
+ }
+
+ .btn-recent-filters {
+ border-radius: 40px;
+ margin: 0 5px 10px 0;
+
+ &:focus {
+ background-color: hsl(0deg 0% 80%);
+ outline: 0;
+ }
+
+ &.fake_disabled_button {
+ cursor: not-allowed;
+ opacity: 0.5;
+
+ &:hover {
+ background-color: hsl(0deg 0% 100%);
+ border-color: hsl(0deg 0% 80%);
+ }
+ }
+ }
+
+ .btn-recent-selected {
+ background-color: hsl(0deg 11% 93%);
+ }
+
+ .unread_count {
+ /* Focus underline can only occupy the total length of the unread count */
+ margin-right: 1px;
+ margin-left: 1px;
+ align-self: center;
+ background-color: hsl(105deg 2% 50%);
+ }
+
+ .unread_mention_info:not(:empty) {
+ /* Zero out right margin from left sidebar presentation. */
+ margin-right: 0;
+ /* Match with its font-size. */
+ line-height: 14px;
+ /* Present a default/arrow cursor */
+ cursor: default;
+ }
+
+ .unread_hidden {
+ visibility: hidden;
+ }
+
+ .flex_container_pm {
+ /* Flex container to fit in user circle and group icon */
+ display: flex;
+ justify-content: space-between;
+
+ .tippy-content {
+ font-weight: 400;
+ }
+ }
+
+ .flex_container {
+ display: flex;
+ align-items: center;
+ }
+
+ .flex_container .right_part {
+ margin-left: auto;
+ display: inline-flex;
+ align-items: center;
+ }
+
+ .recent_topic_actions {
+ /* Add spacing between mention marker, unread count
+ and mute icon */
+ margin-left: 5px;
+ display: flex;
+ flex-flow: row nowrap;
+ }
+
+ .mention_in_unread {
+ opacity: 0.7;
+ }
+
+ .recent_topic_actions.dummy_action_button {
+ visibility: hidden;
+ }
+
+ .recent_topic_actions .recent_view_focusable {
+ /* Keep a uniform distance from the focus-within
+ indicator at bottom. */
+ padding-bottom: 3px;
+ /* But push down with margin by the same amount,
+ so as to preserve vertical alignment introduced
+ by the parent flexbox. */
+ margin-top: 3px;
+ }
+
+ .recent_topic_actions .recipient_bar_icon {
+ /* Zero out padding used in recipient bar. */
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ .recent_view_participants {
+ display: inline-flex; /* Causes LI items to display in row. */
+ list-style-type: none;
+ margin: auto; /* Centers vertically / horizontally in flex container. */
+ height: 24px;
+ padding: 4px;
+ border-radius: 6px;
+ overflow: hidden;
+
+ /*
+ By using the row-reverse layout, the visual ordering will be opposite of
+ the DOM ordering. This will allows us to stack the items in the opposite
+ direction of the natural stacking order without having to mess with the
+ zIndex value. The MAJOR DOWNSIDE is that the HTML itself now reads
+ backwards, which super janky.
+ */
+ flex-direction: row-reverse;
+ }
+
+ .recent_view_participant_item {
+ height: 24px;
+ margin: 0;
+ padding: 0 1.5px;
+ position: relative;
+ min-width: 24px;
+ cursor: pointer;
+
+ .fa-user {
+ opacity: 0.7;
+ }
+ }
+
+ .recent_view_participant_avatar,
+ .recent_view_participant_overflow {
+ border: 0;
+ border-radius: 6px;
+ color: hsl(0deg 0% 100%);
+ display: block;
+ height: 24px;
+ text-align: center;
+ background-color: hsl(0deg 0% 88%);
+ }
+
+ .recent_view_participant_overflow {
+ color: hsl(0deg 0% 0%);
+ line-height: 24px;
+ }
+
+ & tr {
+ background-color: var(--color-background-recent-view-row);
+
+ &:hover {
+ background-color: var(--color-background-recent-view-row-hover);
+
+ .change_visibility_policy .zulip-icon-inherit {
+ opacity: 0.4;
+ }
+ }
+ }
+
+ .unread_topic {
+ background-color: var(--color-background-recent-view-unread-row);
+
+ &:hover {
+ background-color: var(
+ --color-background-recent-view-unread-row-hover
+ );
+ }
+ }
+
+ .last_msg_time {
+ float: left;
+ margin-right: 5px;
+ }
+
+ & thead th {
+ background-color: hsl(0deg 0% 100%);
+ color: inherit;
+ border-top: 1px solid hsl(0deg 0% 0% / 20%) !important;
+ border-bottom: 1px solid hsl(0deg 0% 0% / 20%) !important;
+ z-index: 1;
+
+ &.active::after,
+ &[data-sort]:hover::after {
+ content: " \f0d8";
+ white-space: pre;
+ display: inline-block;
+ position: absolute;
+ padding-top: 3px;
+ font: normal normal normal 12px/1 FontAwesome;
+ font-size: inherit;
+ }
+
+ &.active {
+ opacity: 1;
+ transition: opacity 100ms ease-out;
+
+ &.descend::after {
+ content: " \f0d7";
+ }
+ }
+
+ &[data-sort]:hover {
+ cursor: pointer;
+ background-color: hsl(0deg 0% 95%);
+ transition: background-color 100ms ease-in-out;
+
+ &:not(.active)::after {
+ opacity: 0.3;
+ }
+ }
+ }
+
+ .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 {
+ padding: 8px 0 8px 8px;
+
+ & a {
+ word-break: break-word;
+ hyphens: auto;
+ }
+ }
+
+ .recent_topic_name {
+ width: 40%;
+
+ & a {
+ word-break: break-word;
+ /* No hyphes for word break since it caused hyphens to appear before
+ the ellipsis `longText-...` which is not desirable. Ellipsis appears due
+ to the line clamp applied below.
+ */
+ }
+
+ .line_clamp {
+ /* This -webkit-box display property is webkit-specific, but
+ it appears that line clamping works fine for this component
+ on Firefox anyway. */
+ /* stylelint-disable-next-line value-no-vendor-prefix */
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+ }
+
+ & thead .last_msg_time_header {
+ /* The responsive table of bootstrap
+ somehow ignores the width of ::after
+ element. This ensures it is always visible.
+ 20px = space occupied by ::after (icon) +
+ some extra padding.
+ */
+ padding-right: 20px;
+ }
+
+ @media (width < $md_min) {
+ /* Hide participants and last message time
+ on smaller screens. This ensures user always
+ has a nice UI experience. */
+ .recent_topic_users,
+ .recent_topic_timestamp,
+ thead .participants_header,
+ thead .last_msg_time_header {
+ display: none;
+ }
+
+ .recent_topic_actions {
+ margin-right: 5px;
+ font-size: 15px;
+ }
+ }
+
+ .private_conversation_row {
+ .recent_topic_stream {
+ /* Reduce padding of stream section so that user status
+ icon can have more padding without impacting height of the row */
+ padding: 5px 0 5px 8px;
+ }
+
+ .pm_status_icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ /* Increasing vertical padding any further will increase
+ the height of the row. */
+ padding: 8px;
+ position: relative;
+ right: -8px; /* To cancel padding-right */
+ /* To accommodate fa-group icon */
+ width: 14px;
+ height: 14px;
+
+ .fa-group,
+ .zulip-icon.zulip-icon-bot {
+ font-size: 0.8rem;
+ opacity: 0.6;
+ }
+
+ .user_circle {
+ /* Shrink the user activity circle for the Recent Conversations context. */
+ min-width: 7px;
+ height: 7px;
+ float: left;
+ position: unset;
+ }
+ }
+ }
+
+ .stream-privacy .zulip-icon {
+ position: relative;
+ left: -1px;
+ top: 1.5px;
+ }
+}
+
+#recent_view_bottom_whitespace {
+ #recent_view_loading_messages_indicator,
+ .bottom-messages-logo {
+ display: block;
+ position: absolute;
+ top: 200px;
+ left: 0;
+ right: 0;
+ margin: auto;
+
+ .loading_indicator_spinner {
+ position: relative;
+ top: -7px;
+ }
+ }
}
#recent-view-filter_widget {
diff --git a/web/styles/zulip.css b/web/styles/zulip.css
index 90873f0df7..eda0ba9205 100644
--- a/web/styles/zulip.css
+++ b/web/styles/zulip.css
@@ -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%;
diff --git a/web/templates/recent_view_table.hbs b/web/templates/recent_view_table.hbs
index c957a68d28..2c53e570c5 100644
--- a/web/templates/recent_view_table.hbs
+++ b/web/templates/recent_view_table.hbs
@@ -9,29 +9,20 @@
-