mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Jekyll Initial Commit
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36" version="6.0.1.8" editor="www.draw.io" type="device"><diagram name="Page-1">7Vpbc6M2FP41nrYP6wFksP1oO0k7nd1O2nTa7qMMCtZGICpE4vTXVwIJkCVnnQbbWU/z4MDRBen7zk0HRmCVbX9ksNh8ogkio8BLtiNwNQqC6dwTv1Lw3AjCYNoIUoaTRuR3gjv8D1JCNS6tcIJKoyOnlHBcmMKY5jmKuSGDjNEns9s9JeZTC5giS3AXQ2JL/8QJ3zTSWeh18p8QTjf6yb6nWtYwfkgZrXL1vFEA7uu/pjmDei7Vv9zAhD71ROB6BFaMUt5cZdsVIhJaDVsz7mZPa7tuhnJ+yIBJ1Ix4hKRCesn1wvizBkOMELiLm6VYbSGFMaGVmGD5tMEc3RUwlsInoQlCtuEZEXe+uFRTI8bRdu/6/HbXQpkQzRBnz6KLGhBpjdCK1Nw9daQEGvpNn5C56giVIqTtzB0Y4kLh4cYGOKBIhJqoW8r4hqY0h+S6ky5r7pGcwTPBQFvM/5LicajuPuuWXCys1yRvP6sJSg4ZX0iNrmGHZYljLb7BpJ08T+xOQtjr8gVx/qwsDVacClG3g4+UFqpfyRl9QCtKKKv3DLz6r23R9tARbLEpIKMVixVmCkWx4hSpXoHSOgnni1rAEIEcP5pW+RZOw/kxOPV7jHrjaWiQalA6Bt8IqTvU/BeWQ+9EpEb+8UkNTFLHfmTyGrbtt4hhsQXEviETHoDtJpKcgO3AiliLohCCO8REqBkFERErWib4UVym8vL7X0R6MgpWcp/bgqGyHH8pf9AdxeN6fS1FOijEOdCyot7eEAemZojzdZbRC3L+xBXkZt7b0Zx8Pf4fH4I2fVIQABcEMxcEkwEgiCbHdx8vhYTatVyil/AnjtB/Ii/huxTbcg2LJMP5SObIN+JXHmOYWAbN9zkHewLpet6JI/F1dqwdSTCzrShwWFE0hBGF5zWi6cUaUXhGIwotI1pRBmWX62yNkvIdaDmYOWLFsbTcn9vJRyWeGni3jD5i4T7OAUmkE2Bt+L59TPY9YEMCBoCkXW4HySeIiUrIsNj1OQAJDECmDhVxlQ2GwCME53WEl+oHdc3gLCfMwC6T/UzXwta9XyskJI5Dx0OF3tcxY7aTYx/qN6cDGAVwBBKCxWa+kxjeib1biIhtcRMEU8dymkvLuRe6uiOCBKe51GkxvzyFLyVIOIZkoRoynCS12blQ34Pra4qWhxxnpg6kgyGQjo7hfuyi5WW4mDe4DjX0lmIxY0t9sHOYB7ucNi5Njepoff1EjR+0Jqr1o93PYSWAo5TPTJVpg5Sjzn056mRGLDC3I1ZzLD1BxJrYdZz/X14ci9TwRKQC+/hxw2jtNq7zRJ5CSJXivHTlIwzBWHS8YSiptoemJfqFXxOLE1qt6xbf0BV/TyQd9k3g7rkvmJzw3DeZWsCrYpFQNFU+imAmd52vy8JdK1rRLKtyzJ/39f1qtekTzGHa1bgPJK/KyCKWC+0yoY9wjcgtLXFd7AJXa8o5zUQHIhuW7UvsniWp19h2NsXpDtntO21pd4V+GyKVA+epEg+gEMAMkD6w9QEcSx1mdjrbI9eiDWXrujLwrmkbnhLgOHcfixKdYvco+Q0l2C7HaHgTyGEp4N13Dhj4O4adigSIbGjmcxsa3x8AG/2sgXMBqyRxifE+1J8fnaH8GjpKazRP6dV+p3FqrTYNPnAWHl3v7QZR68CCx3C+PXyivyv5RdVSFhU+qPrAQvQg6J53rbuR975JrexpZMOHslZPOYs/Kbb2LLpS/jskD+LfgsWSj5hXjfgKw5TBrBcUmseZS3ghJTjz9v4QLWNfUkiKDXzlNi6p1mNmIbZbb2VvrDTL3L39LrA55nffXoLrfwE=</diagram></mxfile>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36" version="5.7.1.2" editor="www.draw.io" type="device"><diagram>7Zxbb6M4FIB/TV5WmgjMNY+TtLP70JWqrVa781Q54IB3DI7AmTTz69cGc3VI0iYwKEMVVfHh2Bh/to99jsnMWEVvvydwG/5JfURmQPPfZsbDDABnofH/QnDIBZbr5IIgwX4u0ivBC/6BpFDmC3bYR2lDkVFKGN42hR6NY+SxhgwmCd031TaUNO+6hQFSBC8eJKr0H+yzMJe6llbJ/0A4CIs765q8sobetyChu1jebwaMTfaXX45gUZbUT0Po031NZDzOjFVCKcu/RW8rRETTFs2W5/vScbWsd4JidlEGK8/xHZKdfPYVjSKRO68fOxRtku5xRGDMU8uQRYQLdf51Q2P2IpU0nvZCTPwneKA7cf+U8fYoUsuQJvgH14dFZn45YZI+sEVpmJAVJTThgpjm9yozvYjC5G0SlPJsz8Vz6qXoCaasqAolBG5TvM4qJ1QimAQ4XlLGaCSViqf6UrtzBc1YyuZBCUNvnW2sl+T4gEA0Qiw5cBWZARgSthwMjmzzfdWzdFOqhLVeBVwphLI3B2XRFVH+RULtAGwrgPmlNlv+cKzJNWUJ/YZaLI7ggQQHMU8StBEliIbCfBx9lmJGt6KwLfRwHDxlOg9mJflLPq0Q7UPM0AuXizrt+bTCZZSXtyHZAAmx76NYcKYMMrgue9yW4phlLWQt+Ye32UqbWzOLP9eKp/UqzT9CPWErGvPngzjjh3iP2SPRay6EbR2nLekC+0K69g3gOgrcNfUPE97+8FpgOLyGqeCFaYrY6zSC+0TsuAOOYFdFvGPc5k2Me2Wsa+ZwkIty65A9hvnjTYh7RFwsuwZBDBTEfHHLdhPhPglbA661TEMhnFd1mqn7hewOuOIy1RWXlyDIkP8K1R3xRPl2uyZtwEWXqTo9dlt/otw/ZWPAVZelKyiRH6DCWcUfNaQBjSF5rKTLzHWHfNl8DeYwYZ+Fa7Him8mE40iqo9gvNDzCd2rYy4VSRRTzH2LsID1efKFPBceyIk9U9IgjXWwGDC37K68UXkk9uwNvmX9FHeaOVaS/yot5G4gHPwONNxTdJR5qWjv+hAE6AzdBBDL8vXmDY+Rk1mfRKSv3WOk7lZ3EMFtur7wOMlfFnzc1PNTUZF/vvE/pcy0My8KqF3dWv6hXl76uWaf0+Ze8xlXnLdvwsv6sLjFzinzaWl/i88kcpJ2Tw0NtwvJEqcmRKSviE002TghcI7Isnd3v8pxKp72syaxs53qftE67VD9pcwAarf1JFv7Rzlio0M0mRWzWnmfeh8pWd/XqZHTXgLT5YsR4Fgqe334tPHz8LIA1IiSG0Zuxrkxx3V7rTXstLXplrLW+jPUbZnVbzZNfy+WDtOOlUc+uzK0LzbhqxcExK+7ewoq/1/g6RtM46tZpY2o42in9q42poYbFcr/rPRrTfHCdmKuBaZsjnq7Vhc+9WdMzhMR0bbl2E5ExJkSqc+zeLOrZQWQb5pgsKlCR/J3yJmxTmY51nLaoncTt1rEOw7rwXEcZmLjGuwFUT+Xkh77KdwU6BvhPONdhqBbPx+mWwMNrDCM0Ye4P85DnO4Dqh56iDUNAHvKEB1B3GlOwYQjIwx7xsBWUwwUbhnde1D0Xc8deyPQzSjBvOrFQb3o0ysS1Ho2CX92jkVvLm8cl7Jb/3wQ9xSVa68jiTFhnnOFKfd04E/e4St0Et3XUmKqvdkNgENyjnyafRE5sMXUTyOYYxRbTVvcg97brP4tkYerN0TgmPur68hfjkznOzJbjbEy+TdO95bqhdhJA082mzdUcpxC0zfSIzzZ8cJUA1FVC12rx4m5w8bkT9bRvRH2UwLsM1Jsd4aRygjRtqxn6GdUMqW7O7i20cBaQu9BbsfpREVJfcbo7G3aaUBard8a07nOcWxqtEQfrP2Z9nH72o7rzsf3oRwCr/ufP2QsrCvcpenS6Z3SOarMF8+KXgovTmtcsURw1NjgFj67yRXaN+Z8QPHLUXTk7bKegUY94hwwaOeqmnrdh9Dox7pfxkDEj58iPNgjG0yTdJ+FBA0aO6tfepWh67btnxEO+E+yqjqApvD8I5SHfC3Z1dSBP8f0hKPf4YjBPVr/BlW+gq985Mx7/Bw==</diagram></mxfile>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36" version="6.0.1.3" editor="www.draw.io" type="device"><diagram name="Pre-Moderation">7VlNc9sgEP01nmkP9ejD+vAxdpP20M6kk0PbI5GIRIqEilFs99d3EWAJSc4krp24nebgEQ9Y4O1bdqVM/GWx+cBRlX9mKaYTz0k3E//9xPOiuQO/EtgqIPAiBWScpApyW+CG/MIa1POymqR4ZQ0UjFFBKhtMWFniRFgY4pyt7WF3jNqrVijDA+AmQXSIfiWpyBUaB06Lf8Qky83KrqN7blHyI+OsLvV6E8+/a/5Ud4GMLT1+laOUrTuQfznxl5wxoZ6KzRJTSa2hTc272tO72zfHpXjShEDNeEC0xmbLzcbE1pCxzonANxVKZHsN/p74i1wUFFouPGoDmAu82bsLd3c2kAxmBRZ8C0PMBKMXLRfXmFi35LuG/LxDvB/MtdO1w7Od7fbQ8KDPPc6BN6AgYUUht+6FFBZapOQBHjP5+KbE67cGB8OdrgFvsAroFRqL0zPo2QR6Ro9dAr0RAnfgnxAYP4dAVFWcPeD0HFmcOTaNvjt7QRrnz6GR43u4+P4OGt3wJdXouiNXmuLojkkyO7SEP2tmOt6tmjR0AQPcqNq0nS2nysptH6gNcM3xuwJyIUeCsLLjmbo/BbCBGcDUBve4EBwibF+tBGc/8JJRxgEpWSn9e0co7UGIkqyEZgIOxIAvpHsJpLsL3VGQNKX7xHEERfi2IOIRPcxH9HAUOQwTGk4h2esm4yJnGSsRvWzRRZPBsbTg2IzjDRHfJDyNAt38rkcBFXzb7ZNt07kSiIsLWZlIN1C0WpHEwFeE7syXqRmkXQeI7pdm7rEQW10uoVowgNoDfGKsGhUGFCFO87frMUVNG/CSk8edCxSymidmlCnLEM+w6GJDEXBMISAebPt/5NNwEOKfO2HnfKkx4P9Q/HhOPA2sEJqHwxByghOFUDRyo/ZjqlVuK++9VD+uSBNkbjfCpoEVY1aAqYA7TMjxiI7jcWd0uB4rRg32ZLnrFa4ZadLSeCkXxD0Pqs3rSd3Svmcn6mXhmd8zpI48MNSoYXfqpwlkWAEOBHL6siOcvepbhDPgYGnKN+dGcIyKf+lGCkOb7PlISndOlNK9YYV3RVGWkTJrvgk0rK/gMWXwUzLpgVuuekWOC/2eLltMQfLVvJtAfkICmQ7cBS/rFW58IF6mkA56IRyOCHq0jnaOIWj//G79qeNE9s3vx6EBrjEncEoZDk1+OCwfeCP5IDijfBAeng9sO6dLB97sHJQzi2Ld7gtjXxVxeBFhPqN1VTP/L5pnieYJXyJft8g8rjzOqcY8WB5+78PtLDqWPqDZfgVXw9v/NPiXvwE=</diagram><diagram name="Post-Moderation">3VlNc6M4EP01rpo9xIXEh/Ex9iYzh52qbOWwu0cNKJgZQB4hYnt+/TRGAiRBQhLbmZ0cUqgltcTr160nPHPX+f4jJ9vNZxbTbIadeD9z/5xhvFg68L82HBqDjxeNIeFp3JhQZ7hPf1BplPOSKo1pqQ0UjGUi3erGiBUFjYRmI5yznT7sgWX6qluSUMtwH5HMtv6TxmLTWEPf6eyfaJps1MrIkT1fSPQt4awq5Hoz7D4c/5runChfcny5ITHb9Uzuzcxdc8ZE85Tv1zSroVWwNfNuR3rbfXNaiCkTcDPhkWQVVTs+7kscFBa7TSro/ZZEdXsH4Z65q43IM2gheJQOKBd0P7oJ1L4aMIaynAp+gCFyAlboSbbgULZ3HfYokLZND3dXBYTIeCet7+6d4UG+9jAErgVBxPK83joOMlhoFaeP8JjUjx8KuvtD2cFxr8vCDVYBukJjdX4EXR3AFqw+gHgAwNb4FgC9lwBItlvOHmn8K6LoYV+D0cOXhDEYSMUGogdWY9lDJfheMdVxVR6r5zUMQIvtvuvsIG28fDENlTLcsVJc5VDDOREpK3qRqcw5YLP8gK3Z4UgIISBCj1UpOPtG1yxjHCwFK+r4PqRZZphIliYFNCMIIAX7qg5vCmX6WnbkaRxnY+Q4ASMCPa+G6tJygA+noMPCToUYzijZZFxsWMIKkt101tXx4KG1A0cHnO5T8W9tni982fxPjgIk+KHfV7dVZykIF9f1gVpHISNlmUbKfJtmrfsiVoNk5MAi+2s3X6kQB3nKk0owMHUv8Bdj20FewNnpHP/aHnUWd/leY6LFtmQVj6iWUbDZhArN9JKzyR2kwFtCG1qZ/rmXfM7fFQX7b5RFAOJcr6zYD+xMcvzzZNLyeY3TI3DH8lGonyamyjU0dzzcT7a5H7qyfUd5Cu9RR0NPwitn7mA9DedhM602mPMmJIFnJwFyXhpBd6kXQk+B2oufP1AIn9JncrE7lh5Pt70ecHVxMPOteTU5qS9sDT+BoSl9z3DUAGI5OpKpBWASvxSc7yuiA9/QgMrFc+LFQyfIMYQsDNZKBDr3glOS/04VLTCEwXJAGDhnEgbIvrBcpp7ptUyTDnrBOhawCaXJPUVpQp4eChxeqDQFry1NC8OPe77K5NnMOIWI7GtI9L+UiRa/dGZim5l4mIWcZqDbHnXvb0pv+x54m5EkgYhgp62p73pLNhIuGNByZ7slo8X7VD/Hqn6vY5YqcFrRC4YBn1jBJlNwuBwZ346C5SvLWmj4OaPgshX9E1+cOP1KI/GLfnFy9XuRf9EPd3iCcL1MLrVKAulKAqHwhVef59Jv4JvASGW/TPaZFytTDEzNPlMGuaY6OV36YVvrX4Q2cKGGzO9TJ3SUEhm9UBvS9PWFe0ASNHVoXJkaV1B34JZwGmVqruSZtXcyi/yRLZ+BRfidWDT5GnNSrnhPUwUZAXTORhVzJdPFZKoEI1t+M1Wg2f0A2gzvfmR2b34C</diagram></mxfile>
|
||||
@@ -0,0 +1,22 @@
|
||||
# Debug
|
||||
How we debug errors at Coral
|
||||
|
||||
## React Debugging
|
||||
For debugging React
|
||||
|
||||
### React Developer Tools
|
||||
Another amazing tool for debugging React Applications. You can see where the props are, and much more.
|
||||
|
||||
[React Developer Tools Extension](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)
|
||||
|
||||
|
||||
## Redux Debugging
|
||||
For debugging Redux
|
||||
|
||||
### Redux Devtool Extension
|
||||
Redux Devtool is an amazing debug tool. You can easily see what' happening with the state, the payloads, and more.
|
||||
|
||||
[Redux Devtool Chrome Extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en)
|
||||
|
||||
[Redux Devtool Github Repo](https://github.com/zalmoxisus/redux-devtools-extension)
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
# InmutableJS
|
||||
InmutableJS is a library from Facebook that provides a series of inmutable data structures. They are always immutable. The reference to them can change but the data inside of them cannot which means you can build predictable and reliable state models.
|
||||
We use ImmutableJS in Talk and it becomes really easy to manage Talk’s application state. [Immutable.js](https://facebook.github.io/immutable-js/)
|
||||
|
||||
More about Immutable Data and React:
|
||||
[React.js Conf 2015 - Immutable Data and React - YouTube](https://www.youtube.com/watch?v=I7IdS-PbEgI&feature=youtu.be)
|
||||
|
||||
## Why ImmutableJS?
|
||||
- __Immutable Data is faster__
|
||||
* Tracking mutation and Maintaining state is difficult
|
||||
* Encourages you to think differently about how data flows through your application
|
||||
|
||||
## Getting Started
|
||||
ImmutableJS API is pretty expense. We will try to cover the basics and more to show its power.
|
||||
|
||||
ImmutableJS provides many Persistent Immutable data structures including: `List()`, `Stack()`, `Map()`, `OrderedMap()`, `Set()`, `OrderedSet()` and `Record()`.
|
||||
|
||||
We will cover the most common data structures. `Map()` , `List()` and `Record()` and also we will describe the behaviour of `Seq()` with `Range()`
|
||||
|
||||
|
||||
## Map()
|
||||
- [Map()](https://facebook.github.io/immutable-js/docs/#/Map)
|
||||
* Read values
|
||||
* [get()](https://facebook.github.io/immutable-js/docs/#/Map/get)
|
||||
* [has()](https://facebook.github.io/immutable-js/docs/#/Map/has)
|
||||
* [first()](https://facebook.github.io/immutable-js/docs/#/Map/first)
|
||||
* [last()](https://facebook.github.io/immutable-js/docs/#/Map/last)
|
||||
* Read deep values
|
||||
* [getIn()](https://facebook.github.io/immutable-js/docs/#/Map/getIn)
|
||||
* Change Values
|
||||
- [set()](https://facebook.github.io/immutable-js/docs/#/Map/set)
|
||||
* [merge()](https://facebook.github.io/immutable-js/docs/#/Map/merge)
|
||||
* [update()](https://facebook.github.io/immutable-js/docs/#/Map/update)
|
||||
* [clear()](https://facebook.github.io/immutable-js/docs/#/Map/clear)
|
||||
* [delete()](https://facebook.github.io/immutable-js/docs/#/Map/delete)
|
||||
* Change deep values
|
||||
* [setIn()](https://facebook.github.io/immutable-js/docs/#/Map/getIn)
|
||||
* Conversion to JavaScript types
|
||||
* [toJS()](https://facebook.github.io/immutable-js/docs/#/Map/toJS)
|
||||
* [toArray()](https://facebook.github.io/immutable-js/docs/#/Map/toArray)
|
||||
* [toObject](https://facebook.github.io/immutable-js/docs/#/Map/toObject)
|
||||
* Member
|
||||
* [size](https://facebook.github.io/immutable-js/docs/#/Map/size)
|
||||
|
||||
|
||||
Creates a new Immutable Map. An Object graph. [Map - Immutable.js](https://facebook.github.io/immutable-js/docs/#/Map)
|
||||
|
||||
```js
|
||||
|
||||
const data = {
|
||||
‘one’: {
|
||||
title: ‘One’,
|
||||
value: 1
|
||||
},
|
||||
‘two’: {
|
||||
title: ‘Two’,
|
||||
value: 2
|
||||
}
|
||||
}
|
||||
|
||||
let map = Inmutable.Map(data)
|
||||
```
|
||||
|
||||
### get()
|
||||
Returns the value associated with the provided key, Since inmutable data cannot be mutated they create a new reference to the new data.
|
||||
[get() - Immutable.js](https://facebook.github.io/immutable-js/docs/#/Map/get)
|
||||
|
||||
```js
|
||||
map.get(‘one’).title
|
||||
```
|
||||
|
||||
|
||||
```js
|
||||
let obj = { 1: “one” };
|
||||
Object.keys(obj); // [ “1” ]
|
||||
obj[“1”]; // “one”
|
||||
obj[1]; // “one”
|
||||
|
||||
let map = Map(obj);
|
||||
map.get(“1”); // “one”
|
||||
map.get(1); // undefined
|
||||
```
|
||||
|
||||
### getIn()
|
||||
To get data from a deeply nested structure.
|
||||
[getIn() - Immutable.js](https://facebook.github.io/immutable-js/docs/#/Map/getIn)
|
||||
|
||||
*With a Map()*
|
||||
```js
|
||||
|
||||
let map = Inmutable.Map({
|
||||
title: ‘Todo One’,
|
||||
text: ‘Do todo’
|
||||
category: {
|
||||
title: ‘Some category’,
|
||||
order: 1
|
||||
}
|
||||
})
|
||||
|
||||
map.getIn([‘category’, ‘title’]) // ‘Some Category’
|
||||
|
||||
```
|
||||
|
||||
### length - size
|
||||
To get the size of a Map() or a List()
|
||||
```js
|
||||
map.size
|
||||
```
|
||||
|
||||
### set()
|
||||
```js
|
||||
map.set(‘three’, {title: ‘three’, value: 3})
|
||||
```
|
||||
|
||||
### delete()
|
||||
```js
|
||||
map.delete(‘three’, {title: ‘three’, value: 3})
|
||||
```
|
||||
|
||||
### update()
|
||||
```js
|
||||
map.update(‘one’, item => ‘’)
|
||||
```
|
||||
|
||||
### clear()
|
||||
Returns a new Map containing no keys or values.
|
||||
|
||||
```js
|
||||
map.clear()
|
||||
```
|
||||
|
||||
### merge()
|
||||
Returns a new Map resulting from merging the provided iterables.
|
||||
```js
|
||||
|
||||
let mapX = Inmutable.Map({a: 10, b: 20, c: 30})
|
||||
let mapY = Inmutable.Map({a: 10, b: 20, c: 30})
|
||||
|
||||
mapX.merge(mapY) // { a: 50, b: 40, c: 30, d: 60 }
|
||||
```
|
||||
|
||||
### Querying Methods
|
||||
|
||||
#### has
|
||||
Returns a boolean if it finds the id key
|
||||
|
||||
```js
|
||||
map.has(item.id)
|
||||
```
|
||||
|
||||
#### first
|
||||
Returns the first element of a Map
|
||||
```js
|
||||
map.first()
|
||||
```
|
||||
|
||||
### Iteration Methods
|
||||
We can use methods like `.filter`, `.map`, `.reduce` . However it’s not recommended to use `.forEach` since it can mutate the data producing side effects.
|
||||
|
||||
#### groupBy
|
||||
|
||||
Returns the first element of a Map
|
||||
```js
|
||||
items.groupBy(item => {
|
||||
return todo.completed
|
||||
});
|
||||
```
|
||||
|
||||
### Working with Subsets of a Map()
|
||||
|
||||
#### slice()
|
||||
Returns the last two items of a Map()
|
||||
slice(<from>, <to>)
|
||||
```js
|
||||
items.slice(items.size-2, todos.size);
|
||||
```
|
||||
|
||||
#### takeLast()
|
||||
Returns the last two items of a Map()
|
||||
```js
|
||||
items.takeLast(2);
|
||||
```
|
||||
|
||||
#### butLast()
|
||||
Returns the last item
|
||||
```js
|
||||
items.butLast();
|
||||
```
|
||||
|
||||
#### rest()
|
||||
```js
|
||||
items.rest();
|
||||
```
|
||||
|
||||
#### skip()
|
||||
Returns a Map() skipping the first 5 items
|
||||
```js
|
||||
items.skip(5);
|
||||
```
|
||||
|
||||
#### skipUntil()
|
||||
Returns a Map() skipping until it finds the value
|
||||
```js
|
||||
items.skipUntil(item => item.value === 1);
|
||||
```
|
||||
|
||||
#### skipWhile()
|
||||
Returns a Map() up until it finds 1 included.
|
||||
```js
|
||||
items.skipWhile(item => item.value === 1);
|
||||
```
|
||||
|
||||
### Equality Methods
|
||||
|
||||
#### is()
|
||||
```js
|
||||
let mapX = Inmutable.Map({a: 10, b: 20, c: 30})
|
||||
let mapY = Inmutable.Map({a: 10, b: 20, c: 30})
|
||||
|
||||
Immutable.is(mapX, mapY); // true
|
||||
```
|
||||
|
||||
### FromJS
|
||||
|
||||
#### Object to Map()
|
||||
Creates deeply nested Map() from a plain Javascript Object
|
||||
|
||||
```js
|
||||
let object = {a: 10, b: 20, c: 30};
|
||||
|
||||
Immutable.fromJS(object); // Map()
|
||||
```
|
||||
|
||||
|
||||
#### Array to List()
|
||||
Creates List() from a JS Array
|
||||
|
||||
```js
|
||||
let array = [10,20,30];
|
||||
|
||||
Immutable.fromJS(object); // List()
|
||||
```
|
||||
|
||||
#### Usage of the reviver function
|
||||
The reviver function takes a key and a value. Converting JS to Map() or List()
|
||||
|
||||
```js
|
||||
let array = [10,20,30];
|
||||
|
||||
Immutable.fromJS(array, (key, value) => {
|
||||
return value.toMap();
|
||||
}); // Map()
|
||||
```
|
||||
|
||||
*Note: the getIn will be index based instead of object based if it comes from an array*
|
||||
|
||||
### List()
|
||||
|
||||
Most of the __Map()__ methods can be used with __List()__
|
||||
But there are some differences.
|
||||
|
||||
### Differences between the Immutable Map() and List()
|
||||
List() have the same methods that a JS Array has. But instead of mutating the array it returns a new one.
|
||||
|
||||
Usually we wouldn’t use the push method in immutable data structures but with Immutable.List()s push methods are safe to be used.
|
||||
|
||||
```js
|
||||
let list = Immutable.List()
|
||||
list.push(3)
|
||||
list.toArray() // [3]
|
||||
```
|
||||
|
||||
#### get() and getIn()
|
||||
The get method with Map() is _key_ based and with List() is _index_ based.
|
||||
|
||||
```js
|
||||
// get()
|
||||
let list = Immutable.List();
|
||||
list.push(3);
|
||||
list.get(0); // 3
|
||||
|
||||
let map = Immutable.Map();
|
||||
list.set('active', true);
|
||||
list.get('active'); // true
|
||||
|
||||
// getIn()
|
||||
let map = Inmutable.List([10, 20, 30, [40, 50]])
|
||||
map.getIn([3, 1]) // 50
|
||||
```
|
||||
|
||||
#### of()
|
||||
We can create a __List()__ by using the _of_ method
|
||||
|
||||
```js
|
||||
const items = [];
|
||||
const list = Immutable.List.of('red', 'green', 'blue');
|
||||
```
|
||||
|
||||
*Using the spread operator:*
|
||||
```js
|
||||
const items = ['red', 'green', 'blue'];
|
||||
const list = Immutable.List.of(...items);
|
||||
```
|
||||
|
||||
### Sequences
|
||||
Represents a sequence of values. [Seq() - Immutable.js](https://facebook.github.io/immutable-js/docs/#/Seq)
|
||||
|
||||
- Sequences are immutable — Once a sequence is created, it cannot be changed.
|
||||
- Sequences are Lazy
|
||||
|
||||
Creating sequences with _of()_
|
||||
```js
|
||||
let range = [0, 1, 2 ... 999]
|
||||
let sequence = Immutable.Seq.of(...range)
|
||||
```
|
||||
|
||||
For Example: the following performs no work, because the resulting of the sequence values are never iterated:
|
||||
|
||||
```js
|
||||
|
||||
let operations = 0;
|
||||
|
||||
let squared = sequence.map(num => {
|
||||
operations++;
|
||||
return num * num;
|
||||
})
|
||||
operations; // 0
|
||||
|
||||
// Now using the sequence
|
||||
squared.take(10).toArray();
|
||||
operations; // 10
|
||||
|
||||
```
|
||||
|
||||
Once the sequence is used, it performs only the work necessary. It will return it only when you ask for them.
|
||||
|
||||
This is really powerful because it doesn’t produce an overflow with infinite an infinite range.
|
||||
|
||||
```js
|
||||
let squaredRange = Immutable.Range(1, Infinity);
|
||||
|
||||
squaredRange.size; // Infinity
|
||||
|
||||
first1000squared = squaredRange
|
||||
.take(1000)
|
||||
.map(n => n * n);
|
||||
|
||||
first1000squared.size; // 1000
|
||||
```
|
||||
|
||||
__Seq()__ allows for the efficient chaining of operations
|
||||
|
||||
```js
|
||||
let squaredOdds = Immutable.Range(0, Infinity)
|
||||
.filter(n => n % 2 !== 0)
|
||||
.map(n => n * n)
|
||||
.take(1000);
|
||||
|
||||
console.log(
|
||||
squaredOdds.toArray()
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
You can fin this example here: [Sequences - JS Bin](http://jsbin.com/nilekuj/edit?js,console)
|
||||
|
||||
[image:12FACC54-0BAF-4C93-A782-F77DB7CD04D3-813-00001ABD60F45CC4/Screen Shot 2016-12-22 at 8.23.33 AM.png]
|
||||
|
||||
## Memoization with Immutable JS
|
||||
Immutable JS provides advanced memoization.
|
||||
|
||||
```js
|
||||
const seq = Immutable.Range(1, Infinity)
|
||||
.map(n => ({
|
||||
value: n
|
||||
}))
|
||||
|
||||
console.time(‘First Run’);
|
||||
seq.take(1000);
|
||||
console.timeEnd(‘First Run’); // First Run: 0.577ms
|
||||
|
||||
console.time(‘Second Run’);
|
||||
seq.take(1000);
|
||||
console.timeEnd(‘Second Run’); // Second Run: 0.165ms
|
||||
```
|
||||
|
||||
|
||||
### Play with Immutable JS
|
||||
[JS Bin - Collaborative JavaScript Debugging](http://jsbin.com/nilekuj/edit?js,console)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Experimental plugins
|
||||
|
||||
Talk plugins are, in essence, small programs that hook into the core application in a variety of ways. Ultimately, this code can do anything that javascript is capable of. In addition, plugins can import any core code to hook into talk at any level.
|
||||
|
||||
If you want to write plugins that integrate with core code beyond the api described in [PLUGINS.md](PLUGINS.md), please keep the following things in mind:
|
||||
|
||||
* core code may change and break your plugin
|
||||
* you may introduce inefficiencies with your plugin that could hurt performance/crash Talk
|
||||
* you may cause bugs in other areas of Talk
|
||||
|
||||
If you'd like to build a supported plugin but don't have the hooks you need, please file an issue on this repo and we can discuss deepening the supported plugin api!
|
||||
|
||||
With that said, here's some of the prime experimental integration points:
|
||||
|
||||
## Reducers and Actions : Redux
|
||||
|
||||
Talk is powered by Redux and our plugins can too! Our plugins can have their own reducers and actions.
|
||||
|
||||
```js
|
||||
import MyButton from './MyButton';
|
||||
import reducer from './reducer';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentDetail: [MyButton],
|
||||
},
|
||||
reducer
|
||||
};
|
||||
```
|
||||
|
||||
## Import Actions from Talk
|
||||
We can easily trigger `Talk` actions in our plugin Components.
|
||||
|
||||
```js
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {addTag, removeTag} from 'coral-plugin-commentbox/actions';
|
||||
|
||||
class MyButton extends Component {
|
||||
render() {
|
||||
return <button onClick={this.props.addTag('MY_TAG')}>My Button</button>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({commentBox}) => ({commentBox});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({addTag, removeTag}, dispatch);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
|
||||
```
|
||||
|
||||
## ESlint and Babel
|
||||
In talk we use `eslint:recommended` and Babel with the latest ECMAScript Features. But you can use your own!
|
||||
While building your plugin you need to specify a `.eslintrc.json` file and a`.babelrc` file.
|
||||
|
||||
#### `.eslintrc.json`
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
#### `. babelrc `
|
||||
```json
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
````
|
||||
@@ -0,0 +1,270 @@
|
||||
# Plugins
|
||||
We can build plugins to extend the functionality of Talk.
|
||||
|
||||
This guide is a walkthrough of our plugin architecture and components that we provide that allow you to build on top of Core coral components without having to understand the concepts there in. It is organized into three sections:
|
||||
|
||||
* [Plugin Architecture](#plugin-architecture)
|
||||
* Using our building block components
|
||||
* [Reactions](#reactions)
|
||||
* [Styling](#styling-plugins)
|
||||
|
||||
Advanced users will quickly realize that our plugins have complete access to core code. If you would like to write advanced plugins that reach outside of our published API as described in this document, please see [our notes on experimental pluginss](PLUGINS-experimental.md).
|
||||
|
||||
Under the hood our plugins are powered by *React*, *Redux* and *GraphQL*. We can also build them with simple vanilla javascript.
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
The plugins live in the `/plugins` folder. Each plugin must have an `index.js` file and two folders `client` and `server`.
|
||||
|
||||
### The Client Folder
|
||||
The frontend of our plugin lives inside the `client` folder. The `client` folder must have an `index.js` file that exports the configuration of our plugin.
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── client/
|
||||
│ └── index.js <-- index for client side functionality
|
||||
├── server/
|
||||
└── index.js <-- base plugin index
|
||||
```
|
||||
|
||||
For now our base plugin `index.js` file should look like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// We will add more here later.
|
||||
};
|
||||
```
|
||||
|
||||
### Creating a Component
|
||||
|
||||
We can add our components (or any other javascript code) within the `client` folder.
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── client/
|
||||
│ ├── MyComponent.js
|
||||
│ └── index.js
|
||||
├── server/
|
||||
└── index.js
|
||||
```
|
||||
|
||||
Our component could look like this:
|
||||
|
||||
```js
|
||||
import React, {Component} from 'react';
|
||||
|
||||
class MyButton extends Component {
|
||||
render() {
|
||||
return <button>My Button</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default MyButton;
|
||||
```
|
||||
|
||||
Here we create a component that renders a `button`. Now that we created our component we need to specify where it should get injected within Talk!
|
||||
|
||||
To tell Talk where that Component should get injected we need to specify which *Slots* to insert it into.
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
export default = () => <button>My Button</button>;
|
||||
```
|
||||
|
||||
### Slots
|
||||
In Talk we have defined specific *Slots* where we can inject components.
|
||||
|
||||
Here is how we specify our slots config in `my-plugin/index.js`
|
||||
|
||||
```js
|
||||
import MyButton from './MyButton';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentDetail: [MyButton]
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Here I’m specifying that the MyComponent Component will take place within the `commentDetail` in Talk.
|
||||
|
||||
`commentDetail` it’s a specific slot in the CommentStream. It means that it will be embedded inside de comment detail.
|
||||
|
||||
Slots properties take an`Array` so we can add as many components as we want.
|
||||
|
||||
## Building Blocks (TBD)
|
||||
|
||||
`Note: the concepts in this section are still to be implemented. Code samples are for discussion and may change.`
|
||||
|
||||
In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order components that expose a simple api.
|
||||
|
||||
## Reactions
|
||||
|
||||
Reactions provide users the ability to 'like', 'respect', etc... comments.
|
||||
|
||||
Note: some server side work will need to accompany this client side component. See the like and respect plugins as examples.
|
||||
|
||||
### Building Reactions
|
||||
|
||||
#### Our `client/index.js` :
|
||||
|
||||
```js
|
||||
import LoveButton from './LoveButton';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentReactions: [LoveButton]
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
In this example we add our reaction component to the `commentReaction` Slot
|
||||
|
||||
#### Our Reaction component:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import {withReaction} from 'coral-plugin-api';
|
||||
|
||||
class LoveButton extends React.Component {
|
||||
handleClick = () => {
|
||||
const {
|
||||
postReaction,
|
||||
deleteReaction,
|
||||
alreadyReacted
|
||||
} = this.props;
|
||||
|
||||
if (alreadyReacted()) {
|
||||
deleteReaction();
|
||||
} else {
|
||||
postReaction();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {count} = this.props;
|
||||
return (
|
||||
<button onClick={this.handleClick}>
|
||||
<span>Love</span>
|
||||
<span>{count > 0 && count}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withReaction('love')(LoveButton);
|
||||
```
|
||||
|
||||
|
||||
This feature introduces `withReaction` HOC. `withReaction` takes, as argument, a reaction string and it allows our component to receive specific props for handling reactions.
|
||||
|
||||
* `postReaction` - Posts the reaction
|
||||
|
||||
* `deleteReaction` - Removes the reaction
|
||||
|
||||
* `alreadyReacted` - A function that returns a boolean.
|
||||
|
||||
* `count` - The reaction count
|
||||
|
||||
|
||||
For full reference: Please, check `coral-plugin-love`: [LoveButton.js](https://github.com/coralproject/talk/blob/master/plugins/coral-plugin-love/client/LoveButton.js)
|
||||
|
||||
### Comment Stream
|
||||
|
||||
Comment streams may be created with filtering and ordering in place:
|
||||
|
||||
* filter by user
|
||||
* filter by tag
|
||||
* sort by date ascending / descending
|
||||
|
||||
### Comment Commit hooks
|
||||
|
||||
// docs for the pre/post comment submit commit hooks
|
||||
|
||||
### Mod Queues
|
||||
|
||||
Moderation queues can be added via configuration objects passed in through plugins.
|
||||
|
||||
Basic mod queues will resemble the current moderation queues but can be generated from different lists of comments.
|
||||
|
||||
* filter by user tag
|
||||
* filter by comment tag
|
||||
* filter by comment status
|
||||
* Custom queries (paired with back end plugins that provide queries to get the data)
|
||||
|
||||
#### Advanced mod queues
|
||||
|
||||
Advanced mod queues can be created giving plugin authors the power to create the cards that appear in the queue, create actions and custom buttons, etc...
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
Plugins may rely on configuration options that admins/moderators can set in the Configuration section.
|
||||
|
||||
Basic settings can be added via json configuration in a plugin.
|
||||
|
||||
* Setting headline
|
||||
* Setting description
|
||||
* Setting input type
|
||||
* Default value
|
||||
* Variable name
|
||||
|
||||
#### Advanced Custom Configuration (low prioritiy)
|
||||
|
||||
Users can inject configuration interfaces that they create into the configuration allowing for more advanced configuration.
|
||||
|
||||
|
||||
## Styling Plugins
|
||||
Talk uses CSS Modules. This basically means that you can also add your CSS Module to your plugin without colliding with the rest of Talk!
|
||||
|
||||
##### My Component
|
||||
```js
|
||||
import styles from './style.css';
|
||||
|
||||
class MyCoralButton extends Component {
|
||||
render() {
|
||||
return <button className={styles.button}>My Button</button>;
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
Our `style.css` should could look like this.
|
||||
```css
|
||||
|
||||
.button {
|
||||
background: coral;
|
||||
border-radius: 3px;
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Hooks
|
||||
The plugins injected in the CommentBox such as `commentInputDetailArea` will inherit through props tools for handling hooks.
|
||||
|
||||
### Available hook types:
|
||||
`preSubmit` : To perform actions before submitting the comment.
|
||||
`postSubmit` : To perform actions after submitting the comment.
|
||||
|
||||
### Register Hooks
|
||||
`registerHook` is a function that takes: the hook type, a hook function and returns the hook data.
|
||||
|
||||
#### Usage:
|
||||
```js
|
||||
this.addCommentTagHook = this.props.registerHook('postSubmit', (data) => {
|
||||
const {comment} = data.createComment;
|
||||
this.props.addCommentTag({
|
||||
id: comment.id,
|
||||
tag: 'OFF_TOPIC'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Unregister Hooks
|
||||
|
||||
`unregisterHook` will remove the hook.
|
||||
|
||||
```js
|
||||
this.props.unregisterHook(this.addCommentTagHook);
|
||||
```
|
||||
|
||||
### The server folder and the index file
|
||||
Read more about the `/server` and how to extend Talk here.
|
||||
[talk/PLUGINS.md at master · coralproject/talk · GitHub](https://github.com/coralproject/talk/blob/master/PLUGINS.md)
|
||||
@@ -0,0 +1,121 @@
|
||||
# Frontend Architecture
|
||||
## The Stack
|
||||
- [React](#react)
|
||||
- [Redux](#redux)
|
||||
- [ImmutableJS](#immutablejs)
|
||||
|
||||
|
||||
## The Architecture
|
||||
Our frontend lives within [talk/client](https://github.com/coralproject/talk/tree/153193959cb4dfa5d8feaabb49811325f836ee68/client) folder. Every folder contains a plugin. In [coral-framework](https://github.com/coralproject/talk/tree/153193959cb4dfa5d8feaabb49811325f836ee68/client/coral-framework) you will find the core architecture of Talk.
|
||||
Here is where our Redux Application, translations, components, and helpers live.
|
||||
|
||||
|
||||
## Presentational and Container Components
|
||||
We use a common simple pattern called
|
||||
__Presentational and Container Components__
|
||||
|
||||
It basically consist in having two types of components:
|
||||
- Presentational
|
||||
- Containers
|
||||
|
||||
### Presentational Components
|
||||
- __How our UI looks like__
|
||||
- Are stateless components
|
||||
- Render props
|
||||
- Allow containment of children via `this.props.children`
|
||||
- They have DOM Markup
|
||||
|
||||
### Container Components
|
||||
* __How things work__
|
||||
* They don’t have markup nor styles
|
||||
* They provide data and behaviour to Presentational or Container Components
|
||||
* They connect via `react-redux`’s `connect()` to the state.
|
||||
* They `mapStateToProps` the state to the Presentational Container.
|
||||
* They `mapDispatchToProps` to send actions to the Presentational Container.
|
||||
* Name Convention `<Name>Container.js`
|
||||
|
||||
How a container looks like:
|
||||
```js
|
||||
/*
|
||||
* mapStateToProps
|
||||
* We map the part of the state that we want to use
|
||||
*/
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth.toJS()
|
||||
});
|
||||
|
||||
/*
|
||||
* mapDispatchToProps
|
||||
* We map the actions that we want to use
|
||||
*/
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkLogin: () => dispatch(checkLogin())
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* connect
|
||||
* We wrap our container in a connect() function
|
||||
*/
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(SignInContainer);
|
||||
````
|
||||
|
||||
How our SignInContainer works: [talk/SignInContainer.js · GitHub](https://github.com/coralproject/talk/blob/153193959cb4dfa5d8feaabb49811325f836ee68/client/coral-sign-in/containers/SignInContainer.js)
|
||||
|
||||
Within our plugins we create two folders `containers` and `components` so we can differentiate them:
|
||||
```
|
||||
coral-sign-in/
|
||||
├── containers/
|
||||
│ └── SignInContainer.js
|
||||
└── components/
|
||||
├── SignInContent.js
|
||||
└── SignUpContent.js
|
||||
```
|
||||
|
||||
More about this architecture:
|
||||
|
||||
[Container Components – Learn React with chantastic – Medium](https://medium.com/@learnreact/container-components-c0e67432e005#.w8mzgndcg)
|
||||
|
||||
|
||||
[Presentational and Container Components – Dan Abramov – Medium](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.ai4ih55v3)
|
||||
|
||||
|
||||
## React
|
||||
## Redux
|
||||
We use Redux to handle the state container of Talk.
|
||||
|
||||
[How we to use Redux, and how we use it with Talk](https://github.com/coralproject/talk/blob/frontenddocs/docs/frontend/REDUX.md)
|
||||
|
||||
|
||||
## ImmutableJS
|
||||
We use Immutable JS to maintain our state immutable.
|
||||
We found some really good tradeoffs while building Talk.
|
||||
|
||||
[How to use ImmutableJS and how we use it with Talk](https://github.com/coralproject/talk/blob/frontenddocs/docs/frontend/IMMUTABLEJS.md)
|
||||
|
||||
|
||||
## Test
|
||||
[How we do testing at Coral with Talk](https://github.com/coralproject/talk/blob/frontenddocs/docs/frontend/DEBUG.md)
|
||||
|
||||
|
||||
## Lint
|
||||
For linting in Talk we use `eslint:recommended`
|
||||
|
||||
You can find more info about the rules and best practices here:
|
||||
http://eslint.org/docs/rules/#best-practices
|
||||
|
||||
## Lint the code
|
||||
```js
|
||||
yarn lint
|
||||
```
|
||||
|
||||
|
||||
## The Future of the Frontend
|
||||
- Preact
|
||||
- Reselect
|
||||
@@ -0,0 +1,223 @@
|
||||
# Redux
|
||||
Redux is a predictable state container for JavaScript apps.
|
||||
|
||||
To understand Redux we need to dive into a few concepts.
|
||||
|
||||
- [Actions](#actions)
|
||||
- [Action Creators](#actions)
|
||||
- [Action Types](#actions)
|
||||
- [Reducers](#reducers)
|
||||
- [Stores](#store)
|
||||
|
||||
## The three principles
|
||||
These are the three principles to build Redux applications. The following are specified in the Redux Documentation [Three Principles · Redux](http://redux.js.org/docs/introduction/ThreePrinciples.html)
|
||||
|
||||
### Single source of truth
|
||||
The state of your whole application is stored in an object tree within a single store. We are going to represent the whole state of our application in a single Javascript Object.
|
||||
|
||||
### State is read-only
|
||||
The only way to change the state is to emit an action, an object describing what happened.
|
||||
|
||||
### Changes are made with pure functions
|
||||
To specify how the state tree is transformed by actions, you write pure reducers.
|
||||
|
||||
## Actions
|
||||
Actions describe that something happened in our application. They are payloads of information that send data to your store. __They are the only source of information for the store.__
|
||||
|
||||
Here is an example:
|
||||
```js
|
||||
const ADD_COMMENT = 'ADD_COMMENT';
|
||||
|
||||
{
|
||||
type: ADD_COMMENT,
|
||||
comment: 'This is my comment.'
|
||||
}
|
||||
```
|
||||
|
||||
Actions are JavaScript objects. Every action must have a `type` property that indicates the type of action being performed. Types should be defined as constants.
|
||||
|
||||
Once an app becomes big enough, you may want to move them into a separate module. We store them in a `contants.js` file. [auth.js Constants](https://github.com/coralproject/talk/blob/153193959cb4dfa5d8feaabb49811325f836ee68/client/coral-framework/constants/auth.js)
|
||||
|
||||
```js
|
||||
import { ADD_COMMENT, REMOVE_COMMENT } from './constants'
|
||||
```
|
||||
|
||||
We can dispatch an action by using `dispatch()`.
|
||||
|
||||
Our actions live within the `coral-framework/actions` folder. [talk/client/coral-framework/actions](https://github.com/coralproject/talk/tree/153193959cb4dfa5d8feaabb49811325f836ee68/client/coral-framework/actions)
|
||||
|
||||
More about Actions: [Actions · Redux](http://redux.js.org/docs/basics/Actions.html)
|
||||
|
||||
### Async Actions
|
||||
For our async operations we dispatch three actions.
|
||||
|
||||
- `<ACTION_TYPE>_REQUEST`
|
||||
|
||||
- `<ACTION_TYPE>_SUCCESS`
|
||||
|
||||
- `<ACTION_TYPE>_FAILURE`
|
||||
|
||||
#### Request
|
||||
We use the postfix `_REQUEST` to know that the resource is being requested.
|
||||
|
||||
#### Success
|
||||
We use the postfix `_SUCCESS` to know that the resource response came back successfully.
|
||||
|
||||
#### Failure
|
||||
We use the postfix `_FAILURE` to know that the resource request failed.
|
||||
|
||||
## Action Creators
|
||||
Action Creators are functions that return actions. This makes it easier to use, portable and testable.
|
||||
|
||||
```js
|
||||
function addComment(comment) {
|
||||
return {
|
||||
type: ADD_COMMENT,
|
||||
comment
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So we can later trigger those actions by using `dispatch()`
|
||||
|
||||
```js
|
||||
dispatch(addComment(comment))
|
||||
dispatch(removeComment(comment.id))
|
||||
```
|
||||
|
||||
|
||||
## Dispatch Function
|
||||
The `dispatch()` function can be accessed directly from the store as `store.dispatch()`, but more likely you'll access it using a helper like react-redux's`connect()`.
|
||||
|
||||
We use `connect()`in our containers. More about this in Architecture.
|
||||
|
||||
## Reducers
|
||||
With Actions we describe that something happened in our application. But we don’t specify how our state will be modified with this change.
|
||||
|
||||
In a Reducer we will specify how the state of our application change when an action has been dispatched.
|
||||
|
||||
Here we also will want to specify the `initialState`
|
||||
|
||||
Before building reducers it’s important to that you:
|
||||
- Don’t mutate the state
|
||||
- Return the previous state in the default case.
|
||||
|
||||
Here is an example of an auth reducer:
|
||||
```js
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
error: ''
|
||||
};
|
||||
|
||||
function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_REQUEST:
|
||||
return Object.assign({}, state, {
|
||||
isLoading: true
|
||||
});
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isLoading: false,
|
||||
loggedIn: true,
|
||||
user: action.user,
|
||||
error: ''
|
||||
});
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return Object.assign({}, state, {
|
||||
isLoading: false,
|
||||
error: action.error,
|
||||
loggedIn: false,
|
||||
user: null
|
||||
});
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice that a reducer takes the `state` as first argument and when it’s not defined it returns the `initialState`. As a second argument it takes the `action`. We have our state and we have the action. This is the time to specify how we modify the state.
|
||||
|
||||
### Reducers using ImmutableJS
|
||||
We are using ImmutableJS to maintain our app state. Here is a guide on how to use ImmutableJS.
|
||||
|
||||
This is how a simplified version of our [auth reducer](https://github.com/coralproject/talk/blob/153193959cb4dfa5d8feaabb49811325f836ee68/client/coral-framework/reducers/auth.js) looks like:
|
||||
```js
|
||||
const initialState = Map({
|
||||
isLoading: false,
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
error: ‘’
|
||||
});
|
||||
|
||||
function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case CHECK_LOGIN_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
case CHECK_LOGIN_SUCCESS:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('loggedIn', true)
|
||||
.set('user', action.user)
|
||||
.set('error', '');
|
||||
});
|
||||
case CHECK_LOGIN_FAILURE:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('error', action.error)
|
||||
.set('loggedIn', false)
|
||||
.set('user', null)
|
||||
});
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Looks cleaner, right?
|
||||
|
||||
It’s pretty easy to follow. Here it says if a `CHECK_LOGIN_REQUEST` action has been dispatched set the `isLoading` from our state to `true`. And we can show a tiny loader to let the user now we are requesting something to the server.
|
||||
|
||||
Our actions live within the `coral-framework/reducers` folder. [talk/client/coral-framework/reducers ](https://github.com/coralproject/talk/tree/153193959cb4dfa5d8feaabb49811325f836ee68/client/coral-framework/reducers)
|
||||
|
||||
More about Reducers: [Reducers · Redux](http://redux.js.org/docs/basics/Reducers.html)
|
||||
|
||||
And the last thing we need to see is the __Store__
|
||||
|
||||
### Store
|
||||
The `Store` is what holds the application state. Here we can access and update the state.
|
||||
|
||||
It’s important to note that we will only have a single store in our application called `rootReducer` and we will use reducer composition instead of many stores.
|
||||
|
||||
Here is an example of how create a store with [createStore()](http://redux.js.org/docs/api/createStore.html) using a reducer:
|
||||
```js
|
||||
import { createStore } from 'redux'
|
||||
import authReducer from './auth'
|
||||
let store = createStore(authReducer)
|
||||
```
|
||||
|
||||
We do have a lot of stores so we will need to combine all our reducers with [combineReducers()](http://redux.js.org/docs/api/combineReducers.html) within a single store
|
||||
|
||||
```js
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import authReducer from './auth'
|
||||
import configReducer from './config'
|
||||
import userReducer from './user'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
authReducer,
|
||||
configReducer,
|
||||
userReducer
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
More about Stores: [Store · Redux](http://redux.js.org/docs/basics/Store.html)
|
||||
|
||||
## Useful Resources
|
||||
[Redux Documentation · Redux](http://redux.js.org/)
|
||||
[Getting Started with Redux](https://egghead.io/courses/getting-started-with-redux)
|
||||
[Usage with React · Redux](http://redux.js.org/docs/basics/UsageWithReact.html)
|
||||
@@ -0,0 +1,123 @@
|
||||
# Test
|
||||
How we do testing at Coral with Talk.
|
||||
|
||||
We use Nightwatch and Selenium for our E2E tests and Enzyme for our React Components.
|
||||
|
||||
## E2E tests
|
||||
For our E2E Test we use Nightwatch and Selenium.
|
||||
|
||||
#### Selenium Server Setup
|
||||
|
||||
Selenium Server is a Java application which Nightwatch uses to connect to the various browsers.
|
||||
|
||||
You will need to have the Java Development Kit (JDK) installed.
|
||||
[Java SE Development Kit 8 - Downloads](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
|
||||
|
||||
The minimum required version is 7.
|
||||
You can check this by running `java -version`
|
||||
|
||||
#### Folder Structure
|
||||
|
||||
```
|
||||
e2e/
|
||||
├── pages
|
||||
| ├── adminPage.js
|
||||
| └── embedStreamPage.js
|
||||
├── reports
|
||||
├── tests
|
||||
| ├── Admin
|
||||
| ├── Commenter
|
||||
| ├── Moderator
|
||||
| └── Visitor
|
||||
|
||||
```
|
||||
|
||||
#### Pages
|
||||
Here we will have all the selectors and commands for a Page
|
||||
|
||||
#### Reports
|
||||
The folder that Nightwatch will use after running the tests
|
||||
|
||||
#### Tests
|
||||
Within `tests` folder we have 4 Folders and a couple of files.
|
||||
`Admin`, `Commenter`, `Moderator`, `Visitor` contains all the group tests based on the user role and their actions.
|
||||
|
||||
## Tests
|
||||
The `pree2e` script will create 3 users: a Commenter, a Moderator, and an Admin
|
||||
|
||||
* Commenter
|
||||
* Login
|
||||
- Post a comment
|
||||
* Likes a comment
|
||||
* Flag a comment
|
||||
* Flag a username
|
||||
* Gets Permalink
|
||||
* Visits Permalink
|
||||
|
||||
- Moderator
|
||||
* Login
|
||||
|
||||
- Admin
|
||||
* Login
|
||||
- Approve Comment
|
||||
- Reject Comment
|
||||
* Ban User
|
||||
|
||||
- Visitor
|
||||
* Tries to like a comment
|
||||
* Tries to flag a comment
|
||||
- Tries to flag a username
|
||||
* Signs up
|
||||
|
||||
## Run the tests
|
||||
Run Talk
|
||||
`dotenv yarn start`
|
||||
|
||||
Run e2e tests
|
||||
`yarn e2e`
|
||||
|
||||
|
||||
## Advanced Nightwatch and Selenium Settings
|
||||
|
||||
### Adding an Integration Environment
|
||||
```json
|
||||
{
|
||||
…
|
||||
“test_settings” : {
|
||||
“default” : {
|
||||
“launch_url” : “http://localhost”,
|
||||
“globals” : {
|
||||
“myGlobalVar” : “some value”,
|
||||
“otherGlobal” : “some other value”
|
||||
}
|
||||
},
|
||||
“integration” : {
|
||||
“launch_url” : “http://staging.host”,
|
||||
“globals” : {
|
||||
“myGlobalVar” : “other value”
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`nightwatch —env integration`
|
||||
|
||||
### Chrome Options
|
||||
[List of Chromium Command Line Switches « Peter Beverloo](http://peter.sh/experiments/chromium-command-line-switches/)
|
||||
|
||||
## Tags
|
||||
You'll notice that each test file starts with tags. This is useful to selectively target tests to run.
|
||||
|
||||
_i.e nightwatch --tag login will only run login tests tagged with login_
|
||||
```js
|
||||
module.exports {
|
||||
'@tags': ['login'],
|
||||
'Test': browser => {
|
||||
[...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Source: http://nightwatchjs.org/guide#test-tags
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
# Plugins
|
||||
We can build plugins to extend the client side functionality of Talk.
|
||||
|
||||
The ultimate goal of our client side plugin architecture is to allow developers
|
||||
to build on existing concepts without needing to understand core code while
|
||||
providing complete power and flexibility of javascript, css and html.
|
||||
|
||||
* [Plugin Architecture](#plugin-architecture)
|
||||
* Using our building block components
|
||||
* [Reactions](#reactions)
|
||||
* [Styling](#styling-plugins)
|
||||
|
||||
Advanced users will quickly realize that our plugins have complete access to core code. If you would like to write advanced plugins that reach outside of our published API as described in this document, please see [our notes on experimental plugins](/plugins/EXPERIMENTAL.md).
|
||||
|
||||
Under the hood our plugins are powered by *React*, *Redux* and *GraphQL*. We can also build them with simple vanilla javascript.
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
The plugins live in the `/plugins` folder. Each plugin must have an `index.js` file and two folders `client` and `server`.
|
||||
|
||||
### The Client Folder
|
||||
The frontend of our plugin lives inside the `client` folder. The `client` folder must have an `index.js` file that exports the configuration of our plugin.
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── client/
|
||||
│ └── index.js <-- index for client side functionality
|
||||
├── server/
|
||||
└── index.js <-- base plugin index
|
||||
```
|
||||
|
||||
For now our base plugin `index.js` file should look like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// We will add more here later.
|
||||
};
|
||||
```
|
||||
|
||||
### Creating a Component
|
||||
|
||||
We can add our components (or any other javascript code) within the `client` folder.
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── client/
|
||||
│ ├── MyComponent.js
|
||||
│ └── index.js
|
||||
├── server/
|
||||
└── index.js
|
||||
```
|
||||
|
||||
Our component could look like this:
|
||||
|
||||
```js
|
||||
import React, {Component} from 'react';
|
||||
|
||||
class MyButton extends Component {
|
||||
render() {
|
||||
return <button>My Button</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default MyButton;
|
||||
```
|
||||
|
||||
Here we create a component that renders a `button`. Now that we created our component we need to specify where it should get injected within Talk!
|
||||
|
||||
To tell Talk where that Component should get injected we need to specify which *Slots* to insert it into.
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
export default = () => <button>My Button</button>;
|
||||
```
|
||||
|
||||
### Slots
|
||||
In Talk we have defined specific *Slots* where we can inject components.
|
||||
|
||||
Here is how we specify our slots config in `my-plugin/index.js`
|
||||
|
||||
```js
|
||||
import MyButton from './MyButton';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentDetail: [MyButton]
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Here I’m specifying that the MyComponent Component will take place within the `commentDetail` in Talk.
|
||||
|
||||
`commentDetail` it’s a specific slot in the CommentStream. It means that it will be embedded inside de comment detail.
|
||||
|
||||
Slots properties take an`Array` so we can add as many components as we want.
|
||||
|
||||
## Building Blocks (TBD)
|
||||
|
||||
`Note: the concepts in this section are still to be implemented. Code samples are for discussion and may change.`
|
||||
|
||||
In order to allow you to build more complex plugins, we have wrapped some of our functionality in higher order components that expose a simple api.
|
||||
|
||||
## Reactions
|
||||
|
||||
Reactions provide users the ability to 'like', 'respect', etc... comments.
|
||||
|
||||
Note: some server side work will need to accompany this client side component. See the like and respect plugins as examples.
|
||||
|
||||
### Building Reactions
|
||||
|
||||
#### Our `client/index.js` :
|
||||
|
||||
```js
|
||||
import LoveButton from './LoveButton';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentReactions: [LoveButton]
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
In this example we add our reaction component to the `commentReaction` Slot
|
||||
|
||||
#### Our Reaction component:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import {withReaction} from 'coral-plugin-api';
|
||||
|
||||
class LoveButton extends React.Component {
|
||||
handleClick = () => {
|
||||
const {
|
||||
postReaction,
|
||||
deleteReaction,
|
||||
alreadyReacted
|
||||
} = this.props;
|
||||
|
||||
if (alreadyReacted()) {
|
||||
deleteReaction();
|
||||
} else {
|
||||
postReaction();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {count} = this.props;
|
||||
return (
|
||||
<button onClick={this.handleClick}>
|
||||
<span>Love</span>
|
||||
<span>{count > 0 && count}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withReaction('love')(LoveButton);
|
||||
```
|
||||
|
||||
|
||||
This feature introduces `withReaction` HOC. `withReaction` takes, as argument, a reaction string and it allows our component to receive specific props for handling reactions.
|
||||
|
||||
* `postReaction` - Posts the reaction
|
||||
|
||||
* `deleteReaction` - Removes the reaction
|
||||
|
||||
* `alreadyReacted` - A function that returns a boolean.
|
||||
|
||||
* `count` - The reaction count
|
||||
|
||||
|
||||
For full reference: Please, check `coral-plugin-love`: [LoveButton.js](https://github.com/coralproject/talk/blob/master/plugins/coral-plugin-love/client/LoveButton.js)
|
||||
|
||||
### Comment Stream
|
||||
|
||||
Comment streams may be created with filtering and ordering in place:
|
||||
|
||||
* filter by user
|
||||
* filter by tag
|
||||
* sort by date ascending / descending
|
||||
|
||||
### Comment Commit hooks
|
||||
|
||||
// docs for the pre/post comment submit commit hooks
|
||||
|
||||
### Mod Queues
|
||||
|
||||
Moderation queues can be added via configuration objects passed in through plugins.
|
||||
|
||||
Basic mod queues will resemble the current moderation queues but can be generated from different lists of comments.
|
||||
|
||||
* filter by user tag
|
||||
* filter by comment tag
|
||||
* filter by comment status
|
||||
* Custom queries (paired with back end plugins that provide queries to get the data)
|
||||
|
||||
#### Advanced mod queues
|
||||
|
||||
Advanced mod queues can be created giving plugin authors the power to create the cards that appear in the queue, create actions and custom buttons, etc...
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
Plugins may rely on configuration options that admins/moderators can set in the Configuration section.
|
||||
|
||||
Basic settings can be added via json configuration in a plugin.
|
||||
|
||||
* Setting headline
|
||||
* Setting description
|
||||
* Setting input type
|
||||
* Default value
|
||||
* Variable name
|
||||
|
||||
#### Advanced Custom Configuration (low prioritiy)
|
||||
|
||||
Users can inject configuration interfaces that they create into the configuration allowing for more advanced configuration.
|
||||
|
||||
|
||||
## Styling Plugins
|
||||
Talk uses CSS Modules. This basically means that you can also add your CSS Module to your plugin without colliding with the rest of Talk!
|
||||
|
||||
##### My Component
|
||||
```js
|
||||
import styles from './style.css';
|
||||
|
||||
class MyCoralButton extends Component {
|
||||
render() {
|
||||
return <button className={styles.button}>My Button</button>;
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
Our `style.css` should could look like this.
|
||||
```css
|
||||
|
||||
.button {
|
||||
background: coral;
|
||||
border-radius: 3px;
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Hooks
|
||||
The plugins injected in the CommentBox such as `commentInputDetailArea` will inherit through props tools for handling hooks.
|
||||
|
||||
### Available hook types:
|
||||
`preSubmit` : To perform actions before submitting the comment.
|
||||
`postSubmit` : To perform actions after submitting the comment.
|
||||
|
||||
### Register Hooks
|
||||
`registerHook` is a function that takes: the hook type, a hook function and returns the hook data.
|
||||
|
||||
#### Usage:
|
||||
```js
|
||||
this.addCommentTagHook = this.props.registerHook('postSubmit', (data) => {
|
||||
const {comment} = data.createComment;
|
||||
this.props.addCommentTag({
|
||||
id: comment.id,
|
||||
tag: 'OFF_TOPIC'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Unregister Hooks
|
||||
|
||||
`unregisterHook` will remove the hook.
|
||||
|
||||
```js
|
||||
this.props.unregisterHook(this.addCommentTagHook);
|
||||
```
|
||||
|
||||
### The server folder and the index file
|
||||
Read more about the `/server` and how to extend Talk here.
|
||||
[talk/PLUGINS.md at master · coralproject/talk · GitHub](https://github.com/coralproject/talk/blob/master/PLUGINS.md)
|
||||
@@ -0,0 +1,102 @@
|
||||
# Experimental plugins
|
||||
|
||||
Talk plugins are, in essence, small programs that hook into the core application in a variety of ways. Ultimately, this code can do anything that javascript is capable of. In addition, plugins can import any core code to hook into talk at any level.
|
||||
|
||||
If you want to write plugins that integrate with core code beyond the api described in [PLUGINS.md](PLUGINS.md), please keep the following things in mind:
|
||||
|
||||
* core code may change and break your plugin
|
||||
* you may introduce inefficiencies with your plugin that could hurt performance/crash Talk
|
||||
* you may cause bugs in other areas of Talk
|
||||
|
||||
If you'd like to build a supported plugin but don't have the hooks you need, please file an issue on this repo and we can discuss deepening the supported plugin api!
|
||||
|
||||
With that said, here's some of the prime experimental integration points:
|
||||
|
||||
## Reducers and Actions : Redux
|
||||
|
||||
Talk is powered by Redux and our plugins can too! Our plugins can have their own reducers and actions.
|
||||
|
||||
```js
|
||||
import MyButton from './MyButton';
|
||||
import reducer from './reducer';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
commentDetail: [MyButton],
|
||||
},
|
||||
reducer
|
||||
};
|
||||
```
|
||||
|
||||
## Import Actions from Talk
|
||||
We can easily trigger `Talk` actions in our plugin Components.
|
||||
|
||||
```js
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {addTag, removeTag} from 'coral-plugin-commentbox/actions';
|
||||
|
||||
class MyButton extends Component {
|
||||
render() {
|
||||
return <button onClick={this.props.addTag('MY_TAG')}>My Button</button>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({commentBox}) => ({commentBox});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({addTag, removeTag}, dispatch);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
|
||||
```
|
||||
|
||||
## ESlint and Babel
|
||||
In talk we use `eslint:recommended` and Babel with the latest ECMAScript Features. But you can use your own!
|
||||
While building your plugin you need to specify a `.eslintrc.json` file and a`.babelrc` file.
|
||||
|
||||
#### `.eslintrc.json`
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
#### `. babelrc `
|
||||
```json
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
````
|
||||
@@ -0,0 +1,163 @@
|
||||
# Plugins
|
||||
|
||||
The talk platform ships with a plugin architecture featuring that allows
|
||||
developers to:
|
||||
|
||||
* extend or replace server side graph, rest and auth functionality
|
||||
* inject functionality into front end embeds and the admin application
|
||||
* create new front end build targets for embedding
|
||||
* build plugins from local folders or published via npm/yarn
|
||||
* deploy plugins throughout the application lifecycle
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
All plugin code lives in the `/plugin` directory.
|
||||
|
||||
Each plugin is provided in a single folder named after the plugin.
|
||||
|
||||
### Naming a Plugin
|
||||
|
||||
Each plugin has a name which must be globally unique. Plugins with name
|
||||
collisions will not be able to be run together in an instance of Talk.
|
||||
|
||||
If you are creating a plugin for the open source community, we recommend the
|
||||
following naming convention:
|
||||
|
||||
```
|
||||
coral-talk-plugin-[name]
|
||||
```
|
||||
|
||||
If you are creating a variant of a plugin for an organization, we recommend
|
||||
adding the organization's name:
|
||||
|
||||
```
|
||||
coral-talk-plugin-[name]-[organization]
|
||||
```
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
In order for a Plugin to be active it must be _registered_.
|
||||
|
||||
The parsing order for the plugin registration is as follows:
|
||||
|
||||
- `TALK_PLUGINS_JSON` environment variable
|
||||
- `plugins.json` file
|
||||
- `plugins.default.json` file
|
||||
|
||||
If you need to "disable all plugins", you can simply provide `{}` as the
|
||||
contents of `process.env.TALK_PLUGINS_JSON` or the `plugins.json`.
|
||||
|
||||
### Local Plugins
|
||||
|
||||
The format for plugins.json looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": [
|
||||
"coral-plugin-respect",
|
||||
"coral-plugin-facebook-auth"
|
||||
],
|
||||
"client": [
|
||||
"coral-plugin-respect"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `server` array specifies which plugins will be loaded when the server
|
||||
starts. The `client` array specifies which plugins will be built into the
|
||||
front end bundles.
|
||||
|
||||
Where we have a `server` key with an array of plugins that match the folder
|
||||
name in the `plugins/` folder. For example, the above config would
|
||||
require a plugin from `plugins/coral-plugin-respect` and
|
||||
`plugins/coral-plugin-facebook-auth`.
|
||||
|
||||
### Published Plugins
|
||||
|
||||
If the package is external (available on NPM) you can specify the string for
|
||||
the version by using an object instead, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": [
|
||||
{"people": "^1.2.0"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Resolving Plugins
|
||||
|
||||
External plugins can be _resolved_ by running:
|
||||
|
||||
```bash
|
||||
./bin/cli plugins reconcile
|
||||
```
|
||||
|
||||
This achieves two things:
|
||||
|
||||
1. It will traverse into local plugin folders and install their dependencies.
|
||||
_Note that if the plugin is already installed and available in the node_modules folder, it will not be
|
||||
fetched again unless there is a version mismatch._ This will result in the
|
||||
project `package.json` and `yarn.lock` files to be modified, this is normal as
|
||||
this ensures that repeated deployments (with the same config) will have the
|
||||
same config, these changes should not be committed to source control.
|
||||
2. It will seek out dependencies that are listed in the object notation and try
|
||||
to install them from npm.
|
||||
|
||||
## Plugin Dependencies
|
||||
|
||||
You may also include additional external dependencies in your local packages by
|
||||
specifying a `package.json` at your plugin root which will result in a
|
||||
`node_modules` folder being generated at the plugin root with your specific
|
||||
dependencies.
|
||||
|
||||
## Deployment Solutions
|
||||
|
||||
Plugins can be deployed with a production instance of Talk.
|
||||
|
||||
### Source
|
||||
|
||||
Source deployments can just modify the `plugins.json` file and include any
|
||||
local plugins into the `plugins/` directory. After including the config, you
|
||||
need to reconcile the plugins and build the static assets:
|
||||
|
||||
```bash
|
||||
# get plugin dependancies and remote plugins
|
||||
./bin/cli plugins reconcile
|
||||
|
||||
# build staic assets (including enabled client side plugins)
|
||||
yarn build
|
||||
```
|
||||
|
||||
Then the application can be started as is.
|
||||
|
||||
If you are working on a plugin, our changes to the plugins will be picked up
|
||||
naturally by our development scripts:
|
||||
|
||||
```bash
|
||||
# Watch for changes to client files and rebuild
|
||||
yarn build-watch
|
||||
```
|
||||
|
||||
```bash
|
||||
# Watch for changes to server files and restart
|
||||
yarn dev-start
|
||||
```
|
||||
|
||||
|
||||
### Docker
|
||||
|
||||
If you deploy using Docker, you can extend from the `*-onbuild` image, an
|
||||
example `Dockerfile` for your project could be:
|
||||
|
||||
```Dockerfile
|
||||
FROM coralproject/talk:latest-onbuild
|
||||
```
|
||||
|
||||
Where the directory for your instance would contain a `plugins.json` file
|
||||
describing the plugin requirements and a `plugins` directory containing any
|
||||
other local plugins that should be included.
|
||||
|
||||
Onbuild triggers will execute when the image is building with your custom
|
||||
configuration and will ensure that the image is ready to use by building all
|
||||
assets inside the image as well.
|
||||
@@ -0,0 +1,382 @@
|
||||
# Server Plugins
|
||||
|
||||
### The Client Folder
|
||||
The frontend of our plugin lives inside the `client` folder. The `client` folder must have an `index.js` file that exports the configuration of our plugin.
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── client/
|
||||
│ └── ... <-- client side plugin files
|
||||
├── server/
|
||||
│ └── index.js <-- index for server side functionality
|
||||
└── index.js <-- base plugin index
|
||||
```
|
||||
|
||||
## Specification
|
||||
|
||||
Each plugin should export a single object with all hooks available on it.
|
||||
|
||||
_**Note: You will have access to the whole core and other plugin's typeDefs,
|
||||
context, loaders, mutators, resolvers, hooks. This is intentional, as it
|
||||
encourages composing plugins to merge functionality, like a Slack plugin which
|
||||
provides a Slack notify context function as well as having the loader for
|
||||
comments.**_
|
||||
|
||||
The following are the hooks available:
|
||||
|
||||
### GraphQL hooks
|
||||
|
||||
#### Field: `typeDefs`
|
||||
|
||||
```graphql
|
||||
enum COLOUR {
|
||||
RED
|
||||
BLUE
|
||||
}
|
||||
|
||||
type Person {
|
||||
name: String!
|
||||
colour: COLOUR!
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
createPerson(name: String!): Person
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
people: [Person!]
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
leader: Person
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
|
||||
`typeDefs` should be a string that will be _merged_ with the existing type
|
||||
definitions. `enum`'s will be appended to, types will be appended, and new types
|
||||
will be added.
|
||||
|
||||
#### Field: `context`
|
||||
|
||||
```js
|
||||
{
|
||||
Slack: (context) => ({
|
||||
notify: (message) => {
|
||||
// return a promise after we're done sending notifications.
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Any property provided here will be added to the context parameter available
|
||||
inside all resolvers, loaders, mutators, and of course, other context based
|
||||
plugins.
|
||||
|
||||
The top level item must accept a context for the request which it should use to
|
||||
configure the context plugin before it would be mounted at `context.plugins`.
|
||||
This plugin above would mount at: `context.plugins.Slack`, or, if you're using
|
||||
[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
|
||||
|
||||
#### Field: `loaders`
|
||||
|
||||
```js
|
||||
(context) => ({
|
||||
People: {
|
||||
load: () => db.people.find({user: context.user})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Loaders should be provided as a function which returns a map which is used in
|
||||
the resolvers function. These must return a promise or a value.
|
||||
|
||||
#### Field: `mutators`
|
||||
|
||||
```js
|
||||
(context) => ({
|
||||
People: {
|
||||
create: (name) => {
|
||||
return db.people.insert({user: context.user, name});
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Mutators should be provided as a function which returns a map which is used in
|
||||
the resolvers function. These must return a promise or a value.
|
||||
|
||||
#### Field: `resolvers`
|
||||
|
||||
```js
|
||||
{
|
||||
Person: {
|
||||
name(obj, args, context) {
|
||||
return obj.name;
|
||||
},
|
||||
colour(obj, args, context) {
|
||||
// Bill likes the colour red, everyone else likes blue.
|
||||
return obj.name === 'bill' ? 'RED' : 'BLUE';
|
||||
}
|
||||
},
|
||||
RootQuery: {
|
||||
people(obj, args, {loaders: {People}}) {
|
||||
return People.load();
|
||||
}
|
||||
},
|
||||
RootMutation: {
|
||||
createPerson(obj, {name}, {mutators: {People}}) {
|
||||
return People.create(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Should return a resolver map as described in the
|
||||
[Apollo Docs](http://dev.apollodata.com/tools/graphql-tools/resolvers.html#Resolver-map).
|
||||
|
||||
This will merge with the existing resolvers in core and from previous plugins.
|
||||
|
||||
#### Field: `hooks`
|
||||
|
||||
```js
|
||||
{
|
||||
RootMutation: {
|
||||
createPerson: {
|
||||
post: async (obj, args, {plugins: {Slack}}, info, person) {
|
||||
if (!person) {
|
||||
return person;
|
||||
}
|
||||
|
||||
await Slack.notify(`A new person just was created with name ${person.name}`);
|
||||
|
||||
return person;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hooks here are pretty special, for each resolver field, you can specify a
|
||||
pre/post hook that will execute pre and post field resolution.
|
||||
|
||||
If your post function accepts four parameters, then it can modify the field
|
||||
result. It is *required* that the function resolves a promise (or returns) with
|
||||
the modified value or simply the original if you didn't modify it.
|
||||
|
||||
#### Field: `setupFunctions`
|
||||
|
||||
```js
|
||||
setupFunctions: {
|
||||
leader: (options, args) => ({
|
||||
leader: {
|
||||
filter: (person) => person.place === 1
|
||||
},
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Setup functions allow you to create filters that control which pubsub.publish() events
|
||||
send data to the client. If the type in question contains args, clients may subscribe using those arguments to further filter their subscription.
|
||||
|
||||
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
|
||||
|
||||
### Routes
|
||||
|
||||
#### Field: `router`
|
||||
|
||||
```js
|
||||
(router) => {
|
||||
router.get('/api/v1/people', (req, res) => {
|
||||
res.json({people: [{name: 'Bob'}]});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The Router hook allows you to create a function that accepts the base express
|
||||
router where you can mount any amount of middleware/routes to do any form of
|
||||
action needed by external applications.
|
||||
|
||||
### Authorization middleware
|
||||
|
||||
The following example creates the requisite callback route and passport
|
||||
strategy needed to enable Facebook Authorization:
|
||||
|
||||
```js
|
||||
const authorization = require('middleware/authorization');
|
||||
|
||||
module.exports = {
|
||||
router(router) {
|
||||
router.get('/api/v1/people', authorization.needed('ADMIN'), (req, res) => {
|
||||
res.json({people: [{name: 'SECRET PEOPLE'}]});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Field: `passport`
|
||||
|
||||
```js
|
||||
const FacebookStrategy = require('passport-facebook').Strategy;
|
||||
const UsersService = require('services/users');
|
||||
const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
|
||||
|
||||
module.exports = {
|
||||
passport(passport) {
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: process.env.TALK_FACEBOOK_APP_ID,
|
||||
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
|
||||
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
|
||||
passReqToCallback: true,
|
||||
profileFields: ['id', 'displayName', 'picture.type(large)']
|
||||
}, async (req, accessToken, refreshToken, profile, done) => {
|
||||
|
||||
let user;
|
||||
try {
|
||||
user = await UsersService.findOrCreateExternalUser(profile);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return ValidateUserLogin(profile, user, done);
|
||||
}));
|
||||
},
|
||||
router(router) {
|
||||
|
||||
// Note that we have to import the passport instance here, it is
|
||||
// instantiated after all the strategies have been mounted.
|
||||
const {passport} = require('services/passport');
|
||||
|
||||
/**
|
||||
* Facebook auth endpoint, this will redirect the user immediatly to facebook
|
||||
* for authorization.
|
||||
*/
|
||||
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
|
||||
|
||||
/**
|
||||
* Facebook callback endpoint, this will send the user a html page designed to
|
||||
* send back the user credentials upon sucesfull login.
|
||||
*/
|
||||
router.get('/facebook/callback', (req, res, next) => {
|
||||
|
||||
// Perform the facebook login flow and pass the data back through the opener.
|
||||
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
Contents of `plugins.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": [
|
||||
"people"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Located in `plugins/people/index.js`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
typeDefs: `
|
||||
enum COLOUR {
|
||||
RED
|
||||
BLUE
|
||||
}
|
||||
|
||||
type Person {
|
||||
name: String!
|
||||
colour: COLOUR!
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
createPerson(name: String!): Person
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
people: [Person!]
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
leader: Person
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
Slack: () => ({
|
||||
notify: (message) => {
|
||||
// return a promise after we're done sending notifications.
|
||||
}
|
||||
})
|
||||
},
|
||||
loaders: ({user}) => ({
|
||||
People: {
|
||||
load: () => db.people.find({user})
|
||||
}
|
||||
}),
|
||||
mutators: ({user}) => ({
|
||||
People: {
|
||||
create: (name) => {
|
||||
return db.people.insert({user, name});
|
||||
}
|
||||
}
|
||||
}),
|
||||
resolvers: {
|
||||
Person: {
|
||||
name(obj, args, context) {
|
||||
return obj.name;
|
||||
},
|
||||
colour(obj, args, context) {
|
||||
// Bill likes the colour red, everyone else likes blue.
|
||||
return obj.name === 'bill' ? 'RED' : 'BLUE';
|
||||
}
|
||||
},
|
||||
RootQuery: {
|
||||
people(obj, args, {loaders: {People}}) {
|
||||
return People.load();
|
||||
}
|
||||
},
|
||||
RootMutation: {
|
||||
createPerson(obj, {name}, {mutators: {People}}) {
|
||||
return People.create(name);
|
||||
}
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
RootMutation: {
|
||||
createPerson: {
|
||||
post: async (obj, args, {plugins: {Slack}}, info, person) => {
|
||||
if (!person) {
|
||||
return person;
|
||||
}
|
||||
|
||||
await Slack.notify(`A new person just was created with name ${person.name}`);
|
||||
|
||||
return person;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setupFunctions: {
|
||||
leader: (options, args) => ({
|
||||
leader: {
|
||||
filter: (person) => person.place === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
You can access any API available inside the talk directory in a plugin by simply
|
||||
importing the file relative to the talk project root. An example would be if you
|
||||
wanted to import the `MetadataService`, you would simply write:
|
||||
|
||||
```javascript
|
||||
const MetadataService = require('services/metadata');
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user