From 3210283ae2783952c3724be188f2db8132885a38 Mon Sep 17 00:00:00 2001
From: dtluna <dtluna@openmailbox.org>
Date: Tue, 22 Nov 2016 21:45:18 +0300
Subject: [PATCH 01/14] Add audio player

---
 src/components/attachment/attachment.js  | 4 ++++
 src/components/attachment/attachment.vue | 6 ++++++
 2 files changed, 10 insertions(+)

diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 99958589..47ca03de 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -23,6 +23,10 @@ const Attachment = {
         type = 'video'
       };
 
+      if (this.attachment.mimetype.match(/ogg|audio/)) {
+        type = 'audio'
+      }
+
       return type
     }
   },
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 0e2a228a..4e8c1407 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -8,6 +8,8 @@
 
     <video v-if="type === 'video' && !nsfw" :src="attachment.url" controls></video>
 
+    <audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
+
     <span v-if="type === 'unknown'">Don't know how to display this...</span>
 
     <div v-if="type === 'html' && attachment.oembed" class="oembed">
@@ -42,6 +44,10 @@
               width: 100%;
           }
 
+          audio {
+              width: 100%;
+          }
+
           img.media-upload {
               width: 100%;
               height: 100%;

From 81c6f6e21f904e2889aa1b1d603f57c8f2772bfc Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 24 Nov 2016 18:14:54 +0100
Subject: [PATCH 02/14] Remove example component.

---
 src/components/hello/Hello.css  |  0
 src/components/hello/Hello.html |  0
 src/components/hello/Hello.js   |  8 ------
 src/components/hello/Hello.vue  | 44 ---------------------------------
 4 files changed, 52 deletions(-)
 delete mode 100644 src/components/hello/Hello.css
 delete mode 100644 src/components/hello/Hello.html
 delete mode 100644 src/components/hello/Hello.js
 delete mode 100644 src/components/hello/Hello.vue

diff --git a/src/components/hello/Hello.css b/src/components/hello/Hello.css
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/components/hello/Hello.html b/src/components/hello/Hello.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/components/hello/Hello.js b/src/components/hello/Hello.js
deleted file mode 100644
index c701c560..00000000
--- a/src/components/hello/Hello.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default {
-  name: 'hello',
-  data () {
-    return {
-      msg: 'Welcome to Your Vue.js app'
-    }
-  }
-}
diff --git a/src/components/hello/Hello.vue b/src/components/hello/Hello.vue
deleted file mode 100644
index 828136a8..00000000
--- a/src/components/hello/Hello.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <h2>Essential Links</h2>
-    <ul>
-      <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
-      <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
-      <li><a href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li>
-      <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
-      <br>
-      <li><a href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This template</a></li>
-    </ul>
-    <h2>Ecosystem</h2>
-    <ul>
-      <li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
-      <li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
-      <li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
-      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
-    </ul>
-  </div>
-</template>
-
-<script src='./Hello.js'></script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-h1, h2 {
-  font-weight: normal;
-}
-
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-
-a {
-  color: #42b983;
-}
-</style>

From a5f523922c1a93bbe781921e2d4bbdc8988436cc Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 24 Nov 2016 18:15:34 +0100
Subject: [PATCH 03/14] Make timelineless status adding possible.

---
 src/modules/statuses.js                  | 32 ++++++++++++++++--------
 test/unit/specs/modules/statuses.spec.js | 12 +++++++++
 2 files changed, 33 insertions(+), 11 deletions(-)

diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index a3031b31..734ffc8a 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,4 +1,4 @@
-import { remove, map, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max } from 'lodash'
+import { remove, map, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max, isArray } from 'lodash'
 import moment from 'moment'
 import apiService from '../services/api/api.service.js'
 // import parse from '../services/status_parser/status_parser.js'
@@ -100,11 +100,17 @@ const mergeOrAdd = (arr, item) => {
 }
 
 const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {} }) => {
+  // Sanity check
+  if (!isArray(statuses)) {
+    return false
+  }
+
   const allStatuses = state.allStatuses
   const timelineObject = state.timelines[timeline]
 
   // Set the maxId to the new id if it's larger.
   const updateMaxId = ({id}) => {
+    if (!timeline) { return false }
     timelineObject.maxId = max([id, timelineObject.maxId])
   }
 
@@ -117,15 +123,15 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
     }
 
     // Some statuses should only be added to the global status repository.
