From b84cbff976245faf3eec149c2c8ff889bd763a58 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 1/11] Move 4 spaces to 2 in JSX files


In order to comply with JavaScript style standards, change 4 spaces to 2

---

diff --git a/hubs/static/client/app/components/Constituent.jsx b/hubs/static/client/app/components/Constituent.jsx
index 17896a1..3691755 100644
--- a/hubs/static/client/app/components/Constituent.jsx
+++ b/hubs/static/client/app/components/Constituent.jsx
@@ -1,52 +1,52 @@
 import React from 'react';
 
 export default class Constituent extends React.Component {
-    render() {
-        const match = this.props.match;
-        const hasConstituent = (match['msg_ids'].length === 1 && Object.keys(match['msg_ids'])[0]['long_form'] !== Object.keys(match['msg_ids'])[0]['subtitle']);
-        if ( hasConstituent ) {
-            const constituentKey = Object.keys(match['msg_ids'])[0];
-            const constituent = match['msg_ids'][constituentKey];
-            const differentIcon = constituent['icon'] != constituent['__icon__'];
-            const longLink = ( constituent['link'] && constituent['long_form'].indexOf(constituent['link']) === -1 );
+  render() {
+    const match = this.props.match;
+    const hasConstituent = (match['msg_ids'].length === 1 && Object.keys(match['msg_ids'])[0]['long_form'] !== Object.keys(match['msg_ids'])[0]['subtitle']);
+    if ( hasConstituent ) {
+      const constituentKey = Object.keys(match['msg_ids'])[0];
+      const constituent = match['msg_ids'][constituentKey];
+      const differentIcon = constituent['icon'] != constituent['__icon__'];
+      const longLink = ( constituent['link'] && constituent['long_form'].indexOf(constituent['link']) === -1 );
 
-            let constituentIcon;
-            if (differentIcon)
-                constituentIcon = (
-                    <div className="media-left media-top">
-                        <a href={constituent['link']}>
-                            <img className="media-object" src={constituent['icon']} alt={constituent['subtitle']}/>
-                        </a>
-                    </div>
+      let constituentIcon;
+      if (differentIcon)
+        constituentIcon = (
+          <div className="media-left media-top">
+            <a href={constituent['link']}>
+              <img className="media-object" src={constituent['icon']} alt={constituent['subtitle']}/>
+            </a>
+          </div>
 
-                )
+        )
 
-            let readMore;
-            if(longLink)
-                readMore = (
-                    <div className="media-body">
-                        {constituent['long_form']}
-                        <a href={constituent['link']} target="_blank">
-                            Read more <span class="glyphicon glyphicon-new-window"></span>
-                        </a>
-                    </div>
-                )
+      let readMore;
+      if(longLink)
+        readMore = (
+          <div className="media-body">
+            {constituent['long_form']}
+            <a href={constituent['link']} target="_blank">
+              Read more <span class="glyphicon glyphicon-new-window"></span>
+            </a>
+          </div>
+        )
 
-            return (
-                <div id="content-{{match['dom_id']}}" className="media">
-                    <ExpandCollapse match={this.props.match} />
-                    {constituentIcon}
-                    {readMore}
-                </div>
-            )
-        } else if (match['msg_ids'].length > 1) {
-            <div id="content-{{match['dom_id']}}" className="media">
-                <ExpandCollapse match={this.props.match} />
-                <MessageList msgs={this.match['msg_ids']} />
-            </div>
-        }
-        else {
-            return null
-        }
+      return (
+        <div id="content-{{match['dom_id']}}" className="media">
+          <ExpandCollapse match={this.props.match} />
+          {constituentIcon}
+          {readMore}
+        </div>
+      )
+    } else if (match['msg_ids'].length > 1) {
+      <div id="content-{{match['dom_id']}}" className="media">
+        <ExpandCollapse match={this.props.match} />
+        <MessageList msgs={this.match['msg_ids']} />
+      </div>
     }
+    else {
+      return null
+    }
+  }
 }
diff --git a/hubs/static/client/app/components/ExpandCollapse.jsx b/hubs/static/client/app/components/ExpandCollapse.jsx
index 80ab303..29b1e6f 100644
--- a/hubs/static/client/app/components/ExpandCollapse.jsx
+++ b/hubs/static/client/app/components/ExpandCollapse.jsx
@@ -1,18 +1,18 @@
 import React from 'react';
 
 export default class ExpandCollapse extends React.Component {
-    render() {
-        return (
-            <div className="pull-right">
-                <a id="expand-{this.props.match['dom_id']}" href="javascript:expand_feed_entries({this.props.match['dom_id']});" className="hidden">
-                    <span className="glyphicon glyphicon-chevron-down"></span>
-                    expand
-                </a>
-                <a id="collapse-{this.props.match['dom_id']}" href="javascript:collapse_feed_entries('{this.props.match['dom_id']}');">
-                    <span className="glyphicon glyphicon-chevron-up"></span>
-                    collapse
-                </a>
-            </div>
-        )
-    }
+  render() {
+    return (
+      <div className="pull-right">
+        <a id="expand-{this.props.match['dom_id']}" href="javascript:expand_feed_entries({this.props.match['dom_id']});" className="hidden">
+          <span className="glyphicon glyphicon-chevron-down"></span>
+          expand
+        </a>
+        <a id="collapse-{this.props.match['dom_id']}" href="javascript:collapse_feed_entries('{this.props.match['dom_id']}');">
+          <span className="glyphicon glyphicon-chevron-up"></span>
+          collapse
+        </a>
+      </div>
+    )
+  }
 }
diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 9e6cccb..187a1d1 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -10,55 +10,55 @@ import {Message, MessageList} from './Messages.jsx';
 
 
 class Panel extends React.Component {
-    render() {
-        return (
-            <div className="panel panel-default">
-                <div className="panel-body">
-                    <Icon match={this.props.match} />
-                    <Markup match={this.props.match} />
-                    <Constituent match={this.props.match} />
-                </div>
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div className="panel panel-default">
+        <div className="panel-body">
+          <Icon match={this.props.match} />
+          <Markup match={this.props.match} />
+          <Constituent match={this.props.match} />
+        </div>
+      </div>
+    );
+  }
 }
 
 export default class Feed extends React.Component {
-    constructor (props) {
-        super(props);
-        this.state = {
-            matches: this.props.matches,
-            sse: true,
-        }
-        this.source = (!!window.EventSource) ? new EventSource('http://localhost:9090') : {};
-        this.source.addEventListener('error', () => {
-            this.state.sse = false;
-        }, false);
-        window.onbeforeunload = () => {
-            this.source.close();
-        }
-        this.source.addEventListener('message', resp => {
-            $.get('/api/fedmsg/markup', {
-                message: resp.data,
-                plugin: 'feed',
-            }).done(data => {
-                if(!$.isEmptyObject(data)) {
-                    this.state.matches.push.apply(this.state.matches, data);
-                    this.setState({matches: this.state.matches});
-                }
-            });
-        }, false);
+  constructor (props) {
+    super(props);
+    this.state = {
+      matches: this.props.matches,
+      sse: true,
     }
-    render() {
-        const feedNodes = this.state.matches.map(match => {
-            return <Panel match={match}/>
-        });
-        return (
-            <div>
-                {feedNodes}
-            </div>
-        )
+    this.source = (!!window.EventSource) ? new EventSource('http://localhost:9090') : {};
+    this.source.addEventListener('error', () => {
+      this.state.sse = false;
+    }, false);
+    window.onbeforeunload = () => {
+      this.source.close();
     }
+    this.source.addEventListener('message', resp => {
+      $.get('/api/fedmsg/markup', {
+        message: resp.data,
+        plugin: 'feed',
+      }).done(data => {
+        if(!$.isEmptyObject(data)) {
+          this.state.matches.push.apply(this.state.matches, data);
+          this.setState({matches: this.state.matches});
+        }
+      });
+    }, false);
+  }
+  render() {
+    const feedNodes = this.state.matches.map(match => {
+      return <Panel match={match}/>
+    });
+    return (
+      <div>
+        {feedNodes}
+      </div>
+    )
+  }
 }
 
 
diff --git a/hubs/static/client/app/components/Icon.jsx b/hubs/static/client/app/components/Icon.jsx
index 577050e..70c2d8c 100644
--- a/hubs/static/client/app/components/Icon.jsx
+++ b/hubs/static/client/app/components/Icon.jsx
@@ -1,15 +1,15 @@
 import React from 'react';
 
 export default class Icon extends React.Component {
-    render() {
-        return (
-            <div>
-                <div className="media-left">
-                    <a href={this.props.match['link'] ? this.props.match['link'] : '#'} target="_blank">
-                        <img className="media-object square-32 img-circle" src={this.props.match['secondary_icon']} />
-                    </a>
-                </div>
-            </div>
-        )
-    }
+  render() {
+    return (
+      <div>
+        <div className="media-left">
+          <a href={this.props.match['link'] ? this.props.match['link'] : '#'} target="_blank">
+            <img className="media-object square-32 img-circle" src={this.props.match['secondary_icon']} />
+          </a>
+        </div>
+      </div>
+    )
+  }
 }
diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index 45ccbe3..952fe06 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -1,17 +1,17 @@
 import React from 'react';
 
 export default class Markup extends React.Component {
-    createMarkup() {
-        return {__html: this.props.match['markup']};
-    }
-    render() {
-        return (
-            <div className="media-body">
-                <h4 className="media-heading"
-                    dangerouslySetInnerHTML={this.createMarkup()}
-                ></h4>
-                {this.props.match['human_time']}
-            </div>
-        )
-    }
+  createMarkup() {
+    return {__html: this.props.match['markup']};
+  }
+  render() {
+    return (
+      <div className="media-body">
+        <h4 className="media-heading"
+            dangerouslySetInnerHTML={this.createMarkup()}
+        ></h4>
+        {this.props.match['human_time']}
+      </div>
+    )
+  }
 }
diff --git a/hubs/static/client/app/components/Messages.jsx b/hubs/static/client/app/components/Messages.jsx
index f034a87..a773b52 100644
--- a/hubs/static/client/app/components/Messages.jsx
+++ b/hubs/static/client/app/components/Messages.jsx
@@ -1,36 +1,36 @@
 import React from 'react';
 
 class Message extends React.Component {
-    render() {
-        if(this.props.constituent['link']) {
-            return (
-                <li>
-                    {this.props.constituent['markup']}
-                    <a href={this.props.constituent['link']} target="_blank">
-                        <span className="glyphicon glyphicon-new-window"></span>
-                    </a>
-                </li>
-            )
-        } else {
-            return (<li>{ this.props.constituent['markup'] }</li>)
-        }
+  render() {
+    if(this.props.constituent['link']) {
+      return (
+        <li>
+          {this.props.constituent['markup']}
+          <a href={this.props.constituent['link']} target="_blank">
+            <span className="glyphicon glyphicon-new-window"></span>
+          </a>
+        </li>
+      )
+    } else {
+      return (<li>{ this.props.constituent['markup'] }</li>)
     }
+  }
 }
 
 class MessageList extends React.Component {
-    render() {
-        const messageNodes =  this.props.items.map(function(item) {
-            return <Message constituent={item}/>;
-        });
-        return (
-            <ul id="content-{this.props.match['dom_id']}" className="hidden list-unstyled">
-                {messageNodes}
-            </ul>
-        )
-    }
+  render() {
+    const messageNodes =  this.props.items.map(function(item) {
+      return <Message constituent={item}/>;
+    });
+    return (
+      <ul id="content-{this.props.match['dom_id']}" className="hidden list-unstyled">
+        {messageNodes}
+      </ul>
+    )
+  }
 }
 
 module.exports = {
-    Message: Message,
-    MessageList: MessageList
+  Message: Message,
+  MessageList: MessageList
 }
diff --git a/hubs/static/client/app/index.jsx b/hubs/static/client/app/index.jsx
index 29f5129..a753c36 100644
--- a/hubs/static/client/app/index.jsx
+++ b/hubs/static/client/app/index.jsx
@@ -2,4 +2,3 @@ import React from 'react';
 import { render } from 'react-dom';
 
 import Feed from './components/Feed.jsx'
-
diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html
index 00fc7e2..3b01664 100644
--- a/hubs/widgets/templates/feed.html
+++ b/hubs/widgets/templates/feed.html
@@ -5,11 +5,10 @@
         src ="{{url_for('static', filename='js/hubs.js')}}"></script>
 <script>
  (function() {
-     const FeedElement = React.createElement(Feed, {
-         matches: {{ matches }}
-     });
+   const FeedElement = React.createElement(Feed, {
+     matches: {{ matches }}
+   });
 
-     reactRender(FeedElement, document.getElementById('feed'));
+   reactRender(FeedElement, document.getElementById('feed'));
  })();
-
 </script>

From 3bd168a4232553a43ad09dbd8d47c384825496bd Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 2/11] Move Feed to external SSE


---

diff --git a/ev-server/hubs-stream-server.py b/ev-server/hubs-stream-server.py
deleted file mode 100644
index 56d5b50..0000000
--- a/ev-server/hubs-stream-server.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# encoding=utf8
-'''
-This file is for development purposes only.
-
-The end goal is for the streaming server to be implemented on the FMN side
-'''
-import json
-import logging
-import sys
-import trollius
-import trollius_redis
-import urllib2
-from concurrent.futures import TimeoutError
-from trollius import From
-
-reload(sys)
-sys.setdefaultencoding('utf8')
-
-log = logging.getLogger(__name__)
-SERVER = None
-REDIS_HOST = '0.0.0.0'
-REDIS_PORT = 6379
-REDIS_DB = 0
-EVENTSOURCE_PORT = 9090
-'''
-you need to
-
-pip install trollius trollius_redis
-
-dnf install redis httpie
-systemctl start redis
-
-
-usage: http get 0.0.0.0:9090/
-
-'''
-
-
-def get_recent_posts():
-    delta = "delta=86400"  # one day worth of data
-    rows_per_page = "rows_per_page=50"
-    url = "https://apps.fedoraproject.org/datagrepper/raw" + "?" + delta + "&" + rows_per_page
-    request = urllib2.Request(url)
-    contents = urllib2.urlopen(request).read()
-    json_response = json.loads(contents)
-    return json_response['raw_messages']
-
-
-@trollius.coroutine
-def handle_client(client_reader, client_writer):
-    origin = '*'
-    if origin.endswith('/'):
-        origin = origin[:-1]
-
-    client_writer.write(("HTTP/1.0 200 OK\n"
-                         "Content-Type: text/event-stream\n"
-                         "Cache: nocache\n"
-                         "Connection: keep-alive\n"
-                         "Access-Control-Allow-Origin: %s\n\n" % origin
-                         ).encode())
-
-    connection = yield trollius.From(trollius_redis.Connection.create(
-        host=REDIS_HOST, port=REDIS_PORT,
-        db=REDIS_DB))
-
-    try:
-        posts = get_recent_posts()
-
-        # send 50 latest posts
-        num_post_sent = 0
-        while num_post_sent < 50:
-            reply = posts[num_post_sent]
-            reply = json.dumps(reply)
-            log.info(reply)
-            log.info("Sending post %s %s", str(num_post_sent), reply)
-            client_writer.write(('data: %s\n\n' % reply).encode())
-            yield trollius.From(client_writer.drain())
-            num_post_sent += 1
-            yield From(trollius.sleep(2))
-
-    except trollius.ConnectionResetError:
-        log.exception("ERROR: ConnectionResetError in handle_client")
-    except Exception:
-        log.exception("ERROR: Exception in handle_client")
-    finally:
-        # Wathever happens, close the connection.
-        connection.close()
-        client_writer.close()
-
-
-def main():
-    global SERVER
-
-    try:
-        loop = trollius.get_event_loop()
-        coro = trollius.start_server(handle_client,
-                                     host=None,
-                                     port=EVENTSOURCE_PORT,
-                                     loop=loop)
-        SERVER = loop.run_until_complete(coro)
-        log.info('Serving server at {}'.format(SERVER.sockets[0].getsockname(
-        )))
-        loop.run_forever()
-    except KeyboardInterrupt:
-        pass
-    except trollius.ConnectionResetError as err:
-        log.exception("ERROR: ConnectionResetError in main")
-    except Exception as err:
-        log.exception("ERROR: Exception in main")
-    finally:
-        # Close the server
-        SERVER.close()
-        log.info("End Connection")
-        loop.run_until_complete(SERVER.wait_closed())
-        loop.close()
-        log.info("End")
-
-
-if __name__ == '__main__':
-    log = logging.getLogger("")
-    formatter = logging.Formatter(
-        "%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s")
-
-    # setup console logging
-    log.setLevel(logging.DEBUG)
-    ch = logging.StreamHandler()
-    ch.setLevel(logging.DEBUG)
-
-    aslog = logging.getLogger("asyncio")
-    aslog.setLevel(logging.DEBUG)
-
-    ch.setFormatter(formatter)
-    log.addHandler(ch)
-    main()
diff --git a/hubs/app.py b/hubs/app.py
index c975952..7e24193 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -639,76 +639,3 @@ def hub_leave(hub):
         return flask.abort(400)
     session.commit()
     return flask.redirect(flask.url_for('hub', name=hub.name))
-
-
-PATHS = fmn.lib.load_rules(root='fmn.rules')
-
-
-@app.route('/api/fedmsg/markup', methods=['GET'])
-def markup_fedmsg():
-    '''
-    This is a temporary endpoint to create a human-readable form of a message.
-    For now it serves as a development tool
-
-    This route will be removed once its functionality is integrated into FMN
-    '''
-    from hubs.widgets.feed import (
-        apply_markup, rehydrate_preference,
-        get_remote_preference
-    )
-    try:
-        data = flask.request.args['message']
-        plugin = flask.request.args['plugin']
-    except KeyError:
-        return flask.abort(400)
-
-    widget = hubs.models.Widget.by_plugin(session, plugin)
-
-    if not widget:
-        return flask.abort(400)
-
-    context = widget.config.get('fmn_context')
-
-    messages = []
-    message = json.loads(data)
-
-    if 'topic' not in message:
-        return flask.abort(400)
-
-    try:
-        nickname = flask.g.auth.nickname
-    except AttributeError:      # Not logged in
-        return flask.abort(403)
-
-    preference = get_remote_preference(nickname, context)
-
-    if preference:
-        try:
-            preference = rehydrate_preference(preference)
-        except ImportError:
-            pass
-    recipients = fmn.lib.recipients(
-        [preference], message, PATHS, fedmsg_config)
-    if recipients:
-        messages.append(message)
-    matches = fedmsg.meta.conglomerate(messages, lexers=True, **fedmsg_config)
-
-    for match in matches:
-        match['markup'] = apply_markup(match)
-        for _, constituent in match['msg_ids'].items():
-            constituent['markup'] = apply_markup(constituent)
-            if constituent['long_form'] != constituent['subtitle']:
-                if constituent.get('lexer'):
-                    constituent['long_form'] = pygments.highlight(
-                        constituent['long_form'],
-                        constituent['lexer'],
-                        pygments.formatters.HtmlFormatter(style='monokai'),
-                    )
-                else:
-                    markup = u"<h5>{long_form}</h5>".format(**constituent)
-                    constituent['long_form'] = markup
-
-        # And tack on a unique identifier for each top level entry.
-        match['dom_id'] = six.text_type(uuid.uuid4())
-
-    return flask.jsonify(matches)
diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 187a1d1..da00b25 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -12,11 +12,10 @@ import {Message, MessageList} from './Messages.jsx';
 class Panel extends React.Component {
   render() {
     return (
-      <div className="panel panel-default">
+      <div className="panel panel-default panel-visible">
         <div className="panel-body">
           <Icon match={this.props.match} />
           <Markup match={this.props.match} />
-          <Constituent match={this.props.match} />
         </div>
       </div>
     );
@@ -30,28 +29,22 @@ export default class Feed extends React.Component {
       matches: this.props.matches,
       sse: true,
     }
-    this.source = (!!window.EventSource) ? new EventSource('http://localhost:9090') : {};
+    this.source = (!!window.EventSource) ? new EventSource(this.props.url) : {};
     this.source.addEventListener('error', () => {
       this.state.sse = false;
     }, false);
     window.onbeforeunload = () => {
       this.source.close();
     }
-    this.source.addEventListener('message', resp => {
-      $.get('/api/fedmsg/markup', {
-        message: resp.data,
-        plugin: 'feed',
-      }).done(data => {
-        if(!$.isEmptyObject(data)) {
-          this.state.matches.push.apply(this.state.matches, data);
-          this.setState({matches: this.state.matches});
-        }
-      });
-    }, false);
+    this.source.onmessage = resp => {
+      const data = JSON.parse(resp.data);
+      this.state.matches.push(data);
+      this.setState({matches: this.state.matches});
+    };
   }
   render() {
     const feedNodes = this.state.matches.map(match => {
-      return <Panel match={match}/>
+      return <Panel match={match} />
     });
     return (
       <div>
@@ -61,7 +54,6 @@ export default class Feed extends React.Component {
   }
 }
 
-
 window.Feed = Feed;
 window.React = React;
 window.reactRender = render;
diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html
index 3b01664..4874fb0 100644
--- a/hubs/widgets/templates/feed.html
+++ b/hubs/widgets/templates/feed.html
@@ -6,9 +6,9 @@
 <script>
  (function() {
    const FeedElement = React.createElement(Feed, {
-     matches: {{ matches }}
+     matches: {{ matches }},
+     url: ':8080/user/skrzepto'
    });
-
    reactRender(FeedElement, document.getElementById('feed'));
  })();
 </script>

From e3bfeac5cc07fe9ff3199547b46053e383c38eed Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 3/11] Add class properties req and new data to top


---

diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index da00b25..7498425 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -38,7 +38,7 @@ export default class Feed extends React.Component {
     }
     this.source.onmessage = resp => {
       const data = JSON.parse(resp.data);
-      this.state.matches.push(data);
+      this.state.matches.unshift(data);
       this.setState({matches: this.state.matches});
     };
   }
diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json
index abb023a..26274f1 100644
--- a/hubs/static/client/package.json
+++ b/hubs/static/client/package.json
@@ -9,6 +9,7 @@
   "dependencies": {
     "babel-core": "~6.9.1",
     "babel-loader": "~6.2.4",
+    "babel-plugin-transform-class-properties": "^6.10.2",
     "babel-preset-es2015": "~6.9.0",
     "babel-preset-react": "~6.5.0",
     "react": "~15.1.0",

From 8e39a4b4856f06271baee4068b4eddff14d85ce5 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 4/11] Change feed widget to card format


* Per discussion on #fedora-hubs, implement message limiting
* Per same discussion format the feed to look like other widgets in card
  format
* Update README to be explicit about what to do after building JS
asseets h/t skrzepto

---

diff --git a/README.rst b/README.rst
index 499e7e3..095bc30 100644
--- a/README.rst
+++ b/README.rst
@@ -210,7 +210,11 @@ Build JavaScript assets. Add ``--w`` for live reloading::
     $ cd hubs/static/client && node_modules/.bin/webpack
 
 **Lastly**, (fingers crossed) start up the fedora-hubs webapp and load your
-profile page.  Once there are some messages that get into your local database
+profile page. Change back to the project root and run::
+
+  $ python runserver.py
+
+Once there are some messages that get into your local database
 that *should* show up on your feed.. they should appear there.  (At very least,
 you shouldn't get an error message about that widget being unable to be
 displayed).
diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 7498425..55ddfdb 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -8,6 +8,7 @@ import Icon from './Icon.jsx';
 import Markup from './Markup.jsx';
 import {Message, MessageList} from './Messages.jsx';
 
+const FEED_MESSAGE_LIMIT = 10;
 
 class Panel extends React.Component {
   render() {
@@ -38,6 +39,9 @@ export default class Feed extends React.Component {
     }
     this.source.onmessage = resp => {
       const data = JSON.parse(resp.data);
+      if(this.state.matches.length >= FEED_MESSAGE_LIMIT) {
+        this.state.matches.pop()
+      }
       this.state.matches.unshift(data);
       this.setState({matches: this.state.matches});
     };
@@ -47,9 +51,14 @@ export default class Feed extends React.Component {
       return <Panel match={match} />
     });
     return (
-      <div>
+      <div className="card">
+        <div className="card-header">
+          Live Feed
+        </div>
+      <div className="card-block">
         {feedNodes}
       </div>
+      </div>
     )
   }
 }

From 2b5d0c2dbacbec195f4058059fdb3faa09bfd873 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 5/11] JSX style rules from Airbnb style guide


---

diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 55ddfdb..1f208aa 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -1,65 +1,48 @@
 import React from 'react';
-import {render} from 'react-dom';
+import { render } from 'react-dom';
 
+import Panel from './Panel.jsx';
 
-import Constituent from './Constituent.jsx';
-import ExpandCollapse from './ExpandCollapse.jsx';
-import Icon from './Icon.jsx';
-import Markup from './Markup.jsx';
-import {Message, MessageList} from './Messages.jsx';
-
-const FEED_MESSAGE_LIMIT = 10;
-
-class Panel extends React.Component {
-  render() {
-    return (
-      <div className="panel panel-default panel-visible">
-        <div className="panel-body">
-          <Icon match={this.props.match} />
-          <Markup match={this.props.match} />
-        </div>
-      </div>
-    );
-  }
-}
+const FEED_MESSAGE_LIMIT = 20;
 
 export default class Feed extends React.Component {
-  constructor (props) {
+  constructor(props) {
     super(props);
     this.state = {
       matches: this.props.matches,
+      messageLimit: FEED_MESSAGE_LIMIT,
       sse: true,
-    }
+    };
     this.source = (!!window.EventSource) ? new EventSource(this.props.url) : {};
     this.source.addEventListener('error', () => {
       this.state.sse = false;
     }, false);
     window.onbeforeunload = () => {
       this.source.close();
-    }
+    };
     this.source.onmessage = resp => {
       const data = JSON.parse(resp.data);
-      if(this.state.matches.length >= FEED_MESSAGE_LIMIT) {
-        this.state.matches.pop()
+      if (this.state.matches.length >= this.state.messageLimit) {
+        this.state.matches.pop();
       }
       this.state.matches.unshift(data);
-      this.setState({matches: this.state.matches});
+      this.setState({ matches: this.state.matches });
     };
   }
   render() {
     const feedNodes = this.state.matches.map(match => {
-      return <Panel match={match} />
+      return <Panel match={match} />;
     });
     return (
       <div className="card">
         <div className="card-header">
           Live Feed
         </div>
-      <div className="card-block">
-        {feedNodes}
-      </div>
+        <div className="card-block">
+          {feedNodes}
+        </div>
       </div>
-    )
+    );
   }
 }
 
