Skip to content

Instantly share code, notes, and snippets.

@khsk
Last active September 26, 2017 08:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save khsk/0e8aaa498211a7ab2b1f6e37b93932b9 to your computer and use it in GitHub Desktop.
Save khsk/0e8aaa498211a7ab2b1f6e37b93932b9 to your computer and use it in GitHub Desktop.
Qiitaの記事でフォロイーのいいねを表示するユーザースクリプト ref: http://qiita.com/khsk/items/4ac21bd483b9b2be653a
// ==UserScript==
// @name Qiita show followees iine
// @namespace khsk
// @description 記事に自分(ユーザー)がフォローandいいねしているユーザーを表示
// @include http://qiita.com/*/items/*
// @include https://qiita.com/*/items/*
// @include http://qiita.com/*/private/*
// @include https://qiita.com/*/private/*
// @version 1
// @grant none
// ==/UserScript==
console.time('Qiita show followees iine')
const userId = ''; // 主に自分
const token = ''; // 各自で取得してください
const displayFolloweesNum = 5; // 表示数 -1 で無制限
(async () => {
// 表示場所作成
const iine = document.getElementsByClassName('list-inline ArticleMainHeader__users')[0]
const ciine = iine.cloneNode()
iine.parentElement.insertBefore(ciine, null)
// TODO いいね取得が長いから進捗を適当に書いているんだけれど、事前にheightを確保していないので、フォロイいねが無かったときなどにガタッとレイアウトが変わるかも。オプションめ。
ciine.innerText = 'フォロイー取得中'
let followees = sessionStorage.getItem('followees')
if (!followees) {
followees = await fetchAll('https://qiita.com/api/v2/users/' + userId + '/followees?per_page=100',user => {
// 無いとは思うが、sessionStorage容量上限対策として削れるものは削る
delete user.description
delete user.facebook_id
delete user.followees_count
delete user.followers_count
delete user.github_login_name
delete user.items_count
delete user.linkedin_id
delete user.location
delete user.prganization
delete user.permanent_id
delete user.twitter_screen_name
delete user.website_url
delete user.organization
return user
})
// 保存
followees = JSON.stringify(followees)
sessionStorage.setItem('followees', followees)
}
followees = JSON.parse(followees)
ciine.innerText = 'いいね取得中'
// 記事のいいね取得
const itemId = location.pathname.split('/').pop()
// 1ページ取得→即アイコン表示の方が止まっている疑惑が出にくいが…
let likes = await fetchAll('https://qiita.com/api/v2/items/' + itemId + '/likes?per_page=100', o => { return o.user.id })
ciine.innerText = ''
// 要素作成ループ
// ここ100*1000程度はありそうでヤバめ
// いいねしたフォロイーだけ抽出するとまたループが必要なのでDOMまで作るか…
// breakしたいしsomeは嫌だからforにするか…
// フォロイーといいねどちらを削除していけば性能良いかわからん…
for (let i = 0, likesLength = likes.length, displayedNum = 0; i < likesLength; i++ ) {
if (!followees.length || (displayFolloweesNum > 0 && displayFolloweesNum == displayedNum) ) {
break
}
for (let j = 0, followeesLength = followees.length; j < followeesLength; j++) {
if (likes[i] == followees[j].id) {
let followee = followees[j]
let id = followee.id
let userTemplate = iine.firstChild.cloneNode(true)
userTemplate.dataHovercardTargetName = id
userTemplate.querySelector('a').href = '/' + id
let img = userTemplate.querySelector('img')
img.alt = followee.name || id
img.src = followee.profile_image_url
// ツールチップ追加 フォロイーはアイコンでわかるだろうからオマケ程度
setPopover(userTemplate, followee)
ciine.insertBefore(userTemplate, null)
displayedNum++
// 表示し終わったフォロイーは配列から削除しようと思っていたが、影響が出るような数百単位のいいねの場合はいいね取得がボトルネックに見えるので保留にする
}
}
}
})()
/////////// functions
// 今回は無名関数でなくfuntionを使ってみる。ライブラリコピペを下部に持ってくるので、自作関数も下にまとめたい→変数なら上に書かなければ行けないので関数で
// レスポンスがないとnextがわからないのでオーバーヘッドがだいぶある。
// いいねやフォロイー総数から事前にページ数を計算すれば非同期並列で取得して最後にPromise.allで合算、で高速化できそうなんだけれども、手間がかかる。
// 計算せずとも最初のlinkにラストページが表示されるのでその数までループ、なら総数取得は無しに出来るかも。初回1本+以降並列。
// TODO 進捗表示のためにいちループごとに何か出来るコールバック作るか
async function fetchAll(url, callback = (v) =>{return v}) {
let next = url
let result = []
while (next) {
let response = await fetch(next, {
headers: new Headers({ Authorization: 'Bearer ' + token,}),
})
if (response.ok == false || response.status != 200) {
return result
}
next = li().parse(response.headers.get('Link')).next
let responseData = await response.json()
// ここデフォルト引数付けずに無いならmapしない方が性能いいけど
Array.prototype.push.apply(result, responseData.map(callback))
}
return result
}
// フィードのポップアップスクリプトの関数を持ってきて調整
function setPopover(elm, data) {
// Qiitaのhovercardはhovercard.jsではないのかもで諦め
// 最後に$(elm).tooltip() or popover()を実行するとタイトルがタグになる。謎。なので最初に設定
// datasetでは効かないものがある
$(elm).popover({
trigger : 'hover',
placement : 'bottom', // いいねは下付き
container : 'body',
html : true,
// 画像サイズ固定化や左配置のために、本家で使っている.hovervardクラスをデフォルトテンプレートに追加
template : '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-title"></h3><div class="popover-content hovercard"></div></div>'
})
elm.dataset.toggle = 'popover'
elm.dataset.originalTitle = 'Followee Info'
// hovercardっぽさのためにアイコン画像は今回は載せる
elm.dataset.content = '<img src="' + data.profile_image_url + '">' + '<br>\n' + (data.name || data.id)
// 改行対策にデフォルト値 + nowrap
elm.dataset.viewport = "{ selector: 'body', padding: 0 , white-space: nowrap,}"
}
/////////// 外部ライブラリ(コピペ)
// MITライセンス表示はこれで大丈夫かしら
/**
* Author : José F. Romaniello <jfromaniello@gmail.com> (http://joseoncode.com)
* License : MIT 2014 - JOSE F. ROMANIELLO https://opensource.org/licenses/mit-license.php
* URL : https://github.com/jfromaniello/li
**/
function li () {
// compile regular expressions ahead of time for efficiency
var relsRegExp = /^;\s*([^"=]+)=(?:"([^"]+)"|([^";,]+)(?:[;,]|$))/;
var keysRegExp = /([^\s]+)/g;
var sourceRegExp = /^<([^>]*)>/;
var delimiterRegExp = /^\s*,\s*/;
return {
parse: function (linksHeader, options) {
var match;
var source;
var rels;
var extended = options && options.extended || false;
var links = [];
while (linksHeader) {
linksHeader = linksHeader.trim();
// Parse `<link>`
source = sourceRegExp.exec(linksHeader);
if (!source) break;
var current = {
link: source[1]
};
// Move cursor
linksHeader = linksHeader.slice(source[0].length);
// Parse `; attr=relation` and `; attr="relation"`
var nextDelimiter = linksHeader.match(delimiterRegExp);
while(linksHeader && (!nextDelimiter || nextDelimiter.index > 0)) {
match = relsRegExp.exec(linksHeader);
if (!match) break;
// Move cursor
linksHeader = linksHeader.slice(match[0].length);
nextDelimiter = linksHeader.match(delimiterRegExp);
if (match[1] === 'rel' || match[1] === 'rev') {
// Add either quoted rel or unquoted rel
rels = (match[2] || match[3]).split(/\s+/);
current[match[1]] = rels;
} else {
current[match[1]] = match[2] || match[3];
}
}
links.push(current);
// Move cursor
linksHeader = linksHeader.replace(delimiterRegExp, '');
}
if (!extended) {
return links.reduce(function(result, currentLink) {
if (currentLink.rel) {
currentLink.rel.forEach(function(rel) {
result[rel] = currentLink.link;
});
}
return result;
}, {});
}
return links;
},
stringify: function (headerObject, callback) {
var result = "";
for (var x in headerObject) {
result += '<' + headerObject[x] + '>; rel="' + x + '", ';
}
result = result.substring(0, result.length - 2);
return result;
}
};
}
///////////////
console.timeEnd('Qiita show followees iine')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment