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 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

View File

@ -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

View File

@ -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 = {

View File

@ -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,

View File

@ -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,

View File

@ -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

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_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("<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 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;
}

View File

@ -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}`);
}

View File

@ -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,

View File

@ -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;
}
}
}

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">
<div class="recent_topics_focusable">
<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>
<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="{{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}}

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="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>

View File

@ -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",