const render_all_messages_sidebar_actions = require('../templates/all_messages_sidebar_actions.hbs'); const render_delete_topic_modal = require('../templates/delete_topic_modal.hbs'); const render_starred_messages_sidebar_actions = require('../templates/starred_messages_sidebar_actions.hbs'); const render_stream_sidebar_actions = require('../templates/stream_sidebar_actions.hbs'); const render_topic_sidebar_actions = require('../templates/topic_sidebar_actions.hbs'); const render_unstar_messages_modal = require("../templates/unstar_messages_modal.hbs"); const render_move_topic_to_stream = require("../templates/move_topic_to_stream.hbs"); // We handle stream popovers and topic popovers in this // module. Both are popped up from the left sidebar. let current_stream_sidebar_elem; let current_topic_sidebar_elem; let all_messages_sidebar_elem; let starred_messages_sidebar_elem; function elem_to_stream_id(elem) { const stream_id = parseInt(elem.attr('data-stream-id'), 10); if (stream_id === undefined) { blueslip.error('could not find stream id'); } return stream_id; } function topic_popover_stream_id(e) { return elem_to_stream_id($(e.currentTarget)); } exports.stream_popped = function () { return current_stream_sidebar_elem !== undefined; }; exports.topic_popped = function () { return current_topic_sidebar_elem !== undefined; }; exports.all_messages_popped = function () { return all_messages_sidebar_elem !== undefined; }; exports.starred_messages_popped = function () { return starred_messages_sidebar_elem !== undefined; }; exports.hide_stream_popover = function () { if (exports.stream_popped()) { $(current_stream_sidebar_elem).popover("destroy"); current_stream_sidebar_elem = undefined; } }; exports.hide_topic_popover = function () { if (exports.topic_popped()) { $(current_topic_sidebar_elem).popover("destroy"); current_topic_sidebar_elem = undefined; } }; exports.hide_all_messages_popover = function () { if (exports.all_messages_popped()) { $(all_messages_sidebar_elem).popover("destroy"); all_messages_sidebar_elem = undefined; } }; exports.hide_starred_messages_popover = function () { if (exports.starred_messages_popped()) { $(starred_messages_sidebar_elem).popover("destroy"); starred_messages_sidebar_elem = undefined; } }; // These are the only two functions that is really shared by the // two popovers, so we could split out topic stuff to // another module pretty easily. exports.show_streamlist_sidebar = function () { $(".app-main .column-left").addClass("expanded"); resize.resize_page_components(); }; exports.hide_streamlist_sidebar = function () { $(".app-main .column-left").removeClass("expanded"); }; function stream_popover_sub(e) { const elem = $(e.currentTarget).parents('ul'); const stream_id = elem_to_stream_id(elem); const sub = stream_data.get_sub_by_id(stream_id); if (!sub) { blueslip.error('Unknown stream: ' + stream_id); return; } return sub; } // This little function is a workaround for the fact that // Bootstrap popovers don't properly handle being resized -- // so after resizing our popover to add in the spectrum color // picker, we need to adjust its height accordingly. function update_spectrum(popover, update_func) { const initial_height = popover[0].offsetHeight; const colorpicker = popover.find('.colorpicker-container').find('.colorpicker'); update_func(colorpicker); const after_height = popover[0].offsetHeight; const popover_root = popover.closest(".popover"); const current_top_px = parseFloat(popover_root.css('top').replace('px', '')); const height_delta = after_height - initial_height; let top = current_top_px - height_delta / 2; if (top < 0) { top = 0; popover_root.find("div.arrow").hide(); } else if (top + after_height > $(window).height() - 20) { top = $(window).height() - after_height - 20; if (top < 0) { top = 0; } popover_root.find("div.arrow").hide(); } popover_root.css('top', top + "px"); } function build_stream_popover(opts) { const elt = opts.elt; const stream_id = opts.stream_id; if (exports.stream_popped() && current_stream_sidebar_elem === elt) { // If the popover is already shown, clicking again should toggle it. exports.hide_stream_popover(); return; } popovers.hide_all(); exports.show_streamlist_sidebar(); const content = render_stream_sidebar_actions({ stream: stream_data.get_sub_by_id(stream_id), }); $(elt).popover({ content: content, html: true, trigger: "manual", fixed: true, fix_positions: true, }); $(elt).popover("show"); const popover = $('.streams_popover[data-stream-id=' + stream_id + ']'); update_spectrum(popover, function (colorpicker) { colorpicker.spectrum(stream_color.sidebar_popover_colorpicker_options); }); current_stream_sidebar_elem = elt; } function build_topic_popover(opts) { const elt = opts.elt; const stream_id = opts.stream_id; const topic_name = opts.topic_name; if (exports.topic_popped() && current_topic_sidebar_elem === elt) { // If the popover is already shown, clicking again should toggle it. exports.hide_topic_popover(); return; } const sub = stream_data.get_sub_by_id(stream_id); if (!sub) { blueslip.error('cannot build topic popover for stream: ' + stream_id); return; } popovers.hide_all(); exports.show_streamlist_sidebar(); const is_muted = muting.is_topic_muted(sub.stream_id, topic_name); const can_mute_topic = !is_muted; const can_unmute_topic = is_muted; const content = render_topic_sidebar_actions({ stream_name: sub.name, stream_id: sub.stream_id, topic_name: topic_name, can_mute_topic: can_mute_topic, can_unmute_topic: can_unmute_topic, is_admin: sub.is_admin, color: sub.color, }); $(elt).popover({ content: content, html: true, trigger: "manual", fixed: true, }); $(elt).popover("show"); current_topic_sidebar_elem = elt; } function build_all_messages_popover(e) { const elt = e.target; if (exports.all_messages_popped() && all_messages_sidebar_elem === elt) { exports.hide_all_messages_popover(); e.stopPropagation(); return; } popovers.hide_all(); const content = render_all_messages_sidebar_actions(); $(elt).popover({ content: content, html: true, trigger: "manual", fixed: true, }); $(elt).popover("show"); all_messages_sidebar_elem = elt; e.stopPropagation(); } function build_starred_messages_popover(e) { const elt = e.target; if (exports.starred_messages_popped() && starred_messages_sidebar_elem === elt) { exports.hide_starred_messages_popover(); e.stopPropagation(); return; } popovers.hide_all(); const content = render_starred_messages_sidebar_actions({ starred_message_counts: page_params.starred_message_counts, }); $(elt).popover({ content: content, html: true, trigger: "manual", fixed: true, }); $(elt).popover("show"); starred_messages_sidebar_elem = elt; e.stopPropagation(); } function build_move_topic_to_stream_popover(e, current_stream_id, topic_name) { // TODO: Add support for keyboard-alphabet navigation. Some orgs // many streams and scrolling can be a painful process in that // case. // // NOTE: Private streams are also included in this list. We // likely will make it possible to move messages to/from private // streams in the future. const available_streams = stream_data.subscribed_subs() .filter((s) => s.stream_id !== current_stream_id); const current_stream_name = stream_data.maybe_get_stream_name(current_stream_id); const args = { available_streams, topic_name, current_stream_id, current_stream_name, notify_new_thread: message_edit.notify_new_thread_default, notify_old_thread: message_edit.notify_old_thread_default, }; exports.hide_topic_popover(); $("#move-a-topic-modal-holder").html(render_move_topic_to_stream(args)); const stream_header_colorblock = $(".topic_stream_edit_header").find(".stream_header_colorblock"); ui_util.decorate_stream_bar(current_stream_name, stream_header_colorblock, false); $("#select_stream_id").change(function () { const stream_name = stream_data.maybe_get_stream_name(parseInt(this.value, 10)); ui_util.decorate_stream_bar(stream_name, stream_header_colorblock, false); }); $('#move_topic_modal').modal('show'); e.stopPropagation(); } exports.register_click_handlers = function () { $('#stream_filters').on('click', '.stream-sidebar-menu-icon', function (e) { e.stopPropagation(); const elt = e.target; const stream_li = $(elt).parents('li'); const stream_id = elem_to_stream_id(stream_li); build_stream_popover({ elt: elt, stream_id: stream_id, }); }); $('#stream_filters').on('click', '.topic-sidebar-menu-icon', function (e) { e.stopPropagation(); const elt = $(e.target).closest('.topic-sidebar-menu-icon').expectOne()[0]; const stream_li = $(elt).closest('.narrow-filter').expectOne(); const stream_id = elem_to_stream_id(stream_li); const topic_name = $(elt).closest('li').expectOne().attr('data-topic-name'); build_topic_popover({ elt: elt, stream_id: stream_id, topic_name: topic_name, }); }); $('#global_filters').on('click', '.all-messages-sidebar-menu-icon', build_all_messages_popover); $('#global_filters').on('click', '.starred-messages-sidebar-menu-icon', build_starred_messages_popover); exports.register_stream_handlers(); exports.register_topic_handlers(); }; exports.register_stream_handlers = function () { // Stream settings $('body').on('click', '.open_stream_settings', function (e) { const sub = stream_popover_sub(e); exports.hide_stream_popover(); const stream_edit_hash = hash_util.stream_edit_uri(sub); hashchange.go_to_location(stream_edit_hash); }); // Pin/unpin $('body').on('click', '.pin_to_top', function (e) { const sub = stream_popover_sub(e); exports.hide_stream_popover(); subs.toggle_pin_to_top_stream(sub); e.stopPropagation(); }); // Mark all messages in stream as read $('body').on('click', '.mark_stream_as_read', function (e) { const sub = stream_popover_sub(e); exports.hide_stream_popover(); unread_ops.mark_stream_as_read(sub.stream_id); e.stopPropagation(); }); // Mark all messages as read $('body').on('click', '#mark_all_messages_as_read', function (e) { exports.hide_all_messages_popover(); unread_ops.mark_all_as_read(); e.stopPropagation(); }); // Unstar all messages $('body').on('click', '#unstar_all_messages', function (e) { exports.hide_starred_messages_popover(); e.preventDefault(); e.stopPropagation(); $(".left-sidebar-modal-holder").empty(); $(".left-sidebar-modal-holder").html(render_unstar_messages_modal()); $("#unstar-messages-modal").modal("show"); }); $('body').on('click', '#do_unstar_messages_button', function (e) { $("#unstar-messages-modal").modal("hide"); message_flags.unstar_all_messages(); e.stopPropagation(); }); // Toggle displaying starred message count $('body').on('click', '#toggle_display_starred_msg_count', function (e) { exports.hide_starred_messages_popover(); e.preventDefault(); e.stopPropagation(); const starred_msg_counts = page_params.starred_message_counts; const data = {}; data.starred_message_counts = JSON.stringify(!starred_msg_counts); channel.patch({ url: '/json/settings/display', data: data, }); }); // Mute/unmute $('body').on('click', '.toggle_home', function (e) { const sub = stream_popover_sub(e); exports.hide_stream_popover(); subs.toggle_home(sub); e.stopPropagation(); }); // Unsubscribe $('body').on("click", ".popover_sub_unsub_button", function (e) { $(this).toggleClass("unsub"); $(this).closest(".popover").fadeOut(500).delay(500).remove(); const sub = stream_popover_sub(e); subs.sub_or_unsub(sub); e.preventDefault(); e.stopPropagation(); }); // Choose a different color. $('body').on('click', '.choose_stream_color', function (e) { update_spectrum($(e.target).closest('.streams_popover'), function (colorpicker) { $('.colorpicker-container').show(); colorpicker.spectrum("destroy"); colorpicker.spectrum(stream_color.sidebar_popover_colorpicker_options_full); // In theory this should clean up the old color picker, // but this seems a bit flaky -- the new colorpicker // doesn't fire until you click a button, but the buttons // have been hidden. We work around this by just manually // fixing it up here. colorpicker.parent().find('.sp-container').removeClass('sp-buttons-disabled'); $(e.target).hide(); }); $('.streams_popover').on('click', 'a.sp-cancel', function () { exports.hide_stream_popover(); }); }); }; function topic_popover_sub(e) { const stream_id = topic_popover_stream_id(e); if (!stream_id) { blueslip.error('cannot find stream id'); return; } const sub = stream_data.get_sub_by_id(stream_id); if (!sub) { blueslip.error('Unknown stream: ' + stream_id); return; } return sub; } exports.register_topic_handlers = function () { // Narrow to topic $('body').on('click', '.narrow_to_topic', function (e) { exports.hide_topic_popover(); const sub = topic_popover_sub(e); if (!sub) { return; } const topic = $(e.currentTarget).attr('data-topic-name'); const operators = [ {operator: 'stream', operand: sub.name}, {operator: 'topic', operand: topic}, ]; narrow.activate(operators, {trigger: 'sidebar'}); e.stopPropagation(); }); // Mute the topic $('body').on('click', '.sidebar-popover-mute-topic', function (e) { const stream_id = topic_popover_stream_id(e); if (!stream_id) { return; } const topic = $(e.currentTarget).attr('data-topic-name'); muting_ui.mute(stream_id, topic); e.stopPropagation(); e.preventDefault(); }); // Unmute the topic $('body').on('click', '.sidebar-popover-unmute-topic', function (e) { const stream_id = topic_popover_stream_id(e); if (!stream_id) { return; } const topic = $(e.currentTarget).attr('data-topic-name'); muting_ui.unmute(stream_id, topic); e.stopPropagation(); e.preventDefault(); }); // Mark all messages as read $('body').on('click', '.sidebar-popover-mark-topic-read', function (e) { const stream_id = topic_popover_stream_id(e); if (!stream_id) { return; } const topic = $(e.currentTarget).attr('data-topic-name'); exports.hide_topic_popover(); unread_ops.mark_topic_as_read(stream_id, topic); e.stopPropagation(); }); // Deleting all message in a topic $('body').on('click', '.sidebar-popover-delete-topic-messages', function (e) { const stream_id = topic_popover_stream_id(e); if (!stream_id) { return; } const topic = $(e.currentTarget).attr('data-topic-name'); const args = { topic_name: topic, }; exports.hide_topic_popover(); $('#delete-topic-modal-holder').html(render_delete_topic_modal(args)); $('#do_delete_topic_button').on('click', function () { message_edit.delete_topic(stream_id, topic); }); $('#delete_topic_modal').modal('show'); e.stopPropagation(); }); $('body').on('click', '.sidebar-popover-move-topic-messages', function (e) { const topic_row = $(e.currentTarget); const stream_id = parseInt(topic_row.attr('data-stream-id'), 10); const topic_name = topic_row.attr('data-topic-name'); build_move_topic_to_stream_popover(e, stream_id, topic_name); e.stopPropagation(); e.preventDefault(); }); $('body').on('click', '#topic_stream_edit_form_error .send-status-close', function () { $("#topic_stream_edit_form_error").hide(); }); $('body').on('click', '#do_move_topic_button', function (e) { function show_error_msg(msg) { $("#topic_stream_edit_form_error .error-msg").text(msg); $("#topic_stream_edit_form_error").show(); } const params = $('#move_topic_form').serializeArray().reduce(function (obj, item) { obj[item.name] = item.value; return obj; }, {}); const {old_topic_name, select_stream_id} = params; let {current_stream_id, new_topic_name, send_notification_to_new_thread, send_notification_to_old_thread} = params; new_topic_name = new_topic_name.trim(); send_notification_to_new_thread = send_notification_to_new_thread === 'on'; send_notification_to_old_thread = send_notification_to_old_thread === 'on'; current_stream_id = parseInt(current_stream_id, 10); if (current_stream_id === parseInt(select_stream_id, 10) && new_topic_name.toLowerCase() === old_topic_name.toLowerCase()) { show_error_msg("Please select a different stream or change topic name."); return false; } // The API endpoint for editing messages to change their // content, topic, or stream requires a message ID. // // Because we don't have full data in the browser client, it's // possible that we might display a topic in the left sidebar // (and thus expose the UI for moving its topic to another // stream) without having a message ID that is definitely // within the topic. (The comments in stream_topic_history.js // discuss the tricky issues around message deletion that are // involved here). // // To ensure this option works reliably at a small latency // cost for a rare operation, we just ask the server for the // latest message ID in the topic. const data = { anchor: 'newest', num_before: 1, num_after: 0, narrow: JSON.stringify( [{operator: "stream", operand: current_stream_id}, {operator: "topic", operand: old_topic_name}] ), }; channel.get({ url: '/json/messages', data: data, idempotent: true, success: function (data) { const message_id = data.messages[0].id; if (old_topic_name.trim() === new_topic_name.trim()) { // We use `undefined` to tell the server that // there has been no change in the topic name. new_topic_name = undefined; } if (old_topic_name && select_stream_id) { message_edit.move_topic_containing_message_to_stream( message_id, select_stream_id, new_topic_name, send_notification_to_new_thread, send_notification_to_old_thread); $('#move_topic_modal').modal('hide'); } }, error: function (xhr) { show_error_msg(xhr.responseJSON.msg); }, }); e.preventDefault(); }); }; window.stream_popover = exports;