mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	recent_topics: Add Private message to recent_topics.
This commit adds private messages to the Recent topics view, to make it an all-encompassing overview of recent activity visible to the user. We add a filter "Include PM" to toggle whether PMs should be shown in recent topics. Fixes #19449.
This commit is contained in:
		@@ -25,6 +25,7 @@ const message_lists = mock_esm("../../static/js/message_lists");
 | 
			
		||||
const message_util = mock_esm("../../static/js/message_util");
 | 
			
		||||
const notifications = mock_esm("../../static/js/notifications");
 | 
			
		||||
const pm_list = mock_esm("../../static/js/pm_list");
 | 
			
		||||
const recent_topics_data = mock_esm("../../static/js/recent_topics_data");
 | 
			
		||||
const resize = mock_esm("../../static/js/resize");
 | 
			
		||||
const stream_list = mock_esm("../../static/js/stream_list");
 | 
			
		||||
const unread_ops = mock_esm("../../static/js/unread_ops");
 | 
			
		||||
@@ -91,6 +92,7 @@ run_test("insert_message", ({override}) => {
 | 
			
		||||
    helper.redirect(message_util, "add_new_messages_data");
 | 
			
		||||
    helper.redirect(message_util, "add_new_messages");
 | 
			
		||||
    helper.redirect(notifications, "received_messages");
 | 
			
		||||
    helper.redirect(recent_topics_data, "process_message");
 | 
			
		||||
    helper.redirect(resize, "resize_page_components");
 | 
			
		||||
    helper.redirect(stream_list, "update_streams_sidebar");
 | 
			
		||||
    helper.redirect(unread_ops, "process_visible");
 | 
			
		||||
@@ -113,6 +115,7 @@ run_test("insert_message", ({override}) => {
 | 
			
		||||
        [unread_ops, "process_visible"],
 | 
			
		||||
        [notifications, "received_messages"],
 | 
			
		||||
        [stream_list, "update_streams_sidebar"],
 | 
			
		||||
        [recent_topics_data, "process_message"],
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Despite all of our stubbing/mocking, the call to
 | 
			
		||||
 
 | 
			
		||||
@@ -146,7 +146,6 @@ mock_esm("../../static/js/top_left_corner", {
 | 
			
		||||
mock_esm("../../static/js/unread", {
 | 
			
		||||
    num_unread_for_topic: (stream_id, topic) => {
 | 
			
		||||
        if (stream_id === 1 && topic === "topic-1") {
 | 
			
		||||
            // Only stream1, topic-1 is read.
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
        return 1;
 | 
			
		||||
@@ -281,6 +280,7 @@ function generate_topic_data(topic_info_array) {
 | 
			
		||||
            other_sender_names_html: "",
 | 
			
		||||
            invite_only: false,
 | 
			
		||||
            is_web_public: true,
 | 
			
		||||
            is_private: false,
 | 
			
		||||
            last_msg_time: "Just now",
 | 
			
		||||
            last_msg_url: "https://www.example.com",
 | 
			
		||||
            full_last_msg_date_time: "date at time",
 | 
			
		||||
@@ -290,7 +290,7 @@ function generate_topic_data(topic_info_array) {
 | 
			
		||||
            stream_id,
 | 
			
		||||
            stream_url: "https://www.example.com",
 | 
			
		||||
            topic,
 | 
			
		||||
            topic_key: get_topic_key(stream_id, topic),
 | 
			
		||||
            conversation_key: get_topic_key(stream_id, topic),
 | 
			
		||||
            topic_url: "https://www.example.com",
 | 
			
		||||
            unread_count,
 | 
			
		||||
            muted,
 | 
			
		||||
@@ -343,6 +343,7 @@ test("test_recent_topics_show", ({mock_template, override}) => {
 | 
			
		||||
        filter_participated: false,
 | 
			
		||||
        filter_unread: false,
 | 
			
		||||
        filter_muted: false,
 | 
			
		||||
        filter_pm: false,
 | 
			
		||||
        search_val: "",
 | 
			
		||||
        is_spectator: false,
 | 
			
		||||
    };
 | 
			
		||||
@@ -375,6 +376,7 @@ test("test_filter_all", ({mock_template}) => {
 | 
			
		||||
        filter_participated: false,
 | 
			
		||||
        filter_unread: false,
 | 
			
		||||
        filter_muted: false,
 | 
			
		||||
        filter_pm: false,
 | 
			
		||||
        search_val: "",
 | 
			
		||||
        is_spectator: true,
 | 
			
		||||
    };
 | 
			
		||||
@@ -401,8 +403,8 @@ test("test_filter_all", ({mock_template}) => {
 | 
			
		||||
    rt.process_messages([messages[0]]);
 | 
			
		||||
 | 
			
		||||
    expected_data_to_replace_in_list_widget = [
 | 
			
		||||
        {last_msg_id: 10, participated: true},
 | 
			
		||||
        {last_msg_id: 1, participated: true},
 | 
			
		||||
        {last_msg_id: 10, participated: true, type: "stream"},
 | 
			
		||||
        {last_msg_id: 1, participated: true, type: "stream"},
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    row_data = row_data.concat(generate_topic_data([[1, "topic-7", 1, true, true]]));
 | 
			
		||||
@@ -429,6 +431,7 @@ test("test_filter_unread", ({mock_template}) => {
 | 
			
		||||
            filter_participated: false,
 | 
			
		||||
            filter_unread: expected_filter_unread,
 | 
			
		||||
            filter_muted: false,
 | 
			
		||||
            filter_pm: false,
 | 
			
		||||
            search_val: "",
 | 
			
		||||
            is_spectator: false,
 | 
			
		||||
        });
 | 
			
		||||
@@ -482,34 +485,42 @@ test("test_filter_unread", ({mock_template}) => {
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 11,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 10,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 9,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 7,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 5,
 | 
			
		||||
            participated: false,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 4,
 | 
			
		||||
            participated: false,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 3,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 1,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
@@ -539,6 +550,7 @@ test("test_filter_participated", ({mock_template}) => {
 | 
			
		||||
            filter_participated: expected_filter_participated,
 | 
			
		||||
            filter_unread: false,
 | 
			
		||||
            filter_muted: false,
 | 
			
		||||
            filter_pm: false,
 | 
			
		||||
            search_val: "",
 | 
			
		||||
            is_spectator: false,
 | 
			
		||||
        });
 | 
			
		||||
@@ -602,34 +614,42 @@ test("test_filter_participated", ({mock_template}) => {
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 11,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 10,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 9,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 7,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 5,
 | 
			
		||||
            participated: false,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 4,
 | 
			
		||||
            participated: false,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 3,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 1,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
@@ -656,7 +676,7 @@ test("basic assertions", ({mock_template}) => {
 | 
			
		||||
 | 
			
		||||
    mock_template("recent_topics_table.hbs", false, () => {});
 | 
			
		||||
    mock_template("recent_topic_row.hbs", true, (data, html) => {
 | 
			
		||||
        assert.ok(html.startsWith('<tr id="recent_topic'));
 | 
			
		||||
        assert.ok(html.startsWith('<tr id="recent_conversation'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    stub_out_filter_buttons();
 | 
			
		||||
@@ -673,34 +693,42 @@ test("basic assertions", ({mock_template}) => {
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 11,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 10,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 9,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 7,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 5,
 | 
			
		||||
            participated: false,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 4,
 | 
			
		||||
            participated: false,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 3,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            last_msg_id: 1,
 | 
			
		||||
            participated: true,
 | 
			
		||||
            type: "stream",
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
@@ -713,15 +741,16 @@ test("basic assertions", ({mock_template}) => {
 | 
			
		||||
        "4:topic-10,1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-3,1:topic-2,1:topic-1",
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Process private message
 | 
			
		||||
    rt_data.process_message({
 | 
			
		||||
        type: "private",
 | 
			
		||||
        to_user_ids: "6,7,8",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Private msgs are not processed.
 | 
			
		||||
    assert.equal(all_topics.size, 8);
 | 
			
		||||
    all_topics = rt_data.get();
 | 
			
		||||
    assert.equal(all_topics.size, 9);
 | 
			
		||||
    assert.equal(
 | 
			
		||||
        Array.from(all_topics.keys()).toString(),
 | 
			
		||||
        "4:topic-10,1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-3,1:topic-2,1:topic-1",
 | 
			
		||||
        "4:topic-10,1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-3,1:topic-2,1:topic-1,6,7,8",
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // participated
 | 
			
		||||
@@ -745,7 +774,7 @@ test("basic assertions", ({mock_template}) => {
 | 
			
		||||
    all_topics = rt_data.get();
 | 
			
		||||
    assert.equal(
 | 
			
		||||
        Array.from(all_topics.keys()).toString(),
 | 
			
		||||
        "1:topic-3,4:topic-10,1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-2,1:topic-1",
 | 
			
		||||
        "1:topic-3,4:topic-10,1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-2,1:topic-1,6,7,8",
 | 
			
		||||
    );
 | 
			
		||||
    verify_topic_data(all_topics, stream1, topic3, id, true);
 | 
			
		||||
 | 
			
		||||
@@ -762,7 +791,7 @@ test("basic assertions", ({mock_template}) => {
 | 
			
		||||
    all_topics = rt_data.get();
 | 
			
		||||
    assert.equal(
 | 
			
		||||
        Array.from(all_topics.keys()).toString(),
 | 
			
		||||
        "1:topic-7,1:topic-3,4:topic-10,1:topic-6,1:topic-5,1:topic-4,1:topic-2,1:topic-1",
 | 
			
		||||
        "1:topic-7,1:topic-3,4:topic-10,1:topic-6,1:topic-5,1:topic-4,1:topic-2,1:topic-1,6,7,8",
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // update_topic_is_muted now relies on external libraries completely
 | 
			
		||||
 
 | 
			
		||||
@@ -428,6 +428,7 @@ test("private_messages", () => {
 | 
			
		||||
        display_recipient: [{id: alice.user_id}],
 | 
			
		||||
        type: "private",
 | 
			
		||||
        unread: true,
 | 
			
		||||
        to_user_ids: alice.user_id.toString(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const read_message = {
 | 
			
		||||
 
 | 
			
		||||
@@ -481,7 +481,7 @@ export function initialize() {
 | 
			
		||||
        // The element's parent may re-render while it is being passed to
 | 
			
		||||
        // other functions, so, we get topic_key first.
 | 
			
		||||
        const $topic_row = $(e.target).closest("tr");
 | 
			
		||||
        const topic_key = $topic_row.attr("id").slice("recent_topics:".length - 1);
 | 
			
		||||
        const topic_key = $topic_row.attr("id").slice("recent_conversation:".length);
 | 
			
		||||
        const topic_row_index = $topic_row.index();
 | 
			
		||||
        recent_topics_ui.focus_clicked_element(
 | 
			
		||||
            topic_row_index,
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,11 @@ import * as people from "./people";
 | 
			
		||||
import * as recent_topics_util from "./recent_topics_util";
 | 
			
		||||
 | 
			
		||||
export function get_recipient_label(message) {
 | 
			
		||||
    // TODO: This code path is bit of a type-checking disaster; we mix
 | 
			
		||||
    // actual message objects with fake objects containing just a
 | 
			
		||||
    // couple fields, both those constructed here and potentially
 | 
			
		||||
    // passed in.
 | 
			
		||||
 | 
			
		||||
    if (message === undefined) {
 | 
			
		||||
        if (message_lists.current.empty()) {
 | 
			
		||||
            // For empty narrows where there's a clear reply target,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,21 @@
 | 
			
		||||
import * as people from "./people";
 | 
			
		||||
import {get_topic_key} from "./recent_topics_util";
 | 
			
		||||
import {get_key_from_message} from "./recent_topics_util";
 | 
			
		||||
 | 
			
		||||
export const topics = new Map(); // Key is stream-id:topic.
 | 
			
		||||
 | 
			
		||||
export function process_message(msg) {
 | 
			
		||||
    // This function returns if topic_data
 | 
			
		||||
    // has changed or not.
 | 
			
		||||
    if (msg.type !== "stream") {
 | 
			
		||||
        // We don't process private messages yet.
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    // Initialize topic data
 | 
			
		||||
    const key = get_topic_key(msg.stream_id, msg.topic);
 | 
			
		||||
 | 
			
		||||
    // Initialize topic and pm data
 | 
			
		||||
    // Key for private message is the user id's
 | 
			
		||||
    // to whom the message is begin sent.
 | 
			
		||||
    const key = get_key_from_message(msg);
 | 
			
		||||
    if (!topics.has(key)) {
 | 
			
		||||
        topics.set(key, {
 | 
			
		||||
            last_msg_id: -1,
 | 
			
		||||
            participated: false,
 | 
			
		||||
            type: msg.type,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    // Update topic data
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import render_recent_topic_row from "../templates/recent_topic_row.hbs";
 | 
			
		||||
import render_recent_topics_filters from "../templates/recent_topics_filters.hbs";
 | 
			
		||||
import render_recent_topics_body from "../templates/recent_topics_table.hbs";
 | 
			
		||||
 | 
			
		||||
import * as buddy_data from "./buddy_data";
 | 
			
		||||
import * as compose_closed_ui from "./compose_closed_ui";
 | 
			
		||||
import * as hash_util from "./hash_util";
 | 
			
		||||
import {$t} from "./i18n";
 | 
			
		||||
@@ -21,7 +22,13 @@ import {page_params} from "./page_params";
 | 
			
		||||
import * as people from "./people";
 | 
			
		||||
import * as recent_senders from "./recent_senders";
 | 
			
		||||
import {get, process_message, topics} from "./recent_topics_data";
 | 
			
		||||
import {get_topic_key, is_in_focus, is_visible, set_visible} from "./recent_topics_util";
 | 
			
		||||
import {
 | 
			
		||||
    get_key_from_message,
 | 
			
		||||
    get_topic_key,
 | 
			
		||||
    is_in_focus,
 | 
			
		||||
    is_visible,
 | 
			
		||||
    set_visible,
 | 
			
		||||
} from "./recent_topics_util";
 | 
			
		||||
import * as stream_data from "./stream_data";
 | 
			
		||||
import * as stream_list from "./stream_list";
 | 
			
		||||
import * as sub_store from "./sub_store";
 | 
			
		||||
@@ -69,7 +76,8 @@ export const COLUMNS = {
 | 
			
		||||
// implement wraparound of elements with the right/left keys.  Must be
 | 
			
		||||
// increased when we add new actions, or rethought if we add optional
 | 
			
		||||
// actions that only appear in some rows.
 | 
			
		||||
const MAX_SELECTABLE_COLS = 4;
 | 
			
		||||
const MAX_SELECTABLE_TOPIC_COLS = 4;
 | 
			
		||||
const MAX_SELECTABLE_PM_COLS = 2;
 | 
			
		||||
 | 
			
		||||
// we use localstorage to persist the recent topic filters
 | 
			
		||||
const ls_key = "recent_topic_filters";
 | 
			
		||||
@@ -77,6 +85,8 @@ const ls = localstorage();
 | 
			
		||||
 | 
			
		||||
let filters = new Set();
 | 
			
		||||
 | 
			
		||||
const recent_conversation_key_prefix = "recent_conversion:";
 | 
			
		||||
 | 
			
		||||
export function clear_for_tests() {
 | 
			
		||||
    filters.clear();
 | 
			
		||||
    topics.clear();
 | 
			
		||||
@@ -116,6 +126,33 @@ function is_table_focused() {
 | 
			
		||||
    return $current_focus_elem === "table";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get_row_type(row) {
 | 
			
		||||
    // Return "private" or "stream"
 | 
			
		||||
    // We use CSS method for finding row type until topics_widget gets initialized.
 | 
			
		||||
    if (!topics_widget) {
 | 
			
		||||
        const $topic_rows = $("#recent_topics_table table tbody tr");
 | 
			
		||||
        const $topic_row = $topic_rows.eq(row);
 | 
			
		||||
        const is_private = $topic_row.attr("data-private");
 | 
			
		||||
        if (is_private) {
 | 
			
		||||
            return "private";
 | 
			
		||||
        }
 | 
			
		||||
        return "stream";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const current_list = topics_widget.get_current_list();
 | 
			
		||||
    const current_row = current_list[row];
 | 
			
		||||
    return current_row.type;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get_max_selectable_cols(row) {
 | 
			
		||||
    // returns maximum number of columns in stream message or private message row.
 | 
			
		||||
    const type = get_row_type(row);
 | 
			
		||||
    if (type === "private") {
 | 
			
		||||
        return MAX_SELECTABLE_PM_COLS;
 | 
			
		||||
    }
 | 
			
		||||
    return MAX_SELECTABLE_TOPIC_COLS;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function set_table_focus(row, col, using_keyboard) {
 | 
			
		||||
    const $topic_rows = $("#recent_topics_table table tbody tr");
 | 
			
		||||
    if ($topic_rows.length === 0 || row < 0 || row >= $topic_rows.length) {
 | 
			
		||||
@@ -147,25 +184,35 @@ function set_table_focus(row, col, using_keyboard) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const message = {
 | 
			
		||||
    // TODO: This fake "message" object is designed to allow using the
 | 
			
		||||
    // get_recipient_label helper inside compose_closed_ui. Surely
 | 
			
		||||
    // there's a more readable way to write this code.
 | 
			
		||||
    const type = get_row_type(row);
 | 
			
		||||
    let message;
 | 
			
		||||
    if (type === "private") {
 | 
			
		||||
        message = {
 | 
			
		||||
            display_reply_to: $topic_row.find(".recent_topic_name a").text(),
 | 
			
		||||
        };
 | 
			
		||||
    } else {
 | 
			
		||||
        message = {
 | 
			
		||||
            stream: $topic_row.find(".recent_topic_stream a").text(),
 | 
			
		||||
            topic: $topic_row.find(".recent_topic_name a").text(),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    compose_closed_ui.update_reply_recipient_label(message);
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function get_focused_row_message() {
 | 
			
		||||
    if (is_table_focused()) {
 | 
			
		||||
        const recent_topic_id_prefix_len = "recent_topic:".length;
 | 
			
		||||
        const $topic_rows = $("#recent_topics_table table tbody tr");
 | 
			
		||||
        if ($topic_rows.length === 0) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const $topic_row = $topic_rows.eq(row_focus);
 | 
			
		||||
        const topic_id = $topic_row.attr("id").slice(recent_topic_id_prefix_len);
 | 
			
		||||
        const topic_last_msg_id = topics.get(topic_id).last_msg_id;
 | 
			
		||||
        const conversation_id = $topic_row.attr("id").slice(recent_conversation_key_prefix.length);
 | 
			
		||||
        const topic_last_msg_id = topics.get(conversation_id).last_msg_id;
 | 
			
		||||
        return message_store.get(topic_last_msg_id);
 | 
			
		||||
    }
 | 
			
		||||
    return undefined;
 | 
			
		||||
@@ -252,37 +299,84 @@ export function process_messages(messages) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function format_topic(topic_data) {
 | 
			
		||||
    const last_msg = message_store.get(topic_data.last_msg_id);
 | 
			
		||||
    const stream = last_msg.stream;
 | 
			
		||||
    const stream_id = last_msg.stream_id;
 | 
			
		||||
    const stream_info = sub_store.get(stream_id);
 | 
			
		||||
    if (stream_info === undefined) {
 | 
			
		||||
        // stream was deleted
 | 
			
		||||
        return {};
 | 
			
		||||
function message_to_conversation_unread_count(msg) {
 | 
			
		||||
    if (msg.type === "private") {
 | 
			
		||||
        return unread.num_unread_for_person(msg.to_user_ids);
 | 
			
		||||
    }
 | 
			
		||||
    const topic = last_msg.topic;
 | 
			
		||||
    return unread.num_unread_for_topic(msg.stream_id, msg.topic);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function format_conversation(conversation_data) {
 | 
			
		||||
    const context = {};
 | 
			
		||||
    const last_msg = message_store.get(conversation_data.last_msg_id);
 | 
			
		||||
    const time = new Date(last_msg.timestamp * 1000);
 | 
			
		||||
    const last_msg_time = timerender.last_seen_status_from_date(time);
 | 
			
		||||
    const full_datetime = timerender.get_full_datetime(time);
 | 
			
		||||
    const type = last_msg.type;
 | 
			
		||||
    context.full_last_msg_date_time = timerender.get_full_datetime(time);
 | 
			
		||||
    context.conversation_key = get_key_from_message(last_msg);
 | 
			
		||||
    context.unread_count = message_to_conversation_unread_count(last_msg);
 | 
			
		||||
    context.last_msg_time = timerender.last_seen_status_from_date(time);
 | 
			
		||||
    context.is_private = last_msg.type === "private";
 | 
			
		||||
    let all_senders;
 | 
			
		||||
    let senders;
 | 
			
		||||
    let displayed_other_senders;
 | 
			
		||||
    let extra_sender_ids;
 | 
			
		||||
 | 
			
		||||
    if (type === "stream") {
 | 
			
		||||
        const stream_info = sub_store.get(last_msg.stream_id);
 | 
			
		||||
 | 
			
		||||
        // Stream info
 | 
			
		||||
        context.stream_id = last_msg.stream_id;
 | 
			
		||||
        context.stream = last_msg.stream;
 | 
			
		||||
        context.stream_color = stream_info.color;
 | 
			
		||||
        context.stream_url = hash_util.by_stream_url(context.stream_id);
 | 
			
		||||
        context.invite_only = stream_info.invite_only;
 | 
			
		||||
        context.is_web_public = stream_info.is_web_public;
 | 
			
		||||
        // Topic info
 | 
			
		||||
        context.topic = last_msg.topic;
 | 
			
		||||
        context.topic_url = hash_util.by_stream_topic_url(context.stream_id, context.topic);
 | 
			
		||||
 | 
			
		||||
        // We hide the row according to filters or if it's muted.
 | 
			
		||||
        // We only supply the data to the topic rows and let jquery
 | 
			
		||||
        // display / hide them according to filters instead of
 | 
			
		||||
        // doing complete re-render.
 | 
			
		||||
    const topic_muted = Boolean(user_topics.is_topic_muted(stream_id, topic));
 | 
			
		||||
    const stream_muted = stream_data.is_muted(stream_id);
 | 
			
		||||
    const muted = topic_muted || stream_muted;
 | 
			
		||||
    const unread_count = unread.num_unread_for_topic(stream_id, topic);
 | 
			
		||||
        context.topic_muted = Boolean(user_topics.is_topic_muted(context.stream_id, context.topic));
 | 
			
		||||
        const stream_muted = stream_data.is_muted(context.stream_id);
 | 
			
		||||
        context.muted = context.topic_muted || stream_muted;
 | 
			
		||||
 | 
			
		||||
        // Display in most recent sender first order
 | 
			
		||||
    const all_senders = recent_senders.get_topic_recent_senders(stream_id, topic);
 | 
			
		||||
    const senders = all_senders.slice(-MAX_AVATAR);
 | 
			
		||||
    const senders_info = people.sender_info_for_recent_topics_row(senders);
 | 
			
		||||
        all_senders = recent_senders.get_topic_recent_senders(context.stream_id, context.topic);
 | 
			
		||||
        senders = all_senders.slice(-MAX_AVATAR);
 | 
			
		||||
 | 
			
		||||
        // Collect extra sender fullname for tooltip
 | 
			
		||||
        extra_sender_ids = all_senders.slice(0, -MAX_AVATAR);
 | 
			
		||||
        displayed_other_senders = extra_sender_ids.slice(-MAX_EXTRA_SENDERS);
 | 
			
		||||
    } else if (type === "private") {
 | 
			
		||||
        // Private message info
 | 
			
		||||
        context.pm_with = last_msg.display_reply_to;
 | 
			
		||||
        context.recipient_id = last_msg.recipient_id;
 | 
			
		||||
        context.pm_url = last_msg.pm_with_url;
 | 
			
		||||
        context.is_group = last_msg.display_recipient.length > 2;
 | 
			
		||||
 | 
			
		||||
        // Display in most recent sender first order
 | 
			
		||||
        all_senders = last_msg.display_recipient;
 | 
			
		||||
        senders = all_senders.slice(-MAX_AVATAR).map((sender) => sender.id);
 | 
			
		||||
 | 
			
		||||
        if (!context.is_group) {
 | 
			
		||||
            context.user_circle_class = buddy_data.get_user_circle_class(
 | 
			
		||||
                Number.parseInt(last_msg.to_user_ids, 10),
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Collect extra senders fullname for tooltip.
 | 
			
		||||
    const extra_sender_ids = all_senders.slice(0, -MAX_AVATAR);
 | 
			
		||||
    const displayed_other_senders = extra_sender_ids.slice(-MAX_EXTRA_SENDERS);
 | 
			
		||||
        extra_sender_ids = all_senders.slice(0, -MAX_AVATAR);
 | 
			
		||||
        displayed_other_senders = extra_sender_ids
 | 
			
		||||
            .slice(-MAX_EXTRA_SENDERS)
 | 
			
		||||
            .map((sender) => sender.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    context.senders = people.sender_info_for_recent_topics_row(senders);
 | 
			
		||||
    context.other_senders_count = Math.max(0, all_senders.length - MAX_AVATAR);
 | 
			
		||||
    extra_sender_ids = all_senders.slice(0, -MAX_AVATAR);
 | 
			
		||||
    const displayed_other_names = people.get_display_full_names(displayed_other_senders.reverse());
 | 
			
		||||
 | 
			
		||||
    if (extra_sender_ids.length > MAX_EXTRA_SENDERS) {
 | 
			
		||||
@@ -301,39 +395,19 @@ function format_topic(topic_data) {
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    const other_sender_names_html = displayed_other_names
 | 
			
		||||
    context.other_sender_names_html = displayed_other_names
 | 
			
		||||
        .map((name) => _.escape(name))
 | 
			
		||||
        .join("<br />");
 | 
			
		||||
    context.participated = conversation_data.participated;
 | 
			
		||||
    context.last_msg_url = hash_util.by_conversation_and_time_url(last_msg);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        // stream info
 | 
			
		||||
        stream_id,
 | 
			
		||||
        stream,
 | 
			
		||||
        stream_color: stream_info.color,
 | 
			
		||||
        invite_only: stream_info.invite_only,
 | 
			
		||||
        is_web_public: stream_info.is_web_public,
 | 
			
		||||
        stream_url: hash_util.by_stream_url(stream_id),
 | 
			
		||||
 | 
			
		||||
        topic,
 | 
			
		||||
        topic_key: get_topic_key(stream_id, topic),
 | 
			
		||||
        unread_count,
 | 
			
		||||
        last_msg_time,
 | 
			
		||||
        last_msg_url: hash_util.by_conversation_and_time_url(last_msg),
 | 
			
		||||
        topic_url: hash_util.by_stream_topic_url(stream_id, topic),
 | 
			
		||||
        senders: senders_info,
 | 
			
		||||
        other_senders_count: Math.max(0, all_senders.length - MAX_AVATAR),
 | 
			
		||||
        other_sender_names_html,
 | 
			
		||||
        muted,
 | 
			
		||||
        topic_muted,
 | 
			
		||||
        participated: topic_data.participated,
 | 
			
		||||
        full_last_msg_date_time: full_datetime,
 | 
			
		||||
    };
 | 
			
		||||
    return context;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get_topic_row(topic_data) {
 | 
			
		||||
    const msg = message_store.get(topic_data.last_msg_id);
 | 
			
		||||
    const topic_key = get_topic_key(msg.stream_id, msg.topic);
 | 
			
		||||
    return $(`#${CSS.escape("recent_topic:" + topic_key)}`);
 | 
			
		||||
    const topic_key = get_key_from_message(msg);
 | 
			
		||||
    return $(`#${CSS.escape(recent_conversation_key_prefix + topic_key)}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function process_topic_edit(old_stream_id, old_topic, new_topic, new_stream_id) {
 | 
			
		||||
@@ -372,14 +446,14 @@ export function filters_should_hide_topic(topic_data) {
 | 
			
		||||
    const msg = message_store.get(topic_data.last_msg_id);
 | 
			
		||||
    const sub = sub_store.get(msg.stream_id);
 | 
			
		||||
 | 
			
		||||
    if (sub === undefined || !sub.subscribed) {
 | 
			
		||||
    if ((sub === undefined || !sub.subscribed) && topic_data.type === "stream") {
 | 
			
		||||
        // Never try to process deactivated & unsubscribed stream msgs.
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (filters.has("unread")) {
 | 
			
		||||
        const unreadCount = unread.num_unread_for_topic(msg.stream_id, msg.topic);
 | 
			
		||||
        if (unreadCount === 0) {
 | 
			
		||||
        const unread_count = message_to_conversation_unread_count(msg);
 | 
			
		||||
        if (unread_count === 0) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -388,7 +462,7 @@ export function filters_should_hide_topic(topic_data) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!filters.has("include_muted")) {
 | 
			
		||||
    if (!filters.has("include_muted") && topic_data.type === "stream") {
 | 
			
		||||
        const topic_muted = Boolean(user_topics.is_topic_muted(msg.stream_id, msg.topic));
 | 
			
		||||
        const stream_muted = stream_data.is_muted(msg.stream_id);
 | 
			
		||||
        if (topic_muted || stream_muted) {
 | 
			
		||||
@@ -396,6 +470,10 @@ export function filters_should_hide_topic(topic_data) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!filters.has("include_private") && topic_data.type === "private") {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const search_keyword = $("#recent_topics_search").val();
 | 
			
		||||
    if (!topic_in_search_results(search_keyword, msg.stream, msg.topic)) {
 | 
			
		||||
        return true;
 | 
			
		||||
@@ -438,7 +516,7 @@ export function update_topic_is_muted(stream_id, topic) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function update_topic_unread_count(message) {
 | 
			
		||||
    const topic_key = get_topic_key(message.stream_id, message.topic);
 | 
			
		||||
    const topic_key = get_key_from_message(message);
 | 
			
		||||
    inplace_rerender(topic_key);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -490,6 +568,7 @@ export function update_filters_view() {
 | 
			
		||||
        filter_participated: filters.has("participated"),
 | 
			
		||||
        filter_unread: filters.has("unread"),
 | 
			
		||||
        filter_muted: filters.has("include_muted"),
 | 
			
		||||
        filter_pm: filters.has("include_private"),
 | 
			
		||||
        is_spectator: page_params.is_spectator,
 | 
			
		||||
    });
 | 
			
		||||
    $("#recent_filters_group").html(rendered_filters);
 | 
			
		||||
@@ -498,26 +577,40 @@ export function update_filters_view() {
 | 
			
		||||
    topics_widget.hard_redraw();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stream_sort(a, b) {
 | 
			
		||||
    const a_stream = message_store.get(a.last_msg_id).stream;
 | 
			
		||||
    const b_stream = message_store.get(b.last_msg_id).stream;
 | 
			
		||||
    if (a_stream > b_stream) {
 | 
			
		||||
function sort_comparator(a, b) {
 | 
			
		||||
    // compares strings in lowercase and returns -1, 0, 1
 | 
			
		||||
    if (a.toLowerCase() > b.toLowerCase()) {
 | 
			
		||||
        return 1;
 | 
			
		||||
    } else if (a_stream === b_stream) {
 | 
			
		||||
    } else if (a.toLowerCase() === b.toLowerCase()) {
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
    return -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function topic_sort(a, b) {
 | 
			
		||||
    const a_topic = message_store.get(a.last_msg_id).topic;
 | 
			
		||||
    const b_topic = message_store.get(b.last_msg_id).topic;
 | 
			
		||||
    if (a_topic > b_topic) {
 | 
			
		||||
        return 1;
 | 
			
		||||
    } else if (a_topic === b_topic) {
 | 
			
		||||
        return 0;
 | 
			
		||||
function stream_sort(a, b) {
 | 
			
		||||
    if (a.type === b.type) {
 | 
			
		||||
        const a_msg = message_store.get(a.last_msg_id);
 | 
			
		||||
        const b_msg = message_store.get(b.last_msg_id);
 | 
			
		||||
 | 
			
		||||
        if (a.type === "stream") {
 | 
			
		||||
            return sort_comparator(a_msg.stream, b_msg.stream);
 | 
			
		||||
        }
 | 
			
		||||
    return -1;
 | 
			
		||||
        return sort_comparator(a_msg.display_reply_to, b_msg.display_reply_to);
 | 
			
		||||
    }
 | 
			
		||||
    // if type is not same sort between "private" and "stream"
 | 
			
		||||
    return sort_comparator(a.type, b.type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function topic_sort_key(conversation_data) {
 | 
			
		||||
    const message = message_store.get(conversation_data.last_msg_id);
 | 
			
		||||
    if (message.type === "private") {
 | 
			
		||||
        return message.display_reply_to;
 | 
			
		||||
    }
 | 
			
		||||
    return message.topic;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function topic_sort(a, b) {
 | 
			
		||||
    return sort_comparator(topic_sort_key(a), topic_sort_key(b));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function topic_offset_to_visible_area(topic_row) {
 | 
			
		||||
@@ -601,6 +694,7 @@ export function complete_rerender() {
 | 
			
		||||
        filter_participated: filters.has("participated"),
 | 
			
		||||
        filter_unread: filters.has("unread"),
 | 
			
		||||
        filter_muted: filters.has("include_muted"),
 | 
			
		||||
        filter_pm: filters.has("include_private"),
 | 
			
		||||
        search_val: $("#recent_topics_search").val() || "",
 | 
			
		||||
        is_spectator: page_params.is_spectator,
 | 
			
		||||
    });
 | 
			
		||||
@@ -611,7 +705,7 @@ export function complete_rerender() {
 | 
			
		||||
        name: "recent_topics_table",
 | 
			
		||||
        $parent_container: $("#recent_topics_table"),
 | 
			
		||||
        modifier(item) {
 | 
			
		||||
            return render_recent_topic_row(format_topic(item));
 | 
			
		||||
            return render_recent_topic_row(format_conversation(item));
 | 
			
		||||
        },
 | 
			
		||||
        filter: {
 | 
			
		||||
            // We use update_filters_view & filters_should_hide_topic to do all the
 | 
			
		||||
@@ -730,41 +824,67 @@ export function focus_clicked_element(topic_row_index, col, topic_key) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function left_arrow_navigation(row, col) {
 | 
			
		||||
    if (col === MAX_SELECTABLE_COLS - 1 && !has_unread(row)) {
 | 
			
		||||
    const type = get_row_type(row);
 | 
			
		||||
 | 
			
		||||
    if (type === "stream" && col === MAX_SELECTABLE_TOPIC_COLS - 1 && !has_unread(row)) {
 | 
			
		||||
        col_focus -= 1;
 | 
			
		||||
    }
 | 
			
		||||
    col_focus -= 1;
 | 
			
		||||
 | 
			
		||||
    col_focus -= 1;
 | 
			
		||||
    if (col_focus < 0) {
 | 
			
		||||
        col_focus = MAX_SELECTABLE_COLS - 1;
 | 
			
		||||
        col_focus = get_max_selectable_cols(row) - 1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function right_arrow_navigation(row, col) {
 | 
			
		||||
    if (col === 1 && !has_unread(row)) {
 | 
			
		||||
    const type = get_row_type(row);
 | 
			
		||||
 | 
			
		||||
    if (type === "stream" && col === 1 && !has_unread(row)) {
 | 
			
		||||
        col_focus += 1;
 | 
			
		||||
    }
 | 
			
		||||
    col_focus += 1;
 | 
			
		||||
 | 
			
		||||
    if (col_focus >= MAX_SELECTABLE_COLS) {
 | 
			
		||||
    col_focus += 1;
 | 
			
		||||
    if (col_focus >= get_max_selectable_cols(row)) {
 | 
			
		||||
        col_focus = 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function up_arrow_navigation(row, col) {
 | 
			
		||||
    if (col === 2 && row - 1 >= 0 && !has_unread(row - 1)) {
 | 
			
		||||
    row_focus -= 1;
 | 
			
		||||
    if (row_focus < 0) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const type = get_row_type(row);
 | 
			
		||||
 | 
			
		||||
    if (type === "stream" && col === 2 && row - 1 >= 0 && !has_unread(row - 1)) {
 | 
			
		||||
        col_focus = 1;
 | 
			
		||||
    }
 | 
			
		||||
    row_focus -= 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function down_arrow_navigation(row, col) {
 | 
			
		||||
    if (col === 2 && !has_unread(row + 1)) {
 | 
			
		||||
    const type = get_row_type(row);
 | 
			
		||||
 | 
			
		||||
    if (type === "stream" && col === 2 && !has_unread(row + 1)) {
 | 
			
		||||
        col_focus = 1;
 | 
			
		||||
    }
 | 
			
		||||
    row_focus += 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function check_row_type_transition(row, col) {
 | 
			
		||||
    // This function checks if the row is transitioning
 | 
			
		||||
    // from type "Private messages" to "Stream" or vice versa.
 | 
			
		||||
    // This helps in setting the col_focus as maximum column
 | 
			
		||||
    // of both the type are different.
 | 
			
		||||
    if (row < 0) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    const max_col = get_max_selectable_cols(row);
 | 
			
		||||
    if (col > max_col - 1) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function change_focused_element($elt, input_key) {
 | 
			
		||||
    // Called from hotkeys.js; like all logic in that module,
 | 
			
		||||
    // returning true will cause the caller to do
 | 
			
		||||
@@ -913,7 +1033,13 @@ export function change_focused_element($elt, input_key) {
 | 
			
		||||
                break;
 | 
			
		||||
            case "up_arrow":
 | 
			
		||||
                up_arrow_navigation(row_focus, col_focus);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (check_row_type_transition(row_focus, col_focus)) {
 | 
			
		||||
            col_focus = get_max_selectable_cols(row_focus) - 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        set_table_focus(row_focus, col_focus, true);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -29,3 +29,15 @@ export function is_in_focus() {
 | 
			
		||||
export function get_topic_key(stream_id, topic) {
 | 
			
		||||
    return stream_id + ":" + topic.toLowerCase();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function get_key_from_message(msg) {
 | 
			
		||||
    if (msg.type === "private") {
 | 
			
		||||
        // The to_user_ids field on a private message object is a
 | 
			
		||||
        // string containing the user IDs involved in the message in
 | 
			
		||||
        // sorted order.
 | 
			
		||||
        return msg.to_user_ids;
 | 
			
		||||
    } else if (msg.type === "stream") {
 | 
			
		||||
        return get_topic_key(msg.stream_id, msg.topic);
 | 
			
		||||
    }
 | 
			
		||||
    throw new Error(`Invalid message type ${msg.type}`);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -661,6 +661,11 @@ body.dark-theme {
 | 
			
		||||
 | 
			
		||||
    #recent_topics_table {
 | 
			
		||||
        border-color: hsla(0, 0%, 0%, 0.6);
 | 
			
		||||
 | 
			
		||||
        .fa-envelope,
 | 
			
		||||
        .fa-group {
 | 
			
		||||
            opacity: 0.7;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    thead,
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,21 @@
 | 
			
		||||
            padding-right: 3px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .fa-group {
 | 
			
		||||
            font-size: 0.8rem;
 | 
			
		||||
            margin-left: 5px;
 | 
			
		||||
            /* color: hsl(105, 2%, 50%); */
 | 
			
		||||
            opacity: 0.6;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .fa-envelope {
 | 
			
		||||
            font-size: 0.7rem;
 | 
			
		||||
            margin-right: 2px;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            top: -1px;
 | 
			
		||||
            opacity: 0.6;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .table_fix_head {
 | 
			
		||||
            padding: 0 !important;
 | 
			
		||||
            /* 100px = space occupied by `recent_topics_filter_buttons`( ~49px)
 | 
			
		||||
@@ -148,10 +163,31 @@
 | 
			
		||||
            background-color: hsl(105, 2%, 50%);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .unread_count_pm {
 | 
			
		||||
            /* 10px of unread count + 23px for bell icon */
 | 
			
		||||
            margin-right: 33px;
 | 
			
		||||
            /* match the opacity with topic unread count without hover */
 | 
			
		||||
            opacity: 0.7;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .unread_hidden {
 | 
			
		||||
            visibility: hidden;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .user_circle {
 | 
			
		||||
            /* Shrink the user activity circle for the recent topics context. */
 | 
			
		||||
            min-width: 7px;
 | 
			
		||||
            height: 7px;
 | 
			
		||||
            margin-left: 8px;
 | 
			
		||||
            top: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .flex_container_pm {
 | 
			
		||||
            /* Flex container to fit in user circle and group icon */
 | 
			
		||||
            display: flex;
 | 
			
		||||
            justify-content: space-between;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .flex_container {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
@@ -287,11 +323,22 @@
 | 
			
		||||
           as new messages arrive from the server. */
 | 
			
		||||
        .recent_topic_stream {
 | 
			
		||||
            width: 25%;
 | 
			
		||||
            padding: 8px;
 | 
			
		||||
            padding: 8px 0 8px 8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .recent_topic_name {
 | 
			
		||||
            width: 40%;
 | 
			
		||||
 | 
			
		||||
            .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 {
 | 
			
		||||
@@ -327,6 +374,11 @@
 | 
			
		||||
                margin-right: 5px;
 | 
			
		||||
                font-size: 15px;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .unread_count_pm {
 | 
			
		||||
                /* Margin equal to size of recent topic actions */
 | 
			
		||||
                margin-right: 44px;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,44 @@
 | 
			
		||||
<tr id="recent_topic:{{topic_key}}" {{#if unread_count}}class="unread_topic"{{/if}} data-unread-count="{{unread_count}}" data-muted="{{muted}}" data-participated="{{participated}}">
 | 
			
		||||
<tr id="recent_conversation:{{conversation_key}}" {{#if unread_count}}class="unread_topic"{{/if}} data-unread-count="{{unread_count}}" data-muted="{{muted}}" data-participated="{{participated}}" data-private="{{is_private}}">
 | 
			
		||||
    <td class="recent_topic_stream">
 | 
			
		||||
        <div class="recent_topics_focusable">
 | 
			
		||||
        <div class="flex_container flex_container_pm">
 | 
			
		||||
            <div class="left_part recent_topics_focusable">
 | 
			
		||||
                {{#if is_private}}
 | 
			
		||||
                <span class="fa fa-envelope"></span>
 | 
			
		||||
                <a href="{{pm_url}}">Private messages</a>
 | 
			
		||||
                {{else}}
 | 
			
		||||
                <span id="stream_sidebar_privacy_swatch_{{stream_id}}" class="stream-privacy filter-icon" style="color: {{stream_color}}">
 | 
			
		||||
                    {{> stream_privacy }}
 | 
			
		||||
                </span>
 | 
			
		||||
            <a href="{{stream_url}}">{{stream}}</a>
 | 
			
		||||
                <a href="{{topic_url}}">{{stream}}</a>
 | 
			
		||||
                {{/if}}
 | 
			
		||||
            </div>
 | 
			
		||||
            {{!-- For presence/group indicator --}}
 | 
			
		||||
            {{#if is_private}}
 | 
			
		||||
            <div class="right_part">
 | 
			
		||||
                <span class="stream-privacy filter-icon">
 | 
			
		||||
                    {{#if is_group}}
 | 
			
		||||
                    <span class="fa fa-group"></span>
 | 
			
		||||
                    {{else}}
 | 
			
		||||
                    <span class="{{user_circle_class}} user_circle"></span>
 | 
			
		||||
                    {{/if}}
 | 
			
		||||
                </span>
 | 
			
		||||
            </div>
 | 
			
		||||
            {{/if}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </td>
 | 
			
		||||
    <td class="recent_topic_name">
 | 
			
		||||
        <div class="flex_container">
 | 
			
		||||
            <div class="left_part recent_topics_focusable">
 | 
			
		||||
            <div class="left_part recent_topics_focusable line_clamp">
 | 
			
		||||
                {{#if is_private}}
 | 
			
		||||
                <a href="{{pm_url}}">{{pm_with}}</a>
 | 
			
		||||
                {{else}}
 | 
			
		||||
                <a href="{{topic_url}}">{{topic}}</a>
 | 
			
		||||
                {{/if}}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="right_part">
 | 
			
		||||
                {{#if is_private}}
 | 
			
		||||
                <span class="unread_count unread_count_pm {{#unless unread_count}}unread_hidden{{/unless}}">{{unread_count}}</span>
 | 
			
		||||
                {{else}}
 | 
			
		||||
                <div class="recent_topic_actions">
 | 
			
		||||
                    <div class="recent_topics_focusable hidden-for-spectators">
 | 
			
		||||
                        <span class="unread_count {{#unless unread_count}}unread_hidden{{/unless}} tippy-zulip-tooltip on_hover_topic_read" data-stream-id="{{stream_id}}" data-topic-name="{{topic}}" data-tippy-content="{{t 'Mark as read' }}" role="button" tabindex="0" aria-label="{{t 'Mark as read' }}">{{unread_count}}</span>
 | 
			
		||||
@@ -27,16 +53,17 @@
 | 
			
		||||
                        {{/if}}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {{/if}}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </td>
 | 
			
		||||
    <td class='recent_topic_users'>
 | 
			
		||||
        <ul class="recent_topics_participants">
 | 
			
		||||
            {{#if other_senders_count}}
 | 
			
		||||
            <li class="recent_topics_participant_item tippy-zulip-tooltip" data-tooltip-template-id="recent_topics_participant_overflow_tooltip:{{topic_key}}">
 | 
			
		||||
            <li class="recent_topics_participant_item tippy-zulip-tooltip" data-tooltip-template-id="recent_topics_participant_overflow_tooltip:{{conversation_key}}">
 | 
			
		||||
                <span class="recent_topics_participant_overflow">+{{other_senders_count}}</span>
 | 
			
		||||
            </li>
 | 
			
		||||
            <template id="recent_topics_participant_overflow_tooltip:{{topic_key}}">{{{other_sender_names_html}}}</template>
 | 
			
		||||
            <template id="recent_topics_participant_overflow_tooltip:{{conversation_key}}">{{{other_sender_names_html}}}</template>
 | 
			
		||||
            {{/if}}
 | 
			
		||||
            {{#each senders}}
 | 
			
		||||
                {{#if this.is_muted}}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,12 @@
 | 
			
		||||
<button data-filter="all" type="button" class="btn btn-default btn-recent-filters">{{t 'All' }}</button>
 | 
			
		||||
<button data-filter="include_private" type="button" class="btn btn-default btn-recent-filters {{#if is_spectator}}fake_disabled_button{{/if}}" role="checkbox" aria-checked="true">
 | 
			
		||||
    {{#if filter_pm}}
 | 
			
		||||
    <i class="fa fa-check-square-o"></i>
 | 
			
		||||
    {{else}}
 | 
			
		||||
    <i class="fa fa-square-o"></i>
 | 
			
		||||
    {{/if}}
 | 
			
		||||
    {{t 'Include PMs' }}
 | 
			
		||||
</button>
 | 
			
		||||
<button data-filter="include_muted" type="button" class="btn btn-default btn-recent-filters {{#if is_spectator}}fake_disabled_button{{/if}}" role="checkbox" aria-checked="false">
 | 
			
		||||
    {{#if filter_muted }}
 | 
			
		||||
    <i class="fa fa-check-square-o"></i>
 | 
			
		||||
 
 | 
			
		||||
@@ -134,6 +134,7 @@ EXEMPT_FILES = make_set(
 | 
			
		||||
        "static/js/realm_playground.js",
 | 
			
		||||
        "static/js/realm_user_settings_defaults.ts",
 | 
			
		||||
        "static/js/recent_topics_ui.js",
 | 
			
		||||
        "static/js/recent_topics_util.js",
 | 
			
		||||
        "static/js/reload.js",
 | 
			
		||||
        "static/js/reminder.js",
 | 
			
		||||
        "static/js/resize.js",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user