From 7c678a851ff699e40393f9cb52086eba7b543c16 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 1/16] Naive implementaion of stream prototype


---

diff --git a/hubs/app.py b/hubs/app.py
index 067a3ad..4432e91 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -6,6 +6,7 @@ import os
 import urlparse
 import uuid
 
+import bleach
 import flask
 import flask.json
 import fmn.lib
@@ -103,7 +104,7 @@ def index():
     if not authenticated():
         return flask.redirect(flask.url_for('login'))
 
-    return flask.redirect(flask.url_for('hub', name=flask.g.auth.nickname))
+    return flask.redirect(flask.url_for('stream', name=flask.g.auth.nickname))
 
 
 @app.route('/groups')
@@ -519,6 +520,42 @@ def login_required(function):
 
     return decorated_function
 
+@app.route('/<name>/stream')
+@app.route('/<name>/stream/')
+@login_required
+def stream(name):
+    feeds = [
+        {'url': '', 'name': 'streamFeed', 'matches': []},
+        {'url': '', 'name': 'actionsFeed', 'matches': []},
+        {'url': '', 'name': 'mentionsFeed', 'matches': []},
+    ]
+    saved = hubs.models.SavedNotification.by_username(session, name)
+
+    return flask.render_template('stream.html', feeds=json.dumps(feeds), saved=saved)
+
+
+@app.route('/<user>/notifications')
+@app.route('/<user>/notifications/')
+@login_required
+def notifications(user):
+    if flask.request.method == 'GET':
+        notifications = hubs.models.SavedNotification.by_username(session, user)
+        notifications = [n.__json__() for n in notifications]
+        return json.dumps(notifications)
+
+    if flask.request.method == 'POST':
+        markup = bleach.clean(flask.request.data['markup'])
+        link = bleach.clean(flask.request.data['link'])
+        icon = flask.request.data['icon']
+        notification = hubs.models.SavedNotification(
+            username=user,
+            markup=markup,
+            link=link,
+            secondary_icon=icon
+        )
+        session.add(notification)
+        session.commit()
+        return notification.__json__()
 
 @app.before_request
 def check_auth():
diff --git a/hubs/models.py b/hubs/models.py
index ede60dc..c8021e4 100755
--- a/hubs/models.py
+++ b/hubs/models.py
@@ -28,12 +28,13 @@ import operator
 import os
 import random
 
+import bleach
 import sqlalchemy as sa
 from sqlalchemy import create_engine
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import sessionmaker
 from sqlalchemy.orm import scoped_session
-from sqlalchemy.orm import relation
+from sqlalchemy.orm import relation, relationship
 from sqlalchemy.orm import backref
 
 import fedmsg
@@ -353,6 +354,8 @@ class User(BASE):
     username = sa.Column(sa.Text, primary_key=True)
     fullname = sa.Column(sa.Text)
     created_on = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)
+    saved_notifications = relationship('SavedNotification', backref='users',
+                                       lazy='dynamic')
 
     def __json__(self, session):
         return {
@@ -478,3 +481,38 @@ class VisitCounter(BASE):
             session.add(self)
             session.commit()
         return self
+
+
+class SavedNotification(BASE):
+    __tablename__ = 'savednotifications'
+    user = sa.Column(sa.Text, sa.ForeignKey('users.username'))
+
+    idx = sa.Column(sa.Integer, primary_key=True)
+    markup = sa.Column(sa.Text)
+    link = sa.Column(sa.Text)
+    secondary_icon = sa.column(sa.Text)
+    created = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)
+
+
+    def __init__(self, username=None, markup='', link='', secondary_icon=''):
+        self.user = username
+        self.markup = markup
+        self.link = link
+        self.secondary_icon = secondary_icon
+
+    def __json__(self):
+        return {
+            'idx': self.idx,
+            'markup': bleach.linkify(self.markup),
+            'link': bleach.linkify(self.link),
+            'secondary_icon': self.secondary_icon,
+            'created': self.created
+        }
+
+    @classmethod
+    def by_username(cls, session, username):
+        return session.query(cls).filter_by(user=username)
+
+    @classmethod
+    def all(cls, session):
+        return session.query(cls).all()
diff --git a/hubs/templates/_messages.html b/hubs/templates/_messages.html
new file mode 100644
index 0000000..7f942d3
--- /dev/null
+++ b/hubs/templates/_messages.html
@@ -0,0 +1,294 @@
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
+<div class="panel panel-default panel-visible">
+  <div clas="panel-body">
+    <div class="media-left">
+      <a href="#" target="_blank">
+        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
+      </a>
+    </div>
+    <div className="media-body">
+      <h4 className="media-heading">
+        This is some markup
+      </h4>
+    </div>
+  </div>
+</div>
diff --git a/hubs/templates/includes/_library.html b/hubs/templates/includes/_library.html
new file mode 100644
index 0000000..c4acbea
--- /dev/null
+++ b/hubs/templates/includes/_library.html
@@ -0,0 +1,22 @@
+<div class="col-sm-3">
+  <div class="card-header">
+    My Library
+    <div class="pull-right widget-buttons">
+      <a href="#"><span class="oi" data-glyph="external-link"></span></a>
+      <a data-target="#edit_modal" data-toggle="modal" type="button"
+        class="edit_widget" data-idx="">
+        <span class="oi" data-glyph="pencil"></span>
+      </a>
+    </div>
+  </div>
+  <div class="card">
+    <div class="card-block">
+      <p class="card-text">
+        <li>Lorem</li>
+        <li>Ipsum</li>
+        <li>Sit</li>
+        <li>Amet</li>
+      </p>
+    </div>
+  </div>
+</div>
diff --git a/hubs/templates/includes/_searchstream.html b/hubs/templates/includes/_searchstream.html
new file mode 100644
index 0000000..60b2caf
--- /dev/null
+++ b/hubs/templates/includes/_searchstream.html
@@ -0,0 +1,7 @@
+<!-- NOT HOOKED UP TO ANYTHING XOXO -->
+<div class="input-group">
+  <input type="search" class="form-control" placeholder="Search this activity stream..." aria-describedby="searchform-addon"></input>
+  <span class="input-group-addon" id="searchform-addon">
+    <span class="oi" data-glyph="magnifying-glass"></span>
+  </span>
+</div>
diff --git a/hubs/templates/master.html b/hubs/templates/master.html
index eae7c6f..1e19d91 100644
--- a/hubs/templates/master.html
+++ b/hubs/templates/master.html
@@ -66,6 +66,7 @@
         <ul class="nav navbar-nav">
           <li class="p-x-1 nav-item {% if request.path.endswith('/' + g.auth.user.username + '/') %}active{% endif %}">
             <a class="nav-link" href="/{{g.auth.user.username}}">me</a></li>
+          <li class="p-x-1 nav-item"><a class="nav-link" href="/{{g.auth.user.username}}/stream">My Stream</a></li>
           {% for hub in g.auth.user.bookmarks %}
           <li class='p-x-1 nav-item idle-{{hub.activity_class}}{% if request.path.endswith('/' + hub.name + '/') %} active{% endif %}'>
             <a class="nav-link" href="/{{hub.name}}">{{hub.name}}</a></li>
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
new file mode 100644
index 0000000..fb36b2d
--- /dev/null
+++ b/hubs/templates/stream.html
@@ -0,0 +1,78 @@
+{% extends "master.html" %}
+
+{% block title %}My Stream{% endblock %}
+{% block content %}
+<div class="jumbotron">
+  <h1>My Stream</h1>
+  <p>Notifications, actions, and other things of interest to me</p>
+</div>
+
+<ul class="nav nav-tabs" role="tablist">
+  <li class="nav-item">
+    <a class="nav-link active" href="#stream" role="tab" data-toggle="tab">My stream</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" href="#actions" role="tab" data-toggle="tab">My actions</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" href="#mentions" role="tab" data-toggle="tab">My Mentions</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" href="#saved" role="tab" data-toggle="tab">Saved Notifications</a>
+  </li>
+</ul>
+
+<!-- Tab panes -->
+<div class="tab-content">
+  <div role="tabpanel" class="tab-pane fade in active" id="stream">
+    <div class="container">
+      <div class="row">
+        <div class="col-sm-9 pull-left">
+          {% include "includes/_searchstream.html" %}
+          <div class="alert alert-warning" role="alert">
+            This stream is filtered. <a href="#">View Filters</a>
+          </div>
+          <div id="streamFeed"></div>
+        </div>
+        {% include "includes/_library.html" %}
+      </div>
+    </div>
+  </div>
+
+  <div role="tabpanel" class="tab-pane fade" id="actions">
+    <div id="actionsFeed"></div>
+  </div>
+  <div role="tabpanel" class="tab-pane fade" id="mentions">
+    <div id="mentionsFeed"></div>
+  </div>
+  <div role="tabpanel" class="tab-pane fade" id="saved">
+    <div id="savedFeed"></div>
+  </div>
+</div>
+<script type="text/javascript"
+        src ="{{url_for('static', filename='js/hubs.js')}}"></script>
+<script>
+ (function() {
+   var feeds = {{ feeds|safe }};
+   feeds.forEach(function (feed) {
+     var element = React.createElement(Feed, {
+       matches: feed.matches,
+       url: feed.url,
+       messageLimit: 100
+     });
+     reactRender(element, document.getElementById(feed.name));
+   });
+   var savedFeed = React.createElement(Feed, {
+     matches: {{saved}},
+     url: null,
+     messageLimit: 100
+   });
+   reactRender(savedFeed, document.getElementById('saved'));
+ })();
+</script>
+<style>
+ .jumbotron {
+   margin-bottom: 0;
+ }
+</style>
+{% endblock %}

From c7c827daec9eb5044e8ab8b264921baaf96ca91d Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 2/16] Front and back ends for SavedNotifications


---

diff --git a/hubs/app.py b/hubs/app.py
index 4432e91..957a809 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -19,6 +19,7 @@ from flask.ext.oidc import OpenIDConnect
 import fmn.lib
 import hubs.models
 import hubs.widgets
+from hubs.widgets import clean_input
 
 import datanommer.models
 
@@ -530,23 +531,29 @@ def stream(name):
         {'url': '', 'name': 'mentionsFeed', 'matches': []},
     ]
     saved = hubs.models.SavedNotification.by_username(session, name)
+    saved = [n.__json__() for n in saved]
 
-    return flask.render_template('stream.html', feeds=json.dumps(feeds), saved=saved)
+    return flask.render_template('stream.html', feeds=json.dumps(feeds), saved=json.dumps(saved))
 
 
-@app.route('/<user>/notifications')
-@app.route('/<user>/notifications/')
+@app.route('/<user>/notifications', methods=['GET', 'POST'])
+@app.route('/<user>/notifications/', methods=['GET', 'POST'])
 @login_required
 def notifications(user):
     if flask.request.method == 'GET':
         notifications = hubs.models.SavedNotification.by_username(session, user)
         notifications = [n.__json__() for n in notifications]
-        return json.dumps(notifications)
+        return flask.jsonify(notifications)
 
     if flask.request.method == 'POST':
-        markup = bleach.clean(flask.request.data['markup'])
-        link = bleach.clean(flask.request.data['link'])
-        icon = flask.request.data['icon']
+        data = flask.request.json
+        try:
+            # TODO clean these with pingou's new method
+            markup = data['markup']
+            link = data['link']
+            icon = data['secondary_icon']
+        except:
+            return str(flask.request.data)
         notification = hubs.models.SavedNotification(
             username=user,
             markup=markup,
@@ -555,7 +562,9 @@ def notifications(user):
         )
         session.add(notification)
         session.commit()
-        return notification.__json__()
+        return flask.jsonify(
+            dict(notification=notification.__json__(),success=True)
+        )
 
 @app.before_request
 def check_auth():
diff --git a/hubs/models.py b/hubs/models.py
index c8021e4..03217c1 100755
--- a/hubs/models.py
+++ b/hubs/models.py
@@ -490,7 +490,7 @@ class SavedNotification(BASE):
     idx = sa.Column(sa.Integer, primary_key=True)
     markup = sa.Column(sa.Text)
     link = sa.Column(sa.Text)
-    secondary_icon = sa.column(sa.Text)
+    secondary_icon = sa.Column(sa.Text)
     created = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)
 
 
@@ -502,16 +502,18 @@ class SavedNotification(BASE):
 
     def __json__(self):
         return {
+            'created': str(self.created),
+            'date_time': str(self.created),
             'idx': self.idx,
-            'markup': bleach.linkify(self.markup),
             'link': bleach.linkify(self.link),
-            'secondary_icon': self.secondary_icon,
-            'created': self.created
+            'markup': bleach.linkify(self.markup),
+            'saved': True,
+            'secondary_icon': self.secondary_icon
         }
 
     @classmethod
     def by_username(cls, session, username):
-        return session.query(cls).filter_by(user=username)
+        return session.query(cls).filter_by(user=username).all()
 
     @classmethod
     def all(cls, session):
diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 7d9c618..4086e00 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -11,6 +11,10 @@ export default class Feed extends React.Component {
       messageLimit: this.props.messageLimit,
       sse: true,
     };
+    // If it wasn't instantiated with a url or an error happened, bail
+    if (!this.props.url || !this.state.sse) {
+      return;
+    }
     this.source = (!!window.EventSource) ? new EventSource(this.props.url) : {};
     this.source.addEventListener('error', () => {
       this.state.sse = false;
diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx
index 61e8242..a5aa178 100644
--- a/hubs/static/client/app/components/Panel.jsx
+++ b/hubs/static/client/app/components/Panel.jsx
@@ -1,14 +1,39 @@
 import React from 'react';
+import $ from 'jquery';
 
 import Icon from './Icon.jsx';
 import Markup from './Markup.jsx';
 
 const Panel = function (props) {
+  const payload = {
+    link: props.match.link,
+    markup: props.match.markup,
+    secondary_icon: props.match.secondary_icon,
+  };
+
+  function onClick(data) {
+    $.ajax({
+      type: 'POST',
+      url: '/atelic/notifications/',
+      data: JSON.stringify(data),
+      contentType: 'application/json',
+    }).done(resp => {
+      const id = '#save' + resp.notification.idx;
+      const $saveBtn = $(id);
+      $saveBtn.removeClass('btn-default')
+              .addClass('disabled btn-success')
+              .text('Saved');
+      console.log(resp);
+    });
+  }
+  const button = (<button id={'save' + props.match.idx} onClick={onClick.bind(this, payload)} className="btn btn-default">SAVE</button>);
+
   return (
     <div className="panel panel-default panel-visible">
       <div className="panel-body">
         <Icon match={props.match} />
         <Markup match={props.match} />
+        {button}
       </div>
     </div>
   );
diff --git a/hubs/static/client/app/index.jsx b/hubs/static/client/app/index.jsx
index 7641684..3976b89 100644
--- a/hubs/static/client/app/index.jsx
+++ b/hubs/static/client/app/index.jsx
@@ -2,4 +2,8 @@
  * since the feed component places itself on window.
  * The feed will not work without it.
  */
+import $ from 'jquery';
+
 import Feed from './components/Feed.jsx'; // eslint-disable-line
+
+window.$ = $;
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
index fb36b2d..f940af9 100644
--- a/hubs/templates/stream.html
+++ b/hubs/templates/stream.html
@@ -46,7 +46,13 @@
     <div id="mentionsFeed"></div>
   </div>
   <div role="tabpanel" class="tab-pane fade" id="saved">
-    <div id="savedFeed"></div>
+    <div class="container">
+      <div class="row">
+        <div class="col-sm-9 pull-left">
+          <div id="savedFeed"></div>
+        </div>
+      </div>
+    </div>
   </div>
 </div>
 <script type="text/javascript"
@@ -63,11 +69,11 @@
      reactRender(element, document.getElementById(feed.name));
    });
    var savedFeed = React.createElement(Feed, {
-     matches: {{saved}},
-     url: null,
+     matches: {{saved|safe}},
+     url: false,
      messageLimit: 100
    });
-   reactRender(savedFeed, document.getElementById('saved'));
+   reactRender(savedFeed, document.getElementById('savedFeed'));
  })();
 </script>
 <style>

From 1b1493ba9bcd259fe131d511d21da265d92f7d7e Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 3/16] Data for Mentions, stream, and actions


---

diff --git a/hubs/app.py b/hubs/app.py
index 957a809..67cad13 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -52,6 +52,7 @@ import fedmsg.meta
 
 fedmsg_config = fedmsg.config.load_config()
 fedmsg.meta.make_processors(**fedmsg_config)
+paths = fmn.lib.load_rules(root='fmn.rules')
 
 session = hubs.models.init(fedmsg_config['hubs.sqlalchemy.uri'])
 datanommer.models.init(fedmsg_config['datanommer.sqlalchemy.uri'])
@@ -525,15 +526,91 @@ def login_required(function):
 @app.route('/<name>/stream/')
 @login_required
 def stream(name):
-    feeds = [
-        {'url': '', 'name': 'streamFeed', 'matches': []},
-        {'url': '', 'name': 'actionsFeed', 'matches': []},
-        {'url': '', 'name': 'mentionsFeed', 'matches': []},
-    ]
     saved = hubs.models.SavedNotification.by_username(session, name)
     saved = [n.__json__() for n in saved]
 
-    return flask.render_template('stream.html', feeds=json.dumps(feeds), saved=json.dumps(saved))
+    actions_rule = {
+        'filters': [{
+            'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
+            'id': 20506,
+            'name': 'Events referring to my username',
+            'oneshot': None,
+            'rules': [
+                {
+                    'arguments': {
+                        'fasnick': name
+                    },
+                    'cache_key': '2f59dcddf85cab5822af8d40d82c7390ce0f80b40119f2db67f3037a0594a2a1',
+                    'code_path': 'fmn.rules:user_filter',
+                    'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
+                    'info': {
+                        'args': [
+                            'config',
+                            'message',
+                            'fasnick'
+                        ],
+                        'datanommer-hints': {},
+                        'doc': "<p>Use this rule to include messages that are associated with a\nspecific user.</p>",
+                        'doc-no-links': '<html><head></head><body><p>Use this rule to include messages that are associated with a\nspecific user.</p>\n</body></html>',
+                        'hints-invertible': True,
+                        'submodule': 'generic',
+                        'title': 'A particular user'
+                    },
+                    'negated': False
+                }]
+        }]
+    }
+    actions_rule = hubs.widgets.feed.rehydrate_preference(actions_rule)
+    messages = []
+    matches = []
+
+    delta = datetime.timedelta(days=365)
+    end = datetime.datetime.utcnow()
+
+    fmn_hinting = fmn.lib.hinting.gather_hinting(fedmsg_config, actions_rule['filters'][0]['rules'], paths)
+    total, pages, rows = datanommer.models.Message.grep(
+        start=end - delta,
+        end=end,
+        rows_per_page=100,
+        page=1,
+        order='desc',
+        **fmn_hinting
+    )
+    messages.extend(rows)
+
+    for message in messages:
+        original = message.__json__()
+        if name in fedmsg.meta.msg2usernames(original, **fedmsg_config):
+            matches.append(original)
+
+    # Smash multiple messages into single ones
+    matches = fedmsg.meta.conglomerate(matches, lexers=True, **fedmsg_config)
+
+    # Apply some html markup to the subtitles for extra fanciness.
+    for match in matches:
+        match['markup'] = hubs.widgets.feed.apply_markup(match)
+        for idx, constituent in match['msg_ids'].items():
+            constituent['markup'] = hubs.widgets.feed.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())
+
+    matches = json.dumps(matches, cls=hubs.widgets.feed.PythonObjectEncoder)
+    return flask.render_template(
+        'stream.html',
+        saved=json.dumps(saved),
+        actions=matches
+    )
 
 
 @app.route('/<user>/notifications', methods=['GET', 'POST'])
