From 015bf78dd9b17a1eac11119e38d8e83be8d791ae Mon Sep 17 00:00:00 2001
From: shpuld <shpuld@gmail.com>
Date: Sun, 26 Feb 2017 22:36:54 +0200
Subject: [PATCH 01/24] Heading text floats left, initial attempt at load more
 in the right side of the heading.

---
 src/App.scss                                         |  4 ++--
 src/components/timeline/timeline.vue                 | 12 +++++++++---
 .../user_card_content/user_card_content.vue          |  5 +++++
 3 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/src/App.scss b/src/App.scss
index 0945c76b..8a1942c6 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -109,8 +109,8 @@ main-router {
 .panel-heading {
     border-radius: 10px 10px 0 0;
     background-size: cover;
-    padding: 0.6em 0;
-    text-align: center;
+    padding: 0.6em 1.0em;
+    text-align: left;
     font-size: 1.3em;
     line-height: 24px;
 }
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 45886f6c..078d954c 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,7 +1,7 @@
 <template>
   <div class="timeline">
     <a href="#" v-on:click.prevent='showNewStatuses()' v-if="timeline.newStatusCount > 0">
-      <div class="base01-background base05-border new-status-notification">
+      <div class="base00-background base05-border new-status-notification">
         <p class="text-center" >
           {{timeline.newStatusCount}} new statuses
         </p>
@@ -22,12 +22,18 @@
 <style lang="scss">
  .new-status-notification {
    border-style: solid;
-   border-width: 1px 0 1px 0;
+   float: right;
+   border-width: 1px 1px 0px 1px;
+   border-radius: 5px 5px 0 0;
    font-size: 1.1em;
+   margin-top: -2.08em;
+   margin-right: 0.8em;
+   max-width: 200px;
+   max-height: 2em;
 
    p {
      margin: 0px;
-     padding: 10px;
+     padding: 0.6em;
    }
  }
 </style>
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 178100b8..aa9a15e1 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -106,6 +106,11 @@
 .profile-panel-background {
   background-size: cover;
   border-radius: 10px;
+
+  .panel-heading {
+    padding: 0.6em 0em;
+    text-align: center;
+  }
 }
 
 .profile-panel-body {

From 22e8258a56ab0828231bc0e510b52dd39eebb5c7 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Sun, 5 Mar 2017 11:56:28 +0100
Subject: [PATCH 02/24] Highlight current notice in conversation-page, add
 backlinks

---
 src/components/status/status.js  | 8 +++++++-
 src/components/status/status.vue | 9 ++++++++-
 src/main.js                      | 5 ++++-
 3 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/src/components/status/status.js b/src/components/status/status.js
index 22292ffa..9550c19f 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -4,6 +4,7 @@ import RetweetButton from '../retweet_button/retweet_button.vue'
 import DeleteButton from '../delete_button/delete_button.vue'
 import PostStatusForm from '../post_status_form/post_status_form.vue'
 import UserCardContent from '../user_card_content/user_card_content.vue'
+import { toInteger } from 'lodash'
 
 const Status = {
   props: [
@@ -30,7 +31,12 @@ const Status = {
     loggedIn () {
       return !!this.$store.state.users.currentUser
     },
-    muted () { return !this.unmuted && this.status.user.muted }
+    muted () { return !this.unmuted && this.status.user.muted },
+    focused () {
+      const id = toInteger(this.$route.params.id)
+      return (this.statusoid.id == id)
+    },
+    isReply () { return !!this.statusoid.in_reply_to_status_id }
   },
   components: {
     Attachment,
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 585bf621..6476e1e5 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="status-el base00-background" v-if="!status.deleted" v-bind:class="{ 'expanded-status': !expandable }">
+  <div class="status-el base00-background" v-if="!status.deleted" v-bind:class="[{ 'expanded-status': !expandable }, { 'base01-background': focused }]">
     <template v-if="muted">
       <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>
@@ -34,6 +34,13 @@
                   {{status.in_reply_to_screen_name}}
                 </router-link>
               </small>
+              <template v-if="isReply">
+                  <small>
+                      <router-link :to="{ name: 'conversation', params: { id: status.in_reply_to_status_id } }">
+                          <i class="icon-reply"></i>
+                      </router-link>
+                  </small>
+              </template>
               -
               <small>
                 <router-link :to="{ name: 'conversation', params: { id: status.id } }">
diff --git a/src/main.js b/src/main.js
index fa0a872f..30929f0b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -53,7 +53,7 @@ const routes = [
   { path: '/main/all', component: PublicAndExternalTimeline },
   { path: '/main/public', component: PublicTimeline },
   { path: '/main/friends', component: FriendsTimeline },
-  { name: 'conversation', path: '/notice/:id', component: ConversationPage },
+  { 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 }
@@ -63,6 +63,9 @@ 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 }
   }
 })

From 289326855caa5e763439f89f2829c430056e79c3 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Sun, 5 Mar 2017 16:31:01 +0100
Subject: [PATCH 03/24] Add focused prop to status

---
 src/components/conversation/conversation.js                | 3 +++
 src/components/conversation/conversation.vue               | 2 +-
 src/components/status/status.js                            | 7 ++-----
 .../status_or_conversation/status_or_conversation.vue      | 2 +-
 4 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index ecc76e71..524737fd 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -47,6 +47,9 @@ const conversation = {
           .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
           .then(() => this.fetchConversation())
       }
+    },
+    focused: function(id) {
+      return (id == this.statusoid.id)
     }
   }
 }
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 9675e69f..8803dd57 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -8,7 +8,7 @@
     </div>
     <div class="panel-body">
       <div class="timeline">
