diff --git a/config/index.js b/config/index.js
index 6652048c..c48d91b8 100644
--- a/config/index.js
+++ b/config/index.js
@@ -23,9 +23,15 @@ module.exports = {
     assetsPublicPath: '/',
     proxyTable: {
       '/api': {
-        target: 'https://social.heldscal.la/',
+        target: 'htts://localhost:4000/',
         changeOrigin: true,
         cookieDomainRewrite: 'localhost'
+      },
+      '/socket': {
+        target: 'htts://localhost:4000/',
+        changeOrigin: true,
+        cookieDomainRewrite: 'localhost',
+        ws: true
       }
     },
     // CSS Sourcemaps off by default because relative paths are "buggy"
diff --git a/package.json b/package.json
index e8d84274..4e98647b 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
     "localforage": "^1.5.0",
     "node-sass": "^3.10.1",
     "object-path": "^0.11.3",
+    "phoenix": "^1.3.0",
     "sanitize-html": "^1.13.0",
     "sass-loader": "^4.0.2",
     "vue": "^2.3.4",
diff --git a/src/App.scss b/src/App.scss
index 9aa3ee98..95a653ce 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -16,7 +16,13 @@ h4 {
 }
 
 #content {
-    padding-top: 60px;
+  box-sizing: border-box;
+  padding-top: 60px;
+  margin: auto;
+  min-height: 100vh;
+  max-width: 980px;
+  background-color: rgba(0,0,0,0.15);
+  align-content: flex-start;
 }
 
 .text-center {
@@ -157,15 +163,6 @@ main-router {
     margin: 0;
 }
 
-
-#content {
-    margin: auto;
-    min-height: 100vh;
-    max-width: 980px;
-    padding-bottom: 1em;
-    background-color: rgba(0,0,0,0.1);
-}
-
 .container > * {
     min-width: 0px;
 }
@@ -210,10 +207,11 @@ nav {
 .panel-switcher {
     display: none;
     width: 100%;
-
+    height: 46px;
     button {
         display: block;
         flex: 1;
+        max-height: 32px;
         margin: 0.5em;
         padding: 0.5em;
     }
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 20d10cce..d6a51ffd 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -34,6 +34,13 @@
       display: flex;
       flex-wrap: wrap;
       margin-right: -0.7em;
+
+      .attachment.media-upload-container {
+        flex: 0 0 auto;
+        max-height: 300px;
+        max-width: 100%;
+      }
+      
       .attachment {
           flex: 1 0 30%;
           margin: 0.5em 0.7em 0.6em 0.0em;
@@ -82,9 +89,7 @@
           img.media-upload {
               margin-bottom: -2px;
               max-height: 300px;
-              width: 100%;
-              height: 100%;
-              flex: 1;
+              max-width: 100%;
           }
 
           .oembed {
@@ -126,6 +131,7 @@
                   width: 100%;
                   height: 100%; /* If this isn't here, chrome will stretch the images */
                   max-height: 500px;
+                  image-orientation: from-image;
               }
           }
       }
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
new file mode 100644
index 00000000..ef326d4a
--- /dev/null
+++ b/src/components/chat/chat.js
@@ -0,0 +1,21 @@
+const chat = {
+  data () {
+    return {
+      currentMessage: '',
+      channel: null
+    }
+  },
+  computed: {
+    messages () {
+      return this.$store.state.chat.messages
+    }
+  },
+  methods: {
+    submit (message) {
+      this.$store.state.chat.channel.push('new_msg', {text: message}, 10000)
+      this.currentMessage = ''
+    }
+  }
+}
+
+export default chat
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
new file mode 100644
index 00000000..6c1e2c38
--- /dev/null
+++ b/src/components/chat/chat.vue
@@ -0,0 +1,59 @@
+<template>
+  <div class="chat-panel panel panel-default">
+    <div class="panel-heading timeline-heading base02-background base04">
+      <div class="title">
+        {{$t('chat.title')}}
+      </div>
+    </div>
+    <div class="panel-body base01-background">
+      <div class="chat-window">
+        <div class="chat-message" v-for="message in messages" :key="message.id">
+          <span class="chat-avatar">
+            <img :src="message.author.avatar" />
+            {{message.author.username}}:
+          </span>
+          <span class="chat-text">
+            {{message.text}}
+          </span>
+        </div>
+      </div>
+      <div class="chat-input">
+        <form @submit.prevent="submit(currentMessage)">
+          <input v-model="currentMessage" type="text" >
+        </form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./chat.js"></script>
+
+
+<style lang="scss">
+ .chat-window {
+   max-height: 80vh;
+   overflow-y: auto;
+   overflow-x: hidden;
+ }
+ .chat-message {
+   padding: 0.2em 0.5em
+ }
+ .chat-avatar {
+     img {
+         height: 32px;
+         width: 32px;
+         border-radius: 5px;
+         margin-right: 0.5em;
+     }
+ }
+ .chat-input {
+     display: flex;
+     form {
+         flex: auto;
+         input {
+             margin: 0.5em;
+             width: fill-available;
+         }
+     }
+ }
+</style>
diff --git a/src/components/delete_button/delete_button.vue b/src/components/delete_button/delete_button.vue
index 304f8a63..845ac777 100644
--- a/src/components/delete_button/delete_button.vue
+++ b/src/components/delete_button/delete_button.vue
@@ -1,7 +1,7 @@
 <template>
   <div v-if="canDelete">
     <a href="#" v-on:click.prevent="deleteStatus()">
-      <i class='fa icon-cancel delete-status'></i>
+      <i class='base09 icon-cancel delete-status'></i>
     </a>
   </div>
 </template>
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index 0abece31..dcf28e35 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <i :class='classes' class='favorite-button fa' @click.prevent='favorite()'/>
+    <i :class='classes' class='favorite-button base09' @click.prevent='favorite()'/>
     <span v-if='status.fave_num > 0'>{{status.fave_num}}</span>
   </div>
 </template>
@@ -15,7 +15,7 @@
         color: orange;
       }
   }