@@ -563,7 +640,7 @@ def notifications(user):
         session.add(notification)
         session.commit()
         return flask.jsonify(
-            dict(notification=notification.__json__(),success=True)
+            dict(notification=notification.__json__(), success=True)
         )
 
 @app.before_request
diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 4086e00..bb5aa16 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -37,7 +37,7 @@ export default class Feed extends React.Component {
     });
     return (
       <div>
-      {feedNodes}
+        {feedNodes}
       </div>
     );
   }
diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index 66f4da4..f2464fe 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -6,6 +6,7 @@ export default class Markup extends React.Component {
     return { __html: this.props.match.markup };
   }
   render() {
+    const timestamp = this.props.date_time ? (<TimeAgo date={this.props.match.date_time} />) : null;
     return (
       <div className="media-body">
         <h4
@@ -13,7 +14,7 @@ export default class Markup extends React.Component {
           dangerouslySetInnerHTML={this.createMarkup()}
         >
         </h4>
-        <TimeAgo date={this.props.match.date_time} />
+        {timestamp}
       </div>
     );
   }
diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx
index a5aa178..1651554 100644
--- a/hubs/static/client/app/components/Panel.jsx
+++ b/hubs/static/client/app/components/Panel.jsx
@@ -26,7 +26,10 @@ const Panel = function (props) {
       console.log(resp);
     });
   }
