Refactor link previews

This commit is contained in:
Pavel Djundik 2017-06-26 12:01:55 +03:00
parent c7def1c1b6
commit 14b2ad7938
16 changed files with 250 additions and 127 deletions

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

@ -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");

View File

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

View 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");
});
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

@ -16,7 +16,6 @@ Msg.Type = {
NOTICE: "notice",
PART: "part",
QUIT: "quit",
TOGGLE: "toggle",
CTCP: "ctcp",
TOPIC: "topic",
TOPIC_SET_BY: "topic_set_by",

View File

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

View File

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