diff --git a/frontend_tests/node_tests/example5.js b/frontend_tests/node_tests/example5.js
index d83bfe3d28..5f8117016c 100644
--- a/frontend_tests/node_tests/example5.js
+++ b/frontend_tests/node_tests/example5.js
@@ -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
diff --git a/frontend_tests/node_tests/recent_topics.js b/frontend_tests/node_tests/recent_topics.js
index ede004fbd1..77e9507559 100644
--- a/frontend_tests/node_tests/recent_topics.js
+++ b/frontend_tests/node_tests/recent_topics.js
@@ -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('
{
{
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
diff --git a/frontend_tests/node_tests/unread.js b/frontend_tests/node_tests/unread.js
index f793805d9f..6da0c6dae3 100644
--- a/frontend_tests/node_tests/unread.js
+++ b/frontend_tests/node_tests/unread.js
@@ -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 = {
diff --git a/static/js/click_handlers.js b/static/js/click_handlers.js
index 92aa87a87b..274d543432 100644
--- a/static/js/click_handlers.js
+++ b/static/js/click_handlers.js
@@ -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,
diff --git a/static/js/compose_closed_ui.js b/static/js/compose_closed_ui.js
index 7ed0e4b704..dcbd7207c5 100644
--- a/static/js/compose_closed_ui.js
+++ b/static/js/compose_closed_ui.js
@@ -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,
diff --git a/static/js/recent_topics_data.js b/static/js/recent_topics_data.js
index 532b89396a..b8e868876c 100644
--- a/static/js/recent_topics_data.js
+++ b/static/js/recent_topics_data.js
@@ -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
diff --git a/static/js/recent_topics_ui.js b/static/js/recent_topics_ui.js
index ed8d8f4145..ae468157ab 100644
--- a/static/js/recent_topics_ui.js
+++ b/static/js/recent_topics_ui.js
@@ -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 = {
- stream: $topic_row.find(".recent_topic_stream a").text(),
- topic: $topic_row.find(".recent_topic_name a").text(),
- };
+ // 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;
- // 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);
+ if (type === "stream") {
+ const stream_info = sub_store.get(last_msg.stream_id);
- // 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);
+ // 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);
- // 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);
+ // 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.
+ 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
+ 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.
+ 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("
");
+ 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 sort_comparator(a_msg.display_reply_to, b_msg.display_reply_to);
}
- return -1;
+ // 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;
}
diff --git a/static/js/recent_topics_util.js b/static/js/recent_topics_util.js
index 49525e7fef..7c609b348e 100644
--- a/static/js/recent_topics_util.js
+++ b/static/js/recent_topics_util.js
@@ -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}`);
+}
diff --git a/static/styles/dark_theme.css b/static/styles/dark_theme.css
index 69db5127e6..17ce6ac6c5 100644
--- a/static/styles/dark_theme.css
+++ b/static/styles/dark_theme.css
@@ -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,
diff --git a/static/styles/recent_topics.css b/static/styles/recent_topics.css
index adb1612c2a..4ba33f9bdf 100644
--- a/static/styles/recent_topics.css
+++ b/static/styles/recent_topics.css
@@ -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;
+ }
}
}
diff --git a/static/templates/recent_topic_row.hbs b/static/templates/recent_topic_row.hbs
index ff0ae6dabb..7425023740 100644
--- a/static/templates/recent_topic_row.hbs
+++ b/static/templates/recent_topic_row.hbs
@@ -1,18 +1,44 @@
-
+
-
-
- {{stream}}
+
+
+ {{!-- For presence/group indicator --}}
+ {{#if is_private}}
+
+
+ {{#if is_group}}
+
+ {{else}}
+
+ {{/if}}
+
+
+ {{/if}}
|
-
+
+ {{#if is_private}}
+ {{unread_count}}
+ {{else}}
{{unread_count}}
@@ -27,16 +53,17 @@
{{/if}}
+ {{/if}}
|
{{#if other_senders_count}}
- -
+
-
+{{other_senders_count}}
- {{{other_sender_names_html}}}
+ {{{other_sender_names_html}}}
{{/if}}
{{#each senders}}
{{#if this.is_muted}}
diff --git a/static/templates/recent_topics_filters.hbs b/static/templates/recent_topics_filters.hbs
index c5df770c34..aedc9a8265 100644
--- a/static/templates/recent_topics_filters.hbs
+++ b/static/templates/recent_topics_filters.hbs
@@ -1,4 +1,12 @@
+
|