-  const button = (<button id={'save' + props.match.idx} onClick={onClick.bind(this, payload)} className="btn btn-default">SAVE</button>);
+  const button = props.match.saved
+               ? null
+               : (<button id={'save' + props.match.idx} onClick={onClick.bind(this, payload)} className="btn btn-default">SAVE</button>);
+
 
   return (
     <div className="panel panel-default panel-visible">
diff --git a/hubs/templates/includes/_messages.html b/hubs/templates/includes/_messages.html
new file mode 100644
index 0000000..1502b48
--- /dev/null
+++ b/hubs/templates/includes/_messages.html
@@ -0,0 +1,153 @@
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
+
+<div class="panel panel-default panel-visible">
+  <div class="panel-body">
+    <div>
+      <div class="media-left">
+        <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
+      </div>
+    </div>
+    <div class="media-body">
+      <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>
+      <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>
+    </div>
+  </div>
+</div>
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
index f940af9..bde7758 100644
--- a/hubs/templates/stream.html
+++ b/hubs/templates/stream.html
@@ -40,14 +40,36 @@
   </div>
 
   <div role="tabpanel" class="tab-pane fade" id="actions">
-    <div id="actionsFeed"></div>
+    <div class="container">
+      <div class="row">
+        {% include "includes/_searchstream.html" %}
+        <div class="alert alert-warning" role="alert">
+          This stream is filtered. <a href="#">View Filters</a>
+        </div>
+        <div class="col-sm-9 pull-left">
+          <div id="actionsFeed"></div>
+        </div>
+      </div>
+    </div>
   </div>
   <div role="tabpanel" class="tab-pane fade" id="mentions">
-    <div id="mentionsFeed"></div>
+    <div class="container">
+      <div class="row">
+        {% include "includes/_searchstream.html" %}
+        <br/>
+        <div class="col-sm-9 pull-left">
+          <div id="mentionsFeed">
+            {% include "includes/_messages.html" %}
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
   <div role="tabpanel" class="tab-pane fade" id="saved">
     <div class="container">
       <div class="row">
+        <br/>
+        {% include "includes/_searchstream.html" %}
         <div class="col-sm-9 pull-left">
           <div id="savedFeed"></div>
         </div>
@@ -59,15 +81,19 @@
         src ="{{url_for('static', filename='js/hubs.js')}}"></script>
 <script>
  (function() {
-   var feeds = {{ feeds|safe }};
-   feeds.forEach(function (feed) {
-     var element = React.createElement(Feed, {
-       matches: feed.matches,
-       url: feed.url,
-       messageLimit: 100
-     });
-     reactRender(element, document.getElementById(feed.name));
+   var actions = {{actions|safe}};
+
+   var streamFeed = React.createElement(Feed, {
+     matches: actions,
+     url: false,
+     messageLimit: 100
    });
+   /* Right now, stream and actions are the same.
+    * Once, Mentions is implemented, then each will be its own
+    */
+   reactRender(streamFeed, document.getElementById('streamFeed'));
+   reactRender(streamFeed, document.getElementById('actionsFeed'));
+
    var savedFeed = React.createElement(Feed, {
      matches: {{saved|safe}},
      url: false,

From e4df27462cd833c8f7ced2badc75af431c2134eb Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 4/16] Make stream code more readable, improve save


---

diff --git a/hubs/app.py b/hubs/app.py
index 67cad13..ec76fdb 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -457,7 +457,7 @@ def widget_source(name):
     from hubs.widgets import registry
     base = '/hubs/'
     fname = ''
-    
+
     try:
         fname = base + registry[name].__file__.split(base, 1)[1]
     except KeyError:
@@ -522,6 +522,31 @@ def login_required(function):
 
     return decorated_function
 
+def apply_html(actions):
+    # Smash multiple messages into single ones
+    actions = fedmsg.meta.conglomerate(actions, lexers=True, **fedmsg_config)
+
+    # Apply some html markup to the subtitles for extra fanciness.
+    for match in actions:
+        match['markup'] = hubs.widgets.feed.apply_markup(match)
+        for idx, constituent in match['msg_ids'].items():
+            constituent['markup'] = hubs.widgets.feed.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 actions
+
+
 @app.route('/<name>/stream')
 @app.route('/<name>/stream/')
 @login_required
@@ -529,7 +554,7 @@ def stream(name):
     saved = hubs.models.SavedNotification.by_username(session, name)
     saved = [n.__json__() for n in saved]
 
-    actions_rule = {
+    preference = {
         'filters': [{
             'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
             'id': 20506,
@@ -560,56 +585,37 @@ def stream(name):
                 }]
         }]
     }
-    actions_rule = hubs.widgets.feed.rehydrate_preference(actions_rule)
+    preference = hubs.widgets.feed.rehydrate_preference(preference)
     messages = []
-    matches = []
+    actions = []
 
     delta = datetime.timedelta(days=365)
     end = datetime.datetime.utcnow()
 
-    fmn_hinting = fmn.lib.hinting.gather_hinting(fedmsg_config, actions_rule['filters'][0]['rules'], paths)
-    total, pages, rows = datanommer.models.Message.grep(
-        start=end - delta,
-        end=end,
-        rows_per_page=100,
-        page=1,
-        order='desc',
-        **fmn_hinting
-    )
-    messages.extend(rows)
+    for fmn_filter in preference['filters']:
+        fmn_hinting = fmn.lib.hinting.gather_hinting(fedmsg_config, fmn_filter['rules'], paths)
+        total, pages, rows = datanommer.models.Message.grep(
+            start=end - delta,
+            end=end,
+            rows_per_page=100,
+            page=1,
+            order='desc',
+            **fmn_hinting
+        )
+        messages.extend(rows)
 
     for message in messages:
         original = message.__json__()
         if name in fedmsg.meta.msg2usernames(original, **fedmsg_config):
-            matches.append(original)
-
-    # Smash multiple messages into single ones
-    matches = fedmsg.meta.conglomerate(matches, lexers=True, **fedmsg_config)
+            actions.append(original)
 
-    # Apply some html markup to the subtitles for extra fanciness.
-    for match in matches:
-        match['markup'] = hubs.widgets.feed.apply_markup(match)
-        for idx, constituent in match['msg_ids'].items():
-            constituent['markup'] = hubs.widgets.feed.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())
+    actions = apply_html(actions)
 
-    matches = json.dumps(matches, cls=hubs.widgets.feed.PythonObjectEncoder)
+    actions = json.dumps(actions, cls=hubs.widgets.feed.PythonObjectEncoder)
     return flask.render_template(
         'stream.html',
         saved=json.dumps(saved),
-        actions=matches
+        actions=actions
     )
 
 
@@ -629,13 +635,15 @@ def notifications(user):
             markup = data['markup']
             link = data['link']
             icon = data['secondary_icon']
+            dom_id = data['dom_id']
         except:
             return str(flask.request.data)
         notification = hubs.models.SavedNotification(
             username=user,
             markup=markup,
             link=link,
-            secondary_icon=icon
+            secondary_icon=icon,
+            dom_id=dom_id
         )
         session.add(notification)
         session.commit()
diff --git a/hubs/models.py b/hubs/models.py
index 03217c1..48ed913 100755
--- a/hubs/models.py
+++ b/hubs/models.py
@@ -487,23 +487,26 @@ class SavedNotification(BASE):
     __tablename__ = 'savednotifications'
     user = sa.Column(sa.Text, sa.ForeignKey('users.username'))
 
+    created = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)
+    dom_id = sa.Column(sa.Text)
     idx = sa.Column(sa.Integer, primary_key=True)
-    markup = sa.Column(sa.Text)
     link = sa.Column(sa.Text)
+    markup = sa.Column(sa.Text)
     secondary_icon = sa.Column(sa.Text)
-    created = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)
 
 
-    def __init__(self, username=None, markup='', link='', secondary_icon=''):
+    def __init__(self, username=None, markup='', link='', secondary_icon='', dom_id=''):
         self.user = username
         self.markup = markup
         self.link = link
         self.secondary_icon = secondary_icon
+        self.dom_id = dom_id
 
     def __json__(self):
         return {
             'created': str(self.created),
             'date_time': str(self.created),
+            'dom_id': self.dom_id,
             'idx': self.idx,
             'link': bleach.linkify(self.link),
             'markup': bleach.linkify(self.markup),
diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx
index 1651554..a30eab1 100644
--- a/hubs/static/client/app/components/Panel.jsx
+++ b/hubs/static/client/app/components/Panel.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+ import React from 'react';
 import $ from 'jquery';
 
 import Icon from './Icon.jsx';
@@ -9,6 +9,7 @@ const Panel = function (props) {
     link: props.match.link,
     markup: props.match.markup,
     secondary_icon: props.match.secondary_icon,
+    dom_id: props.match.dom_id,
   };
 
   function onClick(data) {
@@ -18,25 +19,22 @@ const Panel = function (props) {
       data: JSON.stringify(data),
       contentType: 'application/json',
     }).done(resp => {
-      const id = '#save' + resp.notification.idx;
+      const id = "#save" + props.match.dom_id;
       const $saveBtn = $(id);
-      $saveBtn.removeClass('btn-default')
-              .addClass('disabled btn-success')
-              .text('Saved');
-      console.log(resp);
+      $saveBtn.removeClass('btn-default').addClass('btn-success').text('SAVED')
     });
   }
-  const button = props.match.saved
-               ? null
-               : (<button id={'save' + props.match.idx} onClick={onClick.bind(this, payload)} className="btn btn-default">SAVE</button>);
-
+  let saveBtn;
+  if (!props.match.saved) {
+    saveBtn = (<button id={'save' + props.match.dom_id} onClick={onClick.bind(this, payload)} className="btn btn-sm btn-default">SAVE</button>);
+  }
 
   return (
     <div className="panel panel-default panel-visible">
       <div className="panel-body">
         <Icon match={props.match} />
         <Markup match={props.match} />
-        {button}
+        {saveBtn}
       </div>
     </div>
   );

From 122f0cd7420461af6f18083bebc6956ef4d0799f Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 5/16] Hide save button if notif has already been saved..


* Remove unused files
* Remove unused code in python and JSX files

---

diff --git a/hubs/app.py b/hubs/app.py
index ec76fdb..3036b6e 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -6,7 +6,6 @@ import os
 import urlparse
 import uuid
 
-import bleach
 import flask
 import flask.json
 import fmn.lib
@@ -19,7 +18,6 @@ from flask.ext.oidc import OpenIDConnect
 import fmn.lib
 import hubs.models
 import hubs.widgets
-from hubs.widgets import clean_input
 
 import datanommer.models
 
@@ -610,6 +608,9 @@ def stream(name):
             actions.append(original)
 
     actions = apply_html(actions)
+    for action in actions:
+        if any(s['markup'] == action['markup'] for s in saved):
+            action['saved'] = True
 
     actions = json.dumps(actions, cls=hubs.widgets.feed.PythonObjectEncoder)
     return flask.render_template(
diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx
index a30eab1..3bfd96a 100644
--- a/hubs/static/client/app/components/Panel.jsx
+++ b/hubs/static/client/app/components/Panel.jsx
@@ -1,5 +1,4 @@
- import React from 'react';
-import $ from 'jquery';
+import React from 'react';
 
 import Icon from './Icon.jsx';
 import Markup from './Markup.jsx';
diff --git a/hubs/static/client/app/index.jsx b/hubs/static/client/app/index.jsx
index 3976b89..7641684 100644
--- a/hubs/static/client/app/index.jsx
+++ b/hubs/static/client/app/index.jsx
@@ -2,8 +2,4 @@
  * since the feed component places itself on window.
  * The feed will not work without it.
  */
-import $ from 'jquery';
-
 import Feed from './components/Feed.jsx'; // eslint-disable-line
-
-window.$ = $;
diff --git a/hubs/templates/_messages.html b/hubs/templates/_messages.html
deleted file mode 100644
index 7f942d3..0000000
--- a/hubs/templates/_messages.html
+++ /dev/null
@@ -1,294 +0,0 @@
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
-<div class="panel panel-default panel-visible">
-  <div clas="panel-body">
-    <div class="media-left">
-      <a href="#" target="_blank">
-        <img alt="User avatar" class="media-object square-32 img-circle" src="https://placekitten.com/200/200"/>
-      </a>
-    </div>
-    <div className="media-body">
-      <h4 className="media-heading">
-        This is some markup
-      </h4>
-    </div>
-  </div>
-</div>
diff --git a/hubs/widgets/feed.py b/hubs/widgets/feed.py
index 3e6b586..8ea6536 100755
--- a/hubs/widgets/feed.py
+++ b/hubs/widgets/feed.py
@@ -1,29 +1,12 @@
 from __future__ import print_function
 
 from hubs.hinting import hint
-from hubs.widgets.base import argument, cache
+from hubs.widgets.base import argument
 from hubs.widgets import templating
 
-import fmn.lib
-import fmn.lib.hinting
-
-import datanommer.models
-
-import arrow
-import munch
-import requests
-import six
-
-import pygments.formatters
-
-import flask
-
 import hubs.validators as validators
 
-import datetime
-import json
 import logging
-import uuid
 
 log = logging.getLogger('hubs')
 
@@ -32,6 +15,7 @@ chrome = panel('Live Feed')
 template = templating.environment.get_template('templates/feed.html')
 position = 'left'
 
+
 @argument(name="username",
           default=None,
           validator=validators.username,
@@ -44,15 +28,9 @@ def data(session, widget, username, message_limit):
     # Avoid circular import
     from hubs.app import app
     feed_url = app.config['SSE_URL'] + username
-    return dict(
-        matches=[],
-        message_limit=message_limit,
-        feed_url=feed_url
-    )
+    return dict(matches=[], message_limit=message_limit, feed_url=feed_url)
+
 
 @hint(ubiquitous=True)
 def should_invalidate(message, session, widget):
     pass
-
-
-

From deb7a7f4be475755501c487ce0a3c5f53e1cea04 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 6/16] Add widget ability to my stream page


Clean up a lot of the JS code
Pull save button out to own (pure) component

---

diff --git a/hubs/app.py b/hubs/app.py
index 3036b6e..a9d1893 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -549,6 +549,7 @@ def apply_html(actions):
 @app.route('/<name>/stream/')
 @login_required
 def stream(name):
+    hub = get_hub(session, name)
     saved = hubs.models.SavedNotification.by_username(session, name)
     saved = [n.__json__() for n in saved]
 
@@ -615,6 +616,7 @@ def stream(name):
     actions = json.dumps(actions, cls=hubs.widgets.feed.PythonObjectEncoder)
     return flask.render_template(
         'stream.html',
+        hub=hub,
         saved=json.dumps(saved),
         actions=actions
     )
@@ -632,7 +634,6 @@ def notifications(user):
     if flask.request.method == 'POST':
         data = flask.request.json
         try:
-            # TODO clean these with pingou's new method
             markup = data['markup']
             link = data['link']
             icon = data['secondary_icon']
diff --git a/hubs/models.py b/hubs/models.py
index 48ed913..68197d8 100755
--- a/hubs/models.py
+++ b/hubs/models.py
@@ -34,7 +34,7 @@ from sqlalchemy import create_engine
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import sessionmaker
 from sqlalchemy.orm import scoped_session
-from sqlalchemy.orm import relation, relationship
+from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
 import fedmsg
@@ -354,7 +354,7 @@ class User(BASE):
     username = sa.Column(sa.Text, primary_key=True)
     fullname = sa.Column(sa.Text)
     created_on = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)
-    saved_notifications = relationship('SavedNotification', backref='users',
+    saved_notifications = relation('SavedNotification', backref='users',
                                        lazy='dynamic')
 
     def __json__(self, session):
diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index bb5aa16..4562dcd 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -33,7 +33,8 @@ export default class Feed extends React.Component {
   }
   render() {
     const feedNodes = this.state.matches.map((match, idx) => {
-      return <Panel match={match} key={idx} />;
+      const saveUrl = this.props.saveUrl || null;
+      return <Panel match={match} saveUrl={saveUrl} key={idx} />;
     });
     return (
       <div>
diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx
index 3bfd96a..7760893 100644
--- a/hubs/static/client/app/components/Panel.jsx
+++ b/hubs/static/client/app/components/Panel.jsx
@@ -2,30 +2,16 @@ import React from 'react';
 
 import Icon from './Icon.jsx';
 import Markup from './Markup.jsx';
+import SaveNotificationButton from './SaveNotificationButton.jsx';
 
 const Panel = function (props) {
-  const payload = {
-    link: props.match.link,
-    markup: props.match.markup,
-    secondary_icon: props.match.secondary_icon,
-    dom_id: props.match.dom_id,
-  };
-
-  function onClick(data) {
-    $.ajax({
-      type: 'POST',
-      url: '/atelic/notifications/',
-      data: JSON.stringify(data),
-      contentType: 'application/json',
-    }).done(resp => {
-      const id = "#save" + props.match.dom_id;
-      const $saveBtn = $(id);
-      $saveBtn.removeClass('btn-default').addClass('btn-success').text('SAVED')
-    });
-  }
   let saveBtn;
-  if (!props.match.saved) {
-    saveBtn = (<button id={'save' + props.match.dom_id} onClick={onClick.bind(this, payload)} className="btn btn-sm btn-default">SAVE</button>);
+  if (!props.match.saved && props.saveUrl) {
+    saveBtn = (
+      <SaveNotificationButton
+        match={props.match}
+        saveUrl={props.saveUrl}
+      />);
   }
 
   return (
diff --git a/hubs/static/client/app/components/SaveNotificationButton.jsx b/hubs/static/client/app/components/SaveNotificationButton.jsx
new file mode 100644
index 0000000..2b82d78
--- /dev/null
+++ b/hubs/static/client/app/components/SaveNotificationButton.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+
+const SaveNotificationButton = function (props) {
+  const payload = {
+    link: props.match.link,
+    markup: props.match.markup,
+    secondary_icon: props.match.secondary_icon,
+    dom_id: props.match.dom_id,
+  };
+
+  function onClick(data) {
+    $.ajax({
+      type: 'POST',
+      url: props.saveUrl,
+      data: JSON.stringify(data),
+      contentType: 'application/json',
+    }).done(() => {
+      const id = '#save' + props.match.dom_id;
+      const $saveBtn = $(id);
+      $saveBtn.removeClass('btn-default').addClass('btn-success').text('SAVED');
+    });
+  }
+  return (
+    <button
+      id={'save' + props.match.dom_id}
+      onClick={onClick.bind(this, payload)}
+      className="btn btn-sm btn-default"
+    >
+      SAVE
+    </button>
+  );
+};
+
+export default SaveNotificationButton;
diff --git a/hubs/templates/includes/_library.html b/hubs/templates/includes/_library.html
deleted file mode 100644
index c4acbea..0000000
--- a/hubs/templates/includes/_library.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<div class="col-sm-3">
-  <div class="card-header">
-    My Library
-    <div class="pull-right widget-buttons">
-      <a href="#"><span class="oi" data-glyph="external-link"></span></a>
-      <a data-target="#edit_modal" data-toggle="modal" type="button"
-        class="edit_widget" data-idx="">
-        <span class="oi" data-glyph="pencil"></span>
-      </a>
-    </div>
-  </div>
-  <div class="card">
-    <div class="card-block">
-      <p class="card-text">
-        <li>Lorem</li>
-        <li>Ipsum</li>
-        <li>Sit</li>
-        <li>Amet</li>
-      </p>
-    </div>
-  </div>
-</div>
diff --git a/hubs/templates/includes/_rightpanel.html b/hubs/templates/includes/_rightpanel.html
new file mode 100644
index 0000000..2a9f511
--- /dev/null
+++ b/hubs/templates/includes/_rightpanel.html
@@ -0,0 +1,5 @@
+<div id="right_widgets">
+  {% for widget in hub.right_widgets %}
+  <div id="widget-{{ widget.idx }}" class="widget row"></div>
+  {% endfor %}
+</div>
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
index bde7758..6da69e4 100644
--- a/hubs/templates/stream.html
+++ b/hubs/templates/stream.html
@@ -27,14 +27,16 @@
   <div role="tabpanel" class="tab-pane fade in active" id="stream">
     <div class="container">
       <div class="row">
-        <div class="col-sm-9 pull-left">
+        <div class="col-sm-8 pull-left">
           {% include "includes/_searchstream.html" %}
           <div class="alert alert-warning" role="alert">
             This stream is filtered. <a href="#">View Filters</a>
           </div>
           <div id="streamFeed"></div>
         </div>
-        {% include "includes/_library.html" %}
+        <div class="col-sm-4">
+          {% include "includes/_rightpanel.html" %}
+        </div>
       </div>
     </div>
   </div>
@@ -46,9 +48,12 @@
         <div class="alert alert-warning" role="alert">
           This stream is filtered. <a href="#">View Filters</a>
         </div>
-        <div class="col-sm-9 pull-left">
+        <div class="col-sm-8 pull-left">
           <div id="actionsFeed"></div>
         </div>
+        <div class="col-sm-4">
+          {% include "includes/_rightpanel.html" %}
+        </div>
       </div>
     </div>
   </div>
@@ -57,10 +62,13 @@
       <div class="row">
         {% include "includes/_searchstream.html" %}
         <br/>
-        <div class="col-sm-9 pull-left">
+        <div class="col-sm-8 pull-left">
           <div id="mentionsFeed">
             {% include "includes/_messages.html" %}
           </div>
+          <div class="col-sm-4">
+            {% include "includes/_rightpanel.html" %}
+          </div>
         </div>
       </div>
     </div>
@@ -70,22 +78,62 @@
       <div class="row">
         <br/>
         {% include "includes/_searchstream.html" %}
-        <div class="col-sm-9 pull-left">
+        <div class="col-sm-8 pull-left">
           <div id="savedFeed"></div>
         </div>
+        <div class="col-sm-4">
+          {% include "includes/_rightpanel.html" %}
+        </div>
       </div>
     </div>
   </div>
 </div>
+<style>
+ .jumbotron {
+   margin-bottom: 0;
+ }
+</style>
+{% endblock %}
+
+{% block jscripts %}
+{{ super() }}
 <script type="text/javascript"
-        src ="{{url_for('static', filename='js/hubs.js')}}"></script>
+  src ="{{url_for('static', filename='js/hubs.js')}}"></script>
 <script>
- (function() {
+ function setup_widgets(widgets) {
+   var all_widgets = [{% for widget in hub.widgets %}'{{ widget.idx }}',{% endfor %}];
+   if (widgets == undefined) {
+     var widgets = all_widgets;
+   }
+   $.each(widgets, function(i, widget) {
+     $.ajax({
+       url: '/{{hub.name}}/' + widget,
+       dataType: 'html',
+       success: function(html) {
+         $('#widget-' + widget).html(html);
+         setTimeout(function() {
+           $('#widget-' + widget + ' .panel').toggleClass('panel-visible');
+         }, 100);
+       },
+       error: function() {
+         $('#widget-' + widget).html('Got an error retrieving this widget.  Sorry :(');
+         console.log('error');
+         console.trace();
+       },
+     });
+   });
+ }
+ window.onload = function(){
+   setup_widgets();
+   setup_feeds();
+ }
+
+ function setup_feeds() {
    var actions = {{actions|safe}};
 
    var streamFeed = React.createElement(Feed, {
      matches: actions,
-     url: false,
+     saveUrl: '/{{hub.name}}/notifications/',
      messageLimit: 100
    });
    /* Right now, stream and actions are the same.
@@ -100,11 +148,6 @@
      messageLimit: 100
    });
    reactRender(savedFeed, document.getElementById('savedFeed'));
- })();
-</script>
-<style>
- .jumbotron {
-   margin-bottom: 0;
  }
-</style>
+</script>
 {% endblock %}

From 85f4b1d5323666a0f812d027d39b8bb7d9eb5fdb Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 7/16] Add SavedNotifications tests


---

diff --git a/hubs/app.py b/hubs/app.py
index a9d1893..925f3a3 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -632,16 +632,19 @@ def notifications(user):
         return flask.jsonify(notifications)
 
     if flask.request.method == 'POST':
-        data = flask.request.json
+        data = json.loads(flask.request.data)
+        user = hubs.models.User.by_username(session, user)
+        if not user:
+            return flask.abort(400)
         try:
             markup = data['markup']
             link = data['link']
             icon = data['secondary_icon']
             dom_id = data['dom_id']
         except:
-            return str(flask.request.data)
+            return flask.abort(400)
         notification = hubs.models.SavedNotification(
-            username=user,
+            username=user.username,
             markup=markup,
             link=link,
             secondary_icon=icon,
diff --git a/hubs/tests/__init__.py b/hubs/tests/__init__.py
index 153374a..e7f4eba 100644
--- a/hubs/tests/__init__.py
+++ b/hubs/tests/__init__.py
@@ -45,6 +45,10 @@ class APPTest(unittest.TestCase):
             fullname = user.title()
             hubs.models.User.get_or_create(
                 hubs.app.session, username=user, fullname=fullname)
+            saved_notif = hubs.models.SavedNotification(
+                username=user, markup='foo', link='bar'
+            )
+            hubs.app.session.add(saved_notif)
 
         hubs.app.session.flush()
 
diff --git a/hubs/tests/test_api/test_notifications.py b/hubs/tests/test_api/test_notifications.py
new file mode 100644
index 0000000..ea8616f
--- /dev/null
+++ b/hubs/tests/test_api/test_notifications.py
@@ -0,0 +1,84 @@
+import json
+from urlparse import urlparse
+
+import hubs.tests
+import hubs.models
+from hubs.app import app
+
+
+class TestGetNotifications(hubs.tests.APPTest):
+    user = hubs.tests.FakeAuthorization('ralph')
+
+    def test_get_notifications_invalid_name(self):
+        name = 'notarealfasuser'
+
+        with hubs.tests.auth_set(app, self.user):
+            resp = self.app.get('/{}/notifications/'.format(name))
+        self.assertEqual(resp.status_code, 200)
+        data = json.loads(resp.data)
+        self.assertEqual(data, [])
+
+    def test_get_notifications_valid_name(self):
+        with hubs.tests.auth_set(app, self.user):
+            resp = self.app.get('/{}/notifications/'.format(
+                self.user.username))
+
+        self.assertEqual(resp.status_code, 200)
+        data = json.loads(resp.data)
+        self.assertEqual(len(data), 1)
+
+        for saved in data:
+            self.assertEqual(saved['markup'], 'foo')
+            self.assertEqual(saved['link'], 'bar')
+
+
+class TestPostNotifications(hubs.tests.APPTest):
+    user = hubs.tests.FakeAuthorization('ralph')
+    valid_payload = {
+        'username': user.username,
+        'markup': 'foobar',
+        'link': 'baz',
+        'secondary_icon': 'http://placekitten.com/g/200/300',
+        'dom_id': 'reallyuniqueuid'
+    }
+
+    invalid_payload = {
+        'username': user.username,
+    }
+
+    def test_post_notification_invalid_user(self):
+        with hubs.tests.auth_set(app, self.user):
+            resp = self.app.post(
+                '/{}/notifications/'.format('notarealfasuser'),
+                data=json.dumps(self.valid_payload))
+        self.assertEqual(resp.status_code, 400)
+
+    def test_post_notification_invalid_payload(self):
+        with hubs.tests.auth_set(app, self.user):
+            resp = self.app.post(
+                '/{}/notifications/'.format(self.user.username),
+                data=json.dumps(self.invalid_payload))
+        self.assertEqual(resp.status_code, 400)
+
+    def test_post_notification_valid_payload(self):
+        with hubs.tests.auth_set(app, self.user):
+            resp = self.app.post(
+                '/{}/notifications/'.format(self.user.username),
+                data=json.dumps(self.valid_payload))
+
+        self.assertEqual(resp.status_code, 200)
+        data = json.loads(resp.data)
+        self.assertTrue(isinstance(data, dict))
+
+        notification = data['notification']
+        self.assertEqual(notification['markup'], 'foobar')
+        self.assertEqual(notification['link'], 'baz')
+
+        all_saved = hubs.models.SavedNotification.by_username(
+            hubs.app.session, self.user.username)
+        self.assertEqual(len(all_saved), 2)
+        all_saved = [s.__json__() for s in all_saved]
+        self.assertTrue(any(str(s['markup']) == self.valid_payload['markup']
+                            for s in all_saved))
+        self.assertTrue(any(str(s['link']) == self.valid_payload['link']
+                            for s in all_saved))

From 7f1455b3fcecd0bd803de44f599db89ff295dd58 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 8/16] Major improvements for Feed UI


* Revert the breaks in the card interface for feed widget from switching
to fedora-bootstrapp.

* Spacing and font size fixes

---

diff --git a/hubs/static/client/app/components/Icon.jsx b/hubs/static/client/app/components/Icon.jsx
index 08c8d60..9adf0e4 100644
--- a/hubs/static/client/app/components/Icon.jsx
+++ b/hubs/static/client/app/components/Icon.jsx
@@ -2,16 +2,14 @@ import React from 'react';
 
 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 className="media-left">
+      <a href={props.match.link ? props.match.link : '#'} target="_blank">
+        <img
+          alt="User avatar"
+          className="media-object square-32"
+          src={props.match.secondary_icon}
+        />
+      </a>
     </div>
   );
 };
diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index f2464fe..7342f58 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -1,12 +1,25 @@
 import React from 'react';
+import SaveNotificationButton from './SaveNotificationButton.jsx';
+
 import TimeAgo from 'react-timeago';
 
+
 export default class Markup extends React.Component {
   createMarkup() {
     return { __html: this.props.match.markup };
   }
   render() {
     const timestamp = this.props.date_time ? (<TimeAgo date={this.props.match.date_time} />) : null;
+
+
+    let saveBtn;
+    if (!this.props.match.saved && this.props.saveUrl) {
+      saveBtn = (
+        <SaveNotificationButton
+          match={this.props.match}
+          saveUrl={this.props.saveUrl}
+        />);
+    }
     return (
       <div className="media-body">
         <h4
@@ -15,6 +28,7 @@ export default class Markup extends React.Component {
         >
         </h4>
         {timestamp}
+        {saveBtn}
       </div>
     );
   }
diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx
index 7760893..0cd9bdf 100644
--- a/hubs/static/client/app/components/Panel.jsx
+++ b/hubs/static/client/app/components/Panel.jsx
@@ -2,24 +2,13 @@ import React from 'react';
 
 import Icon from './Icon.jsx';
 import Markup from './Markup.jsx';
-import SaveNotificationButton from './SaveNotificationButton.jsx';
 
 const Panel = function (props) {
-  let saveBtn;
-  if (!props.match.saved && props.saveUrl) {
-    saveBtn = (
-      <SaveNotificationButton
-        match={props.match}
-        saveUrl={props.saveUrl}
-      />);
-  }
-
   return (
-    <div className="panel panel-default panel-visible">
-      <div className="panel-body">
+    <div className="card">
+      <div className="card-block">
         <Icon match={props.match} />
-        <Markup match={props.match} />
-        {saveBtn}
+        <Markup match={props.match} saveUrl={props.saveUrl} />
       </div>
     </div>
   );
diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html
index cb29255..3ab75a9 100644
--- a/hubs/templates/hubs.html
+++ b/hubs/templates/hubs.html
@@ -3,6 +3,32 @@
 {% block title %}{{ hub.name }}{% endblock %}
 {% block content %}
 
+  <div class="col-md-2">
+    <nav class="navbar navbar-light p-t-2 p-b-0">
+      <button type="button" class="navbar-toggler hidden-sm-up"
+        data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
+        <span class="sr-only">Toggle navigation</span>
+        <span class="oi" data-glyph="menu"></span>
+      </button>
+
+      <!-- Collect the nav links, forms, and other content for toggling -->
+      {% if g.auth.logged_in %}
+      <div class="collapse navbar-toggleable-xs" id="bs-example-navbar-collapse-1">
+        <ul class="nav navbar-nav">
+          <li class="p-x-1 nav-item {% if request.path.endswith('/' + g.auth.user.username + '/') %}active{% endif %}">
+            <a class="nav-link" href="/{{g.auth.user.username}}">me</a></li>
+          <li class="p-x-1 nav-item"><a class="nav-link" href="/{{g.auth.user.username}}/stream">My Stream</a></li>
+          {% for hub in g.auth.user.bookmarks %}
+          <li class='p-x-1 nav-item idle-{{hub.activity_class}}{% if request.path.endswith('/' + hub.name + '/') %} active{% endif %}'>
+            <a class="nav-link" href="/{{hub.name}}">{{hub.name}}</a></li>
+          {% endfor %}
+          <!-- At the end of the list, tack on a link to all groups -->
+          <li class="p-x-1 nav-item"><a class="nav-link" href="/groups">all</a></li>
+        </ul>
+      </div><!-- /.navbar-collapse -->
+      {% endif %}
+    </nav>
+  </div>
   <header class="p-t-2">
     <div class="container">
 
diff --git a/hubs/templates/includes/_messages.html b/hubs/templates/includes/_messages.html
index 1502b48..1e89ae8 100644
--- a/hubs/templates/includes/_messages.html
+++ b/hubs/templates/includes/_messages.html
@@ -1,5 +1,5 @@
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -12,8 +12,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -26,8 +26,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -40,8 +40,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -54,8 +54,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -68,8 +68,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -82,8 +82,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -96,8 +96,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -110,8 +110,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -124,8 +124,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
@@ -138,8 +138,8 @@
   </div>
 </div>
 
-<div class="panel panel-default panel-visible">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-block">
     <div>
       <div class="media-left">
         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>
diff --git a/hubs/templates/master.html b/hubs/templates/master.html
index 1e19d91..e02081b 100644
--- a/hubs/templates/master.html
+++ b/hubs/templates/master.html
@@ -52,33 +52,6 @@
   </div>
 </div>
 <div class="bodycontent">
-  <div class="col-md-2">
-    <nav class="navbar navbar-light p-t-2 p-b-0">
-      <button type="button" class="navbar-toggler hidden-sm-up"
-        data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
-        <span class="sr-only">Toggle navigation</span>
-        <span><i class="fa fa-bars" aria-hidden="true"></i></span>
-      </button>
-
-      <!-- Collect the nav links, forms, and other content for toggling -->
-      {% if g.auth.logged_in %}
-      <div class="collapse navbar-toggleable-xs" id="bs-example-navbar-collapse-1">
-        <ul class="nav navbar-nav">
-          <li class="p-x-1 nav-item {% if request.path.endswith('/' + g.auth.user.username + '/') %}active{% endif %}">
-            <a class="nav-link" href="/{{g.auth.user.username}}">me</a></li>
-          <li class="p-x-1 nav-item"><a class="nav-link" href="/{{g.auth.user.username}}/stream">My Stream</a></li>
-          {% for hub in g.auth.user.bookmarks %}
-          <li class='p-x-1 nav-item idle-{{hub.activity_class}}{% if request.path.endswith('/' + hub.name + '/') %} active{% endif %}'>
-            <a class="nav-link" href="/{{hub.name}}">{{hub.name}}</a></li>
-          {% endfor %}
-          <!-- At the end of the list, tack on a link to all groups -->
-
-            <li class="p-x-1 nav-item"><a class="nav-link" href="/groups">all</a></li>
-        </ul>
-      </div><!-- /.navbar-collapse -->
-      {% endif %}
-    </nav>
-  </div>
 
 {% block content %}{% endblock %}
 
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
index 6da69e4..eff03f8 100644
--- a/hubs/templates/stream.html
+++ b/hubs/templates/stream.html
@@ -6,13 +6,12 @@
   <h1>My Stream</h1>
   <p>Notifications, actions, and other things of interest to me</p>
 </div>
-
-<ul class="nav nav-tabs" role="tablist">
+<ul class="nav-tabs " role="tablist">
   <li class="nav-item">
-    <a class="nav-link active" href="#stream" role="tab" data-toggle="tab">My stream</a>
+    <a class="nav-link active" href="#stream" role="tab" data-toggle="tab">My Stream</a>
   </li>
   <li class="nav-item">
-    <a class="nav-link" href="#actions" role="tab" data-toggle="tab">My actions</a>
+    <a class="nav-link" href="#actions" role="tab" data-toggle="tab">My Actions</a>
   </li>
   <li class="nav-item">
     <a class="nav-link" href="#mentions" role="tab" data-toggle="tab">My Mentions</a>
@@ -29,8 +28,9 @@
       <div class="row">
         <div class="col-sm-8 pull-left">
           {% include "includes/_searchstream.html" %}
+          <br/>
           <div class="alert alert-warning" role="alert">
-            This stream is filtered. <a href="#">View Filters</a>
+            This stream is filtered. <a href="https://apps.fedoraproject.org/notifications/{{hub.name}}.id.fedoraproject.org/">View Filters</a>
           </div>
           <div id="streamFeed"></div>
         </div>
@@ -45,8 +45,9 @@
     <div class="container">
       <div class="row">
         {% include "includes/_searchstream.html" %}
+        <br/>
         <div class="alert alert-warning" role="alert">
-          This stream is filtered. <a href="#">View Filters</a>
+          This stream is filtered. <a href="https://apps.fedoraproject.org/notifications/{{hub.name}}.id.fedoraproject.org/">View Filters</a>
         </div>
         <div class="col-sm-8 pull-left">
           <div id="actionsFeed"></div>
@@ -92,6 +93,9 @@
  .jumbotron {
    margin-bottom: 0;
  }
+ .nav-tabs {
+   list-style: none;
+ }
 </style>
 {% endblock %}
 

From b18c6c451b9f7491435fa1d3fa9231248bff662e Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 9/16] Fix failing test, add on dropdown for feed nodes


---

diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index 7342f58..e9b585e 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -1,6 +1,5 @@
 import React from 'react';
-import SaveNotificationButton from './SaveNotificationButton.jsx';
-
+import Dropdown from './Dropdown.jsx';
 import TimeAgo from 'react-timeago';
 
 
@@ -11,15 +10,6 @@ export default class Markup extends React.Component {
   render() {
     const timestamp = this.props.date_time ? (<TimeAgo date={this.props.match.date_time} />) : null;
 
-
-    let saveBtn;
-    if (!this.props.match.saved && this.props.saveUrl) {
-      saveBtn = (
-        <SaveNotificationButton
-          match={this.props.match}
-          saveUrl={this.props.saveUrl}
-        />);
-    }
     return (
       <div className="media-body">
         <h4
@@ -28,7 +18,10 @@ export default class Markup extends React.Component {
         >
         </h4>
         {timestamp}
-        {saveBtn}
+        <Dropdown
+          match={this.props.match}
+          saveUrl={this.props.saveUrl}
+        />
       </div>
     );
   }
diff --git a/hubs/static/client/app/components/SaveNotificationButton.jsx b/hubs/static/client/app/components/SaveNotificationButton.jsx
deleted file mode 100644
index 2b82d78..0000000
--- a/hubs/static/client/app/components/SaveNotificationButton.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-
-const SaveNotificationButton = function (props) {
-  const payload = {
-    link: props.match.link,
-    markup: props.match.markup,
-    secondary_icon: props.match.secondary_icon,
-    dom_id: props.match.dom_id,
-  };
-
-  function onClick(data) {
-    $.ajax({
-      type: 'POST',
-      url: props.saveUrl,
-      data: JSON.stringify(data),
-      contentType: 'application/json',
-    }).done(() => {
-      const id = '#save' + props.match.dom_id;
-      const $saveBtn = $(id);
-      $saveBtn.removeClass('btn-default').addClass('btn-success').text('SAVED');
-    });
-  }
-  return (
-    <button
-      id={'save' + props.match.dom_id}
-      onClick={onClick.bind(this, payload)}
-      className="btn btn-sm btn-default"
-    >
-      SAVE
-    </button>
-  );
-};
-
-export default SaveNotificationButton;

From e786bc3a52717f84758c6067f54aa596afeb90ca Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 10/16] Readability, README, and comment improvements


* Update README to clarify that stream needs the feed setup
* Move stream code to its own class
* Add in Dropdown.jsx
* Explain the save tagging loop
* Adjust to removal of feed.py code from #237.

---

diff --git a/README.rst b/README.rst
index 6c1b4f4..8b80f92 100644
--- a/README.rst
+++ b/README.rst
@@ -138,10 +138,10 @@ contents like this::
 Make sure to leave the '' around you username and password - don't delete them! Some widgets will work without the above info being present..
 but it is needed for a subset of them.
 
-Feed Widget - the Extra Mile
-----------------------------
+Feed Widget and My Stream - the Extra Mile
+------------------------------------------
 
-One widget (the big tamale -- the feed widget) requires more legwork to stand
+One widget (the big tamale -- the feed widget) and the My Stream page require more legwork to stand
 up.  If you just want to see how hubs works and you want to hack on other
 peripheral stuff around it, you don't need to bother with these steps.
 
diff --git a/hubs/app.py b/hubs/app.py
index 925f3a3..cc32021 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -4,20 +4,16 @@ import json
 import logging
 import os
 import urlparse
-import uuid
 
 import flask
 import flask.json
-import fmn.lib
 import munch
-import pygments.formatters
-import six
 
 from flask.ext.oidc import OpenIDConnect
 
-import fmn.lib
 import hubs.models
 import hubs.widgets
+import hubs.stream
 
 import datanommer.models
 
@@ -50,7 +46,6 @@ import fedmsg.meta
 
 fedmsg_config = fedmsg.config.load_config()
 fedmsg.meta.make_processors(**fedmsg_config)
-paths = fmn.lib.load_rules(root='fmn.rules')
 
 session = hubs.models.init(fedmsg_config['hubs.sqlalchemy.uri'])
 datanommer.models.init(fedmsg_config['datanommer.sqlalchemy.uri'])
@@ -520,31 +515,6 @@ def login_required(function):
 
     return decorated_function
 
-def apply_html(actions):
-    # Smash multiple messages into single ones
-    actions = fedmsg.meta.conglomerate(actions, lexers=True, **fedmsg_config)
-
-    # Apply some html markup to the subtitles for extra fanciness.
-    for match in actions:
-        match['markup'] = hubs.widgets.feed.apply_markup(match)
-        for idx, constituent in match['msg_ids'].items():
-            constituent['markup'] = hubs.widgets.feed.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 actions
-
-
 @app.route('/<name>/stream')
 @app.route('/<name>/stream/')
 @login_required
@@ -553,67 +523,17 @@ def stream(name):
     saved = hubs.models.SavedNotification.by_username(session, name)
     saved = [n.__json__() for n in saved]
 
-    preference = {
-        'filters': [{
-            'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
-            'id': 20506,
-            'name': 'Events referring to my username',
-            'oneshot': None,
-            'rules': [
-                {
-                    'arguments': {
-                        'fasnick': name
-                    },
-                    'cache_key': '2f59dcddf85cab5822af8d40d82c7390ce0f80b40119f2db67f3037a0594a2a1',
-                    'code_path': 'fmn.rules:user_filter',
-                    'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
-                    'info': {
-                        'args': [
-                            'config',
-                            'message',
-                            'fasnick'
-                        ],
-                        'datanommer-hints': {},
-                        'doc': "<p>Use this rule to include messages that are associated with a\nspecific user.</p>",
-                        'doc-no-links': '<html><head></head><body><p>Use this rule to include messages that are associated with a\nspecific user.</p>\n</body></html>',
-                        'hints-invertible': True,
-                        'submodule': 'generic',
-                        'title': 'A particular user'
-                    },
-                    'negated': False
-                }]
-        }]
-    }
-    preference = hubs.widgets.feed.rehydrate_preference(preference)
-    messages = []
-    actions = []
-
-    delta = datetime.timedelta(days=365)
-    end = datetime.datetime.utcnow()
-
-    for fmn_filter in preference['filters']:
-        fmn_hinting = fmn.lib.hinting.gather_hinting(fedmsg_config, fmn_filter['rules'], paths)
-        total, pages, rows = datanommer.models.Message.grep(
-            start=end - delta,
-            end=end,
-            rows_per_page=100,
-            page=1,
-            order='desc',
-            **fmn_hinting
-        )
-        messages.extend(rows)
-
-    for message in messages:
-        original = message.__json__()
-        if name in fedmsg.meta.msg2usernames(original, **fedmsg_config):
-            actions.append(original)
+    stream = hubs.stream.Stream(name)
+    actions = stream.get_actions()
 
-    actions = apply_html(actions)
+    # An action that has already been saved should not have a saved button
+    # this tags all the saved actions so the front end can decide to display or not.
+    # This will get slow pretty quickly so we should reevaluate after flock
     for action in actions:
         if any(s['markup'] == action['markup'] for s in saved):
             action['saved'] = True
 
-    actions = json.dumps(actions, cls=hubs.widgets.feed.PythonObjectEncoder)
+    actions = json.dumps(actions, cls=hubs.stream.PythonObjectEncoder)
     return flask.render_template(
         'stream.html',
         hub=hub,
diff --git a/hubs/static/client/app/components/Dropdown.jsx b/hubs/static/client/app/components/Dropdown.jsx
new file mode 100644
index 0000000..57de016
--- /dev/null
+++ b/hubs/static/client/app/components/Dropdown.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+
+export default class Dropdown extends React.Component {
+  onClick() {
+    const payload = {
+      link: this.props.match.link,
+      markup: this.props.match.markup,
+      secondary_icon: this.props.match.secondary_icon,
+      dom_id: this.props.match.dom_id,
+    };
+
+    $.ajax({
+      type: 'POST',
+      url: this.props.saveUrl,
+      data: JSON.stringify(payload),
+      contentType: 'application/json',
+    }).done(() => {
+      const id = '#save-' + this.props.match.dom_id;
+      const $saveBtn = $(id);
+      $saveBtn.text('Saved');
+    });
+  }
+
+  render() {
+    let saveBtn;
+    if (!this.props.match.saved && this.props.saveUrl) {
+      saveBtn = (
+        <a
+          className="dropdown-item"
+          onClick={this.onClick.bind(this)}
+          href="#"
+        >
+          Save
+        </a>
+      );
+    }
+
+    return (
+      <div className="dropdown pull-sm-right">
+        <span
+          className="dropdown-toggle" type="button"
+          id={'save-' + this.props.match.dom_id}
+          data-toggle="dropdown" aria-haspopup="true"
+          aria-expanded="true"
+        >
+          More
+        </span>
+        <div className="dropdown-menu" aria-labelledby="dropdownMenu2">
+          {saveBtn}
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/hubs/stream.py b/hubs/stream.py
new file mode 100644
index 0000000..2cbcadf
--- /dev/null
+++ b/hubs/stream.py
@@ -0,0 +1,153 @@
+import datetime
+import json
+import uuid
+
+import datanommer.models
+import flask
+import fmn.lib
+import munch
+import pygments.formatters
+import six
+
+import fedmsg.config
+import fedmsg.meta
+
+
+# TODO Temporary: This is a proof of concept for Flock
+# This needs to be done in fnm.lib
+class Stream(object):
+    paths = fmn.lib.load_rules(root='fmn.rules')
+    fedmsg_config = fedmsg.config.load_config()
+    delta = datetime.timedelta(days=365)
+    end = datetime.datetime.utcnow()
+
+    def __init__(self, name):
+        self.name = name
+        self.preference = {
+            'filters': [{
+                'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
+                'id': 20506,
+                'name': 'Events referring to my username',
+                'oneshot': None,
+                'rules': [
+                    {
+                        'arguments': {
+                            'fasnick': name
+                        },
+                        'cache_key':
+                        '2f59dcddf85cab5822af8d40d82c7390ce0f80b40119f2db67f3037a0594a2a1',
+                        'code_path': 'fmn.rules:user_filter',
+                        'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
+                        'info': {
+                            'args': [
+                                'config', 'message', 'fasnick'
+                            ],
+                            'datanommer-hints': {},
+                            'doc':
+                            "<p>Use this rule to include messages that are associated with a\nspecific user.</p>",
+                            'doc-no-links':
+                            '<html><head></head><body><p>Use this rule to include messages that are associated with a\nspecific user.</p>\n</body></html>',
+                            'hints-invertible': True,
+                            'submodule': 'generic',
+                            'title': 'A particular user'
+                        },
+                        'negated': False
+                    }
+                ]
+            }]
+        }
+
+    def apply_html(self, matches):
+        # Smash multiple messages into single ones
+        matches = fedmsg.meta.conglomerate(matches,
+                                           lexers=True,
+                                           **self.fedmsg_config)
+
+        # Apply some html markup to the subtitles for extra fanciness.
+        for match in matches:
+            match['markup'] = self.apply_markup(match)
+            for idx, constituent in match['msg_ids'].items():
+                constituent['markup'] = self.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 matches
+
+    def get_actions(self):
+        preference = self.rehydrate_preference(self.preference)
+        messages = []
+        actions = []
+
+        for fmn_filter in preference['filters']:
+            fmn_hinting = fmn.lib.hinting.gather_hinting(
+                self.fedmsg_config, fmn_filter['rules'], self.paths)
+            total, pages, rows = datanommer.models.Message.grep(
+                start=self.end - self.delta,
+                end=self.end,
+                rows_per_page=100,
+                page=1,
+                order='desc',
+                **fmn_hinting)
+            messages.extend(rows)
+
+        for message in messages:
+            original = message.__json__()
+            if self.name in fedmsg.meta.msg2usernames(original, **
+                                                      self.fedmsg_config):
+                actions.append(original)
+
+        return self.apply_html(actions)
+
+    def rehydrate_preference(self, preference):
+        for fltr in preference['filters']:
+            fltr['rules'] = [munch.Munch(r) for r in fltr['rules']]
+            for rule in fltr['rules']:
+                code_path = str(rule.code_path)
+                rule.fn = fedmsg.utils.load_class(code_path)
+
+        return preference
+
+    def apply_markup(self, match):
+        markup = match['subtitle']
+        for username in match['usernames']:
+            args = (username, '<a href="{url}">@{username}</a>'.format(
+                url=flask.url_for('hub', name=username),
+                username=username, ))
+            markup = markup.replace(*args)
+            # TODO -- have to add these to conglomerate first
+            # match['long_form'] = match['long_form'].replace(*args)
+
+        for package in match['packages']:
+            args = (package, '<a href="{url}">{package}</a>'.format(
+                url='https://apps.fedoraproject.org/packages/' + package,
+                package=package, ))
+            markup = markup.replace(*args)
+            # TODO -- have to add these to conglomerate first
+            # match['long_form'] = match['long_form'].replace(*args)
+
+        return markup
+
+
+class PythonObjectEncoder(json.JSONEncoder):
+    ''' A JSON encoder that handles python data structures in encoding'''
+
+    def default(self, obj):
+        if isinstance(obj, (list, dict, str, unicode, int, float, bool,
+                            type(None))):
+            return json.JSONEncoder.default(self, obj)
+        if isinstance(obj, set) or isinstance(obj, tuple):
+            return list(obj)
+        if isinstance(obj, object):
+            return obj.__dict__
+        else:
+            raise TypeError
diff --git a/hubs/templates/includes/_searchstream.html b/hubs/templates/includes/_searchstream.html
index 60b2caf..bac3d5e 100644
--- a/hubs/templates/includes/_searchstream.html
+++ b/hubs/templates/includes/_searchstream.html
@@ -5,3 +5,7 @@
     <span class="oi" data-glyph="magnifying-glass"></span>
   </span>
 </div>
+<br/>
+<div class="alert alert-warning" role="alert">
+  This stream is filtered. <a href="https://apps.fedoraproject.org/notifications/{{hub.name}}.id.fedoraproject.org/" target="_blank">View Filters</a>
+</div>
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
index eff03f8..b7a3076 100644
--- a/hubs/templates/stream.html
+++ b/hubs/templates/stream.html
@@ -28,10 +28,6 @@
       <div class="row">
         <div class="col-sm-8 pull-left">
           {% include "includes/_searchstream.html" %}
-          <br/>
-          <div class="alert alert-warning" role="alert">
-            This stream is filtered. <a href="https://apps.fedoraproject.org/notifications/{{hub.name}}.id.fedoraproject.org/">View Filters</a>
-          </div>
           <div id="streamFeed"></div>
         </div>
         <div class="col-sm-4">
@@ -45,10 +41,6 @@
     <div class="container">
       <div class="row">
         {% include "includes/_searchstream.html" %}
-        <br/>
-        <div class="alert alert-warning" role="alert">
-          This stream is filtered. <a href="https://apps.fedoraproject.org/notifications/{{hub.name}}.id.fedoraproject.org/">View Filters</a>
-        </div>
         <div class="col-sm-8 pull-left">
           <div id="actionsFeed"></div>
         </div>

From 7c0aaeac4f9c2c91211c04a823a926f4c96710b5 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 11/16] Update README regarding Stream and Feed setup


---

diff --git a/README.rst b/README.rst
index 8b80f92..a7d2910 100644
--- a/README.rst
+++ b/README.rst
@@ -138,14 +138,27 @@ contents like this::
 Make sure to leave the '' around you username and password - don't delete them! Some widgets will work without the above info being present..
 but it is needed for a subset of them.
 
-Feed Widget and My Stream - the Extra Mile
+JavaScript assets
+--------------
+
+Some widgets and pages run on React so to install those dependencies::
+
+    $ cd hubs/static/client && npm install
+
+For the Feed and My Stream, change the ``SSE_URL`` location in the ``config`` you created
+to point to the streaming server then build JavaScript assets. Add ``--w`` for live reloading::
+
+    $ cd hubs/static/client && node_modules/.bin/webpack
+
+
+My Stream - the Extra Mile
 ------------------------------------------
 
-One widget (the big tamale -- the feed widget) and the My Stream page require more legwork to stand
-up.  If you just want to see how hubs works and you want to hack on other
+One portion of Hubs - the My Stream page - requires more legwork to stand up.
+If you just want to see how hubs works and you want to hack on other
 peripheral stuff around it, you don't need to bother with these steps.
 
-The feed widget requires a direct DB connection to the datanommer
+The stream requires a direct DB connection to the datanommer
 database; it can't proxy through datagrepper because it needs more
 flexibility.  To get this working, you're going to set up:
 
@@ -156,10 +169,6 @@ Start with some required packages::
 
     $ sudo dnf install postgresql-server python-datanommer-consumer datanommer-commands fedmsg-hub npm
 
-The Feed Widget runs on React so install those dependencies too::
-
-    $ cd hubs/static/client && npm install
-
 And there are some support libraries you'll also need::
 
     $ sudo dnf install python-psycopg2 python-fedmsg-meta-fedora-infrastructure
@@ -217,19 +226,14 @@ 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
 
-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 -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,
-you shouldn't get an error message about that widget being unable to be
+that *should* show up on your page. they should appear there.  (At very least,
+you shouldn't get an error message about that page being unable to be
 displayed).
 
 Stubbing out a new Widget

From 284a58468a38bafcda227aeb9897c2da98d14c19 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 12/16] Escape html in fedmsgs and remove More dropdown


* Update saved when changing to that tab

---

diff --git a/hubs/static/client/app/components/Dropdown.jsx b/hubs/static/client/app/components/Dropdown.jsx
index 57de016..47511f7 100644
--- a/hubs/static/client/app/components/Dropdown.jsx
+++ b/hubs/static/client/app/components/Dropdown.jsx
@@ -15,8 +15,9 @@ export default class Dropdown extends React.Component {
       data: JSON.stringify(payload),
       contentType: 'application/json',
     }).done(() => {
-      const id = '#save-' + this.props.match.dom_id;
+      const id = `#save-${this.props.match.dom_id}`;
       const $saveBtn = $(id);
+      $saveBtn.removeClass('btn-primary').addClass('btn-success');
       $saveBtn.text('Saved');
     });
   }
@@ -25,29 +26,20 @@ export default class Dropdown extends React.Component {
     let saveBtn;
     if (!this.props.match.saved && this.props.saveUrl) {
       saveBtn = (
-        <a
-          className="dropdown-item"
+        <button
+          id={`save-${this.props.match.dom_id}`}
+          className="btn btn-sm btn-primary"
           onClick={this.onClick.bind(this)}
           href="#"
         >
           Save
-        </a>
+        </button>
       );
     }
 
     return (
-      <div className="dropdown pull-sm-right">
-        <span
-          className="dropdown-toggle" type="button"
-          id={'save-' + this.props.match.dom_id}
-          data-toggle="dropdown" aria-haspopup="true"
-          aria-expanded="true"
-        >
-          More
-        </span>
-        <div className="dropdown-menu" aria-labelledby="dropdownMenu2">
-          {saveBtn}
-        </div>
+      <div className="pull-right">
+       {saveBtn}
       </div>
     );
   }
diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 4562dcd..4d6c9d2 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import { render } from 'react-dom';
+import ReactDOM from 'react-dom';
 
 import Panel from './Panel.jsx';
 
@@ -46,4 +46,4 @@ export default class Feed extends React.Component {
 
 window.Feed = Feed;
 window.React = React;
-window.reactRender = render;
+window.ReactDOM = ReactDOM;
diff --git a/hubs/stream.py b/hubs/stream.py
index 2cbcadf..8c33bf2 100644
--- a/hubs/stream.py
+++ b/hubs/stream.py
@@ -1,3 +1,4 @@
+import cgi
 import datetime
 import json
 import uuid
@@ -100,6 +101,7 @@ class Stream(object):
                 **fmn_hinting)
             messages.extend(rows)
 
+
         for message in messages:
             original = message.__json__()
             if self.name in fedmsg.meta.msg2usernames(original, **
@@ -117,10 +119,21 @@ class Stream(object):
 
         return preference
 
+    def escape_html(self, match):
+        for key in ['subjective', 'subtitle']:
+            match[key] = cgi.escape(match[key])
+        try:
+            match['long_form'] = cgi.escape(match['long_form'])
+        except:
+            # not all matches have a long form
+            pass
+        return match
+
     def apply_markup(self, match):
+        match = self.escape_html(match)
         markup = match['subtitle']
         for username in match['usernames']:
-            args = (username, '<a href="{url}">@{username}</a>'.format(
+            args = (username, '<a href="{url}" rel="nofollow">@{username}</a>'.format(
                 url=flask.url_for('hub', name=username),
                 username=username, ))
             markup = markup.replace(*args)
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
index b7a3076..585d71b 100644
--- a/hubs/templates/stream.html
+++ b/hubs/templates/stream.html
@@ -135,15 +135,29 @@
    /* Right now, stream and actions are the same.
     * Once, Mentions is implemented, then each will be its own
     */
-   reactRender(streamFeed, document.getElementById('streamFeed'));
-   reactRender(streamFeed, document.getElementById('actionsFeed'));
+   ReactDOM.render(streamFeed, document.getElementById('streamFeed'));
+   ReactDOM.render(streamFeed, document.getElementById('actionsFeed'));
 
    var savedFeed = React.createElement(Feed, {
      matches: {{saved|safe}},
      url: false,
      messageLimit: 100
    });
-   reactRender(savedFeed, document.getElementById('savedFeed'));
+   ReactDOM.render(savedFeed, document.getElementById('savedFeed'));
+   $('a[href="#saved"]').on('shown.bs.tab', function(){
+     $.get('/{{hub.name}}/notifications/').done(function(resp) {
+       var saved = {{saved|safe}};
+       if (resp.length != saved.length) {
+         ReactDOM.unmountComponentAtNode(document.getElementById('savedFeed'));
+         var savedFeed = React.createElement(Feed, {
+           matches: resp,
+           url: false,
+           messageLimit: 100
+         });
+         ReactDOM.render(savedFeed, document.getElementById('savedFeed'));
+       }
+     });
+   });
  }
 </script>
 {% endblock %}
diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html
index 755bec9..bba1c31 100644
--- a/hubs/widgets/templates/feed.html
+++ b/hubs/widgets/templates/feed.html
@@ -10,6 +10,6 @@
      url: '{{ feed_url }}',
      messageLimit: {{ message_limit }}
    });
-   reactRender(FeedElement, document.getElementById('feed'));
+   ReactDOM.render(FeedElement, document.getElementById('feed'));
  })();
 </script>

From 3c508ef47051556e1890702d508cfbb0ddc9c37c Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 13/16] Add DELETE for saved notifications


* Fix open-iconic icon -> font awesome

---

diff --git a/hubs/app.py b/hubs/app.py
index cc32021..ff9cb3a 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -576,6 +576,17 @@ def notifications(user):
             dict(notification=notification.__json__(), success=True)
         )
 
+@app.route('/<user>/notifications/<idx>', methods=['DELETE'])
+@app.route('/<user>/notifications/<idx>/', methods=['DELETE'])
+@login_required
+def delete_notifications(user, idx):
+    notification = session.query(hubs.models.SavedNotification).filter_by(idx=idx).first()
+    if not notification:
+        return flask.abort(400)
+    session.delete(notification)
+    session.commit()
+    return flask.jsonify(dict(status_code=200))
+
 @app.before_request
 def check_auth():
     flask.session.permanent = True
diff --git a/hubs/static/client/app/components/Dropdown.jsx b/hubs/static/client/app/components/Dropdown.jsx
index 47511f7..ae1872c 100644
--- a/hubs/static/client/app/components/Dropdown.jsx
+++ b/hubs/static/client/app/components/Dropdown.jsx
@@ -1,7 +1,7 @@
 import React from 'react';
 
 export default class Dropdown extends React.Component {
-  onClick() {
+  save() {
     const payload = {
       link: this.props.match.link,
       markup: this.props.match.markup,
@@ -11,7 +11,7 @@ export default class Dropdown extends React.Component {
 
     $.ajax({
       type: 'POST',
-      url: this.props.saveUrl,
+      url: this.props.options.saveUrl,
       data: JSON.stringify(payload),
       contentType: 'application/json',
     }).done(() => {
@@ -22,24 +22,49 @@ export default class Dropdown extends React.Component {
     });
   }
 
+  delete() {
+    $.ajax({
+      type: 'DELETE',
+      url: `${this.props.options.saveUrl}${this.props.match.idx}/`,
+    }).done((resp) => {
+      if (resp.status_code === 200) {
+        const $notification = $(`#${this.props.match.dom_id}`);
+        $notification.closest('.card-block').remove();
+      }
+    });
+  }
+
   render() {
     let saveBtn;
-    if (!this.props.match.saved && this.props.saveUrl) {
+    if (!this.props.match.saved && this.props.options.saveUrl) {
       saveBtn = (
         <button
           id={`save-${this.props.match.dom_id}`}
           className="btn btn-sm btn-primary"
-          onClick={this.onClick.bind(this)}
+          onClick={this.save.bind(this)}
           href="#"
         >
           Save
         </button>
       );
     }
+    let deleteBtn;
+    if (this.props.options.delete && this.props.options.saveUrl) {
+      deleteBtn = (
+        <button
+          id={`delete-${this.props.match.dom_id}`}
+          className="btn btn-sm btn-danger"
+          onClick={this.delete.bind(this)}
+        >
+          Remove
+        </button>
+      );
+    }
 
     return (
       <div className="pull-right">
        {saveBtn}
+        {deleteBtn}
       </div>
     );
   }
diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx
index 4d6c9d2..eae102a 100644
--- a/hubs/static/client/app/components/Feed.jsx
+++ b/hubs/static/client/app/components/Feed.jsx
@@ -8,7 +8,7 @@ export default class Feed extends React.Component {
     super(props);
     this.state = {
       matches: this.props.matches,
-      messageLimit: this.props.messageLimit,
+      messageLimit: this.props.options.messageLimit,
       sse: true,
     };
     // If it wasn't instantiated with a url or an error happened, bail
@@ -33,8 +33,7 @@ export default class Feed extends React.Component {
   }
   render() {
     const feedNodes = this.state.matches.map((match, idx) => {
-      const saveUrl = this.props.saveUrl || null;
-      return <Panel match={match} saveUrl={saveUrl} key={idx} />;
+      return <Panel match={match} options={this.props.options || {}} key={idx} />;
     });
     return (
       <div>
diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index e9b585e..437550e 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -20,7 +20,7 @@ export default class Markup extends React.Component {
         {timestamp}
         <Dropdown
           match={this.props.match}
-          saveUrl={this.props.saveUrl}
+          options={this.props.options}
         />
       </div>
     );
diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx
index 0cd9bdf..c443155 100644
--- a/hubs/static/client/app/components/Panel.jsx
+++ b/hubs/static/client/app/components/Panel.jsx
@@ -6,9 +6,9 @@ import Markup from './Markup.jsx';
 const Panel = function (props) {
   return (
     <div className="card">
-      <div className="card-block">
+      <div className="card-block" id={props.match.dom_id}>
         <Icon match={props.match} />
-        <Markup match={props.match} saveUrl={props.saveUrl} />
+        <Markup options={props.options} match={props.match} />
       </div>
     </div>
   );
diff --git a/hubs/templates/includes/_searchstream.html b/hubs/templates/includes/_searchstream.html
index bac3d5e..3bfc7c1 100644
--- a/hubs/templates/includes/_searchstream.html
+++ b/hubs/templates/includes/_searchstream.html
@@ -2,7 +2,7 @@
 <div class="input-group">
   <input type="search" class="form-control" placeholder="Search this activity stream..." aria-describedby="searchform-addon"></input>
   <span class="input-group-addon" id="searchform-addon">
-    <span class="oi" data-glyph="magnifying-glass"></span>
+    <span><i class="fa fa-search" aria-hidden="true"></i></span>
   </span>
 </div>
 <br/>
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
index 585d71b..5e6fc57 100644
--- a/hubs/templates/stream.html
+++ b/hubs/templates/stream.html
@@ -126,11 +126,16 @@
 
  function setup_feeds() {
    var actions = {{actions|safe}};
+   var options = {
+
+   }
 
    var streamFeed = React.createElement(Feed, {
      matches: actions,
-     saveUrl: '/{{hub.name}}/notifications/',
-     messageLimit: 100
+     options: {
+       saveUrl: '/{{hub.name}}/notifications/',
+       messageLimit: 100
+     }
    });
    /* Right now, stream and actions are the same.
     * Once, Mentions is implemented, then each will be its own
@@ -141,7 +146,11 @@
    var savedFeed = React.createElement(Feed, {
      matches: {{saved|safe}},
      url: false,
-     messageLimit: 100
+     options: {
+       messageLimit: 100,
+       delete: true,
+       saveUrl: '/{{hub.name}}/notifications/'
+     }
    });
    ReactDOM.render(savedFeed, document.getElementById('savedFeed'));
    $('a[href="#saved"]').on('shown.bs.tab', function(){
@@ -152,7 +161,11 @@
          var savedFeed = React.createElement(Feed, {
            matches: resp,
            url: false,
-           messageLimit: 100
+           options: {
+             messageLimit: 100,
+             delete: true,
+             saveUrl: '/{{hub.name}}/notifications/'
+           }
          });
          ReactDOM.render(savedFeed, document.getElementById('savedFeed'));
        }
diff --git a/hubs/tests/test_api/test_notifications.py b/hubs/tests/test_api/test_notifications.py
index ea8616f..6706031 100644
--- a/hubs/tests/test_api/test_notifications.py
+++ b/hubs/tests/test_api/test_notifications.py
@@ -82,3 +82,40 @@ class TestPostNotifications(hubs.tests.APPTest):
                             for s in all_saved))
         self.assertTrue(any(str(s['link']) == self.valid_payload['link']
                             for s in all_saved))
+
+
+class TestDeleteNotifications(hubs.tests.APPTest):
+    user = hubs.tests.FakeAuthorization('ralph')
+    notification = hubs.models.SavedNotification(
+        username='ralph',
+        markup='foo',
+        link='bar',
+        secondary_icon='baz',
+        dom_id='qux'
+    )
+
+    def test_delete_notification(self):
+        self.session.add(self.notification)
+        self.session.commit()
+        idx = self.notification.idx
+
+        self.assertIsNotNone(self.notification)
+        with hubs.tests.auth_set(app, self.user):
+            resp = self.app.delete(
+                '/{}/notifications/{}/'.format(self.user.username, idx)
+            )
+
+        self.assertEqual(resp.status_code, 200)
+        notification = self.session.query(
+            hubs.models.SavedNotification).filter_by(idx=idx).first()
+
+        self.assertIsNone(notification)
+
+    def test_400_on_bad_idx(self):
+        idx = 'thisisastringnotanint'
+
+        with hubs.tests.auth_set(app, self.user):
+            resp = self.app.delete(
+                '/{}/notifications/{}/'.format(self.user.username, idx)
+            )
+        self.assertEqual(resp.status_code, 400)

From 962805ef2f7577981b50c9fda6f0eac07c6c0ad3 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 14/16] Fix bugs created by refactoring Feed js


- Make sure to check props.options now.
- Fix typo in checking date_time

---

diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx
index 437550e..92c2c50 100644
--- a/hubs/static/client/app/components/Markup.jsx
+++ b/hubs/static/client/app/components/Markup.jsx
@@ -8,7 +8,7 @@ export default class Markup extends React.Component {
     return { __html: this.props.match.markup };
   }
   render() {
-    const timestamp = this.props.date_time ? (<TimeAgo date={this.props.match.date_time} />) : null;
+    const timestamp = this.props.match.date_time ? (<TimeAgo date={this.props.match.date_time} />) : null;
 
     return (
       <div className="media-body">
diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html
index bba1c31..aeed666 100644
--- a/hubs/widgets/templates/feed.html
+++ b/hubs/widgets/templates/feed.html
@@ -8,7 +8,9 @@
    const FeedElement = React.createElement(Feed, {
      matches: {{ matches }},
      url: '{{ feed_url }}',
-     messageLimit: {{ message_limit }}
+     options: {
+       messageLimit: {{ message_limit }}
+     }
    });
    ReactDOM.render(FeedElement, document.getElementById('feed'));
  })();

From d1ced5f00c7c3746dd75ef992bd726aed84c5f12 Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:35:20 +0000
Subject: [PATCH 15/16] Modify stream.py to fake JSON messages


- Since the Flock demo is done, it decreases complexity to pass JSON
objects until stream functionality can be added in fmn.

---

diff --git a/hubs/actions.json b/hubs/actions.json
new file mode 100644
index 0000000..e174333
--- /dev/null
+++ b/hubs/actions.json
@@ -0,0 +1,2446 @@
+[{
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#245: \"Mockup for web IRC chat interface for waartaa\"",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "in 3 hours",
+    "subtitle": "atelic commented on ticket fedora-hubs#245: \"Mockup for web IRC chat interface for waartaa\"",
+    "timestamp": 1470681231,
+    "start_time": 1470681231,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic commented on ticket fedora-hubs#245: \"Mockup for web IRC chat interface for waartaa\"",
+    "dom_id": "dd5e0f8b-d263-42fd-9923-bdbbcba8b2fa",
+    "msg_ids": {
+        "2016-9baa7dab-d34c-4485-84b2-6d1187608c28": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#245: \"Mockup for web IRC chat interface for waartaa\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#245: \"Mockup for web IRC chat interface for waartaa\"",
+            "long_form": "atelic commented on ticket fedora-hubs#245: \"Mockup for web IRC chat interface for waartaa\"",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/245"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/245#comment-3926",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#245: \"Mockup for web IRC chat interface for waartaa\"",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/issue/245#comment-3926",
+    "end_time": 1470681231,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.issue.comment.added"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "subtitle": "atelic interacted with issue #154 of project \"fedora-hubs\" 3 times",
+    "timestamp": 1470679960,
+    "start_time": 1470679956,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with issue #154 of project \"fedora-hubs\" 3 times",
+    "msg_ids": {
+        "2016-01d790b6-a5f3-452d-8224-29df39704467": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#154: \"Small amount of horizontal scroll\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#154: \"Small amount of horizontal scroll\"",
+            "long_form": "atelic commented on ticket fedora-hubs#154: \"Small amount of horizontal scroll\"",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/154"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/154#comment-3924",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#154: \"Small amount of horizontal scroll\""
+        },
+        "2016-2f10418b-3cbf-4bd3-b28c-2ca0c759b2d8": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic set the status of ticket fedora-hubs#154 to: Fixed",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic set the status of ticket fedora-hubs#154 to: Fixed",
+            "long_form": "atelic set the status of ticket fedora-hubs#154 to: Fixed",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/154"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/154",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.edit",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> set the status of ticket fedora-hubs#154 to: Fixed"
+        },
+        "2016-4e164500-62ba-43ab-9195-99c58c98f7fa": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#154: \"Small amount of horizontal scroll\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#154: \"Small amount of horizontal scroll\"",
+            "long_form": "atelic commented on ticket fedora-hubs#154: \"Small amount of horizontal scroll\"",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/154"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/154#comment-3925",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#154: \"Small amount of horizontal scroll\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/issue/154",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with issue #154 of project \"fedora-hubs\" 3 times",
+    "dom_id": "91f1d5ec-e373-4de0-b4c1-4c9266df2f2a",
+    "end_time": 1470679962,
+    "human_time": "in 3 hours",
+    "topics": [
+        "io.pagure.prod.pagure.issue.edit",
+        "io.pagure.prod.pagure.issue.comment.added"
+    ]
+}, {
+    "subtitle": "atelic interacted with issue #171 of project \"fedora-hubs\" 4 times",
+    "timestamp": 1470679257.25,
+    "start_time": 1470679253,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with issue #171 of project \"fedora-hubs\" 4 times",
+    "msg_ids": {
+        "2016-30d12db3-bc4a-4fd5-914d-0a567c8574f3": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#171: \"Make a decision on JS framework\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#171: \"Make a decision on JS framework\"",
+            "long_form": "atelic commented on ticket fedora-hubs#171: \"Make a decision on JS framework\"",
+            "objects": [
+                "issue/171",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/171#comment-3923",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#171: \"Make a decision on JS framework\""
+        },
+        "2016-d4643f66-6944-43f5-8f62-731f18f5470b": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic edited the priority fields of ticket fedora-hubs#171",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic edited the priority fields of ticket fedora-hubs#171",
+            "long_form": "atelic edited the priority fields of ticket fedora-hubs#171",
+            "objects": [
+                "issue/171",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/171",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.edit",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> edited the priority fields of ticket fedora-hubs#171"
+        },
+        "2016-4aefc797-5521-481d-9ee3-90edf036b3a2": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#171: \"Make a decision on JS framework\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#171: \"Make a decision on JS framework\"",
+            "long_form": "atelic commented on ticket fedora-hubs#171: \"Make a decision on JS framework\"",
+            "objects": [
+                "issue/171",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/171#comment-3922",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#171: \"Make a decision on JS framework\""
+        },
+        "2016-a81dcaa0-77b6-44f3-9ae5-c710c2ee540d": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic set the status of ticket fedora-hubs#171 to: Fixed",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic set the status of ticket fedora-hubs#171 to: Fixed",
+            "long_form": "atelic set the status of ticket fedora-hubs#171 to: Fixed",
+            "objects": [
+                "issue/171",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/171",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.edit",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> set the status of ticket fedora-hubs#171 to: Fixed"
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/issue/171",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with issue #171 of project \"fedora-hubs\" 4 times",
+    "dom_id": "c3484845-2fd0-4019-a6a0-7f2f7a931011",
+    "end_time": 1470679261,
+    "human_time": "in 3 hours",
+    "topics": [
+        "io.pagure.prod.pagure.issue.edit",
+        "io.pagure.prod.pagure.issue.comment.added"
+    ]
+}, {
+    "subtitle": "atelic interacted with issue #234 of project \"fedora-hubs\" 3 times",
+    "timestamp": 1470678655,
+    "start_time": 1470678643,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with issue #234 of project \"fedora-hubs\" 3 times",
+    "msg_ids": {
+        "2016-636f4fb2-9378-4181-8d3b-c0566e3171e1": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#234: \"Subscribers widget has floating dot and subscribe button\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#234: \"Subscribers widget has floating dot and subscribe button\"",
+            "long_form": "atelic commented on ticket fedora-hubs#234: \"Subscribers widget has floating dot and subscribe button\"",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/234"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/234#comment-3921",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#234: \"Subscribers widget has floating dot and subscribe button\""
+        },
+        "2016-1cb62ec3-9e49-4d07-bb27-ee7255c20115": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic set the status of ticket fedora-hubs#234 to: Fixed",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic set the status of ticket fedora-hubs#234 to: Fixed",
+            "long_form": "atelic set the status of ticket fedora-hubs#234 to: Fixed",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/234"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/234",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.edit",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> set the status of ticket fedora-hubs#234 to: Fixed"
+        },
+        "2016-98893984-1624-4142-a890-2791e52f5aad": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#234: \"Subscribers widget has floating dot and subscribe button\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#234: \"Subscribers widget has floating dot and subscribe button\"",
+            "long_form": "atelic commented on ticket fedora-hubs#234: \"Subscribers widget has floating dot and subscribe button\"",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/234"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/234#comment-3920",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#234: \"Subscribers widget has floating dot and subscribe button\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/issue/234",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with issue #234 of project \"fedora-hubs\" 3 times",
+    "dom_id": "5bfbc86e-3167-4aad-b43e-8714c8669f8d",
+    "end_time": 1470678661,
+    "human_time": "in 3 hours",
+    "topics": [
+        "io.pagure.prod.pagure.issue.edit",
+        "io.pagure.prod.pagure.issue.comment.added"
+    ]
+}, {
+    "subtitle": "atelic interacted with pull-request #239 of project \"fedora-hubs\" 7 times",
+    "timestamp": 1470356286.142857,
+    "start_time": 1470263110,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with pull-request #239 of project \"fedora-hubs\" 7 times",
+    "msg_ids": {
+        "2016-d3a27251-f4a6-4ce9-8b04-15d4f930cf50": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic opened pull-request#239: \"Add tests for React front end\" on project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic opened pull-request#239: \"Add tests for React front end\" on project \"fedora-hubs\"",
+            "long_form": "atelic opened pull-request#239: \"Add tests for React front end\" on project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/239"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/239",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened pull-request#239: \"Add tests for React front end\" on project \"fedora-hubs\""
+        },
+        "2016-22379cd4-6375-4720-b62d-3b9e05b9b100": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/239"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/239#comment-8241",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#239 of project \"fedora-hubs\""
+        },
+        "2016-857d6d95-0a12-4f45-9ca3-755a9733534c": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/239"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/239#comment-8181",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#239 of project \"fedora-hubs\""
+        },
+        "2016-682cbdb7-e4d2-43da-9cc1-67e439809b02": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/239"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/239#comment-8202",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#239 of project \"fedora-hubs\""
+        },
+        "2016-9361ad7c-f696-4470-8f96-a93d9387c162": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/239"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/239#comment-8253",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#239 of project \"fedora-hubs\""
+        },
+        "2016-0e6ec94c-763c-4864-a150-07e96eeeac32": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/239"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/239#comment-8227",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#239 of project \"fedora-hubs\""
+        },
+        "2016-d9877a21-df96-49a6-a440-9c2d8e5844bb": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#239 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/239"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/239#comment-8251",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#239 of project \"fedora-hubs\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/239",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with pull-request #239 of project \"fedora-hubs\" 7 times",
+    "dom_id": "e062ee4b-8757-4e8c-ad50-cf2355a5e366",
+    "end_time": 1470431562,
+    "human_time": "3 days ago",
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added",
+        "io.pagure.prod.pagure.pull-request.new"
+    ]
+}, {
+    "subtitle": "atelic interacted with pull-request #231 of project \"fedora-hubs\" 17 times",
+    "timestamp": 1470174598.9411764,
+    "start_time": 1469837470,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with pull-request #231 of project \"fedora-hubs\" 17 times",
+    "msg_ids": {
+        "2016-8b0221db-e924-4c90-aab1-6d0d79534470": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8136",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-f8a4f5ba-26f3-4acf-93bc-63ca9e0dc07d": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8235",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-b4ecafdc-c463-416e-ac8d-db23040a4672": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8130",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-c6023fd0-8424-43f5-be4c-24f582441eea": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8132",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-f42a24d4-db3c-4036-aa53-03df6efd9215": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8252",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-510603d6-8bad-4131-a45a-ad6b475687a0": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8138",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-e63ad5d2-c44f-44d9-80c4-f01910c2385a": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8238",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-a2418776-4589-4041-a3ad-d7f84492d2c6": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8142",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-89d6e37b-9a06-49a3-b1c7-d8bb53c8bbc9": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8139",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-d585ecd3-05e4-479b-bdf2-64fe03270ea0": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8096",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-437f7764-12e2-4c51-bd0b-f6a25c86f782": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8133",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-7ded95aa-b340-4c8b-85d7-8d6bc0d415c9": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8120",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-fa2cb020-eb03-45f7-81b0-a629e2f8dbab": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8121",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-e34c9a7d-89be-488b-8dbe-071199e9196b": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8026",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-87f98c74-dfdb-4261-8536-3a32011f6112": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8140",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-2c743fe3-b0ba-40fc-ac58-9f32a23714b7": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8135",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        },
+        "2016-9fcef2e4-fd16-41c5-b198-824b60dc0dde": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#231 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/231"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/231#comment-8022",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#231 of project \"fedora-hubs\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/231",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with pull-request #231 of project \"fedora-hubs\" 17 times",
+    "dom_id": "dd953aea-1553-4455-a6af-cc9b2128d357",
+    "end_time": 1470420897,
+    "human_time": "5 days ago",
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added"
+    ]
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#246 of project \"fedora-hubs\"",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "2 days ago",
+    "subtitle": "atelic commented on pull-request#246 of project \"fedora-hubs\"",
+    "timestamp": 1470418821,
+    "start_time": 1470418821,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic commented on pull-request#246 of project \"fedora-hubs\"",
+    "dom_id": "685e8109-f19f-42c6-83df-065ba7378351",
+    "msg_ids": {
+        "2016-92ae6f5c-1f4c-4303-a631-fc5584b51579": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#246 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#246 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#246 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/246"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/246#comment-8244",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#246 of project \"fedora-hubs\"",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/246#comment-8244",
+    "end_time": 1470418821,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#241 of project \"fedora-hubs\"",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "3 days ago",
+    "subtitle": "atelic commented on pull-request#241 of project \"fedora-hubs\"",
+    "timestamp": 1470358933,
+    "start_time": 1470358933,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic commented on pull-request#241 of project \"fedora-hubs\"",
+    "dom_id": "f18a101b-90af-4a20-bca2-bed8ba88e544",
+    "msg_ids": {
+        "2016-6d486c9c-519b-489e-a9dc-27b5b458ceff": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#241 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#241 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#241 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/241"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/241#comment-8219",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#241 of project \"fedora-hubs\"",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/241#comment-8219",
+    "end_time": 1470358933,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "subtitle": "atelic interacted with pull-request #2 of project \"internexpo\" 5 times",
+    "timestamp": 1470335760.6,
+    "start_time": 1470267969,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with pull-request #2 of project \"internexpo\" 5 times",
+    "msg_ids": {
+        "2016-ac36fb61-3b6a-47f3-97ac-986c17296d26": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "long_form": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "objects": [
+                "pull-request/2",
+                "project/internexpo"
+            ],
+            "link": "https://pagure.io/internexpo/pull-request/2#comment-8210",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#2 of project \"internexpo\""
+        },
+        "2016-3f48dc8b-319c-43be-b37b-a77e2be7b341": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic opened pull-request#2: \"Add code to this repo\" on project \"internexpo\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic opened pull-request#2: \"Add code to this repo\" on project \"internexpo\"",
+            "long_form": "atelic opened pull-request#2: \"Add code to this repo\" on project \"internexpo\"",
+            "objects": [
+                "pull-request/2",
+                "project/internexpo"
+            ],
+            "link": "https://pagure.io/internexpo/pull-request/2",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened pull-request#2: \"Add code to this repo\" on project \"internexpo\""
+        },
+        "2016-92954af1-b34a-4ca7-9b9a-c2c12143ce19": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "long_form": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "objects": [
+                "pull-request/2",
+                "project/internexpo"
+            ],
+            "link": "https://pagure.io/internexpo/pull-request/2#comment-8214",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#2 of project \"internexpo\""
+        },
+        "2016-c4707879-ac60-4a35-8af0-fa0d9cc94576": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic edited a comment on pull-request#2 of project \"internexpo\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic edited a comment on pull-request#2 of project \"internexpo\"",
+            "long_form": "atelic edited a comment on pull-request#2 of project \"internexpo\"",
+            "objects": [
+                "pull-request/2",
+                "project/internexpo"
+            ],
+            "link": "https://pagure.io/internexpo/pull-request/2#comment-8210",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.edited",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> edited a comment on pull-request#2 of project \"internexpo\""
+        },
+        "2016-1c878b0a-849f-4f97-b501-b4cdf2bbcee0": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "long_form": "atelic commented on pull-request#2 of project \"internexpo\"",
+            "objects": [
+                "pull-request/2",
+                "project/internexpo"
+            ],
+            "link": "https://pagure.io/internexpo/pull-request/2#comment-8211",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#2 of project \"internexpo\""
+        }
+    },
+    "link": "https://pagure.io/internexpo/pull-request/2",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with pull-request #2 of project \"internexpo\" 5 times",
+    "dom_id": "60838fc2-a020-4ef2-803e-6bf35af6170c",
+    "end_time": 1470356477,
+    "human_time": "3 days ago",
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added",
+        "io.pagure.prod.pagure.pull-request.new",
+        "io.pagure.prod.pagure.pull-request.comment.edited"
+    ]
+}, {
+    "subtitle": "atelic interacted with pull-request #240 of project \"fedora-hubs\" 3 times",
+    "timestamp": 1470312049.3333333,
+    "start_time": 1470268764,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with pull-request #240 of project \"fedora-hubs\" 3 times",
+    "msg_ids": {
+        "2016-bc365882-7666-4728-87e0-0413c1d63800": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic opened pull-request#240: \"Stop populate-from-fas.py from crashing on run\" on project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic opened pull-request#240: \"Stop populate-from-fas.py from crashing on run\" on project \"fedora-hubs\"",
+            "long_form": "atelic opened pull-request#240: \"Stop populate-from-fas.py from crashing on run\" on project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/240"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/240",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened pull-request#240: \"Stop populate-from-fas.py from crashing on run\" on project \"fedora-hubs\""
+        },
+        "2016-783b44a7-80f4-41fe-8676-e577164d7327": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#240 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#240 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#240 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/240"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/240#comment-8185",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#240 of project \"fedora-hubs\""
+        },
+        "2016-8cf4dbea-4fcd-468f-a24a-2dd3ed656e72": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic merged pull-request#240 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic merged pull-request#240 of project \"fedora-hubs\"",
+            "long_form": "atelic merged pull-request#240 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/240"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/240",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.closed",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> merged pull-request#240 of project \"fedora-hubs\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/240",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with pull-request #240 of project \"fedora-hubs\" 3 times",
+    "dom_id": "d077be97-905b-4b50-bd2e-bb48f5cf4b2b",
+    "end_time": 1470333692,
+    "human_time": "4 days ago",
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.closed",
+        "io.pagure.prod.pagure.pull-request.comment.added",
+        "io.pagure.prod.pagure.pull-request.new"
+    ]
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened a new ticket internexpo#1: \"This repo is empty\"",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "4 days ago",
+    "subtitle": "atelic opened a new ticket internexpo#1: \"This repo is empty\"",
+    "timestamp": 1470267815,
+    "start_time": 1470267815,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic opened a new ticket internexpo#1: \"This repo is empty\"",
+    "dom_id": "32e00222-c13b-4fb7-b06d-64c85aedc937",
+    "msg_ids": {
+        "2016-8b96b2bd-2925-469a-a437-0054d2146c33": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic opened a new ticket internexpo#1: \"This repo is empty\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic opened a new ticket internexpo#1: \"This repo is empty\"",
+            "long_form": "atelic opened a new ticket internexpo#1: \"This repo is empty\"",
+            "objects": [
+                "issue/1",
+                "project/internexpo"
+            ],
+            "link": "https://pagure.io/internexpo/issue/1",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened a new ticket internexpo#1: \"This repo is empty\"",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/internexpo/issue/1",
+    "end_time": 1470267815,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.issue.new"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> created a new project \"internexpo\"",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "4 days ago",
+    "subtitle": "atelic created a new project \"internexpo\"",
+    "timestamp": 1470267721,
+    "start_time": 1470267721,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic created a new project \"internexpo\"",
+    "dom_id": "3697fedc-2186-432a-af88-990c9b941ce7",
+    "msg_ids": {
+        "2016-1465dd21-7c2e-4ecb-92f3-de5c70bc8cc1": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic created a new project \"internexpo\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic created a new project \"internexpo\"",
+            "long_form": "atelic created a new project \"internexpo\"",
+            "objects": [
+                "project/internexpo"
+            ],
+            "link": "https://pagure.io/internexpo",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.project.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> created a new project \"internexpo\"",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/internexpo",
+    "end_time": 1470267721,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.project.new"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "subtitle": "atelic interacted with issue #164 of project \"fedora-hubs\" 3 times",
+    "timestamp": 1470251976,
+    "start_time": 1470251872,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with issue #164 of project \"fedora-hubs\" 3 times",
+    "msg_ids": {
+        "2016-3884c7cb-bf9f-455c-895c-9de153dc9494": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic set the status of ticket fedora-hubs#164 to: Fixed",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic set the status of ticket fedora-hubs#164 to: Fixed",
+            "long_form": "atelic set the status of ticket fedora-hubs#164 to: Fixed",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/164"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/164",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.edit",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> set the status of ticket fedora-hubs#164 to: Fixed"
+        },
+        "2016-a8ef33a1-c1c9-44dd-afaf-ff9fcabb74a2": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#164: \"Team Hub Member List Widget\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#164: \"Team Hub Member List Widget\"",
+            "long_form": "atelic commented on ticket fedora-hubs#164: \"Team Hub Member List Widget\"",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/164"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/164#comment-3832",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#164: \"Team Hub Member List Widget\""
+        },
+        "2016-da268efb-b7c8-4a75-88a3-369518d9eeee": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on ticket fedora-hubs#164: \"Team Hub Member List Widget\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on ticket fedora-hubs#164: \"Team Hub Member List Widget\"",
+            "long_form": "atelic commented on ticket fedora-hubs#164: \"Team Hub Member List Widget\"",
+            "objects": [
+                "project/fedora-hubs",
+                "issue/164"
+            ],
+            "link": "https://pagure.io/fedora-hubs/issue/164#comment-3834",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on ticket fedora-hubs#164: \"Team Hub Member List Widget\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/issue/164",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with issue #164 of project \"fedora-hubs\" 3 times",
+    "dom_id": "7988a9a4-d6a8-4180-9b5b-0f3bc5631fff",
+    "end_time": 1470252028,
+    "human_time": "4 days ago",
+    "topics": [
+        "io.pagure.prod.pagure.issue.edit",
+        "io.pagure.prod.pagure.issue.comment.added"
+    ]
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> gave <a href=\"/abompard/\" rel=\"nofollow\">@abompard</a>(25) a karma cookie in #fedora-hubs.  \"<a href=\"/abompard/\" rel=\"nofollow\">@abompard</a>++\"",
+    "usernames": [
+        "atelic",
+        "abompard"
+    ],
+    "human_time": "4 days ago",
+    "subtitle": "atelic gave abompard(25) a karma cookie in #fedora-hubs.  \"abompard++\"",
+    "timestamp": 1470249514,
+    "start_time": 1470249514,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/f0c54afccefcf4bc888e0d37618a8598602a668e4ffc2e433b551ca19fc164d8?s=64&d=retro",
+    "subjective": "atelic gave abompard(25) a karma cookie in #fedora-hubs.  \"abompard++\"",
+    "dom_id": "d381d76b-d052-4f2b-8a5d-cb46c026ae0e",
+    "msg_ids": {
+        "2016-dea24f20-dd28-45c5-aab4-e96815b41110": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "subtitle": "atelic gave abompard(25) a karma cookie in #fedora-hubs.  \"abompard++\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/f0c54afccefcf4bc888e0d37618a8598602a668e4ffc2e433b551ca19fc164d8?s=64&d=retro",
+            "subjective": "atelic gave abompard(25) a karma cookie in #fedora-hubs.  \"abompard++\"",
+            "long_form": "atelic gave abompard(25) a karma cookie in #fedora-hubs.  \"abompard++\"",
+            "objects": [
+                "karma/abompard"
+            ],
+            "link": "https://badges.fedoraproject.org/user/abompard",
+            "packages": [],
+            "icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "usernames": [
+                "atelic",
+                "abompard"
+            ],
+            "title": "irc.karma",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> gave <a href=\"/abompard/\" rel=\"nofollow\">@abompard</a>(25) a karma cookie in #fedora-hubs.  \"<a href=\"/abompard/\" rel=\"nofollow\">@abompard</a>++\"",
+            "lexer": null
+        }
+    },
+    "link": "https://badges.fedoraproject.org/user/abompard",
+    "end_time": 1470249514,
+    "packages": [],
+    "topics": [
+        "org.fedoraproject.prod.irc.karma"
+    ],
+    "categories": [
+        "irc"
+    ],
+    "icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro"
+}, {
+    "subtitle": "atelic interacted with pull-request #236 of project \"fedora-hubs\" 2 times",
+    "timestamp": 1470162363.5,
+    "start_time": 1470161171,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with pull-request #236 of project \"fedora-hubs\" 2 times",
+    "msg_ids": {
+        "2016-c482de19-046f-4e0a-baf8-db8a1b173b75": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#236 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#236 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#236 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/236"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/236#comment-8114",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#236 of project \"fedora-hubs\""
+        },
+        "2016-bdbb082e-a651-41d6-9be6-7a21a2c9fd23": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#236 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#236 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#236 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/236"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/236#comment-8123",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#236 of project \"fedora-hubs\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/236",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with pull-request #236 of project \"fedora-hubs\" 2 times",
+    "dom_id": "b4ee44b3-0860-469b-99bf-e2d9ae8be3cf",
+    "end_time": 1470163556,
+    "human_time": "5 days ago",
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added"
+    ]
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened a new ticket <a href=\"/atelic/\" rel=\"nofollow\">@atelic</a>/test#1: \"test &amp; &gt; 1.2 &lt;img src=\"https://pingoured.fr/random\" alt=\"foo\"/&gt; &lt;script&gt; alert('hi')&lt;/script&gt;\"",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "5 days ago",
+    "subtitle": "atelic opened a new ticket atelic/test#1: \"test &amp; &gt; 1.2 &lt;img src=\"https://pingoured.fr/random\" alt=\"foo\"/&gt; &lt;script&gt; alert('hi')&lt;/script&gt;\"",
+    "timestamp": 1470161866,
+    "start_time": 1470161866,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic opened a new ticket atelic/test#1: \"test &amp; &gt; 1.2 &lt;img src=\"https://pingoured.fr/random\" alt=\"foo\"/&gt; &lt;script&gt; alert('hi')&lt;/script&gt;\"",
+    "dom_id": "865a4b73-5b7b-423a-a075-fc41c2faa5cb",
+    "msg_ids": {
+        "2016-95fa6450-e838-40f3-b3fc-8bef1d7637dc": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic opened a new ticket atelic/test#1: \"test &amp; &gt; 1.2 &lt;img src=\"https://pingoured.fr/random\" alt=\"foo\"/&gt; &lt;script&gt; alert('hi')&lt;/script&gt;\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic opened a new ticket atelic/test#1: \"test &amp; &gt; 1.2 &lt;img src=\"https://pingoured.fr/random\" alt=\"foo\"/&gt; &lt;script&gt; alert('hi')&lt;/script&gt;\"",
+            "long_form": "atelic opened a new ticket atelic/test#1: \"test &amp; &gt; 1.2 &lt;img src=\"https://pingoured.fr/random\" alt=\"foo\"/&gt; &lt;script&gt; alert('hi')&lt;/script&gt;\"",
+            "objects": [
+                "issue/1",
+                "project/atelic/test"
+            ],
+            "link": "https://pagure.io/fork/atelic/test/issue/1",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.issue.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened a new ticket <a href=\"/atelic/\" rel=\"nofollow\">@atelic</a>/test#1: \"test &amp; &gt; 1.2 &lt;img src=\"https://pingoured.fr/random\" alt=\"foo\"/&gt; &lt;script&gt; alert('hi')&lt;/script&gt;\"",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/fork/atelic/test/issue/1",
+    "end_time": 1470161866,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.issue.new"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#237 of project \"fedora-hubs\"",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "5 days ago",
+    "subtitle": "atelic commented on pull-request#237 of project \"fedora-hubs\"",
+    "timestamp": 1470160881,
+    "start_time": 1470160881,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic commented on pull-request#237 of project \"fedora-hubs\"",
+    "dom_id": "0d87b48d-4f7d-4ab0-a6b8-1b6112d451c3",
+    "msg_ids": {
+        "2016-fb2099d7-0557-42ae-84e4-bb9ed1b8c320": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#237 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#237 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#237 of project \"fedora-hubs\"",
+            "objects": [
+                "project/fedora-hubs",
+                "pull-request/237"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/237#comment-8110",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#237 of project \"fedora-hubs\"",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/237#comment-8110",
+    "end_time": 1470160881,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> edited the Enforce_signed-off_commits_in_pull-request, Minimum_score_to_merge_pull-request, and 6 others fields of project <a href=\"/atelic/\" rel=\"nofollow\">@atelic</a>/test",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "a month ago",
+    "subtitle": "atelic edited the Enforce_signed-off_commits_in_pull-request, Minimum_score_to_merge_pull-request, and 6 others fields of project atelic/test",
+    "timestamp": 1467917437,
+    "start_time": 1467917437,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic edited the Enforce_signed-off_commits_in_pull-request, Minimum_score_to_merge_pull-request, and 6 others fields of project atelic/test",
+    "dom_id": "2c69bf0c-e6bd-4cb6-afc2-7b5733b8bfdd",
+    "msg_ids": {
+        "2016-c1659952-34fe-463c-ac59-b6dfbbda1359": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic edited the Enforce_signed-off_commits_in_pull-request, Minimum_score_to_merge_pull-request, and 6 others fields of project atelic/test",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic edited the Enforce_signed-off_commits_in_pull-request, Minimum_score_to_merge_pull-request, and 6 others fields of project atelic/test",
+            "long_form": "atelic edited the Enforce_signed-off_commits_in_pull-request, Minimum_score_to_merge_pull-request, and 6 others fields of project atelic/test",
+            "objects": [
+                "project/atelic/test"
+            ],
+            "link": "https://pagure.io/fork/atelic/test",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.project.edit",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> edited the Enforce_signed-off_commits_in_pull-request, Minimum_score_to_merge_pull-request, and 6 others fields of project <a href=\"/atelic/\" rel=\"nofollow\">@atelic</a>/test",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/fork/atelic/test",
+    "end_time": 1467917437,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.project.edit"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> forked project \"test\" to \"<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a>/test\"",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "a month ago",
+    "subtitle": "atelic forked project \"test\" to \"atelic/test\"",
+    "timestamp": 1467917378,
+    "start_time": 1467917378,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic forked project \"test\" to \"atelic/test\"",
+    "dom_id": "14c16b53-5f1d-4b9c-ad7e-beb5785495b6",
+    "msg_ids": {
+        "2016-4363f65e-705d-4f93-afc4-f675f309af0f": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic forked project \"test\" to \"atelic/test\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic forked project \"test\" to \"atelic/test\"",
+            "long_form": "atelic forked project \"test\" to \"atelic/test\"",
+            "objects": [
+                "project/atelic/test"
+            ],
+            "link": "https://pagure.io/fork/atelic/test",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.project.forked",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> forked project \"test\" to \"<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a>/test\"",
+            "lexer": null
+        }
+    },
+    "link": "https://pagure.io/fork/atelic/test",
+    "end_time": 1467917378,
+    "packages": [],
+    "topics": [
+        "io.pagure.prod.pagure.project.forked"
+    ],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png"
+}, {
+    "subtitle": "atelic interacted with pull-request #1101 of project \"pagure\" 14 times",
+    "timestamp": 1467451273,
+    "start_time": 1467319877,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with pull-request #1101 of project \"pagure\" 14 times",
+    "msg_ids": {
+        "2016-b8a93b19-e3d8-42a6-9aed-f6ebb61fe7e1": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6768",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-e913fda8-8488-457f-a6fd-f67a9e930bae": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6762",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-fb3f93c0-bcfb-4bfb-97fd-8746ee6b368e": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6863",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-bbcd7cf5-21a4-4c4f-8581-33a1b86948ed": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic opened pull-request#1101: \"Add dropdown select for # issue tag\" on project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic opened pull-request#1101: \"Add dropdown select for # issue tag\" on project \"pagure\"",
+            "long_form": "atelic opened pull-request#1101: \"Add dropdown select for # issue tag\" on project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened pull-request#1101: \"Add dropdown select for # issue tag\" on project \"pagure\""
+        },
+        "2016-4b437ae8-fac6-4f27-97a4-b96bad9a9fea": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6865",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-28a959ab-4de4-47b6-87f9-ba785a8d5d89": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6861",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.edited",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> edited a comment on pull-request#1101 of project \"pagure\""
+        },
+        "2016-eb3ccad2-6138-43a6-baf2-950ba7fba070": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6802",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-bd253eae-8e04-402a-b977-78097fc58d7b": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6861",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-b9769ced-94ad-4e2f-aea3-c0c4b33a8b9c": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6763",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-8d7af750-206b-4aa2-9f83-aaae05047564": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6790",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-0c38b714-1c1e-4842-a493-eab62a241bcd": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6767",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.edited",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> edited a comment on pull-request#1101 of project \"pagure\""
+        },
+        "2016-d13cf049-13a4-4e6a-ad59-65da3eada965": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6769",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        },
+        "2016-4fbe1729-d38e-480d-b524-a77c0506f546": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic edited a comment on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6767",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.edited",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> edited a comment on pull-request#1101 of project \"pagure\""
+        },
+        "2016-e9b83f54-31e0-46df-85c0-0d83e6fa0c07": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "long_form": "atelic commented on pull-request#1101 of project \"pagure\"",
+            "objects": [
+                "project/pagure",
+                "pull-request/1101"
+            ],
+            "link": "https://pagure.io/pagure/pull-request/1101#comment-6767",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#1101 of project \"pagure\""
+        }
+    },
+    "link": "https://pagure.io/pagure/pull-request/1101",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with pull-request #1101 of project \"pagure\" 14 times",
+    "dom_id": "ea29ebe1-603b-4dcc-a7ac-8944bb3adc13",
+    "end_time": 1467747435,
+    "human_time": "a month ago",
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added",
+        "io.pagure.prod.pagure.pull-request.new",
+        "io.pagure.prod.pagure.pull-request.comment.edited"
+    ]
+}, {
+    "subtitle": "atelic, devyani7_, and 4 others participated in fedora-hubs in #fedora-hubs",
+    "timestamp": 1467743322.6,
+    "start_time": 1467742586,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/01fe73d687f4db328da1183f2a1b5b22962ca9d9c50f0728aafeac974856311c?s=64&d=retro",
+    "subjective": "atelic, devyani7_, and 4 others participated in fedora-hubs in #fedora-hubs",
+    "msg_ids": {
+        "2016-125dc31b-3c19-40be-beaf-1df93bff1e39": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "subtitle": "pingou's meeting titled \"fedora-hubs\" ended in #fedora-hubs",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/01fe73d687f4db328da1183f2a1b5b22962ca9d9c50f0728aafeac974856311c?s=64&d=retro",
+            "subjective": "pingou's meeting titled \"fedora-hubs\" ended in #fedora-hubs",
+            "long_form": "<h5>=========================\n#fedora-hubs: fedora-hubs\n=========================\n\n\nMeeting started by pingou at 14:12:44 UTC. The full logs are available\nat\nhttps://meetbot.fedoraproject.org/fedora-hubs/2016-07-05/fedora-hubs.2016-07-05-14.12.log.html\n.\n\n\n\nMeeting summary\n---------------\n* rollcall  (pingou, 14:12:54)\n\n* status updates  (pingou, 14:16:26)\n  * LINK: https://pagure.io/fedora-bootstrap looks like only Ryan has\n    commit there  (pingou, 14:18:40)\n  * LINK: https://pagure.io/fedora-hubs/issue/33   (pingou, 14:28:18)\n\n* open-floor  (pingou, 14:38:50)\n\nMeeting ended at 14:41:19 UTC.\n\n\n\n\nAction Items\n------------\n\n\n\n\n\nAction Items, by person\n-----------------------\n* **UNASSIGNED**\n  * (none)\n\n\n\n\nPeople Present (lines said)\n---------------------------\n* pingou (50)\n* atelic (10)\n* radhikak (7)\n* zodbot (7)\n* devyani7_ (6)\n* stickster (4)\n\n\n\n\nGenerated by `MeetBot`_ 0.1.4\n\n.. _`MeetBot`: http://wiki.debian.org/MeetBot\n</h5>",
+            "objects": [
+                "attendees/devyani7_",
+                "titles/fedora-hubs",
+                "attendees/stickster",
+                "topics/open-floor",
+                "channels/#fedora-hubs",
+                "attendees/pingou",
+                "attendees/radhikak",
+                "attendees/atelic"
+            ],
+            "link": "https://meetbot.fedoraproject.org/fedora-hubs/2016-07-05/fedora-hubs.2016-07-05-14.12.html",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "usernames": [
+                "atelic",
+                "radhikak",
+                "stickster",
+                "pingou",
+                "devyani7_"
+            ],
+            "title": "meetbot.meeting.complete",
+            "markup": "<a href=\"/pingou/\" rel=\"nofollow\">@pingou</a>'s meeting titled \"fedora-hubs\" ended in #fedora-hubs"
+        },
+        "2016-a6dab4e9-3ab8-4668-a2cb-c3aec2fd3b9c": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "subtitle": "pingou linked to more information in the \"fedora-hubs\" meeting in #fedora-hubs: \"https://pagure.io/fedora-bootstrap looks like only Ryan has commit there\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/01fe73d687f4db328da1183f2a1b5b22962ca9d9c50f0728aafeac974856311c?s=64&d=retro",
+            "subjective": "pingou linked to more information in the \"fedora-hubs\" meeting in #fedora-hubs: \"https://pagure.io/fedora-bootstrap looks like only Ryan has commit there\"",
+            "long_form": "pingou linked to more information in the \"fedora-hubs\" meeting in #fedora-hubs: \"https://pagure.io/fedora-bootstrap looks like only Ryan has commit there\"",
+            "objects": [
+                "attendees/devyani7_",
+                "titles/fedora-hubs",
+                "attendees/stickster",
+                "channels/#fedora-hubs",
+                "attendees/pingou",
+                "attendees/radhikak",
+                "attendees/atelic",
+                "topics/status updates"
+            ],
+            "link": "https://pagure.io/fedora-bootstrap",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "usernames": [
+                "atelic",
+                "radhikak",
+                "stickster",
+                "pingou",
+                "devyani7_"
+            ],
+            "title": "meetbot.meeting.item.link",
+            "markup": "<a href=\"/pingou/\" rel=\"nofollow\">@pingou</a> linked to more information in the \"fedora-hubs\" meeting in #fedora-hubs: \"https://pagure.io/fedora-bootstrap looks like only Ryan has commit there\""
+        },
+        "2016-5d644757-f50d-4b53-a813-9382afb747fd": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "subtitle": "The topic of pingou's \"fedora-hubs\" meeting changed to \"status updates\" in #fedora-hubs",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/01fe73d687f4db328da1183f2a1b5b22962ca9d9c50f0728aafeac974856311c?s=64&d=retro",
+            "subjective": "The topic of pingou's \"fedora-hubs\" meeting changed to \"status updates\" in #fedora-hubs",
+            "long_form": "The topic of pingou's \"fedora-hubs\" meeting changed to \"status updates\" in #fedora-hubs",
+            "objects": [
+                "attendees/devyani7_",
+                "titles/fedora-hubs",
+                "attendees/stickster",
+                "channels/#fedora-hubs",
+                "attendees/pingou",
+                "attendees/radhikak",
+                "attendees/atelic",
+                "topics/status updates"
+            ],
+            "link": null,
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "usernames": [
+                "atelic",
+                "radhikak",
+                "stickster",
+                "pingou",
+                "devyani7_"
+            ],
+            "title": "meetbot.meeting.topic.update",
+            "markup": "The topic of <a href=\"/pingou/\" rel=\"nofollow\">@pingou</a>'s \"fedora-hubs\" meeting changed to \"status updates\" in #fedora-hubs"
+        },
+        "2016-ff66aff5-bb8a-462b-9bfb-58f7fe79ab6d": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "subtitle": "pingou linked to more information in the \"fedora-hubs\" meeting in #fedora-hubs: \"https://pagure.io/fedora-hubs/issue/33\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/01fe73d687f4db328da1183f2a1b5b22962ca9d9c50f0728aafeac974856311c?s=64&d=retro",
+            "subjective": "pingou linked to more information in the \"fedora-hubs\" meeting in #fedora-hubs: \"https://pagure.io/fedora-hubs/issue/33\"",
+            "long_form": "pingou linked to more information in the \"fedora-hubs\" meeting in #fedora-hubs: \"https://pagure.io/fedora-hubs/issue/33\"",
+            "objects": [
+                "attendees/devyani7_",
+                "titles/fedora-hubs",
+                "attendees/stickster",
+                "channels/#fedora-hubs",
+                "attendees/pingou",
+                "attendees/radhikak",
+                "attendees/atelic",
+                "topics/status updates"
+            ],
+            "link": null,
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "usernames": [
+                "atelic",
+                "radhikak",
+                "stickster",
+                "pingou",
+                "devyani7_"
+            ],
+            "title": "meetbot.meeting.item.link",
+            "markup": "<a href=\"/pingou/\" rel=\"nofollow\">@pingou</a> linked to more information in the \"fedora-hubs\" meeting in #fedora-hubs: \"https://pagure.io/fedora-hubs/issue/33\""
+        },
+        "2016-6e626d88-14e8-42b7-be6d-29ea97e4fa30": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "subtitle": "The topic of pingou's \"fedora-hubs\" meeting changed to \"open-floor\" in #fedora-hubs",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/01fe73d687f4db328da1183f2a1b5b22962ca9d9c50f0728aafeac974856311c?s=64&d=retro",
+            "subjective": "The topic of pingou's \"fedora-hubs\" meeting changed to \"open-floor\" in #fedora-hubs",
+            "long_form": "The topic of pingou's \"fedora-hubs\" meeting changed to \"open-floor\" in #fedora-hubs",
+            "objects": [
+                "attendees/devyani7_",
+                "titles/fedora-hubs",
+                "attendees/stickster",
+                "topics/open-floor",
+                "channels/#fedora-hubs",
+                "attendees/pingou",
+                "attendees/radhikak",
+                "attendees/atelic"
+            ],
+            "link": null,
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+            "usernames": [
+                "atelic",
+                "radhikak",
+                "stickster",
+                "pingou",
+                "devyani7_"
+            ],
+            "title": "meetbot.meeting.topic.update",
+            "markup": "The topic of <a href=\"/pingou/\" rel=\"nofollow\">@pingou</a>'s \"fedora-hubs\" meeting changed to \"open-floor\" in #fedora-hubs"
+        }
+    },
+    "link": "https://meetbot.fedoraproject.org/fedora-hubs/2016-07-05/fedora-hubs.2016-07-05-14.12",
+    "packages": [],
+    "categories": [
+        "meetbot"
+    ],
+    "icon": "https://apps.fedoraproject.org/img/icons/meetbot.png",
+    "usernames": [
+        "atelic",
+        "radhikak",
+        "stickster",
+        "pingou",
+        "devyani7_"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a>, <a href=\"/devyani7_/\" rel=\"nofollow\">@devyani7_</a>, and 4 others participated in fedora-hubs in #fedora-hubs",
+    "dom_id": "0270822e-fd91-4184-b38d-818ea99b8e0a",
+    "end_time": 1467744079,
+    "human_time": "a month ago",
+    "topics": [
+        "org.fedoraproject.prod.meetbot.meeting.complete",
+        "org.fedoraproject.prod.meetbot.meeting.item.link",
+        "org.fedoraproject.prod.meetbot.meeting.topic.update"
+    ]
+}, {
+    "subtitle": "atelic interacted with pull-request #206 of project \"fedora-hubs\" 2 times",
+    "timestamp": 1467268038.5,
+    "start_time": 1467228169,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with pull-request #206 of project \"fedora-hubs\" 2 times",
+    "msg_ids": {
+        "2016-23d3bb2f-431a-43f4-bc5b-a3774791e304": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#206 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#206 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#206 of project \"fedora-hubs\"",
+            "objects": [
+                "pull-request/206",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/206#comment-6746",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#206 of project \"fedora-hubs\""
+        },
+        "2016-75011179-c035-421c-b76e-ac18af8a6dcb": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic opened pull-request#206: \"Fix issue with Feed elements not rendering\" on project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic opened pull-request#206: \"Fix issue with Feed elements not rendering\" on project \"fedora-hubs\"",
+            "long_form": "atelic opened pull-request#206: \"Fix issue with Feed elements not rendering\" on project \"fedora-hubs\"",
+            "objects": [
+                "pull-request/206",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/206",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened pull-request#206: \"Fix issue with Feed elements not rendering\" on project \"fedora-hubs\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/206",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with pull-request #206 of project \"fedora-hubs\" 2 times",
+    "dom_id": "c782076a-b8d5-4115-a375-3dcb57990454",
+    "end_time": 1467307908,
+    "human_time": "a month ago",
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added",
+        "io.pagure.prod.pagure.pull-request.new"
+    ]
+}, {
+    "subtitle": "atelic interacted with pull-request #207 of project \"fedora-hubs\" 4 times",
+    "timestamp": 1467233522.75,
+    "start_time": 1467228942,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic interacted with pull-request #207 of project \"fedora-hubs\" 4 times",
+    "msg_ids": {
+        "2016-90a67858-c666-43aa-9a97-1580e479a0c4": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "objects": [
+                "pull-request/207",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/207#comment-6680",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#207 of project \"fedora-hubs\""
+        },
+        "2016-e805a4ad-26e8-4cf3-86a1-697158736e77": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic opened pull-request#207: \"Move 4 spaces to 2 in JSX files\" on project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic opened pull-request#207: \"Move 4 spaces to 2 in JSX files\" on project \"fedora-hubs\"",
+            "long_form": "atelic opened pull-request#207: \"Move 4 spaces to 2 in JSX files\" on project \"fedora-hubs\"",
+            "objects": [
+                "pull-request/207",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/207",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.new",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> opened pull-request#207: \"Move 4 spaces to 2 in JSX files\" on project \"fedora-hubs\""
+        },
+        "2016-f94132c3-6a07-4e06-9b0b-18ce93e03f19": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "objects": [
+                "pull-request/207",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/207#comment-6688",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#207 of project \"fedora-hubs\""
+        },
+        "2016-54b4e842-73cb-4b7d-8b53-5a27a433ac22": {
+            "__icon__": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "subtitle": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "long_form": "atelic commented on pull-request#207 of project \"fedora-hubs\"",
+            "objects": [
+                "pull-request/207",
+                "project/fedora-hubs"
+            ],
+            "link": "https://pagure.io/fedora-hubs/pull-request/207#comment-6681",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "pagure.pull-request.comment.added",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> commented on pull-request#207 of project \"fedora-hubs\""
+        }
+    },
+    "link": "https://pagure.io/fedora-hubs/pull-request/207",
+    "packages": [],
+    "categories": [
+        "pagure"
+    ],
+    "icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png",
+    "usernames": [
+        "atelic"
+    ],
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> interacted with pull-request #207 of project \"fedora-hubs\" 4 times",
+    "dom_id": "16c799c2-d090-441e-a422-a2b6d7c8d062",
+    "end_time": 1467246314,
+    "human_time": "a month ago",
+    "topics": [
+        "io.pagure.prod.pagure.pull-request.comment.added",
+        "io.pagure.prod.pagure.pull-request.new"
+    ]
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "a month ago",
+    "subtitle": "atelic updated their irc filters",
+    "timestamp": 1467236930,
+    "start_time": 1467236930,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic updated their irc filters",
+    "dom_id": "9b57a8fc-ddde-4cfc-bff8-69759ace8d39",
+    "msg_ids": {
+        "2016-a32a2741-63dc-451f-af31-b67f986844af": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "subtitle": "atelic updated their irc filters",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic updated their irc filters",
+            "long_form": "atelic updated their irc filters",
+            "objects": [
+                "atelic/irc/preference/filters"
+            ],
+            "link": "https://apps.fedoraproject.org/notifications/",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "fmn.preference.update",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+            "lexer": null
+        }
+    },
+    "link": "https://apps.fedoraproject.org/notifications/",
+    "end_time": 1467236930,
+    "packages": [],
+    "topics": [
+        "org.fedoraproject.prod.fmn.preference.update"
+    ],
+    "categories": [
+        "fmn"
+    ],
+    "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "a month ago",
+    "subtitle": "atelic updated their irc filters",
+    "timestamp": 1467236926,
+    "start_time": 1467236926,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic updated their irc filters",
+    "dom_id": "d4a17d44-bebb-439b-8b42-027a6c46ec51",
+    "msg_ids": {
+        "2016-bfb5661b-1f74-4fe9-a6b6-c02494846ba9": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "subtitle": "atelic updated their irc filters",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic updated their irc filters",
+            "long_form": "atelic updated their irc filters",
+            "objects": [
+                "atelic/irc/preference/filters"
+            ],
+            "link": "https://apps.fedoraproject.org/notifications/",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "fmn.preference.update",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+            "lexer": null
+        }
+    },
+    "link": "https://apps.fedoraproject.org/notifications/",
+    "end_time": 1467236926,
+    "packages": [],
+    "topics": [
+        "org.fedoraproject.prod.fmn.preference.update"
+    ],
+    "categories": [
+        "fmn"
+    ],
+    "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "a month ago",
+    "subtitle": "atelic updated their irc filters",
+    "timestamp": 1467236923,
+    "start_time": 1467236923,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic updated their irc filters",
+    "dom_id": "a058f25c-4513-4b0c-a591-5bf8ba397d87",
+    "msg_ids": {
+        "2016-dc33fbb0-a733-46a8-8254-1611d0b20393": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "subtitle": "atelic updated their irc filters",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic updated their irc filters",
+            "long_form": "atelic updated their irc filters",
+            "objects": [
+                "atelic/irc/preference/filters"
+            ],
+            "link": "https://apps.fedoraproject.org/notifications/",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "fmn.preference.update",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+            "lexer": null
+        }
+    },
+    "link": "https://apps.fedoraproject.org/notifications/",
+    "end_time": 1467236923,
+    "packages": [],
+    "topics": [
+        "org.fedoraproject.prod.fmn.preference.update"
+    ],
+    "categories": [
+        "fmn"
+    ],
+    "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated the rules on a fmn irc filter",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "a month ago",
+    "subtitle": "atelic updated the rules on a fmn irc filter",
+    "timestamp": 1467226515,
+    "start_time": 1467226515,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic updated the rules on a fmn irc filter",
+    "dom_id": "e6372a10-148e-4967-99c7-c6780742be67",
+    "msg_ids": {
+        "2016-425ad1ce-4c10-4c12-8f9c-7460ae4fe351": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "subtitle": "atelic updated the rules on a fmn irc filter",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic updated the rules on a fmn irc filter",
+            "long_form": "atelic updated the rules on a fmn irc filter",
+            "objects": [
+                "atelic/irc/filter/rules"
+            ],
+            "link": "https://apps.fedoraproject.org/notifications/",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "fmn.filter.update",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated the rules on a fmn irc filter",
+            "lexer": null
+        }
+    },
+    "link": "https://apps.fedoraproject.org/notifications/",
+    "end_time": 1467226515,
+    "packages": [],
+    "topics": [
+        "org.fedoraproject.prod.fmn.filter.update"
+    ],
+    "categories": [
+        "fmn"
+    ],
+    "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "a month ago",
+    "subtitle": "atelic updated their irc filters",
+    "timestamp": 1467226439,
+    "start_time": 1467226439,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic updated their irc filters",
+    "dom_id": "4153a81c-b44d-44a7-8240-4b0820835768",
+    "msg_ids": {
+        "2016-45d90281-40ee-4f22-9901-4ef849c49810": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "subtitle": "atelic updated their irc filters",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic updated their irc filters",
+            "long_form": "atelic updated their irc filters",
+            "objects": [
+                "atelic/irc/preference/filters"
+            ],
+            "link": "https://apps.fedoraproject.org/notifications/",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "fmn.preference.update",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+            "lexer": null
+        }
+    },
+    "link": "https://apps.fedoraproject.org/notifications/",
+    "end_time": 1467226439,
+    "packages": [],
+    "topics": [
+        "org.fedoraproject.prod.fmn.preference.update"
+    ],
+    "categories": [
+        "fmn"
+    ],
+    "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png"
+}, {
+    "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+    "usernames": [
+        "atelic"
+    ],
+    "human_time": "a month ago",
+    "subtitle": "atelic updated their irc filters",
+    "timestamp": 1467226437,
+    "start_time": 1467226437,
+    "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+    "subjective": "atelic updated their irc filters",
+    "dom_id": "487b1807-b798-4b98-ae35-c5819ffc5db0",
+    "msg_ids": {
+        "2016-06917565-a0ac-4c2a-bb7f-fefe98df903d": {
+            "__icon__": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "subtitle": "atelic updated their irc filters",
+            "secondary_icon": "https://seccdn.libravatar.org/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea?s=64&d=retro",
+            "subjective": "atelic updated their irc filters",
+            "long_form": "atelic updated their irc filters",
+            "objects": [
+                "atelic/irc/preference/filters"
+            ],
+            "link": "https://apps.fedoraproject.org/notifications/",
+            "packages": [],
+            "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png",
+            "usernames": [
+                "atelic"
+            ],
+            "title": "fmn.preference.update",
+            "markup": "<a href=\"/atelic/\" rel=\"nofollow\">@atelic</a> updated their irc filters",
+            "lexer": null
+        }
+    },
+    "link": "https://apps.fedoraproject.org/notifications/",
+    "end_time": 1467226437,
+    "packages": [],
+    "topics": [
+        "org.fedoraproject.prod.fmn.preference.update"
+    ],
+    "categories": [
+        "fmn"
+    ],
+    "icon": "https://apps.fedoraproject.org/img/icons/fedmsg.png"
+}]
diff --git a/hubs/app.py b/hubs/app.py
index ff9cb3a..43f32a6 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -523,17 +523,9 @@ def stream(name):
     saved = hubs.models.SavedNotification.by_username(session, name)
     saved = [n.__json__() for n in saved]
 
-    stream = hubs.stream.Stream(name)
-    actions = stream.get_actions()
+    stream = hubs.stream.Stream()
+    actions = stream.get_json()
 
-    # An action that has already been saved should not have a saved button
-    # this tags all the saved actions so the front end can decide to display or not.
-    # This will get slow pretty quickly so we should reevaluate after flock
-    for action in actions:
-        if any(s['markup'] == action['markup'] for s in saved):
-            action['saved'] = True
-
-    actions = json.dumps(actions, cls=hubs.stream.PythonObjectEncoder)
     return flask.render_template(
         'stream.html',
         hub=hub,
diff --git a/hubs/stream.py b/hubs/stream.py
index 8c33bf2..fd45923 100644
--- a/hubs/stream.py
+++ b/hubs/stream.py
@@ -1,166 +1,9 @@
-import cgi
-import datetime
 import json
-import uuid
+import os
 
-import datanommer.models
-import flask
-import fmn.lib
-import munch
-import pygments.formatters
-import six
-
-import fedmsg.config
-import fedmsg.meta
-
-
-# TODO Temporary: This is a proof of concept for Flock
-# This needs to be done in fnm.lib
 class Stream(object):
-    paths = fmn.lib.load_rules(root='fmn.rules')
-    fedmsg_config = fedmsg.config.load_config()
-    delta = datetime.timedelta(days=365)
-    end = datetime.datetime.utcnow()
-
-    def __init__(self, name):
-        self.name = name
-        self.preference = {
-            'filters': [{
-                'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
-                'id': 20506,
-                'name': 'Events referring to my username',
-                'oneshot': None,
-                'rules': [
-                    {
-                        'arguments': {
-                            'fasnick': name
-                        },
-                        'cache_key':
-                        '2f59dcddf85cab5822af8d40d82c7390ce0f80b40119f2db67f3037a0594a2a1',
-                        'code_path': 'fmn.rules:user_filter',
-                        'created_on': 'Thu, 15 Jan 2015 16:07:43 GMT',
-                        'info': {
-                            'args': [
-                                'config', 'message', 'fasnick'
-                            ],
-                            'datanommer-hints': {},
-                            'doc':
-                            "<p>Use this rule to include messages that are associated with a\nspecific user.</p>",
-                            'doc-no-links':
-                            '<html><head></head><body><p>Use this rule to include messages that are associated with a\nspecific user.</p>\n</body></html>',
-                            'hints-invertible': True,
-                            'submodule': 'generic',
-                            'title': 'A particular user'
-                        },
-                        'negated': False
-                    }
-                ]
-            }]
-        }
-
-    def apply_html(self, matches):
-        # Smash multiple messages into single ones
-        matches = fedmsg.meta.conglomerate(matches,
-                                           lexers=True,
-                                           **self.fedmsg_config)
-
-        # Apply some html markup to the subtitles for extra fanciness.
-        for match in matches:
-            match['markup'] = self.apply_markup(match)
-            for idx, constituent in match['msg_ids'].items():
-                constituent['markup'] = self.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 matches
-
-    def get_actions(self):
-        preference = self.rehydrate_preference(self.preference)
-        messages = []
-        actions = []
-
-        for fmn_filter in preference['filters']:
-            fmn_hinting = fmn.lib.hinting.gather_hinting(
-                self.fedmsg_config, fmn_filter['rules'], self.paths)
-            total, pages, rows = datanommer.models.Message.grep(
-                start=self.end - self.delta,
-                end=self.end,
-                rows_per_page=100,
-                page=1,
-                order='desc',
-                **fmn_hinting)
-            messages.extend(rows)
-
-
-        for message in messages:
-            original = message.__json__()
-            if self.name in fedmsg.meta.msg2usernames(original, **
-                                                      self.fedmsg_config):
-                actions.append(original)
-
-        return self.apply_html(actions)
-
-    def rehydrate_preference(self, preference):
-        for fltr in preference['filters']:
-            fltr['rules'] = [munch.Munch(r) for r in fltr['rules']]
-            for rule in fltr['rules']:
-                code_path = str(rule.code_path)
-                rule.fn = fedmsg.utils.load_class(code_path)
-
-        return preference
-
-    def escape_html(self, match):
-        for key in ['subjective', 'subtitle']:
-            match[key] = cgi.escape(match[key])
-        try:
-            match['long_form'] = cgi.escape(match['long_form'])
-        except:
-            # not all matches have a long form
-            pass
-        return match
-
-    def apply_markup(self, match):
-        match = self.escape_html(match)
-        markup = match['subtitle']
-        for username in match['usernames']:
-            args = (username, '<a href="{url}" rel="nofollow">@{username}</a>'.format(
-                url=flask.url_for('hub', name=username),
-                username=username, ))
-            markup = markup.replace(*args)
-            # TODO -- have to add these to conglomerate first
-            # match['long_form'] = match['long_form'].replace(*args)
-
-        for package in match['packages']:
-            args = (package, '<a href="{url}">{package}</a>'.format(
-                url='https://apps.fedoraproject.org/packages/' + package,
-                package=package, ))
-            markup = markup.replace(*args)
-            # TODO -- have to add these to conglomerate first
-            # match['long_form'] = match['long_form'].replace(*args)
-
-        return markup
-
-
-class PythonObjectEncoder(json.JSONEncoder):
-    ''' A JSON encoder that handles python data structures in encoding'''
-
-    def default(self, obj):
-        if isinstance(obj, (list, dict, str, unicode, int, float, bool,
-                            type(None))):
-            return json.JSONEncoder.default(self, obj)
-        if isinstance(obj, set) or isinstance(obj, tuple):
-            return list(obj)
-        if isinstance(obj, object):
-            return obj.__dict__
-        else:
-            raise TypeError
+    def get_json(self):
+        here = os.path.dirname(os.path.abspath(__file__))
+        with open(os.path.join(here, 'actions.json')) as fp:
+            actions = fp.read()
+        return actions
diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html
index 5e6fc57..49a3dab 100644
--- a/hubs/templates/stream.html
+++ b/hubs/templates/stream.html
@@ -126,9 +126,6 @@
 
  function setup_feeds() {
    var actions = {{actions|safe}};
-   var options = {
-
-   }
 
    var streamFeed = React.createElement(Feed, {
      matches: actions,

From 4cbd8ced5c6d4e803ad188fb89d2d2c29dd53b7c Mon Sep 17 00:00:00 2001
From: Eric Barbour <ebarbour@redhat.com>
Date: Aug 10 2016 19:37:26 +0000
Subject: [PATCH 16/16] Force int idx for notification routes.


- To be consistent with other routes, force idx to be an int.
- Update SavedNotification tests to expect this

---

diff --git a/hubs/app.py b/hubs/app.py
index 43f32a6..17b56ad 100755
--- a/hubs/app.py
+++ b/hubs/app.py
@@ -568,8 +568,8 @@ def notifications(user):
             dict(notification=notification.__json__(), success=True)
         )
 
-@app.route('/<user>/notifications/<idx>', methods=['DELETE'])
-@app.route('/<user>/notifications/<idx>/', methods=['DELETE'])
+@app.route('/<user>/notifications/<int:idx>', methods=['DELETE'])
+@app.route('/<user>/notifications/<int:idx>/', methods=['DELETE'])
 @login_required
 def delete_notifications(user, idx):
     notification = session.query(hubs.models.SavedNotification).filter_by(idx=idx).first()
diff --git a/hubs/tests/test_api/test_notifications.py b/hubs/tests/test_api/test_notifications.py
index 6706031..74acdc1 100644
--- a/hubs/tests/test_api/test_notifications.py
+++ b/hubs/tests/test_api/test_notifications.py
@@ -111,11 +111,11 @@ class TestDeleteNotifications(hubs.tests.APPTest):
 
         self.assertIsNone(notification)
 
-    def test_400_on_bad_idx(self):
+    def test_404_on_bad_idx(self):
         idx = 'thisisastringnotanint'
 
         with hubs.tests.auth_set(app, self.user):
             resp = self.app.delete(
                 '/{}/notifications/{}/'.format(self.user.username, idx)
             )
-        self.assertEqual(resp.status_code, 400)
+        self.assertEqual(resp.status_code, 404)

