diff --git a/gulpfile.js b/gulpfile.js
index c25abcae7119a05d57cf7b17697f43ccd392ed72..5164b87387ebb51afe39cf3dffd7f0f5d733fbf2 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -235,10 +235,13 @@ gulp.task('watch:static', function watch () {
 gulp.task('watch:app', function watch (callback) {
     var webpackOptions = webpackDev();
 
-    webpackOptions.entry.unshift(
-        `webpack-dev-server/client?http://localhost:${webpackOptions.devServer.port}/`,
-        `webpack/hot/dev-server`
-    );
+    webpackOptions.entry = Object.keys(webpackOptions.entry).reduce((acc, key) => {
+        acc[key] = [
+            `webpack-dev-server/client?http://localhost:${webpackOptions.devServer.port}/`,
+            'webpack/hot/dev-server',
+        ].concat(webpackOptions.entry[key])
+        return acc;
+    }, {});
 
     var serverOptions = Object.assign(
         {}, webpackOptions.devServer, {
diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index a7cb75e0afddeb0094c57b55fb515547cb29ac91..d38841979716887ec80ee92c46a5ea6067299075 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -4,6 +4,7 @@ import requests
 from django.db import models
 from django.conf import settings
 from django.contrib import messages
+from django.contrib.auth import get_user_model
 from django.template.loader import render_to_string
 
 from .models import INTERNAL, PUBLIC
@@ -11,8 +12,12 @@ from .options import MESSAGES
 from .tasks import send_mail
 
 
+User = get_user_model()
+
+
 def link_to(target, request):
-    return request.scheme + '://' + request.get_host() + target.get_absolute_url()
+    if target:
+        return request.scheme + '://' + request.get_host() + target.get_absolute_url()
 
 
 neat_related = {
@@ -70,11 +75,38 @@ class AdapterBase:
     def recipients(self, message_type, **kwargs):
         raise NotImplementedError()
 
+    def batch_recipients(self, message_type, submissions, **kwargs):
+        # Default batch recipients is to send a message to each of the recipients that would
+        # receive a message under normal conditions
+        return [
+            {
+                'recipients': self.recipients(message_type, submission=submission, **kwargs),
+                'submissions': [submission]
+            }
+            for submission in submissions
+        ]
+
+    def process_batch(self, message_type, events, request, user, submissions, related=None, **kwargs):
+        events_by_submission = {
+            event.submission.id: event
+            for event in events
+        }
+        for recipient in self.batch_recipients(message_type, submissions, **kwargs):
+            recipients = recipient['recipients']
+            submissions = recipient['submissions']
+            events = [events_by_submission[submission.id] for submission in submissions]
+            self.process_send(message_type, recipients, events, request, user, submissions=submissions, submission=None, related=related, **kwargs)
+
     def process(self, message_type, event, request, user, submission, related=None, **kwargs):
+        recipients = self.recipients(message_type, submission=submission, **kwargs)
+        self.process_send(message_type, recipients, [event], request, user, submission, related=related, **kwargs)
+
+    def process_send(self, message_type, recipients, events, request, user, submission, submissions=list(), related=None, **kwargs):
         kwargs = {
             'request': request,
             'user': user,
             'submission': submission,
+            'submissions': submissions,
             'related': related,
             **kwargs,
         }
@@ -85,30 +117,40 @@ class AdapterBase:
         if not message:
             return
 
-        for recipient in self.recipients(message_type, **kwargs):
-            message_log = self.create_log(message, recipient, event)
+        for recipient in recipients:
+            message_logs = self.create_logs(message, recipient, *events)
+
             if settings.SEND_MESSAGES or self.always_send:
-                status = self.send_message(message, recipient=recipient, message_log=message_log, **kwargs)
+                status = self.send_message(message, recipient=recipient, logs=message_logs, **kwargs)
             else:
                 status = 'Message not sent as SEND_MESSAGES==FALSE'
 
-            message_log.update_status(status)
+            message_logs.update_status(status)
 
             if not settings.SEND_MESSAGES:
                 if recipient:
                     message = '{} [to: {}]: {}'.format(self.adapter_type, recipient, message)
                 else:
                     message = '{}: {}'.format(self.adapter_type, message)
-                messages.add_message(request, messages.INFO, message)
+                messages.add_message(request, messages.DEBUG, message)
 
-    def create_log(self, message, recipient, event):
+    def create_logs(self, message, recipient, *events):
         from .models import Message
-        return Message.objects.create(
-            type=self.adapter_type,
-            content=message,
-            recipient=recipient or '',
-            event=event,
+        messages = Message.objects.bulk_create(
+            Message(
+                **self.log_kwargs(message, recipient, event)
+            )
+            for event in events
         )
+        return Message.objects.filter(id__in=[message.id for message in messages])
+
+    def log_kwargs(self, message, recipient, event):
+        return {
+            'type': self.adapter_type,
+            'content': message,
+            'recipient': recipient or '',
+            'event': event,
+        }
 
     def send_message(self, message, **kwargs):
         # Process the message, should return the result of the send
@@ -128,6 +170,7 @@ class ActivityAdapter(AdapterBase):
         MESSAGES.DETERMINATION_OUTCOME: 'Sent a determination. Outcome: {determination.clean_outcome}',
         MESSAGES.INVITED_TO_PROPOSAL: 'Invited to submit a proposal',
         MESSAGES.REVIEWERS_UPDATED: 'reviewers_updated',
+        MESSAGES.BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated',
         MESSAGES.NEW_REVIEW: 'Submitted a review',
         MESSAGES.OPENED_SEALED: 'Opened the submission while still sealed',
         MESSAGES.SCREENING: 'Screening status from {old_status} to {submission.screening_status}'
@@ -145,7 +188,7 @@ class ActivityAdapter(AdapterBase):
             return {'visibility': INTERNAL}
         return {}
 
-    def reviewers_updated(self, added, removed, **kwargs):
+    def reviewers_updated(self, added=list(), removed=list(), **kwargs):
         message = ['Reviewers updated.']
         if added:
             message.append('Added:')
@@ -157,6 +200,9 @@ class ActivityAdapter(AdapterBase):
 
         return ' '.join(message)
 
+    def batch_reviewers_updated(self, added, **kwargs):
+        return 'Batch ' + self.reviewers_updated(added, **kwargs)
+
     def handle_transition(self, old_phase, submission, **kwargs):
         base_message = 'Progressed from {old_display} to {new_display}'
 
@@ -184,7 +230,7 @@ class ActivityAdapter(AdapterBase):
 
         return staff_message
 
-    def send_message(self, message, user, submission, **kwargs):
+    def send_message(self, message, user, submission, submissions, **kwargs):
         from .models import Activity, PUBLIC
         visibility = kwargs.get('visibility', PUBLIC)
 
@@ -195,6 +241,12 @@ class ActivityAdapter(AdapterBase):
         else:
             related_object = None
 
+        try:
+            # If this was a batch action we want to pull out the submission
+            submission = submissions[0]
+        except IndexError:
+            pass
+
         Activity.actions.create(
             user=user,
             submission=submission,
@@ -214,6 +266,7 @@ class SlackAdapter(AdapterBase):
         MESSAGES.EDIT: '{user} has edited <{link}|{submission.title}>',
         MESSAGES.APPLICANT_EDIT: '{user} has edited <{link}|{submission.title}>',
         MESSAGES.REVIEWERS_UPDATED: '{user} has updated the reviewers on <{link}|{submission.title}>',
+        MESSAGES.BATCH_REVIEWERS_UPDATED: 'handle_batch_reviewers',
         MESSAGES.TRANSITION: '{user} has updated the status of <{link}|{submission.title}>: {old_phase.display_name} → {submission.phase}',
         MESSAGES.DETERMINATION_OUTCOME: 'A determination for <{link}|{submission.title}> was sent by email. Outcome: {determination.clean_outcome}',
         MESSAGES.PROPOSAL_SUBMITTED: 'A proposal has been submitted for review: <{link}|{submission.title}>',
@@ -230,13 +283,45 @@ class SlackAdapter(AdapterBase):
 
     def extra_kwargs(self, message_type, **kwargs):
         submission = kwargs['submission']
+        submissions = kwargs['submissions']
         request = kwargs['request']
         link = link_to(submission, request)
-        return {'link': link}
+        links = {
+            submission.id: link_to(submission, request)
+            for submission in submissions
+        }
+        return {
+            'link': link,
+            'links': links,
+        }
 
     def recipients(self, message_type, submission, **kwargs):
         return [self.slack_id(submission.lead)]
 
+    def batch_recipients(self, message_type, submissions, **kwargs):
+        # We group the messages by lead
+        leads = User.objects.filter(id__in=submissions.values('lead'))
+        return [
+            {
+                'recipients': [self.slack_id(lead)],
+                'submissions': submissions.filter(lead=lead),
+            } for lead in leads
+        ]
+
+    def handle_batch_reviewers(self, submissions, links, user, added, **kwargs):
+        submissions_text = ', '.join(
+            f'<{links[submission.id]}|{submission.title}>'
+            for submission in submissions
+        )
+        reviewers_text = ', '.join([str(user) for user in added])
+        return (
+            '{user} has batch added {reviewers_text} as reviewers on: {submissions_text}'.format(
+                user=user,
+                submissions_text=submissions_text,
+                reviewers_text=reviewers_text,
+            )
+        )
+
     def notify_reviewers(self, submission, **kwargs):
         reviewers_to_notify = []
         for reviewer in submission.reviewers.all():
@@ -317,13 +402,17 @@ class EmailAdapter(AdapterBase):
         MESSAGES.READY_FOR_REVIEW: 'messages/email/ready_to_review.html',
     }
 
-    def extra_kwargs(self, message_type, submission, **kwargs):
-        if message_type == MESSAGES.READY_FOR_REVIEW:
-            subject = 'Application ready to review: {submission.title}'.format(submission=submission)
-        else:
-            subject = submission.page.specific.subject or 'Your application to Open Technology Fund: {submission.title}'.format(submission=submission)
+    def get_subject(self, message_type, submission):
+        if submission:
+            if message_type == MESSAGES.READY_FOR_REVIEW:
+                subject = 'Application ready to review: {submission.title}'.format(submission=submission)
+            else:
+                subject = submission.page.specific.subject or 'Your application to Open Technology Fund: {submission.title}'.format(submission=submission)
+            return subject
+
+    def extra_kwargs(self, message_type, submission, submissions, **kwargs):
         return {
-            'subject': subject,
+            'subject': self.get_subject(message_type, submission),
         }
 
     def notify_comment(self, **kwargs):
@@ -352,37 +441,76 @@ class EmailAdapter(AdapterBase):
     def render_message(self, template, **kwargs):
         return render_to_string(template, kwargs)
 
-    def send_message(self, message, submission, subject, recipient, **kwargs):
+    def send_message(self, message, submission, subject, recipient, logs, **kwargs):
         try:
             send_mail(
                 subject,
                 message,
                 submission.page.specific.from_address,
                 [recipient],
-                log=kwargs['message_log']
+                logs=logs
             )
         except Exception as e:
             return 'Error: ' + str(e)
 
 
+class DjangoMessagesAdapter(AdapterBase):
+    adapter_type = 'Django'
+    always_send = True
+
+    messages = {
+        MESSAGES.BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated',
+    }
+
+    def batch_reviewers_updated(self, added, submissions, **kwargs):
+        return (
+            'Batch reviewers added: ' +
+            ', '.join([str(user) for user in added]) +
+            ' to ' +
+            ', '.join(['"{}"'.format(submission.title) for submission in submissions])
+        )
+
+    def recipients(self, *args, **kwargs):
+        return [None]
+
+    def batch_recipients(self, message_type, submissions, *args, **kwargs):
+        return [{
+            'recipients': [None],
+            'submissions': submissions,
+        }]
+
+    def send_message(self, message, request, **kwargs):
+        messages.add_message(request, messages.INFO, message)
+
+
 class MessengerBackend:
     def __init__(self, *adpaters):
         self.adapters = adpaters
 
-    def __call__(self, message_type, request, user, submission, related=None, **kwargs):
-        return self.send(message_type, request=request, user=user, submission=submission, related=related, **kwargs)
+    def __call__(self, *args, related=None, **kwargs):
+        return self.send(*args, related=related, **kwargs)
 
-    def send(self, message_type, request, user, submission, related, **kwargs):
+    def send(self, message_type, request, user, related, submission=None, submissions=list(), **kwargs):
         from .models import Event
-        event = Event.objects.create(type=message_type.name, by=user, submission=submission)
-        for adapter in self.adapters:
-            adapter.process(message_type, event, request=request, user=user, submission=submission, related=related, **kwargs)
+        if submission:
+            event = Event.objects.create(type=message_type.name, by=user, submission=submission)
+            for adapter in self.adapters:
+                adapter.process(message_type, event, request=request, user=user, submission=submission, related=related, **kwargs)
+
+        elif submissions:
+            events = Event.objects.bulk_create(
+                Event(type=message_type.name, by=user, submission=submission)
+                for submission in submissions
+            )
+            for adapter in self.adapters:
+                adapter.process_batch(message_type, events, request=request, user=user, submissions=submissions, related=related, **kwargs)
 
 
 adapters = [
     ActivityAdapter(),
     SlackAdapter(),
     EmailAdapter(),
+    DjangoMessagesAdapter(),
 ]
 
 
diff --git a/opentech/apply/activity/migrations/0014_add_batch_reviewer_message.py b/opentech/apply/activity/migrations/0014_add_batch_reviewer_message.py
new file mode 100644
index 0000000000000000000000000000000000000000..261fc13324a2ad246101a82ddc10b9568855dc5f
--- /dev/null
+++ b/opentech/apply/activity/migrations/0014_add_batch_reviewer_message.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.9 on 2019-01-31 23:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0013_add_new_event_type_screening'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='type',
+            field=models.CharField(choices=[('UPDATE_LEAD', 'Update Lead'), ('EDIT', 'Edit'), ('APPLICANT_EDIT', 'Applicant Edit'), ('NEW_SUBMISSION', 'New Submission'), ('SCREENING', 'Screening'), ('TRANSITION', 'Transition'), ('DETERMINATION_OUTCOME', 'Determination Outcome'), ('INVITED_TO_PROPOSAL', 'Invited To Proposal'), ('REVIEWERS_UPDATED', 'Reviewers Updated'), ('BATCH_REVIEWERS_UPDATED', 'Batch Reviewers Updated'), ('READY_FOR_REVIEW', 'Ready For Review'), ('NEW_REVIEW', 'New Review'), ('COMMENT', 'Comment'), ('PROPOSAL_SUBMITTED', 'Proposal Submitted'), ('OPENED_SEALED', 'Opened Sealed Submission')], max_length=50),
+        ),
+    ]
diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py
index cabc295831be94bc29954fe864d51fe2e33e99ae..ea3e7101324996b16082ff1d3694b1d6b388ec6c 100644
--- a/opentech/apply/activity/models.py
+++ b/opentech/apply/activity/models.py
@@ -81,7 +81,7 @@ class Activity(models.Model):
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
     submission = models.ForeignKey('funds.ApplicationSubmission', related_name='activities', on_delete=models.CASCADE)
     message = models.TextField()
-    visibility = models.CharField(choices=VISIBILITY.items(), default=PUBLIC, max_length=10)
+    visibility = models.CharField(choices=list(VISIBILITY.items()), default=PUBLIC, max_length=10)
 
     # Fields for generic relations to other objects. related_object should implement `get_absolute_url`
     content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE)
@@ -134,6 +134,19 @@ class Event(models.Model):
         return ' '.join([self.get_type_display(), 'by:', str(self.by), 'on:', self.submission.title])
 
 
+class MessagesQueryset(models.QuerySet):
+    def update_status(self, status):
+        if status:
+            return self.update(
+                status=Case(
+                    When(status='', then=Value(status)),
+                    default=Concat('status', Value('<br />' + status))
+                )
+            )
+
+    update_status.queryset_only = True
+
+
 class Message(models.Model):
     """Model to track content of messages sent from an event"""
 
@@ -144,6 +157,8 @@ class Message(models.Model):
     status = models.TextField()
     external_id = models.CharField(max_length=75, null=True, blank=True)  # Stores the id of the object from an external system
 
+    objects = MessagesQueryset.as_manager()
+
     def update_status(self, status):
         if status:
             self.status = Case(
diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py
index 46d744e035fa0af24a76b6c63996edf6e3523e6e..35aa1e64454ab3a0918c60c986792a4f4dc4dec0 100644
--- a/opentech/apply/activity/options.py
+++ b/opentech/apply/activity/options.py
@@ -11,6 +11,7 @@ class MESSAGES(Enum):
     DETERMINATION_OUTCOME = 'Determination Outcome'
     INVITED_TO_PROPOSAL = 'Invited To Proposal'
     REVIEWERS_UPDATED = 'Reviewers Updated'
+    BATCH_REVIEWERS_UPDATED = 'Batch Reviewers Updated'
     READY_FOR_REVIEW = 'Ready For Review'
     NEW_REVIEW = 'New Review'
     COMMENT = 'Comment'
diff --git a/opentech/apply/activity/tasks.py b/opentech/apply/activity/tasks.py
index 73ff71b8343d368b91776ef80fc0b59cdb6a1dd0..569c2775323dcce3630b021ab52e3d5280f62e8b 100644
--- a/opentech/apply/activity/tasks.py
+++ b/opentech/apply/activity/tasks.py
@@ -8,7 +8,7 @@ app = Celery('tasks')
 app.config_from_object(settings, namespace='CELERY', force=True)
 
 
-def send_mail(subject, message, from_address, recipients, log=None):
+def send_mail(subject, message, from_address, recipients, logs=None):
     # Convenience method to wrap the tasks and handle the callback
     send_mail_task.apply_async(
         kwargs={
@@ -17,7 +17,7 @@ def send_mail(subject, message, from_address, recipients, log=None):
             'from_email': from_address,
             'to': recipients,
         },
-        link=update_message_status.s(log.id),
+        link=update_message_status.s([log.pk for log in logs]),
     )
 
 
@@ -42,8 +42,8 @@ def send_mail_task(**kwargs):
 
 
 @app.task
-def update_message_status(response, message_id):
+def update_message_status(response, message_pks):
     from .models import Message
-    message = Message.objects.get(id=message_id)
-    message.external_id = response['id']
-    message.update_status(response['status'])
+    messages = Message.objects.filter(pk__in=message_pks)
+    messages.update(external_id=response['id'])
+    messages.update_status(response['status'])
diff --git a/opentech/apply/activity/tests/test_messaging.py b/opentech/apply/activity/tests/test_messaging.py
index d804f3c4f52f815e9b8376e62ea926cd269b3574..4b9838aaf062e2106768f668114736621d0c2d4a 100644
--- a/opentech/apply/activity/tests/test_messaging.py
+++ b/opentech/apply/activity/tests/test_messaging.py
@@ -193,7 +193,7 @@ class TestActivityAdapter(TestCase):
         user = UserFactory()
         submission = ApplicationSubmissionFactory()
 
-        self.adapter.send_message(message, user=user, submission=submission, related=None)
+        self.adapter.send_message(message, user=user, submission=submission, submissions=[], related=None)
 
         self.assertEqual(Activity.objects.count(), 1)
         activity = Activity.objects.first()
@@ -271,7 +271,7 @@ class TestActivityAdapter(TestCase):
     def test_lead_not_saved_on_activity(self):
         submission = ApplicationSubmissionFactory()
         user = UserFactory()
-        self.adapter.send_message('a message', user=user, submission=submission, related=user)
+        self.adapter.send_message('a message', user=user, submission=submission, submissions=[], related=user)
         activity = Activity.objects.first()
         self.assertEqual(activity.related_object, None)
 
@@ -279,7 +279,7 @@ class TestActivityAdapter(TestCase):
         submission = ApplicationSubmissionFactory()
         user = UserFactory()
         review = ReviewFactory(submission=submission)
-        self.adapter.send_message('a message', user=user, submission=submission, related=review)
+        self.adapter.send_message('a message', user=user, submission=submission, submissions=[], related=review)
         activity = Activity.objects.first()
         self.assertEqual(activity.related_object, review)
 
diff --git a/opentech/apply/activity/tests/test_tasks.py b/opentech/apply/activity/tests/test_tasks.py
index b6aede89d7e74c4ab4886d7b956c5196e26e62b6..f468f396c0e0e2f0415359a9a912beecc5097b31 100644
--- a/opentech/apply/activity/tests/test_tasks.py
+++ b/opentech/apply/activity/tests/test_tasks.py
@@ -16,5 +16,5 @@ class TestSendEmail(TestCase):
             'from_email': 'from_email',
             'to': 'to',
         }
-        send_mail(*kwargs, log=MessageFactory())
+        send_mail(*kwargs, logs=[MessageFactory()])
         email_mock.assert_called_once_with(**kwargs)
diff --git a/opentech/apply/dashboard/templates/dashboard/dashboard.html b/opentech/apply/dashboard/templates/dashboard/dashboard.html
index de14c3caef48d98e96c5c102aa8cdbd8362a1118..d0a54dca98910e400df9fb6b68446f6aa750577f 100644
--- a/opentech/apply/dashboard/templates/dashboard/dashboard.html
+++ b/opentech/apply/dashboard/templates/dashboard/dashboard.html
@@ -18,6 +18,11 @@
 </div>
 <div class="wrapper wrapper--large wrapper--inner-space-medium">
         <div class="wrapper wrapper--large wrapper--inner-space-medium">
+
+            {% if closed_rounds or open_rounds %}
+                {% include "funds/includes/round-block.html" with closed_rounds=closed_rounds open_rounds=open_rounds title=rounds_title %}
+            {% endif %}
+
             <h3>Applications to review</h3>
             {% if in_review.data %}
                 {% render_table in_review %}
@@ -30,4 +35,5 @@
 
 {% block extra_js %}
     <script src="{% static 'js/apply/submission-tooltips.js' %}"></script>
+    <script src="{% static 'js/apply/tabs.js' %}"></script>
 {% endblock %}
diff --git a/opentech/apply/dashboard/views.py b/opentech/apply/dashboard/views.py
index b8703e77d2457cd107d877c81bd101f44ad90d3f..22cba795cec3e3440864a9f85a9cc269ba1656ad 100644
--- a/opentech/apply/dashboard/views.py
+++ b/opentech/apply/dashboard/views.py
@@ -3,7 +3,7 @@ from django.views.generic import TemplateView
 from django_tables2 import RequestConfig
 from django_tables2.views import SingleTableView
 
-from opentech.apply.funds.models import ApplicationSubmission
+from opentech.apply.funds.models import ApplicationSubmission, RoundsAndLabs
 from opentech.apply.funds.tables import SubmissionsTable, AdminSubmissionsTable
 from opentech.apply.utils.views import ViewDispatcher
 
@@ -15,9 +15,21 @@ class AdminDashboardView(TemplateView):
 
         in_review = SubmissionsTable(qs.in_review_for(request.user), prefix='in-review-')
         RequestConfig(request, paginate={'per_page': 10}).configure(in_review)
+        base_query = RoundsAndLabs.objects.with_progress().active().order_by('-end_date')
+        base_query = base_query.by_lead(request.user)
+        open_rounds = base_query.open()[:6]
+        open_query = '?round_state=open'
+        closed_rounds = base_query.closed()[:6]
+        closed_query = '?round_state=closed'
+        rounds_title = 'Your rounds and labs'
 
         return render(request, 'dashboard/dashboard.html', {
             'in_review': in_review,
+            'open_rounds': open_rounds,
+            'open_query': open_query,
+            'closed_rounds': closed_rounds,
+            'closed_query': closed_query,
+            'rounds_title': rounds_title,
         })
 
 
diff --git a/opentech/apply/funds/api_views.py b/opentech/apply/funds/api_views.py
index 4e294f673bb74547106e2521eff91bb93be5fc8a..ece53fb38815dfe6cac8a8578e7adc86c99cba00 100644
--- a/opentech/apply/funds/api_views.py
+++ b/opentech/apply/funds/api_views.py
@@ -1,14 +1,26 @@
+from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
 from django.db.models import Q
-from rest_framework import generics
-from rest_framework import permissions
+from rest_framework import generics, permissions, status
+from rest_framework.response import Response
+from rest_framework.exceptions import PermissionDenied, ValidationError
 from django_filters import rest_framework as filters
 
-from wagtail.core.models import Page
-
 from opentech.api.pagination import StandardResultsSetPagination
-from .models import ApplicationSubmission
-from .serializers import SubmissionListSerializer, SubmissionDetailSerializer
+from opentech.apply.activity.models import Activity, COMMENT
+from opentech.apply.activity.messaging import messenger, MESSAGES
+
+from .models import ApplicationSubmission, RoundsAndLabs
+from .serializers import (
+    CommentSerializer,
+    CommentCreateSerializer,
+    RoundLabDetailSerializer,
+    RoundLabSerializer,
+    SubmissionActionSerializer,
+    SubmissionListSerializer,
+    SubmissionDetailSerializer,
+)
 from .permissions import IsApplyStaffUser
+from .workflow import PHASES
 
 
 class RoundLabFilter(filters.ModelChoiceFilter):
@@ -20,12 +32,22 @@ class RoundLabFilter(filters.ModelChoiceFilter):
 
 
 class SubmissionsFilter(filters.FilterSet):
-    # TODO replace with better call to Round and Lab base class
-    round = RoundLabFilter(queryset=Page.objects.all())
+    round = RoundLabFilter(queryset=RoundsAndLabs.objects.all())
+    status = filters.MultipleChoiceFilter(choices=PHASES)
+    active = filters.BooleanFilter(method='filter_active')
 
     class Meta:
         model = ApplicationSubmission
-        fields = ('status', 'round')
+        fields = ('status', 'round', 'active')
+
+    def filter_active(self, value):
+        if value is None:
+            return qs
+
+        if value:
+            return qs.active()
+        else:
+            return qs.inactive()
 
 
 class SubmissionList(generics.ListAPIView):
@@ -45,3 +67,95 @@ class SubmissionDetail(generics.RetrieveAPIView):
     permission_classes = (
         permissions.IsAuthenticated, IsApplyStaffUser,
     )
+
+
+class SubmissionAction(generics.RetrieveAPIView):
+    queryset = ApplicationSubmission.objects.all()
+    serializer_class = SubmissionActionSerializer
+    permission_classes = (
+        permissions.IsAuthenticated, IsApplyStaffUser,
+    )
+
+    def post(self, request, *args, **kwargs):
+        action = request.data.get('action')
+        if not action:
+            raise ValidationError('Action must be provided.')
+        obj = self.get_object()
+        try:
+            obj.perform_transition(action, self.request.user, request=self.request)
+        except DjangoPermissionDenied as e:
+            raise PermissionDenied(str(e))
+        return Response(status=status.HTTP_200_OK)
+
+
+class RoundLabDetail(generics.RetrieveAPIView):
+    queryset = RoundsAndLabs.objects.all()
+    serializer_class = RoundLabDetailSerializer
+    permission_classes = (
+        permissions.IsAuthenticated, IsApplyStaffUser,
+    )
+
+    def get_object(self):
+        return super().get_object().specific
+
+
+class RoundLabList(generics.ListAPIView):
+    queryset = RoundsAndLabs.objects.specific()
+    serializer_class = RoundLabSerializer
+    permission_classes = (
+        permissions.IsAuthenticated, IsApplyStaffUser,
+    )
+    pagination_class = StandardResultsSetPagination
+
+
+class CommentFilter(filters.FilterSet):
+    since = filters.DateTimeFilter(field_name="timestamp", lookup_expr='gte')
+    before = filters.DateTimeFilter(field_name="timestamp", lookup_expr='lte')
+
+    class Meta:
+        model = Activity
+        fields = ['submission', 'visibility', 'since', 'before']
+
+
+class CommentList(generics.ListAPIView):
+    queryset = Activity.comments.all()
+    serializer_class = CommentSerializer
+    permission_classes = (
+        permissions.IsAuthenticated, IsApplyStaffUser,
+    )
+    filter_backends = (filters.DjangoFilterBackend,)
+    filter_class = CommentFilter
+    pagination_class = StandardResultsSetPagination
+
+    def get_queryset(self):
+        return super().get_queryset().visible_to(self.request.user)
+
+
+class CommentListCreate(generics.ListCreateAPIView):
+    queryset = Activity.comments.all()
+    serializer_class = CommentCreateSerializer
+    permission_classes = (
+        permissions.IsAuthenticated, IsApplyStaffUser,
+    )
+    filter_backends = (filters.DjangoFilterBackend,)
+    filter_fields = ('visibility',)
+    pagination_class = StandardResultsSetPagination
+
+    def get_queryset(self):
+        return super().get_queryset().filter(
+            submission=self.kwargs['pk']
+        ).visible_to(self.request.user)
+
+    def perform_create(self, serializer):
+        obj = serializer.save(
+            type=COMMENT,
+            user=self.request.user,
+            submission_id=self.kwargs['pk']
+        )
+        messenger(
+            MESSAGES.COMMENT,
+            request=self.request,
+            user=self.request.user,
+            submission=obj.submission,
+            related=obj,
+        )
diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py
index 16d36fd6bb706bd270fbe35d67fd151812b2dff2..a75cda24e32c1b113f30aa31bb7bc1643640b6c4 100644
--- a/opentech/apply/funds/forms.py
+++ b/opentech/apply/funds/forms.py
@@ -66,29 +66,29 @@ class UpdateReviewersForm(forms.ModelForm):
         model = ApplicationSubmission
         fields: list = []
 
-    def can_alter_reviewers(self, user):
-        return self.instance.stage.has_external_review and user == self.instance.lead
-
     def __init__(self, *args, **kwargs):
         self.user = kwargs.pop('user')
         super().__init__(*args, **kwargs)
         reviewers = self.instance.reviewers.all()
         self.submitted_reviewers = User.objects.filter(id__in=self.instance.reviews.values('author'))
 
-        staff_field = self.fields['staff_reviewers']
-        staff_field.queryset = staff_field.queryset.exclude(id__in=self.submitted_reviewers)
-        staff_field.initial = reviewers
-
-        if self.can_alter_reviewers(self.user):
-            review_field = self.fields['reviewer_reviewers']
-            review_field.queryset = review_field.queryset.exclude(id__in=self.submitted_reviewers)
-            review_field.initial = reviewers
+        self.prepare_field('staff_reviewers', reviewers, self.submitted_reviewers)
+        if self.can_alter_external_reviewers(self.instance, self.user):
+            self.prepare_field('reviewer_reviewers', reviewers, self.submitted_reviewers)
         else:
             self.fields.pop('reviewer_reviewers')
 
+    def prepare_field(self, field_name, initial, excluded):
+        field = self.fields[field_name]
+        field.queryset = field.queryset.exclude(id__in=excluded)
+        field.initial = initial
+
+    def can_alter_external_reviewers(self, instance, user):
+        return instance.stage.has_external_review and (user == instance.lead or user.is_superuser)
+
     def save(self, *args, **kwargs):
         instance = super().save(*args, **kwargs)
-        if self.can_alter_reviewers(self.user):
+        if self.can_alter_external_reviewers(self.instance, self.user):
             reviewers = self.cleaned_data.get('reviewer_reviewers')
         else:
             reviewers = instance.reviewers_not_reviewed
@@ -99,3 +99,15 @@ class UpdateReviewersForm(forms.ModelForm):
             self.submitted_reviewers
         )
         return instance
+
+
+class BatchUpdateReviewersForm(forms.Form):
+    staff_reviewers = forms.ModelMultipleChoiceField(
+        queryset=User.objects.staff(),
+        widget=Select2MultiCheckboxesWidget(attrs={'data-placeholder': 'Staff'}),
+    )
+    submission_ids = forms.CharField(widget=forms.HiddenInput())
+
+    def clean_submission_ids(self):
+        value = self.cleaned_data['submission_ids']
+        return [int(submission) for submission in value.split(',')]
diff --git a/opentech/apply/funds/models/applications.py b/opentech/apply/funds/models/applications.py
index 30097f1d3833a2c6f29beef9d29353bddf1c3c80..f2a791dee8cbebd6dd92cb2ba855d519d1728147 100644
--- a/opentech/apply/funds/models/applications.py
+++ b/opentech/apply/funds/models/applications.py
@@ -345,6 +345,9 @@ class RoundsAndLabsQueryset(PageQuerySet):
     def closed(self):
         return self.filter(end_date__lt=date.today())
 
+    def by_lead(self, user):
+        return self.filter(lead_pk=user.pk)
+
 
 class RoundsAndLabsProgressQueryset(RoundsAndLabsQueryset):
     def active(self):
@@ -367,6 +370,10 @@ class RoundsAndLabsManager(PageManager):
             end_date=F('roundbase__end_date'),
             parent_path=Left(F('path'), Length('path') - ApplicationBase.steplen, output_field=CharField()),
             fund=Subquery(funds.values('title')[:1]),
+            lead_pk=Coalesce(
+                F('roundbase__lead__pk'),
+                F('labbase__lead__pk'),
+            ),
         )
 
     def with_progress(self):
@@ -406,6 +413,9 @@ class RoundsAndLabsManager(PageManager):
     def new(self):
         return self.get_queryset().new()
 
+    def by_lead(self, user):
+        return self.get_queryset().by_lead(user)
+
 
 class RoundsAndLabs(Page):
     """
diff --git a/opentech/apply/funds/serializers.py b/opentech/apply/funds/serializers.py
index 3203a7ecca0cefc53594d594c9b39964655dbf3e..bd0a79964b08ab07e6e45627f33316cce73f697b 100644
--- a/opentech/apply/funds/serializers.py
+++ b/opentech/apply/funds/serializers.py
@@ -1,24 +1,56 @@
+import mistune
 from rest_framework import serializers
+from django_bleach.templatetags.bleach_tags import bleach_value
 
-from .models import ApplicationSubmission
+from opentech.apply.activity.models import Activity
+from .models import ApplicationSubmission, RoundsAndLabs
+
+markdown = mistune.Markdown()
+
+
+class ActionSerializer(serializers.Field):
+    def to_representation(self, instance):
+        actions = instance.get_actions_for_user(self.context['request'].user)
+        return {
+            transition: action
+            for transition, action in actions
+        }
+
+
+class ReviewSummarySerializer(serializers.Field):
+    def to_representation(self, instance):
+        return {
+            'count': instance.reviews.count(),
+            'score': instance.reviews.score(),
+            'recommendation': instance.reviews.recommendation(),
+        }
 
 
 class SubmissionListSerializer(serializers.ModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='funds:submissions-api:detail')
+    url = serializers.HyperlinkedIdentityField(view_name='funds:api:submissions:detail')
+    round = serializers.SerializerMethodField()
 
     class Meta:
         model = ApplicationSubmission
-        fields = ('id', 'title', 'status', 'url')
+        fields = ('id', 'title', 'status', 'url', 'round')
+
+    def get_round(self, obj):
+        """
+        This gets round or lab ID.
+        """
+        return obj.round_id or obj.page_id
 
 
 class SubmissionDetailSerializer(serializers.ModelSerializer):
     questions = serializers.SerializerMethodField()
     meta_questions = serializers.SerializerMethodField()
     stage = serializers.CharField(source='stage.name')
+    actions = ActionSerializer(source='*')
+    review = ReviewSummarySerializer(source='*')
 
     class Meta:
         model = ApplicationSubmission
-        fields = ('id', 'title', 'stage', 'meta_questions', 'questions')
+        fields = ('id', 'title', 'stage', 'status', 'meta_questions', 'questions', 'actions', 'review')
 
     def serialize_questions(self, obj, fields):
         for field_id in fields:
@@ -45,3 +77,54 @@ class SubmissionDetailSerializer(serializers.ModelSerializer):
 
     def get_questions(self, obj):
         return self.serialize_questions(obj, obj.normal_blocks)
+
+
+class SubmissionActionSerializer(serializers.ModelSerializer):
+    actions = ActionSerializer(source='*')
+
+    class Meta:
+        model = ApplicationSubmission
+        fields = ('id', 'actions',)
+
+
+class RoundLabDetailSerializer(serializers.ModelSerializer):
+    workflow = serializers.SerializerMethodField()
+
+    class Meta:
+        model = RoundsAndLabs
+        fields = ('id', 'title', 'workflow')
+
+    def get_workflow(self, obj):
+        return [
+            {
+                'value': phase.name,
+                'display': phase.display_name
+            }
+            for phase in obj.workflow.values()
+        ]
+
+
+class RoundLabSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = RoundsAndLabs
+        fields = ('id', 'title')
+
+
+class CommentSerializer(serializers.ModelSerializer):
+    user = serializers.StringRelatedField()
+    message = serializers.SerializerMethodField()
+
+    class Meta:
+        model = Activity
+        fields = ('id', 'timestamp', 'user', 'submission', 'message', 'visibility')
+
+    def get_message(self, obj):
+        return bleach_value(markdown(obj.message))
+
+
+class CommentCreateSerializer(serializers.ModelSerializer):
+    user = serializers.StringRelatedField()
+
+    class Meta:
+        model = Activity
+        fields = ('id', 'timestamp', 'user', 'message', 'visibility')
diff --git a/opentech/apply/funds/tables.py b/opentech/apply/funds/tables.py
index 4456510530e47f6676caf9973e04d0eed9e8d320..b49943f90a7c98996142e0df56f073873d95516e 100644
--- a/opentech/apply/funds/tables.py
+++ b/opentech/apply/funds/tables.py
@@ -65,8 +65,25 @@ class SubmissionsTable(tables.Table):
         return qs, True
 
 
-class AdminSubmissionsTable(SubmissionsTable):
-    """Adds admin only columns to the submissions table"""
+class LabeledCheckboxColumn(tables.CheckBoxColumn):
+    def wrap_with_label(self, checkbox, for_value):
+        return format_html(
+            '<label for="{}">{}</label>',
+            for_value,
+            checkbox,
+        )
+
+    @property
+    def header(self):
+        checkbox = super().header
+        return self.wrap_with_label(checkbox, 'selectall')
+
+    def render(self, value, record, bound_column):
+        checkbox = super().render(value=value, record=record, bound_column=bound_column)
+        return self.wrap_with_label(checkbox, value)
+
+
+class BaseAdminSubmissionsTable(SubmissionsTable):
     lead = tables.Column(order_by=('lead.full_name',))
     reviews_stats = tables.TemplateColumn(template_name='funds/tables/column_reviews.html', verbose_name=mark_safe("Reviews\n<span>Assgn.\tComp.</span>"), orderable=False)
     screening_status = tables.Column(verbose_name="Screening")
@@ -79,8 +96,17 @@ class AdminSubmissionsTable(SubmissionsTable):
         return format_html('<span>{}</span>', value)
 
 
-class SummarySubmissionsTable(AdminSubmissionsTable):
-    class Meta(AdminSubmissionsTable.Meta):
+class AdminSubmissionsTable(BaseAdminSubmissionsTable):
+    """Adds admin only columns to the submissions table"""
+    selected = LabeledCheckboxColumn(accessor=A('pk'), attrs={'input': {'class': 'js-batch-select'}, 'th__input': {'class': 'js-batch-select-all'}})
+
+    class Meta(BaseAdminSubmissionsTable.Meta):
+        fields = ('selected', *BaseAdminSubmissionsTable.Meta.fields)
+        sequence = fields
+
+
+class SummarySubmissionsTable(BaseAdminSubmissionsTable):
+    class Meta(BaseAdminSubmissionsTable.Meta):
         orderable = False
 
 
@@ -125,9 +151,16 @@ class Select2ModelMultipleChoiceFilter(Select2MultipleChoiceFilter, filters.Mode
 
 
 class StatusMultipleChoiceFilter(Select2MultipleChoiceFilter):
-    def __init__(self, *args, **kwargs):
-        choices = [(slugify(status), status) for status in STATUSES]
-        self.status_map = {slugify(name): status for name, status in STATUSES.items()}
+    def __init__(self, limit_to, *args, **kwargs):
+        choices = [
+            (slugify(name), name)
+            for name, statuses in STATUSES.items()
+            if not limit_to or self.has_any(statuses, limit_to)
+        ]
+        self.status_map = {
+            slugify(name): status
+            for name, status in STATUSES.items()
+        }
         super().__init__(
             *args,
             name='status',
@@ -136,6 +169,9 @@ class StatusMultipleChoiceFilter(Select2MultipleChoiceFilter):
             **kwargs,
         )
 
+    def has_any(self, first, second):
+        return any(item in second for item in first)
+
     def get_filter_predicate(self, v):
         return {f'{ self.field_name }__in': self.status_map[v]}
 
@@ -143,7 +179,6 @@ class StatusMultipleChoiceFilter(Select2MultipleChoiceFilter):
 class SubmissionFilter(filters.FilterSet):
     round = Select2ModelMultipleChoiceFilter(queryset=get_used_rounds, label='Rounds')
     fund = Select2ModelMultipleChoiceFilter(name='page', queryset=get_used_funds, label='Funds')
-    status = StatusMultipleChoiceFilter()
     lead = Select2ModelMultipleChoiceFilter(queryset=get_round_leads, label='Leads')
     reviewers = Select2ModelMultipleChoiceFilter(queryset=get_reviewers, label='Reviewers')
     screening_status = Select2ModelMultipleChoiceFilter(queryset=get_screening_statuses, label='Screening')
@@ -152,9 +187,11 @@ class SubmissionFilter(filters.FilterSet):
         model = ApplicationSubmission
         fields = ('fund', 'round', 'status')
 
-    def __init__(self, *args, exclude=list(), **kwargs):
+    def __init__(self, *args, exclude=list(), limit_statuses=None, **kwargs):
         super().__init__(*args, **kwargs)
 
+        self.filters['status'] = StatusMultipleChoiceFilter(limit_to=limit_statuses)
+
         self.filters = {
             field: filter
             for field, filter in self.filters.items()
diff --git a/opentech/apply/funds/templates/funds/base_submissions_table.html b/opentech/apply/funds/templates/funds/base_submissions_table.html
index d9214f80da196c86ca5211367455a9e292fd1bac..322a4dca51c1d0a1b9c8aaf5e03531a308549429 100644
--- a/opentech/apply/funds/templates/funds/base_submissions_table.html
+++ b/opentech/apply/funds/templates/funds/base_submissions_table.html
@@ -3,6 +3,7 @@
 {% load render_table from django_tables2 %}
 
 {% block extra_css %}
+<link rel="stylesheet" href="{% static 'css/apply/fancybox.css' %}">
 {{ filter.form.media.css }}
 {% endblock %}
 
@@ -16,10 +17,13 @@
 
 {% block extra_js %}
     {{ filter.form.media.js }}
+    <script src="//cdnjs.cloudflare.com/ajax/libs/fancybox/3.4.1/jquery.fancybox.min.js"></script>
+    <script src="{% static 'js/apply/fancybox-global.js' %}"></script>
     <script src="{% static 'js/apply/all-submissions-table.js' %}"></script>
     <script src="https://cdn.jsdelivr.net/npm/symbol-es6@0.1.2/symbol-es6.min.js"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/url-search-params/1.1.0/url-search-params.js"></script>
     <script src="{% static 'js/apply/submission-filters.js' %}"></script>
     <script src="{% static 'js/apply/submission-tooltips.js' %}"></script>
     <script src="{% static 'js/apply/tabs.js' %}"></script>
+    <script src="{% static 'js/apply/batch-actions.js' %}"></script>
 {% endblock %}
diff --git a/opentech/apply/funds/templates/funds/includes/batch_update_reviewer_form.html b/opentech/apply/funds/templates/funds/includes/batch_update_reviewer_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..d74095ba0a497d3c206669684cd33608c9003813
--- /dev/null
+++ b/opentech/apply/funds/templates/funds/includes/batch_update_reviewer_form.html
@@ -0,0 +1,9 @@
+<div class="modal modal--secondary" id="batch-update-reviewers">
+    <h4 class="modal__header-bar">Add Reviewers</h4>
+    <div class="modal__list-item modal__list-item--meta" aria-live="polite">
+        <span class="js-batch-title-count"></span>
+        <a href="#" class="modal__hide-link js-toggle-batch-list">Show</a>
+    </div>
+    <div class="modal__list js-batch-titles is-closed" aria-live="polite"></div>
+    {% include 'funds/includes/delegated_form_base.html' with form=batch_reviewer_form value='Update'%}
+</div>
diff --git a/opentech/apply/funds/templates/funds/includes/round-block.html b/opentech/apply/funds/templates/funds/includes/round-block.html
index 8fdca206107d2438cef6a3115d49dfe101260797..98efb329f1b9c5f5ce983a8bbd2bfc41fd20057f 100644
--- a/opentech/apply/funds/templates/funds/includes/round-block.html
+++ b/opentech/apply/funds/templates/funds/includes/round-block.html
@@ -1,6 +1,6 @@
 <div class="wrapper wrapper--bottom-space">
     <section class="section section--with-options">
-        <h4 class="heading heading--normal heading--no-margin">All Rounds and Labs</h4>
+        <h4 class="heading heading--normal heading--no-margin">{{ title }}</h4>
         <div class="js-tabs">
             <a class="tab__item tab__item--alt" href="#closed-rounds" data-tab="tab-1">Closed</a>
             <a class="tab__item tab__item--alt" href="#open-rounds" data-tab="tab-2">Open</a>
diff --git a/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html b/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html
index e4cff0f2d2f2147b94009bcebc63f27aefbe9f7f..7edcad1832cbf156157176e6ead6b98116ed6392 100644
--- a/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html
+++ b/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html
@@ -1,14 +1,30 @@
 <div class="wrapper wrapper--table-actions">
-    <button class="button button--filters button--contains-icons js-toggle-filters">Filters</button>
+    <div class="actions-bar">
+        {# Left #}
+        <div class="actions-bar__inner actions-bar__inner--left">
+            <p class="actions-bar__total"><span class="js-total-actions">0</span> Selected</p>
+            <form action="" class="js-batch-update-status">
+                <button class="button button--action button--change-status" type="submit">Change status</button>
+            </form>
+
+            <button data-fancybox data-src="#batch-update-reviewers" class="button button--action button--reviewers js-batch-update-reviewers" type="submit">Reviewers</button>
+        </div>
+
+        {# Right #}
+        <div class="actions-bar__inner actions-bar__inner--right">
+            <button class="button button--filters button--contains-icons button--action js-toggle-filters">Filters</button>
+
+            {% if use_search|default:False %}
+            <form method="get" role="search" class="form form--search-desktop js-search-form">
+                <button class="button button--search" type="submit" aria-label="Search">
+                    <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg>
+                </button>
+                <input class="input input--search input--secondary js-search-input" type="text" placeholder="Search submissions" name="query"{% if search_term %} value="{{ search_term }}"{% endif %} aria-label="Search input">
+            </form>
+            {% endif %}
+        </div>
+    </div>
 
-    {% if use_search|default:False %}
-    <form method="get" role="search" class="form form--search js-search-form">
-        <button class="button button--search" type="submit" aria-label="Search">
-            <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg>
-        </button>
-        <input class="input input--search input--secondary js-search-input" type="text" placeholder="Search submissions" name="query"{% if search_term %} value="{{ search_term }}"{% endif %} aria-label="Search input">
-    </form>
-    {% endif %}
 </div>
 
 <div class="filters">
@@ -27,3 +43,5 @@
         </ul>
     </form>
 </div>
+
+{% include "funds/includes/batch_update_reviewer_form.html" %}
diff --git a/opentech/apply/funds/templates/funds/submissions.html b/opentech/apply/funds/templates/funds/submissions.html
index 353450fa78d785a9339a12023d7eb3f69c5f3736..ca70880476eaac1808566d08c7b7a7c21e54ef21 100644
--- a/opentech/apply/funds/templates/funds/submissions.html
+++ b/opentech/apply/funds/templates/funds/submissions.html
@@ -14,6 +14,11 @@
 </div>
 
 <div class="wrapper wrapper--large wrapper--inner-space-medium">
+
+    {% if closed_rounds or open_rounds %}
+        {% include "funds/includes/round-block.html" with closed_rounds=closed_rounds open_rounds=open_rounds title=rounds_title %}
+    {% endif %}
+
     {% block table %}
         {{ block.super }}
     {% endblock %}
diff --git a/opentech/apply/funds/templates/funds/submissions_by_round.html b/opentech/apply/funds/templates/funds/submissions_by_round.html
index 2aee68595093c4fb1ecc7df571dbef701a500b53..f09d574a2b7b0a371a9f59e3779181165e732f24 100644
--- a/opentech/apply/funds/templates/funds/submissions_by_round.html
+++ b/opentech/apply/funds/templates/funds/submissions_by_round.html
@@ -1,4 +1,5 @@
 {% extends "funds/base_submissions_table.html" %}
+{% load static %}
 {% load render_bundle from webpack_loader %}
 
 {% block title %}{{ object }}{% endblock %}
@@ -21,6 +22,16 @@
             {% endblock %}
         </div>
     </div>
-{% render_bundle 'main' %}
+{% render_bundle 'submissionsByRound' %}
 
+<a href="#" class="js-open-feed link link--open-feed">
+    <h4 class="heading heading--no-margin heading--activity-feed">Activity Feed</h4>
+</a>
+{% include "funds/includes/activity-feed.html" %}
+
+{% endblock %}
+
+{% block extra_js %}
+    {{ block.super }}
+    <script src="{% static 'js/apply/activity-feed.js' %}"></script>
 {% endblock %}
diff --git a/opentech/apply/funds/templates/funds/submissions_by_status.html b/opentech/apply/funds/templates/funds/submissions_by_status.html
new file mode 100644
index 0000000000000000000000000000000000000000..0cbd08c1b79098e8eadf0a415531e4fb00609490
--- /dev/null
+++ b/opentech/apply/funds/templates/funds/submissions_by_status.html
@@ -0,0 +1,25 @@
+{% extends "funds/base_submissions_table.html" %}
+{% load render_bundle from webpack_loader %}
+
+{% block title %}{{ status }}{% endblock %}
+
+{% block content %}
+    <div class="admin-bar">
+        <div class="admin-bar__inner admin-bar__inner--with-button">
+            <div>
+                <h1 class="gamma heading heading--no-margin heading--bold">{{ status }}</h1>
+            </div>
+            <div id="submissions-by-status-app-react-switcher"></div>
+        </div>
+    </div>
+
+    <div id="submissions-by-status-react-app" data-statuses="{{ statuses|join:',' }}">
+        <div class="wrapper wrapper--large wrapper--inner-space-medium">
+            {% block table %}
+                {{ block.super }}
+            {% endblock %}
+        </div>
+    </div>
+{% render_bundle 'submissionsByStatus' %}
+
+{% endblock %}
diff --git a/opentech/apply/funds/templates/funds/submissions_overview.html b/opentech/apply/funds/templates/funds/submissions_overview.html
index 3963db804992e273f05a4202a2846aede8554c0a..120cb947c51be68cf0517fd02263108195169e1b 100644
--- a/opentech/apply/funds/templates/funds/submissions_overview.html
+++ b/opentech/apply/funds/templates/funds/submissions_overview.html
@@ -17,7 +17,7 @@
 <div class="wrapper wrapper--large wrapper--inner-space-medium">
 
     {% if closed_rounds or open_rounds %}
-        {% include "funds/includes/round-block.html" with closed_rounds=closed_rounds open_rounds=open_rounds %}
+        {% include "funds/includes/round-block.html" with closed_rounds=closed_rounds open_rounds=open_rounds title=rounds_title%}
     {% endif %}
 
     {% block table %}
diff --git a/opentech/apply/funds/templates/funds/tables/table.html b/opentech/apply/funds/templates/funds/tables/table.html
index cb3095fc7f961f67e78ed0eadc73308fbf4e2f24..66599d9371b55d206e44642fe6df8ed0f9000a23 100644
--- a/opentech/apply/funds/templates/funds/tables/table.html
+++ b/opentech/apply/funds/templates/funds/tables/table.html
@@ -5,7 +5,9 @@
     <tr {{ row.attrs.as_html }}>
         {% for column, cell in row.items %}
             <td {{ column.attrs.td.as_html }}>
-                <span class="mobile-label {{ column.attrs.td.class }}">{{ column.header }}: </span>
+                {% if column.name != "selected" %}
+                    <span class="mobile-label {{ column.attrs.td.class }}">{{ column.header }}: </span>
+                {% endif %}
                 {% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
             </td>
         {% endfor %}
@@ -54,6 +56,7 @@
                     <tr class="submission-meta__row">
                     {% for column in row.table.columns %}
                         {% if forloop.first %}
+                        {% elif forloop.counter == 2 %}
                             <th>Linked {{ row.record.previous.stage }}</th>
                         {% else %}
                             <th class="th th--{{ column.header|lower }}">{{ column.header }}</th>
@@ -63,7 +66,13 @@
 
                     {# mutate the row to render the data for the child row #}
                     {% with row=row|row_from_record:row.record.previous %}
-                        {{ block.super }}
+                        <tr {{ row.attrs.as_html }}>
+                            {% for column, cell in row.items %}
+                                {% if column.name != "selected" %}
+                                    <td {{ column.attrs.td.as_html }}>{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}</td>
+                                    {% endif %}
+                            {% endfor %}
+                        </tr>
                     {% endwith %}
                 </table>
             </td>
diff --git a/opentech/apply/funds/tests/models/test_roundsandlabs.py b/opentech/apply/funds/tests/models/test_roundsandlabs.py
index 5eb8709d7b39e407a568deb51dd14ed9ff0ee27c..a41aa04568fc2445992316eaaa4772f3220df3b8 100644
--- a/opentech/apply/funds/tests/models/test_roundsandlabs.py
+++ b/opentech/apply/funds/tests/models/test_roundsandlabs.py
@@ -67,6 +67,18 @@ class BaseRoundsAndLabTestCase:
         self.assertEqual(fetched_obj, obj)
         self.assertFalse(base_qs.active().exists())
 
+    def test_by_lead(self):
+        obj = self.base_factory()
+        # Create an additional round which will create a new staff lead
+        round_other_lead = RoundFactory()
+        qs_all = RoundsAndLabs.objects.with_progress()
+        qs_by_lead = qs_all.by_lead(obj.lead)
+        fetched_obj = qs_by_lead.first()
+        self.assertEqual(qs_all.count(), 2)
+        self.assertEqual(qs_by_lead.count(), 1)
+        self.assertEqual(fetched_obj.lead, obj.lead.full_name)
+        self.assertNotEqual(round_other_lead.title, fetched_obj.title)
+
 
 class TestForLab(BaseRoundsAndLabTestCase, TestCase):
     base_factory = LabFactory
diff --git a/opentech/apply/funds/tests/views/__init__.py b/opentech/apply/funds/tests/views/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/funds/urls.py b/opentech/apply/funds/urls.py
index 7897ce287fc42c0919b4bbd171a1f4c9096a8ca3..4a8712ba959b95e2e59ad663ba018d20f8229d44 100644
--- a/opentech/apply/funds/urls.py
+++ b/opentech/apply/funds/urls.py
@@ -5,13 +5,22 @@ from .views import (
     RevisionListView,
     RoundListView,
     SubmissionsByRound,
+    SubmissionsByStatus,
     SubmissionDetailView,
     SubmissionEditView,
     SubmissionListView,
     SubmissionOverviewView,
     SubmissionSealedView,
 )
-from .api_views import SubmissionList, SubmissionDetail
+from .api_views import (
+    CommentList,
+    CommentListCreate,
+    RoundLabDetail,
+    RoundLabList,
+    SubmissionAction,
+    SubmissionList,
+    SubmissionDetail,
+)
 
 
 revision_urls = ([
@@ -35,14 +44,24 @@ submission_urls = ([
         path('', include('opentech.apply.determinations.urls', namespace="determinations")),
         path('revisions/', include(revision_urls, namespace="revisions")),
     ])),
+    path('<slug:status>/', SubmissionsByStatus.as_view(), name='status'),
 ], 'submissions')
 
-
-submission_api_urls = ([
-    path('', SubmissionList.as_view(), name='list'),
-    path('<int:pk>/', SubmissionDetail.as_view(), name='detail'),
-], 'submissions-api')
-
+api_urls = ([
+    path('submissions/', include(([
+        path('', SubmissionList.as_view(), name='list'),
+        path('<int:pk>/', SubmissionDetail.as_view(), name='detail'),
+        path('<int:pk>/actions/', SubmissionAction.as_view(), name='actions'),
+        path('<int:pk>/comments/', CommentListCreate.as_view(), name='comments'),
+    ], 'submissions'))),
+    path('rounds/', include(([
+        path('', RoundLabList.as_view(), name='list'),
+        path('<int:pk>/', RoundLabDetail.as_view(), name='detail'),
+    ], 'rounds'))),
+    path('comments/', include(([
+        path('', CommentList.as_view(), name='list'),
+    ], 'comments')))
+], 'api')
 
 rounds_urls = ([
     path('', RoundListView.as_view(), name="list"),
@@ -53,5 +72,5 @@ rounds_urls = ([
 urlpatterns = [
     path('submissions/', include(submission_urls)),
     path('rounds/', include(rounds_urls)),
-    path('api/submissions/', include(submission_api_urls)),
+    path('api/', include(api_urls)),
 ]
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index 9d0376b7d31452fcb5c30ce2792e830e7cfa4889..67d504a0ff2eb369940586ed130e7571d769bc8f 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -10,7 +10,7 @@ from django.urls import reverse_lazy
 from django.utils.decorators import method_decorator
 from django.utils.text import mark_safe
 from django.utils.translation import ugettext_lazy as _
-from django.views.generic import DetailView, ListView, UpdateView
+from django.views.generic import DetailView, FormView, ListView, UpdateView
 
 from django_filters.views import FilterView
 from django_tables2.views import SingleTableMixin
@@ -27,10 +27,17 @@ from opentech.apply.activity.messaging import messenger, MESSAGES
 from opentech.apply.determinations.views import DeterminationCreateOrUpdateView
 from opentech.apply.review.views import ReviewContextMixin
 from opentech.apply.users.decorators import staff_required
-from opentech.apply.utils.views import DelegateableView, ViewDispatcher
+from opentech.apply.users.models import User
+from opentech.apply.utils.views import DelegateableListView, DelegateableView, ViewDispatcher
 
 from .differ import compare
-from .forms import ProgressSubmissionForm, ScreeningSubmissionForm, UpdateReviewersForm, UpdateSubmissionLeadForm
+from .forms import (
+    BatchUpdateReviewersForm,
+    ProgressSubmissionForm,
+    ScreeningSubmissionForm,
+    UpdateReviewersForm,
+    UpdateSubmissionLeadForm,
+)
 from .models import ApplicationSubmission, ApplicationRevision, RoundsAndLabs, RoundBase, LabBase
 from .tables import (
     AdminSubmissionsTable,
@@ -39,7 +46,7 @@ from .tables import (
     SubmissionFilterAndSearch,
     SummarySubmissionsTable,
 )
-from .workflow import STAGE_CHANGE_ACTIONS
+from .workflow import STAGE_CHANGE_ACTIONS, PHASES_MAPPING
 
 
 @method_decorator(staff_required, name='dispatch')
@@ -59,24 +66,58 @@ class BaseAdminSubmissionsTable(SingleTableMixin, FilterView):
     def get_table_kwargs(self, **kwargs):
         return {**self.excluded, **kwargs}
 
-    def get_filterset_kwargs(self, filterset_class):
-        kwargs = super().get_filterset_kwargs(filterset_class)
-        kwargs.update(self.excluded)
-        return kwargs
+    def get_filterset_kwargs(self, filterset_class, **kwargs):
+        new_kwargs = super().get_filterset_kwargs(filterset_class)
+        new_kwargs.update(self.excluded)
+        new_kwargs.update(kwargs)
+        return new_kwargs
 
     def get_queryset(self):
         return self.filterset_class._meta.model.objects.current().for_table(self.request.user)
 
     def get_context_data(self, **kwargs):
-        kwargs = super().get_context_data(**kwargs)
-
         search_term = self.request.GET.get('query')
-        kwargs.update(
+
+        return super().get_context_data(
             search_term=search_term,
             filter_action=self.filter_action,
+            **kwargs,
         )
 
-        return super().get_context_data(**kwargs)
+
+@method_decorator(staff_required, name='dispatch')
+class BatchUpdateReviewersView(DelegatedViewMixin, FormView):
+    form_class = BatchUpdateReviewersForm
+    context_name = 'batch_reviewer_form'
+
+    def form_invalid(self, form):
+        messages.error(self.request, mark_safe(_('Sorry something went wrong') + form.errors.as_ul()))
+        return super().form_invalid(form)
+
+    def form_valid(self, form):
+        """
+        Loop through all submissions selected on the page,
+        Add any reviewers that were selected,  only if they are not
+        currently saved to that submission.
+        Send out a message of updates.
+        """
+        reviewers = User.objects.filter(id__in=form.cleaned_data['staff_reviewers'])
+
+        submission_ids = form.cleaned_data['submission_ids']
+        submissions = ApplicationSubmission.objects.filter(id__in=submission_ids)
+
+        for submission in submissions:
+            submission.reviewers.add(*reviewers)
+
+        messenger(
+            MESSAGES.BATCH_REVIEWERS_UPDATED,
+            request=self.request,
+            user=self.request.user,
+            submissions=submissions,
+            added=reviewers,
+        )
+
+        return super().form_valid(form)
 
 
 class SubmissionOverviewView(AllActivityContextMixin, BaseAdminSubmissionsTable):
@@ -89,27 +130,35 @@ class SubmissionOverviewView(AllActivityContextMixin, BaseAdminSubmissionsTable)
         return super().get_table_data().order_by(F('last_update').desc(nulls_last=True))[:5]
 
     def get_context_data(self, **kwargs):
-        base_query = RoundsAndLabs.objects.with_progress().order_by('end_date')
+        base_query = RoundsAndLabs.objects.with_progress().active().order_by('-end_date')
         open_rounds = base_query.open()[:6]
         open_query = '?round_state=open'
         closed_rounds = base_query.closed()[:6]
         closed_query = '?round_state=closed'
+        rounds_title = 'All Rounds and Labs'
 
         return super().get_context_data(
             open_rounds=open_rounds,
             open_query=open_query,
             closed_rounds=closed_rounds,
             closed_query=closed_query,
+            rounds_title=rounds_title,
             **kwargs,
         )
 
 
-class SubmissionListView(AllActivityContextMixin, BaseAdminSubmissionsTable):
+class SubmissionListView(AllActivityContextMixin, BaseAdminSubmissionsTable, DelegateableListView):
     template_name = 'funds/submissions.html'
+    form_views = [
+        BatchUpdateReviewersView
+    ]
 
 
-class SubmissionsByRound(BaseAdminSubmissionsTable):
+class SubmissionsByRound(AllActivityContextMixin, BaseAdminSubmissionsTable, DelegateableListView):
     template_name = 'funds/submissions_by_round.html'
+    form_views = [
+        BatchUpdateReviewersView
+    ]
 
     excluded_fields = ('round', 'lead', 'fund')
 
@@ -128,6 +177,34 @@ class SubmissionsByRound(BaseAdminSubmissionsTable):
         return super().get_context_data(object=self.obj, **kwargs)
 
 
+class SubmissionsByStatus(BaseAdminSubmissionsTable):
+    template_name = 'funds/submissions_by_status.html'
+    status_mapping = PHASES_MAPPING
+
+    def get(self, request, *args, **kwargs):
+        self.status = kwargs.get('status')
+        status_data = self.status_mapping[self.status]
+        self.status_name = status_data['name']
+        self.statuses = status_data['statuses']
+        if self.status not in self.status_mapping:
+            raise Http404(_("No statuses match the requested value"))
+
+        return super().get(request, *args, **kwargs)
+
+    def get_filterset_kwargs(self, filterset_class, **kwargs):
+        return super().get_filterset_kwargs(filterset_class, limit_statuses=self.statuses, **kwargs)
+
+    def get_queryset(self):
+        return super().get_queryset().filter(status__in=self.statuses)
+
+    def get_context_data(self, **kwargs):
+        return super().get_context_data(
+            status=self.status_name,
+            statuses=self.statuses,
+            **kwargs,
+        )
+
+
 @method_decorator(staff_required, name='dispatch')
 class ProgressSubmissionView(DelegatedViewMixin, UpdateView):
     model = ApplicationSubmission
@@ -207,10 +284,11 @@ class UpdateReviewersView(DelegatedViewMixin, UpdateView):
             added=added,
             removed=removed,
         )
+
         return response
 
 
-class AdminSubmissionDetailView(ReviewContextMixin, ActivityContextMixin, DelegateableView):
+class AdminSubmissionDetailView(ReviewContextMixin, ActivityContextMixin, DelegateableView, DetailView):
     template_name_suffix = '_admin_detail'
     model = ApplicationSubmission
     form_views = [
@@ -298,7 +376,7 @@ class SubmissionSealedView(DetailView):
             return HttpResponseRedirect(reverse_lazy('funds:submissions:sealed', args=(submission.id,)))
 
 
-class ApplicantSubmissionDetailView(ActivityContextMixin, DelegateableView):
+class ApplicantSubmissionDetailView(ActivityContextMixin, DelegateableView, DetailView):
     model = ApplicationSubmission
     form_views = [CommentFormView]
 
diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py
index 3ed61c1752f7f97d68cd31fa01a388209f659697..b0a965ccab242409cac75da402486072c01eb284 100644
--- a/opentech/apply/funds/workflow.py
+++ b/opentech/apply/funds/workflow.py
@@ -349,7 +349,7 @@ SingleStageExternalDefinition = [
                 'ext_post_external_review_more_info': 'Request More Information',
                 'ext_determination': 'Ready For Determination',
             },
-            'display': 'Ready for Discussion',
+            'display': 'Ready For Discussion',
             'stage': RequestExt,
             'permissions': hidden_from_applicant_permissions,
         },
@@ -444,7 +444,7 @@ DoubleStageDefinition = [
                 'concept_review_more_info': 'Request More Information',
                 'concept_determination': 'Ready For Preliminary Determination',
             },
-            'display': 'Ready for Discussion',
+            'display': 'Ready For Discussion',
             'stage': Concept,
             'permissions': hidden_from_applicant_permissions,
         },
@@ -555,7 +555,7 @@ DoubleStageDefinition = [
                 'proposal_rejected': 'Dismiss',
                 'post_proposal_review_more_info': 'Request More Information',
             },
-            'display': 'Ready for Discussion',
+            'display': 'Ready For Discussion',
             'stage': Proposal,
             'permissions': hidden_from_applicant_permissions,
         },
@@ -591,7 +591,7 @@ DoubleStageDefinition = [
                 'proposal_determination': 'Ready For Final Determination',
                 'post_external_review_more_info': 'Request More Information',
             },
-            'display': 'Ready for Discussion',
+            'display': 'Ready For Discussion',
             'stage': Proposal,
             'permissions': hidden_from_applicant_permissions,
         },
@@ -733,3 +733,50 @@ def get_determination_transitions():
 
 
 DETERMINATION_OUTCOMES = get_determination_transitions()
+
+
+def phases_matching(phrase, exclude=list()):
+    return [
+        status for status, _ in PHASES
+        if status.endswith(phrase) and status not in exclude
+    ]
+
+
+PHASES_MAPPING = {
+    'received': {
+        'name': 'Received',
+        'statuses': [INITIAL_STATE, 'proposal_discussion'],
+    },
+    'internal-review': {
+        'name': 'Internal Review',
+        'statuses': phases_matching('internal_review'),
+    },
+    'in-discussion': {
+        'name': 'In Discussion',
+        'statuses': phases_matching('discussion', exclude=[INITIAL_STATE, 'proposal_discussion']),
+    },
+    'more-information': {
+        'name': 'More Information Requested',
+        'statuses': phases_matching('more_info'),
+    },
+    'invited-for-proposal': {
+        'name': 'Invited for proposal',
+        'statuses': ['draft_proposal'],
+    },
+    'external-review': {
+        'name': 'AC Review',
+        'statuses': phases_matching('external_review'),
+    },
+    'ready-for-determination': {
+        'name': 'Ready for determination',
+        'statuses': phases_matching('determination'),
+    },
+    'accepted': {
+        'name': 'Accepted',
+        'statuses': phases_matching('accepted'),
+    },
+    'dismissed': {
+        'name': 'Dismissed',
+        'statuses': phases_matching('rejected'),
+    },
+}
diff --git a/opentech/apply/utils/views.py b/opentech/apply/utils/views.py
index 87932814703c400b72c81e554d1bc1695c9e806e..3e83f17d44a38942df0dc9f1133d0c3141c7cff2 100644
--- a/opentech/apply/utils/views.py
+++ b/opentech/apply/utils/views.py
@@ -1,7 +1,9 @@
 from django.contrib.auth.decorators import login_required
+from django.forms.models import ModelForm
 from django.utils.decorators import method_decorator
 from django.views import defaults
-from django.views.generic import DetailView, View
+from django.views.generic import View
+from django.views.generic.base import ContextMixin
 from django.views.generic.detail import SingleObjectTemplateResponseMixin
 from django.views.generic.edit import ModelFormMixin, ProcessFormView
 
@@ -35,12 +37,20 @@ class ViewDispatcher(View):
         return view.as_view()(request, *args, **kwargs)
 
 
-class DelegateableView(DetailView):
-    """A view which passes its context to child form views to allow them to post to the same URL """
+class DelegatableBase(ContextMixin):
+    """
+    A view which passes its context to child form views to allow them to post to the same URL
+    `DelegateableViews` objects should contain form views that inherit from `DelegatedViewMixin`
+    and `FormView`
+    """
     form_prefix = 'form-submitted-'
 
+    def get_form_args(self):
+        return (None, None)
+
     def get_context_data(self, **kwargs):
-        forms = dict(form_view.contribute_form(self.object, self.request.user) for form_view in self.form_views)
+        forms = dict(form_view.contribute_form(*self.get_form_args()) for form_view in self.form_views)
+
         return super().get_context_data(
             form_prefix=self.form_prefix,
             **forms,
@@ -48,13 +58,9 @@ class DelegateableView(DetailView):
         )
 
     def post(self, request, *args, **kwargs):
-        self.object = self.get_object()
-
-        kwargs['submission'] = self.object
-
         # Information to pretend we originate from this view
-        kwargs['template_names'] = self.get_template_names()
         kwargs['context'] = self.get_context_data()
+        kwargs['template_names'] = self.get_template_names()
 
         for form_view in self.form_views:
             if self.form_prefix + form_view.context_name in request.POST:
@@ -64,14 +70,34 @@ class DelegateableView(DetailView):
         return self.get(request, *args, **kwargs)
 
 
+class DelegateableView(DelegatableBase):
+    def get_form_args(self):
+        return self.object, self.request.user
+
+    def post(self, request, *args, **kwargs):
+        self.object = self.get_object()
+
+        kwargs['submission'] = self.object
+
+        return super().post(request, *args, **kwargs)
+
+
+class DelegateableListView(DelegatableBase):
+    def post(self, request, *args, **kwargs):
+        self.object_list = self.get_queryset()
+        return super().post(request, *args, **kwargs)
+
+
 class DelegatedViewMixin(View):
     """For use on create views accepting forms from another view"""
+
     def get_template_names(self):
         return self.kwargs['template_names']
 
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
-        kwargs['user'] = self.request.user
+        if self.is_model_form():
+            kwargs['user'] = self.request.user
         return kwargs
 
     def get_form(self, *args, **kwargs):
@@ -86,12 +112,22 @@ class DelegatedViewMixin(View):
         kwargs.update(**{self.context_name: form})
         return super().get_context_data(**kwargs)
 
+    @classmethod
+    def is_model_form(cls):
+        return issubclass(cls.form_class, ModelForm)
+
     @classmethod
     def contribute_form(cls, submission, user):
-        form = cls.form_class(instance=submission, user=user)
+        if cls.is_model_form():
+            form = cls.form_class(instance=submission, user=user)
+        else:
+            form = cls.form_class()  # This is for the batch update, we don't pass in the user or a single submission
         form.name = cls.context_name
         return cls.context_name, form
 
+    def get_success_url(self):
+        return self.request.path
+
 
 class CreateOrUpdateView(SingleObjectTemplateResponseMixin, ModelFormMixin, ProcessFormView):
 
diff --git a/opentech/settings/base.py b/opentech/settings/base.py
index fb39394186581b10c4c316deb34e0a6de93ce67c..ca17c3ff66873d59355387e1ff0c228ca0b8195e 100644
--- a/opentech/settings/base.py
+++ b/opentech/settings/base.py
@@ -446,6 +446,11 @@ HIJACK_DECORATOR = 'opentech.apply.users.decorators.superuser_decorator'
 
 # Messaging Settings
 SEND_MESSAGES = env.get('SEND_MESSAGES', 'false').lower() == 'true'
+
+if not SEND_MESSAGES:
+    from django.contrib.messages import constants as message_constants
+    MESSAGE_LEVEL = message_constants.DEBUG
+
 SLACK_DESTINATION_URL = env.get('SLACK_DESTINATION_URL', None)
 SLACK_DESTINATION_ROOM = env.get('SLACK_DESTINATION_ROOM', None)
 
diff --git a/opentech/settings/test.py b/opentech/settings/test.py
index e084affe86bd4e5fa78898f94cac87274848dc42..11a71f78e5248d8568b15eaf0f8d0ca00ff70acd 100644
--- a/opentech/settings/test.py
+++ b/opentech/settings/test.py
@@ -1,3 +1,7 @@
+import logging
+
+logging.disable(logging.CRITICAL)
+
 from .base import *  # noqa
 
 # Should only include explicit testing settings
diff --git a/opentech/static_src/src/app/src/SubmissionsByRoundApp.js b/opentech/static_src/src/app/src/SubmissionsByRoundApp.js
index 133ec908ac6d5d229408761c8f6bb708eae82fed..33a3c41831079424ba5c05019a7c9c4f6b83f2d8 100644
--- a/opentech/static_src/src/app/src/SubmissionsByRoundApp.js
+++ b/opentech/static_src/src/app/src/SubmissionsByRoundApp.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux'
 
-import Switcher from '@components/Switcher';
+import SwitcherApp from './SwitcherApp';
 import GroupByStatusDetailView from '@containers/GroupByStatusDetailView';
 import { setCurrentSubmissionRound } from '@actions/submissions';
 
@@ -15,43 +15,15 @@ class SubmissionsByRoundApp extends React.Component {
         pageContent: PropTypes.node.isRequired,
     };
 
-
-    state = { detailOpened: false };
-
     componentDidMount() {
         this.props.setSubmissionRound(this.props.roundID);
     }
 
-    openDetail = () => {
-        this.setState(state => ({
-            style: { ...state.style, display: 'none' } ,
-            detailOpened: true,
-        }));
-    }
-
-    closeDetail = () => {
-        this.setState(state => {
-            const newStyle = { ...state.style };
-            delete newStyle.display;
-            return {
-                style: newStyle,
-                detailOpened: false,
-            };
-        });
-    }
-
     render() {
-        return (
-            <>
-                <Switcher selector='submissions-by-round-app-react-switcher' open={this.state.detailOpened} handleOpen={this.openDetail} handleClose={this.closeDetail} />
-
-                <div style={this.state.style} ref={this.setOriginalContentRef} dangerouslySetInnerHTML={{ __html: this.props.pageContent }} />
-
-                {this.state.detailOpened &&
-                    <GroupByStatusDetailView roundId={this.props.roundID} />
-                }
-            </>
-        )
+        return <SwitcherApp
+                detailComponent={<GroupByStatusDetailView />}
+                switcherSelector={'submissions-by-round-app-react-switcher'}
+                pageContent={this.props.pageContent} />;
     }
 }
 
diff --git a/opentech/static_src/src/app/src/SubmissionsByStatusApp.js b/opentech/static_src/src/app/src/SubmissionsByStatusApp.js
new file mode 100644
index 0000000000000000000000000000000000000000..785d1c2ffdd6373d27d69aa9a856a24997a4b87f
--- /dev/null
+++ b/opentech/static_src/src/app/src/SubmissionsByStatusApp.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import SwitcherApp from './SwitcherApp';
+import { hot } from 'react-hot-loader';
+
+import GroupByRoundDetailView from '@containers/GroupByRoundDetailView';
+
+
+class SubmissionsByStatusApp extends React.Component {
+    static propTypes = {
+        pageContent: PropTypes.node.isRequired,
+        statuses: PropTypes.arrayOf(PropTypes.string),
+    };
+
+    render() {
+        return <SwitcherApp
+                detailComponent={<GroupByRoundDetailView submissionStatuses={this.props.statuses} />}
+                switcherSelector={'submissions-by-status-app-react-switcher'}
+                pageContent={this.props.pageContent} />;
+    }
+}
+
+export default hot(module)(SubmissionsByStatusApp);
diff --git a/opentech/static_src/src/app/src/SwitcherApp.js b/opentech/static_src/src/app/src/SwitcherApp.js
new file mode 100644
index 0000000000000000000000000000000000000000..7394d65977a8b3bf16e6683f7a6a1d56480855a2
--- /dev/null
+++ b/opentech/static_src/src/app/src/SwitcherApp.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Switcher from '@components/Switcher';
+
+export default class SwitcherApp extends React.Component {
+    static propTypes = {
+        pageContent: PropTypes.node.isRequired,
+        detailComponent: PropTypes.node.isRequired,
+        switcherSelector: PropTypes.string.isRequired,
+    };
+
+
+    state = { detailOpened: false };
+
+    openDetail = () => {
+        document.body.classList.add('app-open');
+        this.setState(state => ({
+            style: { ...state.style, display: 'none' } ,
+            detailOpened: true,
+        }));
+    }
+
+    closeDetail = () => {
+        document.body.classList.remove('app-open');
+        this.setState(state => {
+            const newStyle = { ...state.style };
+            delete newStyle.display;
+            return {
+                style: newStyle,
+                detailOpened: false,
+            };
+        });
+    }
+
+    render() {
+        return (
+            <>
+                <Switcher selector={this.props.switcherSelector} open={this.state.detailOpened} handleOpen={this.openDetail} handleClose={this.closeDetail} />
+
+                <div style={this.state.style} ref={this.setOriginalContentRef} dangerouslySetInnerHTML={{ __html: this.props.pageContent }} />
+
+                {this.state.detailOpened && this.props.detailComponent}
+            </>
+        )
+    }
+}
diff --git a/opentech/static_src/src/app/src/api/index.js b/opentech/static_src/src/app/src/api/index.js
index 6fcd16016bf03484a9777a09772ecd92b46f4507..e71a708a4cda32b21936364955ec21d3e5bd8fd9 100644
--- a/opentech/static_src/src/app/src/api/index.js
+++ b/opentech/static_src/src/app/src/api/index.js
@@ -1,6 +1,15 @@
-import { fetchSubmission, fetchSubmissionsByRound } from '@api/submissions';
+import { fetchSubmission, fetchSubmissionsByRound, fetchSubmissionsByStatuses } from '@api/submissions';
+import { fetchRound, fetchRounds } from '@api/rounds';
+import { createNoteForSubmission, fetchNotesForSubmission } from '@api/notes';
 
 export default {
     fetchSubmissionsByRound,
+    fetchSubmissionsByStatuses,
     fetchSubmission,
+
+    fetchRound,
+    fetchRounds,
+
+    fetchNotesForSubmission,
+    createNoteForSubmission,
 };
diff --git a/opentech/static_src/src/app/src/api/notes.js b/opentech/static_src/src/app/src/api/notes.js
new file mode 100644
index 0000000000000000000000000000000000000000..ec431cf28874a8b7baed6976de7a1988a751c5ab
--- /dev/null
+++ b/opentech/static_src/src/app/src/api/notes.js
@@ -0,0 +1,20 @@
+export function fetchNotesForSubmission(submissionID, visibility = 'internal') {
+    return {
+        path: `/apply/api/submissions/${submissionID}/comments/`,
+        params: {
+            visibility,
+            page_size: 1000,
+        }
+    };
+}
+
+
+export function createNoteForSubmission(submissionID, note) {
+    return {
+        path: `/apply/api/submissions/${submissionID}/comments/`,
+        method: 'POST',
+        options: {
+            body: note,
+        }
+    };
+}
diff --git a/opentech/static_src/src/app/src/api/rounds.js b/opentech/static_src/src/app/src/api/rounds.js
new file mode 100644
index 0000000000000000000000000000000000000000..c7ef0a75be9a4e641419fa1e2227d6fe8e55464b
--- /dev/null
+++ b/opentech/static_src/src/app/src/api/rounds.js
@@ -0,0 +1,14 @@
+export function fetchRound(id) {
+    return {
+        path:`/apply/api/rounds/${id}/`,
+    };
+}
+
+export function fetchRounds() {
+    return {
+        path:`/apply/api/rounds/`,
+        params: {
+            page_size: 1000,
+        },
+    };
+}
diff --git a/opentech/static_src/src/app/src/api/submissions.js b/opentech/static_src/src/app/src/api/submissions.js
index 8d7f95f776558bdb14a68dc874901214de1dcceb..f6f2cb41fc9acfcfe364cf07cdac9828ff7cbbb7 100644
--- a/opentech/static_src/src/app/src/api/submissions.js
+++ b/opentech/static_src/src/app/src/api/submissions.js
@@ -1,13 +1,27 @@
-import { apiFetch } from '@api/utils';
+export function fetchSubmissionsByRound(id) {
+    return {
+        path:'/apply/api/submissions/',
+        params: {
+            round: id,
+            page_size: 1000,
+        }
+    };
+}
+
 
-export async function fetchSubmissionsByRound(id) {
-    return apiFetch('/apply/api/submissions/', 'GET', {
-        'round': id,
-        'page_size': 1000,
-    });
+export function fetchSubmission(id) {
+    return {
+        path: `/apply/api/submissions/${id}/`,
+    };
 }
 
+export function fetchSubmissionsByStatuses(statuses) {
+    const params = new URLSearchParams
+    params.append('page_size', 1000)
+    statuses.forEach(v => params.append('status', v));
 
-export async function fetchSubmission(id) {
-    return apiFetch(`/apply/api/submissions/${id}/`, 'GET');
+    return {
+        path:'/apply/api/submissions/',
+        params,
+    };
 }
diff --git a/opentech/static_src/src/app/src/api/utils.js b/opentech/static_src/src/app/src/api/utils.js
index 094b3281fc9d5ee1ff4d5ea12e8ede48d92f40af..29e4a3ef537aaa4054ba4738a0eb1fb989c288a8 100644
--- a/opentech/static_src/src/app/src/api/utils.js
+++ b/opentech/static_src/src/app/src/api/utils.js
@@ -1,20 +1,46 @@
+import Cookies from 'js-cookie';
+
 const getBaseUrl = () => {
     return process.env.API_BASE_URL;
 };
 
-export async function apiFetch(path, method = 'GET', params, options) {
+export function apiFetch({path, method = 'GET', params = new URLSearchParams, options = {}}) {
     const url = new URL(getBaseUrl());
     url.pathname = path;
 
-    if (params !== undefined) {
-        for (const [paramKey, paramValue] of Object.entries(params)) {
-            url.searchParams.set(paramKey, paramValue);
-        }
+    for (const [paramKey, paramValue] of getIteratorForParams(params)) {
+        url.searchParams.append(paramKey, paramValue);
+    }
+
+    if (['post', 'put', 'patch', 'delete'].includes(method.toLowerCase())) {
+        options.headers = {
+            ...options.headers,
+            'Content-Type': 'application/json',
+            'X-CSRFToken': getCSRFToken(),
+        };
     }
+
     return fetch(url, {
         ...options,
+        headers: {
+            ...options.headers,
+            'Accept': 'application/json',
+        },
         method,
         mode: 'same-origin',
         credentials: 'include'
     });
 }
+
+function getCSRFToken() {
+    return Cookies.get('csrftoken');
+}
+
+
+function getIteratorForParams(params) {
+    if (params instanceof URLSearchParams) {
+        return params;
+    }
+
+    return Object.entries(params);
+}
diff --git a/opentech/static_src/src/app/src/components/DetailView/index.js b/opentech/static_src/src/app/src/components/DetailView/index.js
index b392e211ef577a62aaa64ad8a674b62544cf83cf..933c972c3496be0e01adcbedc313df5c989468ce 100644
--- a/opentech/static_src/src/app/src/components/DetailView/index.js
+++ b/opentech/static_src/src/app/src/components/DetailView/index.js
@@ -19,57 +19,31 @@ class DetailView extends Component {
         clearSubmission: PropTypes.func.isRequired,
     };
 
-    state = {
-        listingShown: true,
-        firstRender: true,
-    }
-
     isMobile = (width) => (width ? width : this.props.windowSize.windowWidth) < 1024
 
     renderDisplay () {
         return <DisplayPanel />
     }
 
-    componentDidUpdate (prevProps, prevState) {
-        if (this.isMobile()) {
-            const haveCleared = prevProps.submissionID && !this.props.submissionID
-            const haveUpdated = !prevProps.submissionID && this.props.submissionID
-
-            if ( haveCleared ) {
-                this.setState({listingShown: true})
-            } else if ( haveUpdated && this.state.firstRender ) {
-                // Listing automatically updating after update
-                // clear, but dont run again
-                this.props.clearSubmission()
-                this.setState({firstRender: false})
-            } else if ( prevProps.submissionID !== this.props.submissionID) {
-                // Submission has changed and we want to show it
-                // reset the firstRender so that we can clear it again
-                this.setState({
-                    listingShown: false,
-                    firstRender: true,
-                })
-            }
-        }
-    }
-
     render() {
-        const { listing } = this.props;
+        const { listing, submissionID } = this.props;
+
+        const activeSubmision = !!submissionID;
 
         if (this.isMobile()) {
             var activeDisplay;
-            if (this.state.listingShown){
-                activeDisplay = (
-                    <SlideOutLeft key={"listing"}>
-                        {listing}
-                    </SlideOutLeft>
-                )
-            } else {
+            if (activeSubmision) {
                 activeDisplay = (
                     <SlideInRight key={"display"}>
                         { this.renderDisplay() }
                     </SlideInRight>
                 )
+            } else {
+                activeDisplay = (
+                    <SlideOutLeft key={"listing"}>
+                        { React.cloneElement(listing, { shouldSelectFirst: false }) }
+                    </SlideOutLeft>
+                )
             }
 
             return (
diff --git a/opentech/static_src/src/app/src/components/DetailView/style.scss b/opentech/static_src/src/app/src/components/DetailView/style.scss
index 2c70a676efe6257473dd79fc4c59d2ae0214535e..8ef5f56405c459ec09a38d11a18317cb17ed3c31 100644
--- a/opentech/static_src/src/app/src/components/DetailView/style.scss
+++ b/opentech/static_src/src/app/src/components/DetailView/style.scss
@@ -1,13 +1,7 @@
 .detail-view {
     margin: 0 -20px;
-    overflow-y: overlay;
 
     @include media-query(tablet-landscape) {
-        display: grid;
-        grid-template-columns: 250px 1fr;
-    }
-
-    @include media-query(desktop) {
         // breakout of the wrapper
         width: 100vw;
         position: relative;
@@ -15,6 +9,11 @@
         right: 50%;
         margin-left: -50vw;
         margin-right: -50vw;
+        display: grid;
+        grid-template-columns: 250px 1fr;
+    }
+
+    @include media-query(desktop) {
         grid-template-columns: 390px 1fr;
     }
 
diff --git a/opentech/static_src/src/app/src/components/EmptyPanel/index.js b/opentech/static_src/src/app/src/components/EmptyPanel/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2fa5274f9b8b3bbe779c86d27ab2a47ce157659
--- /dev/null
+++ b/opentech/static_src/src/app/src/components/EmptyPanel/index.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import PropTypes from 'prop-types';
+
+import NoteIcon from 'images/note.svg';
+
+export default class EmptyPanel extends React.Component {
+    static propTypes = {
+        column: PropTypes.string,
+    }
+
+    render() {
+        const { column } = this.props;
+
+        return (
+            <div className={`listing__list listing__list--${column} is-blank`}>
+                {column === 'notes' &&
+                    <>
+                        <div className="listing__blank-icon">
+                            <NoteIcon />
+                        </div>
+                        <p className="listing__help-text listing__help-text--standout">
+                            There aren&apos;t any notes<br /> for this appication yet&hellip;
+                        </p>
+                    </>
+                }
+
+                {column === 'applications' &&
+                    <p>No results found.</p>
+                }
+            </div>
+        )
+    }
+}
diff --git a/opentech/static_src/src/app/src/components/GroupedListing/index.js b/opentech/static_src/src/app/src/components/GroupedListing/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..bd796df960a714afc24048d18b7e491dca0d561d
--- /dev/null
+++ b/opentech/static_src/src/app/src/components/GroupedListing/index.js
@@ -0,0 +1,154 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Listing from '@components/Listing';
+import ListingGroup from '@components/ListingGroup';
+import ListingItem from '@components/ListingItem';
+import ListingDropdown from '@components/ListingDropdown';
+
+import './styles.scss'
+
+export default class GroupedListing extends React.Component {
+    static propTypes = {
+        items: PropTypes.array,
+        activeItem: PropTypes.number,
+        isLoading: PropTypes.bool,
+        error: PropTypes.string,
+        groupBy: PropTypes.string,
+        order: PropTypes.arrayOf(PropTypes.shape({
+            key: PropTypes.string.isRequired,
+            display: PropTypes.string.isRequired,
+            values: PropTypes.arrayOf(PropTypes.string),
+        })),
+        onItemSelection: PropTypes.func,
+        shouldSelectFirst: PropTypes.bool,
+    };
+
+    static defaultProps = {
+        shouldSelectFirst: true,
+    }
+
+
+    state = {
+        orderedItems: [],
+    };
+
+    constructor(props) {
+        super(props);
+        this.listRef = React.createRef();
+    }
+
+    componentDidMount() {
+        this.orderItems();
+
+        // get the height of the dropdown container
+        this.dropdownContainerHeight = this.dropdownContainer.offsetHeight;
+    }
+
+    shouldComponentUpdate(nextProps, nextState) {
+        const propsToCheck = ['items', 'isLoading', 'error']
+        if ( propsToCheck.some(prop => nextProps[prop] !== this.props[prop])) {
+            return true
+        }
+        if ( nextState.orderedItems !== this.state.orderedItems ) {
+            return true
+        }
+        return false
+    }
+
+    componentDidUpdate(prevProps, prevState) {
+        // Order items
+        if (this.props.items !== prevProps.items) {
+            this.orderItems();
+        }
+
+        if ( this.props.shouldSelectFirst ){
+            const oldItem = prevProps.activeItem
+            const newItem = this.props.activeItem
+
+            // If we have never activated a submission, get the first item
+            if ( !newItem && !oldItem ) {
+                const firstGroup = this.state.orderedItems[0]
+                if ( firstGroup && firstGroup.items[0] ) {
+                    this.setState({firstUpdate: false})
+                    this.props.onItemSelection(firstGroup.items[0].id)
+                }
+            }
+        }
+    }
+
+    getGroupedItems() {
+        const { groupBy, items } = this.props;
+
+        return items.reduce((tmpItems, v) => {
+            const groupByValue = v[groupBy];
+            if (!(groupByValue in tmpItems)) {
+                tmpItems[groupByValue] = [];
+            }
+            tmpItems[groupByValue].push({...v});
+            return tmpItems;
+        }, {});
+    }
+
+    orderItems() {
+        const groupedItems = this.getGroupedItems();
+        const { order = [] } = this.props;
+        const orderedItems = order.map(({key, display, values}) => ({
+            name: display,
+            key,
+            items: values.reduce((acc, value) => acc.concat(groupedItems[value] || []), [])
+        })).filter(({items}) => items.length !== 0)
+
+        this.setState({orderedItems});
+    }
+
+    renderItem = group => {
+        const { activeItem, onItemSelection } = this.props;
+        return (
+            <ListingGroup key={`listing-group-${group.key}`} id={group.key} item={group}>
+                {group.items.map(item => {
+                    return <ListingItem
+                        selected={!!activeItem && activeItem===item.id}
+                        onClick={() => onItemSelection(item.id)}
+                        key={`listing-item-${item.id}`}
+                        item={item}/>;
+                })}
+            </ListingGroup>
+        );
+    }
+
+    render() {
+        const { isLoading, error } = this.props;
+        const isError = Boolean(error);
+
+        const passProps = {
+            items: this.state.orderedItems,
+            renderItem: this.renderItem,
+            isLoading,
+            isError,
+            error
+        };
+
+        // set css custom prop to allow scrolling from dropdown to last item in the list
+        if (this.listRef.current) {
+            document.documentElement.style.setProperty('--last-listing-item-height', this.listRef.current.firstChild.lastElementChild.offsetHeight + 'px');
+        }
+
+        return  (
+            <div className="grouped-listing">
+                <div className="grouped-listing__dropdown" ref={(ref) => this.dropdownContainer = ref}>
+                    {!error && !isLoading &&
+                        <ListingDropdown
+                            error={error}
+                            isLoading={isLoading}
+                            listRef={this.listRef}
+                            groups={this.state.orderedItems}
+                            scrollOffset={this.dropdownContainerHeight}
+                        />
+                    }
+                </div>
+                <Listing {...passProps} listRef={this.listRef} column="applications" />
+            </div>
+        );
+    }
+}
diff --git a/opentech/static_src/src/app/src/components/GroupedListing/styles.scss b/opentech/static_src/src/app/src/components/GroupedListing/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..2306ff7050659868426b7340760a01304183a791
--- /dev/null
+++ b/opentech/static_src/src/app/src/components/GroupedListing/styles.scss
@@ -0,0 +1,11 @@
+.grouped-listing {
+    &__dropdown {
+        @include submission-list-item;
+        height: $listing-header-height;
+        padding: 20px;
+    }
+
+    .loading-panel__icon::after {
+        background: $color--light-grey;
+    }
+}
diff --git a/opentech/static_src/src/app/src/components/Listing/index.js b/opentech/static_src/src/app/src/components/Listing/index.js
index 65f786726b058511d888c230700a004e3c03c841..a4d86373e65bac32b4f10ca16c9a5518f08c9c40 100644
--- a/opentech/static_src/src/app/src/components/Listing/index.js
+++ b/opentech/static_src/src/app/src/components/Listing/index.js
@@ -1,122 +1,89 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { TransitionGroup } from 'react-transition-group';
 
-import ListingGroup from '@components/ListingGroup';
-import ListingItem from '@components/ListingItem';
 import LoadingPanel from '@components/LoadingPanel';
+import EmptyPanel from '@components/EmptyPanel';
+
+import SadNoteIcon from 'images/sad-note.svg';
 
 import './style.scss';
 
 export default class Listing extends React.Component {
     static propTypes = {
-        items: PropTypes.array,
-        activeItem: PropTypes.number,
+        items: PropTypes.array.isRequired,
         isLoading: PropTypes.bool,
+        isError: PropTypes.bool,
         error: PropTypes.string,
         groupBy: PropTypes.string,
         order: PropTypes.arrayOf(PropTypes.string),
         onItemSelection: PropTypes.func,
+        renderItem: PropTypes.func.isRequired,
+        handleRetry: PropTypes.func,
+        listRef: PropTypes.object,
+        column: PropTypes.string,
     };
 
-    state = {
-        orderedItems: [],
-    };
-
-    componentDidMount() {
-        this.orderItems();
-    }
-
-    componentDidUpdate(prevProps, prevState) {
-        // Order items
-        if (this.props.items !== prevProps.items) {
-            this.orderItems();
-        }
-
-        const oldItem = prevProps.activeItem
-        const newItem = this.props.activeItem
-
-        // If we have never activated a submission, get the first item
-        if ( !newItem && !oldItem ) {
-            const firstGroup = this.state.orderedItems[0]
-            if ( firstGroup && firstGroup.items[0] ) {
-                this.setState({firstUpdate: false})
-                this.props.onItemSelection(firstGroup.items[0].id)
-            }
-        }
-    }
-
     renderListItems() {
-        const { isLoading, error, items, onItemSelection, activeItem } = this.props;
+        const {
+            isError,
+            isLoading,
+            items,
+            renderItem,
+            column,
+            listRef,
+        } = this.props;
 
         if (isLoading) {
             return (
-                <div className="listing__list is-loading">
+                <div className="listing__list">
                     <LoadingPanel />
                 </div>
-            )
-        } else if (error) {
-            return (
-                <div className="listing__list is-loading">
-                    <p>Something went wrong. Please try again later.</p>
-                    <p>{ error }</p>
-                </div>
-            )
+            );
+        } else if (isError) {
+            return this.renderError();
         } else if (items.length === 0) {
-            return (
-                <div className="listing__list is-loading">
-                    <p>No results found.</p>
-                </div>
-            )
+            return <EmptyPanel column={this.props.column} />;
         }
 
         return (
-            <ul className="listing__list">
-                {this.state.orderedItems.map(group => {
-                    return (
-                        <ListingGroup key={`listing-group-${group.name}`} item={group}>
-                            {group.items.map(item => {
-                                return <ListingItem
-                                    selected={!!activeItem && activeItem===item.id}
-                                    onClick={() => onItemSelection(item.id)}
-                                    key={`listing-item-${item.id}`}
-                                    item={item}/>;
-                            })}
-                        </ListingGroup>
-                    );
-                })}
+            <ul className={`listing__list listing__list--${column}`} ref={listRef}>
+                <TransitionGroup>
+                    {items.map(v => renderItem(v))}
+                </TransitionGroup>
             </ul>
         );
     }
 
-    getGroupedItems() {
-        const { groupBy, items } = this.props;
+    renderError = () => {
+        const { handleRetry, error, column } = this.props;
+        const retryButton = <a className="listing__help-link" onClick={handleRetry}>Refresh</a>;
 
-        return items.reduce((tmpItems, v) => {
-            const groupByValue = v[groupBy];
-            if (!(groupByValue in tmpItems)) {
-                tmpItems[groupByValue] = [];
-            }
-            tmpItems[groupByValue].push({...v});
-            return tmpItems;
-        }, {});
-    }
+        return (
+            <div className={`listing__list listing__list--${column} is-blank`}>
+                {error && <p>{error}</p>}
+
+                {!handleRetry &&
+                    <p>Something went wrong!</p>
+                }
 
-    orderItems() {
-        const groupedItems = this.getGroupedItems();
-        const { order = [] } = this.props;
-        const leftOverKeys = Object.keys(groupedItems).filter(v => !order.includes(v));
-        this.setState({
-            orderedItems: order.concat(leftOverKeys).filter(key => groupedItems[key] ).map(key => ({
-                name: key,
-                items: groupedItems[key] || []
-            })),
-        });
+                {handleRetry && retryButton &&
+                    <>
+                        <div className="listing__blank-icon">
+                            <SadNoteIcon  />
+                        </div>
+                        <p className="listing__help-text listing__help-text--standout">Something went wrong!</p>
+                        <p className="listing__help-text">Sorry we couldn&apos;t load the notes</p>
+                        {retryButton}
+                    </>
+                }
+            </div>
+        );
     }
 
     render() {
         return (
             <div className="listing">
-                <div className="listing__header"></div>
                 {this.renderListItems()}
             </div>
         );
diff --git a/opentech/static_src/src/app/src/components/Listing/style.scss b/opentech/static_src/src/app/src/components/Listing/style.scss
index 97b57fb30221cf84bc81de50bb72f32c7ff9e6c3..618ac101961b8804509915306bdf585d3f97ae57 100644
--- a/opentech/static_src/src/app/src/components/Listing/style.scss
+++ b/opentech/static_src/src/app/src/components/Listing/style.scss
@@ -5,36 +5,48 @@
     }
 
     &__header {
-        @include submission-list-item;
         height: $listing-header-height;
         padding: 20px;
     }
 
     // containing <ul>
     &__list {
-        @include media-query(tablet-landscape) {
-            // only allow columns to be scrolled on larger screens
-            height: calc(100vh - var(--header-admin-height) - #{$listing-header-height});
-            overflow-y: scroll;
-        }
+        @include column-scrolling;
+        overflow-y: scroll;
+        height: calc(100vh -  #{$listing-header-height});
+
+        // ensures the last item will be at the top of the column after navigating to it via the dropdown
+        &--applications {
+            padding-bottom: calc(100vh - var(--last-listing-item-height) - #{$listing-header-height});
 
-        @include media-query(laptop-short) {
-            // allow for vertical scrolling on laptops
-            height: calc(100vh -  #{$listing-header-height});
+            @include media-query(tablet-landscape) {
+                padding-bottom: calc(100vh - var(--header-admin-height) - var(--last-listing-item-height) - #{$listing-header-height});
+            }
+
+            @include media-query(laptop-short) {
+                padding-bottom: calc(100vh - var(--last-listing-item-height) - #{$listing-header-height});
+            }
         }
 
-        &.is-loading {
-            padding: 20px;
-            border-right: 2px solid $color--light-mid-grey;
+        &--notes {
+            box-shadow: inset 0 -20px 20px -10px $color--light-mid-grey;
+            overflow-y: auto;
+            height: 100%;
 
-            p {
-                margin: 20px 0 20px 20px;
+            @include media-query(tablet-landscape) {
+                height: calc(75vh - var(--header-admin-height) - #{$listing-header-height});
             }
 
-            .loading-panel__icon::after {
-                background: $color--light-grey;
+            @include media-query(laptop-short) {
+                // allow for vertical scrolling on laptops
+                height: calc(60vh -  #{$listing-header-height});
             }
         }
+
+        &.is-blank {
+            padding: 20px;
+            text-align: center;
+        }
     }
 
     // inner <li>'s
@@ -102,4 +114,35 @@
         border-radius: 5px;
         font-size: 14px;
     }
+
+    &__blank-icon {
+        border-radius: 50%;
+        background-color: $color--light-mid-grey;
+        padding: 20px;
+        width: 150px;
+        height: 150px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin: 40px auto 20px;
+
+        svg {
+            fill: $color--white;
+            width: 74px;
+            height: 68px;
+        }
+    }
+
+    &__help-text {
+        margin: 0 0 5px;
+        color: $color--dark-blue;
+
+        &--standout {
+            font-weight: $weight--bold;
+        }
+    }
+
+    &__help-link {
+        text-decoration: underline;
+    }
 }
diff --git a/opentech/static_src/src/app/src/components/ListingDropdown/index.js b/opentech/static_src/src/app/src/components/ListingDropdown/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..8f8369dbf68472ac2895c16cabad2fe1ff936d01
--- /dev/null
+++ b/opentech/static_src/src/app/src/components/ListingDropdown/index.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import smoothscroll from 'smoothscroll-polyfill';
+
+export default class ListingDropdown extends React.Component {
+    static propTypes = {
+        listRef: PropTypes.object,
+        groups: PropTypes.array,
+        scrollOffset: PropTypes.number,
+    }
+
+    componentDidMount() {
+        // polyfill element.scrollTo
+        smoothscroll.polyfill();
+    }
+
+    handleChange(e) {
+        const groupHeaderPosition = document.getElementById(e.target.value).offsetTop - this.props.scrollOffset;
+
+        this.props.listRef.current.scrollTo({
+            top: groupHeaderPosition
+        })
+    }
+
+    renderListDropdown() {
+        const { groups } = this.props;
+
+        return (
+            <form className="form form__select">
+                <select onChange={(e) => this.handleChange(e)} aria-label="Jump to listing group">
+                    {groups.map(group => {
+                        return (
+                            <option key={`listing-heading-${group.key}`} value={group.key}>{group.name}</option>
+                        )
+                    })}
+                </select>
+            </form>
+        )
+    }
+
+    render() {
+        return (
+            <>
+                {this.renderListDropdown()}
+            </>
+        )
+    }
+}
diff --git a/opentech/static_src/src/app/src/components/ListingGroup.js b/opentech/static_src/src/app/src/components/ListingGroup.js
index 6508a18b13c4c5574109451211c0fcd6c4af8f6f..901956f2bde2c6642fc5fc599e9043da86c68be9 100644
--- a/opentech/static_src/src/app/src/components/ListingGroup.js
+++ b/opentech/static_src/src/app/src/components/ListingGroup.js
@@ -10,17 +10,18 @@ export default class ListingGroup extends React.Component {
         item: PropTypes.shape({
             name: PropTypes.string,
         }),
+        id: PropTypes.string,
     };
 
     render() {
-        const {item, children} = this.props
+        const {id, item, children} = this.props
         return (
-            <>
-                <ListingHeading title={item.name} count={children.length} />
+            <li id={id}>
+                <ListingHeading  title={item.name} count={children.length} />
                 <ul>
                     {children}
                 </ul>
-            </>
+            </li>
         );
     }
 }
diff --git a/opentech/static_src/src/app/src/components/ListingHeading.js b/opentech/static_src/src/app/src/components/ListingHeading.js
index 996f7a933e214c8ae0aad300df9f4d1d8fda27ff..b8b05c897d14df5c0478ee2b3ea98b9a64964a54 100644
--- a/opentech/static_src/src/app/src/components/ListingHeading.js
+++ b/opentech/static_src/src/app/src/components/ListingHeading.js
@@ -3,11 +3,12 @@ import PropTypes from 'prop-types';
 
 export default class ListingHeading extends React.Component {
     render() {
+        const parsedTitle = this.props.title.split(' ').join('-').toLowerCase();
         return (
-            <li className="listing__item listing__item--heading">
+            <div className="listing__item listing__item--heading" id={parsedTitle}>
                 <h5 className="listing__title">{this.props.title}</h5>
                 <span className="listing__count">{this.props.count}</span>
-            </li>
+            </div>
         );
     }
 }
diff --git a/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss b/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss
index aaaad97ad9b4bb64522d7e988a23995fa27d2053..1a54f514d49bbb1af93c7c127382e14fb21c2302 100644
--- a/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss
+++ b/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss
@@ -1,5 +1,6 @@
 .loading-panel {
     text-align: center;
+    padding: 20px;
 
     &__icon {
         font-size: 10px;
diff --git a/opentech/static_src/src/app/src/components/NoteListingItem/index.js b/opentech/static_src/src/app/src/components/NoteListingItem/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..686828008a1e1e02529e0aecd36427d2f1eb446b
--- /dev/null
+++ b/opentech/static_src/src/app/src/components/NoteListingItem/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+
+import './styles.scss';
+
+export default class NoteListingItem extends React.Component {
+    static propTypes = {
+        user: PropTypes.string.isRequired,
+        message: PropTypes.string.isRequired,
+        timestamp: PropTypes.instanceOf(moment).isRequired,
+    };
+
+    parseUser() {
+        const { user } = this.props;
+
+        if (user.length > 16) {
+            return `${user.substring(0, 16)}...`
+        } else {
+            return user;
+        }
+    }
+
+    render() {
+        const { timestamp, message } = this.props;
+
+        return (
+            <li className="note">
+                <p className="note__meta">
+                    <span>{this.parseUser()}</span>
+                    <span className="note__date">{timestamp.format('ll')}</span>
+                </p>
+                <div className="note__content" dangerouslySetInnerHTML={{__html: message}} />
+            </li>
+        );
+    }
+}
diff --git a/opentech/static_src/src/app/src/components/NoteListingItem/styles.scss b/opentech/static_src/src/app/src/components/NoteListingItem/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ee7e8f87d19642189f479dbd0185d1f2e1dc1756
--- /dev/null
+++ b/opentech/static_src/src/app/src/components/NoteListingItem/styles.scss
@@ -0,0 +1,55 @@
+.note {
+    margin: 20px;
+    font-size: 14px;
+    transition: opacity 200ms ease-out 200ms, transform 200ms ease-out 200ms;
+
+    &.add-note-enter {
+        opacity: 0;
+        transform: translate(0, 10px);
+    }
+
+    &.add-note-enter-done {
+        opacity: 1;
+        transform: translate(0, 0);
+    }
+
+    &__meta {
+        display: flex;
+        justify-content: space-between;
+        font-weight: $weight--bold;
+        text-transform: uppercase;
+        margin-bottom: 10px;
+        color: transparentize( $color--default, .2);
+        letter-spacing: .3px;
+        font-size: 15px;
+    }
+
+    &__date {
+        color: $color--dark-blue;
+        margin-left: 10px;
+    }
+
+    &__content {
+        margin: 0;
+        word-break: break-all;
+        hyphens: auto;
+
+        ul {
+            list-style: initial;
+            list-style-position: inside;
+        }
+
+        ol {
+            list-style-type: decimal;
+            list-style-position: inside;
+        }
+
+        blockquote {
+            margin: 0;
+            padding-left: 1em;
+            margin-top: 1em;
+            margin-bottom: 1em;
+            border-left: 2px solid $color--dark-blue;
+        }
+    }
+}
diff --git a/opentech/static_src/src/app/src/components/RichTextForm/index.js b/opentech/static_src/src/app/src/components/RichTextForm/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..5f5b42857f3d78231f8f2d64cb0761ae3920c114
--- /dev/null
+++ b/opentech/static_src/src/app/src/components/RichTextForm/index.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import RichTextEditor from 'react-rte';
+
+const toolbarConfig = {
+    display: ['INLINE_STYLE_BUTTONS', 'BLOCK_TYPE_BUTTONS', 'BLOCK_TYPE_DROPDOWN', 'LINK_BUTTONS'],
+    INLINE_STYLE_BUTTONS: [
+        {label: 'Bold', style: 'BOLD', className: 'custom-css-class'},
+        {label: 'Italic', style: 'ITALIC'},
+        {label: 'Underline', style: 'UNDERLINE'},
+        {label: 'Blockquote', style: 'blockquote'},
+
+    ],
+    BLOCK_TYPE_DROPDOWN: [
+        {label: 'Normal', style: 'unstyled'},
+        {label: 'H1', style: 'header-four'},
+        {label: 'H2', style: 'header-five'},
+    ],
+    BLOCK_TYPE_BUTTONS: [
+        {label: 'UL', style: 'unordered-list-item'},
+        {label: 'OL', style: 'ordered-list-item'}
+    ]
+};
+
+export default class RichTextForm extends React.Component {
+    static defaultProps = {
+        disabled: false,
+        initialValue: '',
+    };
+
+    static propTypes = {
+        disabled: PropTypes.bool.isRequired,
+        onValueChange: PropTypes.func,
+        value: PropTypes.string,
+        instance: PropTypes.string,
+        onSubmit: PropTypes.func,
+    };
+
+    state = {
+        value: RichTextEditor.createEmptyValue(),
+    };
+
+    resetEditor = () => {
+        this.setState({value: RichTextEditor.createEmptyValue()});
+    }
+
+    render() {
+        const { instance, disabled } = this.props;
+
+        return (
+            <div className={ instance } >
+                <RichTextEditor
+                    disabled={ disabled }
+                    onChange={ this.handleValueChange }
+                    value={ this.state.value }
+                    className="add-note-form__container"
+                    toolbarClassName="add-note-form__toolbar"
+                    editorClassName="add-note-form__editor"
+                    toolbarConfig={toolbarConfig}
+                />
+                <button
+                    disabled={this.isEmpty() || disabled}
+                    onClick={this.handleSubmit}
+                    className={`button ${instance}__button`}
+                >
+                    Submit
+                </button>
+            </div>
+        );
+    }
+
+    isEmpty = () => {
+        return !this.state.value.getEditorState().getCurrentContent().hasText();
+    }
+
+    handleValueChange = value => {
+        this.setState({value});
+    }
+
+    handleSubmit = () => {
+        this.props.onSubmit(this.state.value.toString('html'), this.resetEditor);
+    }
+}
diff --git a/opentech/static_src/src/app/src/components/SubmissionDisplay/index.js b/opentech/static_src/src/app/src/components/SubmissionDisplay/index.js
index b917578e041a12a90ea442b59624a7b24bd73ee1..3d00eeac22975f6b0358f229ae01c1f4946d2c90 100644
--- a/opentech/static_src/src/app/src/components/SubmissionDisplay/index.js
+++ b/opentech/static_src/src/app/src/components/SubmissionDisplay/index.js
@@ -64,14 +64,14 @@ export default class SubmissionDisplay extends Component {
                 </div>
             )
         }
-        const { meta_questions = [], questions = [], stage} = this.props.submission;
+        const { metaQuestions = [], questions = [], stage} = this.props.submission;
 
         return (
             <div className="application-display">
                 <h3>{stage} Information</h3>
 
                 <div className="grid grid--proposal-info">
-                    {meta_questions.map((response, index) => (
+                    {metaQuestions.map((response, index) => (
                         <MetaResponse key={index} {...response} />
                     ))}
                 </div>
diff --git a/opentech/static_src/src/app/src/components/SubmissionDisplay/styles.scss b/opentech/static_src/src/app/src/components/SubmissionDisplay/styles.scss
index 5bb01974ad7f67428f6cced6b0f09f9da9ddad0d..7ad72c885e015d6c5ce5f0fd1eba44d70f5db652 100644
--- a/opentech/static_src/src/app/src/components/SubmissionDisplay/styles.scss
+++ b/opentech/static_src/src/app/src/components/SubmissionDisplay/styles.scss
@@ -1,11 +1,4 @@
 .application-display {
-    @include media-query(tablet-landscape) {
-        height: calc(100vh - var(--header-admin-height) - #{$listing-header-height} - 40px);
-        overflow-y: scroll;
-    }
-
-    @include media-query(laptop-short) {
-        // allow for vertical scrolling on laptops
-        height: calc(100vh -  #{$listing-header-height} - 40px);
-    }
+    @include column-scrolling;
+    padding: 20px;
 }
diff --git a/opentech/static_src/src/app/src/components/Tabber/index.js b/opentech/static_src/src/app/src/components/Tabber/index.js
index 1b1ee8a357ddc3c527d589305bed96414800805c..33d34ec6b942576389a6595c22cdedbcafb2fdb4 100644
--- a/opentech/static_src/src/app/src/components/Tabber/index.js
+++ b/opentech/static_src/src/app/src/components/Tabber/index.js
@@ -1,7 +1,7 @@
 import React, {Component} from 'react';
 import PropTypes from 'prop-types';
 
-
+import './styles.scss';
 
 export const Tab = ({button, children, handleClick}) => <div>{children}</div>
 Tab.propTypes = {
@@ -43,7 +43,12 @@ class Tabber extends Component {
             <div className="tabber">
                 <div className="tabber__navigation">
                     {children.map((child, i) => {
-                            return <a onClick={child.props.handleClick ? child.props.handleClick : () => this.handleClick(i)} className="display-panel__link" key={child.key}>{child.props.button}</a>
+                        return <a
+                        key={child.key}
+                        onClick={child.props.handleClick ? child.props.handleClick : () => this.handleClick(i)}
+                        className={`tabber__link ${this.state.activeTab === i ? 'is-active' : ''}`}>
+                            {child.props.button}
+                        </a>
                         })
                     }
                 </div>
diff --git a/opentech/static_src/src/app/src/components/Tabber/styles.scss b/opentech/static_src/src/app/src/components/Tabber/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d72ffea8108d7fcf757831d9592c65f0809d5ca7
--- /dev/null
+++ b/opentech/static_src/src/app/src/components/Tabber/styles.scss
@@ -0,0 +1,52 @@
+.tabber {
+    &__navigation {
+        height: $listing-header-height;
+        display: flex;
+        justify-content: flex-start;
+        margin: 0 10px;
+
+        @include media-query(tablet-landscape) {
+            display: block;
+            margin: 0 20px;
+        }
+    }
+
+    &__link {
+        align-items: center;
+        display: inline-flex;
+        height: calc(100% - 2px);
+        padding: 0 10px;
+        position: relative;
+        color: $color--default;
+        font-weight: $weight--semibold;
+        opacity: .6 ;
+        transition: opacity $transition;
+
+        @include media-query(tablet-landscape) {
+            margin-right: 20px;
+        }
+
+        &::after {
+            content: '';
+            height: 0;
+            width: 100%;
+            position: absolute;
+            bottom: 0;
+            left: 0;
+            background-color: $color--dark-blue;
+            transition: height $transition;
+        }
+
+        &:hover,
+        &:focus,
+        &.is-active {
+            opacity: 1;
+        }
+
+        &.is-active {
+            &::after {
+                height: 4px;
+            }
+        }
+    }
+}
diff --git a/opentech/static_src/src/app/src/containers/AddNoteForm.js b/opentech/static_src/src/app/src/containers/AddNoteForm.js
new file mode 100644
index 0000000000000000000000000000000000000000..8daaf79b03d786317a25f0d1e86ece142a980a96
--- /dev/null
+++ b/opentech/static_src/src/app/src/containers/AddNoteForm.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+
+import { createNoteForSubmission } from '@actions/notes';
+import RichTextForm from '@components/RichTextForm';
+
+import {
+    getNoteCreatingErrorForSubmission,
+    getNoteCreatingStateForSubmission,
+} from '@selectors/notes';
+
+import './AddNoteForm.scss';
+
+class AddNoteForm extends React.Component {
+    static propTypes = {
+        submitNote: PropTypes.func,
+        submissionID: PropTypes.number,
+        error: PropTypes.any,
+        isCreating: PropTypes.bool,
+    };
+
+    render() {
+        const { error, isCreating } = this.props;
+        return (
+            <>
+                {Boolean(error) && <p>{error}</p>}
+                <RichTextForm
+                    disabled={isCreating}
+                    onSubmit={this.onSubmit}
+                    instance="add-note-form"
+                />
+            </>
+        );
+    }
+
+    onSubmit = (message, resetEditor) => {
+        this.props.submitNote(this.props.submissionID, {
+            message,
+            visibility: 'internal',
+        }).then(() => resetEditor());
+    }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+    error: getNoteCreatingErrorForSubmission(ownProps.submissionID)(state),
+    isCreating: getNoteCreatingStateForSubmission(ownProps.submissionID)(state),
+});
+
+const mapDispatchToProps = dispatch => ({
+    submitNote: (submissionID, note) => dispatch(createNoteForSubmission(submissionID, note)),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddNoteForm);
diff --git a/opentech/static_src/src/app/src/containers/AddNoteForm.scss b/opentech/static_src/src/app/src/containers/AddNoteForm.scss
new file mode 100644
index 0000000000000000000000000000000000000000..82c8ea9731d11f4acafb1b4b6e1aeb724543b6b6
--- /dev/null
+++ b/opentech/static_src/src/app/src/containers/AddNoteForm.scss
@@ -0,0 +1,59 @@
+$submit-button-height: 60px;
+
+.add-note-form {
+    position: relative;
+
+    @include media-query(tablet-landscape) {
+        height: 25vh; // .list--notes is set to 75vh
+    }
+
+    @include media-query(laptop-short) {
+        height: 40vh; // .list--notes is set to 60vh
+    }
+
+    &__container {
+        width: 100%;
+        height: calc(100% - #{$submit-button-height});
+        border: 0;
+        overflow: hidden;
+        font-family: inherit;
+    }
+
+    &__toolbar {
+        margin: 0;
+        padding: 10px;
+    }
+
+    &__editor {
+        height: 250px;
+        overflow-y: scroll;
+        padding-bottom: 60px;
+
+        @include media-query(tablet-landscape) {
+            height: 100%;
+            padding-bottom: 95px;
+        }
+    }
+
+    &__button {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        width: 100%;
+        border-top: 1px solid $color--mid-grey;
+        color: $color--dark-blue;
+        font-weight: $weight--bold;
+        font-size: 18px;
+        height: $submit-button-height;
+        opacity: 1;
+    }
+
+    textarea,
+    &__button {
+        &:disabled {
+            cursor: not-allowed;
+            opacity: .5;
+        }
+    }
+}
diff --git a/opentech/static_src/src/app/src/containers/ByRoundListing.js b/opentech/static_src/src/app/src/containers/ByRoundListing.js
new file mode 100644
index 0000000000000000000000000000000000000000..8cb617de8b8d1785d9066d9d0bbe1ef907f03d3f
--- /dev/null
+++ b/opentech/static_src/src/app/src/containers/ByRoundListing.js
@@ -0,0 +1,109 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux'
+
+import GroupedListing from '@components/GroupedListing';
+import {
+    loadRounds,
+    loadSubmissionsOfStatuses,
+    setCurrentSubmission,
+} from '@actions/submissions';
+import {
+    getRounds,
+    getRoundsFetching,
+    getRoundsErrored,
+} from '@selectors/rounds';
+import {
+    getSubmissionsByGivenStatuses,
+    getCurrentSubmissionID,
+    getByGivenStatusesError,
+    getByGivenStatusesLoading,
+} from '@selectors/submissions';
+
+
+const loadData = props => {
+    props.loadRounds()
+    props.loadSubmissions()
+}
+
+class ByRoundListing extends React.Component {
+    static propTypes = {
+        submissionStatuses: PropTypes.arrayOf(PropTypes.string),
+        loadSubmissions: PropTypes.func,
+        submissions: PropTypes.arrayOf(PropTypes.object),
+        isErrored: PropTypes.bool,
+        setCurrentItem: PropTypes.func,
+        activeSubmission: PropTypes.number,
+        shouldSelectFirst: PropTypes.bool,
+        rounds: PropTypes.array,
+        isLoading: PropTypes.bool,
+    };
+
+    componentDidMount() {
+        // Update items if round ID is defined.
+        if ( this.props.submissionStatuses ) {
+            loadData(this.props)
+        }
+    }
+
+    componentDidUpdate(prevProps) {
+        const { submissionStatuses } = this.props;
+        if (!submissionStatuses.every(v => prevProps.submissionStatuses.includes(v))) {
+            loadData(this.props)
+        }
+    }
+
+    prepareOrder = () => {
+        const { isLoading, rounds, submissions } = this.props;
+        if (isLoading)
+            return []
+        return submissions.map(submission => submission.round)
+                          .filter((round, index, arr) => arr.indexOf(round) === index)
+                          .map((round, i) => ({
+                              display: rounds[parseInt(round)].title,
+                              key: `round-${round}`,
+                              position: i,
+                              values: [round],
+                          }));
+    }
+
+    render() {
+        const { isLoading, isErrored, submissions, setCurrentItem, activeSubmission, shouldSelectFirst} = this.props;
+        const order = this.prepareOrder();
+        return <GroupedListing
+                isLoading={isLoading}
+                error={isErrored ? 'Fetching failed.' : undefined}
+                items={submissions || []}
+                activeItem={activeSubmission}
+                onItemSelection={setCurrentItem}
+                shouldSelectFirst={shouldSelectFirst}
+                groupBy={'round'}
+                order={order}
+            />;
+    }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+    submissions: getSubmissionsByGivenStatuses(ownProps.submissionStatuses)(state),
+    isErrored: (
+        getByGivenStatusesError(ownProps.submissionStatuses)(state) ||
+        getRoundsErrored(state)
+    ),
+    isLoading: (
+        getByGivenStatusesLoading(ownProps.submissionStatuses)(state) ||
+        getRoundsFetching(state)
+    ),
+    activeSubmission: getCurrentSubmissionID(state),
+    rounds: getRounds(state),
+})
+
+const mapDispatchToProps = (dispatch, ownProps) => ({
+    loadSubmissions: () => dispatch(loadSubmissionsOfStatuses(ownProps.submissionStatuses)),
+    loadRounds: () => dispatch(loadRounds()),
+    setCurrentItem: id => dispatch(setCurrentSubmission(id)),
+});
+
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(ByRoundListing);
diff --git a/opentech/static_src/src/app/src/containers/ByStatusListing.js b/opentech/static_src/src/app/src/containers/ByStatusListing.js
index 873036c9467564588ebc4fba69d8bba81002c29c..4eb413c94a9ccc2ca1d7c79024d381b831822432 100644
--- a/opentech/static_src/src/app/src/containers/ByStatusListing.js
+++ b/opentech/static_src/src/app/src/containers/ByStatusListing.js
@@ -2,9 +2,10 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { connect } from 'react-redux'
 
-import Listing from '@components/Listing';
+import GroupedListing from '@components/GroupedListing';
 import {
     loadCurrentRound,
+    loadCurrentRoundSubmissions,
     setCurrentSubmission,
 } from '@actions/submissions';
 import {
@@ -17,18 +18,21 @@ import {
 
 
 const loadData = props => {
-    props.loadSubmissions(['submissions'])
+    props.loadRound(['workflow'])
+    props.loadSubmissions()
 }
 
 class ByStatusListing extends React.Component {
     static propTypes = {
-        loadSubmissions: PropTypes.func,
+        loadRound: PropTypes.func.isRequired,
+        loadSubmissions: PropTypes.func.isRequired,
         submissions: PropTypes.arrayOf(PropTypes.object),
         roundID: PropTypes.number,
         round: PropTypes.object,
         error: PropTypes.string,
         setCurrentItem: PropTypes.func,
         activeSubmission: PropTypes.number,
+        shouldSelectFirst: PropTypes.bool,
     };
 
     componentDidMount() {
@@ -42,33 +46,39 @@ class ByStatusListing extends React.Component {
         const { roundID } = this.props;
         // Update entries if round ID is changed or is not null.
         if (roundID && prevProps.roundID !== roundID) {
-            console.log('wooop')
             loadData(this.props)
         }
     }
 
+    prepareOrder(round) {
+        if ( !round ) { return []}
+        const slugify = value => value.toLowerCase().replace(/\s/g, '-')
+        const workflow = round.workflow
+        const order = workflow.reduce((accumulator, {display, value}, idx) => {
+            const key = slugify(display);
+            const existing = accumulator[key] || {}
+            const existingValues = existing.values || []
+            const position = existing.position || idx
+            accumulator[key] = {key, display, position, values: [...existingValues, value]}
+            return accumulator
+        }, {})
+        const arrayOrder = Object.values(order).sort((a,b) => a.position - b.position)
+        return arrayOrder
+    }
+
     render() {
-        const { error, submissions, round, setCurrentItem, activeSubmission } = this.props;
-        const isLoading = round && round.isFetching
-        return <Listing
+        const { error, submissions, round, setCurrentItem, activeSubmission, shouldSelectFirst } = this.props;
+        const isLoading = !round || ( round && (round.isFetching || round.submissions.isFetching) )
+        const order = this.prepareOrder(round)
+        return <GroupedListing
                     isLoading={isLoading}
                     error={error}
                     items={submissions}
                     activeItem={activeSubmission}
                     onItemSelection={setCurrentItem}
+                    shouldSelectFirst={shouldSelectFirst}
                     groupBy={'status'}
-                    order={[
-                        // TODO: Set the proper order of statuses.
-                        'post_external_review_discussion',
-                        'in_discussion',
-                        'more_info',
-                        'internal_review',
-                        'post_review_discussion',
-                        'post_review_more_info',
-                        'accepted',
-                        'rejected',
-                    ]}
-            />;
+                    order={ order } />;
     }
 }
 
@@ -81,7 +91,8 @@ const mapStateToProps = state => ({
 })
 
 const mapDispatchToProps = dispatch => ({
-    loadSubmissions: () => dispatch(loadCurrentRound()),
+    loadRound: fields => dispatch(loadCurrentRound(fields)),
+    loadSubmissions: () => dispatch(loadCurrentRoundSubmissions()),
     setCurrentItem: id => dispatch(setCurrentSubmission(id)),
 });
 
diff --git a/opentech/static_src/src/app/src/containers/DisplayPanel/index.js b/opentech/static_src/src/app/src/containers/DisplayPanel/index.js
index 87fa4e56cfc7178637fe7a63f91c40804878b58c..b17607d50d550a6ebf1d991368e87dc75c9e5715 100644
--- a/opentech/static_src/src/app/src/containers/DisplayPanel/index.js
+++ b/opentech/static_src/src/app/src/containers/DisplayPanel/index.js
@@ -13,10 +13,11 @@ import {
 } from '@selectors/submissions';
 
 import CurrentSubmissionDisplay from '@containers/CurrentSubmissionDisplay'
+import AddNoteForm from '@containers/AddNoteForm';
+import NoteListing from '@containers/NoteListing';
 import Tabber, {Tab} from '@components/Tabber'
 import './style.scss';
 
-
 class DisplayPanel extends React.Component  {
     static propTypes = {
         submissionID: PropTypes.number,
@@ -28,18 +29,18 @@ class DisplayPanel extends React.Component  {
     };
 
     render() {
-        const { windowSize: {windowWidth: width} } = this.props;
+        const { windowSize: {windowWidth: width}, submissionID } = this.props;
         const { clearSubmission } = this.props;
 
         const isMobile = width < 1024;
-        const { submissionID } = this.props;
         const submissionLink = "/apply/submissions/" + submissionID + "/";
 
         const submission = <CurrentSubmissionDisplay />
 
         let tabs = [
             <Tab button="Notes" key="note">
-                <p>Notes</p>
+                <NoteListing submissionID={submissionID} />
+                <AddNoteForm submissionID={submissionID} />
             </Tab>,
             <Tab button="Status" key="status">
                 <p>Status</p>
@@ -61,7 +62,7 @@ class DisplayPanel extends React.Component  {
                 { !isMobile && (
                     <div className="display-panel__column">
                         <div className="display-panel__header display-panel__header--spacer"></div>
-                        <div className="display-panel__body">
+                        <div className="display-panel__body display-panel__body--center">
                             <a target="_blank" rel="noopener noreferrer" href={ submissionLink }>Open in new tab</a>
                             { submission }
                         </div>
@@ -91,5 +92,4 @@ const mapDispatchToProps = {
     clearSubmission: clearCurrentSubmission
 }
 
-
 export default connect(mapStateToProps, mapDispatchToProps)(withWindowSizeListener(DisplayPanel));
diff --git a/opentech/static_src/src/app/src/containers/DisplayPanel/style.scss b/opentech/static_src/src/app/src/containers/DisplayPanel/style.scss
index 673a9b9266a9501e4a417f087cc8aa11e3bb97d0..52f1dce3709a245af5640445f82c182cf035b2c2 100644
--- a/opentech/static_src/src/app/src/containers/DisplayPanel/style.scss
+++ b/opentech/static_src/src/app/src/containers/DisplayPanel/style.scss
@@ -3,10 +3,6 @@
 
     @include media-query(tablet-landscape) {
         display: grid;
-        grid-template-columns: 1fr 250px;
-    }
-
-    @include media-query(desktop) {
         grid-template-columns: 1fr 390px;
         grid-template-rows: 75px 1fr;
     }
@@ -17,22 +13,23 @@
         width: 100%;
     }
 
-    &__body,
+    &__column {
+        border-bottom: 2px solid $color--light-mid-grey;
+    }
+
     &__header {
-        @include submission-list-item;
-        padding: 20px;
+        border-right: 2px solid $color--light-mid-grey;
     }
 
-    // loading container
-    &__loading {
-        @include media-query(tablet-portrait) {
-            height: calc(100vh - var(--header-admin-height) - #{$listing-header-height} - 40px);
+    &__body {
+        &--center {
+            border-right: 2px solid $color--light-mid-grey;
         }
+    }
 
-        // 100vh - listing header - display-panel__body padding
-        @include media-query(laptop-short) {
-            height: calc(100vh -  #{$listing-header-height} - 40px);
-        }
+    // loading container
+    &__loading {
+        @include column-scrolling;
     }
 
     &__header {
diff --git a/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js b/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js
new file mode 100644
index 0000000000000000000000000000000000000000..657a6f871220600cce0727cdf9f1733d6fb71368
--- /dev/null
+++ b/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import DetailView from '@components/DetailView';
+import ByRoundListing from '@containers/ByRoundListing';
+
+export default class GroupByRoundDetailView extends React.Component {
+    static propTypes = {
+        submissionStatuses: PropTypes.arrayOf(PropTypes.string),
+    };
+
+    render() {
+        const listing = <ByRoundListing submissionStatuses={this.props.submissionStatuses} />;
+        return (
+            <DetailView listing={listing} />
+        );
+    }
+}
diff --git a/opentech/static_src/src/app/src/containers/Note.js b/opentech/static_src/src/app/src/containers/Note.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f297145bc34b0939cef8354166084d8db41435d
--- /dev/null
+++ b/opentech/static_src/src/app/src/containers/Note.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+
+import { getNoteOfID } from '@selectors/notes';
+import NoteListingItem from '@components/NoteListingItem';
+
+class Note extends React.Component {
+    static propTypes = {
+        note: PropTypes.shape({
+            user: PropTypes.string,
+            timestamp: PropTypes.string,
+            message: PropTypes.string,
+        }),
+    };
+
+    render() {
+        const { note } = this.props;
+
+        return <NoteListingItem
+                user={note.user}
+                message={note.message}
+                timestamp={moment(note.timestamp)}
+        />;
+    }
+
+}
+
+const mapStateToProps = (state, ownProps) => ({
+    note: getNoteOfID(ownProps.noteID)(state),
+});
+
+export default connect(mapStateToProps)(Note);
diff --git a/opentech/static_src/src/app/src/containers/NoteListing.js b/opentech/static_src/src/app/src/containers/NoteListing.js
new file mode 100644
index 0000000000000000000000000000000000000000..a6f80fbf6135cabc2ef70e86f5443ee42ef29756
--- /dev/null
+++ b/opentech/static_src/src/app/src/containers/NoteListing.js
@@ -0,0 +1,84 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { CSSTransition } from 'react-transition-group';
+
+import { fetchNotesForSubmission } from '@actions/notes';
+import Listing from '@components/Listing';
+import Note from '@containers/Note';
+import {
+    getNotesErrorState,
+    getNoteIDsForSubmissionOfID,
+    getNotesFetchState,
+} from '@selectors/notes';
+
+class NoteListing extends React.Component {
+    static propTypes = {
+        loadNotes: PropTypes.func,
+        submissionID: PropTypes.number,
+        noteIDs: PropTypes.array,
+        isErrored: PropTypes.bool,
+        isLoading: PropTypes.bool,
+    };
+
+    componentDidUpdate(prevProps) {
+        const { isLoading, loadNotes, submissionID } = this.props;
+        const prevSubmissionID = prevProps.submissionID;
+
+        if(
+            submissionID !== null && submissionID !== undefined &&
+            prevSubmissionID !== submissionID && !isLoading
+        ) {
+            loadNotes(submissionID);
+        }
+    }
+
+    componentDidMount() {
+        const { isLoading, loadNotes, submissionID } = this.props;
+
+        if (submissionID && !isLoading) {
+            loadNotes(submissionID);
+        }
+    }
+
+    handleRetry = () => {
+        if (this.props.isLoading || !this.props.isErrored) {
+            return;
+        }
+        this.props.loadNotes(this.props.submissionID);
+    }
+
+    renderItem = noteID => {
+        return (
+            <CSSTransition key={`note-${noteID}`} timeout={200} classNames="add-note">
+                <Note key={`note-${noteID}`} noteID={noteID} />
+            </CSSTransition>
+        );
+    }
+
+    render() {
+        const { noteIDs } = this.props;
+        const passProps = {
+            isLoading: this.props.isLoading,
+            isError: this.props.isErrored,
+            handleRetry: this.handleRetry,
+            renderItem: this.renderItem,
+            items: noteIDs,
+        };
+        return (
+            <Listing {...passProps} column="notes" />
+        );
+    }
+}
+
+const mapDispatchToProps = dispatch => ({
+    loadNotes: submissionID => dispatch(fetchNotesForSubmission(submissionID)),
+});
+
+const mapStateToProps = (state, ownProps) => ({
+    noteIDs: getNoteIDsForSubmissionOfID(ownProps.submissionID)(state),
+    isLoading: getNotesFetchState(state),
+    isErrored: getNotesErrorState(state),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(NoteListing);
diff --git a/opentech/static_src/src/app/src/datetime.js b/opentech/static_src/src/app/src/datetime.js
new file mode 100644
index 0000000000000000000000000000000000000000..f906f3acee590e92f3df0b5c7a9bb66a8ca79e24
--- /dev/null
+++ b/opentech/static_src/src/app/src/datetime.js
@@ -0,0 +1,8 @@
+import moment from 'moment';
+import 'moment-timezone';
+
+// Use GMT globally for all the dates.
+moment.tz.setDefault('GMT');
+
+// Use en-US locale for all the dates.
+moment.locale('en');
diff --git a/opentech/static_src/src/app/src/redux/actions/notes.js b/opentech/static_src/src/app/src/redux/actions/notes.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0200db84a47cf27a91b4162eaa4fe1bb79c8384
--- /dev/null
+++ b/opentech/static_src/src/app/src/redux/actions/notes.js
@@ -0,0 +1,36 @@
+import { CALL_API } from '@middleware/api'
+
+import api from '@api';
+
+export const FAIL_FETCHING_NOTES = 'FAIL_FETCHING_NOTES';
+export const START_FETCHING_NOTES = 'START_FETCHING_NOTES';
+export const UPDATE_NOTES = 'UPDATE_NOTES';
+export const UPDATE_NOTE = 'UPDATE_NOTE';
+
+export const START_CREATING_NOTE_FOR_SUBMISSION = 'START_CREATING_NOTE_FOR_SUBMISSION';
+export const FAIL_CREATING_NOTE_FOR_SUBMISSION = 'FAIL_CREATING_NOTE_FOR_SUBMISSION';
+
+export const fetchNotesForSubmission = submissionID => (dispatch, getState) => {
+    return dispatch(fetchNotes(submissionID))
+}
+
+const fetchNotes = (submissionID) => ({
+    [CALL_API]: {
+        types: [ START_FETCHING_NOTES, UPDATE_NOTES, FAIL_FETCHING_NOTES],
+        endpoint: api.fetchNotesForSubmission(submissionID),
+    },
+    submissionID,
+})
+
+
+export const createNoteForSubmission = (submissionID, note) => (dispatch, getState) => {
+    return dispatch(createNote(submissionID, note))
+}
+
+const createNote = (submissionID, note) => ({
+    [CALL_API]: {
+        types: [ START_CREATING_NOTE_FOR_SUBMISSION, UPDATE_NOTE, FAIL_CREATING_NOTE_FOR_SUBMISSION],
+        endpoint: api.createNoteForSubmission(submissionID, note),
+    },
+    submissionID,
+})
diff --git a/opentech/static_src/src/app/src/redux/actions/submissions.js b/opentech/static_src/src/app/src/redux/actions/submissions.js
index db35aec0678e3239cec174980b3de9ff6a8d6459..07520243e5b9c57d07534baf3cc3f67c037ca168 100644
--- a/opentech/static_src/src/app/src/redux/actions/submissions.js
+++ b/opentech/static_src/src/app/src/redux/actions/submissions.js
@@ -1,18 +1,39 @@
+import { CALL_API } from '@middleware/api'
+
 import api from '@api';
 import {
     getCurrentSubmission,
     getCurrentSubmissionID,
     getCurrentRoundID,
     getCurrentRound,
+    getCurrentRoundSubmissionIDs,
+    getRounds,
+    getSubmissionsByGivenStatuses,
 } from '@selectors/submissions';
 
 
+// Round
+export const UPDATE_ROUND = 'UPDATE_ROUND';
+export const START_LOADING_ROUND = 'START_LOADING_ROUND';
+export const FAIL_LOADING_ROUND = 'FAIL_LOADING_ROUND';
+
+
+// Rounds
+export const UPDATE_ROUNDS = 'UPDATE_ROUNDS';
+export const START_LOADING_ROUNDS = 'START_LOADING_ROUNDS';
+export const FAIL_LOADING_ROUNDS = 'FAIL_LOADING_ROUNDS';
+
 // Submissions by round
 export const SET_CURRENT_SUBMISSION_ROUND = 'SET_CURRENT_SUBMISSION_ROUND';
 export const UPDATE_SUBMISSIONS_BY_ROUND = 'UPDATE_SUBMISSIONS_BY_ROUND';
 export const START_LOADING_SUBMISSIONS_BY_ROUND = 'START_LOADING_SUBMISSIONS_BY_ROUND';
 export const FAIL_LOADING_SUBMISSIONS_BY_ROUND = 'FAIL_LOADING_SUBMISSIONS_BY_ROUND';
 
+// Submissions by statuses
+export const UPDATE_SUBMISSIONS_BY_STATUSES = 'UPDATE_SUBMISSIONS_BY_STATUSES';
+export const START_LOADING_SUBMISSIONS_BY_STATUSES = 'START_LOADING_SUBMISSIONS_BY_STATUSES';
+export const FAIL_LOADING_SUBMISSIONS_BY_STATUSES = 'FAIL_LOADING_SUBMISSIONS_BY_STATUSES';
+
 // Submissions
 export const SET_CURRENT_SUBMISSION = 'SET_CURRENT_SUBMISSION';
 export const START_LOADING_SUBMISSION = 'START_LOADING_SUBMISSION';
@@ -20,6 +41,9 @@ export const FAIL_LOADING_SUBMISSION = 'FAIL_LOADING_SUBMISSION';
 export const UPDATE_SUBMISSION = 'UPDATE_SUBMISSION';
 export const CLEAR_CURRENT_SUBMISSION = 'CLEAR_CURRENT_SUBMISSION';
 
+// Notes
+export const ADD_NOTE_FOR_SUBMISSION = 'ADD_NOTE_FOR_SUBMISSION';
+
 export const setCurrentSubmissionRound = id => ({
     type: SET_CURRENT_SUBMISSION_ROUND,
     id,
@@ -30,53 +54,98 @@ export const setCurrentSubmission = id => ({
     id,
 });
 
+
 export const loadCurrentRound = (requiredFields=[]) => (dispatch, getState) => {
-    const round = getCurrentRound(getState())
+    const state = getState()
+    const round = getCurrentRound(state)
 
-    if (round && requiredFields.every(key => round.hasOwnProperty(key))) {
+    if ( round && requiredFields.every(key => round.hasOwnProperty(key)) ) {
         return null
     }
 
-    return dispatch(fetchSubmissionsByRound(getCurrentRoundID(getState())))
+    return dispatch(fetchRound(getCurrentRoundID(state)))
 }
 
 
-export const fetchSubmissionsByRound = roundID => {
-    return async function(dispatch) {
-        dispatch(startLoadingSubmissionsByRound(roundID));
-        try {
-            const response = await api.fetchSubmissionsByRound(roundID);
-            const json = await response.json();
-            if (response.ok) {
-                dispatch(updateSubmissionsByRound(roundID, json));
-            } else {
-                dispatch(failLoadingSubmissionsByRound(json.meta.error));
-            }
-        } catch (e) {
-            dispatch(failLoadingSubmissionsByRound(e.message));
-        }
-    };
-};
+export const loadRounds = () => (dispatch, getState) => {
+    const state = getState()
+    const rounds = getRounds(state)
+
+    if ( rounds && Object.keys(rounds).length !== 0 ) {
+        return null
+    }
+    return dispatch(fetchRounds())
+}
 
+export const loadCurrentRoundSubmissions = () => (dispatch, getState) => {
+    const state = getState()
+    const submissions = getCurrentRoundSubmissionIDs(state)
 
-const updateSubmissionsByRound = (roundID, data) => ({
-    type: UPDATE_SUBMISSIONS_BY_ROUND,
+    if ( submissions && submissions.length !== 0 ) {
+        return null
+    }
+
+    return dispatch(fetchSubmissionsByRound(getCurrentRoundID(state)))
+}
+
+
+const fetchRound = (roundID) => ({
+    [CALL_API]: {
+        types: [ START_LOADING_ROUND, UPDATE_ROUND, FAIL_LOADING_ROUND],
+        endpoint: api.fetchRound(roundID),
+    },
     roundID,
-    data,
-});
+})
+
 
+const fetchRounds = () => ({
+    [CALL_API]: {
+        types: [ START_LOADING_ROUNDS, UPDATE_ROUNDS, FAIL_LOADING_ROUNDS],
+        endpoint: api.fetchRounds(),
+    },
+})
 
-const startLoadingSubmissionsByRound = (roundID) => ({
-    type: START_LOADING_SUBMISSIONS_BY_ROUND,
+const fetchSubmissionsByRound = (roundID) => ({
+    [CALL_API]: {
+        types: [ START_LOADING_SUBMISSIONS_BY_ROUND, UPDATE_SUBMISSIONS_BY_ROUND, FAIL_LOADING_SUBMISSIONS_BY_ROUND],
+        endpoint: api.fetchSubmissionsByRound(roundID),
+    },
     roundID,
-});
+})
 
 
-const failLoadingSubmissionsByRound = (message) => ({
-    type: FAIL_LOADING_SUBMISSIONS_BY_ROUND,
-    message,
-});
+const fetchSubmissionsByStatuses = statuses => {
+    if(!Array.isArray(statuses)) {
+        throw new Error("Statuses have to be an array of statuses");
+    }
 
+    return {
+        [CALL_API]: {
+            types: [ START_LOADING_SUBMISSIONS_BY_STATUSES, UPDATE_SUBMISSIONS_BY_STATUSES, FAIL_LOADING_SUBMISSIONS_BY_STATUSES],
+            endpoint: api.fetchSubmissionsByStatuses(statuses),
+        },
+        statuses,
+    };
+};
+
+export const loadSubmissionsOfStatuses = statuses => (dispatch, getState) => {
+    const state = getState()
+    const submissions = getSubmissionsByGivenStatuses(statuses)(state)
+
+    if ( submissions && submissions.length !== 0 ) {
+        return null
+    }
+
+    return dispatch(fetchSubmissionsByStatuses(statuses))
+}
+
+const fetchSubmission = (submissionID) => ({
+    [CALL_API]: {
+        types: [ START_LOADING_SUBMISSION, UPDATE_SUBMISSION, FAIL_LOADING_SUBMISSION],
+        endpoint: api.fetchSubmission(submissionID),
+    },
+    submissionID,
+})
 
 export const loadCurrentSubmission = (requiredFields=[]) => (dispatch, getState) => {
     const submissionID = getCurrentSubmissionID(getState())
@@ -89,46 +158,16 @@ export const loadCurrentSubmission = (requiredFields=[]) => (dispatch, getState)
         return null
     }
 
-    return dispatch(fetchSubmission(getCurrentSubmissionID(getState())))
+    return dispatch(fetchSubmission(submissionID))
 }
 
 
-export const fetchSubmission = submissionID => {
-    return async function(dispatch) {
-
-        dispatch(startLoadingSubmission(submissionID));
-        try {
-            const response = await api.fetchSubmission(submissionID);
-            const json = await response.json();
-            if (response.ok) {
-                dispatch(updateSubmission(submissionID, json));
-            } else {
-                dispatch(failLoadingSubmission(json.meta.error));
-            }
-        } catch (e) {
-            dispatch(failLoadingSubmission(e.message));
-        }
-    };
-};
-
-
-const startLoadingSubmission = submissionID => ({
-    type: START_LOADING_SUBMISSION,
-    submissionID,
-});
-
-const failLoadingSubmission = submissionID => ({
-    type: FAIL_LOADING_SUBMISSION,
-    submissionID,
+export const clearCurrentSubmission = () => ({
+    type: CLEAR_CURRENT_SUBMISSION,
 });
 
-
-const updateSubmission = (submissionID, data) => ({
-    type: UPDATE_SUBMISSION,
+export const appendNoteIDForSubmission = (submissionID, noteID) => ({
+    type: ADD_NOTE_FOR_SUBMISSION,
     submissionID,
-    data,
-});
-
-export const clearCurrentSubmission = () => ({
-    type: CLEAR_CURRENT_SUBMISSION,
+    noteID,
 });
diff --git a/opentech/static_src/src/app/src/redux/middleware/api.js b/opentech/static_src/src/app/src/redux/middleware/api.js
new file mode 100644
index 0000000000000000000000000000000000000000..42878fd7415ddc712ccfb061d41496ba3c9813d0
--- /dev/null
+++ b/opentech/static_src/src/app/src/redux/middleware/api.js
@@ -0,0 +1,87 @@
+import { camelizeKeys, decamelizeKeys } from 'humps'
+
+import { apiFetch } from '@api/utils'
+
+const callApi = (endpoint) => {
+    // If body is an object, decamelize the keys.
+    const { options } = endpoint;
+    if (options !== undefined && typeof options.body === 'object') {
+        endpoint = {
+            ...endpoint,
+            options: {
+                ...options,
+                body: JSON.stringify(decamelizeKeys(options.body))
+            }
+        }
+    }
+
+    return apiFetch(endpoint)
+        .then(response =>
+              response.json().then(json => {
+                  if (!response.ok) {
+                      return Promise.reject({message: json.error})
+                  }
+                  return camelizeKeys(json)
+              })
+             )
+}
+
+
+export const CALL_API = 'Call API'
+
+
+// A Redux middleware that interprets actions with CALL_API info specified.
+// Performs the call and promises when such actions are dispatched.
+export default store => next => action => {
+    const callAPI = action[CALL_API]
+
+    if (callAPI === undefined) {
+        return next(action)
+    }
+
+    let { endpoint } = callAPI
+    const { types } = callAPI
+
+    if (typeof endpoint === 'function') {
+        endpoint = endpoint(store.getState())
+    }
+
+    if (typeof endpoint !== 'object' && typeof endpoint.path !== 'string') {
+        throw new Error('Specify a string endpoint URL.')
+    }
+
+    if (!Array.isArray(types) || types.length !== 3) {
+        throw new Error('Expected an array of three action types.')
+
+    }
+    if (!types.every(type => typeof type === 'string')) {
+        throw new Error('Expected action types to be strings.')
+    }
+
+    const actionWith = data => {
+        const finalAction = {...action, ...data}
+        delete finalAction[CALL_API]
+        return finalAction
+    }
+
+    const [ requestType, successType, failureType ] = types
+    next(actionWith({ type: requestType }))
+
+    return new Promise((resolve, reject) => {
+        return callApi(endpoint).then(
+            response => {
+                resolve();
+                return next(actionWith({
+                    data: response,
+                    type: successType
+                }))
+            },
+            error => {
+                reject();
+                return next(actionWith({
+                    type: failureType,
+                    error: error.message || 'Something bad happened'
+                }))
+            })
+    });
+}
diff --git a/opentech/static_src/src/app/src/redux/reducers/index.js b/opentech/static_src/src/app/src/redux/reducers/index.js
index d1e5e237467ee34a6e305045c469b99560e3e92b..f7e4cfa44051f3fffa375327372c6fb087a8d119 100644
--- a/opentech/static_src/src/app/src/redux/reducers/index.js
+++ b/opentech/static_src/src/app/src/redux/reducers/index.js
@@ -2,8 +2,10 @@ import { combineReducers } from 'redux'
 
 import submissions from '@reducers/submissions';
 import rounds from '@reducers/rounds';
+import notes from '@reducers/notes';
 
 export default combineReducers({
+    notes,
     submissions,
     rounds,
 });
diff --git a/opentech/static_src/src/app/src/redux/reducers/notes.js b/opentech/static_src/src/app/src/redux/reducers/notes.js
new file mode 100644
index 0000000000000000000000000000000000000000..4c4a196fde9887c9862d70f3e6a84b7bd6b682b3
--- /dev/null
+++ b/opentech/static_src/src/app/src/redux/reducers/notes.js
@@ -0,0 +1,116 @@
+import { combineReducers } from 'redux';
+
+import {
+    UPDATE_NOTE,
+    UPDATE_NOTES,
+    START_FETCHING_NOTES,
+    FAIL_FETCHING_NOTES,
+    START_CREATING_NOTE_FOR_SUBMISSION,
+    FAIL_CREATING_NOTE_FOR_SUBMISSION,
+} from '@actions/notes';
+
+function notesFetching(state = false, action) {
+    switch (action.type) {
+        case START_FETCHING_NOTES:
+            return true;
+        case UPDATE_NOTES:
+        case FAIL_FETCHING_NOTES:
+            return false;
+        default:
+            return state;
+    }
+}
+
+function notesErrored(state = false, action) {
+    switch (action.type) {
+        case UPDATE_NOTES:
+        case START_FETCHING_NOTES:
+            return false;
+        case FAIL_FETCHING_NOTES:
+            return true;
+        default:
+            return state;
+    }
+}
+
+function note(state, action) {
+    switch (action.type) {
+        case UPDATE_NOTE:
+            return {
+                ...state,
+                ...action.data,
+            };
+        default:
+            return state;
+    }
+}
+
+function notesCreating(state = [], action) {
+    switch (action.type) {
+        case START_CREATING_NOTE_FOR_SUBMISSION:
+            return [
+                ...state,
+                action.submissionID,
+            ];
+        case UPDATE_NOTE:
+        case FAIL_CREATING_NOTE_FOR_SUBMISSION:
+            return state.filter(v => v !== action.submissionID);
+        default:
+            return state
+    }
+}
+
+
+function notesFailedCreating(state = {}, action) {
+    switch (action.type) {
+        case UPDATE_NOTE:
+        case START_CREATING_NOTE_FOR_SUBMISSION:
+            return Object.entries(state).reduce((acc, [k, v]) => {
+                if (parseInt(k) !== action.submissionID) {
+                    acc[k] = v;
+                }
+                return acc;
+            }, {});
+        case FAIL_CREATING_NOTE_FOR_SUBMISSION:
+            return {
+                ...state,
+                [action.submissionID]: action.error,
+            };
+        default:
+            return state
+    }
+}
+
+function notesByID(state = {}, action) {
+    switch (action.type) {
+        case UPDATE_NOTES:
+            return {
+                ...state,
+                ...action.data.results.reduce((newNotesAccumulator, newNote) => {
+                    newNotesAccumulator[newNote.id] = note(state[newNote.id], {
+                        type: UPDATE_NOTE,
+                        data: newNote,
+                    });
+                    return newNotesAccumulator;
+                }, {}),
+            };
+        case UPDATE_NOTE:
+            return {
+                ...state,
+                [action.data.id]: note(state[action.data.id], {
+                    type: UPDATE_NOTE,
+                    data: action.data,
+                }),
+            };
+        default:
+            return state;
+    }
+}
+
+export default combineReducers({
+    byID: notesByID,
+    isFetching: notesFetching,
+    isErrored: notesErrored,
+    createError: notesFailedCreating,
+    isCreating: notesCreating,
+});
diff --git a/opentech/static_src/src/app/src/redux/reducers/rounds.js b/opentech/static_src/src/app/src/redux/reducers/rounds.js
index 0f4253b713fa386196a99315fa6221bb47e12630..fd7117a1978323b9fbd9b384497ddbf92746ae20 100644
--- a/opentech/static_src/src/app/src/redux/reducers/rounds.js
+++ b/opentech/static_src/src/app/src/redux/reducers/rounds.js
@@ -5,16 +5,22 @@ import {
     SET_CURRENT_SUBMISSION_ROUND,
     START_LOADING_SUBMISSIONS_BY_ROUND,
     UPDATE_SUBMISSIONS_BY_ROUND,
+    FAIL_LOADING_ROUND,
+    START_LOADING_ROUND,
+    UPDATE_ROUND,
+    UPDATE_ROUNDS,
+    FAIL_LOADING_ROUNDS,
+    START_LOADING_ROUNDS,
 } from '@actions/submissions';
 
+const submissionsDefaultState = {ids: [], isFetching: false};
 
-function round(state={id: null, submissions: [], isFetching: false}, action) {
-    switch(action.type) {
+function submissions(state=submissionsDefaultState, action) {
+    switch (action.type) {
         case UPDATE_SUBMISSIONS_BY_ROUND:
             return {
                 ...state,
-                id: action.roundID,
-                submissions: action.data.results.map(submission => submission.id),
+                ids: action.data.results.map(submission => submission.id),
                 isFetching: false,
             };
         case FAIL_LOADING_SUBMISSIONS_BY_ROUND:
@@ -22,7 +28,39 @@ function round(state={id: null, submissions: [], isFetching: false}, action) {
                 ...state,
                 isFetching: false,
             };
+        case START_LOADING_SUBMISSIONS_BY_ROUND:
+            return {
+                ...state,
+                isFetching: true,
+            };
+        default:
+            return state;
+    }
+}
+
+
+function round(state={id: null, submissions: submissionsDefaultState, isFetching: false, workflow: []}, action) {
+    switch(action.type) {
+        case UPDATE_SUBMISSIONS_BY_ROUND:
+        case FAIL_LOADING_SUBMISSIONS_BY_ROUND:
         case START_LOADING_SUBMISSIONS_BY_ROUND:
+            return {
+                ...state,
+                id: action.roundID,
+                submissions: submissions(state.submissions, action),
+            };
+        case UPDATE_ROUND:
+            return {
+                ...state,
+                ...action.data,
+                isFetching: false,
+            };
+        case FAIL_LOADING_ROUND:
+            return {
+                ...state,
+                isFetching: false,
+            };
+        case START_LOADING_ROUND:
             return {
                 ...state,
                 id: action.roundID,
@@ -39,10 +77,24 @@ function roundsByID(state = {}, action) {
         case UPDATE_SUBMISSIONS_BY_ROUND:
         case FAIL_LOADING_SUBMISSIONS_BY_ROUND:
         case START_LOADING_SUBMISSIONS_BY_ROUND:
+        case UPDATE_ROUND:
+        case START_LOADING_ROUND:
+        case FAIL_LOADING_ROUND:
             return {
                 ...state,
                 [action.roundID]: round(state[action.roundID], action)
             };
+        case UPDATE_ROUNDS:
+            return {
+                ...state,
+                ...action.data.results.reduce((acc, value) => {
+                    acc[value.id] = round(state[value.id], {
+                        type: UPDATE_ROUND,
+                        data: value
+                    });
+                    return acc;
+                }, {}),
+            };
         default:
             return state;
     }
@@ -52,9 +104,12 @@ function roundsByID(state = {}, action) {
 function errorMessage(state = null, action) {
     switch(action.type) {
     case FAIL_LOADING_SUBMISSIONS_BY_ROUND:
+    case FAIL_LOADING_ROUND:
         return action.message;
     case UPDATE_SUBMISSIONS_BY_ROUND:
     case START_LOADING_SUBMISSIONS_BY_ROUND:
+    case UPDATE_ROUND:
+    case START_LOADING_ROUND:
         return null;
     default:
         return state;
@@ -62,6 +117,30 @@ function errorMessage(state = null, action) {
 
 }
 
+function roundsErrored(state = false, action) {
+    switch (action.type) {
+        case START_LOADING_ROUNDS:
+        case UPDATE_ROUNDS:
+            return false;
+        case FAIL_LOADING_ROUNDS:
+            return true;
+        default:
+            return state;
+    }
+}
+
+function roundsFetching(state = false, action) {
+    switch (action.type) {
+        case FAIL_LOADING_ROUNDS:
+        case UPDATE_ROUNDS:
+            return false;
+        case START_LOADING_ROUNDS:
+            return true;
+        default:
+            return state;
+    }
+}
+
 
 function currentRound(state = null, action) {
     switch(action.type) {
@@ -77,6 +156,8 @@ const rounds = combineReducers({
     byID: roundsByID,
     current: currentRound,
     error: errorMessage,
+    isFetching: roundsFetching,
+    isErrored: roundsErrored,
 });
 
 export default rounds;
diff --git a/opentech/static_src/src/app/src/redux/reducers/submissions.js b/opentech/static_src/src/app/src/redux/reducers/submissions.js
index 7563d1e25f0ac19364f82d0c96af3624f2125d4c..946039d231a22cffcd039e6b96c4c212fd94df09 100644
--- a/opentech/static_src/src/app/src/redux/reducers/submissions.js
+++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js
@@ -7,8 +7,13 @@ import {
     UPDATE_SUBMISSIONS_BY_ROUND,
     UPDATE_SUBMISSION,
     SET_CURRENT_SUBMISSION,
+    UPDATE_SUBMISSIONS_BY_STATUSES,
+    START_LOADING_SUBMISSIONS_BY_STATUSES,
+    FAIL_LOADING_SUBMISSIONS_BY_STATUSES,
 } from '@actions/submissions';
 
+import { UPDATE_NOTES, UPDATE_NOTE } from '@actions/notes'
+
 
 function submission(state, action) {
     switch(action.type) {
@@ -31,6 +36,19 @@ function submission(state, action) {
                 isFetching: false,
                 isErrored: false,
             };
+        case UPDATE_NOTES:
+            return {
+                ...state,
+                comments: action.data.results.map(note => note.id),
+            };
+        case UPDATE_NOTE:
+            return {
+                ...state,
+                comments: [
+                    action.data.id,
+                    ...(state.comments || []),
+                ]
+            };
         default:
             return state;
     }
@@ -42,10 +60,13 @@ function submissionsByID(state = {}, action) {
         case START_LOADING_SUBMISSION:
         case FAIL_LOADING_SUBMISSION:
         case UPDATE_SUBMISSION:
+        case UPDATE_NOTE:
+        case UPDATE_NOTES:
             return {
                 ...state,
                 [action.submissionID]: submission(state[action.submissionID], action),
             };
+        case UPDATE_SUBMISSIONS_BY_STATUSES:
         case UPDATE_SUBMISSIONS_BY_ROUND:
             return {
                 ...state,
@@ -78,9 +99,52 @@ function currentSubmission(state = null, action) {
 }
 
 
+function submissionsByStatuses(state = {}, action) {
+    switch (action.type) {
+        case UPDATE_SUBMISSIONS_BY_STATUSES:
+            return {
+                ...state,
+                ...action.data.results.reduce((accumulator, submission) => {
+                    const submissions = accumulator[submission.status] || []
+                    if ( !submissions.includes(submission.id) ) {
+                        accumulator[submission.status] = [...submissions, submission.id]
+                    }
+                    return state
+                }, state)
+            };
+        default:
+            return state
+    }
+}
+
+
+function submissionsFetchingState(state = {isFetching: true, isError: false}, action) {
+    switch (action.type) {
+        case FAIL_LOADING_SUBMISSIONS_BY_STATUSES:
+            return {
+                isFetching: false,
+                isErrored: true,
+            };
+        case START_LOADING_SUBMISSIONS_BY_STATUSES:
+            return {
+                isFetching: true,
+                isErrored: false,
+            };
+        case UPDATE_SUBMISSIONS_BY_STATUSES:
+            return {
+                isFetching: true,
+                isErrored: false,
+            };
+        default:
+            return state
+    }
+}
+
 const submissions = combineReducers({
     byID: submissionsByID,
     current: currentSubmission,
+    byStatuses: submissionsByStatuses,
+    fetchingState: submissionsFetchingState,
 });
 
 export default submissions;
diff --git a/opentech/static_src/src/app/src/redux/selectors/notes.js b/opentech/static_src/src/app/src/redux/selectors/notes.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd035999ff96834f3262b618a24f43750aa251b8
--- /dev/null
+++ b/opentech/static_src/src/app/src/redux/selectors/notes.js
@@ -0,0 +1,30 @@
+import { createSelector } from 'reselect';
+
+import { getSubmissionOfID } from '@selectors/submissions';
+
+const getNotes = state => state.notes.byID;
+
+export const getNoteOfID = (noteID) => createSelector(
+    [getNotes], notes => notes[noteID]
+);
+
+export const getNotesFetchState = state => state.notes.isFetching === true;
+
+export const getNotesErrorState = state => state.notes.isErrored === true;
+
+export const getNoteIDsForSubmissionOfID = submissionID => createSelector(
+    [getSubmissionOfID(submissionID)],
+    submission => (submission || {}).comments || []
+);
+
+const getNoteCreatingErrors = state => state.notes.createError;
+
+export const getNoteCreatingErrorForSubmission = submissionID => createSelector(
+    [getNoteCreatingErrors], errors => errors[submissionID]
+);
+
+const getNoteCreatingState = state => state.notes.isCreating;
+
+export const getNoteCreatingStateForSubmission = submissionID => createSelector(
+    [getNoteCreatingState], creatingStates => creatingStates.includes(submissionID)
+);
diff --git a/opentech/static_src/src/app/src/redux/selectors/rounds.js b/opentech/static_src/src/app/src/redux/selectors/rounds.js
new file mode 100644
index 0000000000000000000000000000000000000000..05a7c166b677bb310bab8ae402fa58b87dd3d18f
--- /dev/null
+++ b/opentech/static_src/src/app/src/redux/selectors/rounds.js
@@ -0,0 +1,33 @@
+import { createSelector } from 'reselect';
+
+const getRounds = state => state.rounds.byID;
+
+const getCurrentRoundID = state => state.rounds.current;
+
+const getCurrentRound = createSelector(
+    [ getCurrentRoundID, getRounds],
+    (id, rounds) => {
+        return rounds[id];
+    }
+);
+
+const getCurrentRoundSubmissionIDs = createSelector(
+    [ getCurrentRound ],
+    (round) => {
+        return round ? round.submissions.ids : [];
+    }
+);
+
+const getRoundsFetching = state => state.rounds.isFetching === true;
+
+const getRoundsErrored = state => state.rounds.isErrored === true;
+
+export {
+    getRounds,
+    getCurrentRound,
+    getCurrentRoundID,
+    getCurrentRoundSubmissionIDs,
+    getRoundsErrored,
+    getRoundsFetching,
+};
+
diff --git a/opentech/static_src/src/app/src/redux/selectors/submissions.js b/opentech/static_src/src/app/src/redux/selectors/submissions.js
index 09124b6896da42c2c177f2e706a8b2528deaca54..643684d2e8d4648477eb560e69e25be39dde5f11 100644
--- a/opentech/static_src/src/app/src/redux/selectors/submissions.js
+++ b/opentech/static_src/src/app/src/redux/selectors/submissions.js
@@ -1,26 +1,45 @@
 import { createSelector } from 'reselect';
 
+import {
+    getCurrentRound,
+    getCurrentRoundID,
+    getCurrentRoundSubmissionIDs,
+    getRounds,
+} from '@selectors/rounds';
+
 const getSubmissions = state => state.submissions.byID;
 
-const getRounds = state => state.rounds.byID;
+const getSubmissionsByStatuses = state => state.submissions.byStatuses;
 
-const getCurrentRoundID = state => state.rounds.current;
+const getSubmissionsFetchingState = state => state.submissions.fetchingState;
 
-const getCurrentRound = createSelector(
-    [ getCurrentRoundID, getRounds],
-    (id, rounds) => {
-        return rounds[id];
+const getCurrentSubmissionID = state => state.submissions.current;
+
+const getByGivenStatusesObject = statuses => createSelector(
+    [getSubmissionsByStatuses], (byStatuses) => {
+        return statuses.reduce((acc, status) => acc.concat(byStatuses[status] || []), [])
     }
 );
 
-const getCurrentSubmissionID = state => state.submissions.current;
+const getSubmissionsByGivenStatuses = statuses => createSelector(
+    [getSubmissions, getByGivenStatusesObject(statuses)],
+    (submissions, byStatus) => byStatus.map(id => submissions[id])
+);
+
+const getByGivenStatusesError = statuses => createSelector(
+    [getSubmissionsFetchingState],
+    state => state.isErrored === true
+);
 
+const getByGivenStatusesLoading = statuses => createSelector(
+    [getByGivenStatusesObject],
+    state => state.isFetching === true
+);
 
 const getCurrentRoundSubmissions = createSelector(
-    [ getCurrentRound, getSubmissions],
-    (round, submissions) => {
-        const roundSubmissions = round ? round.submissions : [];
-        return roundSubmissions.map(submissionID => submissions[submissionID]);
+    [ getCurrentRoundSubmissionIDs, getSubmissions],
+    (submissionIDs, submissions) => {
+        return submissionIDs.map(submissionID => submissions[submissionID]);
     }
 );
 
@@ -32,6 +51,10 @@ const getCurrentSubmission = createSelector(
     }
 );
 
+const getSubmissionOfID = (submissionID) => createSelector(
+    [getSubmissions], submissions => submissions[submissionID]
+);
+
 const getSubmissionLoadingState = state => state.submissions.itemLoading === true;
 
 const getSubmissionErrorState = state => state.submissions.itemLoadingError === true;
@@ -41,13 +64,19 @@ const getSubmissionsByRoundError = state => state.rounds.error;
 const getSubmissionsByRoundLoadingState = state => state.submissions.itemsLoading === true;
 
 export {
+    getByGivenStatusesError,
+    getByGivenStatusesLoading,
     getCurrentRoundID,
     getCurrentRound,
+    getCurrentRoundSubmissionIDs,
     getCurrentRoundSubmissions,
     getCurrentSubmission,
     getCurrentSubmissionID,
+    getRounds,
     getSubmissionsByRoundError,
     getSubmissionsByRoundLoadingState,
     getSubmissionLoadingState,
     getSubmissionErrorState,
+    getSubmissionOfID,
+    getSubmissionsByGivenStatuses,
 };
diff --git a/opentech/static_src/src/app/src/redux/store.js b/opentech/static_src/src/app/src/redux/store.js
index ed0b719d6c0ff1968140fc66689ce15d53ddaa07..a7002461574d3319ee31b524868b725189d11b00 100644
--- a/opentech/static_src/src/app/src/redux/store.js
+++ b/opentech/static_src/src/app/src/redux/store.js
@@ -4,9 +4,11 @@ import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'
 import logger from 'redux-logger'
 
 import rootReducer from '@reducers';
+import api from '@middleware/api'
 
 const MIDDLEWARE = [
     ReduxThunk,
+    api,
 ];
 
 if (process.env.NODE_ENV === 'development') {
diff --git a/opentech/static_src/src/app/src/index.js b/opentech/static_src/src/app/src/submissionsByRoundIndex.js
similarity index 100%
rename from opentech/static_src/src/app/src/index.js
rename to opentech/static_src/src/app/src/submissionsByRoundIndex.js
diff --git a/opentech/static_src/src/app/src/submissionsByStatusIndex.js b/opentech/static_src/src/app/src/submissionsByStatusIndex.js
new file mode 100644
index 0000000000000000000000000000000000000000..e492499ee6a10917cf0f45a021908941ed5f752a
--- /dev/null
+++ b/opentech/static_src/src/app/src/submissionsByStatusIndex.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
+
+import SubmissionsByStatusApp from './SubmissionsByStatusApp';
+import createStore from '@redux/store';
+
+
+const container = document.getElementById('submissions-by-status-react-app');
+
+const store = createStore();
+
+ReactDOM.render(
+    <Provider store={store}>
+        <SubmissionsByStatusApp pageContent={container.innerHTML} statuses={container.dataset.statuses.split(',')} />
+    </Provider>,
+    container
+);
diff --git a/opentech/static_src/src/app/webpack.base.config.js b/opentech/static_src/src/app/webpack.base.config.js
index b6017a03190fc4870e014265d069f79bdd48d734..9942fe66aa894b2721a11ed82524f93bac79f9d1 100644
--- a/opentech/static_src/src/app/webpack.base.config.js
+++ b/opentech/static_src/src/app/webpack.base.config.js
@@ -1,9 +1,14 @@
 var path = require('path');
 
+var COMMON_ENTRY = ['@babel/polyfill', './src/datetime']
+
 module.exports = {
     context: __dirname,
 
-    entry: ['@babel/polyfill', './src/index'],
+    entry: {
+        submissionsByRound: COMMON_ENTRY.concat(['./src/submissionsByRoundIndex']),
+        submissionsByStatus: COMMON_ENTRY.concat(['./src/submissionsByStatusIndex']),
+    },
 
     output: {
         filename: '[name]-[hash].js'
@@ -71,6 +76,7 @@ module.exports = {
             '@reducers': path.resolve(__dirname, 'src/redux/reducers'),
             '@selectors': path.resolve(__dirname, 'src/redux/selectors'),
             '@actions': path.resolve(__dirname, 'src/redux/actions'),
+            '@middleware': path.resolve(__dirname, 'src/redux/middleware'),
             '@api': path.resolve(__dirname, 'src/api'),
         }
     }
diff --git a/opentech/static_src/src/app/webpack.dev.config.js b/opentech/static_src/src/app/webpack.dev.config.js
index e609036e52110ccc23ef1cc93158c14893846966..9f5d6d2b0a140c2ae27c3c5e1dec84003c2e234e 100644
--- a/opentech/static_src/src/app/webpack.dev.config.js
+++ b/opentech/static_src/src/app/webpack.dev.config.js
@@ -25,7 +25,9 @@ config.devServer = {
         'Access-Control-Allow-Headers': '*',
     },
     hotOnly: true,
-    port: 3000
+    host: 'localhost',
+    port: 3000,
+    overlay: true
 }
 
 config.devtool = 'source-map'
diff --git a/opentech/static_src/src/images/add-person.svg b/opentech/static_src/src/images/add-person.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4d2cd548b3494d17e1ba5a881de76ddd8fb753e6
--- /dev/null
+++ b/opentech/static_src/src/images/add-person.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewbox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#0d7db0" d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
diff --git a/opentech/static_src/src/images/arrow-split.svg b/opentech/static_src/src/images/arrow-split.svg
new file mode 100644
index 0000000000000000000000000000000000000000..27ba4b500c1fd7fd6b54c479e9769ca4b4b82ef3
--- /dev/null
+++ b/opentech/static_src/src/images/arrow-split.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewbox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#0d7db0" d="M14 4l2.29 2.29-2.88 2.88 1.42 1.42 2.88-2.88L20 10V4zm-4 0H4v6l2.29-2.29 4.71 4.7V20h2v-8.41l-5.29-5.3z"/></svg>
diff --git a/opentech/static_src/src/images/note.svg b/opentech/static_src/src/images/note.svg
new file mode 100644
index 0000000000000000000000000000000000000000..08515968b2079e49ab13d11bb269dac6e3136650
--- /dev/null
+++ b/opentech/static_src/src/images/note.svg
@@ -0,0 +1 @@
+<svg width="74" height="68" xmlns="http://www.w3.org/2000/svg"><g fill="#000" fill-rule="evenodd"><path d="M41.454 22H10.545C9.692 22 9 22.672 9 23.5c0 .829.692 1.5 1.545 1.5h30.909c.853 0 1.546-.671 1.546-1.5 0-.828-.693-1.5-1.546-1.5z"/><path d="M45.882 1.545c0-.853-.686-1.545-1.53-1.545-.845 0-1.528.692-1.528 1.545v3.19h-6.119v-3.19C36.705.692 36.021 0 35.176 0c-.844 0-1.53.692-1.53 1.545v3.19H27.53v-3.19c0-.853-.684-1.545-1.53-1.545-.844 0-1.53.692-1.53 1.545v3.19h-6.116v-3.19c0-.853-.685-1.545-1.53-1.545-.844 0-1.53.692-1.53 1.545v3.19H9.177v-3.19C9.176.692 8.492 0 7.647 0c-.844 0-1.53.692-1.53 1.545v3.19H0V68h52V4.735h-6.118v-3.19zm3.06 6.281V64.91H3.059V7.826h3.058v2.992c0 .853.686 1.545 1.53 1.545.845 0 1.53-.692 1.53-1.545V7.826h6.117v2.992c0 .853.685 1.545 1.53 1.545.844 0 1.529-.692 1.529-1.545V7.826h6.117v2.992c0 .853.685 1.545 1.53 1.545s1.529-.692 1.529-1.545V7.826h6.117v2.992c0 .853.686 1.545 1.53 1.545.845 0 1.53-.691 1.53-1.545V7.826h6.118v2.992c0 .853.683 1.545 1.528 1.545.844 0 1.53-.692 1.53-1.545V7.826h3.06zM74 12.21C74 8.78 71.315 6 68.001 6 64.685 6 62 8.78 62 12.21v41.92h1.861L62 60.86 68.001 65 74 60.86l-1.861-6.73H74V12.21zm-9 0C65 10.5 66.345 9.106 68 9.106c1.653 0 3 1.393 3 3.104v38.816h-6.002V12.21zm5.513 47.323L68 61.268l-2.514-1.735 1.495-5.402h2.036l1.495 5.402z" fill-rule="nonzero"/><path d="M41.454 31H10.545C9.692 31 9 31.672 9 32.5c0 .829.692 1.5 1.545 1.5h30.909c.853 0 1.546-.671 1.546-1.5 0-.828-.693-1.5-1.546-1.5zM41.454 40H10.545C9.692 40 9 40.672 9 41.5c0 .829.692 1.5 1.545 1.5h30.909c.853 0 1.546-.671 1.546-1.5 0-.828-.693-1.5-1.546-1.5zM26.417 50H10.583C9.709 50 9 50.672 9 51.5c0 .829.709 1.5 1.583 1.5h15.834c.874 0 1.583-.671 1.583-1.5 0-.828-.709-1.5-1.583-1.5z"/></g></svg>
\ No newline at end of file
diff --git a/opentech/static_src/src/images/sad-note.svg b/opentech/static_src/src/images/sad-note.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7e60dfeb78eb23ed89b88102865261bf43125f3e
--- /dev/null
+++ b/opentech/static_src/src/images/sad-note.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 74 68"><g fill="#000" fill-rule="evenodd"><path d="M45.882 1.545c0-.853-.686-1.545-1.53-1.545-.845 0-1.528.692-1.528 1.545v3.19h-6.119v-3.19C36.705.692 36.021 0 35.176 0c-.844 0-1.53.692-1.53 1.545v3.19H27.53v-3.19c0-.853-.684-1.545-1.53-1.545-.844 0-1.53.692-1.53 1.545v3.19h-6.116v-3.19c0-.853-.685-1.545-1.53-1.545-.844 0-1.53.692-1.53 1.545v3.19H9.177v-3.19C9.176.692 8.492 0 7.647 0c-.844 0-1.53.692-1.53 1.545v3.19H0V68h52V4.735h-6.118v-3.19zm3.06 6.281V64.91H3.059V7.826h3.058v2.992c0 .853.686 1.545 1.53 1.545.845 0 1.53-.692 1.53-1.545V7.826h6.117v2.992c0 .853.685 1.545 1.53 1.545.844 0 1.529-.692 1.529-1.545V7.826h6.117v2.992c0 .853.685 1.545 1.53 1.545s1.529-.692 1.529-1.545V7.826h6.117v2.992c0 .853.686 1.545 1.53 1.545.845 0 1.53-.691 1.53-1.545V7.826h6.118v2.992c0 .853.683 1.545 1.528 1.545.844 0 1.53-.692 1.53-1.545V7.826h3.06zM74 12.21C74 8.78 71.315 6 68.001 6 64.685 6 62 8.78 62 12.21v41.92h1.861L62 60.86 68.001 65 74 60.86l-1.861-6.73H74V12.21zm-9 0C65 10.5 66.345 9.106 68 9.106c1.653 0 3 1.393 3 3.104v38.816h-6.002V12.21zm5.513 47.323L68 61.268l-2.514-1.735 1.495-5.402h2.036l1.495 5.402z" fill-rule="nonzero"/><path d="M34.094 37.953a1.5 1.5 0 1 1-2.41 1.787 7.985 7.985 0 0 0-6.43-3.24 7.978 7.978 0 0 0-6.11 2.834 1.5 1.5 0 1 1-2.289-1.938 10.977 10.977 0 0 1 8.399-3.896c3.527 0 6.779 1.674 8.84 4.453z" fill-rule="nonzero"/><circle cx="19" cy="26" r="2"/><circle cx="32" cy="26" r="2"/></g></svg>
diff --git a/opentech/static_src/src/javascript/apply/activity-feed.js b/opentech/static_src/src/javascript/apply/activity-feed.js
index 51ebf8839af492c2a2f17dea70d552a860852300..bedfb677d04535df7d04a62a98e6c827ef023d21 100644
--- a/opentech/static_src/src/javascript/apply/activity-feed.js
+++ b/opentech/static_src/src/javascript/apply/activity-feed.js
@@ -27,7 +27,10 @@
     });
 
     // Scroll to the top of the activity feed
-    $('.js-to-top').click(() => $('.js-activity-feed').animate({scrollTop: 0}, 250));
+    $('.js-to-top').click((e) => {
+        e.preventDefault();
+        $('.js-activity-feed').animate({scrollTop: 0}, 250);
+    });
 
     // Collaps long comments in activity feed.
     $('.feed__item').each(function () {
diff --git a/opentech/static_src/src/javascript/apply/batch-actions.js b/opentech/static_src/src/javascript/apply/batch-actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..6d2af16f8d6269e0b28086a1440e76f8f18a2fab
--- /dev/null
+++ b/opentech/static_src/src/javascript/apply/batch-actions.js
@@ -0,0 +1,108 @@
+(function ($) {
+
+    'use strict';
+
+    const $body = $('body');
+    const $checkbox = $('.js-batch-select');
+    const $allCheckboxInput = $('.js-batch-select-all');
+    const $batchReviewersButton = $('.js-batch-update-reviewers');
+    const $batchTitlesList = $('.js-batch-titles');
+    const $batchTitleCount = $('.js-batch-title-count');
+    const $hiddenIDlist = $('#id_submission_ids');
+    const $toggleBatchList = $('.js-toggle-batch-list');
+    const activeClass = 'batch-actions-enabled';
+    const closedClass = 'is-closed';
+
+    $(window).on('load', function () {
+        toggleBatchActions();
+        updateCount();
+    });
+
+    $allCheckboxInput.change(function () {
+        if ($(this).is(':checked')) {
+            $checkbox.each(function () {
+                this.checked = true;
+            });
+        }
+        else {
+            $checkbox.each(function () {
+                this.checked = false;
+            });
+        }
+
+        toggleBatchActions();
+        updateCount();
+    });
+
+    $checkbox.change(function () {
+        // see how many checkboxes are :checked
+        toggleBatchActions();
+
+        // updates selected checbox count
+        updateCount();
+
+        // reset the check all input
+        if (!$(this).is(':checked') && $allCheckboxInput.is(':checked')) {
+            resetCheckAllInput();
+        }
+    });
+
+    // append selected project titles to batch update reviewer modal
+    $batchReviewersButton.click(function () {
+        $batchTitlesList.html('');
+        $batchTitleCount.html('');
+        $batchTitlesList.addClass(closedClass);
+        $toggleBatchList.html('Show');
+
+        let selectedIDs = [];
+
+        $checkbox.each(function () {
+            if ($(this).is(':checked')) {
+                const href = $(this).parents('tr').find('.js-title').find('a').attr('href');
+                const title = $(this).parents('tr').find('.js-title').data('tooltip');
+
+                $batchTitlesList.append(`
+                    <a href="${href}" class="modal__list-item" target="_blank" rel="noopener noreferrer" title="${title}">
+                        ${title}
+                        <svg class="modal__open-link-icon"><use xlink:href="#open-in-new-tab"></use></svg>
+                    </a>
+                `);
+                selectedIDs.push($(this).parents('tr').data('record-id'));
+            }
+        });
+
+        $batchTitleCount.append(`${selectedIDs.length} submissions selected`);
+        $hiddenIDlist.val(selectedIDs.join(','));
+    });
+
+    // show/hide the list of actions
+    $toggleBatchList.click(e => {
+        e.preventDefault();
+
+        if ($('.js-batch-titles').hasClass(closedClass)) {
+            $toggleBatchList.html('Hide');
+        }
+        else {
+            $toggleBatchList.html('Show');
+        }
+
+        $batchTitlesList.toggleClass(closedClass);
+    });
+
+    function toggleBatchActions() {
+        if ($('.js-batch-select:checked').length) {
+            $body.addClass(activeClass);
+        }
+        else {
+            $body.removeClass(activeClass);
+        }
+    }
+
+    function updateCount() {
+        $('.js-total-actions').html($('.js-batch-select:checked').length);
+    }
+
+    function resetCheckAllInput() {
+        $allCheckboxInput.prop('checked', false);
+    }
+})(jQuery);
diff --git a/opentech/static_src/src/sass/apply/abstracts/_mixins.scss b/opentech/static_src/src/sass/apply/abstracts/_mixins.scss
index a4ac8a42d3d28b1fc951d0226fb1a445087cdc1e..ed0308e137cac385d8899c56d96e7b2f0d8f30b4 100644
--- a/opentech/static_src/src/sass/apply/abstracts/_mixins.scss
+++ b/opentech/static_src/src/sass/apply/abstracts/_mixins.scss
@@ -209,3 +209,35 @@
         }
     }
 }
+
+@mixin column-scrolling {
+    @include media-query(tablet-landscape) {
+        height: calc(100vh - var(--header-admin-height) - #{$listing-header-height});
+        overflow-y: scroll;
+    }
+
+    @include media-query(laptop-short) {
+        // allow for vertical scrolling on laptops
+        height: calc(100vh -  #{$listing-header-height});
+    }
+}
+
+@mixin checkbox-without-label {
+    input[type='checkbox'] {
+        margin: 0 auto;
+        display: block;
+        width: 20px;
+        height: 20px;
+        border: 1px solid $color--mid-grey;
+        -webkit-appearance: none; // sass-lint:disable-line no-vendor-prefixes
+        -moz-appearance: none; // sass-lint:disable-line no-vendor-prefixes
+        appearance: none;
+        background-color: $color--white;
+
+        &:checked {
+            background: url('./../../images/tick.svg') $color--dark-blue center no-repeat;
+            background-size: 12px;
+            border: 1px solid $color--dark-blue;
+        }
+    }
+}
diff --git a/opentech/static_src/src/sass/apply/abstracts/_variables.scss b/opentech/static_src/src/sass/apply/abstracts/_variables.scss
index c4e87dfd68b25855e2d32040473b05eebd7d5b49..3e4831ec425eefa82cd0bb48d50267fc1efc503e 100644
--- a/opentech/static_src/src/sass/apply/abstracts/_variables.scss
+++ b/opentech/static_src/src/sass/apply/abstracts/_variables.scss
@@ -1,6 +1,8 @@
 // sass-lint:disable no-color-keywords; no-color-hex
+// Set via JS
 :root {
     --header-admin-height: 0;
+    --last-listing-item-height: 0;
 }
 
 // Default
@@ -11,7 +13,6 @@ $color--light-grey: #f7f7f7;
 $color--light-mid-grey: #e8e8e8;
 $color--mid-grey: #cfcfcf;
 $color--mid-dark-grey: #919191;
-$color--dark-grey: #404041;
 
 // Brand
 $color--light-blue: #0d7db0;
diff --git a/opentech/static_src/src/sass/apply/components/_actions-bar.scss b/opentech/static_src/src/sass/apply/components/_actions-bar.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e60aea02d6b353dda695cd0b54b49a9ae2330f2f
--- /dev/null
+++ b/opentech/static_src/src/sass/apply/components/_actions-bar.scss
@@ -0,0 +1,54 @@
+.actions-bar {
+    margin: 20px 0;
+    width: 100%;
+
+    @include media-query(tablet-landscape) {
+        display: flex;
+        justify-content: space-between;
+    }
+
+    &__inner {
+        & > * {
+            margin-bottom: 20px;
+        }
+
+        @include media-query(tablet-landscape) {
+            display: flex;
+            align-items: center;
+
+            & > * {
+                margin: 0 0 0 20px;
+
+                &:first-child {
+                    margin-left: 0;
+                }
+            }
+        }
+
+        &--left {
+            display: none;
+
+            @include media-query(tablet-landscape) {
+                display: flex;
+                opacity: 0;
+                pointer-events: none;
+                transition: opacity $quick-transition;
+
+                .batch-actions-enabled & {
+                    opacity: 1;
+                    pointer-events: all;
+                }
+            }
+        }
+    }
+
+    &__total {
+        background-color: $color--light-blue;
+        color: $color--white;
+        padding: 6px 16px;
+        border-radius: 30px;
+        min-width: 120px;
+        text-align: center;
+        font-weight: $weight--semibold;
+    }
+}
diff --git a/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss b/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss
index 81df77c7fe8888b7d61db64eda57aaaaa8f5850f..f7e6b5fe2420ffff8aecb5754eed5e70002a72cf 100644
--- a/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss
+++ b/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss
@@ -24,7 +24,7 @@
             &.title {
                 @include media-query($table-breakpoint) {
                     width: 130px;
-                    padding-left: 20px;
+                    padding-left: 10px;
                 }
 
                 @include media-query(desktop) {
@@ -37,6 +37,14 @@
                     width: 150px;
                 }
             }
+
+            &.selected {
+                @include checkbox-without-label;
+
+                @include media-query($table-breakpoint) {
+                    width: 60px;
+                }
+            }
         }
 
         tr {
@@ -57,13 +65,12 @@
                 @include media-query($table-breakpoint) {
                     display: flex;
                     align-items: center;
-                    padding-top: 20px;
+                    padding-top: 10px;
                     padding-left: 10px;
                 }
 
                 @include media-query(desktop) {
                     display: table-cell;
-                    padding-left: 20px;
                 }
 
                 &.has-tooltip {
@@ -142,6 +149,16 @@
                 }
             }
 
+            // batch action checkboxes
+            &.selected {
+                @include checkbox-without-label;
+                display: none;
+
+                @include media-query($table-breakpoint) {
+                    display: table-cell;
+                }
+            }
+
             // arrow to toggle project info - added via js
             @include media-query($table-breakpoint) {
                 .arrow {
diff --git a/opentech/static_src/src/sass/apply/components/_button.scss b/opentech/static_src/src/sass/apply/components/_button.scss
index 5ebcce8979c9328a65d199918924f6676d478454..1b35fecaee3e34a6f7c3126a87de812b780706dc 100644
--- a/opentech/static_src/src/sass/apply/components/_button.scss
+++ b/opentech/static_src/src/sass/apply/components/_button.scss
@@ -80,25 +80,10 @@
         width: 100%;
 
         @include media-query(tablet-landscape) {
-            background: none;
-            padding: 0 10px;
-            border: 0;
-            align-items: center;
-            justify-content: flex-start;
-            max-width: initial;
-            width: auto;
-
             &::before {
                 content: '';
                 background-image: url('./../../images/filters.svg');
-                background-color: transparent;
-                background-position: left center;
                 transform: rotate(90deg);
-                background-size: 20px;
-                width: 20px;
-                height: 20px;
-                display: inline-block;
-                margin-right: 10px;
             }
         }
     }
@@ -221,4 +206,54 @@
             fill: $color--white;
         }
     }
+
+    &--action {
+        display: flex;
+        font-weight: $weight--normal;
+        color: $color--default;
+        transition: none;
+
+        @include media-query(tablet-landscape) {
+            background: none;
+            padding: 0;
+            border: 0;
+            align-items: center;
+            justify-content: flex-start;
+            width: auto;
+            transition: opacity $transition;
+            opacity: .7;
+            font-weight: $weight--semibold;
+
+            &:hover {
+                opacity: 1;
+            }
+
+            &::before {
+                content: '';
+                background-image: url('./../../images/filters.svg');
+                background-color: transparent;
+                background-position: left center;
+                background-size: 20px;
+                width: 20px;
+                height: 20px;
+                display: inline-block;
+                margin-right: 10px;
+            }
+        }
+    }
+
+    &--change-status {
+        display: none;
+
+        &::before {
+            background-image: url('./../../images/arrow-split.svg');
+            transform: rotate(90deg);
+        }
+    }
+
+    &--reviewers {
+        &::before {
+            background-image: url('./../../images/add-person.svg');
+        }
+    }
 }
diff --git a/opentech/static_src/src/sass/apply/components/_form.scss b/opentech/static_src/src/sass/apply/components/_form.scss
index c7708add8d06a46a2ac14c522915aa821a09bc82..bb1fad78da89d37102766f16e9bd0e48fa2702d1 100644
--- a/opentech/static_src/src/sass/apply/components/_form.scss
+++ b/opentech/static_src/src/sass/apply/components/_form.scss
@@ -17,11 +17,10 @@
         }
     }
 
-    &--search {
+    &--search-desktop {
         position: relative;
         max-width: 300px;
         margin-top: $mobile-gutter;
-        width: 100%;
 
         @include media-query(tablet-landscape) {
             max-width: 280px;
diff --git a/opentech/static_src/src/sass/apply/components/_link.scss b/opentech/static_src/src/sass/apply/components/_link.scss
index 9217908648e8a569ebe7e7d86b8769db2ab0d153..ad8fbec69a79fe53c36ea9cf1eb8c98bd73ea2a7 100644
--- a/opentech/static_src/src/sass/apply/components/_link.scss
+++ b/opentech/static_src/src/sass/apply/components/_link.scss
@@ -96,6 +96,10 @@
         color: $color--white;
         background: url('./../../images/speech-bubble-blue.svg') no-repeat center;
 
+        .app-open & {
+            display: none;
+        }
+
         @include media-query(tablet-portrait) {
             @include button($color--light-blue, $color--dark-blue);
             right: 5%;
@@ -166,6 +170,7 @@
         color: $color--black-50;
         opacity: 0;
         transition: opacity $transition;
+        pointer-events: none;
 
         @include media-query(tablet-portrait) {
             right: 30px;
@@ -174,6 +179,7 @@
 
         &.is-visible {
             opacity: 1;
+            pointer-events: all;
         }
     }
 
diff --git a/opentech/static_src/src/sass/apply/components/_messages.scss b/opentech/static_src/src/sass/apply/components/_messages.scss
index f568ead89a69a28b226f0e4c52e0642618a9a8c4..69ae151b8e57fe22635e7cd42e6df7389033a3cf 100644
--- a/opentech/static_src/src/sass/apply/components/_messages.scss
+++ b/opentech/static_src/src/sass/apply/components/_messages.scss
@@ -1,46 +1,73 @@
 .messages {
-    position: relative;
-    right: 50%;
-    left: 50%;
-    width: 100vw;
-    margin-right: -50vw;
-    margin-left: -50vw;
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 20;
+    pointer-events: none;
 
     &__text {
         position: relative;
+        padding: 15px 20px;
+        color: $color--white;
+        font-size: 14px;
+        opacity: 1;
+        transition: opacity, max-height, $transition;
+        pointer-events: all;
         max-height: 1000px;
-        padding: 15px;
-        padding-right: 35px;
-        border: solid 1px;
+
+        @include media-query(desktop) {
+            padding: 15px 30px;
+        }
 
         &--info,
         &--success {
-            background: $color--pastel-green;
-            border-color: darken($color--pastel-green, 20%);
+            background: $color--dark-blue;
         }
 
         &--error,
         &--warning {
-            font-weight: bold;
-            color: $color--white;
-            background: $color--error;
-            border-color: darken($color--error, 20%);
+            background: darken($color--error, 20%);
+        }
+
+        &--debug {
+            background: darken($color--pink, 30%);
         }
 
         &--hide {
+            opacity: 0;
+            pointer-events: none;
             max-height: 0;
-            padding-top: 0;
-            padding-bottom: 0;
-            border: 0 none;
-            transition: all $transition; // sass-lint:disable-line no-transition-all
-            transform-origin: top;
+            padding: 0;
         }
     }
 
-    &__close {
-        position: absolute;
-        top: 15px;
-        right: 15px;
+    &__inner {
+        display: flex;
+        align-items: center;
+        max-width: $site-width;
+        margin: 0 auto;
+    }
+
+    &__copy {
+        padding-right: 20px;
+        margin: 0;
+        flex: 1;
+    }
+
+    &__button {
+        margin-left: auto;
+        color: $color--dark-blue;
+        background-color: $color--white;
+        display: inline-block;
+        font-weight: $weight--bold;
+        padding: 2px 20px;
+    }
 
+    &__icon {
+        width: 25px;
+        height: 25px;
+        fill: $color--white;
+        margin-right: 10px;
     }
 }
diff --git a/opentech/static_src/src/sass/apply/components/_modal.scss b/opentech/static_src/src/sass/apply/components/_modal.scss
index beeb6c0ae4d8e7246db72fb32ba9a05671b1268d..d486cca48b00a9e2f839ecf737089c43b4cef4bd 100644
--- a/opentech/static_src/src/sass/apply/components/_modal.scss
+++ b/opentech/static_src/src/sass/apply/components/_modal.scss
@@ -1,10 +1,72 @@
 .modal {
+    $root: &;
     display: none;
     width: calc(100% - 40px);
     padding: 20px;
 
+    &--secondary {
+        padding: 0;
+    }
+
     @include media-query(small-tablet) {
         width: 580px;
         padding: 30px;
     }
+
+    &__header-bar {
+        color: $color--white;
+        background-color: $color--dark-blue;
+        margin: -24px -24px 0;
+        padding: 15px;
+        text-align: center;
+    }
+
+    &__list {
+        max-height: 200px;
+        overflow: scroll;
+        margin: 0 -24px 20px;
+        padding: 0;
+        border-bottom: 2px solid $color--light-mid-grey;
+        box-shadow: inset 0 -10px 20px -10px $color--mid-grey;
+        transition: max-height $transition;
+
+        &.is-closed {
+            max-height: 0;
+            border-bottom: 0;
+        }
+    }
+
+    &__list-item {
+        display: block;
+        font-size: map-get($font-sizes, zeta);
+        padding: 12px 28px;
+        border-bottom: 2px solid $color--light-mid-grey;
+        margin: 0;
+        color: $color--default;
+
+        &--meta {
+            color: $color--dark-blue;
+            font-weight: $weight--semibold;
+            display: flex;
+            justify-content: space-between;
+            margin: 0 -24px;
+        }
+    }
+
+    &__hide-link {
+        text-decoration: underline;
+    }
+
+    &__open-link-icon {
+        width: 20px;
+        height: 20px;
+        fill: $color--dark-grey;
+        opacity: 0;
+        transition: opacity $quick-transition;
+        pointer-events: none;
+
+        #{$root}__list-item:hover & {
+            opacity: 1;
+        }
+    }
 }
diff --git a/opentech/static_src/src/sass/apply/fancybox.scss b/opentech/static_src/src/sass/apply/fancybox.scss
index 440a4fc308a538505f29399ac7eb603b85581d2b..e01a47923d6f75200525d969ec325368fda585f0 100644
--- a/opentech/static_src/src/sass/apply/fancybox.scss
+++ b/opentech/static_src/src/sass/apply/fancybox.scss
@@ -325,6 +325,10 @@ body.fancybox-iosfix {
 	position: relative;
     overflow: visible;
     shape-rendering: geometricPrecision;
+
+	.modal--secondary & {
+		display: none;
+	}
 }
 
 .fancybox-button svg path {
@@ -424,6 +428,13 @@ body.fancybox-iosfix {
 	transition: background-color .25s;
 	box-sizing: border-box;
 	z-index: 2;
+
+	.modal--secondary & {
+		color: white;
+		font-size: 40px;
+		margin-top: 12px;
+    	margin-right: 8px;
+	}
 }
 
 .fancybox-close-small:focus {
diff --git a/opentech/static_src/src/sass/apply/main.scss b/opentech/static_src/src/sass/apply/main.scss
index 4423f6ed552181b4708da92017bba897de5f1aa7..386c5f11111e127b697b8f9782045cb4176366b7 100644
--- a/opentech/static_src/src/sass/apply/main.scss
+++ b/opentech/static_src/src/sass/apply/main.scss
@@ -12,6 +12,7 @@
 @import 'components/all-rounds-table';
 @import 'components/admin-bar';
 @import 'components/activity-feed';
+@import 'components/actions-bar';
 @import 'components/comment';
 @import 'components/button';
 @import 'components/editor';
diff --git a/opentech/templates/includes/message_item.html b/opentech/templates/includes/message_item.html
index 49f1a6f123431441e3cd4bd0f7346156998ee855..3a63088a9f710f0ccf12ab3753652dcfb4916a20 100644
--- a/opentech/templates/includes/message_item.html
+++ b/opentech/templates/includes/message_item.html
@@ -1,8 +1,9 @@
 <li class="messages__text{% if tag %} messages__text--{{ tag }}{% endif %}{% if close %} js-message{% endif %}">
-    {{ message }}
-    {% if close %}
-    <a href="#" class="messages__close js-close-message">
-        <svg class="icon icon--close"><use xlink:href="#cross"></use></svg>
-    </a>
-    {% endif %}
+    <div class="messages__inner">
+        <svg class="messages__icon"><use xlink:href="#exclamation-point"></use></svg>
+        <p class="messages__copy">{{ message }}</p>
+        {% if close %}
+            <a href="#" class="messages__button js-close-message">Ok</a>
+        {% endif %}
+    </div>
 </li>
diff --git a/opentech/templates/includes/sprites.html b/opentech/templates/includes/sprites.html
index b952c2e9570347fb06605d39f5bbe2b88d03c791..42bf4cb3d50f9fe3d9a3ac8da8fd2091430dcb80 100644
--- a/opentech/templates/includes/sprites.html
+++ b/opentech/templates/includes/sprites.html
@@ -296,4 +296,12 @@
     <symbol id="padlock" viewBox="0 0 64 64">
         <g id="Icon-Lock" transform="translate(284 430)"><path class="st0" d="M-237.7-401.3h-3v-6.4c0-6.2-5.1-11.3-11.3-11.3-6.2 0-11.3 5.1-11.3 11.3v6.4h-3v-6.4c0-7.9 6.4-14.3 14.3-14.3s14.3 6.4 14.3 14.3v6.4" id="Fill-66"/><path class="st0" d="M-239.2-374.1h-25.6c-2.6 0-4.8-2.2-4.8-4.8v-19.2c0-2.6 2.2-4.8 4.8-4.8h25.6c2.6 0 4.8 2.2 4.8 4.8v19.2c0 2.7-2.2 4.8-4.8 4.8zm-25.6-25.6c-.9 0-1.6.7-1.6 1.6v19.2c0 .9.7 1.6 1.6 1.6h25.6c.9 0 1.6-.7 1.6-1.6v-19.2c0-.9-.7-1.6-1.6-1.6h-25.6z" id="Fill-67"/><path class="st0" d="M-248.8-393.3c0 1.8-1.4 3.2-3.2 3.2s-3.2-1.4-3.2-3.2 1.4-3.2 3.2-3.2 3.2 1.5 3.2 3.2" id="Fill-68"/><path class="st0" id="Fill-69" d="M-251.2-393.3h-1.6l-1.6 9.6h4.8l-1.6-9.6"/></g>
     </symbol>
+
+    <symbol id="exclamation-point" viewbox="0 0 24 24">
+        <path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
+    </symbol>
+
+    <symbol id="open-in-new-tab" viewbox="0 0 24 24">
+        <path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
+    </symbol>
 </svg>
diff --git a/package-lock.json b/package-lock.json
index c03d14147002a5f8545753a762f00216e4b07807..d9f73a8d8ae9bbf64c4dfefce0aef828f32433ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1456,6 +1456,11 @@
             "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
             "dev": true
         },
+        "asap": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+            "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
+        },
         "asn1": {
             "version": "0.2.4",
             "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@@ -1604,6 +1609,22 @@
                 "util.promisify": "^1.0.0"
             }
         },
+        "babel-runtime": {
+            "version": "6.26.0",
+            "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+            "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+            "requires": {
+                "core-js": "^2.4.0",
+                "regenerator-runtime": "^0.11.0"
+            },
+            "dependencies": {
+                "regenerator-runtime": {
+                    "version": "0.11.1",
+                    "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+                    "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
+                }
+            }
+        },
         "bach": {
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz",
@@ -2166,6 +2187,11 @@
             "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
             "dev": true
         },
+        "class-autobind": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/class-autobind/-/class-autobind-0.1.4.tgz",
+            "integrity": "sha1-NFFsSRZ8+NP2Od3Bhrz6Imiv/zQ="
+        },
         "class-utils": {
             "version": "0.3.6",
             "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -2187,6 +2213,11 @@
                 }
             }
         },
+        "classnames": {
+            "version": "2.2.6",
+            "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
+            "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
+        },
         "clean-css": {
             "version": "4.2.1",
             "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
@@ -3109,6 +3140,63 @@
                 "domelementtype": "1"
             }
         },
+        "draft-js": {
+            "version": "0.10.5",
+            "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.10.5.tgz",
+            "integrity": "sha512-LE6jSCV9nkPhfVX2ggcRLA4FKs6zWq9ceuO/88BpXdNCS7mjRTgs0NsV6piUCJX9YxMsB9An33wnkMmU2sD2Zg==",
+            "requires": {
+                "fbjs": "^0.8.15",
+                "immutable": "~3.7.4",
+                "object-assign": "^4.1.0"
+            }
+        },
+        "draft-js-export-html": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/draft-js-export-html/-/draft-js-export-html-1.2.0.tgz",
+            "integrity": "sha1-HL4reOH+10/CnHzcv9dUBGjsogk=",
+            "requires": {
+                "draft-js-utils": "^1.2.0"
+            }
+        },
+        "draft-js-export-markdown": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/draft-js-export-markdown/-/draft-js-export-markdown-1.3.0.tgz",
+            "integrity": "sha512-kOiDGQ9KehcbYYcwzlkR+Gja6svEwIgId1gz3EtEVsZ09cxZaV13Qlkydm0J5wPy5Omthvdpj0Iw1B2E4BZRZQ==",
+            "requires": {
+                "draft-js-utils": "^1.2.0"
+            }
+        },
+        "draft-js-import-element": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.2.2.tgz",
+            "integrity": "sha512-atwfQFg5YWsKdBiOIkIYxYh9lOsS5gzzDaqV89GgG1UIb/E1689FI9PsH2OmuJ4DUhHouzBWAAPSa5DerGNnBQ==",
+            "requires": {
+                "draft-js-utils": "^1.2.4",
+                "synthetic-dom": "^1.2.0"
+            }
+        },
+        "draft-js-import-html": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.2.1.tgz",
+            "integrity": "sha512-FP1y9kdmOVDvOxoI4ny+H0g4CVoTQwdW++Zjf+qMsnz07NsYOCLcQ34j7TiwuPfArFAcOjBOc41Mn+qOa1G14w==",
+            "requires": {
+                "draft-js-import-element": "^1.2.1"
+            }
+        },
+        "draft-js-import-markdown": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/draft-js-import-markdown/-/draft-js-import-markdown-1.2.3.tgz",
+            "integrity": "sha512-NPcXwWSsIA+uwASzdJWLQM4y+xW1vTDtDdIDHCHfP76i9cx8zYpH75GW8Ezz8L9SW2qetNcFW056Hj2yxRZ+2g==",
+            "requires": {
+                "draft-js-import-element": "^1.2.1",
+                "synthetic-dom": "^1.2.0"
+            }
+        },
+        "draft-js-utils": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.2.4.tgz",
+            "integrity": "sha512-oy7WL6VCcSJ1WOUVCxkU0t/nGoLs/Kv0+zKalC61WjFTN4I2Lt1I8Oj5m4oUFBxfF7K9+0C0U5ilgvb4F4rovg=="
+        },
         "duplexer": {
             "version": "0.1.1",
             "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@@ -3180,6 +3268,14 @@
             "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
             "dev": true
         },
+        "encoding": {
+            "version": "0.1.12",
+            "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
+            "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+            "requires": {
+                "iconv-lite": "~0.4.13"
+            }
+        },
         "end-of-stream": {
             "version": "1.4.1",
             "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -3921,6 +4017,27 @@
                 "websocket-driver": ">=0.5.1"
             }
         },
+        "fbjs": {
+            "version": "0.8.17",
+            "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
+            "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
+            "requires": {
+                "core-js": "^1.0.0",
+                "isomorphic-fetch": "^2.1.1",
+                "loose-envify": "^1.0.0",
+                "object-assign": "^4.1.0",
+                "promise": "^7.1.1",
+                "setimmediate": "^1.0.5",
+                "ua-parser-js": "^0.7.18"
+            },
+            "dependencies": {
+                "core-js": {
+                    "version": "1.2.7",
+                    "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
+                    "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
+                }
+            }
+        },
         "figgy-pudding": {
             "version": "3.5.1",
             "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
@@ -5707,11 +5824,15 @@
             "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
             "dev": true
         },
+        "humps": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz",
+            "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao="
+        },
         "iconv-lite": {
             "version": "0.4.24",
             "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
             "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-            "dev": true,
             "requires": {
                 "safer-buffer": ">= 2.1.2 < 3"
             }
@@ -5749,6 +5870,11 @@
             "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
             "dev": true
         },
+        "immutable": {
+            "version": "3.7.6",
+            "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
+            "integrity": "sha1-E7TTyxK++hVIKib+Gy665kAHHks="
+        },
         "import-fresh": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@@ -6260,8 +6386,7 @@
         "is-stream": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
-            "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
-            "dev": true
+            "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
         },
         "is-symbol": {
             "version": "1.0.2",
@@ -6320,6 +6445,15 @@
             "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
             "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
         },
+        "isomorphic-fetch": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
+            "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
+            "requires": {
+                "node-fetch": "^1.0.1",
+                "whatwg-fetch": ">=0.10.0"
+            }
+        },
         "isstream": {
             "version": "0.1.2",
             "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -6330,6 +6464,11 @@
             "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.0.tgz",
             "integrity": "sha512-wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g=="
         },
+        "js-cookie": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz",
+            "integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s="
+        },
         "js-levenshtein": {
             "version": "1.1.4",
             "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz",
@@ -7133,6 +7272,19 @@
                 }
             }
         },
+        "moment": {
+            "version": "2.24.0",
+            "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
+            "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
+        },
+        "moment-timezone": {
+            "version": "0.5.23",
+            "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz",
+            "integrity": "sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==",
+            "requires": {
+                "moment": ">= 2.9.0"
+            }
+        },
         "move-concurrently": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -7231,6 +7383,15 @@
             "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
             "dev": true
         },
+        "node-fetch": {
+            "version": "1.7.3",
+            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
+            "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
+            "requires": {
+                "encoding": "^0.1.11",
+                "is-stream": "^1.0.1"
+            }
+        },
         "node-forge": {
             "version": "0.7.5",
             "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
@@ -8133,6 +8294,14 @@
             "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
             "dev": true
         },
+        "promise": {
+            "version": "7.3.1",
+            "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+            "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+            "requires": {
+                "asap": "~2.0.3"
+            }
+        },
         "promise-inflight": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -8393,6 +8562,30 @@
                 }
             }
         },
+        "react-rte": {
+            "version": "0.16.1",
+            "resolved": "https://registry.npmjs.org/react-rte/-/react-rte-0.16.1.tgz",
+            "integrity": "sha512-CD5kf+6CHqOgJ1yB0i9tkMMch13wOXW5/FQx60gb7nzhXC1ZeFJjtW9dYfCVlfw1AvksHf+lMmKTjhIwyfZR7w==",
+            "requires": {
+                "babel-runtime": "^6.23.0",
+                "class-autobind": "^0.1.4",
+                "classnames": "^2.2.5",
+                "draft-js": ">=0.10.0",
+                "draft-js-export-html": ">=0.6.0",
+                "draft-js-export-markdown": ">=0.3.0",
+                "draft-js-import-html": ">=0.4.0",
+                "draft-js-import-markdown": ">=0.3.0",
+                "draft-js-utils": ">=0.2.0",
+                "immutable": "^3.8.1"
+            },
+            "dependencies": {
+                "immutable": {
+                    "version": "3.8.2",
+                    "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
+                    "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
+                }
+            }
+        },
         "react-transition-group": {
             "version": "2.5.3",
             "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz",
@@ -9417,8 +9610,7 @@
         "setimmediate": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
-            "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
-            "dev": true
+            "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
         },
         "setprototypeof": {
             "version": "1.1.0",
@@ -9504,6 +9696,11 @@
                 }
             }
         },
+        "smoothscroll-polyfill": {
+            "version": "0.4.3",
+            "resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.3.tgz",
+            "integrity": "sha512-aUg0sY8XlWw9reC3VGlVdmC9W4K565alN4t8Cm50kULz53NB4GvsZbrinWPLqYqLolY60NBdqHDyh89MqDUc/Q=="
+        },
         "snapdragon": {
             "version": "0.8.2",
             "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -10054,6 +10251,11 @@
             "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
             "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
         },
+        "synthetic-dom": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.2.0.tgz",
+            "integrity": "sha1-81iar+K14pnzN7sylzqb5C3VYl4="
+        },
         "table": {
             "version": "4.0.3",
             "resolved": "http://registry.npmjs.org/table/-/table-4.0.3.tgz",
@@ -10485,6 +10687,11 @@
             "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
             "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
         },
+        "ua-parser-js": {
+            "version": "0.7.19",
+            "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
+            "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
+        },
         "uglify-js": {
             "version": "3.4.9",
             "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
@@ -11531,6 +11738,11 @@
             "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
             "dev": true
         },
+        "whatwg-fetch": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
+            "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
+        },
         "which": {
             "version": "1.3.1",
             "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
diff --git a/package.json b/package.json
index 7ad0fcfa4597981cdb4b18bee2f73c21963125a2..ec640e8844fe3bdaffbea8c408ce7f7f8213e7b2 100644
--- a/package.json
+++ b/package.json
@@ -23,17 +23,23 @@
         "gulp-size": "^3.0.0",
         "gulp-touch-cmd": "0.0.1",
         "gulp-uglify": "^3.0.1",
+        "humps": "^2.0.1",
+        "js-cookie": "^2.2.0",
+        "moment": "^2.24.0",
+        "moment-timezone": "^0.5.23",
         "node-sass-import-once": "^1.2.0",
         "prop-types": "^15.6.2",
         "react": "^16.7.0",
         "react-dom": "^16.7.0",
         "react-redux": "^6.0.0",
+        "react-rte": "^0.16.1",
         "react-transition-group": "^2.5.3",
         "react-window-size-listener": "^1.2.3",
         "redux": "^4.0.1",
         "redux-logger": "^3.0.6",
         "redux-thunk": "^2.3.0",
-        "reselect": "^4.0.0"
+        "reselect": "^4.0.0",
+        "smoothscroll-polyfill": "^0.4.3"
     },
     "devDependencies": {
         "@babel/core": "^7.2.2",