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:
madrix01
2022-04-24 09:43:19 +05:30
committed by Tim Abbott
parent a3f6220fe4
commit 550a32bb20
13 changed files with 387 additions and 118 deletions

View File

@@ -25,6 +25,7 @@ const message_lists = mock_esm("../../static/js/message_lists");
const message_util = mock_esm("../../static/js/message_util"); const message_util = mock_esm("../../static/js/message_util");
const notifications = mock_esm("../../static/js/notifications"); const notifications = mock_esm("../../static/js/notifications");
const pm_list = mock_esm("../../static/js/pm_list"); 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 resize = mock_esm("../../static/js/resize");
const stream_list = mock_esm("../../static/js/stream_list"); const stream_list = mock_esm("../../static/js/stream_list");
const unread_ops = mock_esm("../../static/js/unread_ops"); 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_data");
helper.redirect(message_util, "add_new_messages"); helper.redirect(message_util, "add_new_messages");
helper.redirect(notifications, "received_messages"); helper.redirect(notifications, "received_messages");
helper.redirect(recent_topics_data, "process_message");
helper.redirect(resize, "resize_page_components"); helper.redirect(resize, "resize_page_components");
helper.redirect(stream_list, "update_streams_sidebar"); helper.redirect(stream_list, "update_streams_sidebar");
helper.redirect(unread_ops, "process_visible"); helper.redirect(unread_ops, "process_visible");
@@ -113,6 +115,7 @@ run_test("insert_message", ({override}) => {
[unread_ops, "process_visible"], [unread_ops, "process_visible"],
[notifications, "received_messages"], [notifications, "received_messages"],
[stream_list, "update_streams_sidebar"], [stream_list, "update_streams_sidebar"],
[recent_topics_data, "process_message"],
]); ]);
// Despite all of our stubbing/mocking, the call to // Despite all of our stubbing/mocking, the call to

View File