-  .icon-star {
+  .favorite-button.icon-star {
       color: orange;
   }
 
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index b839067b..9e6ad608 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,8 +1,8 @@
 <template>
   <div class="media-upload" @drop.prevent @dragover.prevent="fileDrag" @drop="fileDrop">
     <label class="btn btn-default">
-      <i class="fa icon-spin4 animate-spin" v-if="uploading"></i>
-      <i class="fa icon-upload" v-if="!uploading"></i>
+      <i class="base09 icon-spin4 animate-spin" v-if="uploading"></i>
+      <i class="base09 icon-upload" v-if="!uploading"></i>
       <input type=file style="position: fixed; top: -100em"></input>
     </label>
   </div>
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index baeaaede..ea5d7ea4 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -2,6 +2,9 @@ const NavPanel = {
   computed: {
     currentUser () {
       return this.$store.state.users.currentUser
+    },
+    chat () {
+      return this.$store.state.chat.channel
     }
   }
 }
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index aea841e9..ccc772a8 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -7,6 +7,11 @@
             {{ $t("nav.timeline") }}
           </router-link>
         </li>
+        <li v-if='chat && currentUser'>
+          <router-link class="base00-background" to='/chat'>
+            {{ $t("nav.chat") }}
+          </router-link>
+        </li>
         <li v-if='currentUser'>
           <router-link class="base00-background" :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
             {{ $t("nav.mentions") }}
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index db7b0843..241f10b4 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -59,6 +59,10 @@
         color: $blue;
       }
 