diff --git a/hubs/static/client/app/components/Icon.jsx b/hubs/static/client/app/components/Icon.jsx
index 70c2d8c..08c8d60 100644
--- a/hubs/static/client/app/components/Icon.jsx
+++ b/hubs/static/client/app/components/Icon.jsx
@@ -1,15 +1,19 @@
 import React from 'react';
 
-export default class Icon extends React.Component {
-  render() {
-    return (
-      <div>
-        <div className="media-left">
-          <a href={this.props.match['link'] ? this.props.match['link'] : '#'} target="_blank">
-            <img className="media-object square-32 img-circle" src={this.props.match['secondary_icon']} />
-          </a>
-        </div>
+const Icon = function (props) {
+  return (
+    <div>
+      <div className="media-left">
+        <a href={props.match.link ? props.match.link : '#'} target="_blank">
+          <img
+            alt="User avatar"
+            className="media-object square-32 img-circle"
+            src={props.match.secondary_icon}
+          />
+        </a>
       </div>
-    )
-  }
-}
+    </div>
+  );
+};
+
+export default Icon;
diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index 952fe06..768a81b 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -2,16 +2,19 @@ import React from 'react';
 
 export default class Markup extends React.Component {
   createMarkup() {
-    return {__html: this.props.match['markup']};
+    return { __html: this.props.match.markup };
   }
   render() {
+    debugger;
     return (
       <div className="media-body">
-        <h4 className="media-heading"
-            dangerouslySetInnerHTML={this.createMarkup()}
-        ></h4>
-        {this.props.match['human_time']}
+        <h4
+          className="media-heading"
+          dangerouslySetInnerHTML={this.createMarkup()}
+        >
+        </h4>
+        {this.props.match.human_time}
       </div>
-    )
+    );
   }
 }
diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx
new file mode 100644
index 0000000..61e8242
--- /dev/null
+++ b/hubs/static/client/app/components/Panel.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import Icon from './Icon.jsx';
+import Markup from './Markup.jsx';
+
+const Panel = function (props) {
+  return (
+    <div className="panel panel-default panel-visible">
+      <div className="panel-body">
+        <Icon match={props.match} />
+        <Markup match={props.match} />
+      </div>
+    </div>
+  );
+};
+
+export default Panel;
diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json
index 26274f1..045880c 100644
--- a/hubs/static/client/package.json
+++ b/hubs/static/client/package.json
@@ -12,17 +12,18 @@
     "babel-plugin-transform-class-properties": "^6.10.2",
     "babel-preset-es2015": "~6.9.0",
     "babel-preset-react": "~6.5.0",
+    "moment": "^2.14.1",
     "react": "~15.1.0",
     "react-dom": "~15.1.0",
     "reactify": "~1.1.1",
     "webpack": "~1.13.1"
   },
   "devDependencies": {
-    "eslint-config-airbnb": "~9.0.1",
-    "eslint-plugin-react": "~5.2.1",
-    "eslint-plugin-jsx-a11y": "~1.5.3",
-    "eslint-plugin-import": "~1.8.1",
-    "eslint": "~2.12.0"
+    "eslint": "^2.13.1",
+    "eslint-config-airbnb": "^9.0.1",
+    "eslint-plugin-import": "^1.10.2",
+    "eslint-plugin-jsx-a11y": "^1.5.5",
+    "eslint-plugin-react": "^5.2.2"
   },
   "scripts": {
     "dev": "webpack --watch",

From 3beee9df2c297fffb3a03cd1da6e2c380ef6d4c7 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 6/11] Make feed limit configurable


