Skip to content

Commit

Permalink
feat(embed): add cutting-edge embed with facepile
Browse files Browse the repository at this point in the history
A new experimental cutting-edge version of the embed, that will change frequently and thus should be expected to break and therefore not really used live unless one watches this project very closely, has been added and with it a new Facepile feature that separates out all like/repost interactions from the rest and only includes such interactions if they explicitly liked or reposted something matching the query of the embed (often the page the embed is soon)

One uses this new embed by adding a new `version` query parameter thats set to ”cutting-edge” and if one doesn’t want a facepile because one might be building eg a mentions feed, then one can also add a new `nofacepile` query parameter to include the likes and posts like they used to be included.
  • Loading branch information
voxpelli committed Dec 30, 2014
1 parent e12e339 commit 175a0cf
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 41 deletions.
152 changes: 112 additions & 40 deletions lib/routes/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ var LRU = require('lru-cache'),
getMentions,
pingSuccessResponse,
pingErrorResponse,
handleCuttingEdgeEmbed,
handleLegacyEmbed,
mentionsEmitter = new EventEmitter();

pubsub = new PGPubsub(options.db);
Expand Down Expand Up @@ -92,27 +94,32 @@ distillMention = function (row) {
return false;
}

var data = _.pick(row.data, ['url', 'name', 'published', 'summary', 'author']);
var data = _.pick(row.data, ['url', 'name', 'published', 'summary', 'author', 'interactionType', 'interactions']);

data.author = _.pick(data.author || {}, ['name', 'photo', 'url']);

data.url = data.url || row.url;
data.targets = row.targets;
data.type = row.type || data.interactionType || 'mention';
data.interactions = data.interactions || [];

delete data.interactionType;

return data;
};

getMention = function (entryId) {
return knex('entries')
.first('entries.url as url', 'data', knex.raw('array_agg(mentions.url) as targets'))
.first('entries.url as url', 'data', 'type', knex.raw('array_agg(mentions.url) as targets'))
.innerJoin('mentions', 'entries.id', 'mentions.eid')
.where('id', entryId)
.groupBy('entries.id')
.then(distillMention);
};