-        <status v-for="status in conversation" :key="status.id" v-bind:statusoid="status":expandable='false'></status>
+        <status v-for="status in conversation" :key="status.id" v-bind:statusoid="status":expandable='false':focused="focused(status.id)"></status>
       </div>
     </div>
   </div>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 9550c19f..f1682e68 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -9,7 +9,8 @@ import { toInteger } from 'lodash'
 const Status = {
   props: [
     'statusoid',
-    'expandable'
+    'expandable',
+    'focused'
   ],
   data: () => ({
     replying: false,
@@ -32,10 +33,6 @@ const Status = {
       return !!this.$store.state.users.currentUser
     },
     muted () { return !this.unmuted && this.status.user.muted },
-    focused () {
-      const id = toInteger(this.$route.params.id)
-      return (this.statusoid.id == id)
-    },
     isReply () { return !!this.statusoid.in_reply_to_status_id }
   },
   components: {
diff --git a/src/components/status_or_conversation/status_or_conversation.vue b/src/components/status_or_conversation/status_or_conversation.vue
index 4fabfab2..4aaaf2ff 100644
--- a/src/components/status_or_conversation/status_or_conversation.vue
+++ b/src/components/status_or_conversation/status_or_conversation.vue
@@ -1,7 +1,7 @@
 <template>
   <div>
     <conversation v-if="expanded" @toggleExpanded="toggleExpanded" :collapsable="true" :statusoid="statusoid"></conversation>
-    <status v-if="!expanded" @toggleExpanded="toggleExpanded" :expandable="true" :statusoid="statusoid"></status>
+    <status v-if="!expanded" @toggleExpanded="toggleExpanded" :expandable="true" :statusoid="statusoid" :focused="false"></status>
   </div>
 </template>
 

From 42ae57dace4089141e2f31351119dc80d8e7cc4d Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Sun, 5 Mar 2017 16:49:45 +0100
Subject: [PATCH 04/24] Remove unused import

---
 src/components/status/status.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/status/status.js b/src/components/status/status.js
index f1682e68..101a0e14 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -4,7 +4,6 @@ import RetweetButton from '../retweet_button/retweet_button.vue'
 import DeleteButton from '../delete_button/delete_button.vue'
 import PostStatusForm from '../post_status_form/post_status_form.vue'
 import UserCardContent from '../user_card_content/user_card_content.vue'
-import { toInteger } from 'lodash'
 
 const Status = {
   props: [

From 51160f0fdb068f0c9a1133fe66286467b1f4fd78 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Sun, 5 Mar 2017 20:36:22 +0100
Subject: [PATCH 05/24] Style fixes.

---
 src/components/conversation/conversation.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 524737fd..a598b521 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -48,8 +48,8 @@ const conversation = {
           .then(() => this.fetchConversation())
       }
     },
-    focused: function(id) {
-      return (id == this.statusoid.id)
+    focused: function (id) {
+      return (id === this.statusoid.id)
     }
   }
 }

From 9d56721533d46cbd88db8e64edb8d6fddf61298e Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Mon, 6 Mar 2017 16:21:11 +0100
Subject: [PATCH 06/24] Highlight original notice when expanding retweets

---
 src/components/conversation/conversation.js | 6 +++++-
 src/components/status/status.js             | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index a598b521..769893d3 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -49,7 +49,11 @@ const conversation = {
       }
     },
     focused: function (id) {
-      return (id === this.statusoid.id)
+      if (!!this.statusoid.retweeted_status) {
+        return (id === this.statusoid.retweeted_status.id)
+      } else {
+        return (id === this.statusoid.id)
+      }
     }
   }
 }
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 101a0e14..bc9d6e6c 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -32,7 +32,7 @@ const Status = {
       return !!this.$store.state.users.currentUser
     },
     muted () { return !this.unmuted && this.status.user.muted },