---

diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 1f208aa..7d9c618 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -3,14 +3,12 @@ import { render } from 'react-dom';
 
 import Panel from './Panel.jsx';
 
-const FEED_MESSAGE_LIMIT = 20;
-
 export default class Feed extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
       matches: this.props.matches,
-      messageLimit: FEED_MESSAGE_LIMIT,
+      messageLimit: this.props.messageLimit,
       sse: true,
     };
     this.source = (!!window.EventSource) ? new EventSource(this.props.url) : {};
@@ -30,17 +28,12 @@ export default class Feed extends React.Component {
     };
   }
   render() {
-    const feedNodes = this.state.matches.map(match => {
-      return <Panel match={match} />;
+    const feedNodes = this.state.matches.map((match, idx) => {
+      return <Panel match={match} key={idx} />;
     });
     return (
-      <div className="card">
-        <div className="card-header">
-          Live Feed
-        </div>
-        <div className="card-block">
-          {feedNodes}
-        </div>
+      <div>
+      {feedNodes}
       </div>
     );
   }
diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index 768a81b..5023498 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -5,7 +5,6 @@ export default class Markup extends React.Component {
     return { __html: this.props.match.markup };
   }
   render() {
-    debugger;
     return (
       <div className="media-body">
         <h4
diff --git a/hubs/widgets/feed.py b/hubs/widgets/feed.py
index c6e3797..f6cf651 100755
--- a/hubs/widgets/feed.py
+++ b/hubs/widgets/feed.py
@@ -34,9 +34,8 @@ fmn_url = config['fmn.url']
 bazillion = 1000
 paths = fmn.lib.load_rules(root='fmn.rules')
 
-# No chrome around the feed.
-# from hubs.widgets.chrome import panel
-# chrome = panel()
+from hubs.widgets.chrome import panel
+chrome = panel('Live Feed')
 template = templating.environment.get_template('templates/feed.html')
 position = 'left'
 
@@ -86,7 +85,11 @@ class PythonObjectEncoder(json.JSONEncoder):
           default="irc",  # TODO - Make this 'hubs', or...
           validator=validators.fmn_context,
           help="A FMN context.")
-def data(session, widget, username, fmn_context):
+@argument(name="message_limit",
+          default=20,
+          validator=validators.integer,
+          help="Max number of feed messages to display")
+def data(session, widget, username, fmn_context, message_limit):
     messages = []
     matches = []
     preference = get_remote_preference(username, fmn_context)
@@ -151,7 +154,7 @@ def data(session, widget, username, fmn_context):
         # And tack on a unique identifier for each top level entry.
         match['dom_id'] = six.text_type(uuid.uuid4())
     matches = json.dumps(matches, cls=PythonObjectEncoder)
-    return dict(matches=matches)
+    return dict(matches=matches, message_limit=message_limit)
 
 
 @hint(ubiquitous=True)

From 721c1e990e93acb1827cf638ca413a191847d75c Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 7/11] Fix human readable time, clean up JS/JSX


---

diff --git a/.gitignore b/.gitignore
index 7787cd3..74ff6cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ build/
 scratch/
 
 npm-debug.log
+client_secrets.json
\ No newline at end of file
diff --git a/hubs/static/client/.eslintrc.json b/hubs/static/client/.eslintrc.json
new file mode 100644
index 0000000..079ce96
--- /dev/null
+++ b/hubs/static/client/.eslintrc.json
@@ -0,0 +1,8 @@
+{
+  "extends": "airbnb",
+  "rules": {
+    "react/prop-types": "off",
+    "arrow-body-style": "off",
+    "func-names": "off"
+  }
+}
diff --git a/hubs/static/client/app/components/Constituent.jsx b/hubs/static/client/app/components/Constituent.jsx
deleted file mode 100644
index 3691755..0000000
--- a/hubs/static/client/app/components/Constituent.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from 'react';
-
-export default class Constituent extends React.Component {
-  render() {
-    const match = this.props.match;
-    const hasConstituent = (match['msg_ids'].length === 1 && Object.keys(match['msg_ids'])[0]['long_form'] !== Object.keys(match['msg_ids'])[0]['subtitle']);
-    if ( hasConstituent ) {
-      const constituentKey = Object.keys(match['msg_ids'])[0];
-      const constituent = match['msg_ids'][constituentKey];
-      const differentIcon = constituent['icon'] != constituent['__icon__'];
-      const longLink = ( constituent['link'] && constituent['long_form'].indexOf(constituent['link']) === -1 );
-
-      let constituentIcon;
-      if (differentIcon)
-        constituentIcon = (
-          <div className="media-left media-top">
-            <a href={constituent['link']}>
-              <img className="media-object" src={constituent['icon']} alt={constituent['subtitle']}/>
-            </a>
-          </div>
-
-        )
-
-      let readMore;
-      if(longLink)
-        readMore = (
-          <div className="media-body">
-            {constituent['long_form']}
-            <a href={constituent['link']} target="_blank">
-              Read more <span class="glyphicon glyphicon-new-window"></span>
-            </a>
-          </div>
-        )
-
-      return (
-        <div id="content-{{match['dom_id']}}" className="media">
-          <ExpandCollapse match={this.props.match} />
-          {constituentIcon}
-          {readMore}
-        </div>
-      )
-    } else if (match['msg_ids'].length > 1) {
-      <div id="content-{{match['dom_id']}}" className="media">
-        <ExpandCollapse match={this.props.match} />
-        <MessageList msgs={this.match['msg_ids']} />
-      </div>
-    }
-    else {
-      return null
-    }
-  }
-}
diff --git a/hubs/static/client/app/components/ExpandCollapse.jsx b/hubs/static/client/app/components/ExpandCollapse.jsx
deleted file mode 100644
index 29b1e6f..0000000
--- a/hubs/static/client/app/components/ExpandCollapse.jsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-
-export default class ExpandCollapse extends React.Component {
-  render() {
-    return (
-      <div className="pull-right">
-        <a id="expand-{this.props.match['dom_id']}" href="javascript:expand_feed_entries({this.props.match['dom_id']});" className="hidden">
-          <span className="glyphicon glyphicon-chevron-down"></span>
-          expand
-        </a>
-        <a id="collapse-{this.props.match['dom_id']}" href="javascript:collapse_feed_entries('{this.props.match['dom_id']}');">
-          <span className="glyphicon glyphicon-chevron-up"></span>
-          collapse
-        </a>
-      </div>
-    )
-  }
-}
diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index 5023498..66f4da4 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -1,4 +1,5 @@
 import React from 'react';
+import TimeAgo from 'react-timeago';
 
 export default class Markup extends React.Component {
   createMarkup() {
@@ -12,7 +13,7 @@ export default class Markup extends React.Component {
           dangerouslySetInnerHTML={this.createMarkup()}
         >
         </h4>
-        {this.props.match.human_time}
+        <TimeAgo date={this.props.match.date_time} />
       </div>
     );
   }
diff --git a/hubs/static/client/app/components/Messages.jsx b/hubs/static/client/app/components/Messages.jsx
deleted file mode 100644
index a773b52..0000000
--- a/hubs/static/client/app/components/Messages.jsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-
-class Message extends React.Component {
-  render() {
-    if(this.props.constituent['link']) {
-      return (
-        <li>
-          {this.props.constituent['markup']}
-          <a href={this.props.constituent['link']} target="_blank">
-            <span className="glyphicon glyphicon-new-window"></span>
-          </a>
-        </li>
-      )
-    } else {
-      return (<li>{ this.props.constituent['markup'] }</li>)
-    }
-  }
-}
-
-class MessageList extends React.Component {
-  render() {
-    const messageNodes =  this.props.items.map(function(item) {
-      return <Message constituent={item}/>;
-    });
-    return (
-      <ul id="content-{this.props.match['dom_id']}" className="hidden list-unstyled">
-        {messageNodes}
-      </ul>
-    )
-  }
-}
-
-module.exports = {
-  Message: Message,
-  MessageList: MessageList
-}
diff --git a/hubs/static/client/app/index.jsx b/hubs/static/client/app/index.jsx
index a753c36..b12429e 100644
--- a/hubs/static/client/app/index.jsx
+++ b/hubs/static/client/app/index.jsx
@@ -1,4 +1,4 @@
 import React from 'react';
 import { render } from 'react-dom';
 
-import Feed from './components/Feed.jsx'
+import Feed from './components/Feed.jsx';
diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json
index 045880c..f717e0d 100644
--- a/hubs/static/client/package.json
+++ b/hubs/static/client/package.json
@@ -7,29 +7,24 @@
     "doc": "docs"
   },
   "dependencies": {
-    "babel-core": "~6.9.1",
     "babel-loader": "~6.2.4",
     "babel-plugin-transform-class-properties": "^6.10.2",
     "babel-preset-es2015": "~6.9.0",
     "babel-preset-react": "~6.5.0",
-    "moment": "^2.14.1",
     "react": "~15.1.0",
     "react-dom": "~15.1.0",
-    "reactify": "~1.1.1",
+    "react-timeago": "^3.1.1",
     "webpack": "~1.13.1"
   },
   "devDependencies": {
     "eslint": "^2.13.1",
-    "eslint-config-airbnb": "^9.0.1",
-    "eslint-plugin-import": "^1.10.2",
-    "eslint-plugin-jsx-a11y": "^1.5.5",
-    "eslint-plugin-react": "^5.2.2"
+    "depcheck": "~0.6.3"
   },
   "scripts": {
     "dev": "webpack --watch",
     "build": "webpack -p",
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "eslint app/* && depcheck ."
   },
   "author": "",
-  "license": "BSD"
+  "license": "AGPL-3.0"
 }
diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html
index 4874fb0..9973c29 100644
--- a/hubs/widgets/templates/feed.html
+++ b/hubs/widgets/templates/feed.html
@@ -7,7 +7,8 @@
  (function() {
    const FeedElement = React.createElement(Feed, {
      matches: {{ matches }},
-     url: ':8080/user/skrzepto'
+     url: 'localhost:8080/user/skrzepto',
+     messageLimit: {{ message_limit }}
    });
    reactRender(FeedElement, document.getElementById('feed'));
  })();

From 791b715958004f08f03dbabc82629b370eda9f4a Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 8/11] Use application config to set the SSE url


