├── .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 | [](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 | [](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 | [](https://i.imgur.com/PnJvc6I.png)
51 | *Options Page for extension, showing addition of keywords of user's interest*
52 |
53 | [](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 `},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 `,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``},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 `}(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 * \n *
\n * \n */\n.react-tags {\n position: relative;\n padding: 6px 0 0 6px;\n border: 1px solid #eee;\n border-radius: 1px;\n\n /* shared font styles */\n font-size: 1em;\n line-height: 1.2;\n\n /* clicking anywhere will focus the input */\n cursor: text;\n}\n\n.react-tags.is-focused {\n border-color: #ddd;\n}\n\n.react-tags__selected {\n display: inline;\n}\n\n.react-tags__selected-tag {\n display: inline-block;\n box-sizing: border-box;\n margin: 0 6px 6px 0 !important;\n padding: 6px 8px;\n background: #ecf0f7;\n\n /* match the font styles */\n font-size: 15px !important;\n line-height: inherit;\n font-weight: normal !important;\n}\n\n.react-tags__selected-tag:after {\n content: \'\\2715\';\n color: #AAA;\n margin-left: 8px;\n}\n\n.react-tags__search {\n display: inline-block;\n\n /* match tag layout */\n padding: 7px 2px;\n margin-bottom: 6px;\n\n /* prevent autoresize overflowing the container */\n max-width: 100%;\n}\n\n@media screen and (min-width: 30em) {\n\n .react-tags__search {\n /* this will become the offsetParent for suggestions */\n position: relative;\n }\n\n}\n\n.react-tags__search input {\n /* prevent autoresize overflowing the container */\n max-width: 100%;\n\n /* remove styles and layout from this element */\n margin: 0;\n padding: 0;\n border: 0;\n outline: none;\n\n /* match the font styles */\n font-size: inherit;\n line-height: inherit;\n}\n\n.react-tags__search input::-ms-clear {\n display: none;\n}\n\n.react-tags__suggestions {\n position: absolute;\n top: 100%;\n left: 0;\n width: 100%;\n z-index: 999999;\n}\n\n@media screen and (min-width: 30em) {\n\n .react-tags__suggestions {\n width: 240px;\n }\n\n}\n\n.react-tags__suggestions ul {\n margin: 4px -1px;\n padding: 0;\n list-style: none;\n background: white;\n border: 1px solid #ccc;\n border-color: rgba(0, 0, 0, .15);\n border-radius: 2px;\n box-shadow: 0 4px 6px 2px rgba(0, 0, 0, .10);\n}\n\n.react-tags__suggestions li {\n /*border-bottom: 1px solid #ddd;*/\n padding: 6px 8px;\n}\n\n.react-tags__suggestions li mark {\n /*text-decoration: underline;*/\n background: none;\n font-weight: 600;\n}\n\n.react-tags__suggestions li:hover {\n cursor: pointer;\n background: #eee;\n}\n\n.react-tags__suggestions li.is-active {\n background: #ecf0f7;\n}\n\n.react-tags__suggestions li.is-disabled {\n opacity: 0.5;\n cursor: auto;\n}\n',""])},45:function(e,n){e.exports=function(e){var n="undefined"!=typeof window&&window.location;if(!n)throw new Error("fixUrls requires window.location");if(!e||"string"!=typeof e)return e;var t=n.protocol+"//"+n.host,r=t+n.pathname.replace(/\/[^\/]*$/,"/");return e.replace(/url\s*\(((?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)\)/gi,function(e,n){var o=n.trim().replace(/^"(.*)"$/,function(e,n){return n}).replace(/^'(.*)'$/,function(e,n){return n});if(/^(#|data:|http:\/\/|https:\/\/|file:\/\/\/)/i.test(o))return e;var a;return a=0===o.indexOf("//")?o:0===o.indexOf("/")?t+o:r+o.replace(/^\.\//,""),"url("+JSON.stringify(a)+")"})}},46:function(e,n,t){var r=t(47);"string"==typeof r&&(r=[[e.i,r,""]]);var o={hmr:!0};o.transform=void 0;t(19)(r,o);r.locals&&(e.exports=r.locals)},47:function(e,n,t){n=e.exports=t(18)(void 0),n.push([e.i,"* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\na {\n text-decoration: none;\n}\n\nbody {\n display: flex;\n background: #4267b2;\n color: #1d2129;\n padding: 1vh 2vw;\n margin: 1em auto;\n font-family: Helvetica, Arial, sans-serif;\n font-size: 16px;\n min-height: 96vh;\n}\n\n#root {\n background: #fff;\n /*padding: 1em;*/\n width: 100%;\n box-shadow: 1px 1px 3px 0px #333;\n flex: 1;\n border-radius: 6px;\n}\n\n.page-title {\n font-size: 2em;\n padding: 1em 0 0.3em;\n /*margin-bottom: 1em;*/\n background: #264075;\n color: #fff;\n padding: 1em;\n /*border-bottom: 4px solid #4267b2;*/\n}\n\n#root > div > form, #root > div > div {\n padding: 1em;\n background: #fff;\n}\n\ninput {\n max-width: 100%;\n border: 0;\n outline: none;\n font-size: inherit;\n line-height: inherit;\n}\n\na.hint {\n background: #365899;\n color: #fff;\n padding: 0 4px;\n border-radius: 4px;\n}\na.hint:hover {\n background: #4267b2;\n}\n\nh3 {\n color: #444;\n margin: 3px 0;\n}\n\n.apikey_form .form-group {\n display: flex;\n width: 100%;\n align-items: stretch;\n justify-content: space-between;\n}\n\n.apikey_form input {\n position: relative;\n padding: 7px 2px;\n flex: 1;\n cursor: text;\n font-size: 12px;\n font-family: monospace;\n border: 1px solid #eee;\n border-radius: 2px;\n color: #555;\n}\n.apikey_form input:focus {\n border-color: #ddd;\n}\n\n.apikey_form button, .react-tags__selected-tag {\n background-color: #f6f7f9;\n color: #4b4f56;\n border: none;\n line-height: 26px;\n padding: 0 10px;\n transition: 200ms cubic-bezier(.08,.52,.52,1) background-color, 200ms cubic-bezier(.08,.52,.52,1) box-shadow, 200ms cubic-bezier(.08,.52,.52,1) transform;\n border: 1px solid #ced0d4;\n border-radius: 2px;\n box-sizing: content-box;\n font-size: 12px;\n margin: 0 1px;\n -webkit-font-smoothing: antialiased;\n font-weight: bold;\n position: relative;\n}\n.apikey_form button:hover, .react-tags__selected-tag:hover{\n background: #e9ebee;\n cursor: pointer;\n}\n.apikey_form button:focus, .apikey_form button:active, .react-tags__selected-tag:focus, .react-tags__selected-tag:active{\n box-shadow: 0 0 1px 2px rgba(88, 144, 255, .75), 0 1px 1px rgba(0, 0, 0, .15);\n outline: none;\n}\n",""])}},[20]);
--------------------------------------------------------------------------------
/dist/utils.js:
--------------------------------------------------------------------------------
1 | function scrollToItem(t,n=1e3){var r,i=window.pageYOffset,e=(t=>window.pageYOffset+t.getBoundingClientRect().top)(t),u=(document.body.scrollHeight-et<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1)(o),window.scrollTo(0,i+u*o),s=0}String.prototype.linkify=function(){return this.replace(/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim,'$& ').replace(/(^|[^\/])(www\.[\S]+(\b|$))/gim,'$1$2 ').replace(/\w+@[a-zA-Z_]+?(?:\.[a-zA-Z]{2,6})+/gim,'$& ').replace(/(^|\s)#(\w+)/g,'$1#$2 ')},function(){"use strict";var t=!1,n="Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),r="Sun Mon Tue Wed Thu Fri Sat".split(" "),i="AM PM".split(" "),e='MMMM d""x "at" h:mm tt',u="January February March April May June July August September October November December".split(" "),s="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),o=function(){var o=/([dfFhmHMstxyz]+)(?=([^"']*["'][^"']*["'])*[^"']*$)/g,d=function(n){return"get"+(t?"UTC":"")+n},y={d:function(){return this[d("Date")]()},dd:function(){return h(this[d("Date")]())},ddd:function(){return a.call(this,!0)},dddd:function(){return a.call(this)},f:function(){return h(h(this[d("Milliseconds")](),3),6,!0).substr(0,1)},ff:function(){return h(h(this[d("Milliseconds")](),3),6,!0).substr(0,2)},fff:function(){return h(h(this[d("Milliseconds")](),3),6,!0).substr(0,3)},ffff:function(){return h(h(this[d("Milliseconds")](),3),6,!0).substr(0,4)},fffff:function(){return h(h(this[d("Milliseconds")](),3),6,!0).substr(0,5)},ffffff:function(){return h(h(this[d("Milliseconds")](),3),6,!0).substr(0,6)},F:function(){var t=h(this[d("Milliseconds")](),3).substr(0,1);return"0"===t?"":t},FF:function(){var t=h(this[d("Milliseconds")](),3).substr(0,2);return"00"===t?"":t},FFF:function(){return h(this[d("Milliseconds")](),3).substr(0,3)},FFFF:function(){return y.FFF.call(this)},FFFFF:function(){return y.FFF.call(this)},FFFFFF:function(){return y.FFF.call(this)},h:function(){var t=this[d("Hours")]();return t>12?t-12:t},hh:function(){var t=this[d("Hours")]();return h(t>12?t-12:t)},m:function(){return this[d("Minutes")]()},mm:function(){return h(this[d("Minutes")]())},H:function(){return this[d("Hours")]()},HH:function(){return h(this[d("Hours")]())},M:function(){return this[d("Month")]()+1},MM:function(){return h(this[d("Month")]()+1)},MMM:function(){return c.call(this,!0)},MMMM:function(){return c.call(this)},s:function(){return this[d("Seconds")]()},ss:function(){return h(this[d("Seconds")]())},tt:function(){return l.call(this)},x:function(){return f.call(this)},y:function(){var t=this[d("FullYear")](),n=t.toString();return t<10?t:n.substr(n.length-2)},yy:function(){return h(this[d("FullYear")](),2)},yyy:function(){var t=this[d("FullYear")](),n=t.toString();return t<1e3?h(t,3):n.substr(n.length-4)},yyyy:function(){return h(this[d("FullYear")](),4)},yyyyy:function(){return h(this[d("FullYear")](),5)},yyyyyy:function(){return h(this[d("FullYear")](),6)},z:function(){var t=this.getTimezoneOffset();return(t>0?"-":"+")+Math.abs(t/60)},zz:function(){var t=this.getTimezoneOffset();return(t>0?"-":"+")+h(Math.abs(t/60))},zzz:function(){var t=this.getTimezoneOffset();return(t>0?"-":"+")+h(Math.abs(t/60))+":"+h(Math.abs(t/60)%1*60)}};return function(){var a,c,l=this;return"string"==typeof arguments[0]?(a=arguments[0],c=arguments[1]):(a=null,c=arguments[0]),c&&(void 0!==c.asUtc&&(t=c.asUtc),c.days&&(n=c.days),c.daysAbbr&&(r=c.daysAbbr),c.designator&&(i=c.designator),c.format&&(e=c.format),c.getDateOrdinal&&(f=c.getDateOrdinal),c.months&&(u=c.months),c.monthsAbbr&&(s=c.monthsAbbr)),a||(a=e),a.replace(o,function(t,n){var r=y[n];return"function"==typeof r?r.call(l):t}).replace(/["']/g,"")}}(),a=function(t){var i=this.getDay();return t?r[i]:n[i]},c=function(t){var n=this.getMonth();return t?s[n]:u[n]},f=function(){var t="th st nd rd th".split(" ");return function(){var n=this.getDate();return n>3&&n<21?t[0]:t[Math.min(n%10,4)]}}(),l=function(){return this.getHours()>=12?i[1]:i[0]},h=function(){var t="000000";return function(n,r,i){return i?(n+t).slice(0,r?Math.min(r,t.length):2):(t+n).slice(-(r?Math.min(r,t.length):2))}}();Date.prototype.format=function(t,n){return o.call(this,t,n)}}();
--------------------------------------------------------------------------------
/groups.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const groups = require('./options.dev/groups');
4 |
5 | console.log('Creating dist/groups.js...');
6 |
7 | const outputPath = path.resolve(path.join(__dirname, 'dist'), 'groups.js');
8 |
9 | let content = 'fbDevInterest.ALL_GROUPS = ';
10 | content += JSON.stringify(groups.map(g => g.id).sort(), null, 2);
11 | content += ';'
12 |
13 | fs.writeFileSync(outputPath, content);
14 |
--------------------------------------------------------------------------------
/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sidvishnoi/fb-dev-interest/HEAD/icon128.png
--------------------------------------------------------------------------------
/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sidvishnoi/fb-dev-interest/HEAD/icon16.png
--------------------------------------------------------------------------------
/icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sidvishnoi/fb-dev-interest/HEAD/icon32.png
--------------------------------------------------------------------------------
/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sidvishnoi/fb-dev-interest/HEAD/icon48.png
--------------------------------------------------------------------------------
/inject.js:
--------------------------------------------------------------------------------
1 | // util to create elements
2 | fbDevInterest.createElement = function(type, options) {
3 | const el = document.createElement(type);
4 | if (options.classList) el.classList.add(...options.classList.split(' '));
5 | if (options.attrs) {
6 | for (const attr in options.attrs) {
7 | el.setAttribute(attr, options.attrs[attr]);
8 | }
9 | }
10 | if (options.innerHTML) el.innerHTML = options.innerHTML;
11 | if (options.events) {
12 | for (const event in options.events) {
13 | el.addEventListener(event, options.events[event]);
14 | }
15 | }
16 | if (options.parent) options.parent.appendChild(el);
17 | return el;
18 | }
19 |
20 | // clears localStorage of extension in case of out of quota
21 | fbDevInterest.clearLocalStorage = function() {
22 | const keys = Object.keys(localStorage);
23 | let count = 0;
24 | for (const key of keys) {
25 | if (key.startsWith('https://graph.facebook.com/v2.11/')) {
26 | localStorage.removeItem(key);
27 | ++count;
28 | }
29 | if (count === 10) return; // let's delete just 10 items for now
30 | }
31 | };
32 |
33 | 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}`;
34 | 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`
35 |
36 | // remove blacklisted groups from ALL_GROUPS
37 | fbDevInterest.clearBlacklist = function() {
38 | for (const groupId of this._blacklist) {
39 | const idx = this.ALL_GROUPS.indexOf(parseInt(groupId, 10));
40 | if (idx > -1) this.ALL_GROUPS.splice(idx, 1);
41 | }
42 |
43 | // prioritize the group user is presently on
44 | try {
45 | let activeGroupId = document
46 | .querySelector('meta[property=\'al:ios:url\']')
47 | .content
48 | .match(/id=(\d*)/)[1];
49 | activeGroupId = parseInt(activeGroupId, 10);
50 | // remove from ALL_GROUPS
51 | const idx = this.ALL_GROUPS.indexOf(activeGroupId);
52 | if (idx > -1) this.ALL_GROUPS.splice(idx, 1);
53 | // add to front of ALL_GROUPS
54 | this.ALL_GROUPS.unshift(activeGroupId);
55 | } catch (e) {
56 | // do nothing
57 | }
58 | }
59 |
60 | // creates a placeholder post
61 | fbDevInterest.createPlaceholder = function() {
62 | const fbfeed_placeholder_story = ``;
63 | return this.createElement('div', {
64 | classList: '_3-u2 mbm _2iwp _4-u8',
65 | innerHTML: fbfeed_placeholder_story,
66 | parent: this.parent,
67 | });
68 | };
69 |
70 | // gets a group id from ALL_GROUPS list
71 | fbDevInterest.getGroupId = {};
72 | fbDevInterest.getGroupId = function () {
73 | const self = this;
74 | let i = -1;
75 | return (function *() {
76 | while (true) {
77 | yield self.ALL_GROUPS[++i % self.ALL_GROUPS.length];
78 | };
79 | })();
80 | }
81 |
82 | fbDevInterest.parent = document.querySelector('#pagelet_group_mall'); // feed parent
83 | fbDevInterest.state = {}; // stores next fetch urls for group and group names
84 | fbDevInterest.requestList = new Set();
85 |
86 | // find matching keywords in post body
87 | fbDevInterest.findMatchedKeywords = function(content) {
88 | if (this._keywords.size === 0) return [[0, 0]];
89 | const $content = content.toLowerCase();
90 | const matches = [];
91 | for (const keyword of this._keywords) {
92 | const re = new RegExp(`(?:^|\\b)(${keyword.replace(/\s\s+/g, '\\s*').replace(/\./g, '\\.?')})(?=\\b|$)`);
93 | const match = $content.match(re);
94 | if (match) matches.push([match.index, match[0].length])
95 | }
96 | function compare(a,b) {
97 | if (a[0] < b[0]) return -1;
98 | if (a[0] > b[0]) return 1;
99 | if (a[1] < b[1]) return 1; // to have larger keyword before
100 | if (a[1] > b[1]) return -1;
101 | return 0;
102 | }
103 | return matches.sort(compare);
104 | }
105 |
106 | // highlight matched keywords in post body
107 | fbDevInterest.highlightMatches = function(str, matches) {
108 | let $str = ''
109 | let pos;
110 | let flag = 0;
111 | const alreadyIncludedStart = new Set();
112 | for (let i = 0; i < matches.length; ++i) {
113 | pos = (i === 0) ? 0 : matches[i-1][0] + matches[i-1][1];
114 | const [ $start, $length ] = matches[i];
115 | if (alreadyIncludedStart.has($start)) {
116 | flag = matches[i-1][0] + matches[i-1][1];
117 | continue; // to prevent highlight again if a smaller keyword in a larger keyword
118 | }
119 | alreadyIncludedStart.add($start);
120 | $str += `${str.substring(flag !== 0 ? flag : pos, $start)}${str.substring($start, $start + $length)} `;
121 | flag = 0;
122 | }
123 | const lastMatch = matches[matches.length - 1];
124 | $str += str.substring(lastMatch[0] + lastMatch[1]);
125 | return $str;
126 | }
127 |
128 | fbDevInterest.showComments = function(json, parent) {
129 | const self = this;
130 |
131 | const getMessageHtml = function(e) {
132 | const created_time = new Date(e.created_time);
133 | return `
134 |
161 | `;
162 | }
163 |
164 | const addComment = function (e, p) {
165 | const comment = self.createElement('div', {
166 | classList: 'UFIRow _48ph _48pi UFIComment _4oep',
167 | parent: p,
168 | innerHTML: getMessageHtml(e),
169 | attrs: { style: 'border-top: none' },
170 | });
171 | return comment;
172 | }
173 |
174 | const addReply = function (e, p) {
175 | const reply = self.createElement('div', {
176 | classList: 'UFIRow _48ph _48pi _4204 UFIComment _4oep',
177 | parent: p,
178 | innerHTML: getMessageHtml(e),
179 | attrs: { style: 'border-left-width: 2px' },
180 | });
181 | return reply;
182 | }
183 |
184 | const commentsTrigger = document.getElementById(`comment_trigger_${json.id}`);
185 | commentsTrigger.innerHTML = 'View all comments'
186 | if (!json.comments) {
187 | commentsTrigger.innerHTML = 'No comments yet.'
188 | return;
189 | };
190 |
191 | for (const $comment of json.comments.data) {
192 | const comment = addComment($comment, parent);
193 | if (!$comment.comments) continue;
194 | const replyList = self.createElement('div', { classList: 'UFIReplyList', parent: parent });
195 | for (const $reply of $comment.comments.data) {
196 | const reply = addReply($reply, replyList);
197 | }
198 | }
199 | }
200 |
201 | // gets comments for a post and shows them
202 | fbDevInterest.getComments = function(postid, parent) {
203 | const self = this;
204 | const createCommentArea = function(pid, p) {
205 | const html = `
206 |
212 | `;
213 | const c = self.createElement('div', {
214 | innerHTML: html,
215 | parent: self.createElement('div', {
216 | classList: '_3b-9 _j6a',
217 | parent: self.createElement('div', {
218 | classList: 'UFIList',
219 | innerHTML: `Comments `,
220 | parent: self.createElement('div', {
221 | classList: 'uiUfi UFIContainer _5pc9 _5vsj _5v9k',
222 | parent: self.createElement('form', {
223 | classList: 'commentable_item',
224 | parent: p,
225 | }),
226 | }),
227 | }),
228 | }),
229 | });
230 |
231 | document.getElementById(`comment_trigger_${postid}`).innerHTML = `View all comments `;
232 |
233 | return c;
234 | }
235 | const commentsArea = createCommentArea(postid, parent);
236 |
237 | const fetchUrl = self.COMMENTS_API_URL.replace('POSTID', postid);
238 |
239 | let cachedResult = localStorage.getItem(fetchUrl);
240 | if (cachedResult) {
241 | cachedResult = JSON.parse(cachedResult);
242 | const minuteDiff = (new Date() - new Date(cachedResult.last_fetch_time)) / (1000*60);
243 | if (minuteDiff < 10) { // 10 min cache
244 | return handleJsonResponse(cachedResult);
245 | }
246 | }
247 |
248 | self.requestList.add(fetchUrl);
249 | fetch(fetchUrl)
250 | .then(res => res.json())
251 | .then((json) => {
252 | json.last_fetch_time = new Date();
253 | try {
254 | localStorage.setItem(fetchUrl, JSON.stringify(json));
255 | } catch (e) {
256 | // out of storage? don't store and clear items for extension
257 | self.clearLocalStorage();
258 | }
259 | self.requestList.delete(fetchUrl);
260 | self.showComments(json, commentsArea);
261 | })
262 | .catch((err) => {
263 | console.error(err);
264 | self.requestList.delete(fetchUrl);
265 | document.getElementById(`comment_trigger_${postid}`).innerHTML = `Some error occured. Try again?`;
266 | });
267 | }
268 |
269 | // shows like, comment and share buttons for a post
270 | fbDevInterest.showPostButtons = function(entry, parent) {
271 | const self = this;
272 | function _showComments() {
273 | self.getComments(entry.id, parent);
274 | this.removeEventListener('click', _showComments);
275 | }
276 |
277 | const el = self.createElement('div', {
278 | classList: '_sa_ _gsd _fgm _5vsi _192z',
279 | parent: parent,
280 | });
281 |
282 | const el6 = self.createElement('div', {
283 | classList: '_42nr',
284 | parent: self.createElement('div', {
285 | classList: '_524d',
286 | parent: self.createElement('div', {
287 | classList: '_3399 _a7s _20h6 _610i _125r clearfix _zw3',
288 | parent: self.createElement('div', {
289 | classList: '_57w',
290 | parent: self.createElement('div', {
291 | parent: self.createElement('div', {
292 | classList: '_37uu',
293 | parent: el,
294 | }),
295 | }),
296 | }),
297 | }),
298 | }),
299 | });
300 |
301 | const likeButton = self.createElement('a', {
302 | classList: 'UFILikeLink _4x9- _4x9_ _48-k',
303 | innerHTML: 'Like',
304 | attrs: {
305 | href: entry.permalink_url,
306 | target: '_blank',
307 | },
308 | parent: self.createElement('div', {
309 | classList: '_khz _4sz1',
310 | parent: self.createElement('span', {
311 | classList: '_1mto',
312 | parent: el6,
313 | }),
314 | }),
315 | });
316 |
317 | const commentButton = self.createElement('a', {
318 | classList: 'comment_link _5yxe',
319 | innerHTML: 'Comment',
320 | attrs: {
321 | href: '#',
322 | role: 'button',
323 | },
324 | events: {
325 | click: _showComments,
326 | },
327 | parent: self.createElement('div', {
328 | classList: '_6a _15-7 _3h-u',
329 | parent: self.createElement('span', {
330 | classList: '_1mto',
331 | parent: el6,
332 | }),
333 | }),
334 | });
335 |
336 | const shareButton = self.createElement('a', {
337 | classList: 'share_action_link _5f9b',
338 | innerHTML: 'Share',
339 | attrs: {
340 | href: entry.permalink_url,
341 | target: '_blank',
342 | },
343 | parent: self.createElement('div', {
344 | classList: '_27de',
345 | parent: self.createElement('span', {
346 | classList: '_1mto',
347 | parent: el6,
348 | }),
349 | }),
350 | });
351 |
352 | return el;
353 | }
354 |
355 | // appends a post in feed
356 | fbDevInterest.showPost = function(post) {
357 | const placeholder = document.querySelector('._3-u2.mbm._2iwp._4-u8')
358 | if (placeholder) this.parent.replaceChild(post, placeholder)
359 | else this.parent.appendChild(post);
360 | }
361 |
362 | // split post body if too long
363 | fbDevInterest.splitPostBody = function(content) {
364 | const splitted = content.split('');
365 | if (splitted.length < 6) return content;
366 | const visible = splitted.slice(0, 6).join('');
367 | const hidden = splitted.slice(6).join('');
368 |
369 | const id = Math.random().toString().replace('.', '');
370 | const postBody = `${visible}
... ${hidden}
See more `;
371 | return postBody;
372 | }
373 |
374 | // creates a post
375 | fbDevInterest.createPost = function(entry, group) {
376 | const self = this;
377 | if (!entry.message) return;
378 |
379 | const matchedKeywords = self.findMatchedKeywords(entry.message);
380 | if (matchedKeywords.length === 0) return;
381 |
382 | const getBodyHTML = function(e, grp) {
383 | let message = e.message;
384 | if (self._highlightMatches) message = self.highlightMatches(message, matchedKeywords);
385 | message = message.replace(/\n/g, "").linkify();
386 |
387 | if (e.link && e.full_picture && e.full_picture.includes('//external.')) {
388 | message = `${message}
[LINK: ${e.link} ]`;
389 | }
390 | message = self.splitPostBody(message);
391 |
392 | const created_time = new Date(e.created_time);
393 |
394 | const image = (e.full_picture && !e.full_picture.includes('//external.'))
395 | ? `
`
396 | : '';
397 |
398 | const html = `
399 |
400 |
401 |
402 |
403 |
408 |
412 |
413 |
${message}
414 | ${image}
415 |
416 |
417 |
418 |
419 |
420 | `;
421 | return html;
422 | };
423 |
424 | const p = self.createElement('div', {
425 | classList: '_4-u2 mbm _4mrt _5jmm _5pat _5v3q _4-u8',
426 | innerHTML: getBodyHTML(entry, group),
427 | attrs: { id: `mall_post_${entry.id}:6:0` }
428 | });
429 |
430 | self.showPost(p);
431 | self.showPostButtons(entry, p);
432 | };
433 |
434 | // gets feed json and shows posts
435 | fbDevInterest.getGroupFeed = function(options) {
436 | const self = this;
437 | let fetchUrl = self.BASE_API_URL.replace('GROUPID', options.groupId)
438 | const state = self.state[options.groupId];
439 | if (state && state.nextPageUrl) {
440 | fetchUrl = state.nextPageUrl;
441 | }
442 | if (fetchUrl === 'False') return;
443 |
444 | function handleJsonResponse(json) {
445 | if (json.error) throw json.error;
446 | if (!json.feed) {
447 | if (Array.isArray(json.data)) {
448 | json.feed = {
449 | data: json.data,
450 | paging: json.paging,
451 | };
452 | } else {
453 | throw new Error(`failed to fetch: ${fetchUrl}`);
454 | }
455 | }
456 | self.state[options.groupId] = self.state[options.groupId] || { name: json.name };
457 | try {
458 | self.state[options.groupId].nextPageUrl = json.feed.paging.next;
459 | } catch (err) {
460 | // no more posts, remove group from list
461 | const idx = self.ALL_GROUPS.indexOf(options.groupId);
462 | self.ALL_GROUPS.splice(idx, 1);
463 | self.state[options.groupId].nextPageUrl = 'False';
464 | }
465 | for (const entry of json.feed.data) {
466 | try {
467 | self.createPost(entry, { name: self.state[options.groupId].name, id: options.groupId });
468 | } catch (err) {
469 | console.error(err);
470 | }
471 | }
472 | self.getPostsOnScroll();
473 | }
474 |
475 | function handleJsonError(err) {
476 | console.error(err);
477 | const errorPost = self.createElement('div', {
478 | classList: '_4-u2 mbm _4mrt _5jmm _5pat _5v3q _4-u8',
479 | innerHTML: `${JSON.stringify(err, null, 2)} `,
480 | attrs: { style: 'color: red;' },
481 | });
482 | self.showPost(errorPost);
483 | }
484 |
485 | let cachedResult = localStorage.getItem(fetchUrl);
486 | if (cachedResult) {
487 | cachedResult = JSON.parse(cachedResult);
488 | const minuteDiff = (new Date() - new Date(cachedResult.last_fetch_time)) / (1000*60);
489 | if (minuteDiff < 10) { // 10 min cache
490 | return handleJsonResponse(cachedResult);
491 | }
492 | }
493 |
494 | self.requestList.add(fetchUrl);
495 | fetch(fetchUrl)
496 | .then((res) => res.json())
497 | .then((json) => {
498 | json.last_fetch_time = new Date();
499 | try {
500 | localStorage.setItem(fetchUrl, JSON.stringify(json));
501 | } catch(e) {
502 | // out of storage? don't store and clear items for extension
503 | self.clearLocalStorage();
504 | }
505 | self.requestList.delete(fetchUrl);
506 | handleJsonResponse(json);
507 | })
508 | .catch((err) => {
509 | self.requestList.delete(fetchUrl);
510 | handleJsonError(err);
511 | });
512 | };
513 |
514 | // gets more posts on scroll (replacement of fb's infinite scroll)
515 | fbDevInterest.getPostsOnScroll = function() {
516 | const self = this;
517 | const a = document.querySelectorAll('._4mrt._5jmm._5pat._5v3q._4-u8');
518 | const last = a[a.length -1 ];
519 | const onVisible = function(e) {
520 | const groupId = self.getGroupId.next().value;
521 | if (!groupId) {
522 | fbDevInterest.nomore = true;
523 | console.log('No more posts in criteria');
524 | window.removeEventListener('scroll', onVisible, true);
525 | return;
526 | };
527 | if (self.requestList.size > 10) return;
528 | if (!last) {
529 | return self.getGroupFeed({ groupId }); // when no post in feed yet
530 | }
531 | if (isScrolledIntoView(last)) {
532 | window.removeEventListener('scroll', onVisible, true);
533 | if (document.querySelectorAll('._3-u2.mbm._2iwp._4-u8').length < 3) {
534 | for (let i = 0; i < 5; ++i) self.createPlaceholder();
535 | }
536 | self.getGroupFeed({ groupId });
537 | }
538 | };
539 | if (!fbDevInterest.nomore) window.addEventListener('scroll', onVisible, true);
540 | }
541 |
542 | // clears feed to show custom posts
543 | fbDevInterest.clearPosts = function() {
544 | const removeElements = (elms) => Array.from(elms).forEach(el => el.remove());
545 | removeElements(document.querySelectorAll('._4mrt._5jmm._5pat._5v3q._4-u8')); // posts
546 | removeElements(document.querySelectorAll('._5umn')); // pinned post labels etc
547 | removeElements(document.querySelectorAll('._4wcq')); // recent activity label etc
548 |
549 | // XXX: prevent fb's infinite scroll
550 | window.addEventListener('scroll', (e) => e.stopPropagation(), true);
551 | };
552 |
553 | fbDevInterest.init = function() {
554 | this.clearBlacklist();
555 | this.clearPosts();
556 | this.getGroupId = this.getGroupId();
557 | for (let i = 0; i < 5; ++i) this.createPlaceholder();
558 | scrollToItem(this.parent); // smooth scroll to feed start (from utils.js)
559 | this.getGroupFeed({ groupId: this.getGroupId.next().value });
560 | };
561 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/options.dev/ApiKeyForm.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactDOM = require('react-dom');
3 |
4 | const defaultAppToken = '148818419074509|CrToch6NqQe-mDJ8601f7HU_-3I';
5 |
6 | class ApiKeyForm extends React.Component {
7 | constructor() {
8 | super();
9 | this.state = { value: defaultAppToken, status: 'Set API Key' };
10 | chrome.storage.sync.get('apikey', (res) => {
11 | if (res.apikey)
12 | this.setState({ value: res.apikey, status: 'Modify API Key' });
13 | });
14 | this.handleChange = this.handleChange.bind(this);
15 | this.handleSubmit = this.handleSubmit.bind(this);
16 | }
17 |
18 | handleChange(event) {
19 | this.setState({ value: event.target.value, status: 'Update API Key' });
20 | }
21 |
22 | handleSubmit(event) {
23 | event.preventDefault();
24 | const self = this;
25 | chrome.storage.sync.set({ apikey: self.state.value }, () => {
26 | self.setState({ status: 'Saved' });
27 | setTimeout(function() {
28 | self.setState({ status: 'Modify API Key' })
29 | }, 1500);
30 | });
31 | if (self.state.value !== defaultAppToken) {
32 | FB.AppEvents.logEvent('UsedCustomAppToken');
33 | }
34 | }
35 |
36 | render() {
37 | return (
38 |
47 | );
48 | }
49 | };
50 |
51 | module.exports = ApiKeyForm;
52 |
--------------------------------------------------------------------------------
/options.dev/BlacklistGroups.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactDOM = require('react-dom');
3 | const ReactTags = require('react-tag-autocomplete');
4 | const groups = require('./groups');
5 |
6 | class BlacklistGroups extends React.Component {
7 | constructor (props) {
8 | super(props);
9 |
10 | this.state = {
11 | tags: [],
12 | suggestions: groups,
13 | };
14 | chrome.storage.sync.get('groups', (res) => {
15 | const $groups = res.groups || [];
16 | const blacklistGroups = $groups.map((id) => {
17 | return groups.find(g => g.id === id);
18 | });
19 | const newSuggestions = groups.filter((group) => {
20 | const toRemove = !($groups.includes(group.id));
21 | return toRemove;
22 | });
23 | this.setState({ tags: blacklistGroups, suggestions: newSuggestions });
24 | });
25 | }
26 |
27 | handleDelete (i) {
28 | const tags = this.state.tags.slice(0);
29 | const removed = tags.splice(i, 1);
30 | this.setState({ tags });
31 |
32 | const updatedSuggestions = [...this.state.suggestions, ...removed];
33 | this.setState({ suggestions: updatedSuggestions });
34 |
35 | const blacklistedGroupIds = tags.map((group) => group.id);
36 | chrome.storage.sync.set({ groups: blacklistedGroupIds });
37 | }
38 |
39 | handleAddition (tag) {
40 | let tags = [].concat(this.state.tags, tag);
41 | tags = tags.filter(Boolean);
42 | this.setState({ tags });
43 |
44 | const updatedSuggestions = this.state.suggestions.filter((group) => {
45 | return group.id !== tag.id;
46 | });
47 | this.setState({ suggestions: updatedSuggestions });
48 |
49 | const blacklistedGroupIds = tags.map((group) => group.id);
50 | chrome.storage.sync.set({ groups: blacklistedGroupIds });
51 | }
52 |
53 | clearStorage() {
54 | chrome.storage.sync.remove('keywords');
55 | window.location.href = '';
56 | }
57 |
58 | render () {
59 | return (
60 |
61 |
Blacklist Groups ✕
62 |
68 |
69 | )
70 | }
71 | };
72 |
73 | module.exports = BlacklistGroups;
74 |
--------------------------------------------------------------------------------
/options.dev/HighlightMatches.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactDOM = require('react-dom');
3 | const ToggleButton = require('react-toggle-button');
4 |
5 | class HighlightMatches extends React.Component {
6 | constructor (props) {
7 | super(props);
8 | this.state = { value: true };
9 | const self = this;
10 | chrome.storage.sync.get('highlightMatches', (res) => {
11 | self.setState({ value: typeof res.highlightMatches === 'undefined' ? true : res.highlightMatches })
12 | });
13 | }
14 |
15 | handleToggle(value) {
16 | this.setState({ value: !value });
17 | chrome.storage.sync.set({ highlightMatches: !value });
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
Highlight Keyword Matches
24 |
47 |
48 | );
49 | }
50 | };
51 |
52 | module.exports = HighlightMatches;
53 |
--------------------------------------------------------------------------------
/options.dev/KeywordsFilter.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactDOM = require('react-dom');
3 | const ReactTags = require('react-tag-autocomplete');
4 |
5 | let keywords = require('./keywords');
6 |
7 | class KeywordsFilter extends React.Component {
8 | constructor (props) {
9 | super(props);
10 |
11 | this.state = {
12 | tags: [],
13 | suggestions: keywords,
14 | };
15 | chrome.storage.sync.get('keywords', (res) => {
16 | const $keywords = res.keywords || [];
17 | keywords = [...keywords, ...$keywords];
18 | const selectedKeywords = $keywords.map((keyword) => {
19 | return keywords.find(k => k.name === keyword.name);
20 | });
21 | const newSuggestions = keywords.filter((keyword) => {
22 | const toRemove = !($keywords.includes(keyword.name));
23 | return toRemove;
24 | });
25 |
26 | this.setState({ tags: selectedKeywords, suggestions: newSuggestions });
27 | });
28 | }
29 |
30 | handleDelete (i) {
31 | const tags = this.state.tags.slice(0);
32 | const removed = tags.splice(i, 1);
33 | const updatedSuggestions = [...this.state.suggestions, ...removed];
34 |
35 | this.setState({ tags, suggestions: updatedSuggestions });
36 | chrome.storage.sync.set({ keywords: tags });
37 | }
38 |
39 | handleAddition (tag) {
40 | if (!tag.id) tag.id = -1*(this.state.tags.length + this.state.suggestions.length + 1);
41 | let tags = [].concat(this.state.tags, tag);
42 | tags = tags.filter(Boolean);
43 |
44 | const updatedSuggestions = this.state.suggestions.filter((group) => {
45 | return group.id !== tag.id;
46 | });
47 |
48 | this.setState({ suggestions: updatedSuggestions, tags });
49 |
50 | chrome.storage.sync.set({ keywords: tags });
51 |
52 | // tracking
53 | FB.AppEvents.logEvent('UsedKeyword', null, { keyword: tag.name });
54 | const isNewKeyword = !(keywords.find(k => k.name === tag.name));
55 | if (isNewKeyword) {
56 | FB.AppEvents.logEvent('NewKeyword', null, { keyword: tag.name });
57 | }
58 | }
59 |
60 | clearStorage() {
61 | chrome.storage.sync.remove('keywords');
62 | window.location.href = '';
63 | }
64 |
65 | render () {
66 | return (
67 |
68 |
My Interests ✕
69 |
76 |
77 | )
78 | }
79 | };
80 |
81 | module.exports = KeywordsFilter;
82 |
--------------------------------------------------------------------------------
/options.dev/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 |
--------------------------------------------------------------------------------
/options.dev/app.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactDOM = require('react-dom');
3 | const ApiKeyForm = require('./ApiKeyForm.jsx');
4 | const BlacklistGroups = require('./BlacklistGroups.jsx');
5 | const KeywordsFilter = require('./KeywordsFilter.jsx');
6 | const HighlightMatches = require('./HighlightMatches.jsx');
7 |
8 | require('./react-tags.css');
9 | require('./style.css');
10 |
11 | class App extends React.Component {
12 | render() {
13 | return (
14 |
15 |
Settings: Merge Facebook Dev Circles by Interests
16 |
17 |
18 |
19 |
20 |
21 | )
22 | };
23 | }
24 |
25 | ReactDOM.render(
26 | ,
27 | document.getElementById('root')
28 | );
29 |
--------------------------------------------------------------------------------
/options.dev/groups.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | { id: 249598592040574, name: "Facebook Developer Circle: Delhi, NCR" },
3 | { id: 1378294582253698, name: "Facebook Developer Circle: Nsukka" },
4 | { id: 2224932161064321, name: "Facebook Developer Circle: Tunis" },
5 | { id: 1924443867832338, name: "Facebook Developer Circle: Bangkok" },
6 | { id: 1922538421363451, name: "Facebook Developer Circle: Santiago" },
7 | { id: 1920036621597031, name: "Facebook Developer Circle: Malang" },
8 | { id: 1903916609822504, name: "Facebook Developer Circle: Ado-Ekiti" },
9 | { id: 1841081392797911, name: "Facebook Developer Circle: Surabaya" },
10 | { id: 1806620552895262, name: "Facebook Developer Circle: Porto Alegre" },
11 | { id: 1780072415645281, name: "Facebook Developer Circle: Chennai" },
12 | { id: 1741843536047014, name: "Facebook Developer Circle: Kathmandu" },
13 | { id: 1724152667880378, name: "Facebook Developer Circle: Berlin" },
14 | { id: 1607133026028061, name: "Facebook Developer Circle: Accra" },
15 | { id: 1494181493938081, name: "Facebook Developer Circle: Islamabad" },
16 | { id: 1443394385967980, name: "Facebook Developer Circle: Kampala" },
17 | { id: 1258355007573190, name: "Facebook Developer Circle: Cairo" },
18 | { id: 1152576018114322, name: "Facebook Developer Circle: Bengaluru" },
19 | { id: 1148469218498930, name: "Facebook Developer Circle: Lagos" },
20 | { id: 1075017422642967, name: "Facebook Developer Circle: Ciudad de Guatemala" },
21 | { id: 1074858042611323, name: "Facebook Developer Circle: Hyderabad" },
22 | { id: 1071045349642536, name: "Facebook Developer Circle: Lahore" },
23 | { id: 1041205739348709, name: "Facebook Developer Circle: Bogotá" },
24 | { id: 893652180764182, name: "Facebook Developer Circle: Santa Rita do Sapucaí" },
25 | { id: 886251554842166, name: "Facebook Developer Circle: São Paulo" },
26 | { id: 885490321621308, name: "Facebook Developer Circle: Harare" },
27 | { id: 854314664699156, name: "Facebook Developer Circle: Ho Chi Minh City" },
28 | { id: 826341790867138, name: "Facebook Developer Circle: Guadalajara" },
29 | { id: 813879575430133, name: "Facebook Developer Circle: Taipei" },
30 | { id: 811281355669013, name: "Facebook Developer Circle: Dhaka" },
31 | { id: 793016410839401, name: "Facebook Developer Circle: Karachi" },
32 | { id: 786453984830109, name: "Facebook Developer Circle: Mumbai" },
33 | { id: 638854212931776, name: "Facebook Developer Circle: Manila" },
34 | { id: 485698195138488, name: "Facebook Developer Circle: Lille" },
35 | { id: 476463749198108, name: "Facebook Developer Circle: Ciudad de México" },
36 | { id: 428973767504677, name: "Facebook Developer Circle: Bali" },
37 | { id: 402137910152010, name: "Facebook Developer Circle: Bandung" },
38 | { id: 362906487478469, name: "Facebook Developer Circle: Geneva" },
39 | { id: 348458995586076, name: "Facebook Developer Circle: Amman" },
40 | { id: 332006040559709, name: "Facebook Developer Circle: Oldenburg" },
41 | { id: 313087542449350, name: "Facebook Developer Circle: Jakarta" },
42 | { id: 309450039518404, name: "Facebook Developer Circle: Vienna" },
43 | { id: 304477986647756, name: "Facebook Developer Circle: Uyo" },
44 | { id: 293458267749614, name: "Facebook Developer Circle: Yogyakarta" },
45 | { id: 265793323822652, name: "Facebook Developer Circle: Casablanca" },
46 | { id: 223094988221674, name: "Facebook Developer Circle: Buea" },
47 | { id: 199036970608482, name: "Facebook Developer Circle: Paris" },
48 | { id: 187217085094857, name: "Facebook Developer Circle: Dakar" },
49 | { id: 186924858495604, name: "Facebook Developer Circle: Kolkata" },
50 | { id: 160941794378470, name: "Facebook Developer Circle: Gaza" },
51 | { id: 152127978670639, name: "Facebook Developer Circle: Cape Town" },
52 | { id: 132580147377707, name: "Facebook Developer Circle: Buenos Aires" },
53 | { id: 125327974795168, name: "Facebook Developer Circle: Madrid" },
54 | { id: 111858152705945, name: "Facebook Developer Circle: Campinas" }
55 | ];
56 |
--------------------------------------------------------------------------------
/options.dev/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 |
--------------------------------------------------------------------------------
/options.dev/keywords.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | { id: 1, name: 'js' },
3 | { id: 2, name: 'javascript' },
4 | { id: 3, name: 'angular' },
5 | { id: 4, name: 'react' },
6 | { id: 5, name: 'reactdom' },
7 | { id: 6, name: 'react native' },
8 | { id: 7, name: 'python' },
9 | { id: 8, name: 'machine learning' },
10 | { id: 9, name: 'ruby' },
11 | { id: 10, name: 'android' },
12 | { id: 11, name: 'ios' },
13 | { id: 12, name: 'sql' },
14 | { id: 13, name: 'java' },
15 | { id: 14, name: '.net' },
16 | { id: 15, name: 'laravel' },
17 | { id: 16, name: 'php' },
18 | { id: 17, name: 'cordovo' },
19 | { id: 18, name: 'unity' },
20 | { id: 18, name: 'unity3d' },
21 | { id: 19, name: 'vue.js' },
22 | { id: 20, name: 'node.js' },
23 | { id: 21, name: 'swift' },
24 | { id: 22, name: 'django' },
25 | { id: 20, name: 'kotlin' }
26 | ];
27 |
--------------------------------------------------------------------------------
/options.dev/react-tags.css:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | *
4 | *
5 | *
6 | *
7 | *
8 | *
24 | */
25 | .react-tags {
26 | position: relative;
27 | padding: 6px 0 0 6px;
28 | border: 1px solid #eee;
29 | border-radius: 1px;
30 |
31 | /* shared font styles */
32 | font-size: 1em;
33 | line-height: 1.2;
34 |
35 | /* clicking anywhere will focus the input */
36 | cursor: text;
37 | }
38 |
39 | .react-tags.is-focused {
40 | border-color: #ddd;
41 | }
42 |
43 | .react-tags__selected {
44 | display: inline;
45 | }
46 |
47 | .react-tags__selected-tag {
48 | display: inline-block;
49 | box-sizing: border-box;
50 | margin: 0 6px 6px 0 !important;
51 | padding: 6px 8px;
52 | background: #ecf0f7;
53 |
54 | /* match the font styles */
55 | font-size: 15px !important;
56 | line-height: inherit;
57 | font-weight: normal !important;
58 | }
59 |
60 | .react-tags__selected-tag:after {
61 | content: '\2715';
62 | color: #AAA;
63 | margin-left: 8px;
64 | }
65 |
66 | .react-tags__search {
67 | display: inline-block;
68 |
69 | /* match tag layout */
70 | padding: 7px 2px;
71 | margin-bottom: 6px;
72 |
73 | /* prevent autoresize overflowing the container */
74 | max-width: 100%;
75 | }
76 |
77 | @media screen and (min-width: 30em) {
78 |
79 | .react-tags__search {
80 | /* this will become the offsetParent for suggestions */
81 | position: relative;
82 | }
83 |
84 | }
85 |
86 | .react-tags__search input {
87 | /* prevent autoresize overflowing the container */
88 | max-width: 100%;
89 |
90 | /* remove styles and layout from this element */
91 | margin: 0;
92 | padding: 0;
93 | border: 0;
94 | outline: none;
95 |
96 | /* match the font styles */
97 | font-size: inherit;
98 | line-height: inherit;
99 | }
100 |
101 | .react-tags__search input::-ms-clear {
102 | display: none;
103 | }
104 |
105 | .react-tags__suggestions {
106 | position: absolute;
107 | top: 100%;
108 | left: 0;
109 | width: 100%;
110 | z-index: 999999;
111 | }
112 |
113 | @media screen and (min-width: 30em) {
114 |
115 | .react-tags__suggestions {
116 | width: 240px;
117 | }
118 |
119 | }
120 |
121 | .react-tags__suggestions ul {
122 | margin: 4px -1px;
123 | padding: 0;
124 | list-style: none;
125 | background: white;
126 | border: 1px solid #ccc;
127 | border-color: rgba(0, 0, 0, .15);
128 | border-radius: 2px;
129 | box-shadow: 0 4px 6px 2px rgba(0, 0, 0, .10);
130 | }
131 |
132 | .react-tags__suggestions li {
133 | /*border-bottom: 1px solid #ddd;*/
134 | padding: 6px 8px;
135 | }
136 |
137 | .react-tags__suggestions li mark {
138 | /*text-decoration: underline;*/
139 | background: none;
140 | font-weight: 600;
141 | }
142 |
143 | .react-tags__suggestions li:hover {
144 | cursor: pointer;
145 | background: #eee;
146 | }
147 |
148 | .react-tags__suggestions li.is-active {
149 | background: #ecf0f7;
150 | }
151 |
152 | .react-tags__suggestions li.is-disabled {
153 | opacity: 0.5;
154 | cursor: auto;
155 | }
156 |
--------------------------------------------------------------------------------
/options.dev/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | a {
8 | text-decoration: none;
9 | }
10 |
11 | body {
12 | display: flex;
13 | background: #4267b2;
14 | color: #1d2129;
15 | padding: 1vh 2vw;
16 | margin: 1em auto;
17 | font-family: Helvetica, Arial, sans-serif;
18 | font-size: 16px;
19 | min-height: 96vh;
20 | }
21 |
22 | #root {
23 | background: #fff;
24 | /*padding: 1em;*/
25 | width: 100%;
26 | box-shadow: 1px 1px 3px 0px #333;
27 | flex: 1;
28 | border-radius: 6px;
29 | }
30 |
31 | .page-title {
32 | font-size: 2em;
33 | padding: 1em 0 0.3em;
34 | /*margin-bottom: 1em;*/
35 | background: #264075;
36 | color: #fff;
37 | padding: 1em;
38 | /*border-bottom: 4px solid #4267b2;*/
39 | }
40 |
41 | #root > div > form, #root > div > div {
42 | padding: 1em;
43 | background: #fff;
44 | }
45 |
46 | input {
47 | max-width: 100%;
48 | border: 0;
49 | outline: none;
50 | font-size: inherit;
51 | line-height: inherit;
52 | }
53 |
54 | a.hint {
55 | background: #365899;
56 | color: #fff;
57 | padding: 0 4px;
58 | border-radius: 4px;
59 | }
60 | a.hint:hover {
61 | background: #4267b2;
62 | }
63 |
64 | h3 {
65 | color: #444;
66 | margin: 3px 0;
67 | }
68 |
69 | .apikey_form .form-group {
70 | display: flex;
71 | width: 100%;
72 | align-items: stretch;
73 | justify-content: space-between;
74 | }
75 |
76 | .apikey_form input {
77 | position: relative;
78 | padding: 7px 2px;
79 | flex: 1;
80 | cursor: text;
81 | font-size: 12px;
82 | font-family: monospace;
83 | border: 1px solid #eee;
84 | border-radius: 2px;
85 | color: #555;
86 | }
87 | .apikey_form input:focus {
88 | border-color: #ddd;
89 | }
90 |
91 | .apikey_form button, .react-tags__selected-tag {
92 | background-color: #f6f7f9;
93 | color: #4b4f56;
94 | border: none;
95 | line-height: 26px;
96 | padding: 0 10px;
97 | transition: 200ms cubic-bezier(.08,.52,.52,1) background-color, 200ms cubic-bezier(.08,.52,.52,1) box-shadow, 200ms cubic-bezier(.08,.52,.52,1) transform;
98 | border: 1px solid #ced0d4;
99 | border-radius: 2px;
100 | box-sizing: content-box;
101 | font-size: 12px;
102 | margin: 0 1px;
103 | -webkit-font-smoothing: antialiased;
104 | font-weight: bold;
105 | position: relative;
106 | }
107 | .apikey_form button:hover, .react-tags__selected-tag:hover{
108 | background: #e9ebee;
109 | cursor: pointer;
110 | }
111 | .apikey_form button:focus, .apikey_form button:active, .react-tags__selected-tag:focus, .react-tags__selected-tag:active{
112 | box-shadow: 0 0 1px 2px rgba(88, 144, 255, .75), 0 1px 1px rgba(0, 0, 0, .15);
113 | outline: none;
114 | }
115 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fb-dev-interest",
3 | "version": "1.0.0",
4 | "description": "A Chrome extension that filters through Facebook Dev Circles around the world to get only the interesting posts in your feed.",
5 | "main": "background.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "rimraf dist && node_modules/.bin/webpack && node groups.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/sidvishnoi/fb-dev-interest.git"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/sidvishnoi/fb-dev-interest/issues"
19 | },
20 | "homepage": "https://github.com/sidvishnoi/fb-dev-interest#readme",
21 | "dependencies": {
22 | "react": "^16.1.1",
23 | "react-dom": "^16.1.1",
24 | "react-tag-autocomplete": "^5.5.0",
25 | "react-toggle-button": "^2.2.0"
26 | },
27 | "devDependencies": {
28 | "babel-core": "^6.26.0",
29 | "babel-loader": "^7.1.2",
30 | "babel-preset-es2015": "^6.24.1",
31 | "babel-preset-react": "^6.24.1",
32 | "copy-webpack-plugin": "^4.2.1",
33 | "css-loader": "^0.28.7",
34 | "extract-text-webpack-plugin": "^3.0.2",
35 | "html-webpack-plugin": "^2.30.1",
36 | "rimraf": "^2.6.2",
37 | "style-loader": "^0.19.0",
38 | "uglify-es": "^3.1.10",
39 | "webpack": "^3.8.1"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/scrape-group-lists.js:
--------------------------------------------------------------------------------
1 | /*
2 | https://www.facebook.com/pg/DeveloperCircles/groups/
3 | */
4 | var lst = [];
5 | for (const group of $$('._3lkd')) {
6 | const _ = group.querySelector('._266w a');
7 | const name = _.innerText;
8 | const id = _.getAttribute('data-hovercard').split("?id=")[1].split("&")[0];
9 | const isPublic = group.querySelector('._2fxm').innerText === 'Public group';
10 | if (isPublic) lst.push(id);
11 | }
12 | console.log(lst);
13 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | // js smooth scroll
2 | // https://jsfiddle.net/s61x7c4e/
3 | function scrollToItem(element, duration = 1000) {
4 | const getElementY = (el) => window.pageYOffset + el.getBoundingClientRect().top;
5 | const easing = (t) => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1;
6 | var startingY = window.pageYOffset;
7 | var elementY = getElementY(element);
8 | var targetY = ((document.body.scrollHeight - elementY < window.innerHeight) ? document.body.scrollHeight - window.innerHeight : elementY) - 100;
9 | var diff = targetY - startingY;
10 | var start;
11 | if (!diff) return;
12 | window.requestAnimationFrame(function step(timestamp) {
13 | if (!start) start = timestamp;
14 | var time = timestamp - start;
15 | var percent = Math.min(time / duration, 1);
16 | percent = easing(percent);
17 | window.scrollTo(0, startingY + diff * percent);
18 | if (time < duration) window.requestAnimationFrame(step);
19 | });
20 | }
21 |
22 | function isScrolledIntoView(el) {
23 | var elemTop = el.getBoundingClientRect().top;
24 | var elemBottom = el.getBoundingClientRect().bottom;
25 | var isVisible = elemTop < window.innerHeight && elemBottom >= 0;
26 | return isVisible;
27 | }
28 |
29 | // https://gist.github.com/skattyadz/1501387
30 | String.prototype.linkify = function() {
31 | // http://, https://, ftp://
32 | var urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
33 | // www. sans http:// or https://
34 | var pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
35 | // Email addresses
36 | var emailAddressPattern = /\w+@[a-zA-Z_]+?(?:\.[a-zA-Z]{2,6})+/gim;
37 | var hashtagPattern = /(^|\s)#(\w+)/g;
38 |
39 | return this
40 | .replace(urlPattern, '
$& ')
41 | .replace(pseudoUrlPattern, '$1
$2 ')
42 | .replace(emailAddressPattern, '
$& ')
43 | .replace(hashtagPattern, '$1
#$2 ');
44 | };
45 |
46 | /**
47 | * Date Format
48 | * Copyright (c) 2011, marlun78
49 | * MIT License, https://gist.github.com/marlun78/1351171
50 | */
51 | (function () {
52 |
53 | 'use strict';
54 |
55 | var asUtc = false,
56 | days = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'.split(' '),
57 | daysAbbr = 'Sun Mon Tue Wed Thu Fri Sat'.split(' '),
58 | designators = 'AM PM'.split(' '),
59 | defaultFormat = 'MMMM d""x "at" h:mm tt',
60 | months = 'January February March April May June July August September October November December'.split(' '),
61 | monthsAbbr = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' '),
62 |
63 | // The formatting function
64 | formatDate =(function(){
65 | var findPatterns = /([dfFhmHMstxyz]+)(?=([^"']*["'][^"']*["'])*[^"']*$)/g,
66 | getMethod = function (method) {
67 | return 'get' + (asUtc ? 'UTC' : '') + method;
68 | },
69 | handlers = {
70 | d: function () { return this[getMethod('Date')](); },
71 | dd: function () { return pad(this[getMethod('Date')]()); },
72 | ddd: function () { return getDayName.call(this, true); },
73 | dddd: function () { return getDayName.call(this); },
74 | f: function () { return pad(pad(this[getMethod('Milliseconds')](), 3), 6, true).substr(0, 1); },
75 | ff: function () { return pad(pad(this[getMethod('Milliseconds')](), 3), 6, true).substr(0, 2); },
76 | fff: function () { return pad(pad(this[getMethod('Milliseconds')](), 3), 6, true).substr(0, 3); },
77 | ffff: function () { return pad(pad(this[getMethod('Milliseconds')](), 3), 6, true).substr(0, 4); },
78 | fffff: function () { return pad(pad(this[getMethod('Milliseconds')](), 3), 6, true).substr(0, 5); },
79 | ffffff: function () { return pad(pad(this[getMethod('Milliseconds')](), 3), 6, true).substr(0, 6); },
80 | F: function () { var r = pad(this[getMethod('Milliseconds')](), 3).substr(0, 1); return r === '0' ? '' : r; },
81 | FF: function () { var r = pad(this[getMethod('Milliseconds')](), 3).substr(0, 2); return r === '00' ? '' : r; },
82 | FFF: function () { return pad(this[getMethod('Milliseconds')](), 3).substr(0, 3); },
83 | FFFF: function () { return handlers.FFF.call(this); },
84 | FFFFF: function () { return handlers.FFF.call(this); },
85 | FFFFFF: function () { return handlers.FFF.call(this); },
86 | h: function () { var h = this[getMethod('Hours')](); return h > 12 ? h - 12 : h; },
87 | hh: function () { var h = this[getMethod('Hours')](); return pad(h > 12 ? h - 12 : h); },
88 | m: function () { return this[getMethod('Minutes')](); },
89 | mm: function () { return pad(this[getMethod('Minutes')]()); },
90 | H: function () { return this[getMethod('Hours')](); },
91 | HH: function () { return pad(this[getMethod('Hours')]()); },
92 | M: function () { return this[getMethod('Month')]() + 1; },
93 | MM: function () { return pad(this[getMethod('Month')]() + 1); },
94 | MMM: function () { return getMonthName.call(this, true); },
95 | MMMM: function () { return getMonthName.call(this); },
96 | s: function () { return this[getMethod('Seconds')](); },
97 | ss: function () { return pad(this[getMethod('Seconds')]()); },
98 | tt: function () { return getDesignator.call(this); },
99 | x: function () { return getOrdinal.call(this); },
100 | y: function () { var y = this[getMethod('FullYear')](), s = y.toString(); return y < 10 ? y : s.substr(s.length - 2); },
101 | yy: function () { return pad(this[getMethod('FullYear')](), 2); },
102 | yyy: function () { var y = this[getMethod('FullYear')](), s = y.toString(); return y < 1000 ? pad(y, 3) : s.substr(s.length - 4); },
103 | yyyy: function () { return pad(this[getMethod('FullYear')](), 4); },
104 | yyyyy: function () { return pad(this[getMethod('FullYear')](), 5); },
105 | yyyyyy: function () { return pad(this[getMethod('FullYear')](), 6); },
106 | z: function () { var t = this.getTimezoneOffset(); return (t > 0 ? '-' : '+') + Math.abs(t / 60); },
107 | zz: function () { var t = this.getTimezoneOffset(); return (t > 0 ? '-' : '+') + pad(Math.abs(t / 60)); },
108 | zzz: function () { var t = this.getTimezoneOffset(); return (t > 0 ? '-' : '+') + pad(Math.abs(t / 60)) + ':' + pad((Math.abs(t / 60) % 1) * 60); }
109 | };
110 |
111 | return function () {
112 | var self = this, format, settings;
113 | // Evaluate arguments
114 | if (typeof arguments[0] === 'string') {
115 | format = arguments[0];
116 | settings = arguments[1];
117 | }
118 | else {
119 | format = null;
120 | settings = arguments[0];
121 | }
122 | // Store any passed settings
123 | if (settings) {
124 | if (typeof settings.asUtc !== 'undefined') asUtc = settings.asUtc;
125 | if (settings.days) days = settings.days;
126 | if (settings.daysAbbr) daysAbbr = settings.daysAbbr;
127 | if (settings.designator) designators = settings.designator;
128 | if (settings.format) defaultFormat = settings.format;
129 | if (settings.getDateOrdinal) getOrdinal = settings.getDateOrdinal;
130 | if (settings.months) months = settings.months;
131 | if (settings.monthsAbbr) monthsAbbr = settings.monthsAbbr;
132 | }
133 | if (!format) {
134 | format = defaultFormat;
135 | }
136 | return format.replace(findPatterns, function (match, group1) {
137 | var fn = handlers[group1];
138 | return typeof fn === 'function' ? fn.call(self) : match;
139 | }).replace(/["']/g, '');
140 | };
141 | }()),
142 |
143 |
144 | getDayName = function (asAbbr) {
145 | var n = this.getDay();
146 | return asAbbr ? daysAbbr[n] : days[n];
147 | },
148 |
149 | getMonthName = function (asAbbr) {
150 | var n = this.getMonth();
151 | return asAbbr ? monthsAbbr[n] : months[n];
152 | },
153 |
154 | getOrdinal = (function () {
155 | var os = 'th st nd rd th'.split(' ');
156 | return function () {
157 | var d = this.getDate();
158 | return (d > 3 && d < 21) ? os[0] : os[Math.min(d % 10, 4)];
159 | };
160 | }()),
161 |
162 | getDesignator = function () {
163 | return this.getHours() >= 12 ? designators[1] : designators[0];
164 | },
165 |
166 | pad = (function () {
167 | var p = '000000';
168 | return function (obj, len, fromRight) {
169 | return fromRight ?
170 | (obj + p).slice(0, len ? Math.min(len, p.length) : 2) :
171 | (p + obj).slice(-(len ? Math.min(len, p.length) : 2));
172 | };
173 | }());
174 |
175 | // Expose the formatting method
176 | // As a global function
177 | //window.formatDate = function (date, format, options) {
178 | // if(isNaN(+date) || !date.getDate) throw new Error('Not a valid date');
179 | // return formatDate.call(date, format, options);
180 | //};
181 | // Or as a method on the Date object
182 | Date.prototype.format = function (format, options) {
183 | return formatDate.call(this, format, options);
184 | };
185 | }());
186 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | const CopyWebpackPlugin = require('copy-webpack-plugin');
6 | const UglifyJS = require("uglify-es");
7 |
8 | const BUILD_DIR = path.resolve(__dirname, 'dist/options');
9 | const APP_DIR = path.resolve(__dirname, 'options.dev');
10 |
11 | function minifyFilesBeforeCopy(content, $path) {
12 | const result = UglifyJS.minify(content.toString());
13 | if (result.error) {
14 | console.error($path);
15 | throw result.error;
16 | }
17 | return result.code;
18 | }
19 |
20 | const config = {
21 | entry: {
22 | options: APP_DIR + '/app.jsx',
23 | vendor: ['react', 'react-dom', 'react-tag-autocomplete', 'react-toggle-button'],
24 | },
25 | output: {
26 | path: BUILD_DIR,
27 | filename: '[name].js'
28 | },
29 | module: {
30 | loaders: [{
31 | test: /\.jsx$/,
32 | exclude: /node_modules/,
33 | loader: 'babel-loader',
34 | query: {
35 | presets: ['es2015', 'react']
36 | }
37 | }, {
38 | test: /\.css$/,
39 | loader: 'style-loader!css-loader'
40 | }]
41 | },
42 | resolve: {
43 | extensions: ['.js', '.jsx'],
44 | },
45 | plugins: [
46 | new webpack.optimize.UglifyJsPlugin(),
47 | new HtmlWebpackPlugin({ template: path.join(APP_DIR, 'index.html') }),
48 | new ExtractTextPlugin('[name].css'),
49 | new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.bundle.js' }),
50 | new CopyWebpackPlugin([
51 | { from: path.resolve('options.dev/analytics.js'), to: 'analytics.js'},
52 | { from: 'manifest.json', to: '..' },
53 | { from: 'icon*.png', to: '..' },
54 | { from: 'inject.js', to: '..', transform: minifyFilesBeforeCopy },
55 | { from: 'utils.js', to: '..', transform: minifyFilesBeforeCopy },
56 | { from: 'contentscript.js', to: '..', transform: minifyFilesBeforeCopy },
57 | { from: 'background.js', to: '..', transform: minifyFilesBeforeCopy },
58 | ]),
59 | ],
60 | };
61 |
62 | module.exports = config;
63 |
--------------------------------------------------------------------------------