+      .icon-star.lit {
+        color: orange;
+      }
+
       .status-content {
         margin: 0;
         max-height: 300px;
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 64624873..b341fcef 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -4,7 +4,7 @@
       <div class="panel-heading base02-background base04">
         <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
         {{$t('notifications.notifications')}}
-        <button @click.prevent="markAsSeen" class="base04 base02-background read-button">{{$t('notifications.read')}}</button>
+        <button v-if="unseenCount" @click.prevent="markAsSeen" class="base04 base02-background read-button">{{$t('notifications.read')}}</button>
       </div>
       <div class="panel-body base03-border">
         <div v-for="notification in visibleNotifications" :key="notification" class="notification" :class='{"unseen": !notification.seen}'>
@@ -17,7 +17,7 @@
             <div v-if="notification.type === 'favorite'">
               <h1>
                 <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
-                <i class="fa icon-star"></i>
+                <i class="fa icon-star lit"></i>
                 <small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
               </h1>
               <div class="notification-gradient" :style="hiderStyle"></div>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 2eb091f4..8c0cd5ee 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -157,6 +157,14 @@ const PostStatusForm = {
     type (fileInfo) {
       return fileTypeService.fileType(fileInfo.mimetype)
     },
+    paste (e) {
+      if (e.clipboardData.files.length > 0) {
+        // Strangely, files property gets emptied after event propagation
+        // Trying to wrap it in array doesn't work. Plus I doubt it's possible
+        // to hold more than one file in clipboard.
+        this.dropFiles = [e.clipboardData.files[0]]
+      }
+    },
     fileDrop (e) {
       if (e.dataTransfer.files.length > 0) {
         e.preventDefault()  // allow dropping text like before
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 8a2ec24d..8e436428 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -2,7 +2,7 @@
   <div class="post-status-form">
     <form @submit.prevent="postStatus(newStatus)">
       <div class="form-group base03-border" >
-        <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize"></textarea>
+        <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize" @paste="paste"></textarea>
       </div>
       <div style="position:relative;" v-if="candidates">
         <div class="autocomplete-panel base05-background">
@@ -26,7 +26,7 @@
         <i class="icon-cancel" @click="clearError"></i>
       </div>
       <div class="attachments">
-        <div class="attachment base03-border" v-for="file in newStatus.files">
+        <div class="media-upload-container attachment base03-border" v-for="file in newStatus.files">
           <i class="fa icon-cancel" @click="removeMediaFile(file)"></i>
           <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
           <video v-if="type(file) === 'video'" :src="file.image" controls></video>
@@ -41,6 +41,7 @@
 <script src="./post_status_form.js"></script>
 
 <style lang="scss">
+
  .tribute-container {
    ul {
      padding: 0px;
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index d923c5c4..edbfef32 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <i :class='classes' class='icon-retweet fa' v-on:click.prevent='retweet()'></i>
+    <i :class='classes' class='icon-retweet base09' v-on:click.prevent='retweet()'></i>
     <span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
   </div>
 </template>
@@ -16,7 +16,7 @@
       color: $green;
      }
   }
-  .retweeted {
+  .icon-retweet.retweeted {
      color: $green;
   }
 </style>
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 5e3df8ba..d6c8cdb3 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -5,7 +5,7 @@
       <div class='status-actions'>
         <div>
           <a href="#" v-on:click.prevent="toggleReplying">
-            <i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i>
+            <i class="base09 icon-reply" :class="{'icon-reply-active': replying}"></i>
           </a>
         </div>
         <retweet-button :status=status></retweet-button>
@@ -19,7 +19,7 @@
       <div class="media status container muted">
         <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
         <small class="muteWords">{{muteWordHits.join(', ')}}</small>
-        <a href="#" class="unmute" @click.prevent="toggleMute"><i class="fa icon-eye-off"></i></a>
+        <a href="#" class="unmute" @click.prevent="toggleMute"><i class="base09 icon-eye-off"></i></a>
       </div>
     </template>
     <template v-if="!muted">
@@ -75,10 +75,10 @@
                 </h4>
               </div>
               <div class="heading-icons">
-                <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="fa icon-eye-off"></i></a>
-                <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="fa icon-binoculars"></i></a>
+                <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="base09 icon-eye-off"></i></a>
+                <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="base09 icon-binoculars"></i></a>
                 <template v-if="expandable">
-                  <a href="#" @click.prevent="toggleExpanded" class="expand"><i class="fa icon-plus-squared"></i></a>
+                  <a href="#" @click.prevent="toggleExpanded" class="expand"><i class="base09 icon-plus-squared"></i></a>
                 </template>
               </div>
             </div>
@@ -94,7 +94,7 @@
               </div>
             </div>
             <div class="status-preview status-preview-loading base00-background base03-border" v-else-if="showPreview">
-              <i class="fa icon-spin4 animate-spin"></i>
+              <i class="base09 icon-spin4 animate-spin"></i>
             </div>
 
             <div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
@@ -109,7 +109,7 @@
             <div class='status-actions'>
               <div>
                 <a href="#" v-on:click.prevent="toggleReplying">
-                  <i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i>
+                  <i class="base09 icon-reply" :class="{'icon-reply-active': replying}"></i>
                 </a>
               </div>
               <retweet-button :status=status></retweet-button>
@@ -324,7 +324,7 @@
      color: $blue;
  }
 
- .icon-reply-active {
+ .icon-reply.icon-reply-active {
      color: $blue;
  }
 
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index b1359d13..a762f914 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,3 +1,5 @@
+import { rgbstr2hex } from '../../services/color_convert/color_convert.js'
+
 export default {
   data () {
     return {
@@ -19,13 +21,6 @@ export default {
       })
   },
   mounted () {
-    const rgbstr2hex = (rgb) => {
-      if (rgb[0] === '#') {
-        return rgb
-      }
-      rgb = rgb.match(/\d+/g)
-      return `#${((Number(rgb[0]) << 16) + (Number(rgb[1]) << 8) + Number(rgb[2])).toString(16)}`
-    }
     this.bgColorLocal = rgbstr2hex(this.$store.state.config.colors['base00'])
     this.fgColorLocal = rgbstr2hex(this.$store.state.config.colors['base02'])
     this.textColorLocal = rgbstr2hex(this.$store.state.config.colors['base05'])
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index be0aefc1..660a8752 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -29,6 +29,13 @@ const Timeline = {
     },
     newStatusCount () {
       return this.timeline.newStatusCount
+    },
+    newStatusCountStr () {
+      if (this.timeline.flushMarker !== 0) {
+        return ''
+      } else {
+        return ` (${this.newStatusCount})`
+      }
     }
   },
   components: {
@@ -64,8 +71,14 @@ const Timeline = {
   },
   methods: {
     showNewStatuses () {
-      this.$store.commit('showNewStatuses', { timeline: this.timelineName })
-      this.paused = false
+      if (this.timeline.flushMarker !== 0) {
+        this.$store.commit('clearTimeline', { timeline: this.timelineName })
+        this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
+        this.fetchOlderStatuses()
+      } else {
+        this.$store.commit('showNewStatuses', { timeline: this.timelineName })
+        this.paused = false
+      }
     },
     fetchOlderStatuses () {
       const store = this.$store
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 0e2ed92c..9d2e1ea1 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -5,7 +5,7 @@
         {{title}}
       </div>
       <button @click.prevent="showNewStatuses" class="base05 base02-background loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
-        {{$t('timeline.show_new')}} ({{timeline.newStatusCount}})
+        {{$t('timeline.show_new')}}{{newStatusCountStr}}
       </button>
       <div @click.prevent class="base06 error  loadmore-text" v-if="timelineError">
         {{$t('timeline.error_fetching')}}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index ba315faa..dd14d1b4 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -59,13 +59,16 @@
   }
 
   .usercard {
-    width: -webkit-fill-available;
-    width: -moz-webkit-fill-available;
-    stretch: fill;
+    width: fill-available;
     margin: 0.2em 0 0.7em 0;
-    border-radius: 5px;
+    border-radius: 10px;
     border-style: solid;
     border-color: inherit;
     border-width: 1px;
+    overflow: hidden;
+
+    p {
+      margin-bottom: 0;
+    }
   }
 </style>
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
new file mode 100644
index 00000000..6e67a321
--- /dev/null
+++ b/src/components/user_card_content/user_card_content.js
@@ -0,0 +1,64 @@
+import { hex2rgb } from '../../services/color_convert/color_convert.js'
+
+export default {
+  props: [ 'user', 'switcher' ],
+  computed: {
+    headingStyle () {
+      const color = this.$store.state.config.colors['base00']
+      if (color) {
+        const rgb = hex2rgb(color)
+        console.log(rgb)
+        return {
+          backgroundColor: `rgb(${Math.floor(rgb[0] * 0.53)}, ${Math.floor(rgb[1] * 0.56)}, ${Math.floor(rgb[2] * 0.59)})`,
+          backgroundImage: `url(${this.user.cover_photo})`
+        }
+      }
+    },
+    bodyStyle () {
+      return {
+        background: `linear-gradient(to bottom, rgba(0, 0, 0, 0), ${this.$store.state.config.colors['base00']} 80%)`
+      }
+    },
+    isOtherUser () {
+      return this.user.id !== this.$store.state.users.currentUser.id
+    },
+    loggedIn () {
+      return this.$store.state.users.currentUser
+    },
+    dailyAvg () {
+      const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
+      return Math.round(this.user.statuses_count / days)
+    }
+  },
+  methods: {
+    followUser () {
+      const store = this.$store
+      store.state.api.backendInteractor.followUser(this.user.id)
+        .then((followedUser) => store.commit('addNewUsers', [followedUser]))
+    },
+    unfollowUser () {
+      const store = this.$store
+      store.state.api.backendInteractor.unfollowUser(this.user.id)
+        .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
+    },
+    blockUser () {
+      const store = this.$store
+      store.state.api.backendInteractor.blockUser(this.user.id)
+        .then((blockedUser) => store.commit('addNewUsers', [blockedUser]))
+    },
+    unblockUser () {
+      const store = this.$store
+      store.state.api.backendInteractor.unblockUser(this.user.id)
+        .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser]))
+    },
+    toggleMute () {
+      const store = this.$store
+      store.commit('setMuted', {user: this.user, muted: !this.user.muted})
+      store.state.api.backendInteractor.setUserMute(this.user)
+    },
+    setProfileView (v) {
+      const store = this.$store
+      store.commit('setProfileView', { v })
+    }
+  }
+}
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 5635a177..4c40c55f 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -84,69 +84,7 @@
   </div>
 </template>
 
-<script>
-  export default {
-    props: [ 'user', 'switcher' ],
-    computed: {
-      headingStyle () {
-        let color = this.$store.state.config.colors['base00']
-        if (color) {
-          let rgb = this.$store.state.config.colors['base00'].match(/\d+/g)
-          return {
-            backgroundColor: `rgb(${Math.floor(rgb[0] * 0.53)}, ${Math.floor(rgb[1] * 0.56)}, ${Math.floor(rgb[2] * 0.59)})`,
-            backgroundImage: `url(${this.user.cover_photo})`
-          }
-        }
-      },
-      bodyStyle () {
-        return {
-          background: `linear-gradient(to bottom, rgba(0, 0, 0, 0), ${this.$store.state.config.colors['base00']} 80%)`
-        }
-      },
-      isOtherUser () {
-        return this.user.id !== this.$store.state.users.currentUser.id
-      },
-      loggedIn () {
-        return this.$store.state.users.currentUser
-      },
-      dailyAvg () {
-        const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
-        return Math.round(this.user.statuses_count / days)
-      }
-    },
-    methods: {
-      followUser () {
-        const store = this.$store
-        store.state.api.backendInteractor.followUser(this.user.id)
-          .then((followedUser) => store.commit('addNewUsers', [followedUser]))
-      },
-      unfollowUser () {
-        const store = this.$store
-        store.state.api.backendInteractor.unfollowUser(this.user.id)
-          .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
-      },
-      blockUser () {
-        const store = this.$store
-        store.state.api.backendInteractor.blockUser(this.user.id)
-          .then((blockedUser) => store.commit('addNewUsers', [blockedUser]))
-      },
-      unblockUser () {
-        const store = this.$store
-        store.state.api.backendInteractor.unblockUser(this.user.id)
-          .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser]))
-      },
-      toggleMute () {
-        const store = this.$store
-        store.commit('setMuted', {user: this.user, muted: !this.user.muted})
-        store.state.api.backendInteractor.setUserMute(this.user)
-      },
-      setProfileView (v) {
-        const store = this.$store
-        store.commit('setProfileView', { v })
-      }
-    }
-  }
-</script>
+<script src="./user_card_content.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
@@ -164,7 +102,6 @@
 .profile-panel-body {
   top: -0em;
   padding-top: 4em;
-
   word-wrap: break-word;
 }
 
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 74b0ff2a..515fd253 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -22,7 +22,7 @@
         <div>
           <input type="file" @change="uploadFile(0, $event)" ></input>
         </div>
-        <i class="fa icon-spin4 animate-spin" v-if="uploading[0]"></i>
+        <i class="base09 icon-spin4 animate-spin" v-if="uploading[0]"></i>
         <button class="btn btn-default base05 base02-background" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
       </div>
       <div class="setting-item">
@@ -35,7 +35,7 @@
         <div>
           <input type="file" @change="uploadFile(1, $event)" ></input>
         </div>
-        <i class="fa icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
+        <i class="base09 icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
         <button class="btn btn-default base05 base02-background" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
       </div>
       <div class="setting-item">
@@ -46,7 +46,7 @@
         <div>
           <input type="file" @change="uploadFile(2, $event)" ></input>
         </div>
-        <i class="fa icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
+        <i class="base09 icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
         <button class="btn btn-default base05 base02-background" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
       </div>
     </div>
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index 9aeffdfa..4c5be151 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -1,5 +1,9 @@
 const de = {
+  chat: {
+    title: 'Chat'
+  },
   nav: {
+    chat: 'Lokaler Chat',
     timeline: 'Zeitleiste',
     mentions: 'Erwähnungen',
     public_tl: 'Lokale Zeitleiste',
@@ -179,7 +183,11 @@ const fi = {
 }
 
 const en = {
+  chat: {
+    title: 'Chat'
+  },
   nav: {
+    chat: 'Local Chat',
     timeline: 'Timeline',
     mentions: 'Mentions',
     public_tl: 'Public Timeline',
diff --git a/src/main.js b/src/main.js
index 14cb27eb..51e0f7eb 100644
--- a/src/main.js
+++ b/src/main.js
@@ -12,11 +12,13 @@ import UserProfile from './components/user_profile/user_profile.vue'
 import Settings from './components/settings/settings.vue'
 import Registration from './components/registration/registration.vue'
 import UserSettings from './components/user_settings/user_settings.vue'
+import Chat from './components/chat/chat.vue'
 
 import statusesModule from './modules/statuses.js'
 import usersModule from './modules/users.js'
 import apiModule from './modules/api.js'
 import configModule from './modules/config.js'
+import chatModule from './modules/chat.js'
 
 import VueTimeago from 'vue-timeago'
 import VueI18n from 'vue-i18n'
@@ -57,35 +59,12 @@ const store = new Vuex.Store({
     statuses: statusesModule,
     users: usersModule,
     api: apiModule,
-    config: configModule
+    config: configModule,
+    chat: chatModule
   },
   plugins: [createPersistedState(persistedStateOptions)],
-  strict: process.env.NODE_ENV !== 'production'
-})
-
-const routes = [
-  { name: 'root', path: '/', redirect: '/main/all' },
-  { path: '/main/all', component: PublicAndExternalTimeline },
-  { path: '/main/public', component: PublicTimeline },
-  { path: '/main/friends', component: FriendsTimeline },
-  { path: '/tag/:tag', component: TagTimeline },
-  { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
-  { name: 'user-profile', path: '/users/:id', component: UserProfile },
-  { name: 'mentions', path: '/:username/mentions', component: Mentions },
-  { name: 'settings', path: '/settings', component: Settings },
-  { name: 'registration', path: '/registration', component: Registration },
-  { name: 'user-settings', path: '/user-settings', component: UserSettings }
-]
-
-const router = new VueRouter({
-  mode: 'history',
-  routes,
-  scrollBehavior: (to, from, savedPosition) => {
-    if (to.matched.some(m => m.meta.dontScroll)) {
-      return false
-    }
-    return savedPosition || { x: 0, y: 0 }
-  }
+  strict: false // Socket modifies itself, let's ignore this for now.
+  // strict: process.env.NODE_ENV !== 'production'
 })
 
 const i18n = new VueI18n({
@@ -94,23 +73,53 @@ const i18n = new VueI18n({
   messages
 })
 
-/* eslint-disable no-new */
-new Vue({
-  router,
-  store,
-  i18n,
-  el: '#app',
-  render: h => h(App)
-})
-
 window.fetch('/static/config.json')
   .then((res) => res.json())
-  .then(({name, theme, background, logo, registrationOpen}) => {
+  .then((data) => {
+    const {name, theme, background, logo, registrationOpen} = data
     store.dispatch('setOption', { name: 'name', value: name })
     store.dispatch('setOption', { name: 'theme', value: theme })
     store.dispatch('setOption', { name: 'background', value: background })
     store.dispatch('setOption', { name: 'logo', value: logo })
     store.dispatch('setOption', { name: 'registrationOpen', value: registrationOpen })
+    if (data['chatDisabled']) {
+      store.dispatch('disableChat')
+    }
+
+    const routes = [
+      { name: 'root', path: '/', redirect: data['defaultPath'] || '/main/all' },
+      { path: '/main/all', component: PublicAndExternalTimeline },
+      { path: '/main/public', component: PublicTimeline },
+      { path: '/main/friends', component: FriendsTimeline },
+      { path: '/tag/:tag', component: TagTimeline },
+      { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
+      { name: 'user-profile', path: '/users/:id', component: UserProfile },
+      { name: 'mentions', path: '/:username/mentions', component: Mentions },
+      { name: 'settings', path: '/settings', component: Settings },
+      { name: 'registration', path: '/registration', component: Registration },
+      { name: 'user-settings', path: '/user-settings', component: UserSettings },
+      { name: 'chat', path: '/chat', component: Chat }
+    ]
+
+    const router = new VueRouter({
+      mode: 'history',
+      routes,
+      scrollBehavior: (to, from, savedPosition) => {
+        if (to.matched.some(m => m.meta.dontScroll)) {
+          return false
+        }
+        return savedPosition || { x: 0, y: 0 }
+      }
+    })
+
+    /* eslint-disable no-new */
+    new Vue({
+      router,
+      store,
+      i18n,
+      el: '#app',
+      render: h => h(App)
+    })
   })
 
 window.fetch('/static/terms-of-service.html')
@@ -120,13 +129,19 @@ window.fetch('/static/terms-of-service.html')
   })
 
 window.fetch('/api/pleroma/emoji.json')
-  .then((res) => res.json())
-  .then((values) => {
-    const emoji = Object.keys(values).map((key) => {
-      return { shortcode: key, image_url: values[key] }
-    })
-    store.dispatch('setOption', { name: 'customEmoji', value: emoji })
-  })
+  .then(
+    (res) => res.json()
+      .then(
+        (values) => {
+          const emoji = Object.keys(values).map((key) => {
+            return { shortcode: key, image_url: values[key] }
+          })
+          store.dispatch('setOption', { name: 'emoji', value: emoji })
+        },
+        (failure) => {}
+      ),
+    (error) => console.log(error)
+  )
 
 window.fetch('/static/emoji.json')
   .then((res) => res.json())
diff --git a/src/modules/api.js b/src/modules/api.js
index e61382eb..c91fb97b 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -1,10 +1,13 @@
 import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
 import {isArray} from 'lodash'
+import { Socket } from 'phoenix'
 
 const api = {
   state: {
     backendInteractor: backendInteractorService(),
-    fetchers: {}
+    fetchers: {},
+    socket: null,
+    chatDisabled: false
   },
   mutations: {
     setBackendInteractor (state, backendInteractor) {
@@ -15,6 +18,12 @@ const api = {
     },
     removeFetcher (state, {timeline}) {
       delete state.fetchers[timeline]
+    },
+    setSocket (state, socket) {
+      state.socket = socket
+    },
+    setChatDisabled (state, value) {
+      state.chatDisabled = value
     }
   },
   actions: {
@@ -37,6 +46,17 @@ const api = {
       const fetcher = store.state.fetchers[timeline]
       window.clearInterval(fetcher)
       store.commit('removeFetcher', {timeline})
+    },
+    initializeSocket (store, token) {
+      // Set up websocket connection
+      if (!store.state.chatDisabled) {
+        let socket = new Socket('/socket', {params: {token: token}})
+        socket.connect()
+        store.dispatch('initializeChat', socket)
+      }
+    },
+    disableChat (store) {
+      store.commit('setChatDisabled', true)
     }
   }
 }
diff --git a/src/modules/chat.js b/src/modules/chat.js
new file mode 100644
index 00000000..b1244ebe
--- /dev/null
+++ b/src/modules/chat.js
@@ -0,0 +1,33 @@
+const chat = {
+  state: {
+    messages: [],
+    channel: null
+  },
+  mutations: {
+    setChannel (state, channel) {
+      state.channel = channel
+    },
+    addMessage (state, message) {
+      state.messages.push(message)
+      state.messages = state.messages.slice(-19, 20)
+    },
+    setMessages (state, messages) {
+      state.messages = messages.slice(-19, 20)
+    }
+  },
+  actions: {
+    initializeChat (store, socket) {
+      const channel = socket.channel('chat:public')
+      channel.on('new_msg', (msg) => {
+        store.commit('addMessage', msg)
+      })
+      channel.on('messages', ({messages}) => {
+        store.commit('setMessages', messages)
+      })
+      channel.join()
+      store.commit('setChannel', channel)
+    }
+  }
+}
+
+export default chat
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index d954b023..18191424 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -22,7 +22,8 @@ export const defaultState = {
       loading: false,
       followers: [],
       friends: [],
-      viewing: 'statuses'
+      viewing: 'statuses',
+      flushMarker: 0
     },
     public: {
       statuses: [],
@@ -36,7 +37,8 @@ export const defaultState = {
       loading: false,
       followers: [],
       friends: [],
-      viewing: 'statuses'
+      viewing: 'statuses',
+      flushMarker: 0
     },
     user: {
       statuses: [],
@@ -50,7 +52,8 @@ export const defaultState = {
       loading: false,
       followers: [],
       friends: [],
-      viewing: 'statuses'
+      viewing: 'statuses',
+      flushMarker: 0
     },
     publicAndExternal: {
       statuses: [],
@@ -64,7 +67,8 @@ export const defaultState = {
       loading: false,
       followers: [],
       friends: [],
-      viewing: 'statuses'
+      viewing: 'statuses',
+      flushMarker: 0
     },
     friends: {
       statuses: [],
@@ -78,7 +82,8 @@ export const defaultState = {
       loading: false,
       followers: [],
       friends: [],
-      viewing: 'statuses'
+      viewing: 'statuses',
+      flushMarker: 0
     },
     tag: {
       statuses: [],
@@ -92,7 +97,8 @@ export const defaultState = {
       loading: false,
       followers: [],
       friends: [],
-      viewing: 'statuses'
+      viewing: 'statuses',
+      flushMarker: 0
     }
   }
 }
@@ -381,7 +387,8 @@ export const mutations = {
       loading: false,
       followers: [],
       friends: [],
-      viewing: 'statuses'
+      viewing: 'statuses',
+      flushMarker: 0
     }
 
     state.timelines[timeline] = emptyTimeline
@@ -422,6 +429,9 @@ export const mutations = {
     each(notifications, (notification) => {
       notification.seen = true
     })
+  },
+  queueFlush (state, { timeline, id }) {
+    state.timelines[timeline].flushMarker = id
   }
 }
 
@@ -458,6 +468,9 @@ const statuses = {
       // Optimistic retweeting...
       commit('setRetweeted', { status, value: true })
       apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
+    },
+    queueFlush ({ rootState, commit }, { timeline, id }) {
+      commit('queueFlush', { timeline, id })
     }
   },
   mutations
diff --git a/src/modules/users.js b/src/modules/users.js
index 30f8dc27..8303ecc1 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -97,6 +97,10 @@ const users = {
                   // Set our new backend interactor
                   commit('setBackendInteractor', backendInteractorService(userCredentials))
 
+                  if (user.token) {
+                    store.dispatch('initializeSocket', user.token)
+                  }
+
                   // Start getting fresh tweets.
                   store.dispatch('startFetching', 'friends')
 
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 5de0a457..5b078bc8 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -43,6 +43,16 @@ let fetch = (url, options) => {
   return oldfetch(fullUrl, options)
 }
 
+// from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
+let utoa = (str) => {
+  // first we use encodeURIComponent to get percent-encoded UTF-8,
+  // then we convert the percent encodings into raw bytes which
+  // can be fed into btoa.
+  return btoa(encodeURIComponent(str)
+              .replace(/%([0-9A-F]{2})/g,
+                       (match, p1) => { return String.fromCharCode('0x' + p1) }))
+}
+
 // Params
 // cropH
 // cropW
@@ -156,7 +166,7 @@ const register = (params) => {
 
 const authHeaders = (user) => {
   if (user && user.username && user.password) {
-    return { 'Authorization': `Basic ${btoa(`${user.username}:${user.password}`)}` }
+    return { 'Authorization': `Basic ${utoa(`${user.username}:${user.password}`)}` }
   } else {
     return { }
   }
@@ -281,6 +291,8 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
     url += `/${tag}.json`
   }
 
+  params.push(['count', 20])
+
   const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
   url += `?${queryString}`
 
diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js
new file mode 100644
index 00000000..13dd8979
--- /dev/null
+++ b/src/services/color_convert/color_convert.js
@@ -0,0 +1,34 @@
+import { map } from 'lodash'
+
+const rgb2hex = (r, g, b) => {
+  [r, g, b] = map([r, g, b], (val) => {
+    val = Math.ceil(val)
+    val = val < 0 ? 0 : val
+    val = val > 255 ? 255 : val
+    return val
+  })
+  return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
+}
+
+const hex2rgb = (hex) => {
+  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
+  return result ? {
+    r: parseInt(result[1], 16),
+    g: parseInt(result[2], 16),
+    b: parseInt(result[3], 16)
+  } : null
+}
+
+const rgbstr2hex = (rgb) => {
+  if (rgb[0] === '#') {
+    return rgb
+  }
+  rgb = rgb.match(/\d+/g)
+  return `#${((Number(rgb[0]) << 16) + (Number(rgb[1]) << 8) + Number(rgb[2])).toString(16)}`
+}
+
+export {
+  rgb2hex,
+  hex2rgb,
+  rgbstr2hex
+}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 62296e79..6863bd0a 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,4 +1,5 @@
-import { times, map } from 'lodash'
+import { times } from 'lodash'
+import { rgb2hex, hex2rgb } from '../color_convert/color_convert.js'
 
 // While this is not used anymore right now, I left it in if we want to do custom
 // styles that aren't just colors, so user can pick from a few different distinct
@@ -56,16 +57,6 @@ const setStyle = (href, commit) => {
   cssEl.addEventListener('load', setDynamic)
 }
 
-const rgb2hex = (r, g, b) => {
-  [r, g, b] = map([r, g, b], (val) => {
-    val = Math.ceil(val)
-    val = val < 0 ? 0 : val
-    val = val > 255 ? 255 : val
-    return val
-  })
-  return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
-}
-
 const setColors = (col, commit) => {
   const head = document.head
   const body = document.body
@@ -82,6 +73,7 @@ const setColors = (col, commit) => {
   if (isDark) {
     mod = mod * -1
   }
+
   colors['base00'] = rgb2hex(col.bg.r, col.bg.g, col.bg.b)                         // background
   colors['base01'] = rgb2hex((col.bg.r + col.fg.r) / 2, (col.bg.g + col.fg.g) / 2, (col.bg.b + col.fg.b) / 2) // hilighted bg
   colors['base02'] = rgb2hex(col.fg.r, col.fg.g, col.fg.b)                         // panels & buttons
@@ -91,11 +83,13 @@ const setColors = (col, commit) => {
   colors['base06'] = rgb2hex(col.text.r - mod, col.text.g - mod, col.text.b - mod) // strong text
   colors['base07'] = rgb2hex(col.text.r - mod * 2, col.text.g - mod * 2, col.text.b - mod * 2)
   colors['base08'] = rgb2hex(col.link.r, col.link.g, col.link.b)                   // links
+  colors['base09'] = rgb2hex((col.bg.r + col.text.r) / 2, (col.bg.g + col.text.g) / 2, (col.bg.b + col.text.b) / 2) // icons
 
-  times(9, (n) => {
-    const color = colors[`base0${8 - n}`]
-    styleSheet.insertRule(`.base0${8 - n} { color: ${color}`, 'index-max')
-    styleSheet.insertRule(`.base0${8 - n}-background { background-color: ${color}`, 'index-max')
+  const num = 10
+  times(num, (n) => {
+    const color = colors[`base0${num - 1 - n}`]
+    styleSheet.insertRule(`.base0${num - 1 - n} { color: ${color}`, 'index-max')
+    styleSheet.insertRule(`.base0${num - 1 - n}-background { background-color: ${color}`, 'index-max')
   })
 
   styleSheet.insertRule(`a { color: ${colors['base08']}`, 'index-max')
@@ -108,15 +102,6 @@ const setColors = (col, commit) => {
   commit('setOption', { name: 'customTheme', value: col })
 }
 
-const hex2rgb = (hex) => {
-  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
-  return result ? {
-    r: parseInt(result[1], 16),
-    g: parseInt(result[2], 16),
-    b: parseInt(result[3], 16)
-  } : null
-}
-
 const setPreset = (val, commit) => {
   window.fetch('/static/styles.json')
     .then((data) => data.json())
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 6b76eb54..a4a80df0 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -29,12 +29,19 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
   args['tag'] = tag
 
   return apiService.fetchTimeline(args)
-    .then((statuses) => update({store, statuses, timeline, showImmediately}),
-      () => store.dispatch('setError', { value: true }))
+    .then((statuses) => {
+      if (!older && statuses.length >= 20) {
+        store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
+      }
+      update({store, statuses, timeline, showImmediately})
+    }, () => store.dispatch('setError', { value: true }))
 }
 
 const startFetching = ({timeline = 'friends', credentials, store, userId = false, tag = false}) => {
-  fetchAndUpdate({timeline, credentials, store, showImmediately: true, userId, tag})
+  const rootState = store.rootState || store.state
+  const timelineData = rootState.statuses.timelines[camelCase(timeline)]
+  const showImmediately = timelineData.visibleStatuses.length === 0
+  fetchAndUpdate({timeline, credentials, store, showImmediately, userId, tag})
   const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag })
   return setInterval(boundFetchAndUpdate, 10000)
 }
diff --git a/static/config.json b/static/config.json
index 8b596992..880efca8 100644
--- a/static/config.json
+++ b/static/config.json
@@ -3,5 +3,7 @@
   "theme": "pleroma-dark",
   "background": "/static/bg.jpg",
   "logo": "/static/logo.png",
-  "registrationOpen": false
+  "registrationOpen": false,
+  "defaultPath": "/main/all",
+  "chatDisabled": false
 }
diff --git a/yarn.lock b/yarn.lock
index d0d2dde9..3fcd29ab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4226,6 +4226,10 @@ phantomjs-prebuilt@^2.1.3, phantomjs-prebuilt@^2.1.7:
     request-progress "~2.0.1"
     which "~1.2.10"
 
+phoenix@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.3.0.tgz#1df2c27f986ee295e37c9983ec28ebac1d7f4a3e"
+
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"