* Use app.config to set SSE url
* Pin min version for flask-oidc
* Readme instructions for SSE url

---

diff --git a/.gitignore b/.gitignore
index 74ff6cc..096613a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@ build/
 scratch/
 
 npm-debug.log
-client_secrets.json
\ No newline at end of file
+client_secrets.json
+hubs.js
diff --git a/README.rst b/README.rst
index 095bc30..531a78a 100644
--- a/README.rst
+++ b/README.rst
@@ -205,14 +205,15 @@ will just grow and grow over time::
     [2015-07-01 14:33:21][    fedmsg    INFO] copr has 6 entries
     [2015-07-01 14:33:21][    fedmsg    INFO] askbot has 2 entries
 
-Build JavaScript assets. Add ``--w`` for live reloading::
+Change the ``SSE_URL`` location in ``default_config.py`` to point to the
+streaming server then build JavaScript assets. Add ``--w`` for live reloading::
 
     $ cd hubs/static/client && node_modules/.bin/webpack
 
 **Lastly**, (fingers crossed) start up the fedora-hubs webapp and load your
 profile page. Change back to the project root and run::
 
-  $ python runserver.py
+  $ python runserver.py -c config
 
 Once there are some messages that get into your local database
 that *should* show up on your feed.. they should appear there.  (At very least,
diff --git a/hubs/default_config.py b/hubs/default_config.py
index 0ce12f8..4125923 100755
--- a/hubs/default_config.py
+++ b/hubs/default_config.py
@@ -9,6 +9,7 @@ PROMOTED_GROUPS = [
 
 HUB_OF_THE_MONTH = 'commops'
 
+SSE_URL = 'http://localhost:8080/user/'
 
 OIDC_CLIENT_SECRETS = os.path.join(os.path.dirname(
     os.path.abspath(__file__)), '..', 'client_secrets.json')
diff --git a/hubs/defaults.py b/hubs/defaults.py
index 4b2049d..ddd04db 100755
--- a/hubs/defaults.py
+++ b/hubs/defaults.py
@@ -11,7 +11,8 @@ def add_user_widgets(session, hub, username, fullname):
         plugin='feed', index=0, left=True,
         _config=json.dumps({
             'username': username,
-            'fmn_context': 'irc',  # TODO -- make this 'hubs'
+            'fmn_context': 'irc',  # TODO -- make this 'hubs',
+            'message_limit': 20
         }))
     hub.widgets.append(widget)
 
diff --git a/hubs/widgets/feed.py b/hubs/widgets/feed.py
index f6cf651..42b3cb5 100755
--- a/hubs/widgets/feed.py
+++ b/hubs/widgets/feed.py
@@ -92,6 +92,9 @@ class PythonObjectEncoder(json.JSONEncoder):
 def data(session, widget, username, fmn_context, message_limit):
     messages = []
     matches = []
+    # Avoid circular import
+    from hubs.app import app
+    feed_url = app.config['SSE_URL'] + username
     preference = get_remote_preference(username, fmn_context)
     if preference:
         try:
@@ -114,7 +117,7 @@ def data(session, widget, username, fmn_context, message_limit):
             total, pages, rows = datanommer.models.Message.grep(
                 start=end - delta,
                 end=end,
-                rows_per_page=bazillion,
+                rows_per_page=message_limit,
                 page=page,
                 order='desc',
                 **fmn_hinting
@@ -154,7 +157,11 @@ def data(session, widget, username, fmn_context, message_limit):
         # And tack on a unique identifier for each top level entry.
         match['dom_id'] = six.text_type(uuid.uuid4())
     matches = json.dumps(matches, cls=PythonObjectEncoder)
-    return dict(matches=matches, message_limit=message_limit)
+    return dict(
+        matches=matches,
+        message_limit=message_limit,
+        feed_url=feed_url
+    )
 
 
 @hint(ubiquitous=True)
diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html
index 9973c29..755bec9 100644
--- a/hubs/widgets/templates/feed.html
+++ b/hubs/widgets/templates/feed.html
@@ -7,7 +7,7 @@
  (function() {
    const FeedElement = React.createElement(Feed, {
      matches: {{ matches }},
-     url: 'localhost:8080/user/skrzepto',
+     url: '{{ feed_url }}',
      messageLimit: {{ message_limit }}
    });
    reactRender(FeedElement, document.getElementById('feed'));
diff --git a/requirements.txt b/requirements.txt
index 883cb24..01e9cc9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,7 @@ dogpile.cache
 fedmsg
 fedmsg_meta_fedora_infrastructure
 flask
-flask-oidc
+flask-oidc>=1.0.3
 fmn.lib
 fmn.rules
 gunicorn

From d89312eb0e4eb5999a58f722dd640da4efe5f061 Mon Sep 17 00:00:00 2001
From: Eric Barbour <emb4gu@virginia.edu>
Date: Jul 14 2016 14:40:39 +0000
Subject: [PATCH 9/11] Remove 'irc' feed context, add package.json author


---

diff --git a/hubs/defaults.py b/hubs/defaults.py
index ddd04db..706bb03 100755
--- a/hubs/defaults.py
+++ b/hubs/defaults.py
@@ -11,7 +11,6 @@ def add_user_widgets(session, hub, username, fullname):
         plugin='feed', index=0, left=True,
         _config=json.dumps({
             'username': username,
-            'fmn_context': 'irc',  # TODO -- make this 'hubs',
             'message_limit': 20
         }))
     hub.widgets.append(widget)
diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json
index f717e0d..235be28 100644
--- a/hubs/static/client/package.json
+++ b/hubs/static/client/package.json
@@ -25,6 +25,6 @@
     "build": "webpack -p",
     "test": "eslint app/* && depcheck ."
   },
-  "author": "",
+  "author": "Eric Barbour <ebarbour@redhat.com>",
   "license": "AGPL-3.0"
 }
diff --git a/hubs/widgets/feed.py b/hubs/widgets/feed.py
index 42b3cb5..d3870c3 100755
--- a/hubs/widgets/feed.py
+++ b/hubs/widgets/feed.py
@@ -81,21 +81,17 @@ class PythonObjectEncoder(json.JSONEncoder):
           default=None,
           validator=validators.username,
           help="A FAS username.")
