Refactor link previews
This commit is contained in:
parent
c7def1c1b6
commit
14b2ad7938
@ -1096,10 +1096,6 @@ kbd {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
#chat .msg.toggle .time {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#chat .toggle-button {
|
||||
background: #f5f5f5;
|
||||
border-radius: 2px;
|
||||
@ -1110,6 +1106,10 @@ kbd {
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
#chat .toggle-button:after {
|
||||
content: "···";
|
||||
}
|
||||
|
||||
#chat .toggle-content {
|
||||
background: #f5f5f5;
|
||||
border-radius: 2px;
|
||||
@ -1122,10 +1122,6 @@ kbd {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#chat .toggle-content a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#chat .toggle-content img {
|
||||
max-width: 100%;
|
||||
max-height: 128px;
|
||||
@ -1937,10 +1933,6 @@ kbd {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#chat .msg.toggle .time {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chat .date-marker,
|
||||
#chat .unread-marker {
|
||||
margin: 0;
|
||||
|
2
client/js/libs/jquery/stickyscroll.js
vendored
2
client/js/libs/jquery/stickyscroll.js
vendored
@ -37,7 +37,7 @@ import jQuery from "jquery";
|
||||
lastStick = Date.now();
|
||||
this.scrollTop = this.scrollHeight;
|
||||
})
|
||||
.on("msg.sticky", keepToBottom)
|
||||
.on("keepToBottom.sticky", keepToBottom)
|
||||
.scrollBottom();
|
||||
|
||||
return self;
|
||||
|
@ -681,25 +681,6 @@ $(function() {
|
||||
});
|
||||
});
|
||||
|
||||
chat.on("click", ".toggle-button", function() {
|
||||
var self = $(this);
|
||||
var localChat = self.closest(".chat");
|
||||
var bottom = localChat.isScrollBottom();
|
||||
var content = self.parent().next(".toggle-content");
|
||||
if (bottom && !content.hasClass("show")) {
|
||||
var img = content.find("img");
|
||||
if (img.length !== 0 && !img.width()) {
|
||||
img.on("load", function() {
|
||||
localChat.scrollBottom();
|
||||
});
|
||||
}
|
||||
}
|
||||
content.toggleClass("show");
|
||||
if (bottom) {
|
||||
localChat.scrollBottom();
|
||||
}
|
||||
});
|
||||
|
||||
var forms = $("#sign-in, #connect, #change-password");
|
||||
|
||||
windows.on("show", "#sign-in", function() {
|
||||
|
@ -30,6 +30,10 @@ const options = $.extend({
|
||||
|
||||
module.exports = options;
|
||||
|
||||
module.exports.shouldOpenMessagePreview = function(type) {
|
||||
return (options.links && type === "link") || (options.thumbnails && type === "image");
|
||||
};
|
||||
|
||||
for (var i in options) {
|
||||
if (i === "userStyles") {
|
||||
if (!/[?&]nocss/.test(window.location.search)) {
|
||||
|
@ -35,6 +35,10 @@ function buildChatMessage(data) {
|
||||
target = "#chan-" + chat.find(".active").data("id");
|
||||
}
|
||||
|
||||
if (data.msg.preview) {
|
||||
data.msg.preview.shown = options.shouldOpenMessagePreview(data.msg.preview.type);
|
||||
}
|
||||
|
||||
const chan = chat.find(target);
|
||||
let template = "msg";
|
||||
|
||||
|
@ -6,6 +6,7 @@ require("./init");
|
||||
require("./join");
|
||||
require("./more");
|
||||
require("./msg");
|
||||
require("./msg_preview");
|
||||
require("./names");
|
||||
require("./network");
|
||||
require("./nick");
|
||||
@ -13,6 +14,5 @@ require("./open");
|
||||
require("./part");
|
||||
require("./quit");
|
||||
require("./sync_sort");
|
||||
require("./toggle");
|
||||
require("./topic");
|
||||
require("./users");
|
||||
|
@ -35,7 +35,8 @@ socket.on("msg", function(data) {
|
||||
.trigger("msg", [
|
||||
target,
|
||||
data
|
||||
]);
|
||||
])
|
||||
.trigger("keepToBottom");
|
||||
|
||||
var lastVisible = container.find("div:visible").last();
|
||||
if (data.msg.self
|
||||
|
51
client/js/socket-events/msg_preview.js
Normal file
51
client/js/socket-events/msg_preview.js
Normal file
@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
|
||||
const $ = require("jquery");
|
||||
const socket = require("../socket");
|
||||
const templates = require("../../views");
|
||||
const options = require("../options");
|
||||
|
||||
socket.on("msg:preview", function(data) {
|
||||
data.preview.shown = options.shouldOpenMessagePreview(data.preview.type);
|
||||
|
||||
const msg = $("#msg-" + data.id);
|
||||
const container = msg.parent(".messages");
|
||||
const bottom = container.isScrollBottom();
|
||||
|
||||
msg.find(".text").append(templates.msg_preview({preview: data.preview}));
|
||||
|
||||
if (data.preview.shown && bottom) {
|
||||
handleImageInPreview(msg.find(".toggle-content"), container);
|
||||
}
|
||||
|
||||
container.trigger("keepToBottom");
|
||||
});
|
||||
|
||||
$("#chat").on("click", ".toggle-button", function() {
|
||||
const self = $(this);
|
||||
const container = self.closest(".messages");
|
||||
const content = self.parent().next(".toggle-content");
|
||||
const bottom = container.isScrollBottom();
|
||||
|
||||
if (bottom && !content.hasClass("show")) {
|
||||
handleImageInPreview(content, container);
|
||||
}
|
||||
|
||||
content.toggleClass("show");
|
||||
|
||||
// If scrollbar was at the bottom before toggling the preview, keep it at the bottom
|
||||
if (bottom) {
|
||||
container.scrollBottom();
|
||||
}
|
||||
});
|
||||
|
||||
function handleImageInPreview(content, container) {
|
||||
const img = content.find("img");
|
||||
|
||||
// Trigger scroll logic after the image loads
|
||||
if (img.length && !img.width()) {
|
||||
img.on("load", function() {
|
||||
container.trigger("keepToBottom");
|
||||
});
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const $ = require("jquery");
|
||||
const socket = require("../socket");
|
||||
const templates = require("../../views");
|
||||
const options = require("../options");
|
||||
|
||||
socket.on("toggle", function(data) {
|
||||
const toggle = $("#toggle-" + data.id);
|
||||
toggle.parent().after(templates.toggle({toggle: data}));
|
||||
switch (data.type) {
|
||||
case "link":
|
||||
if (options.links) {
|
||||
toggle.click();
|
||||
}
|
||||
break;
|
||||
|
||||
case "image":
|
||||
if (options.thumbnails) {
|
||||
toggle.click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
@ -25,9 +25,9 @@ module.exports = {
|
||||
date_marker: require("./date-marker.tpl"),
|
||||
msg: require("./msg.tpl"),
|
||||
msg_action: require("./msg_action.tpl"),
|
||||
msg_preview: require("./msg_preview.tpl"),
|
||||
msg_unhandled: require("./msg_unhandled.tpl"),
|
||||
network: require("./network.tpl"),
|
||||
toggle: require("./toggle.tpl"),
|
||||
unread_marker: require("./unread_marker.tpl"),
|
||||
user: require("./user.tpl"),
|
||||
user_filtered: require("./user_filtered.tpl"),
|
||||
|
@ -7,17 +7,10 @@
|
||||
{{> user_name nick=from}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{#equal type "toggle"}}
|
||||
<span class="text">
|
||||
<div class="force-newline">
|
||||
<button id="toggle-{{id}}" class="toggle-button" aria-label="Toggle prefetched media">···</button>
|
||||
</div>
|
||||
{{#if toggle}}
|
||||
{{> toggle}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="text">{{{parse text}}}</span>
|
||||
{{/equal}}
|
||||
<span class="text">
|
||||
{{~{parse text}~}}
|
||||
{{#if preview}}
|
||||
{{> msg_preview}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
|
18
client/views/msg_preview.tpl
Normal file
18
client/views/msg_preview.tpl
Normal file
@ -0,0 +1,18 @@
|
||||
{{#preview}}
|
||||
<div>
|
||||
<button class="toggle-button" aria-label="Toggle prefetched media"></button>
|
||||
</div>
|
||||
<a href="{{link}}" target="_blank" rel="noopener" class="toggle-content toggle-type-{{type}}{{#if shown}} show{{/if}}">
|
||||
{{#equal type "image"}}
|
||||
<img src="{{link}}">
|
||||
{{else}}
|
||||
{{#if thumb}}
|
||||
<img src="{{thumb}}" class="thumb">
|
||||
{{/if}}
|
||||
<div class="head">{{head}}</div>
|
||||
<div class="body">
|
||||
{{body}}
|
||||
</div>
|
||||
{{/equal}}
|
||||
</a>
|
||||
{{/preview}}
|
@ -1,19 +0,0 @@
|
||||
{{#toggle}}
|
||||
<div class="toggle-content toggle-type-{{type}}">
|
||||
{{#equal type "image"}}
|
||||
<a href="{{link}}" target="_blank">
|
||||
<img src="{{link}}">
|
||||
</a>
|
||||
{{else}}
|
||||
<a href="{{link}}" target="_blank">
|
||||
{{#if thumb}}
|
||||
<img src="{{thumb}}" class="thumb">
|
||||
{{/if}}
|
||||
<div class="head">{{head}}</div>
|
||||
<div class="body">
|
||||
{{body}}
|
||||
</div>
|
||||
</a>
|
||||
{{/equal}}
|
||||
</div>
|
||||
{{/toggle}}
|
@ -16,7 +16,6 @@ Msg.Type = {
|
||||
NOTICE: "notice",
|
||||
PART: "part",
|
||||
QUIT: "quit",
|
||||
TOGGLE: "toggle",
|
||||
CTCP: "ctcp",
|
||||
TOPIC: "topic",
|
||||
TOPIC_SET_BY: "topic_set_by",
|
||||
|
@ -1,35 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
const cheerio = require("cheerio");
|
||||
const Msg = require("../../models/msg");
|
||||
const request = require("request");
|
||||
const Helper = require("../../helper");
|
||||
const findLinks = require("../../../client/js/libs/handlebars/ircmessageparser/findLinks");
|
||||
const es = require("event-stream");
|
||||
|
||||
process.setMaxListeners(0);
|
||||
|
||||
module.exports = function(client, chan, originalMsg) {
|
||||
module.exports = function(client, chan, msg) {
|
||||
if (!Helper.config.prefetch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = originalMsg.text
|
||||
.replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "")
|
||||
.split(" ")
|
||||
.filter((w) => /^https?:\/\//.test(w));
|
||||
// Remove all IRC formatting characters before searching for links
|
||||
const cleanText = msg.text.replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "");
|
||||
|
||||
// We will only try to prefetch http(s) links
|
||||
const links = findLinks(cleanText).filter((w) => /^https?:\/\//.test(w.link));
|
||||
|
||||
if (links.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = new Msg({
|
||||
type: Msg.Type.TOGGLE,
|
||||
time: originalMsg.time,
|
||||
self: originalMsg.self,
|
||||
});
|
||||
chan.pushMessage(client, msg);
|
||||
|
||||
const link = escapeHeader(links[0]);
|
||||
const link = escapeHeader(links[0].link);
|
||||
fetch(link, function(res) {
|
||||
if (res === null) {
|
||||
return;
|
||||
@ -40,8 +34,7 @@ module.exports = function(client, chan, originalMsg) {
|
||||
};
|
||||
|
||||
function parse(msg, url, res, client) {
|
||||
var toggle = msg.toggle = {
|
||||
id: msg.id,
|
||||
const preview = {
|
||||
type: "",
|
||||
head: "",
|
||||
body: "",
|
||||
@ -52,35 +45,35 @@ function parse(msg, url, res, client) {
|
||||
switch (res.type) {
|
||||
case "text/html":
|
||||
var $ = cheerio.load(res.text);
|
||||
toggle.type = "link";
|
||||
toggle.head =
|
||||
preview.type = "link";
|
||||
preview.head =
|
||||
$("meta[property=\"og:title\"]").attr("content")
|
||||
|| $("title").text()
|
||||
|| "";
|
||||
toggle.body =
|
||||
preview.body =
|
||||
$("meta[property=\"og:description\"]").attr("content")
|
||||
|| $("meta[name=\"description\"]").attr("content")
|
||||
|| "";
|
||||
toggle.thumb =
|
||||
preview.thumb =
|
||||
$("meta[property=\"og:image\"]").attr("content")
|
||||
|| $("meta[name=\"twitter:image:src\"]").attr("content")
|
||||
|| "";
|
||||
|
||||
// Make sure thumbnail is a valid url
|
||||
if (!/^https?:\/\//.test(toggle.thumb)) {
|
||||
toggle.thumb = "";
|
||||
if (!/^https?:\/\//.test(preview.thumb)) {
|
||||
preview.thumb = "";
|
||||
}
|
||||
|
||||
// Verify that thumbnail pic exists and is under allowed size
|
||||
if (toggle.thumb.length) {
|
||||
fetch(escapeHeader(toggle.thumb), (resThumb) => {
|
||||
if (preview.thumb.length) {
|
||||
fetch(escapeHeader(preview.thumb), (resThumb) => {
|
||||
if (resThumb === null
|
||||
|| !(/^image\/.+/.test(resThumb.type))
|
||||
|| resThumb.size > (Helper.config.prefetchMaxImageSize * 1024)) {
|
||||
toggle.thumb = "";
|
||||
preview.thumb = "";
|
||||
}
|
||||
|
||||
emitToggle(client, toggle);
|
||||
emitPreview(client, msg, preview);
|
||||
});
|
||||
|
||||
return;
|
||||
@ -93,7 +86,7 @@ function parse(msg, url, res, client) {
|
||||
case "image/jpg":
|
||||
case "image/jpeg":
|
||||
if (res.size < (Helper.config.prefetchMaxImageSize * 1024)) {
|
||||
toggle.type = "image";
|
||||
preview.type = "image";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@ -103,21 +96,26 @@ function parse(msg, url, res, client) {
|
||||
return;
|
||||
}
|
||||
|
||||
emitToggle(client, toggle);
|
||||
emitPreview(client, msg, preview);
|
||||
}
|
||||
|
||||
function emitToggle(client, toggle) {
|
||||
function emitPreview(client, msg, preview) {
|
||||
// If there is no title but there is preview or description, set title
|
||||
// otherwise bail out and show no preview
|
||||
if (!toggle.head.length) {
|
||||
if (toggle.thumb.length || toggle.body.length) {
|
||||
toggle.head = "Untitled page";
|
||||
if (!preview.head.length && preview.type === "link") {
|
||||
if (preview.thumb.length || preview.body.length) {
|
||||
preview.head = "Untitled page";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
client.emit("toggle", toggle);
|
||||
msg.preview = preview;
|
||||
|
||||
client.emit("msg:preview", {
|
||||
id: msg.id,
|
||||
preview: preview
|
||||
});
|
||||
}
|
||||
|
||||
function fetch(url, cb) {
|
||||
|
@ -4,10 +4,14 @@ var assert = require("assert");
|
||||
|
||||
var util = require("../util");
|
||||
var link = require("../../src/plugins/irc-events/link.js");
|
||||
const path = require("path");
|
||||
|
||||
describe("Link plugin", function() {
|
||||
before(function(done) {
|
||||
this.app = util.createWebserver();
|
||||
this.app.get("/real-test-image.png", function(req, res) {
|
||||
res.sendFile(path.resolve(__dirname, "../../client/img/apple-touch-icon-120x120.png"));
|
||||
});
|
||||
this.connection = this.app.listen(9002, done);
|
||||
});
|
||||
|
||||
@ -28,11 +32,132 @@ describe("Link plugin", function() {
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
|
||||
this.app.get("/basic", function(req, res) {
|
||||
res.send("<title>test</title>");
|
||||
res.send("<title>test title</title><meta name='description' content='simple description'>");
|
||||
});
|
||||
|
||||
this.irc.once("toggle", function(data) {
|
||||
assert.equal(data.head, "test");
|
||||
this.irc.once("msg:preview", function(data) {
|
||||
assert.equal(data.preview.type, "link");
|
||||
assert.equal(data.preview.head, "test title");
|
||||
assert.equal(data.preview.body, "simple description");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should prefer og:title over title", function(done) {
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:9002/basic-og"
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
|
||||
this.app.get("/basic-og", function(req, res) {
|
||||
res.send("<title>test</title><meta property='og:title' content='opengraph test'>");
|
||||
});
|
||||
|
||||
this.irc.once("msg:preview", function(data) {
|
||||
assert.equal(data.preview.head, "opengraph test");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should prefer og:description over description", function(done) {
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:9002/description-og"
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
|
||||
this.app.get("/description-og", function(req, res) {
|
||||
res.send("<meta name='description' content='simple description'><meta property='og:description' content='opengraph description'>");
|
||||
});
|
||||
|
||||
this.irc.once("msg:preview", function(data) {
|
||||
assert.equal(data.preview.body, "opengraph description");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should find og:image with full url", function(done) {
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:9002/thumb"
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
|
||||
this.app.get("/thumb", function(req, res) {
|
||||
res.send("<title>Google</title><meta property='og:image' content='http://localhost:9002/real-test-image.png'>");
|
||||
});
|
||||
|
||||
this.irc.once("msg:preview", function(data) {
|
||||
assert.equal(data.preview.head, "Google");
|
||||
assert.equal(data.preview.thumb, "http://localhost:9002/real-test-image.png");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not use thumbnail with invalid url", function(done) {
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:9002/invalid-thumb"
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
|
||||
this.app.get("/invalid-thumb", function(req, res) {
|
||||
res.send("<title>test</title><meta property='og:image' content='/real-test-image.png'>");
|
||||
});
|
||||
|
||||
this.irc.once("msg:preview", function(data) {
|
||||
assert.equal(data.preview.thumb, "");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should send untitled page if there is a thumbnail", function(done) {
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:9002/thumb-no-title"
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
|
||||
this.app.get("/thumb-no-title", function(req, res) {
|
||||
res.send("<meta property='og:image' content='http://localhost:9002/real-test-image.png'>");
|
||||
});
|
||||
|
||||
this.irc.once("msg:preview", function(data) {
|
||||
assert.equal(data.preview.head, "Untitled page");
|
||||
assert.equal(data.preview.thumb, "http://localhost:9002/real-test-image.png");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send thumbnail if image is 404", function(done) {
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:9002/thumb-404"
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
|
||||
this.app.get("/thumb-404", function(req, res) {
|
||||
res.send("<title>404 image</title><meta property='og:image' content='http://localhost:9002/this-image-does-not-exist.png'>");
|
||||
});
|
||||
|
||||
this.irc.once("msg:preview", function(data) {
|
||||
assert.equal(data.preview.head, "404 image");
|
||||
assert.equal(data.preview.thumb, "");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should send image preview", function(done) {
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:9002/real-test-image.png"
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
|
||||
this.irc.once("msg:preview", function(data) {
|
||||
assert.equal(data.preview.type, "image");
|
||||
assert.equal(data.preview.link, "http://localhost:9002/real-test-image.png");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user