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_privacy }} - - {{stream}} +
+
+ {{#if is_private}} + + Private messages + {{else}} + + {{> stream_privacy }} + + {{stream}} + {{/if}} +
+ {{!-- For presence/group indicator --}} + {{#if is_private}} +
+ + {{#if is_group}} + + {{else}} + + {{/if}} + +
+ {{/if}}
-
+
+ {{#if is_private}} + {{pm_with}} + {{else}} {{topic}} + {{/if}}
+ {{#if is_private}} + {{unread_count}} + {{else}}
{{unread_count}} @@ -27,16 +53,17 @@ {{/if}}
+ {{/if}}