-@argument(name="fmn_context",
-          default="irc",  # TODO - Make this 'hubs', or...
-          validator=validators.fmn_context,
-          help="A FMN context.")
 @argument(name="message_limit",
           default=20,
           validator=validators.integer,
           help="Max number of feed messages to display")
-def data(session, widget, username, fmn_context, message_limit):
+def data(session, widget, username, message_limit):
     messages = []
     matches = []
     # Avoid circular import
     from hubs.app import app
     feed_url = app.config['SSE_URL'] + username
-    preference = get_remote_preference(username, fmn_context)
+    preference = get_remote_preference(username, 'sse')
     if preference:
         try:
             preference = rehydrate_preference(preference)
@@ -167,7 +163,7 @@ def data(session, widget, username, fmn_context, message_limit):
 @hint(ubiquitous=True)
 def should_invalidate(message, session, widget):
     username = widget.config['username']
-    fmn_context = widget.config['fmn_context']
+    fmn_context = 'sse'
     preference = get_remote_preference(username, fmn_context)
     if not preference:
         return False

From 7866df8ec4033b9919e201a7a73303925b5ffc38 Mon Sep 17 00:00:00 2001
From: Eric Barbour <emb4gu@virginia.edu>
Date: Jul 14 2016 17:09:01 +0000
Subject: [PATCH 10/11] Fix tests fmn_context change + fedmsg api removal