-    if (addToTimeline) {
+    if (timeline && addToTimeline) {
       mergeOrAdd(timelineObject.statuses, status)
     }
 
-    if (showImmediately) {
+    if (timeline && showImmediately) {
       // Add it directly to the visibleStatuses, don't change
       // newStatusCount
       mergeOrAdd(timelineObject.visibleStatuses, status)
-    } else if (addToTimeline && result.new) {
+    } else if (timeline && addToTimeline && result.new) {
       // Just change newStatuscount
       timelineObject.newStatusCount += 1
     }
@@ -159,7 +165,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
       let retweet
       // If the retweeted status is already there, don't add the retweet
       // to the timeline.
-      if (find(timelineObject.visibleStatuses, {id: retweetedStatus.id})) {
+      if (timeline && find(timelineObject.visibleStatuses, {id: retweetedStatus.id})) {
         // Already have it visible, don't add to timeline, don't show.
         retweet = addStatus(status, false, false)
       } else {
@@ -177,8 +183,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
       updateMaxId(deletion)
 
       remove(allStatuses, { uri })
-      remove(timelineObject.statuses, { uri })
-      remove(timelineObject.visibleStatuses, { uri })
+      if (timeline) {
+        remove(timelineObject.statuses, { uri })
+        remove(timelineObject.visibleStatuses, { uri })
+      }
     },
     'default': (unknown) => {
       console.log(unknown)
@@ -192,9 +200,11 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
   })
 
   // Keep the visible statuses sorted
-  timelineObject.visibleStatuses = sortBy(timelineObject.visibleStatuses, ({id}) => -id)
-  timelineObject.statuses = sortBy(timelineObject.statuses, ({id}) => -id)
-  timelineObject.minVisibleId = (last(timelineObject.statuses) || {}).id
+  if (timeline) {
+    timelineObject.visibleStatuses = sortBy(timelineObject.visibleStatuses, ({id}) => -id)
+    timelineObject.statuses = sortBy(timelineObject.statuses, ({id}) => -id)
+    timelineObject.minVisibleId = (last(timelineObject.statuses) || {}).id
+  }
 }
 
 export const mutations = {
@@ -228,7 +238,7 @@ export const mutations = {
 const statuses = {
   state: defaultState,
   actions: {
-    addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline }) {
+    addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false }) {
       commit('addNewStatuses', { statuses, showImmediately, timeline, user: rootState.users.currentUser })
     },
     favorite ({ rootState, commit }, status) {
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index 574e4f74..a50e4f9c 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -67,6 +67,18 @@ describe('The Statuses module', () => {
     expect(state.timelines.public.newStatusCount).to.equal(1)
   })
 
+  it('add the statuses to allStatuses if no timeline is given', () => {
+    const state = cloneDeep(defaultState)
+    const status = makeMockStatus({id: 1})
+
+    mutations.addNewStatuses(state, { statuses: [status] })
+
+    expect(state.allStatuses).to.eql([status])
+    expect(state.timelines.public.statuses).to.eql([])
+    expect(state.timelines.public.visibleStatuses).to.eql([])
+    expect(state.timelines.public.newStatusCount).to.equal(0)
+  })
+
   it('adds the status to allStatuses and to the given timeline, directly visible', () => {
     const state = cloneDeep(defaultState)
     const status = makeMockStatus({id: 1})

From 18c11e405a343d542d63b4bfd09a3388f9f6b0d8 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 24 Nov 2016 18:16:20 +0100
Subject: [PATCH 04/14] Add status and conversation fetching to apiService.

---
 src/services/api/api.service.js | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index d828aff0..87102376 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -7,18 +7,16 @@ const FAVORITE_URL = '/api/favorites/create'
 const UNFAVORITE_URL = '/api/favorites/destroy'
 const RETWEET_URL = '/api/statuses/retweet'
 const STATUS_UPDATE_URL = '/api/statuses/update.json'
+const STATUS_URL = '/api/statuses/show'
 const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload'
-// const CONVERSATION_URL = '/api/statusnet/conversation/';
+const CONVERSATION_URL = '/api/statusnet/conversation'
 
-// const FORM_CONTENT_TYPE = {'Content-Type': 'application/x-www-form-urlencoded'};
-
-// import { param, ajax } from 'jquery';
-// import { merge } from 'lodash';
+const oldfetch = window.fetch
 
 let fetch = (url, options) => {
   const baseUrl = ''
   const fullUrl = baseUrl + url
-  return window.fetch(fullUrl, options)
+  return oldfetch(fullUrl, options)
 }
 
 const authHeaders = (user) => {
@@ -29,6 +27,16 @@ const authHeaders = (user) => {
   }
 }
 
+const fetchConversation = ({id}) => {
+  let url = `${CONVERSATION_URL}/${id}.json?count=100`
+  return fetch(url).then((data) => data.json())
+}
+
+const fetchStatus = ({id}) => {
+  let url = `${STATUS_URL}/${id}.json`
+  return fetch(url).then((data) => data.json())
+}
+
 const fetchTimeline = ({timeline, credentials, since = false, until = false}) => {
   const timelineUrls = {
     public: PUBLIC_TIMELINE_URL,
@@ -108,6 +116,8 @@ const uploadMedia = ({formData, credentials}) => {
 const apiService = {
   verifyCredentials,
   fetchTimeline,
+  fetchConversation,
+  fetchStatus,
   favorite,
   unfavorite,
   retweet,

From e245074ef70e0a98c79168cb7e04390dc8d5e97c Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 24 Nov 2016 18:17:09 +0100
Subject: [PATCH 05/14] Add status / conversation component.

And wire it up.
---
 src/components/conversation/conversation.js  | 48 ++++++++++++++++++++
 src/components/conversation/conversation.vue | 12 +++++
 src/main.js                                  |  4 +-
 3 files changed, 63 insertions(+), 1 deletion(-)
 create mode 100644 src/components/conversation/conversation.js
 create mode 100644 src/components/conversation/conversation.vue

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
new file mode 100644
index 00000000..ea26d958
--- /dev/null
+++ b/src/components/conversation/conversation.js
@@ -0,0 +1,48 @@
+import { find, filter, sortBy, toInteger } from 'lodash'
+import Status from '../status/status.vue'
+import apiService from '../../services/api/api.service.js'
+
+const conversation = {
+  computed: {
+    status () {
+      const id = toInteger(this.$route.params.id)
+      const statuses = this.$store.state.statuses.allStatuses
+      const status = find(statuses, {id})
+
+      return status
+    },
+    conversation () {
+      if (!this.status) {
+        return false
+      }
+
+      const conversationId = this.status.statusnet_conversation_id
+      const statuses = this.$store.state.statuses.allStatuses
+      const conversation = filter(statuses, { statusnet_conversation_id: conversationId })
+      return sortBy(conversation, 'id')
+    }
+  },
+  components: {
+    Status
+  },
+  created () {
+    this.fetchConversation()
+  },
+  methods: {
+    fetchConversation () {
+      if (this.status) {
+        const conversationId = this.status.statusnet_conversation_id
+        apiService.fetchConversation({id: conversationId})
+          .then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
+          .then(() => this.$store.commit('updateTimestamps'))
+      } else {
+        const id = this.$route.params.id
+        apiService.fetchStatus({id})
+          .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
+          .then(() => this.fetchConversation())
+      }
+    }
+  }
+}
+
+export default conversation
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
new file mode 100644
index 00000000..60b3f044
--- /dev/null
+++ b/src/components/conversation/conversation.vue
@@ -0,0 +1,12 @@
+<template>
+  <div class="timeline panel panel-default">
+    <div class="panel-heading">Status</div>
+    <div class="panel-body">
+      <div class="timeline">
+        <status v-for="status in conversation" :key="status.id" v-bind:statusoid="status"></status>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./conversation.js"></script>
diff --git a/src/main.js b/src/main.js
index de3b2af1..64d331f1 100644
--- a/src/main.js
+++ b/src/main.js
@@ -5,6 +5,7 @@ import App from './App.vue'
 import PublicTimeline from './components/public_timeline/public_timeline.vue'
 import PublicAndExternalTimeline from './components/public_and_external_timeline/public_and_external_timeline.vue'
 import FriendsTimeline from './components/friends_timeline/friends_timeline.vue'
+import Conversation from './components/conversation/conversation.vue'
 
 import statusesModule from './modules/statuses.js'
 import usersModule from './modules/users.js'
@@ -23,7 +24,8 @@ const routes = [
   { path: '/', redirect: '/main/all' },
   { path: '/main/all', component: PublicAndExternalTimeline },
   { path: '/main/public', component: PublicTimeline },
-  { path: '/main/friends', component: FriendsTimeline }
+  { path: '/main/friends', component: FriendsTimeline },
+  { name: 'conversation', path: '/notice/:id', component: Conversation }
 ]
 
 const router = new VueRouter({

From 2341a3692a31f40c484a3b027f757c84551bf1fe Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 24 Nov 2016 18:19:01 +0100
Subject: [PATCH 06/14] Add link to conversation in status.

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

diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d4bcc279..a84917e6 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -20,7 +20,11 @@
           <small><a :href="status.user.statusnet_profile_url">{{status.user.screen_name}}</a></small>
           <small v-if="status.in_reply_to_screen_name"> &gt; <a :href="status.in_reply_to_profileurl">{{status.in_reply_to_screen_name}}</a></small>
           -
-          <small>{{status.created_at_parsed}}</small>
+          <small>
+            <router-link :to="{ name: 'conversation', params: { id: status.id } }">
+              {{status.created_at_parsed}}
+            </router-link>
+          </small>
         </h4>
 
         <div class="status-content" v-html="status.statusnet_html"></div>

From ce0071d6e085ada533810d73200607381b9a5807 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 24 Nov 2016 18:24:35 +0100
Subject: [PATCH 07/14] Remember positions on scroll.

---
 src/main.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/main.js b/src/main.js
index 64d331f1..3d3ef1b4 100644
--- a/src/main.js
+++ b/src/main.js
@@ -30,7 +30,10 @@ const routes = [
 
 const router = new VueRouter({
   mode: 'history',
-  routes
+  routes,
+  scrollBehavior: (to, from, savedPosition) => {
+    return savedPosition || { x: 0, y: 0 }
+  }
 })
 
 /* eslint-disable no-new */

From 4f0155c5eb532fea6cde2aea870824f936923132 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 24 Nov 2016 18:31:18 +0100
Subject: [PATCH 08/14] Timeline status adding fixes.

Don't show new statuses immediately if we already have something in there.
---
 src/components/timeline/timeline.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 4ebc383f..8799e69c 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -12,12 +12,13 @@ const Timeline = {
   created () {
     const store = this.$store
     const credentials = store.state.users.currentUser.credentials
+    const showImmediately = this.timeline.visibleStatuses.length === 0
 
     timelineFetcher.fetchAndUpdate({
       store,
       credentials,
       timeline: this.timelineName,
-      showImmediately: true
+      showImmediately
     })
   },
   methods: {

From c8d25eab61450340039374f0ffc4b2388825714b Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Thu, 24 Nov 2016 20:49:30 +0100
Subject: [PATCH 09/14] Image CSS fix for Firefox.

---
 src/components/attachment/attachment.vue | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 4e8c1407..d5578c04 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -97,7 +97,6 @@
 
               img {
                   width: 100%;
-                  flex: 1;
                   border: 1px solid;
                   border-radius: 0.5em;
                   width: 100%;

From 08393b8580385d2627da2ebd9e3a09455d101a3a Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Fri, 25 Nov 2016 13:42:33 +0100
Subject: [PATCH 10/14] Fix word wrapping on Firefox.

Also, move some css around.

See https://bugzilla.mozilla.org/show_bug.cgi?id=1136818 for the word
wrapping stuff.
---
 src/App.scss                     | 28 ++--------------------------
 src/components/status/status.vue | 24 ++++++++++++++++++++++++
 2 files changed, 26 insertions(+), 26 deletions(-)

diff --git a/src/App.scss b/src/App.scss
index a97ad56d..47886e31 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -145,10 +145,6 @@ status.ng-enter.ng-enter-active {
 }
 
 
-.media-body {
-    flex: 1
-}
-
 #content {
     margin: auto;
     max-width: 920px;
@@ -163,34 +159,14 @@ status.ng-enter.ng-enter-active {
     padding-left: 0.3em;
 }
 
-.status .avatar {
-    width: 48px;
-}
-
-.status.compact .avatar {
-    width: 32px;
-}
-
-.status {
-    padding: 0.5em;
-    padding-right: 1em;
-    border-bottom: 1px solid silver;
-}
-
-.status-el:last-child .status {
-    border: none
+.container > * {
+    min-width: 0px;
 }
 
 [ng-click] {
     cursor: pointer;
 }
 
-.status-el p {
-    margin: 0;
-    margin-top: 0.2em;
-    margin-bottom: 0.5em;
-}
-
 .user-info {
     padding: 1em;
     img {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index a84917e6..e74be215 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -71,6 +71,12 @@
          margin-top: 3px;
          margin-bottom: 3px;
      }
+
+     p {
+       margin: 0;
+       margin-top: 0.2em;
+       margin-bottom: 0.5em;
+     }
   }
 
   .status-actions {
@@ -80,4 +86,22 @@
   .icon-reply:hover {
      color: $blue;
   }
+
+  .status .avatar {
+    width: 48px;
+  }
+
+  .status.compact .avatar {
+    width: 32px;
+  }
+
+  .status {
+    padding: 0.5em;
+    padding-right: 1em;
+    border-bottom: 1px solid silver;
+  }
+
+  .status-el:last-child .status {
+    border: none
+  }
 </style>

From 1d8c8131352d05d796a6baf65a09fbdd1726fedf Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Fri, 25 Nov 2016 16:56:08 +0100
Subject: [PATCH 11/14] Better handling of favorites.

---
 src/modules/statuses.js                  | 16 ++++++++++--
 test/unit/specs/modules/statuses.spec.js | 33 ++++++++++++++++++++++--
 2 files changed, 45 insertions(+), 4 deletions(-)

diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 734ffc8a..37115506 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -7,6 +7,7 @@ export const defaultState = {
   allStatuses: [],
   maxId: 0,
   notifications: [],
+  favorites: new Set(),
   timelines: {
     public: {
       statuses: [],
@@ -147,6 +148,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
     const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) })
     if (status) {
       status.fave_num += 1
+
+      // This is our favorite, so the relevant bit.
+      if (favorite.user.id === user.id) {
+        status.favorited = true
+      }
+
+      // Add a notification if the user's status is favorited
       if (status.user.id === user.id) {
         addNotification({type: 'favorite', status, action: favorite})
       }
@@ -175,8 +183,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
       retweet.retweeted_status = retweetedStatus
     },
     'favorite': (favorite) => {
-      updateMaxId(favorite)
-      favoriteStatus(favorite)
+      // Only update if this is a new favorite.
+      if (!state.favorites.has(favorite.id)) {
+        state.favorites.add(favorite.id)
+        updateMaxId(favorite)
+        favoriteStatus(favorite)
+      }
     },
     'deletion': (deletion) => {
       const uri = deletion.uri
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index a50e4f9c..f068bb92 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -197,7 +197,8 @@ describe('The Statuses module', () => {
       is_post_verb: false,
       in_reply_to_status_id: '1', // The API uses strings here...
       uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
-      text: 'a favorited something by b'
+      text: 'a favorited something by b',
+      user: {}
     }
 
     mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
@@ -206,6 +207,33 @@ describe('The Statuses module', () => {
     expect(state.timelines.public.visibleStatuses.length).to.eql(1)
     expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
     expect(state.timelines.public.maxId).to.eq(favorite.id)
+
+    // Adding it again does nothing
+    mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' })
+
+    expect(state.timelines.public.visibleStatuses.length).to.eql(1)
+    expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
+    expect(state.timelines.public.maxId).to.eq(favorite.id)
+
+    // If something is favorited by the current user, it also sets the 'favorited' property
+    const user = {
+      id: 1
+    }
+
+    const ownFavorite = {
+      id: 3,
+      is_post_verb: false,
+      in_reply_to_status_id: '1', // The API uses strings here...
+      uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
+      text: 'a favorited something by b',
+      user
+    }
+
+    mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user })
+
+    expect(state.timelines.public.visibleStatuses.length).to.eql(1)
+    expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(2)
+    expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true)
   })
 
   describe('notifications', () => {
@@ -220,7 +248,8 @@ describe('The Statuses module', () => {
         is_post_verb: false,
         in_reply_to_status_id: '1', // The API uses strings here...
         uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
-        text: 'a favorited something by b'
+        text: 'a favorited something by b',
+        user: {}
       }
 
       mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public', user })

From 5986afbb201e2a42bd0954c35e284c932b2e993c Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Fri, 25 Nov 2016 17:19:46 +0100
Subject: [PATCH 12/14] Re-indent style.

---
 src/components/status/status.vue | 48 ++++++++++++++++----------------
 1 file changed, 24 insertions(+), 24 deletions(-)

diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index e74be215..b00f97ca 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -55,8 +55,8 @@
 <script src="./status.js" ></script>
 
 <style lang="scss">
-  @import '../../_variables.scss';
-  .status-el {
+ @import '../../_variables.scss';
+ .status-el {
      hyphens: auto;
      overflow-wrap: break-word;
      word-wrap: break-word;
@@ -73,35 +73,35 @@
      }
 
      p {
-       margin: 0;
-       margin-top: 0.2em;
-       margin-bottom: 0.5em;
+         margin: 0;
+         margin-top: 0.2em;
+         margin-bottom: 0.5em;
      }
-  }
+ }
 
-  .status-actions {
+ .status-actions {
      padding-top: 5px;
-  }
+ }
 
-  .icon-reply:hover {
+ .icon-reply:hover {
      color: $blue;
-  }
+ }
 
-  .status .avatar {
-    width: 48px;
-  }
+ .status .avatar {
+     width: 48px;
+ }
 
-  .status.compact .avatar {
-    width: 32px;
-  }
+ .status.compact .avatar {
+     width: 32px;
+ }
 
-  .status {
-    padding: 0.5em;
-    padding-right: 1em;
-    border-bottom: 1px solid silver;
-  }
+ .status {
+     padding: 0.5em;
+     padding-right: 1em;
+     border-bottom: 1px solid silver;
+ }
 
-  .status-el:last-child .status {
-    border: none
-  }
+ .status-el:last-child .status {
+     border: none
+ }
 </style>

From b515586485fab7030e02cad3cf1ce2c6e451c7d9 Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Fri, 25 Nov 2016 17:34:41 +0100
Subject: [PATCH 13/14] Show link to status source url.

---
 src/components/status/status.vue | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index b00f97ca..14792cae 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -25,6 +25,9 @@
               {{status.created_at_parsed}}
             </router-link>
           </small>
+          <small v-if="!status.is_local" class="source_url">
+            <a :href="status.external_url" >Source</a>
+          </small>
         </h4>
 
         <div class="status-content" v-html="status.statusnet_html"></div>
@@ -62,6 +65,10 @@
      word-wrap: break-word;
      word-break: break-word;
 
+     .source_url {
+       float: right;
+     }
+
      a {
          display: inline-block;
          word-break: break-all;

From 1be1d7563c94fa961c1cc0cef03e7e4e69df178a Mon Sep 17 00:00:00 2001
From: Roger Braun <roger@rogerbraun.net>
Date: Fri, 25 Nov 2016 17:34:59 +0100
Subject: [PATCH 14/14] Greentext.

---
 src/components/status/status.vue | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 14792cae..9d17b8a7 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -69,6 +69,10 @@
        float: right;
      }
 
+     .greentext {
+         color: green;
+     }
+
      a {
          display: inline-block;
          word-break: break-all;