-    isReply () { return !!this.statusoid.in_reply_to_status_id }
+    isReply () { return !!this.status.in_reply_to_status_id }
   },
   components: {
     Attachment,

From 0bb1ec30d0ccba0fded1c6555d75297a6d9c83d2 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Mon, 6 Mar 2017 19:55:08 +0100
Subject: [PATCH 07/24] Temporarily remove persistence.

---
 src/main.js | 12 ------------
 1 file changed, 12 deletions(-)

diff --git a/src/main.js b/src/main.js
index 30929f0b..b448510e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -17,8 +17,6 @@ import configModule from './modules/config.js'
 
 import VueTimeago from 'vue-timeago'
 
-import createPersistedState from './lib/persisted_state.js'
-
 Vue.use(Vuex)
 Vue.use(VueRouter)
 Vue.use(VueTimeago, {
@@ -28,15 +26,6 @@ Vue.use(VueTimeago, {
   }
 })
 
-const persistedStateOptions = {
-  paths: [
-    'config.hideAttachments',
-    'config.hideNsfw',
-    'statuses.notifications',
-    'users.users'
-  ]
-}
-
 const store = new Vuex.Store({
   modules: {
     statuses: statusesModule,
@@ -44,7 +33,6 @@ const store = new Vuex.Store({
     api: apiModule,
     config: configModule
   },
-  plugins: [createPersistedState(persistedStateOptions)],
   strict: process.env.NODE_ENV !== 'production'
 })
 

From 990047725a4f6c7bbda1e7519a605b1ae1ece51e Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Mon, 6 Mar 2017 19:57:00 +0100
Subject: [PATCH 08/24] Revert "Temporarily remove persistence."

This reverts commit 0bb1ec30d0ccba0fded1c6555d75297a6d9c83d2.
---
 src/main.js | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/src/main.js b/src/main.js
index b448510e..30929f0b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -17,6 +17,8 @@ import configModule from './modules/config.js'
 
 import VueTimeago from 'vue-timeago'
 
+import createPersistedState from './lib/persisted_state.js'
+
 Vue.use(Vuex)
 Vue.use(VueRouter)
 Vue.use(VueTimeago, {
@@ -26,6 +28,15 @@ Vue.use(VueTimeago, {
   }
 })
 
+const persistedStateOptions = {
+  paths: [
+    'config.hideAttachments',
+    'config.hideNsfw',
+    'statuses.notifications',
+    'users.users'
+  ]
+}
+
 const store = new Vuex.Store({
   modules: {
     statuses: statusesModule,
@@ -33,6 +44,7 @@ const store = new Vuex.Store({
     api: apiModule,
     config: configModule
   },
+  plugins: [createPersistedState(persistedStateOptions)],
   strict: process.env.NODE_ENV !== 'production'
 })
 

From a4ebf44f242fd877506dcdadf035840168e16364 Mon Sep 17 00:00:00 2001
From: shpuld <shpuld@gmail.com>
Date: Mon, 6 Mar 2017 23:51:39 +0200
Subject: [PATCH 09/24] Fix overlapping styles in timeline and notifications
 that screwed up user-card.

---
 src/components/notifications/notifications.scss |  2 +-
 src/components/notifications/notifications.vue  |  2 +-
 src/components/timeline/timeline.vue            | 17 +++++++++--------
 3 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 1378e730..8e3e0721 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -6,7 +6,7 @@
     // force the text to stay centered, while keeping
     // the button in the right side of the panel heading
     position: relative;
-    button {
+    .read-button {
       position: absolute;
       padding: 0.1em 0.3em 0.25em 0.3em;
       right: 0.7em;
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 180b86a1..e0dcb12d 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -3,7 +3,7 @@
     <div class="panel panel-default base00-background">
       <div class="panel-heading base01-background base04">
         Notifications ({{unseenCount}})
-        <button @click.prevent="markAsSeen" class="base06 base02-background">Read!</button>
+        <button @click.prevent="markAsSeen" class="base06 base02-background read-button">Read!</button>
       </div>
       <div class="panel-body">
         <div v-for="notification in visibleNotifications" class="notification" :class='{"unseen": !notification.seen}'>
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 8a302e18..11280a08 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,13 +1,13 @@
 <template>
   <div class="timeline panel panel-default">
-    <div class="panel-heading base01-background base04">
+    <div class="panel-heading timeline-heading base01-background base04">
       <div class="title">
         {{title}}
       </div>
-      <button @click.prevent="showNewStatuses" class="base06 base02-background" v-if="timeline.newStatusCount > 0">
+      <button @click.prevent="showNewStatuses" class="base06 base02-background loadmore-button" v-if="timeline.newStatusCount > 0">
         Show new ({{timeline.newStatusCount}})
       </button>
-      <button @click.prevent class="base04 base01-background no-press" v-if="!timeline.newStatusCount > 0">
+      <button @click.prevent class="base04 base01-background no-press loadmore-button" v-if="!timeline.newStatusCount > 0">
         Up-to-date
       </button>
     </div>
@@ -26,17 +26,18 @@
 
 <style lang="scss">
 
-  .timeline .panel-heading {
-    position: relative;
-    display: flex;
-
+  .timeline {
+    .timeline-heading {
+      position: relative;
+      display: flex;
+    }
     .title {
       white-space: nowrap;
       overflow: hidden;
       text-overflow: ellipsis;
       max-width: 70%;
     }
-    button {
+    .loadmore-button {
       position: absolute;
       right: 0.6em;
       padding: 0.1em 0.3em 0.25em 0.3em;

From 5143ae7f72c7535872188d9887dc61628add09fb Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Tue, 7 Mar 2017 08:43:48 +0100
Subject: [PATCH 10/24] Switch to localforage to use indexeddb by default.

---
 package.json               |   2 +-
 src/lib/persisted_state.js |  47 ++++------------
 yarn.lock                  | 112 +++++++++++++++++++++++++++++++++----
 3 files changed, 114 insertions(+), 47 deletions(-)

diff --git a/package.json b/package.json
index e7aa7241..74706389 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
     "babel-plugin-lodash": "^3.2.11",
     "diff": "^3.0.1",
     "karma-mocha-reporter": "^2.2.1",
-    "lz-string": "^1.4.4",
+    "localforage": "^1.5.0",
     "node-sass": "^3.10.1",
     "object-path": "^0.11.3",
     "sanitize-html": "^1.13.0",
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index 6a17ca99..a518cb51 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -1,7 +1,7 @@
 import merge from 'lodash.merge'
 import objectPath from 'object-path'
+import localforage from 'localforage'
 import { throttle } from 'lodash'
-import lzstring from 'lz-string'
 
 const defaultReducer = (state, paths) => (
   paths.length === 0 ? state : paths.reduce((substate, path) => {
@@ -11,32 +11,11 @@ const defaultReducer = (state, paths) => (
 )
 
 const defaultStorage = (() => {
-  const hasLocalStorage = typeof window !== 'undefined' && window.localStorage
-  if (hasLocalStorage) {
-    return window.localStorage
-  }
-
-  class InternalStorage {
-    setItem (key, item) {
-      this[key] = item
-      return item
-    }
-    getItem (key) {
-      return this[key]
-    }
-    removeItem (key) {
-      delete this[key]
-    }
-    clear () {
-      Object.keys(this).forEach(key => delete this[key])
-    }
-  }
-
-  return new InternalStorage()
+  return localforage
 })()
 
 const defaultSetState = (key, state, storage) => {
-  return storage.setItem(key, lzstring.compressToUTF16(JSON.stringify(state)))
+  return storage.setItem(key, state)
 }
 
 export default function createPersistedState ({
@@ -44,12 +23,7 @@ export default function createPersistedState ({
   paths = [],
   getState = (key, storage) => {
     let value = storage.getItem(key)
-    try {
-      value = lzstring.decompressFromUTF16(value) // inflate(value, { to: 'string' })
-    } catch (e) {
-      console.log("Couldn't inflate value... Maybe upgrading")
-    }
-    return value && value !== 'undefined' ? JSON.parse(value) : undefined
+    return value
   },
   setState = throttle(defaultSetState, 60000),
   reducer = defaultReducer,
@@ -57,12 +31,13 @@ export default function createPersistedState ({
   subscriber = store => handler => store.subscribe(handler)
 } = {}) {
   return store => {
-    const savedState = getState(key, storage)
-    if (typeof savedState === 'object') {
-      store.replaceState(
-        merge({}, store.state, savedState)
-      )
-    }
+    getState(key, storage).then((savedState) => {
+      if (typeof savedState === 'object') {
+        store.replaceState(
+          merge({}, store.state, savedState)
+        )
+      }
+    })
 
     subscriber(store)((mutation, state) => {
       try {
diff --git a/yarn.lock b/yarn.lock
index 92f734bc..677c9690 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -23,6 +23,10 @@ acorn@4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
 
+acorn@^1.0.3:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014"
+
 acorn@^3.0.0, acorn@^3.0.4, acorn@^3.1.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
@@ -193,11 +197,11 @@ ast-traverse@~0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/ast-traverse/-/ast-traverse-0.1.1.tgz#69cf2b8386f19dcda1bb1e05d68fe359d8897de6"
 
-ast-types@0.8.12:
+ast-types@0.8.12, ast-types@0.x.x:
   version "0.8.12"
   resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.12.tgz#a0d90e4351bb887716c83fd637ebf818af4adfcc"
 
-ast-types@0.9.5, ast-types@0.x.x:
+ast-types@0.9.5:
   version "0.9.5"
   resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.5.tgz#1a660a09945dbceb1f9c9cbb715002617424e04a"
 
@@ -836,6 +840,10 @@ balanced-match@^0.4.1, balanced-match@^0.4.2:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
 
+base62@0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/base62/-/base62-0.1.1.tgz#7b4174c2f94449753b11c2651c083da841a7b084"
+
 base64-arraybuffer@0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
@@ -1815,6 +1823,14 @@ error-ex@^1.2.0:
   dependencies:
     is-arrayish "^0.2.1"
 
+es3ify@^0.1.3:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/es3ify/-/es3ify-0.1.4.tgz#ad9fa5df1ae34f3f31e1211b5818b2d51078dfd1"
+  dependencies:
+    esprima-fb "~3001.0001.0000-dev-harmony-fb"
+    jstransform "~3.0.0"
+    through "~2.3.4"
+
 es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7:
   version "0.10.12"
   resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047"
@@ -1978,6 +1994,10 @@ eslint@^3.7.1:
     text-table "~0.2.0"
     user-home "^2.0.0"
 
+esmangle-evaluator@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/esmangle-evaluator/-/esmangle-evaluator-1.0.1.tgz#620d866ef4861b3311f75766d52a8572bb3c6336"
+
 espree@^3.4.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.0.tgz#41656fa5628e042878025ef467e78f125cb86e1d"
@@ -1989,6 +2009,10 @@ esprima-fb@~15001.1001.0-dev-harmony-fb:
   version "15001.1001.0-dev-harmony-fb"
   resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659"
 
+esprima-fb@~3001.0001.0000-dev-harmony-fb, esprima-fb@~3001.1.0-dev-harmony-fb:
+  version "3001.1.0-dev-harmony-fb"
+  resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz#b77d37abcd38ea0b77426bb8bc2922ce6b426411"
+
 esprima@2.7.x, esprima@^2.1.0, esprima@^2.6.0, esprima@^2.7.1:
   version "2.7.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
@@ -2136,6 +2160,15 @@ extsprintf@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
 
+falafel@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/falafel/-/falafel-1.2.0.tgz#c18d24ef5091174a497f318cd24b026a25cddab4"
+  dependencies:
+    acorn "^1.0.3"
+    foreach "^2.0.5"
+    isarray "0.0.1"
+    object-keys "^1.0.6"
+
 fast-levenshtein@~2.0.4:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -2248,6 +2281,10 @@ for-own@^0.1.4:
   dependencies:
     for-in "^0.1.5"
 
+foreach@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+
 forever-agent@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -2683,6 +2720,10 @@ ignore@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410"
 
+immediate@~3.0.5:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+
 imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@@ -2730,6 +2771,13 @@ inject-loader@^2.0.1:
   dependencies:
     loader-utils "^0.2.3"
 
+inline-process-browser@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/inline-process-browser/-/inline-process-browser-1.0.0.tgz#46a61b153dd3c9b1624b1a00626edb4f7f414f22"
+  dependencies:
+    falafel "^1.0.1"
+    through2 "^0.6.5"
+
 inquirer@^0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
@@ -3092,6 +3140,14 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.3.6"
 
+jstransform@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-3.0.0.tgz#a2591ab6cee8d97bf3be830dbfa2313b87cd640b"
+  dependencies:
+    base62 "0.1.1"
+    esprima-fb "~3001.1.0-dev-harmony-fb"
+    source-map "0.1.31"
+
 karma-coverage@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-1.1.1.tgz#5aff8b39cf6994dc22de4c84362c76001b637cf6"
@@ -3214,6 +3270,15 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+lie@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/lie/-/lie-3.0.2.tgz#ffda21d7bba26f377cad865d3649b2fc8ce39fea"
+  dependencies:
+    es3ify "^0.1.3"
+    immediate "~3.0.5"
+    inline-process-browser "^1.0.0"
+    unreachable-branch-transform "^0.3.0"
+
 load-json-file@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -3233,6 +3298,12 @@ loader-utils@0.2.x, loader-utils@^0.2.10, loader-utils@^0.2.11, loader-utils@^0.
     json5 "^0.5.0"
     object-assign "^4.0.1"
 
+localforage@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.5.0.tgz#6b994e19b56611fa85df3992df397ac4ab66e815"
+  dependencies:
+    lie "3.0.2"
+
 lodash._arraycopy@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1"
@@ -3549,10 +3620,6 @@ lru-cache@~2.6.5:
   version "2.6.5"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5"
 
-lz-string@^1.4.4:
-  version "1.4.4"
-  resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
-
 macaddress@^0.2.8:
   version "0.2.8"
   resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
@@ -3936,6 +4003,10 @@ object-hash@^1.1.4:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.1.5.tgz#bdd844e030d0861b692ca175c6cab6868ec233d7"
 
+object-keys@^1.0.6:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+
 object-path@^0.11.3:
   version "0.11.3"
   resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.3.tgz#3e21a42ad07234d815429ae9e15c1c5f38050554"
@@ -4597,7 +4668,7 @@ read-pkg@^1.0.0:
     normalize-package-data "^2.3.2"
     path-type "^1.0.0"
 
-readable-stream@1.0, readable-stream@~1.0.2:
+readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.2:
   version "1.0.34"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
   dependencies:
@@ -4667,7 +4738,7 @@ readline2@^1.0.1:
     is-fullwidth-code-point "^1.0.0"
     mute-stream "0.0.5"
 
-recast@0.10.33:
+recast@0.10.33, recast@^0.10.1:
   version "0.10.33"
   resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.33.tgz#942808f7aa016f1fa7142c461d7e5704aaa8d697"
   dependencies:
@@ -5143,6 +5214,12 @@ source-map-support@^0.4.2:
   dependencies:
     source-map "^0.5.3"
 
+source-map@0.1.31:
+  version "0.1.31"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.31.tgz#9f704d0d69d9e138a81badf6ebb4fde33d151c61"
+  dependencies:
+    amdefine ">=0.0.4"
+
 source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3:
   version "0.5.6"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
@@ -5374,7 +5451,14 @@ throttleit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
 
-through@^2.3.6, through@~2.3.8:
+through2@^0.6.2, through2@^0.6.5:
+  version "0.6.5"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
+  dependencies:
+    readable-stream ">=1.0.33-1 <1.1.0-0"
+    xtend ">=4.0.0 <4.1.0-0"
+
+through@^2.3.6, through@~2.3.4, through@~2.3.8:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
 
@@ -5516,6 +5600,14 @@ unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
 
+unreachable-branch-transform@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/unreachable-branch-transform/-/unreachable-branch-transform-0.3.0.tgz#d99cc4c6e746d264928845b611db54b0f3474caa"
+  dependencies:
+    esmangle-evaluator "^1.0.0"
+    recast "^0.10.1"
+    through2 "^0.6.2"
+
 upper-case@^1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
@@ -5812,7 +5904,7 @@ xregexp@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
 
-xtend@^4.0.0:
+"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
 

From 58fdf9e70d56e23fe6c6cd73e31532ebde4caac1 Mon Sep 17 00:00:00 2001
From: shpuld <shpuld@gmail.com>
Date: Tue, 7 Mar 2017 15:55:00 +0200
Subject: [PATCH 11/24] Put the number of notifications inside a red circle
 (kinda like qvitter and everyone else does), make the red notification line
 slightly less transparent.

---
 src/components/notifications/notifications.scss | 15 ++++++++++++++-
 src/components/notifications/notifications.vue  |  3 ++-
 2 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 8e3e0721..6ad7ec1e 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -13,6 +13,19 @@
     }
   }
 
+  .unseen-count {
+    display: inline-block;
+    background-color: rgba(255, 16, 8, 0.8);
+    text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5);
+    min-width: 1.3em;
+    border-radius: 1.3em;
+    margin: 0 0.2em 0 -0.4em;
+    color: white;
+    font-size: 0.9em;
+    text-align: center;
+    line-height: 1.3em;
+  }
+
   .notification {
     // Will have to use pixels here to ensure consistent distance with
     // pad alone and pad + border, browsers bad at rounding this with em,
@@ -64,7 +77,7 @@
   }
 
   .unseen {
-    border-left: 4px solid rgba(255, 48, 16, 0.65);
+    border-left: 4px solid rgba(255, 16, 8, 0.75);
     padding-left: 6px;
   }
 }
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index e0dcb12d..91f6cfdc 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -2,7 +2,8 @@
   <div class="notifications">
     <div class="panel panel-default base00-background">
       <div class="panel-heading base01-background base04">
-        Notifications ({{unseenCount}})
+        <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
+        Notifications
         <button @click.prevent="markAsSeen" class="base06 base02-background read-button">Read!</button>
       </div>
       <div class="panel-body">

From ae570ea0ff10853d96465c59bbc6ab9e086786b2 Mon Sep 17 00:00:00 2001
From: shpuld <shpuld@gmail.com>
Date: Tue, 7 Mar 2017 16:00:45 +0200
Subject: [PATCH 12/24] Put conversation 'Collapse' back on the same line with
 the rest of the heading.'

---
 src/components/conversation/conversation.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 5f395f46..33a43e15 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -1,10 +1,10 @@
 <template>
   <div class="timeline panel panel-default base00-background">
-    <div class="panel-heading base01-background base04" style="justify-content:space-between;">
+    <div class="panel-heading base01-background base04">
       Conversation
-      <div v-if="collapsable">
+      <span v-if="collapsable" style="float:right;">
         <small><a href="#" @click.prevent="$emit('toggleExpanded')">Collapse</a></small>
-      </div>
+      </span>
     </div>
     <div class="panel-body">
       <div class="timeline">

From 086dd832d3d2c8103e7d6f99d72d27bea97ce7b0 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Tue, 7 Mar 2017 17:27:12 +0100
Subject: [PATCH 13/24] Visual feedback on failure to fetch new statuses

---
 src/components/timeline/timeline.vue           | 10 ++++++++--
 src/modules/statuses.js                        | 18 ++++++++++++++----
 .../timeline_fetcher.service.js                | 11 +++++++++++
 3 files changed, 33 insertions(+), 6 deletions(-)

diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 11280a08..ac074f3c 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -4,10 +4,13 @@
       <div class="title">
         {{title}}
       </div>
-      <button @click.prevent="showNewStatuses" class="base06 base02-background loadmore-button" v-if="timeline.newStatusCount > 0">
+      <button @click.prevent="showNewStatuses" class="base06 base02-background loadmore-button" v-if="timeline.newStatusCount > 0 && !timeline.error">
         Show new ({{timeline.newStatusCount}})
       </button>
-      <button @click.prevent class="base04 base01-background no-press loadmore-button" v-if="!timeline.newStatusCount > 0">
+      <button @click.prevent class="base06 error no-press loadmore-button" v-if="timeline.error">
+          Error fetching updates
+      </button>
+      <button @click.prevent class="base04 base01-background no-press loadmore-button" v-if="!timeline.newStatusCount > 0 && !timeline.error">
         Up-to-date
       </button>
     </div>
@@ -43,6 +46,9 @@
       padding: 0.1em 0.3em 0.25em 0.3em;
       min-width: 6em;
     }
+    .error {
+      background-color: rgba(255, 48, 16, 0.65);
+    }
     .no-press {
       opacity: 0.8;
       cursor: default;
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 491d0024..b19109b2 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -15,7 +15,8 @@ export const defaultState = {
       newStatusCount: 0,
       maxId: 0,
       minVisibleId: 0,
-      loading: false
+      loading: false,
+      error: false
     },
     public: {
       statuses: [],
@@ -24,7 +25,8 @@ export const defaultState = {
       newStatusCount: 0,
       maxId: 0,
       minVisibleId: 0,
-      loading: false
+      loading: false,
+      error: false
     },
     publicAndExternal: {
       statuses: [],
@@ -33,7 +35,8 @@ export const defaultState = {
       newStatusCount: 0,
       maxId: 0,
       minVisibleId: 0,
-      loading: false
+      loading: false,
+      error: false
     },
     friends: {
       statuses: [],
@@ -42,7 +45,8 @@ export const defaultState = {
       newStatusCount: 0,
       maxId: 0,
       minVisibleId: 0,
-      loading: false
+      loading: false,
+      error: false
     }
   }
 }
@@ -280,6 +284,9 @@ export const mutations = {
     const newStatus = find(state.allStatuses, { id })
     newStatus.nsfw = nsfw
   },
+  setError (state, { timeline, value }) {
+    state.timelines[timeline].error = value
+  },
   markNotificationsAsSeen (state, notifications) {
     each(notifications, (notification) => {
       notification.seen = true
@@ -293,6 +300,9 @@ const statuses = {
     addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false }) {
       commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser })
     },
+    setError ({ rootState, commit }, { timeline, value }) {
+      commit('setError', { timeline, value })
+    },
     deleteStatus ({ rootState, commit }, status) {
       commit('setDeleted', { status })
       apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 37bbcd82..40f568c3 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -12,6 +12,15 @@ const update = ({store, statuses, timeline, showImmediately}) => {
   })
 }
 
+const setError = ({store, timeline, value}) => {
+  const ccTimeline = camelCase(timeline)
+
+  store.dispatch('setError', {
+    timeline: ccTimeline,
+    value
+  })
+}
+
 const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false}) => {
   const args = { timeline, credentials }
   const rootState = store.rootState || store.state
@@ -25,6 +34,8 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
 
   return apiService.fetchTimeline(args)
     .then((statuses) => update({store, statuses, timeline, showImmediately}))
+    .then(() => setError({store, timeline, value: false}))
+    .catch(() => setError({store, timeline, value: true}))
 }
 
 const startFetching = ({ timeline = 'friends', credentials, store }) => {

From 7d522583780cae5746112f204b3f8c604d5a87bd Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Tue, 7 Mar 2017 21:38:55 +0100
Subject: [PATCH 14/24] Move rejection handler

---
 src/services/timeline_fetcher/timeline_fetcher.service.js | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 40f568c3..e684a170 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -5,6 +5,8 @@ import apiService from '../api/api.service.js'
 const update = ({store, statuses, timeline, showImmediately}) => {
   const ccTimeline = camelCase(timeline)
 
+  setError({store, timeline, value: false})
+
   store.dispatch('addNewStatuses', {
     timeline: ccTimeline,
     statuses,
@@ -33,9 +35,8 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
   }
 
   return apiService.fetchTimeline(args)
-    .then((statuses) => update({store, statuses, timeline, showImmediately}))
-    .then(() => setError({store, timeline, value: false}))
-    .catch(() => setError({store, timeline, value: true}))
+    .then((statuses) => update({store, statuses, timeline, showImmediately}),
+      () => setError({store, timeline, value: true}))
 }
 
 const startFetching = ({ timeline = 'friends', credentials, store }) => {

From ccbfc64bfcaf5efd598af6987e304291e23def1c Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Wed, 8 Mar 2017 16:54:06 +0100
Subject: [PATCH 15/24] Don't redirect after login.

---
 src/components/login_form/login_form.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index b55f770f..827c704c 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -7,9 +7,7 @@ const LoginForm = {
   },
   methods: {
     submit () {
-      this.$store.dispatch('loginUser', this.user).then(() => {
-        this.$router.push('/main/friends')
-      })
+      this.$store.dispatch('loginUser', this.user)
     }
   }
 }

From bde12418433f3ae9d2ad3781343f334caf58cc95 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Wed, 8 Mar 2017 17:58:49 +0100
Subject: [PATCH 16/24] eslint fixes.

---
 src/components/conversation/conversation.js | 2 +-
 test/unit/specs/modules/statuses.spec.js    | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 769893d3..281b0183 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -49,7 +49,7 @@ const conversation = {
       }
     },
     focused: function (id) {
-      if (!!this.statusoid.retweeted_status) {
+      if (this.statusoid.retweeted_status) {
         return (id === this.statusoid.retweeted_status.id)
       } else {
         return (id === this.statusoid.id)
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index b1581e03..891423ca 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -1,6 +1,7 @@
 import { cloneDeep } from 'lodash'
 import { defaultState, mutations, findMaxId, prepareStatus, statusType } from '../../../../src/modules/statuses.js'
 
+// eslint-disable-next-line camelcase
 const makeMockStatus = ({id, text, is_post_verb = true}) => {
   return {
     id,

From 480a1ba253d49d4df36fbc64192e1260fc51a181 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Wed, 8 Mar 2017 17:59:12 +0100
Subject: [PATCH 17/24] Use cache to quickly access users.

---
 src/lib/persisted_state.js |  9 ++++++++-
 src/modules/users.js       | 10 ++++++----
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index a518cb51..a47ad7d5 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -1,7 +1,7 @@
 import merge from 'lodash.merge'
 import objectPath from 'object-path'
 import localforage from 'localforage'
-import { throttle } from 'lodash'
+import { throttle, each } from 'lodash'
 
 const defaultReducer = (state, paths) => (
   paths.length === 0 ? state : paths.reduce((substate, path) => {
@@ -33,6 +33,13 @@ export default function createPersistedState ({
   return store => {
     getState(key, storage).then((savedState) => {
       if (typeof savedState === 'object') {
+        // build user cache
+        const usersState = savedState.users || {}
+        usersState.usersObject = {}
+        const users = usersState.users || []
+        each(users, (user) => { usersState.usersObject[user.id] = user })
+        savedState.users = usersState
+
         store.replaceState(
           merge({}, store.state, savedState)
         )
diff --git a/src/modules/users.js b/src/modules/users.js
index 31731880..dc910c91 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -3,9 +3,9 @@ import { compact, map, each, find, merge } from 'lodash'
 import { set } from 'vue'
 
 // TODO: Unify with mergeOrAdd in statuses.js
-export const mergeOrAdd = (arr, item) => {
+export const mergeOrAdd = (arr, obj, item) => {
   if (!item) { return false }
-  const oldItem = find(arr, {id: item.id})
+  const oldItem = obj[item.id]
   if (oldItem) {
     // We already have this, so only merge the new info.
     merge(oldItem, item)
@@ -13,6 +13,7 @@ export const mergeOrAdd = (arr, item) => {
   } else {
     // This is a new item, prepare it
     arr.push(item)
+    obj[item.id] = item
     return {item, new: true}
   }
 }
@@ -32,7 +33,7 @@ export const mutations = {
     state.loggingIn = false
   },
   addNewUsers (state, users) {
-    each(users, (user) => mergeOrAdd(state.users, user))
+    each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
   },
   setUserForStatus (state, status) {
     status.user = find(state.users, status.user)
@@ -42,7 +43,8 @@ export const mutations = {
 export const defaultState = {
   currentUser: false,
   loggingIn: false,
-  users: []
+  users: [],
+  usersObject: {}
 }
 
 const users = {

From 5699872bb5e05b5dc576c75cac2c79b7ece3f2ac Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Wed, 8 Mar 2017 18:04:21 +0100
Subject: [PATCH 18/24] Use user cache in users module.

---
 src/modules/users.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index dc910c91..9367ec68 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,5 +1,5 @@
 import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
-import { compact, map, each, find, merge } from 'lodash'
+import { compact, map, each, merge } from 'lodash'
 import { set } from 'vue'
 
 // TODO: Unify with mergeOrAdd in statuses.js
@@ -20,7 +20,7 @@ export const mergeOrAdd = (arr, obj, item) => {
 
 export const mutations = {
   setMuted (state, { user: {id}, muted }) {
-    const user = find(state.users, {id})
+    const user = state.usersObject[id]
     set(user, 'muted', muted)
   },
   setCurrentUser (state, user) {
@@ -36,7 +36,7 @@ export const mutations = {
     each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
   },
   setUserForStatus (state, status) {
-    status.user = find(state.users, status.user)
+    status.user = state.usersObject[status.user.id]
   }
 }
 

From a6b6fe95c0fe2aa60ebbfca87fde47e629035c49 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Wed, 8 Mar 2017 18:28:41 +0100
Subject: [PATCH 19/24] Show visual feedback on login error, redirect on
 success

---
 src/components/login_form/login_form.js  |  8 ++-
 src/components/login_form/login_form.vue |  9 ++++
 src/modules/users.js                     | 68 +++++++++++++-----------
 3 files changed, 53 insertions(+), 32 deletions(-)

diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 827c704c..2ad5b0b5 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -1,13 +1,17 @@
 const LoginForm = {
   data: () => ({
-    user: {}
+    user: {},
+    authError: false
   }),
   computed: {
     loggingIn () { return this.$store.state.users.loggingIn }
   },
   methods: {
     submit () {
-      this.$store.dispatch('loginUser', this.user)
+      this.$store.dispatch('loginUser', this.user).then(
+        () => { this.$router.push('/main/friends')}, 
+        () => { this.authError = true }
+      )
     }
   }
 }
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index c0273bae..279469ee 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -17,6 +17,9 @@
         <div class='form-group'>
           <button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Submit</button>
         </div>
+        <div v-if="authError" class='form-group'>
+          <button disabled='true' class='btn btn-default base05 error'>Error logging in, try again</button>
+        </div>
       </form>
     </div>
   </div>
@@ -39,6 +42,12 @@
     margin-top: 1.0em;
     min-height: 28px;
   }
+
+  .error {
+    margin-top: 0em;
+    margin-bottom: 0em;
+    background-color: rgba(255, 48, 16, 0.65);
+  }
 }
 
 </style>
diff --git a/src/modules/users.js b/src/modules/users.js
index 31731880..a5274480 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -65,40 +65,48 @@ const users = {
       })
     },
     loginUser (store, userCredentials) {
-      const commit = store.commit
-      commit('beginLogin')
-      store.rootState.api.backendInteractor.verifyCredentials(userCredentials)
-        .then((response) => {
-          if (response.ok) {
-            response.json()
-              .then((user) => {
-                user.credentials = userCredentials
-                commit('setCurrentUser', user)
-                commit('addNewUsers', [user])
+      return new Promise((resolve, reject) => {
+        const commit = store.commit
+        commit('beginLogin')
+        store.rootState.api.backendInteractor.verifyCredentials(userCredentials)
+          .then((response) => {
+            if (response.ok) {
+              response.json()
+                .then((user) => {
+                  user.credentials = userCredentials
+                  commit('setCurrentUser', user)
+                  commit('addNewUsers', [user])
 
-                // Set our new backend interactor
-                commit('setBackendInteractor', backendInteractorService(userCredentials))
+                  // Set our new backend interactor
+                  commit('setBackendInteractor', backendInteractorService(userCredentials))
 
-                // Start getting fresh tweets.
-                store.dispatch('startFetching', 'friends')
+                  // Start getting fresh tweets.
+                  store.dispatch('startFetching', 'friends')
 
-                // Get user mutes and follower info
-                store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
-                  each(mutedUsers, (user) => { user.muted = true })
-                  store.commit('addNewUsers', mutedUsers)
+                  // Get user mutes and follower info
+                  store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
+                    each(mutedUsers, (user) => { user.muted = true })
+                    store.commit('addNewUsers', mutedUsers)
+                  })
+
+                  // Fetch our friends
+                  store.rootState.api.backendInteractor.fetchFriends()
+                    .then((friends) => commit('addNewUsers', friends))
                 })
-
-                // Fetch our friends
-                store.rootState.api.backendInteractor.fetchFriends()
-                  .then((friends) => commit('addNewUsers', friends))
-              })
-          }
-          commit('endLogin')
-        })
-        .catch((error) => {
-          console.log(error)
-          commit('endLogin')
-        })
+            } else {
+              // Authentication failed
+              commit('endLogin')
+              reject()
+            }
+            commit('endLogin')
+            resolve()
+          })
+          .catch((error) => {
+            console.log(error)
+            commit('endLogin')
+            reject()
+          })
+      })
     }
   }
 }

From c0e8111d642ca9f85fbb4091f2ac9e86f4238a58 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Wed, 8 Mar 2017 19:08:01 +0100
Subject: [PATCH 20/24] Clear username and password field on failed login

---
 src/components/login_form/login_form.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 2ad5b0b5..e489f381 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -10,7 +10,11 @@ const LoginForm = {
     submit () {
       this.$store.dispatch('loginUser', this.user).then(
         () => { this.$router.push('/main/friends')}, 
-        () => { this.authError = true }
+        () => {
+          this.authError = true
+          this.user.username = ''
+          this.user.password = ''
+        }
       )
     }
   }

From ccc460bb5ed1c8b7338f8a26bdb3029c74b26024 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Wed, 8 Mar 2017 19:22:56 +0100
Subject: [PATCH 21/24] Give more specific reason for failed login

---
 src/components/login_form/login_form.js  | 4 ++--
 src/components/login_form/login_form.vue | 2 +-
 src/modules/users.js                     | 8 ++++++--
 3 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index e489f381..bc801397 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -10,8 +10,8 @@ const LoginForm = {
     submit () {
       this.$store.dispatch('loginUser', this.user).then(
         () => { this.$router.push('/main/friends')}, 
-        () => {
-          this.authError = true
+        (error) => {
+          this.authError = error
           this.user.username = ''
           this.user.password = ''
         }
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index 279469ee..8a32e064 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -18,7 +18,7 @@
           <button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Submit</button>
         </div>
         <div v-if="authError" class='form-group'>
-          <button disabled='true' class='btn btn-default base05 error'>Error logging in, try again</button>
+          <button disabled='true' class='btn btn-default base05 error'>{{authError}}</button>
         </div>
       </form>
     </div>
diff --git a/src/modules/users.js b/src/modules/users.js
index a5274480..482c3b14 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -96,7 +96,11 @@ const users = {
             } else {
               // Authentication failed
               commit('endLogin')
-              reject()
+              if (response.status === 401) {
+                reject('Wrong username or password')
+              } else {
+                reject('An error occured, please try again')
+              }
             }
             commit('endLogin')
             resolve()
@@ -104,7 +108,7 @@ const users = {
           .catch((error) => {
             console.log(error)
             commit('endLogin')
-            reject()
+            reject('Failed to connect to server, try again')
           })
       })
     }

From 0810b2d51a6f0fbbfe1604f6d1954cde8ed08290 Mon Sep 17 00:00:00 2001
From: wakarimasen <wakarimasen@airmail.cc>
Date: Wed, 8 Mar 2017 19:31:39 +0100
Subject: [PATCH 22/24] Fix typo

---
 src/modules/users.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index 482c3b14..51643bd1 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -99,7 +99,7 @@ const users = {
               if (response.status === 401) {
                 reject('Wrong username or password')
               } else {
-                reject('An error occured, please try again')
+                reject('An error occurred, please try again')
               }
             }
             commit('endLogin')

From 0d39ed809b17dc7a3304f9a659c3d5e99d960f26 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Wed, 8 Mar 2017 21:04:48 +0100
Subject: [PATCH 23/24] Add caching system to statuses.

---
 src/modules/statuses.js | 34 ++++++++++++++++++++++++----------
 1 file changed, 24 insertions(+), 10 deletions(-)

diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index b19109b2..e4528520 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -4,14 +4,17 @@ import apiService from '../services/api/api.service.js'
 
 export const defaultState = {
   allStatuses: [],
+  allStatusesObject: {},
   maxId: 0,
   notifications: [],
   favorites: new Set(),
   timelines: {
     mentions: {
       statuses: [],
+      statusesObject: {},
       faves: [],
       visibleStatuses: [],
+      visibleStatusesObject: {},
       newStatusCount: 0,
       maxId: 0,
       minVisibleId: 0,
@@ -20,8 +23,10 @@ export const defaultState = {
     },
     public: {
       statuses: [],
+      statusesObject: {},
       faves: [],
       visibleStatuses: [],
+      visibleStatusesObject: {},
       newStatusCount: 0,
       maxId: 0,
       minVisibleId: 0,
@@ -30,8 +35,10 @@ export const defaultState = {
     },
     publicAndExternal: {
       statuses: [],
+      statusesObject: {},
       faves: [],
       visibleStatuses: [],
+      visibleStatusesObject: {},
       newStatusCount: 0,
       maxId: 0,
       minVisibleId: 0,
@@ -40,8 +47,10 @@ export const defaultState = {
     },
     friends: {
       statuses: [],
+      statusesObject: {},
       faves: [],
       visibleStatuses: [],
+      visibleStatusesObject: {},
       newStatusCount: 0,
       maxId: 0,
       minVisibleId: 0,
@@ -91,8 +100,9 @@ export const findMaxId = (...args) => {
   return (maxBy(flatten(args), 'id') || {}).id
 }
 
-const mergeOrAdd = (arr, item) => {
-  const oldItem = find(arr, {id: item.id})
+const mergeOrAdd = (arr, obj, item) => {
+  const oldItem = obj[item.id]
+
   if (oldItem) {
     // We already have this, so only merge the new info.
     merge(oldItem, item)
@@ -103,6 +113,7 @@ const mergeOrAdd = (arr, item) => {
     // This is a new item, prepare it
     prepareStatus(item)
     arr.push(item)
+    obj[item.id] = item
     return {item, new: true}
   }
 }
@@ -122,6 +133,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
   }
 
   const allStatuses = state.allStatuses
+  const allStatusesObject = state.allStatusesObject
   const timelineObject = state.timelines[timeline]
 
   // Set the maxId to the new id if it's larger.
@@ -131,7 +143,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
   }
 
   const addStatus = (status, showImmediately, addToTimeline = true) => {
-    const result = mergeOrAdd(allStatuses, status)
+    const result = mergeOrAdd(allStatuses, allStatusesObject, status)
     status = result.item
 
     if (result.new) {
@@ -147,7 +159,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
 
         // Add the mention to the mentions timeline
         if (timelineObject !== mentions) {
-          mergeOrAdd(mentions.statuses, status)
+          mergeOrAdd(mentions.statuses, mentions.statusesObject, status)
           mentions.newStatusCount += 1
 
           sortTimeline(mentions)
@@ -161,13 +173,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
     let resultForCurrentTimeline
     // Some statuses should only be added to the global status repository.
     if (timeline && addToTimeline) {
-      resultForCurrentTimeline = mergeOrAdd(timelineObject.statuses, status)
+      resultForCurrentTimeline = mergeOrAdd(timelineObject.statuses, timelineObject.statusesObject, status)
     }
 
     if (timeline && showImmediately) {
       // Add it directly to the visibleStatuses, don't change
       // newStatusCount
-      mergeOrAdd(timelineObject.visibleStatuses, status)
+      mergeOrAdd(timelineObject.visibleStatuses, timelineObject.visibleStatusesObject, status)
     } else if (timeline && addToTimeline && resultForCurrentTimeline.new) {
       // Just change newStatuscount
       timelineObject.newStatusCount += 1
@@ -264,24 +276,26 @@ export const mutations = {
 
     oldTimeline.newStatusCount = 0
     oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50)
+    oldTimeline.visibleStatusesObject = {}
+    each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
   },
   setFavorited (state, { status, value }) {
-    const newStatus = find(state.allStatuses, status)
+    const newStatus = state.allStatusesObject[status.id]
     newStatus.favorited = value
   },
   setRetweeted (state, { status, value }) {
-    const newStatus = find(state.allStatuses, status)
+    const newStatus = state.allStatusesObject[status.id]
     newStatus.repeated = value
   },
   setDeleted (state, { status }) {
-    const newStatus = find(state.allStatuses, status)
+    const newStatus = state.allStatusesObject[status.id]
     newStatus.deleted = true
   },
   setLoading (state, { timeline, value }) {
     state.timelines[timeline].loading = value
   },
   setNsfw (state, { id, nsfw }) {
-    const newStatus = find(state.allStatuses, { id })
+    const newStatus = state.allStatusesObject[id]
     newStatus.nsfw = nsfw
   },
   setError (state, { timeline, value }) {

From 9511691c9467e26c3237b4a2c936e8a757b3e515 Mon Sep 17 00:00:00 2001
From: shpuld <shpuld@gmail.com>
Date: Thu, 9 Mar 2017 02:21:23 +0200
Subject: [PATCH 24/24] Make the error into a div instead of a button to get
 rid of the hover effects.

---
 src/components/login_form/login_form.vue | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index 8a32e064..b2fa5341 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -18,7 +18,7 @@
           <button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Submit</button>
         </div>
         <div v-if="authError" class='form-group'>
-          <button disabled='true' class='btn btn-default base05 error'>{{authError}}</button>
+          <div class='error base05'>{{authError}}</div>
         </div>
       </form>
     </div>
@@ -44,9 +44,11 @@
   }
 
   .error {
-    margin-top: 0em;
-    margin-bottom: 0em;
+    border-radius: 5px;
+    text-align: center;
     background-color: rgba(255, 48, 16, 0.65);
+    min-height: 28px;
+    line-height: 28px;
   }
 }