├── .gitignore ├── README.md ├── background.js ├── contentscript.js ├── dist ├── background.js ├── contentscript.js ├── groups.js ├── icon128.png ├── icon16.png ├── icon32.png ├── icon48.png ├── inject.js ├── manifest.json ├── options │ ├── analytics.js │ ├── index.html │ ├── options.js │ └── vendor.bundle.js └── utils.js ├── groups.js ├── icon128.png ├── icon16.png ├── icon32.png ├── icon48.png ├── inject.js ├── manifest.json ├── options.dev ├── ApiKeyForm.jsx ├── BlacklistGroups.jsx ├── HighlightMatches.jsx ├── KeywordsFilter.jsx ├── analytics.js ├── app.jsx ├── groups.js ├── index.html ├── keywords.js ├── react-tags.css └── style.css ├── package.json ├── scrape-group-lists.js ├── utils.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | key.pem 4 | *.crx 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Merge Facebook Dev Circles by Interests 2 | 3 | > A Chrome extension that filters through Facebook Dev Circles around the world to get only the interesting posts in your feed. 4 | 5 | [![Merge Facebook Dev Circles by Interests](https://i.imgur.com/evnMkm8.jpg)](https://www.youtube.com/watch?v=yJPD3iW6ZvY) 6 | *https://www.youtube.com/watch?v=yJPD3iW6ZvY* 7 | 8 | > Won Developer Circles Community Challenge by Facebook \ 9 | > as "Best App by a Student Team". Hurray! \ 10 | > See: https://devpost.com/software/fb-dev-interest 11 | 12 | ## Vision 13 | 14 | Instead of having dev circles according to locations, we can now have posts from various dev circles based on our interests. 15 | Developers are generally interested in a common set of coding languages and they work on a predefined tech stack and as the community grows, the group communication becomes difficult also sub groups gets difficult to manage which makes the community weak even after having so many members. The Vision is to maintain that community and show relevant content to group users as much as possible. 16 | 17 | We are starting with Facebook Dev circles and soon will be making it general for every Tech group on Facebook and in the long run we want the extension to be available for every other group based on how users use it. 18 | 19 | 20 | ## What is it? 21 | 22 | It is a Google Chrome extension that replaces the feed of your Facebook Dev Circle group according to your interests. 23 | 24 | 25 | ## How it helps Developers? 26 | 27 | The most important asset for a Developer is time. Getting the job done in a minimal time is the most important task. People use Dev Circles to ask question or to help. At times, post gets missed due to the number of posts which depletes the purpose of the group. 28 | The extension allows Developers to see every post from every Dev C group which makes the community closer and can lead to have better answers to their queries and better chances of collaborating from all around the world. 29 | 30 | 31 | ## What does it do? 32 | 33 | You put some keywords of your interest, say javascript, node.js, in the options of extension and when you run the extension on a Facebook group page (say your local Dev Circle), you get posts from various public dev circle groups which match your keywords, right into your feed. 34 | 35 | So instead of having dev circles by region, you now have dev circles by your interests! 36 | 37 | It has some pre-defined tags that help you add your keywords easily. You also have the option to blacklist certain groups, if you don't want to see posts from them (maybe due to language differences). 38 | 39 | It uses the graph API to fetch feeds from various dev circle groups. 40 | 41 | ## [Download the extension (.crx)](https://github.com/sidvishnoi/fb-dev-interest/releases/download/v1.3/Merge.Facebook.Dev.Circles.by.Interests.crx) 42 | 43 | (See How to Install and Note below) 44 | 45 | # Screenshots 46 | 47 | [![Showing posts from various public Facebook Dev Circle groups (based on keywords - node.js, react) in a Facebook Dev Circle group](https://i.imgur.com/4zV6eHj.png)](https://i.imgur.com/4zV6eHj.png) 48 | *Showing posts from various public Facebook Dev Circle groups (based on keywords - `node.js, react`) in a Facebook Dev Circle group* 49 | 50 | [![Options Page for extension, showing addition of keywords of user's interest](https://i.imgur.com/PnJvc6I.png)](https://i.imgur.com/PnJvc6I.png) 51 | *Options Page for extension, showing addition of keywords of user's interest* 52 | 53 | [![Comments on a post can also be viewed in the custom feed](https://i.imgur.com/Ttzscqy.png)](https://i.imgur.com/Ttzscqy.png) 54 | *Comments on a post can also be viewed in the custom feed* 55 | 56 | 57 | # How to Install 58 | 59 | ([Video - How to Install and configure](https://www.youtube.com/watch?v=A-LR6KWdAsM)) 60 | 61 | 1. Download the extension (`Merge.Facebook.Dev.Circles.by.Interests.crx`) from link above or from [releases page](https://github.com/sidvishnoi/fb-dev-interest/releases). 62 | 2. Drag and Drop downloaded file into Google Chrome's Extensions page (`chrome://extensions/`). 63 | 3. Install the extension by clicking Install button. 64 | 4. Go to extension's options and configure. 65 | 66 | ## Note 67 | 68 | The above mentioned method might not work due to an an update to Google Chrome 63 (See: https://bugs.chromium.org/p/chromium/issues/detail?id=794219) (Thanks to Stefanie M from Devpost for reporting) 69 | 70 | The following method will work on all versions of Chrome: 71 | 72 | 1. Downlod the extension (`fb-dev-interest-unpacked.zip`) from the [releases page](https://github.com/sidvishnoi/fb-dev-interest/releases) and Unzip the downloaded file. 73 | 2. Go to `chrome://extensions/` and click the checkbox to enable **Developer mode**. 74 | 3. Click the **Load unpacked extension** button and select the unzipped folder. 75 | 4. Install it as prompted and now you can have the extension's equivalent. Go to extension's options and configure and use it. 76 | 77 | I'll update the upcoming releases and release v1.3 to have the `fb-dev-interest-unpacked.zip` file. The .crx files won't be added to new releases until the update can be resolved. 78 | 79 | # Configure 80 | 81 | ### App Token (required) 82 | 83 | Get your app token from [https://developers.facebook.com/tools/accesstoken/](https://developers.facebook.com/tools/accesstoken/). You might need to create a Facebook Developer Account and/or a app. Create an app if not exists and get the `App Token` (not User Token) and paste in App Token field in extension's options page. 84 | 85 | Although a default app token is added to the app, you are recommended to not to use it. It is for testing purposes only and you still need to "Set" it as it is not saved in your settings by default. 86 | 87 | The App Token is needed to authenticate Facebook's Graph API requests. 88 | 89 | ### My Interests 90 | 91 | Add the keywords that interest you in this field to filter posts. Type a keyword and press Enter to add it. Click a keyword to remove it. We've added some keywords as suggestions. You can contribute by adding more keywords :) 92 | 93 | If you leave this field blank, you'll get all posts from all public dev circle groups. 94 | 95 | ### Highlight Keyword Matches 96 | 97 | Enable to highlight matched keywords in posts, otherwise disable. 98 | 99 | ### Blacklist Groups 100 | 101 | Add a group's name (and make use to auto-complete) to blacklist it (avoid showing posts from that group in your feed). You might want to blacklist a group as you might not be interested in their posts (no hate!) or not able to understand their language. 102 | 103 | # How we built it? 104 | 105 | The extension uses the **Graph API** to fetch feeds from various dev circle groups. The posts shown in feed have same CSS classes as of regular posts in feed. We did this to make the feed look as natural as possible. 106 | 107 | The options page is written using the **React** framework. Used react components can be seen in GitHub repo. 108 | 109 | ## Challenges we ran into 110 | 111 | 1. Never built a Chrome extension before. 112 | 2. Never used Graph API, React before. 113 | 3. Finding the correct classes and minimal html for rendering posts in feed was quite a challenge. 114 | 4. @sidvishnoi (the coder) was sick during development process. 115 | 5. The other teammate (the one with original idea) was busy with out of station work. 116 | 117 | # Proud moments 118 | 119 | Making the posts and comments look similar to the ones shown in felt pretty cool. It seemed like I have finally built something. 120 | 121 | @sidvishnoi learnt react and webpack in one day and made the options page (not a lot, but enough to make it work). 122 | 123 | Creating a Chrome extension was also a fun thing to have done. 124 | 125 | # Contributing 126 | 127 | I would love your PRs. You can contribute : 128 | 129 | - by adding more keyword suggestions (edit: [/options.dev/keywords.js](/options.dev/keywords.js)). 130 | - letting me know of additions/removals of public Dev circle groups (edit: [/options.dev/groups.js](/options.dev/groups.js), you may use [/scrape-group-lists.js](/scrape-group-lists.js)). 131 | - Make the extension better. Fix bugs, if found. Add more features. Improve existing features. 132 | 133 | ## Instructions for Developers 134 | 135 | To rebuild the extension: 136 | ``` bash 137 | $ git clone https://github.com/sidvishnoi/fb-dev-interest.git 138 | $ cd fb-dev-interest 139 | $ npm install 140 | $ npm run build # builds the dist folder only, run after each edit 141 | ``` 142 | 143 | You can load the dist folder in Chrome from the chrome://extensions/ page in Developer Mode to install the extension. 144 | 145 | To create the extension's CRX file, use the Google Chrome Browser: https://developer.chrome.com/extensions/packaging. Use the `dist` folder to create package. 146 | The CRX file can be installed by a simple drag and drop in chrome://extensions/ page. 147 | 148 | Rename the .crx file from `dist.crx` to `Merge Facebook Dev Circles by Interests.crx` and `dist.pem` to `key.pem` to create a release. 149 | 150 | ## Todo 151 | 152 | - More interactive feed. 153 | - Get App Token through login SDK. 154 | - Track keywords with Facebook Analytics SDK. 155 | - Custom groups and user groups 156 | - Firefox add-on maybe? 157 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(function() { 2 | chrome.declarativeContent.onPageChanged.removeRules(undefined, function() { 3 | chrome.declarativeContent.onPageChanged.addRules([{ 4 | conditions: [ 5 | new chrome.declarativeContent.PageStateMatcher({ 6 | pageUrl: { urlContains: 'localhost' }, 7 | }), 8 | new chrome.declarativeContent.PageStateMatcher({ 9 | pageUrl: { urlContains: 'https://www.facebook.com/groups/' }, 10 | }) 11 | ], 12 | actions: [new chrome.declarativeContent.ShowPageAction()] 13 | }]); 14 | }); 15 | }); 16 | 17 | chrome.pageAction.onClicked.addListener((tab) => { 18 | chrome.tabs.executeScript(tab.id, 19 | {code: `document.body.style.background = '#e9ebee';`}, 20 | function() { 21 | chrome.tabs.executeScript(tab.id, { file: 'contentscript.js' }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /contentscript.js: -------------------------------------------------------------------------------- 1 | function asyncParallel(tasks, cb) { 2 | var results, pending, keys 3 | var isSync = true 4 | 5 | if (Array.isArray(tasks)) { 6 | results = [] 7 | pending = tasks.length 8 | } else { 9 | keys = Object.keys(tasks) 10 | results = {} 11 | pending = keys.length 12 | } 13 | 14 | function done(err) { 15 | function end() { 16 | if (cb) cb(err, results) 17 | cb = null 18 | } 19 | if (isSync) process.nextTick(end) 20 | else end() 21 | } 22 | 23 | function each(i, err, result) { 24 | results[i] = result 25 | if (--pending === 0 || err) { 26 | done(err) 27 | } 28 | } 29 | 30 | if (!pending) { 31 | // empty 32 | done(null) 33 | } else if (keys) { 34 | // object 35 | keys.forEach(function(key) { 36 | tasks[key](function(err, result) { each(key, err, result) }) 37 | }) 38 | } else { 39 | // array 40 | tasks.forEach(function(task, i) { 41 | task(function(err, result) { each(i, err, result) }) 42 | }) 43 | } 44 | 45 | isSync = false 46 | } 47 | 48 | // get access token from saved extension settings 49 | function getAccessToken(callback) { 50 | chrome.storage.sync.get('apikey', (res) => { 51 | if (!res.apikey) { 52 | return callback('API key not set. Please set the App Token in extension\'s settings'); 53 | }; 54 | callback(null, res.apikey); 55 | }); 56 | } 57 | 58 | function getBlacklistedGroups(callback) { 59 | chrome.storage.sync.get('groups', (res) => { 60 | callback(null, res.groups || []); 61 | }); 62 | } 63 | 64 | function getInterestKeywords(callback) { 65 | chrome.storage.sync.get('keywords', (res) => callback(null, res.keywords || [])); 66 | } 67 | 68 | function getHighlightMatches(callback) { 69 | chrome.storage.sync.get('highlightMatches', (res) => callback(null, typeof res.highlightMatches === 'undefined' ? true : res.highlightMatches)); 70 | } 71 | 72 | // inject scripts to page: https://stackoverflow.com/a/9517879 73 | function injectScriptFile(fname, callback) { 74 | const s = document.createElement('script'); 75 | s.src = chrome.extension.getURL(fname); 76 | s.onload = function() { 77 | this.remove(); 78 | callback(); 79 | }; 80 | (document.head||document.documentElement).appendChild(s); 81 | } 82 | 83 | function injectScriptText(str) { 84 | var script = document.createElement('script'); 85 | script.textContent = str; 86 | (document.head||document.documentElement).appendChild(script); 87 | script.remove(); 88 | } 89 | 90 | asyncParallel({ 91 | getAccessToken, 92 | getBlacklistedGroups, 93 | getInterestKeywords, 94 | getHighlightMatches, 95 | }, (err, results) => { 96 | if (err) return alert(err.message || err); 97 | 98 | const keywords = [...results.getInterestKeywords].map(k => k.name); 99 | injectScriptText(`var fbDevInterest = { 100 | _apiKey: '${results.getAccessToken}', 101 | _blacklist: [${results.getBlacklistedGroups}], 102 | _keywords: new Set([${keywords.map(v => `'${v}'`)}]), 103 | _highlightMatches: ${results.getHighlightMatches}, 104 | }`); 105 | 106 | asyncParallel({ 107 | utils: cb => injectScriptFile('utils.js', cb), 108 | groups: cb => injectScriptFile('groups.js', cb), 109 | inject: cb => injectScriptFile('inject.js', cb), 110 | analytics: cb => injectScriptFile('options/analytics.js', cb), 111 | }, (err, res) => { 112 | injectScriptText(` 113 | console.log('<<< fbDevInterest >>>'); 114 | fbDevInterest.parent.innerHTML = ''; 115 | fbDevInterest.init(); 116 | `); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /dist/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(function(){chrome.declarativeContent.onPageChanged.removeRules(void 0,function(){chrome.declarativeContent.onPageChanged.addRules([{conditions:[new chrome.declarativeContent.PageStateMatcher({pageUrl:{urlContains:"localhost"}}),new chrome.declarativeContent.PageStateMatcher({pageUrl:{urlContains:"https://www.facebook.com/groups/"}})],actions:[new chrome.declarativeContent.ShowPageAction]}])})}),chrome.pageAction.onClicked.addListener(e=>{chrome.tabs.executeScript(e.id,{code:"document.body.style.background = '#e9ebee';"},function(){chrome.tabs.executeScript(e.id,{file:"contentscript.js"})})}); -------------------------------------------------------------------------------- /dist/contentscript.js: -------------------------------------------------------------------------------- 1 | function asyncParallel(e,t){function n(e){function n(){t&&t(e,i),t=null}r?process.nextTick(n):n()}function c(e,t,c){i[e]=c,(0==--s||t)&&n(t)}var i,s,o,r=!0;Array.isArray(e)?(i=[],s=e.length):(o=Object.keys(e),i={},s=o.length),s?o?o.forEach(function(t){e[t](function(e,n){c(t,e,n)})}):e.forEach(function(e,t){e(function(e,n){c(t,e,n)})}):n(null),r=!1}function getAccessToken(e){chrome.storage.sync.get("apikey",t=>{if(!t.apikey)return e("API key not set. Please set the App Token in extension's settings");e(null,t.apikey)})}function getBlacklistedGroups(e){chrome.storage.sync.get("groups",t=>{e(null,t.groups||[])})}function getInterestKeywords(e){chrome.storage.sync.get("keywords",t=>e(null,t.keywords||[]))}function getHighlightMatches(e){chrome.storage.sync.get("highlightMatches",t=>e(null,void 0===t.highlightMatches||t.highlightMatches))}function injectScriptFile(e,t){const n=document.createElement("script");n.src=chrome.extension.getURL(e),n.onload=function(){this.remove(),t()},(document.head||document.documentElement).appendChild(n)}function injectScriptText(e){var t=document.createElement("script");t.textContent=e,(document.head||document.documentElement).appendChild(t),t.remove()}asyncParallel({getAccessToken:getAccessToken,getBlacklistedGroups:getBlacklistedGroups,getInterestKeywords:getInterestKeywords,getHighlightMatches:getHighlightMatches},(e,t)=>{if(e)return alert(e.message||e);const n=[...t.getInterestKeywords].map(e=>e.name);injectScriptText(`var fbDevInterest = {\n _apiKey: '${t.getAccessToken}',\n _blacklist: [${t.getBlacklistedGroups}],\n _keywords: new Set([${n.map(e=>`'${e}'`)}]),\n _highlightMatches: ${t.getHighlightMatches},\n }`),asyncParallel({utils:e=>injectScriptFile("utils.js",e),groups:e=>injectScriptFile("groups.js",e),inject:e=>injectScriptFile("inject.js",e),analytics:e=>injectScriptFile("options/analytics.js",e)},(e,t)=>{injectScriptText("\n console.log('<<< fbDevInterest >>>');\n fbDevInterest.parent.innerHTML = '';\n fbDevInterest.init();\n ")})}); -------------------------------------------------------------------------------- /dist/groups.js: -------------------------------------------------------------------------------- 1 | fbDevInterest.ALL_GROUPS = [ 2 | 1041205739348709, 3 | 1071045349642536, 4 | 1074858042611323, 5 | 1075017422642967, 6 | 111858152705945, 7 | 1148469218498930, 8 | 1152576018114322, 9 | 125327974795168, 10 | 1258355007573190, 11 | 132580147377707, 12 | 1378294582253698, 13 | 1443394385967980, 14 | 1494181493938081, 15 | 152127978670639, 16 | 1607133026028061, 17 | 160941794378470, 18 | 1724152667880378, 19 | 1741843536047014, 20 | 1780072415645281, 21 | 1806620552895262, 22 | 1841081392797911, 23 | 186924858495604, 24 | 187217085094857, 25 | 1903916609822504, 26 | 1920036621597031, 27 | 1922538421363451, 28 | 1924443867832338, 29 | 199036970608482, 30 | 2224932161064321, 31 | 223094988221674, 32 | 249598592040574, 33 | 265793323822652, 34 | 293458267749614, 35 | 304477986647756, 36 | 309450039518404, 37 | 313087542449350, 38 | 332006040559709, 39 | 348458995586076, 40 | 362906487478469, 41 | 402137910152010, 42 | 428973767504677, 43 | 476463749198108, 44 | 485698195138488, 45 | 638854212931776, 46 | 786453984830109, 47 | 793016410839401, 48 | 811281355669013, 49 | 813879575430133, 50 | 826341790867138, 51 | 854314664699156, 52 | 885490321621308, 53 | 886251554842166, 54 | 893652180764182 55 | ]; -------------------------------------------------------------------------------- /dist/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidvishnoi/fb-dev-interest/HEAD/dist/icon128.png -------------------------------------------------------------------------------- /dist/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidvishnoi/fb-dev-interest/HEAD/dist/icon16.png -------------------------------------------------------------------------------- /dist/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidvishnoi/fb-dev-interest/HEAD/dist/icon32.png -------------------------------------------------------------------------------- /dist/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidvishnoi/fb-dev-interest/HEAD/dist/icon48.png -------------------------------------------------------------------------------- /dist/inject.js: -------------------------------------------------------------------------------- 1 | fbDevInterest.createElement=function(e,t){const n=document.createElement(e);if(t.classList&&n.classList.add(...t.classList.split(" ")),t.attrs)for(const e in t.attrs)n.setAttribute(e,t.attrs[e]);if(t.innerHTML&&(n.innerHTML=t.innerHTML),t.events)for(const e in t.events)n.addEventListener(e,t.events[e]);return t.parent&&t.parent.appendChild(n),n},fbDevInterest.clearLocalStorage=function(){const e=Object.keys(localStorage);let t=0;for(const n of e)if(n.startsWith("https://graph.facebook.com/v2.11/")&&(localStorage.removeItem(n),++t),10===t)return},fbDevInterest.BASE_API_URL=`https://graph.facebook.com/v2.11/GROUPID/?&access_token=${fbDevInterest._apiKey}&fields=name,id,feed{message,id,name,from,permalink_url,full_picture,link,created_time}`,fbDevInterest.COMMENTS_API_URL=`https://graph.facebook.com/v2.11/POSTID/?&access_token=${fbDevInterest._apiKey}&fields=comments{from,permalink_url,message,created_time,comments{from,permalink_url,message,created_time}},permalink_url`,fbDevInterest.clearBlacklist=function(){for(const e of this._blacklist){const t=this.ALL_GROUPS.indexOf(parseInt(e,10));t>-1&&this.ALL_GROUPS.splice(t,1)}try{let e=document.querySelector("meta[property='al:ios:url']").content.match(/id=(\d*)/)[1];e=parseInt(e,10);const t=this.ALL_GROUPS.indexOf(e);t>-1&&this.ALL_GROUPS.splice(t,1),this.ALL_GROUPS.unshift(e)}catch(e){}},fbDevInterest.createPlaceholder=function(){return this.createElement("div",{classList:"_3-u2 mbm _2iwp _4-u8",innerHTML:'
',parent:this.parent})},fbDevInterest.getGroupId={},fbDevInterest.getGroupId=function(){const e=this;let t=-1;return function*(){for(;;)yield e.ALL_GROUPS[++t%e.ALL_GROUPS.length]}()},fbDevInterest.parent=document.querySelector("#pagelet_group_mall"),fbDevInterest.state={},fbDevInterest.requestList=new Set,fbDevInterest.findMatchedKeywords=function(e){if(0===this._keywords.size)return[[0,0]];const t=e.toLowerCase(),n=[];for(const e of this._keywords){const s=new RegExp(`(?:^|\\b)(${e.replace(/\s\s+/g,"\\s*").replace(/\./g,"\\.?")})(?=\\b|$)`),a=t.match(s);a&&n.push([a.index,a[0].length])}return n.sort(function(e,t){return e[0]t[0]?1:e[1]t[1]?-1:0})},fbDevInterest.highlightMatches=function(e,t){let n,s="",a=0;const r=new Set;for(let i=0;i${e.substring(o,o+c)}`,a=0)}const i=t[t.length-1];return s+=e.substring(i[0]+i[1])},fbDevInterest.showComments=function(e,t){const n=this,s=function(e){const t=new Date(e.created_time);return`\n
\n
\n
\n
\n
\n
\n
\n
\n \n \n ${e.from.name}\n \n ${e.message.linkify()}\n \n \n
\n
\n
\n
\n
\n
\n
\n `},a=function(e,t){return n.createElement("div",{classList:"UFIRow _48ph _48pi UFIComment _4oep",parent:t,innerHTML:s(e),attrs:{style:"border-top: none"}})},r=function(e,t){return n.createElement("div",{classList:"UFIRow _48ph _48pi _4204 UFIComment _4oep",parent:t,innerHTML:s(e),attrs:{style:"border-left-width: 2px"}})},i=document.getElementById(`comment_trigger_${e.id}`);if(i.innerHTML="View all comments",e.comments)for(const s of e.comments.data){a(s,t);if(!s.comments)continue;const e=n.createElement("div",{classList:"UFIReplyList",parent:t});for(const t of s.comments.data){r(t,e)}}else i.innerHTML="No comments yet."},fbDevInterest.getComments=function(e,t){const n=this,s=function(t,s){const a=`\n
\n \n
\n `,r=n.createElement("div",{innerHTML:a,parent:n.createElement("div",{classList:"_3b-9 _j6a",parent:n.createElement("div",{classList:"UFIList",innerHTML:'
Comments
',parent:n.createElement("div",{classList:"uiUfi UFIContainer _5pc9 _5vsj _5v9k",parent:n.createElement("form",{classList:"commentable_item",parent:s})})})})});return document.getElementById(`comment_trigger_${e}`).innerHTML='View all comments ',r}(e,t),a=n.COMMENTS_API_URL.replace("POSTID",e);let r=localStorage.getItem(a);if(r){r=JSON.parse(r);if((new Date-new Date(r.last_fetch_time))/6e4<10)return handleJsonResponse(r)}n.requestList.add(a),fetch(a).then(e=>e.json()).then(e=>{e.last_fetch_time=new Date;try{localStorage.setItem(a,JSON.stringify(e))}catch(e){n.clearLocalStorage()}n.requestList.delete(a),n.showComments(e,s)}).catch(t=>{console.error(t),n.requestList.delete(a),document.getElementById(`comment_trigger_${e}`).innerHTML="Some error occured. Try again?"})},fbDevInterest.showPostButtons=function(e,t){function n(){s.getComments(e.id,t),this.removeEventListener("click",n)}const s=this,a=s.createElement("div",{classList:"_sa_ _gsd _fgm _5vsi _192z",parent:t}),r=s.createElement("div",{classList:"_42nr",parent:s.createElement("div",{classList:"_524d",parent:s.createElement("div",{classList:"_3399 _a7s _20h6 _610i _125r clearfix _zw3",parent:s.createElement("div",{classList:"_57w",parent:s.createElement("div",{parent:s.createElement("div",{classList:"_37uu",parent:a})})})})})});s.createElement("a",{classList:"UFILikeLink _4x9- _4x9_ _48-k",innerHTML:"Like",attrs:{href:e.permalink_url,target:"_blank"},parent:s.createElement("div",{classList:"_khz _4sz1",parent:s.createElement("span",{classList:"_1mto",parent:r})})}),s.createElement("a",{classList:"comment_link _5yxe",innerHTML:"Comment",attrs:{href:"#",role:"button"},events:{click:n},parent:s.createElement("div",{classList:"_6a _15-7 _3h-u",parent:s.createElement("span",{classList:"_1mto",parent:r})})}),s.createElement("a",{classList:"share_action_link _5f9b",innerHTML:"Share",attrs:{href:e.permalink_url,target:"_blank"},parent:s.createElement("div",{classList:"_27de",parent:s.createElement("span",{classList:"_1mto",parent:r})})});return a},fbDevInterest.showPost=function(e){const t=document.querySelector("._3-u2.mbm._2iwp._4-u8");t?this.parent.replaceChild(e,t):this.parent.appendChild(e)},fbDevInterest.splitPostBody=function(e){const t=e.split("

");if(t.length<6)return e;const n=t.slice(0,6).join("

"),s=t.slice(6).join("

"),a=Math.random().toString().replace(".","");return`
${n}...
${s}
See more
`},fbDevInterest.createPost=function(e,t){const n=this;if(!e.message)return;const s=n.findMatchedKeywords(e.message);if(0===s.length)return;const a=n.createElement("div",{classList:"_4-u2 mbm _4mrt _5jmm _5pat _5v3q _4-u8",innerHTML:function(e,t){let a=e.message;n._highlightMatches&&(a=n.highlightMatches(a,s)),a=a.replace(/\n/g,"

").linkify(),e.link&&e.full_picture&&e.full_picture.includes("//external.")&&(a=`${a}

[LINK: ${e.link}]`),a=n.splitPostBody(a);const r=new Date(e.created_time),i=e.full_picture&&!e.full_picture.includes("//external.")?`

`:"";return`\n
\n
\n
\n
\n
\n ${e.from.name}\n ‎ ▶\n ${t.name}\n
\n \n
\n

${a}

\n ${i}\n
\n
\n
\n
\n
\n `}(e,t),attrs:{id:`mall_post_${e.id}:6:0`}});n.showPost(a),n.showPostButtons(e,a)},fbDevInterest.getGroupFeed=function(e){function t(t){if(t.error)throw t.error;if(!t.feed){if(!Array.isArray(t.data))throw new Error(`failed to fetch: ${s}`);t.feed={data:t.data,paging:t.paging}}n.state[e.groupId]=n.state[e.groupId]||{name:t.name};try{n.state[e.groupId].nextPageUrl=t.feed.paging.next}catch(t){const s=n.ALL_GROUPS.indexOf(e.groupId);n.ALL_GROUPS.splice(s,1),n.state[e.groupId].nextPageUrl="False"}for(const s of t.feed.data)try{n.createPost(s,{name:n.state[e.groupId].name,id:e.groupId})}catch(e){console.error(e)}n.getPostsOnScroll()}const n=this;let s=n.BASE_API_URL.replace("GROUPID",e.groupId);const a=n.state[e.groupId];if(a&&a.nextPageUrl&&(s=a.nextPageUrl),"False"===s)return;let r=localStorage.getItem(s);if(r){r=JSON.parse(r);if((new Date-new Date(r.last_fetch_time))/6e4<10)return t(r)}n.requestList.add(s),fetch(s).then(e=>e.json()).then(e=>{e.last_fetch_time=new Date;try{localStorage.setItem(s,JSON.stringify(e))}catch(e){n.clearLocalStorage()}n.requestList.delete(s),t(e)}).catch(e=>{n.requestList.delete(s),function(e){console.error(e);const t=n.createElement("div",{classList:"_4-u2 mbm _4mrt _5jmm _5pat _5v3q _4-u8",innerHTML:`
${JSON.stringify(e,null,2)}
`,attrs:{style:"color: red;"}});n.showPost(t)}(e)})},fbDevInterest.getPostsOnScroll=function(){const e=this,t=document.querySelectorAll("._4mrt._5jmm._5pat._5v3q._4-u8"),n=t[t.length-1],s=function(t){const a=e.getGroupId.next().value;if(!a)return fbDevInterest.nomore=!0,console.log("No more posts in criteria"),void window.removeEventListener("scroll",s,!0);if(!(e.requestList.size>10)){if(!n)return e.getGroupFeed({groupId:a});if(isScrolledIntoView(n)){if(window.removeEventListener("scroll",s,!0),document.querySelectorAll("._3-u2.mbm._2iwp._4-u8").length<3)for(let t=0;t<5;++t)e.createPlaceholder();e.getGroupFeed({groupId:a})}}};fbDevInterest.nomore||window.addEventListener("scroll",s,!0)},fbDevInterest.clearPosts=function(){const e=e=>Array.from(e).forEach(e=>e.remove());e(document.querySelectorAll("._4mrt._5jmm._5pat._5v3q._4-u8")),e(document.querySelectorAll("._5umn")),e(document.querySelectorAll("._4wcq")),window.addEventListener("scroll",e=>e.stopPropagation(),!0)},fbDevInterest.init=function(){this.clearBlacklist(),this.clearPosts(),this.getGroupId=this.getGroupId();for(let e=0;e<5;++e)this.createPlaceholder();scrollToItem(this.parent),this.getGroupFeed({groupId:this.getGroupId.next().value})}; -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Merge Facebook Dev Circles by Interests", 4 | "description": "A Chrome extension that filters through Facebook Dev Circles around the world to get only the interesting posts in your feed.", 5 | "version": "1.3", 6 | "icons": 7 | { 8 | "16": "icon16.png", 9 | "32": "icon32.png", 10 | "48": "icon48.png", 11 | "128": "icon128.png" 12 | }, 13 | "page_action": 14 | { 15 | "default_title": "Merge Facebook Dev Circles by Interests", 16 | "default_icon": "icon32.png" 17 | }, 18 | "background": 19 | { 20 | "scripts": ["background.js"], 21 | "persistent": false 22 | }, 23 | "options_page": "options/index.html", 24 | "web_accessible_resources": [ 25 | "utils.js", 26 | "groups.js", 27 | "options/analytics.js", 28 | "inject.js" 29 | ], 30 | "permissions": [ 31 | "declarativeContent", 32 | "activeTab", 33 | "storage" 34 | ], 35 | "content_security_policy": "script-src 'self' https://connect.facebook.net/en_US/sdk.js; object-src 'self'" 36 | } 37 | -------------------------------------------------------------------------------- /dist/options/analytics.js: -------------------------------------------------------------------------------- 1 | window.fbAsyncInit = function() { 2 | FB.init({ 3 | appId: '148818419074509', 4 | xfbml: true, 5 | version: 'v2.11' 6 | }); 7 | 8 | if (window.location.href.includes('facebook.com')) { 9 | FB.AppEvents.logEvent('UsedExtension'); 10 | } 11 | }; 12 | 13 | (function(d, s, id) { 14 | var js, fjs = d.getElementsByTagName(s)[0]; 15 | if (d.getElementById(id)) { return; } 16 | js = d.createElement(s); 17 | js.id = id; 18 | js.src = "https://connect.facebook.net/en_US/sdk.js"; 19 | fjs.parentNode.insertBefore(js, fjs); 20 | }(document, 'script', 'facebook-jssdk')); 21 | -------------------------------------------------------------------------------- /dist/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Merge Facebook Dev Circles by Interests | Options 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /dist/options/options.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0],{18:function(e,n){function t(e,n){var t=e[1]||"",o=e[3];if(!o)return t;if(n&&"function"==typeof btoa){var a=r(o);return[t].concat(o.sources.map(function(e){return"/*# sourceURL="+o.sourceRoot+e+" */"})).concat([a]).join("\n")}return[t].join("\n")}function r(e){return"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(e))))+" */"}e.exports=function(e){var n=[];return n.toString=function(){return this.map(function(n){var r=t(n,e);return n[2]?"@media "+n[2]+"{"+r+"}":r}).join("")},n.i=function(e,t){"string"==typeof e&&(e=[[null,e,""]]);for(var r={},o=0;o=0&&y.splice(n,1)}function s(e){var n=document.createElement("style");return e.attrs.type="text/css",l(n,e.attrs),a(e,n),n}function c(e){var n=document.createElement("link");return e.attrs.type="text/css",e.attrs.rel="stylesheet",l(n,e.attrs),a(e,n),n}function l(e,n){Object.keys(n).forEach(function(t){e.setAttribute(t,n[t])})}function u(e,n){var t,r,o,a;if(n.transform&&e.css){if(!(a=n.transform(e.css)))return function(){};e.css=a}if(n.singleton){var l=v++;t=m||(m=s(n)),r=d.bind(null,t,l,!1),o=d.bind(null,t,l,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(t=c(n),r=f.bind(null,t,n),o=function(){i(t),t.href&&URL.revokeObjectURL(t.href)}):(t=s(n),r=p.bind(null,t),o=function(){i(t)});return r(e),function(n){if(n){if(n.css===e.css&&n.media===e.media&&n.sourceMap===e.sourceMap)return;r(e=n)}else o()}}function d(e,n,t,r){var o=t?"":r.css;if(e.styleSheet)e.styleSheet.cssText=x(n,o);else{var a=document.createTextNode(o),i=e.childNodes;i[n]&&e.removeChild(i[n]),i.length?e.insertBefore(a,i[n]):e.appendChild(a)}}function p(e,n){var t=n.css,r=n.media;if(r&&e.setAttribute("media",r),e.styleSheet)e.styleSheet.cssText=t;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(t))}}function f(e,n,t){var r=t.css,o=t.sourceMap,a=void 0===n.convertToAbsoluteUrls&&o;(n.convertToAbsoluteUrls||a)&&(r=k(r)),o&&(r+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+" */");var i=new Blob([r],{type:"text/css"}),s=e.href;e.href=URL.createObjectURL(i),s&&URL.revokeObjectURL(s)}var h={},b=function(e){var n;return function(){return void 0===n&&(n=e.apply(this,arguments)),n}}(function(){return window&&document&&document.all&&!window.atob}),g=function(e){var n={};return function(t){if(void 0===n[t]){var r=e.call(this,t);if(r instanceof window.HTMLIFrameElement)try{r=r.contentDocument.head}catch(e){r=null}n[t]=r}return n[t]}}(function(e){return document.querySelector(e)}),m=null,v=0,y=[],k=t(45);e.exports=function(e,n){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");n=n||{},n.attrs="object"==typeof n.attrs?n.attrs:{},n.singleton||(n.singleton=b()),n.insertInto||(n.insertInto="head"),n.insertAt||(n.insertAt="bottom");var t=o(e,n);return r(t,n),function(e){for(var a=[],i=0;i\n *
\n * \n *
\n *