├── scripts ├── chrome └── firefox ├── README.md ├── firefox └── manifest.json ├── chrome └── manifest.json ├── LICENSE ├── dashboard.css └── dashboard.js /scripts/chrome: -------------------------------------------------------------------------------- 1 | mkdir tmp && cp *.* tmp && cp chrome/manifest.json tmp/manifest.json && cd tmp && zip -r ../chrome.zip * && cd .. && rm -rf tmp -------------------------------------------------------------------------------- /scripts/firefox: -------------------------------------------------------------------------------- 1 | mkdir tmp && cp *.* tmp && cp firefox/manifest.json tmp/manifest.json && cd tmp && zip -r ../firefox.zip * && cd .. && rm -rf tmp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Activity filter for GitHub 2 | 3 | A web extension for filtering the activity feed on github.com dashboard. 4 | 5 | ⚠️ This does not work with the new "For you" event feed. 6 | 7 | ![screenshot](https://user-images.githubusercontent.com/1153134/45003071-37311780-afac-11e8-8a2a-f64ba93f6593.png) 8 | 9 | ### Install 10 | 11 | - [Download in Chrome Web Store](https://chrome.google.com/webstore/detail/pcnaddhmngnnpookfhhamkelhhakimdg) 12 | - [Download in Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/dashboard-filter-for-github/) 13 | -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dashboard filter for GitHub", 3 | "short_name": "dashboard", 4 | "version": "0.8.8", 5 | "manifest_version": 2, 6 | "description": "This adds an activity filter menu to GitHub dashboard.", 7 | "homepage_url": "http://github.com/muan/dashboard", 8 | "permissions": [ 9 | "https://api.github.com/*" 10 | ], 11 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 12 | "content_scripts": [ 13 | { 14 | "matches": ["https://github.com/", "https://github.com/orgs/*/dashboard"], 15 | "css": ["dashboard.css"], 16 | "js": ["dashboard.js"] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dashboard filter for GitHub", 3 | "short_name": "dashboard", 4 | "version": "0.8.8", 5 | "manifest_version": 3, 6 | "description": "This adds an activity filter menu to GitHub dashboard.", 7 | "homepage_url": "http://github.com/muan/dashboard", 8 | "content_security_policy": { 9 | "extension_pages": "script-src 'self'; object-src 'self'" 10 | }, 11 | "content_scripts": [ 12 | { 13 | "matches": [ 14 | "https://github.com/", 15 | "https://github.com/orgs/*/dashboard" 16 | ], 17 | "css": [ 18 | "dashboard.css" 19 | ], 20 | "js": [ 21 | "dashboard.js" 22 | ] 23 | } 24 | ], 25 | "host_permissions": [ 26 | "https://api.github.com/*" 27 | ] 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mu-An Chiou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dashboard.css: -------------------------------------------------------------------------------- 1 | .by_followed_people.commit_comment, 2 | .by_internet.commit_comment, 3 | .body.by_internet:has(.commit_comment), 4 | .by_followed_people.create, 5 | .body.by_followed_people:has(.create), 6 | .by_internet.create, 7 | .body.by_internet:has(.create), 8 | .by_followed_people.follow, 9 | .body.by_followed_people:has(.follow), 10 | .by_internet.follow, 11 | .body.by_internet:has(.follow), 12 | .by_followed_people.fork, 13 | .body.by_followed_people:has(.fork), 14 | .by_internet.fork, 15 | .body.by_internet:has(.fork), 16 | .by_followed_people.git-branch, 17 | .body.by_followed_people:has(.git-branch), 18 | .by_internet.git-branch, 19 | .body.by_internet:has(.git-branch), 20 | .by_followed_people.gollum, 21 | .body.by_followed_people:has(.gollum), 22 | .by_internet.gollum, 23 | .body.by_internet:has(.gollum), 24 | .by_followed_people.issues_closed, 25 | .body.by_followed_people:has(.issues_closed), 26 | .by_internet.issues_closed, 27 | .body.by_internet:has(.issues_closed), 28 | .by_followed_people.issues_comment, 29 | .body.by_followed_people:has(.issues_comment), 30 | .by_internet.issues_comment, 31 | .body.by_internet:has(.issues_comment), 32 | .by_followed_people.issues_labeled, 33 | .body.by_followed_people:has(.issues_labeled), 34 | .by_internet.issues_labeled, 35 | .body.by_internet:has(.issues_labeled), 36 | .by_followed_people.issues_opened, 37 | .body.by_followed_people:has(.issues_opened), 38 | .by_internet.issues_opened, 39 | .body.by_internet:has(.issues_opened), 40 | .by_followed_people.issues_reopened, 41 | .body.by_followed_people:has(.issues_reopened), 42 | .by_internet.issues_reopened, 43 | .body.by_internet:has(.issues_reopened), 44 | .by_followed_people.issues_merged, 45 | .body.by_followed_people:has(.issues_merged), 46 | .by_internet.issues_merged, 47 | .body.by_internet:has(.issues_merged), 48 | .by_followed_people.public, 49 | .body.by_followed_people:has(.public), 50 | .by_internet.public, 51 | .body.by_internet:has(.public), 52 | .by_followed_people.push, 53 | .body.by_followed_people:has(.push), 54 | .by_internet.push, 55 | .body.by_internet:has(.push), 56 | .by_followed_people[data-hydro-click*='PushEvent'], 57 | .body.by_followed_people:has([data-hydro-click*='PushEvent']), 58 | .by_internet[data-hydro-click*='PushEvent'], 59 | .body.by_internet:has([data-hydro-click*='PushEvent']), 60 | .by_followed_people.release, 61 | .body.by_followed_people:has(.release), 62 | .by_internet.release, 63 | .body.by_internet:has(.release), 64 | .by_followed_people.repo, 65 | .body.by_followed_people:has(.repo), 66 | .by_internet.repo, 67 | .body.by_internet:has(.repo), 68 | .by_followed_people.tag, 69 | .body.by_followed_people:has(.tag), 70 | .by_internet.tag, 71 | .body.by_internet:has(.tag), 72 | .by_followed_people.team_add, 73 | .body.by_followed_people:has(.team_add), 74 | .by_internet.team_add, 75 | .body.by_internet:has(.team_add), 76 | .by_followed_people.member_add, 77 | .body.by_followed_people:has(.member_add), 78 | .by_internet.member_add, 79 | .body.by_internet:has(.member_add), 80 | .by_followed_people.watch_started, 81 | .body.by_followed_people:has(.watch_started), 82 | .by_internet.watch_started, 83 | .body.by_internet:has(.watch_started), 84 | .by_followed_people.sponsor, 85 | .body.by_followed_people:has(.sponsor), 86 | .by_internet.sponsor, 87 | .body.by_internet:has(.sponsor) { 88 | display: none; 89 | } 90 | 91 | .show_starred_and_followed_by .watch_started.by_internet, 92 | .show_starred_and_followed_by .body.by_internet:has(.watch_started), 93 | .show_starred_and_followed_by .follow.by_internet, 94 | .show_starred_and_followed_by .body.by_internet:has(.follow) { 95 | display: block; 96 | } 97 | 98 | .show_sponsored_by .sponsor.by_followed_people, 99 | .show_sponsored_by .body.by_followed_people:has(.sponsor), 100 | .show_sponsored_by .sponsor.by_internet, 101 | .show_sponsored_by .body.by_internet:has(.sponsor) { 102 | display: block; 103 | } 104 | 105 | .show_forked_by .fork.by_internet, 106 | .show_forked_by .body.by_internet:has(.fork), 107 | .show_forks .fork.by_followed_people, 108 | .show_forks .body.by_followed_people:has(.fork) { 109 | display: block; 110 | } 111 | 112 | .show_stars_and_follows .watch_started.by_followed_people, 113 | .show_stars_and_follows .body.by_followed_people:has(.watch_started), 114 | .show_stars_and_follows .follow.by_followed_people, 115 | .show_stars_and_follows .body.by_followed_people:has(.follow) { 116 | display: block; 117 | } 118 | 119 | .show_open_source .repo.by_followed_people, 120 | .show_open_source .body.by_followed_people:has(.repo), 121 | .show_open_source .create.by_followed_people, 122 | .show_open_source .body.by_followed_people:has(.create), 123 | .show_open_source .public.by_followed_people, 124 | .show_open_source .body.by_followed_people:has(.public) { 125 | display: block; 126 | } 127 | 128 | .show_releases .tag, 129 | .show_releases .release { 130 | display: block; 131 | } 132 | 133 | .show_code .gollum, 134 | .show_code .git-branch, 135 | .show_code .issues_merged, 136 | .show_code .push, 137 | .show_code .Details [data-hydro-click*='PushEvent'] { 138 | display: block; 139 | } 140 | 141 | .show_conversations .issues_comment, 142 | .show_conversations .commit_comment, 143 | .show_conversations .issues_opened, 144 | .show_conversations .issues_labeled, 145 | .show_conversations .issues_closed, 146 | .show_conversations .issues_reopened { 147 | display: block; 148 | } 149 | 150 | .show_administration .team_add, 151 | .show_administration .member_add { 152 | display: block; 153 | } 154 | 155 | .js-recent-activity-container + details { 156 | float: left; 157 | margin-bottom: -6px; 158 | margin-right: 10px; 159 | margin-top: -3px; 160 | } 161 | -------------------------------------------------------------------------------- /dashboard.js: -------------------------------------------------------------------------------- 1 | // Could break if GitHub changes its markup 2 | const context = document.querySelector('[data-ga-click*="context:organization"]') ? 'org' : 'user' 3 | const menuItems = { 4 | user: [ 5 | 'Watched repositories --', 6 | 'Code', 7 | 'Releases', 8 | 'Conversations', 9 | 'Following --', 10 | 'Open source', 11 | 'Stars and follows', 12 | 'Forks', 13 | 'You --', 14 | 'Starred and followed by', 15 | 'Forked by', 16 | 'Sponsored by' 17 | ], 18 | org: [ 19 | 'Code', 20 | 'Releases', 21 | 'Conversations', 22 | 'Administration' 23 | ] 24 | } 25 | 26 | const eventClasses = [ 27 | // Code 28 | '.git-branch', '.push', '.gollum', '.issues_merged', '[data-hydro-click*=\'PushEvent\']', 29 | // Releases 30 | '.release', '.tag', 31 | // Conversations 32 | '.issues_closed', '.issues_labeled', '.issues_opened', '.issues_reopened', '.commit_comment', '.issues_comment', 33 | // Open source 34 | '.create', '.public', '.repo', 35 | // Stars and follows / Starred and followed by 36 | '.watch_started', '.follow', 37 | // Forks / Forked by 38 | '.fork', 39 | // Sponsorship 40 | '.sponsor', 41 | // Administration 42 | '.team_add', '.member_add' 43 | ].join() 44 | 45 | let listOfFollowees 46 | 47 | init() 48 | updateClasses() 49 | if (context === 'user') specifyTimelineEvents() 50 | 51 | document.addEventListener('change', function (evt) { 52 | if (evt.target.classList.contains('js-dashboard-filter-checkbox')) { 53 | updateClasses() 54 | rememberPreference() 55 | } 56 | }) 57 | 58 | document.addEventListener('click', function (evt) { 59 | if (evt.shiftKey && evt.target.classList.contains('js-dashboard-filter-label')) { 60 | for (const checkbox of document.querySelectorAll('.js-dashboard-filter-checkbox')) { 61 | if (checkbox === evt.target) continue 62 | checkbox.checked = false 63 | } 64 | } 65 | }) 66 | 67 | function updateClasses() { 68 | const target = document.querySelector('#dashboard') 69 | for (const checkbox of document.querySelectorAll('.js-dashboard-filter-checkbox')) { 70 | target.classList.toggle(`show_${checkbox.id}`, checkbox.checked) 71 | } 72 | } 73 | 74 | function init () { 75 | const details = document.createElement('details') 76 | const classes = context === 'user' 77 | ? ['position-relative', 'js-dropdown-details', 'details-overlay', 'float-left', 'mt-2', 'mr-3'] 78 | : ['position-relative', 'js-dropdown-details', 'details-overlay', 'mb-n1', 'mt-3', 'mb-2'] 79 | details.classList.add(...classes) 80 | details.style.userSelect = 'none' 81 | const summary = document.createElement('summary') 82 | const btnClasses = context === 'user' ? ['btn'] : ['btn', 'btn-sm'] 83 | summary.classList.add(...btnClasses) 84 | summary.textContent = 'Filter' 85 | const container = document.createElement('div') 86 | container.classList.add('dropdown-menu', 'dropdown-menu-se', 'f5') 87 | container.style.width = '260px' 88 | 89 | for (const key of menuItems[context]) { 90 | const isHeader = key.split(/ --$/) 91 | if (isHeader.length > 1) { 92 | const header = document.createElement('div') 93 | header.textContent = isHeader[0] 94 | header.classList.add('dropdown-header') 95 | container.appendChild(header) 96 | continue 97 | } 98 | const id = key.toLowerCase().replace(/\s/g, '_').replace(/\/_/g, '') 99 | const input = document.createElement('input') 100 | input.type = 'checkbox' 101 | input.id = id 102 | input.className = 'position-absolute my-2 ml-3 js-dashboard-filter-checkbox' 103 | 104 | const label = document.createElement('label') 105 | label.className = 'pl-6 dropdown-item js-dashboard-filter-label' 106 | label.innerText = key 107 | label.htmlFor = id 108 | 109 | container.appendChild(input) 110 | container.appendChild(label) 111 | } 112 | details.appendChild(summary) 113 | details.appendChild(container) 114 | 115 | const positionMarker = document.querySelector('#dashboard') 116 | if (positionMarker) { 117 | positionMarker.prepend(details) 118 | applyPreference() 119 | } else { 120 | console.log('Dashboard extension: position marker not found.') 121 | } 122 | } 123 | 124 | function rememberPreference () { 125 | const preference = JSON.parse(localStorage.getItem(`dashboard:select:${context}`) || '{}') 126 | for (const box of document.querySelectorAll('.js-dashboard-filter-checkbox')) { 127 | preference[box.id] = box.checked 128 | } 129 | 130 | localStorage.setItem(`dashboard:select:${context}`, JSON.stringify(preference)) 131 | } 132 | 133 | function applyPreference () { 134 | const preference = JSON.parse(localStorage.getItem(`dashboard:select:${context}`) || '{}') 135 | 136 | for (const box of document.querySelectorAll('.js-dashboard-filter-checkbox')) { 137 | box.checked = (typeof preference[box.id] === 'boolean') ? preference[box.id] : true 138 | } 139 | } 140 | 141 | function specifyTimelineEvents() { 142 | const dashboard = document.querySelector('#dashboard .news') 143 | if (!dashboard) return 144 | const observer = new MutationObserver(addMoreSpecificIdentifiers) 145 | observer.observe(dashboard, {subtree: true, childList: true}) 146 | } 147 | 148 | async function getFolloweeList() { 149 | if (listOfFollowees) return listOfFollowees 150 | 151 | console.log('Dashboard extension: getting list of people you follow from localStorage') 152 | const followees = localStorage.getItem('dashboard:following') 153 | if (!followees || (followees && (new Date().getTime() - new Date(JSON.parse(followees).updatedAt))/1000 > 24*60*60)) { 154 | const results = await fetchFollowees() 155 | const followees = { 156 | updatedAt: (new Date()).getTime(), 157 | following: results 158 | } 159 | localStorage.setItem('dashboard:following', JSON.stringify(followees)) 160 | listOfFollowees = results 161 | } else { 162 | listOfFollowees = JSON.parse(followees).following 163 | } 164 | 165 | return listOfFollowees 166 | } 167 | 168 | async function fetchFollowees() { 169 | console.log('Dashboard extension: updating list of people you follow from GitHub API (once every 24h)') 170 | return new Promise(async function(resolve) { 171 | let followees = [] 172 | const user = document.querySelector('.HeaderNavlink.name .avatar, .Header-link .avatar').alt.slice(1) 173 | const endpoint = `https://api.github.com/users/${user}/following` 174 | let page = 1 175 | while (page > 0) { 176 | const res = await fetch(`${endpoint}?page=${page}`) 177 | const people = await res.json() 178 | followees = followees.concat(people) 179 | if (people.length === 30) { 180 | page++ 181 | } else { 182 | page = 0 183 | resolve(followees.map(o => o.login)) 184 | } 185 | } 186 | }) 187 | } 188 | 189 | // Could break if GitHub changes its markup 190 | async function addMoreSpecificIdentifiers(list) { 191 | const followees = await getFolloweeList() 192 | for (const record of list) { 193 | const closestParent = record.target.closest('.news, #panel-1') 194 | if (!closestParent) continue 195 | 196 | for (const eventItem of record.target.querySelectorAll(eventClasses)) { 197 | if (!(eventItem instanceof HTMLElement)) continue 198 | let target = eventItem 199 | const parentEventItem = target.parentElement.closest(eventClasses) 200 | if (parentEventItem) continue 201 | 202 | const expandable = target.parentElement.closest('.body:has(.Details)') 203 | if (expandable) target = expandable 204 | 205 | // Check if any links are to one of the followed people 206 | const fromFollowedPeople = Array.from(target.querySelectorAll('a')).some(function(maybeActor) { 207 | return followees.indexOf(maybeActor.pathname.slice(1)) >= 0 208 | }) 209 | target.classList.add(fromFollowedPeople ? 'by_followed_people' : 'by_internet') 210 | } 211 | } 212 | } 213 | --------------------------------------------------------------------------------