popovers: Enabled link result pastable both as a plain URL and HTML.

Fixes: #31813 addressed the feedback and added test for the module.
This commit is contained in:
OmarAmeen01 2024-10-16 19:26:36 +05:30
parent e0bd3713cc
commit 226493dc4d
4 changed files with 157 additions and 12 deletions

View File

@ -0,0 +1,42 @@
import * as hash_util from "./hash_util";
import * as stream_data from "./stream_data";
export function generate_formatted_link(link_text: string): string {
const stream_topic = hash_util.decode_stream_topic_from_url(link_text);
if (!stream_topic) {
return "Invalid stream";
}
const stream = stream_data.get_sub_by_id(stream_topic.stream_id);
// had to replace <> characters from stream and topic name
// with HTML entity because sites supporting html won't
// render that name and treats that name as tag.
const topic_name = stream_topic.topic_name?.replace(/</g, "&lt;")?.replace(/>/g, "&gt;");
const stream_name = stream?.name?.replace(/</g, "&lt;")?.replace(/>/g, "&gt;");
if (topic_name !== undefined) {
return `<a href="${link_text}">#${stream_name}>${topic_name}</a>`;
}
return `<a href="${link_text}">#${stream_name}</a>`;
}
export function copy_to_clipboard(link_text: string, after_copy_cb: () => void): void {
const formatted_url = generate_formatted_link(link_text);
if (formatted_url === "Invalid stream") {
return;
}
const clipboardItem = new ClipboardItem({
"text/plain": new Blob([link_text], {
type: "text/plain",
}),
"text/html": new Blob([formatted_url], {type: "text/html"}),
});
void navigator.clipboard.write([clipboardItem]).then(after_copy_cb);
}

View File