@@ -146,7 +146,6 @@ mock_esm("../../static/js/top_left_corner", {
mock_esm("../../static/js/unread", { mock_esm("../../static/js/unread", {
num_unread_for_topic: (stream_id, topic) => { num_unread_for_topic: (stream_id, topic) => {
if (stream_id === 1 && topic === "topic-1") { if (stream_id === 1 && topic === "topic-1") {
// Only stream1, topic-1 is read.
return 0; return 0;
} }
return 1; return 1;
@@ -281,6 +280,7 @@ function generate_topic_data(topic_info_array) {
other_sender_names_html: "", other_sender_names_html: "",
invite_only: false, invite_only: false,
is_web_public: true, is_web_public: true,
is_private: false,
last_msg_time: "Just now", last_msg_time: "Just now",
last_msg_url: "https://www.example.com", last_msg_url: "https://www.example.com",
full_last_msg_date_time: "date at time", full_last_msg_date_time: "date at time",
@@ -290,7 +290,7 @@ function generate_topic_data(topic_info_array) {
stream_id, stream_id,
stream_url: "https://www.example.com", stream_url: "https://www.example.com",
topic, topic,
topic_key: get_topic_key(stream_id, topic), conversation_key: get_topic_key(stream_id, topic),
topic_url: "https://www.example.com", topic_url: "https://www.example.com",
unread_count, unread_count,
muted, muted,
@@ -343,6 +343,7 @@ test("test_recent_topics_show", ({mock_template, override}) => {
filter_participated: false, filter_participated: false,
filter_unread: false, filter_unread: false,
filter_muted: false, filter_muted: false,
filter_pm: false,
search_val: "", search_val: "",
is_spectator: false, is_spectator: false,
}; };
@@ -375,6 +376,7 @@ test("test_filter_all", ({mock_template}) => {
filter_participated: false, filter_participated: false,
filter_unread: false, filter_unread: false,
filter_muted: false, filter_muted: false,
filter_pm: false,
search_val: "", search_val: "",
is_spectator: true, is_spectator: true,
}; };
@@ -401,8 +403,8 @@ test("test_filter_all", ({mock_template}) => {
rt.process_messages([messages[0]]); rt.process_messages([messages[0]]);
expected_data_to_replace_in_list_widget = [ expected_data_to_replace_in_list_widget = [
{last_msg_id: 10, participated: true}, {last_msg_id: 10, participated: true, type: "stream"},
{last_msg_id: 1, participated: true}, {last_msg_id: 1, participated: true, type: "stream"},
]; ];
row_data = row_data.concat(generate_topic_data([[1, "topic-7", 1, true, true]])); 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_participated: false,
filter_unread: expected_filter_unread, filter_unread: expected_filter_unread,
filter_muted: false, filter_muted: false,
filter_pm: false,
search_val: "", search_val: "",
is_spectator: false, is_spectator: false,
}); });
@@ -482,34 +485,42 @@ test("test_filter_unread", ({mock_template}) => {
{ {
last_msg_id: 11, last_msg_id: 11,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 10, last_msg_id: 10,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 9, last_msg_id: 9,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 7, last_msg_id: 7,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 5, last_msg_id: 5,
participated: false, participated: false,
type: "stream",
}, },
{ {
last_msg_id: 4, last_msg_id: 4,
participated: false, participated: false,
type: "stream",
}, },
{ {
last_msg_id: 3, last_msg_id: 3,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 1, last_msg_id: 1,
participated: true, participated: true,
type: "stream",
}, },
]; ];
@@ -539,6 +550,7 @@ test("test_filter_participated", ({mock_template}) => {
filter_participated: expected_filter_participated, filter_participated: expected_filter_participated,
filter_unread: false, filter_unread: false,
filter_muted: false, filter_muted: false,
filter_pm: false,
search_val: "", search_val: "",
is_spectator: false, is_spectator: false,
}); });
@@ -602,34 +614,42 @@ test("test_filter_participated", ({mock_template}) => {
{ {
last_msg_id: 11, last_msg_id: 11,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 10, last_msg_id: 10,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 9, last_msg_id: 9,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 7, last_msg_id: 7,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 5, last_msg_id: 5,
participated: false, participated: false,
type: "stream",
}, },
{ {
last_msg_id: 4, last_msg_id: 4,
participated: false, participated: false,
type: "stream",
}, },
{ {
last_msg_id: 3, last_msg_id: 3,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 1, last_msg_id: 1,
participated: true, participated: true,
type: "stream",
}, },
]; ];
@@ -656,7 +676,7 @@ test("basic assertions", ({mock_template}) => {
mock_template("recent_topics_table.hbs", false, () => {}); mock_template("recent_topics_table.hbs", false, () => {});
mock_template("recent_topic_row.hbs", true, (data, html) => { 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(); stub_out_filter_buttons();
@@ -673,34 +693,42 @@ test("basic assertions", ({mock_template}) => {
{ {
last_msg_id: 11, last_msg_id: 11,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 10, last_msg_id: 10,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 9, last_msg_id: 9,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 7, last_msg_id: 7,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 5, last_msg_id: 5,
participated: false, participated: false,
type: "stream",
}, },
{ {
last_msg_id: 4, last_msg_id: 4,
participated: false, participated: false,
type: "stream",
}, },
{ {
last_msg_id: 3, last_msg_id: 3,
participated: true, participated: true,
type: "stream",
}, },
{ {
last_msg_id: 1, last_msg_id: 1,
participated: true, 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", "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({ rt_data.process_message({
type: "private", type: "private",
to_user_ids: "6,7,8",
}); });
all_topics = rt_data.get();
// Private msgs are not processed. assert.equal(all_topics.size, 9);
assert.equal(all_topics.size, 8);
assert.equal( assert.equal(
Array.from(all_topics.keys()).toString(), 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 // participated
@@ -745,7 +774,7 @@ test("basic assertions", ({mock_template}) => {
all_topics = rt_data.get(); all_topics = rt_data.get();
assert.equal( assert.equal(
Array.from(all_topics.keys()).toString(), 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); verify_topic_data(all_topics, stream1, topic3, id, true);
@@ -762,7 +791,7 @@ test("basic assertions", ({mock_template}) => {
all_topics = rt_data.get(); all_topics = rt_data.get();
assert.equal( assert.equal(
Array.from(all_topics.keys()).toString(), 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 // update_topic_is_muted now relies on external libraries completely

View File

@@ -428,6 +428,7 @@ test("private_messages", () => {
display_recipient: [{id: alice.user_id}], display_recipient: [{id: alice.user_id}],
type: "private", type: "private",
unread: true, unread: true,
to_user_ids: alice.user_id.toString(),
}; };
const read_message = { const read_message = {

View File

@@ -481,7 +481,7 @@ export function initialize() {
// The element's parent may re-render while it is being passed to // The element's parent may re-render while it is being passed to
// other functions, so, we get topic_key first. // other functions, so, we get topic_key first.
const $topic_row = $(e.target).closest("tr"); 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(); const topic_row_index = $topic_row.index();
recent_topics_ui.focus_clicked_element( recent_topics_ui.focus_clicked_element(
topic_row_index, topic_row_index,

View File

@@ -9,6 +9,11 @@ import * as people from "./people";
import * as recent_topics_util from "./recent_topics_util"; import * as recent_topics_util from "./recent_topics_util";
export function get_recipient_label(message) { 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 === undefined) {
if (message_lists.current.empty()) { if (message_lists.current.empty()) {
// For empty narrows where there's a clear reply target, // For empty narrows where there's a clear reply target,

View File

@@ -1,21 +1,21 @@
import * as people from "./people"; 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 const topics = new Map(); // Key is stream-id:topic.
export function process_message(msg) { export function process_message(msg) {
// This function returns if topic_data // This function returns if topic_data
// has changed or not. // has changed or not.
if (msg.type !== "stream") {
// We don't process private messages yet. // Initialize topic and pm data
return false; // Key for private message is the user id's
} // to whom the message is begin sent.
// Initialize topic data const key = get_key_from_message(msg);
const key = get_topic_key(msg.stream_id, msg.topic);
if (!topics.has(key)) { if (!topics.has(key)) {
topics.set(key, { topics.set(key, {
last_msg_id: -1, last_msg_id: -1,
participated: false, participated: false,
type: msg.type,
}); });
} }
// Update topic data // Update topic data

View File

@@ -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_filters from "../templates/recent_topics_filters.hbs";
import render_recent_topics_body from "../templates/recent_topics_table.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 compose_closed_ui from "./compose_closed_ui";
import * as hash_util from "./hash_util"; import * as hash_util from "./hash_util";
import {$t} from "./i18n"; import {$t} from "./i18n";
@@ -21,7 +22,13 @@ import {page_params} from "./page_params";
import * as people from "./people"; import * as people from "./people";
import * as recent_senders from "./recent_senders"; import * as recent_senders from "./recent_senders";
import {get, process_message, topics} from "./recent_topics_data"; 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_data from "./stream_data";
import * as stream_list from "./stream_list"; import * as stream_list from "./stream_list";
import * as sub_store from "./sub_store"; 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 // implement wraparound of elements with the right/left keys. Must be
// increased when we add new actions, or rethought if we add optional // increased when we add new actions, or rethought if we add optional
// actions that only appear in some rows. // 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 // we use localstorage to persist the recent topic filters
const ls_key = "recent_topic_filters"; const ls_key = "recent_topic_filters";
@@ -77,6 +85,8 @@ const ls = localstorage();
let filters = new Set(); let filters = new Set();
const recent_conversation_key_prefix = "recent_conversion:";
export function clear_for_tests() { export function clear_for_tests() {
filters.clear(); filters.clear();
topics.clear(); topics.clear();
@@ -116,6 +126,33 @@ function is_table_focused() {
return $current_focus_elem === "table"; 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) { function set_table_focus(row, col, using_keyboard) {
const $topic_rows = $("#recent_topics_table table tbody tr"); const $topic_rows = $("#recent_topics_table table tbody tr");
if ($topic_rows.length === 0 || row < 0 || row >= $topic_rows.length) { 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(), stream: $topic_row.find(".recent_topic_stream a").text(),
topic: $topic_row.find(".recent_topic_name a").text(), topic: $topic_row.find(".recent_topic_name a").text(),
}; };
}
compose_closed_ui.update_reply_recipient_label(message); compose_closed_ui.update_reply_recipient_label(message);
return true; return true;
} }
export function get_focused_row_message() { export function get_focused_row_message() {
if (is_table_focused()) { if (is_table_focused()) {
const recent_topic_id_prefix_len = "recent_topic:".length;
const $topic_rows = $("#recent_topics_table table tbody tr"); const $topic_rows = $("#recent_topics_table table tbody tr");
if ($topic_rows.length === 0) { if ($topic_rows.length === 0) {
return undefined; return undefined;
} }
const $topic_row = $topic_rows.eq(row_focus); const $topic_row = $topic_rows.eq(row_focus);
const topic_id = $topic_row.attr("id").slice(recent_topic_id_prefix_len); const conversation_id = $topic_row.attr("id").slice(recent_conversation_key_prefix.length);
const topic_last_msg_id = topics.get(topic_id).last_msg_id; const topic_last_msg_id = topics.get(conversation_id).last_msg_id;
return message_store.get(topic_last_msg_id); return message_store.get(topic_last_msg_id);
} }
return undefined; return undefined;
@@ -252,37 +299,84 @@ export function process_messages(messages) {
} }
} }
function format_topic(topic_data) { function message_to_conversation_unread_count(msg) {
const last_msg = message_store.get(topic_data.last_msg_id); if (msg.type === "private") {
const stream = last_msg.stream; return unread.num_unread_for_person(msg.to_user_ids);
const stream_id = last_msg.stream_id;
const stream_info = sub_store.get(stream_id);
if (stream_info === undefined) {
// stream was deleted
return {};
} }
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 time = new Date(last_msg.timestamp * 1000);
const last_msg_time = timerender.last_seen_status_from_date(time); const type = last_msg.type;
const full_datetime = timerender.get_full_datetime(time); 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 hide the row according to filters or if it's muted.
// We only supply the data to the topic rows and let jquery // We only supply the data to the topic rows and let jquery
// display / hide them according to filters instead of // display / hide them according to filters instead of
// doing complete re-render. // doing complete re-render.
const topic_muted = Boolean(user_topics.is_topic_muted(stream_id, topic)); context.topic_muted = Boolean(user_topics.is_topic_muted(context.stream_id, context.topic));
const stream_muted = stream_data.is_muted(stream_id); const stream_muted = stream_data.is_muted(context.stream_id);
const muted = topic_muted || stream_muted; context.muted = context.topic_muted || stream_muted;
const unread_count = unread.num_unread_for_topic(stream_id, topic);
// Display in most recent sender first order // Display in most recent sender first order
const all_senders = recent_senders.get_topic_recent_senders(stream_id, topic); all_senders = recent_senders.get_topic_recent_senders(context.stream_id, context.topic);
const senders = all_senders.slice(-MAX_AVATAR); senders = all_senders.slice(-MAX_AVATAR);
const senders_info = people.sender_info_for_recent_topics_row(senders);
// 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. // Collect extra senders fullname for tooltip.
const extra_sender_ids = all_senders.slice(0, -MAX_AVATAR); extra_sender_ids = all_senders.slice(0, -MAX_AVATAR);
const displayed_other_senders = extra_sender_ids.slice(-MAX_EXTRA_SENDERS); 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()); const displayed_other_names = people.get_display_full_names(displayed_other_senders.reverse());
if (extra_sender_ids.length > MAX_EXTRA_SENDERS) { 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)) .map((name) => _.escape(name))
.join("<br />"); .join("<br />");
context.participated = conversation_data.participated;
context.last_msg_url = hash_util.by_conversation_and_time_url(last_msg);
return { return context;
// 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,
};
} }
function get_topic_row(topic_data) { function get_topic_row(topic_data) {
const msg = message_store.get(topic_data.last_msg_id); const msg = message_store.get(topic_data.last_msg_id);
const topic_key = get_topic_key(msg.stream_id, msg.topic); const topic_key = get_key_from_message(msg);
return $(`#${CSS.escape("recent_topic:" + topic_key)}`); return $(`#${CSS.escape(recent_conversation_key_prefix + topic_key)}`);
} }
export function process_topic_edit(old_stream_id, old_topic, new_topic, new_stream_id) { 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 msg = message_store.get(topic_data.last_msg_id);
const sub = sub_store.get(msg.stream_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. // Never try to process deactivated & unsubscribed stream msgs.
return true; return true;
} }
if (filters.has("unread")) { if (filters.has("unread")) {
const unreadCount = unread.num_unread_for_topic(msg.stream_id, msg.topic); const unread_count = message_to_conversation_unread_count(msg);
if (unreadCount === 0) { if (unread_count === 0) {
return true; return true;
} }
} }
@@ -388,7 +462,7 @@ export function filters_should_hide_topic(topic_data) {
return true; 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 topic_muted = Boolean(user_topics.is_topic_muted(msg.stream_id, msg.topic));
const stream_muted = stream_data.is_muted(msg.stream_id); const stream_muted = stream_data.is_muted(msg.stream_id);
if (topic_muted || stream_muted) { 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(); const search_keyword = $("#recent_topics_search").val();
if (!topic_in_search_results(search_keyword, msg.stream, msg.topic)) { if (!topic_in_search_results(search_keyword, msg.stream, msg.topic)) {
return true; return true;
@@ -438,7 +516,7 @@ export function update_topic_is_muted(stream_id, topic) {
} }
export function update_topic_unread_count(message) { 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); inplace_rerender(topic_key);
} }
@@ -490,6 +568,7 @@ export function update_filters_view() {
filter_participated: filters.has("participated"), filter_participated: filters.has("participated"),
filter_unread: filters.has("unread"), filter_unread: filters.has("unread"),
filter_muted: filters.has("include_muted"), filter_muted: filters.has("include_muted"),
filter_pm: filters.has("include_private"),
is_spectator: page_params.is_spectator, is_spectator: page_params.is_spectator,
}); });
$("#recent_filters_group").html(rendered_filters); $("#recent_filters_group").html(rendered_filters);
@@ -498,26 +577,40 @@ export function update_filters_view() {
topics_widget.hard_redraw(); topics_widget.hard_redraw();
} }
function stream_sort(a, b) { function sort_comparator(a, b) {
const a_stream = message_store.get(a.last_msg_id).stream; // compares strings in lowercase and returns -1, 0, 1
const b_stream = message_store.get(b.last_msg_id).stream; if (a.toLowerCase() > b.toLowerCase()) {
if (a_stream > b_stream) {
return 1; return 1;
} else if (a_stream === b_stream) { } else if (a.toLowerCase() === b.toLowerCase()) {
return 0; return 0;
} }
return -1; return -1;
} }
function topic_sort(a, b) { function stream_sort(a, b) {
const a_topic = message_store.get(a.last_msg_id).topic; if (a.type === b.type) {
const b_topic = message_store.get(b.last_msg_id).topic; const a_msg = message_store.get(a.last_msg_id);
if (a_topic > b_topic) { const b_msg = message_store.get(b.last_msg_id);
return 1;
} else if (a_topic === b_topic) { if (a.type === "stream") {
return 0; 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) { function topic_offset_to_visible_area(topic_row) {
@@ -601,6 +694,7 @@ export function complete_rerender() {
filter_participated: filters.has("participated"), filter_participated: filters.has("participated"),
filter_unread: filters.has("unread"), filter_unread: filters.has("unread"),
filter_muted: filters.has("include_muted"), filter_muted: filters.has("include_muted"),
filter_pm: filters.has("include_private"),
search_val: $("#recent_topics_search").val() || "", search_val: $("#recent_topics_search").val() || "",
is_spectator: page_params.is_spectator, is_spectator: page_params.is_spectator,
}); });
@@ -611,7 +705,7 @@ export function complete_rerender() {
name: "recent_topics_table", name: "recent_topics_table",
$parent_container: $("#recent_topics_table"), $parent_container: $("#recent_topics_table"),
modifier(item) { modifier(item) {
return render_recent_topic_row(format_topic(item)); return render_recent_topic_row(format_conversation(item));
}, },
filter: { filter: {
// We use update_filters_view & filters_should_hide_topic to do all the // 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) { 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;
col_focus -= 1;
if (col_focus < 0) { if (col_focus < 0) {
col_focus = MAX_SELECTABLE_COLS - 1; col_focus = get_max_selectable_cols(row) - 1;
} }
} }
function right_arrow_navigation(row, col) { 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;
} }
col_focus += 1;
if (col_focus >= MAX_SELECTABLE_COLS) { col_focus += 1;
if (col_focus >= get_max_selectable_cols(row)) {
col_focus = 0; col_focus = 0;
} }
} }
function up_arrow_navigation(row, col) { 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; col_focus = 1;
} }
row_focus -= 1;
} }
function down_arrow_navigation(row, col) { 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; col_focus = 1;
} }
row_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) { export function change_focused_element($elt, input_key) {
// Called from hotkeys.js; like all logic in that module, // Called from hotkeys.js; like all logic in that module,
// returning true will cause the caller to do // returning true will cause the caller to do
@@ -913,7 +1033,13 @@ export function change_focused_element($elt, input_key) {
break; break;
case "up_arrow": case "up_arrow":
up_arrow_navigation(row_focus, col_focus); 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); set_table_focus(row_focus, col_focus, true);
return true; return true;
} }

View File

@@ -29,3 +29,15 @@ export function is_in_focus() {
export function get_topic_key(stream_id, topic) { export function get_topic_key(stream_id, topic) {
return stream_id + ":" + topic.toLowerCase(); 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}`);
}

View File

@@ -661,6 +661,11 @@ body.dark-theme {
#recent_topics_table { #recent_topics_table {
border-color: hsla(0, 0%, 0%, 0.6); border-color: hsla(0, 0%, 0%, 0.6);
.fa-envelope,
.fa-group {
opacity: 0.7;
}
} }
thead, thead,

View File

@@ -70,6 +70,21 @@
padding-right: 3px; 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 { .table_fix_head {
padding: 0 !important; padding: 0 !important;
/* 100px = space occupied by `recent_topics_filter_buttons`( ~49px) /* 100px = space occupied by `recent_topics_filter_buttons`( ~49px)
@@ -148,10 +163,31 @@
background-color: hsl(105, 2%, 50%); 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 { .unread_hidden {
visibility: 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 { .flex_container {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -287,11 +323,22 @@
as new messages arrive from the server. */ as new messages arrive from the server. */
.recent_topic_stream { .recent_topic_stream {
width: 25%; width: 25%;
padding: 8px; padding: 8px 0 8px 8px;
} }
.recent_topic_name { .recent_topic_name {
width: 40%; 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 { .recent_topic_users {
@@ -327,6 +374,11 @@
margin-right: 5px; margin-right: 5px;
font-size: 15px; font-size: 15px;
} }
.unread_count_pm {
/* Margin equal to size of recent topic actions */
margin-right: 44px;
}
} }
} }

View File

@@ -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"> <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}}"> <span id="stream_sidebar_privacy_swatch_{{stream_id}}" class="stream-privacy filter-icon" style="color: {{stream_color}}">
{{> stream_privacy }} {{> stream_privacy }}
</span> </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> </div>
</td> </td>
<td class="recent_topic_name"> <td class="recent_topic_name">
<div class="flex_container"> <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> <a href="{{topic_url}}">{{topic}}</a>
{{/if}}
</div> </div>
<div class="right_part"> <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_topic_actions">
<div class="recent_topics_focusable hidden-for-spectators"> <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> <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}} {{/if}}
</div> </div>
</div> </div>
{{/if}}
</div> </div>
</div> </div>
</td> </td>
<td class='recent_topic_users'> <td class='recent_topic_users'>
<ul class="recent_topics_participants"> <ul class="recent_topics_participants">
{{#if other_senders_count}} {{#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> <span class="recent_topics_participant_overflow">+{{other_senders_count}}</span>
</li> </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}} {{/if}}
{{#each senders}} {{#each senders}}
{{#if this.is_muted}} {{#if this.is_muted}}

View File

@@ -1,4 +1,12 @@
<button data-filter="all" type="button" class="btn btn-default btn-recent-filters">{{t 'All' }}</button> <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"> <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 }} {{#if filter_muted }}
<i class="fa fa-check-square-o"></i> <i class="fa fa-check-square-o"></i>

View File

@@ -134,6 +134,7 @@ EXEMPT_FILES = make_set(
"static/js/realm_playground.js", "static/js/realm_playground.js",
"static/js/realm_user_settings_defaults.ts", "static/js/realm_user_settings_defaults.ts",
"static/js/recent_topics_ui.js", "static/js/recent_topics_ui.js",
"static/js/recent_topics_util.js",
"static/js/reload.js", "static/js/reload.js",
"static/js/reminder.js", "static/js/reminder.js",
"static/js/resize.js", "static/js/resize.js",