---

diff --git a/hubs/tests/test_api/test_fedmsg.py b/hubs/tests/test_api/test_fedmsg.py
deleted file mode 100644
index 332d4d1..0000000
--- a/hubs/tests/test_api/test_fedmsg.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import json
-import os
-
-import hubs.tests
-import hubs.models
-from hubs.app import app
-from hubs.tests import json_path
-
-
-class TestFeed(hubs.tests.APPTest):
-
-    with open(json_path + 'message_involved.json', 'r') as fp:
-        message_involved = json.loads(fp.read())
-
-    with open(json_path + 'message_not_involved.json', 'r') as fp:
-        message_not_involved = json.loads(fp.read())
-
-    # TODO this test relies on a specific filter for the atelic user
-    # It would be better to fake preferences on FAS
-    user = hubs.tests.FakeAuthorization('atelic')
-
-    plugin = 'feed'
-
-    def test_returns_400_when_no_message(self):
-        payload = {'plugin': 'feed'}
-        response = self.app.get('/api/fedmsg/markup', query_string=payload)
-        self.assertEqual(response.status_code, 400)
-
-    def test_returns_400_when_no_plugin(self):
-        payload = {'message': json.dumps(self.message_involved)}
-        response = self.app.get('/api/fedmsg/markup', query_string=payload)
-        self.assertEqual(response.status_code, 400)
-
-    def test_returns_400_when_bad_plugin_name(self):
-        payload = {
-            'message': json.dumps(self.message_involved),
-            'plugin': 'notarealplugin'
-        }
-        response = self.app.get('/api/fedmsg/markup', query_string=payload)
-        self.assertEqual(response.status_code, 400)
-
-    def test_returns_403_when_not_logged_in(self):
-        payload = {
-            'message': json.dumps(self.message_involved),
-            'plugin': 'feed'
-        }
-
-        response = self.app.get('/api/fedmsg/markup', query_string=payload)
-        self.assertEqual(response.status_code, 403)
-
-    def test_returns_match_if_involved(self):
-        payload = {
-            'message': json.dumps(self.message_involved),
-            'plugin': 'feed'
-        }
-        with self.app.session_transaction() as sess:
-            sess['nickname'] = 'atelic'
-
-        with hubs.tests.auth_set(app, self.user):
-            response = self.app.get('/api/fedmsg/markup', query_string=payload)
-
-            self.assertEqual(response.status_code, 200)
-            data = json.loads(response.data)
-            self.assertTrue(data)
-            self.assertTrue(isinstance(data[0], dict))
-
-    def test_returns_no_match_if_not_involved(self):
-        payload = {
-            'message': json.dumps(self.message_not_involved),
-            'plugin': 'feed'
-        }
-        with self.app.session_transaction() as sess:
-            sess['nickname'] = 'atelic'
-
-        with hubs.tests.auth_set(app, self.user):
-            response = self.app.get('/api/fedmsg/markup', query_string=payload)
-
-            self.assertEqual(response.status_code, 200)
-            data = json.loads(response.data)
-            self.assertFalse(data)
diff --git a/hubs/tests/test_fedora_hubs_flask_api.py b/hubs/tests/test_fedora_hubs_flask_api.py
index 254b40e..7b7dbff 100644
--- a/hubs/tests/test_fedora_hubs_flask_api.py
+++ b/hubs/tests/test_fedora_hubs_flask_api.py
@@ -268,7 +268,7 @@ class HubsAPITest(hubs.tests.APPTest):
         with tests.auth_set(app, user):
             result = self.app.get('/ralph/31/edit', follow_redirects=True)
             self.assertEqual(result.status_code, 200)
-            expected_str = '<input id="fmn_context" class="form-control" type="text"'
+            expected_str = '<input id="message_limit" class="form-control" type="text"'
             self.assertTrue(expected_str in result.data)
 
     @unittest.skip("Authorization layer not present yet")

From 290a3b7b04fbc04f2ca654e1dc87d0f5e71b1dab Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Jul 14 2016 20:02:43 +0000
Subject: [PATCH 11/11] Explain seemingly unused import in index.jsx


---

diff --git a/hubs/static/client/app/index.jsx b/hubs/static/client/app/index.jsx
index b12429e..7641684 100644
--- a/hubs/static/client/app/index.jsx
+++ b/hubs/static/client/app/index.jsx
@@ -1,4 +1,5 @@
-import React from 'react';
-import { render } from 'react-dom';
-
-import Feed from './components/Feed.jsx';
+/* This import is needed even though it's not used
+ * since the feed component places itself on window.
+ * The feed will not work without it.
+ */
+import Feed from './components/Feed.jsx'; // eslint-disable-line