getMentions = function (target) {
getMentions = function (target, options) {
target = target || {};
options = options || {};

if (!_.isPlainObject(target)) {
target = { url: target };
Expand All @@ -124,39 +131,59 @@ getMentions = function (target) {
return Promise.resolve([]);
}

var query = knex('mentions').distinct('eid');
var entryQuery = knex('entries'),
query = knex('mentions').distinct('eid'),
interactionTypes = ['like', 'repost'];

if (!_.isEmpty(target.url)) {
//TODO: Validate URL?
target.url = _.isArray(target.url) ? target.url : [target.url];
query = query.orWhereIn('mentions.normalizedUrl', _.map(target.url, urlTools.normalizeUrl));
}
if (!_.isEmpty(target.site)) {
target.site = _.isArray(target.site) ? target.site : [target.site];
query = query.orWhereIn('mentions.hostname', _.map(target.site, function (hostname) {
if (!urlTools.simpleHostnameValidation.test(hostname)) {
return undefined;
}
try {
return urlTools.normalizeUrl('http://' + hostname + '/', { raw: true }).hostname;
} catch (e) {
return undefined;
}
}));
}
if (!_.isEmpty(target.path)) {
target.path = _.isArray(target.path) ? target.path : [target.path];
_.each(target.path, function (path) {
query = query.orWhere('normalizedUrl', 'like', urlTools.normalizeUrl(path).replace(/\\/g, '').replace(/[%_]/g, '\\%') + '%');
});
query = query.where(function() {
if (!_.isEmpty(target.url)) {
//TODO: Validate URL?
target.url = _.isArray(target.url) ? target.url : [target.url];
this.orWhereIn('mentions.normalizedUrl', _.map(target.url, urlTools.normalizeUrl));
}
if (!_.isEmpty(target.site)) {
target.site = _.isArray(target.site) ? target.site : [target.site];
this.orWhereIn('mentions.hostname', _.map(target.site, function (hostname) {
if (!urlTools.simpleHostnameValidation.test(hostname)) {
return undefined;
}
try {
return urlTools.normalizeUrl('http://' + hostname + '/', { raw: true }).hostname;
} catch (e) {
return undefined;
}
}));
}
if (!_.isEmpty(target.path)) {
target.path = _.isArray(target.path) ? target.path : [target.path];
_.each(target.path, function (path) {
this.orWhere('normalizedUrl', 'like', urlTools.normalizeUrl(path).replace(/\\/g, '').replace(/[%_]/g, '\\%') + '%');
}.bind(this));
}
});

if (options.interactions === true) {
query = query.where('interaction', true);
}

return knex('entries')
.select('entries.url as url', 'data', knex.raw('array_agg(mentions.url) as targets'))
entryQuery = entryQuery.select(
'entries.url as url',
'data',
'type',
knex.raw('array_agg(mentions.url) as targets')
)
.innerJoin('mentions', 'entries.id', 'mentions.eid')
.whereIn('id', query)
.groupBy('entries.id')
.orderBy('published', 'asc') //TODO: Add option for instead sort on fetched
.orderBy('published', 'asc'); //TODO: Add option for instead sort on fetched

if (options.interactions === true) {
entryQuery = entryQuery.whereIn('type', interactionTypes);
} else if (options.interactions === false) {
entryQuery = entryQuery.whereNotIn('type', interactionTypes);
}

return entryQuery
.map(distillMention)
.then(
function (rows) {
Expand Down Expand Up @@ -224,20 +251,65 @@ pingErrorResponse = function (res, err) {
});
};

handleCuttingEdgeEmbed = function (req, res) {
var nofacepile = req.query.nofacepile;

Promise.all([
getMentions(mentionsArguments(req), {
interactions: nofacepile ? undefined : false,
}),
nofacepile ? Promise.resolve([]) : getMentions(mentionsArguments(req), {
interactions: true,
}),
]).then(function (result) {
var mentions = result[0],
interactions = result[1],
query = _.clone(req.query);

delete query.version;

res.setHeader('Content-Type', 'text/javascript');
res.render('cutting-edge-embed.ejs', {
interactions: interactions,
mentions: mentions,
options: {
baseUrl: (options.https ? 'https://' : 'http://') + options.hostname,
query: qs.stringify(query),
},
});
});
};

handleLegacyEmbed = function (req, res) {
getMentions(mentionsArguments(req)).then(function (mentions) {
// Remove unneeded data
mentions = _.map(mentions, function (mention) {
return _.omit(mention, ['targets', 'type', 'interactions']);
});

// And render the embed script
res.setHeader('Content-Type', 'text/javascript');
res.render('embed.ejs', {
mentions: mentions,
options: {
baseUrl: (options.https ? 'https://' : 'http://') + options.hostname,
query: qs.stringify(req.query),
},
});
});
};

// Route setup

module.exports = function (app) {
app.get('/api/embed', function (req, res) {
getMentions(mentionsArguments(req)).then(function (mentions) {
res.setHeader('Content-Type', 'text/javascript');
res.render('embed.ejs', {
mentions: mentions,
options: {
baseUrl: (options.https ? 'https://' : 'http://') + options.hostname,
query: qs.stringify(req.query),
},
});
});
var version = req.query.version;

if (version === 'cutting-edge') {
handleCuttingEdgeEmbed(req, res);
} else {
handleLegacyEmbed(req, res);
}
});

app.get('/api/mentions', cors(), function (req, res) {
Expand Down
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ <h2>Example: The mentions of this site</h2>
url = document.querySelectorAll ? document.querySelectorAll('link[rel~=canonical]') : false;
url = url && url[0] ? url[0].href : false;
sn.type = 'text/javascript'; sn.async = true;
sn.src = '//' + window.location.host + '/api/embed?url=' + encodeURIComponent(url || window.location);
sn.src = '//' + window.location.host + '/api/embed?version=cutting-edge&url=' + encodeURIComponent(url || window.location);
s.parentNode.insertBefore(sn, s);
}());
</script>
Expand Down
45 changes: 45 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,48 @@ textarea:first-of-type{margin-top:0}
width: 7%;
border-radius: 240px;
}
.webmention-facepile {
margin-bottom: 25px;
}
.webmention-facepile > li {
display: inline-block;
position: relative;
margin: 0 5px 5px 0;
}
.webmention-interaction-presentation:after {
display: block;
position: absolute;
bottom: -4px;
right: -4px;
padding: 4px;
width: 1em;
height: 1em;

font-size: 12px;
line-height: 1;
text-align: center;

color: #000;
background: #eee;
border: 1px solid #ccc;
border-radius: 50px;
}
.webmention-interaction-like .webmention-interaction-presentation:after {content: "♥"}
.webmention-interaction-repost .webmention-interaction-presentation:after {content: "♺"}
.webmention-interaction-presentation > span {
display: block;
text-indent: -5000em;
position: relative;
overflow: hidden;
width: 2em;
height: 2em;
border-radius: 240px;
background: #eee;
border: 1px solid #ccc;
}
.webmention-interaction-presentation > span > img {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
Loading

0 comments on commit 175a0cf

Please sign in to comment.