@ -1,4 +1,3 @@
import ClipboardJS from "clipboard";
import $ from "jquery";
import assert from "minimalistic-assert";
@ -8,6 +7,7 @@ import render_left_sidebar_stream_actions_popover from "../templates/popovers/le
import * as blueslip from "./blueslip.ts";
import * as browser_history from "./browser_history.ts";
import * as clipboard_handler from "./clipboard_handler";
import * as composebox_typeahead from "./composebox_typeahead.ts";
import * as dialog_widget from "./dialog_widget.ts";
import * as dropdown_widget from "./dropdown_widget.ts";
@ -212,9 +212,11 @@ function build_stream_popover(opts) {
$(e.currentTarget).hide();
e.stopPropagation();
});
new ClipboardJS($popper.find(".copy_stream_link")[0]).on("success", () => {
popover_menus.hide_current_popover_if_visible(instance);
$popper.on("click", ".copy_stream_link", () => {
clipboard_handler.copy_to_clipboard(
$(".copy_stream_link").data("clipboard-text"),
() => popover_menus.hide_current_popover_if_visible(instance),
);
});
},
onHidden() {

View File

@ -1,9 +1,9 @@
import ClipboardJS from "clipboard";
import $ from "jquery";
import render_delete_topic_modal from "../templates/confirm_dialog/confirm_delete_topic.hbs";
import render_left_sidebar_topic_actions_popover from "../templates/popovers/left_sidebar/left_sidebar_topic_actions_popover.hbs";
import * as clipboard_handler from "./clipboard_handler";
import * as confirm_dialog from "./confirm_dialog.ts";
import {$t_html} from "./i18n.ts";
import * as message_edit from "./message_edit.ts";
@ -157,13 +157,12 @@ export function initialize() {
stream_popover.build_move_topic_to_stream_popover(stream_id, topic_name, true);
popover_menus.hide_current_popover_if_visible(instance);
});
new ClipboardJS($popper.find(".sidebar-popover-copy-link-to-topic")[0]).on(
"success",
() => {
popover_menus.hide_current_popover_if_visible(instance);
},
$popper.on("click", ".sidebar-popover-copy-link-to-topic", () => {
clipboard_handler.copy_to_clipboard(
$(".sidebar-popover-copy-link-to-topic").data("clipboard-text"),
() => popover_menus.hide_current_popover_if_visible(instance),
);
});
},
onHidden(instance) {
instance.destroy();

View File

@ -0,0 +1,102 @@
"use strict";
const assert = require("node:assert/strict");
const {zrequire} = require("./lib/namespace");
const {make_stub} = require("./lib/stub");
const {run_test} = require("./lib/test");
const clipboard_handler = zrequire("clipboard_handler");
const stream_data = zrequire("stream_data");
const stream = {
name: "Stream",
description: "Color and Lights",
stream_id: 1,
subscribed: true,
type: "stream",
};
const markdown_stream = {
name: "<Stream*$`&>",
description: "Colors and lights",
stream_id: 2,
subscribe: true,
type: "stream",
};
stream_data.add_sub(stream);
stream_data.add_sub(markdown_stream);
const normal_stream_with_topic =
"http://zulip.zulipdev.com/#narrow/stream/1-Stream/topic/normal.20topic";
const markdown_stream_with_normal_topic =
"http://zulip.zulipdev.com/#narrow/channel/2-.3CStream*.24.60.26.3E/topic/normal.20topic";
const normal_stream_with_markdown_topic =
"http://zulip.zulipdev.com/#narrow/stream/1-Stream/topic/.3C.24topic.60*.26.3E";
const markdown_stream_with_markdown_topic =
"http://zulip.zulipdev.com/#narrow/channel/2-.3CStream*.24.60.26.3E/topic/.3C.24topic.60*.26.3E";
const invalid_stream = "http://zulip.zulipdev.com/#narrow/stream/99-Stream";
const normal_stream_no_topic = "http://zulip.zulipdev.com/#narrow/stream/1-Stream";
const markdown_stream_no_topic =
"http://zulip.zulipdev.com/#narrow/channel/2-.3CStream*.24.60.26.3E";
run_test("generate_formatted_url", () => {
assert.equal(
clipboard_handler.generate_formatted_link(normal_stream_with_topic),
`<a href="${normal_stream_with_topic}">#Stream>normal topic</a>`,
);
assert.equal(
clipboard_handler.generate_formatted_link(markdown_stream_with_normal_topic),
`<a href="${markdown_stream_with_normal_topic}">#&lt;Stream*$\`&&gt;>normal topic</a>`,
);
assert.equal(
clipboard_handler.generate_formatted_link(normal_stream_with_markdown_topic),
`<a href="${normal_stream_with_markdown_topic}">#Stream>&lt;$topic\`*&&gt;</a>`,
);
assert.equal(
clipboard_handler.generate_formatted_link(markdown_stream_with_markdown_topic),
`<a href="${markdown_stream_with_markdown_topic}">#&lt;Stream*$\`&&gt;>&lt;$topic\`*&&gt;</a>`,
);
assert.equal(clipboard_handler.generate_formatted_link(invalid_stream), "Invalid stream");
assert.equal(
clipboard_handler.generate_formatted_link(normal_stream_no_topic),
`<a href="${normal_stream_no_topic}">#Stream</a>`,
);
assert.equal(
clipboard_handler.generate_formatted_link(markdown_stream_no_topic),
`<a href="${markdown_stream_no_topic}">#&lt;Stream*$\`&&gt;</a>`,
);
});
global.ClipboardItem = class {
constructor(items) {
this.items = items;
}
};
global.navigator = {
clipboard: {},
};
run_test("copy_to_clipboard", async ({override}) => {
const after_copy_cb = make_stub();
// replacing navigator.clipboard.write with custom function
override(global.navigator.clipboard, "write", () => Promise.resolve());
// ensuring that function returns early when link in invalid
await clipboard_handler.copy_to_clipboard(invalid_stream, after_copy_cb.f);
assert.deepEqual(after_copy_cb.num_calls, 0);
// ensuring function is beign called and after_copy_cb is triggered when link is correct
await clipboard_handler.copy_to_clipboard(normal_stream_with_topic, after_copy_cb.f);
assert.deepEqual(global.navigator.clipboard.write(), Promise.resolve());
assert.deepEqual(after_copy_cb.num_calls, 1);
});