aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorShpuld Shpuldson <shpuld@gmail.com>2017-06-15 23:25:19 +0300
committerShpuld Shpuldson <shpuld@gmail.com>2017-06-15 23:25:19 +0300
commite0e8965c08c09fee16d17e312c3788e13cdd1c88 (patch)
tree53e6c4eebd4328f868c2d3f6eb0b26b3c3e1951d /src
parent44923afbee23ef7bd22c20d25bf7776b284f5f88 (diff)
parent7d46e3965d06e039537066eeb5fac99ebcab978d (diff)
update branch and fix merge conflicts
Diffstat (limited to 'src')
-rw-r--r--src/App.js4
-rw-r--r--src/App.scss100
-rw-r--r--src/App.vue13
-rw-r--r--src/components/attachment/attachment.js2
-rw-r--r--src/components/attachment/attachment.vue6
-rw-r--r--src/components/conversation/conversation.js42
-rw-r--r--src/components/conversation/conversation.vue38
-rw-r--r--src/components/notifications/notifications.js5
-rw-r--r--src/components/notifications/notifications.scss10
-rw-r--r--src/components/notifications/notifications.vue29
-rw-r--r--src/components/post_status_form/post_status_form.js10
-rw-r--r--src/components/post_status_form/post_status_form.vue41
-rw-r--r--src/components/settings/settings.js16
-rw-r--r--src/components/settings/settings.vue19
-rw-r--r--src/components/status/status.js55
-rw-r--r--src/components/status/status.vue241
-rw-r--r--src/components/timeline/timeline.js17
-rw-r--r--src/components/user_card_content/user_card_content.vue17
-rw-r--r--src/components/user_finder/user_finder.js22
-rw-r--r--src/components/user_finder/user_finder.vue23
-rw-r--r--src/components/user_profile/user_profile.js24
-rw-r--r--src/components/user_profile/user_profile.vue7
-rw-r--r--src/lib/persisted_state.js34
-rw-r--r--src/main.js5
-rw-r--r--src/modules/api.js11
-rw-r--r--src/modules/config.js5
-rw-r--r--src/modules/statuses.js34
-rw-r--r--src/services/api/api.service.js32
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js9
-rw-r--r--src/services/timeline_fetcher/timeline_fetcher.service.js10
30 files changed, 688 insertions, 193 deletions
diff --git a/src/App.js b/src/App.js
index 2a00b369..a2d891f7 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,13 +1,15 @@
import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.vue'
+import UserFinder from './components/user_finder/user_finder.vue'
export default {
name: 'app',
components: {
UserPanel,
NavPanel,
- Notifications
+ Notifications,
+ UserFinder
},
data: () => ({
mobileActivePanel: 'timeline'
diff --git a/src/App.scss b/src/App.scss
index 8a1942c6..a5f190cb 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -52,6 +52,8 @@ button{
.item {
flex: 1;
+ line-height: 21px;
+ height: 21px;
}
.gaps > .item {
@@ -134,11 +136,6 @@ main-router {
background-color: rgba(0,0,0,0.1);
}
-.media-body {
- flex: 1;
- padding-left: 0.5em;
-}
-
.container > * {
min-width: 0px;
}
@@ -147,60 +144,6 @@ main-router {
color: grey;
}
-.status-actions {
- width: 50%;
- display: flex;
-
- div, favorite-button {
- flex: 1;
- }
-}
-
-status-text-container {
- display: block;
-}
-
-.status-el {
- line-height: 18px;
-
- .notify {
- .avatar {
- border-width: 3px;
- border-style: solid;
- }
- }
-
- .media-left {
- img {
- margin-top: 0.2em;
- float: right;
- margin-right: 0.3em;
- border-radius: 5px;
- }
- }
-
- .retweet-info {
- padding: 0.7em 0 0 0.6em;
-
- .media-left {
- display: flex;
-
- i {
- align-self: center;
- text-align: right;
- flex: 1;
- padding-right: 0.3em;
- }
- }
- }
-
- .media-heading {
- small {
- font-weight: lighter;
- }
- margin-bottom: 0.3em;
- }
-}
nav {
z-index: 1000;
}
@@ -213,13 +156,20 @@ nav {
}
.main {
- flex: 1;
- flex-basis: 65%;
+ flex-basis: 60%;
+ flex-grow: 1;
+ flex-shrink: 1;
}
.sidebar {
- flex: 1;
- flex-basis: 35%;
+ flex: 0;
+ flex-basis: 35%;
+}
+
+.sidebar-flexer {
+ flex: 1;
+ flex-basis: 345px;
+ width: 365px;
}
.mobile-shown {
@@ -238,6 +188,30 @@ nav {
}
}
+@media all and (min-width: 960px) {
+ .sidebar {
+ overflow: hidden;
+ max-height: 100vh;
+ width: 350px;
+ position: fixed;
+ margin-top: -10px;
+
+ .sidebar-container {
+ height: 96vh;
+ width: 362px;
+ padding-top: 10px;
+ padding-right: 20px;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ }
+ }
+ .sidebar-flexer {
+ max-height: 96vh;
+ flex-shrink: 0;
+ flex-grow: 0;
+ }
+}
+
@media all and (max-width: 959px) {
.mobile-hidden {
display: none;
diff --git a/src/App.vue b/src/App.vue
index c4b3cb13..b2d8df8b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -6,6 +6,7 @@
<router-link :to="{ name: 'root'}">{{sitename}}</router-link>
</div>
<div class='item right'>
+ <user-finder></user-finder>
<router-link :to="{ name: 'settings'}"><i class="icon-cog"></i></router-link>
</div>
</div>
@@ -15,10 +16,14 @@
<button @click="activatePanel('sidebar')">Sidebar</button>
<button @click="activatePanel('timeline')">Timeline</button>
</div>
- <div class="sidebar" :class="{ 'mobile-hidden': mobileActivePanel != 'sidebar' }">
- <user-panel></user-panel>
- <nav-panel></nav-panel>
- <notifications v-if="currentUser"></notifications>
+ <div class="sidebar-flexer" :class="{ 'mobile-hidden': mobileActivePanel != 'sidebar'}">
+ <div class="sidebar" :class="{ 'mobile-hidden': mobileActivePanel != 'sidebar' }">
+ <div class="sidebar-container">
+ <user-panel></user-panel>
+ <nav-panel></nav-panel>
+ <notifications v-if="currentUser"></notifications>
+ </div>
+ </div>
</div>
<div class="main" :class="{ 'mobile-hidden': mobileActivePanel != 'timeline' }">
<transition name="fade">
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 7715add5..ccf26b79 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -26,7 +26,7 @@ const Attachment = {
autoHeight () {
if (this.type === 'image' && this.nsfw) {
return {
- 'min-height': '311px'
+ 'min-height': '109px'
}
}
}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 6af23391..d50664b6 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -33,10 +33,10 @@
.attachments {
display: flex;
flex-wrap: wrap;
- margin-right: -0.8em;
+ margin-right: -0.7em;
.attachment {
flex: 1 0 30%;
- margin: 0.5em 0.8em 0.6em 0.0em;
+ margin: 0.5em 0.7em 0.6em 0.0em;
align-self: flex-start;
&.html {
@@ -116,8 +116,10 @@
border-style: solid;
border-width: 1px;
border-radius: 5px;
+ object-fit: contain;
width: 100%;
height: 100%; /* If this isn't here, chrome will stretch the images */
+ max-height: 500px;
}
}
}
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 281b0183..059028f9 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,4 +1,4 @@
-import { filter, sortBy } from 'lodash'
+import { find, filter, sortBy } from 'lodash'
import { statusType } from '../../modules/statuses.js'
import Status from '../status/status.vue'
@@ -8,6 +8,16 @@ const sortAndFilterConversation = (conversation) => {
}
const conversation = {
+ data () {
+ return {
+ highlight: null,
+ preview: {
+ x: 0,
+ y: 0,
+ status: null
+ }
+ }
+ },
props: [
'statusoid',
'collapsable'
@@ -22,7 +32,6 @@ const conversation = {
const conversationId = this.status.statusnet_conversation_id
const statuses = this.$store.state.statuses.allStatuses
const conversation = filter(statuses, { statusnet_conversation_id: conversationId })
-
return sortAndFilterConversation(conversation)
}
},
@@ -41,6 +50,7 @@ const conversation = {
const conversationId = this.status.statusnet_conversation_id
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
+ .then(() => this.setHighlight(this.statusoid.id))
} else {
const id = this.$route.params.id
this.$store.state.api.backendInteractor.fetchStatus({id})
@@ -48,12 +58,38 @@ const conversation = {
.then(() => this.fetchConversation())
}
},
- focused: function (id) {
+ getReplies (id) {
+ let res = []
+ id = Number(id)
+ let i
+ for (i = 0; i < this.conversation.length; i++) {
+ if (Number(this.conversation[i].in_reply_to_status_id) === id) {
+ res.push({
+ name: `#${i}`,
+ id: this.conversation[i].id
+ })
+ }
+ }
+ return res
+ },
+ focused (id) {
if (this.statusoid.retweeted_status) {
return (id === this.statusoid.retweeted_status.id)
} else {
return (id === this.statusoid.id)
}
+ },
+ setHighlight (id) {
+ this.highlight = Number(id)
+ },
+ setPreview (id, x, y) {
+ if (id) {
+ this.preview.x = x
+ this.preview.y = y
+ this.preview.status = find(this.conversation, { id: id })
+ } else {
+ this.preview.status = null
+ }
}
}
}
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 726cfb65..e8d97f99 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -8,7 +8,17 @@
</div>
<div class="panel-body">
<div class="timeline">
- <status v-for="status in conversation" :key="status.id" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true'></status>
+ <status v-for="status in conversation" @goto="setHighlight" :key="status.id" @preview="setPreview" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true' :highlight="highlight" :replies="getReplies(status.id)"></status>
+ </div>
+ </div>
+ <div class="status-preview base00-background base03-border" :style="{ left: preview.x + 'px', top: preview.y + 'px'}" v-if="preview.status">
+ <img class="avatar" :src="preview.status.user.profile_image_url_original">
+ <div class="text">
+ <h4>
+ {{ preview.status.user.name }}
+ <small><a>{{ preview.status.user.screen_name}}</a></small>
+ </h4>
+ <div @click.prevent="linkClicked" class="status-content" v-html="preview.status.statusnet_html"></div>
</div>
</div>
</div>
@@ -21,4 +31,30 @@
border-bottom-style: solid;
border-bottom-width: 1px;
}
+
+ .status-preview {
+ position: absolute;
+ max-width: 35em;
+ padding: 0.5em;
+ display: flex;
+ border-color: inherit;
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 4px;
+ box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
+ .avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ }
+ .text {
+ h4 {
+ margin-bottom: 0.4em;
+ small {
+ font-weight: lighter;
+ }
+ }
+ padding: 0 0.5em 0.5em 0.5em;
+ }
+ }
</style>
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index c8d5e212..c0c86c68 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -1,3 +1,5 @@
+import Status from '../status/status.vue'
+
import { sortBy, take, filter } from 'lodash'
const Notifications = {
@@ -23,6 +25,9 @@ const Notifications = {
return this.unseenNotifications.length
}
},
+ components: {
+ Status
+ },
watch: {
unseenCount (count) {
if (count > 0) {
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 9bc2a5ec..f02ced8d 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -1,6 +1,8 @@
@import '../../_variables.scss';
.notifications {
+ // a bit of a hack to allow scrolling below notifications
+ padding-bottom: 15em;
.panel-heading {
// force the text to stay centered, while keeping
@@ -43,19 +45,23 @@
word-wrap: break-word;
line-height:18px;
- .icon-retweet {
+ .icon-retweet.lit {
color: $green;
}
- .icon-reply {
+ .icon-reply.lit {
color: $blue;
}
h1 {
+ word-break: break-all;
margin: 0 0 0.3em;
padding: 0;
font-size: 1em;
line-height:20px;
+ small {
+ font-weight: lighter;
+ }
}
padding: 0.3em 0.8em 0.5em;
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 661d842c..f5950ac9 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -7,23 +7,34 @@
<button @click.prevent="markAsSeen" class="base06 base02-background read-button">Read!</button>
</div>
<div class="panel-body base03-border">
- <div v-for="notification in visibleNotifications" class="notification" :class='{"unseen": !notification.seen}'>
+ <div v-for="notification in visibleNotifications" :key="notification" class="notification" :class='{"unseen": !notification.seen}'>
<a :href="notification.action.user.statusnet_profile_url">
<img class='avatar' :src="notification.action.user.profile_image_url_original">
</a>
- <div class='text'>
- <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+ <div class='text' style="width: 100%;">
<div v-if="notification.type === 'favorite'">
- <h1>{{ notification.action.user.name }}<br><i class="fa icon-star"></i> favorited your <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">status</h1>
- <p>{{ notification.status.text }}</p>
+ <h1>
+ <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+ <i class="fa icon-star"></i>
+ <small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
+ </h1>
+ <div v-html="notification.status.statusnet_html"></div>
</div>
<div v-if="notification.type === 'repeat'">
- <h1>{{ notification.action.user.name }}<br><i class="fa icon-retweet"></i> repeated your <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">status</h1>
- <p>{{ notification.status.text }}</p>
+ <h1>
+ <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+ <i class="fa icon-retweet lit"></i>
+ <small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
+ </h1>
+ <div v-html="notification.status.statusnet_html"></div>
</div>
<div v-if="notification.type === 'mention'">
- <h1>{{ notification.action.user.name }}<br><i class="fa icon-reply"></i> <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">mentioned</router-link> you</h1>
- <p>{{ notification.status.text }}</p>
+ <h1>
+ <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+ <i class="fa icon-reply lit"></i>
+ <small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
+ </h1>
+ <status :compact="true" :statusoid="notification.status"></status>
</div>
</div>
</div>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 797fcdbb..881a9d1c 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -5,6 +5,7 @@ import Completion from '../../services/completion/completion.js'
import { take, filter, reject, map, uniqBy } from 'lodash'
+
const buildMentionsString = ({user, attentions}, currentUser) => {
let allAttentions = [...attentions]
@@ -87,6 +88,8 @@ const PostStatusForm = {
files: []
}
this.$emit('posted')
+ let el = this.$el.querySelector('textarea')
+ el.style.height = '16px'
},
addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo)
@@ -113,6 +116,13 @@ const PostStatusForm = {
},
fileDrag (e) {
e.dataTransfer.dropEffect = 'copy'
+ },
+ resize (e) {
+ e.target.style.height = 'auto'
+ e.target.style.height = `${e.target.scrollHeight - 10}px`
+ if (e.target.value === '') {
+ e.target.style.height = '16px'
+ }
}
}
}
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 12a9c88a..4f6d4565 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -1,17 +1,8 @@
<template>
<div class="post-status-form">
<form @submit.prevent="postStatus(newStatus)">
- <div class="form-group" >
- <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control" @keyup.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag"></textarea>
- </div>
- <div class="attachments">
- <div class="attachment" v-for="file in newStatus.files">
- <i class="fa icon-cancel" @click="removeMediaFile(file)"></i>
- <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
- <video v-if="type(file) === 'video'" :src="file.image" controls></video>
- <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
- <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
- </div>
+ <div class="form-group base03-border" >
+ <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" placeholder="Just landed in L.A." rows="1" class="form-control" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize"></textarea>
</div>
<div>
<h1>Word</h1>
@@ -24,6 +15,15 @@
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload>
<button :disabled="submitDisabled" type="submit" class="btn btn-default base05 base01-background">Submit</button>
</div>
+ <div class="attachments">
+ <div class="attachment" v-for="file in newStatus.files">
+ <i class="fa icon-cancel" @click="removeMediaFile(file)"></i>
+ <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
+ <video v-if="type(file) === 'video'" :src="file.image" controls></video>
+ <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
+ <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
+ </div>
+ </div>
</form>
</div>
</template>
@@ -51,14 +51,20 @@
.form-bottom {
display: flex;
padding: 0.5em;
+ height: 32px;
button {
- flex: 2;
+ width: 10em;
}
}
.attachments {
- padding: 0.5em;
+ padding: 0 0.5em;
+
+ .attachment {
+ position: relative;
+ margin: 0.5em 0.8em 0.2em 0;
+ }
i {
position: absolute;
@@ -86,11 +92,16 @@
form textarea {
border: solid;
border-width: 1px;
- border-color: silver;
+ border-color: inherit;
border-radius: 5px;
line-height:16px;
padding: 5px;
- resize: vertical;
+ resize: none;
+ overflow: hidden;
+ }
+
+ form textarea:focus {
+ min-height: 48px;
}
.btn {
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index 3d373283..998aa354 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -1,11 +1,15 @@
import StyleSwitcher from '../style_switcher/style_switcher.vue'
+import { filter, trim } from 'lodash'
const settings = {
data () {
return {
hideAttachmentsLocal: this.$store.state.config.hideAttachments,
hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv,
- hideNsfwLocal: this.$store.state.config.hideNsfw
+ hideNsfwLocal: this.$store.state.config.hideNsfw,
+ autoLoadLocal: this.$store.state.config.autoLoad,
+ hoverPreviewLocal: this.$store.state.config.hoverPreview,
+ muteWordsString: this.$store.state.config.muteWords.join('\n')
}
},
components: {
@@ -20,6 +24,16 @@ const settings = {
},
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
+ },
+ autoLoadLocal (value) {
+ this.$store.dispatch('setOption', { name: 'autoLoad', value })
+ },
+ hoverPreviewLocal (value) {
+ this.$store.dispatch('setOption', { name: 'hoverPreview', value })
+ },
+ muteWordsString (value) {
+ value = filter(value.split('\n'), (word) => trim(word).length > 0)
+ this.$store.dispatch('setOption', { name: 'muteWords', value })
}
}
}
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 478d761a..af0242c4 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -9,6 +9,11 @@
<style-switcher></style-switcher>
</div>
<div class="setting-item">
+ <h2>Filtering</h2>
+ <p>All notices containing these words will be muted, one per line</p>
+ <textarea id="muteWords" v-model="muteWordsString"></textarea>
+ </div>
+ <div class="setting-item">
<h2>Attachments</h2>
<ul class="setting-list">
<li>
@@ -23,6 +28,14 @@
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
<label for="hideNsfw">Enable clickthrough NSFW attachment hiding</label>
</li>
+ <li>
+ <input type="checkbox" id="autoLoad" v-model="autoLoadLocal">
+ <label for="autoLoad">Enable automatic loading when scrolled to the bottom</label>
+ </li>
+ <li>
+ <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
+ <label for="hoverPreview">Enable reply-link preview on mouse hover</label>
+ </li>
</ul>
</div>
</div>
@@ -32,9 +45,13 @@
<script src="./settings.js">
</script>
-<style>
+<style lang="scss">
.setting-item {
margin: 1em 1em 1.4em;
+ textarea {
+ width: 100%;
+ height: 100px;
+ }
}
.setting-list {
list-style-type: none;
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 87fff879..4f5093e1 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -4,13 +4,17 @@ 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 { filter } from 'lodash'
const Status = {
props: [
'statusoid',
'expandable',
'inConversation',
- 'focused'
+ 'focused',
+ 'highlight',
+ 'compact',
+ 'replies'
],
data: () => ({
replying: false,
@@ -19,6 +23,9 @@ const Status = {
userExpanded: false
}),
computed: {
+ muteWords () {
+ return this.$store.state.config.muteWords
+ },
hideAttachments () {
return (this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation)
@@ -35,12 +42,30 @@ const Status = {
loggedIn () {
return !!this.$store.state.users.currentUser
},
- muted () { return !this.unmuted && this.status.user.muted },
+ muteWordHits () {
+ const statusText = this.status.text.toLowerCase()
+ const hits = filter(this.muteWords, (muteWord) => {
+ return statusText.includes(muteWord.toLowerCase())
+ })
+
+ return hits
+ },
+ muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
isReply () { return !!this.status.in_reply_to_status_id },
borderColor () {
return {
borderBottomColor: this.$store.state.config.colors['base02']
}
+ },
+ isFocused () {
+ // retweet or root of an expanded conversation
+ if (this.focused) {
+ return true
+ } else if (!this.inConversation) {
+ return false
+ }
+ // use conversation highlight only when in conversation
+ return this.status.id === this.highlight
}
},
components: {
@@ -63,6 +88,10 @@ const Status = {
toggleReplying () {
this.replying = !this.replying
},
+ gotoOriginal (id) {
+ // only handled by conversation, not status_or_conversation
+ this.$emit('goto', id)
+ },
toggleExpanded () {
this.$emit('toggleExpanded')
},
@@ -71,6 +100,28 @@ const Status = {
},
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
+ },
+ replyEnter (id, event) {
+ if (this.$store.state.config.hoverPreview) {
+ let rect = event.target.getBoundingClientRect()
+ this.$emit('preview', Number(id), rect.left + 20, rect.top + 20 + window.pageYOffset)
+ }
+ },
+ replyLeave () {
+ this.$emit('preview', 0, 0, 0)
+ }
+ },
+ watch: {
+ 'highlight': function (id) {
+ id = Number(id)
+ if (this.status.id === id) {
+ let rect = this.$el.getBoundingClientRect()
+ if (rect.top < 100) {
+ window.scrollBy(0, rect.top - 200)
+ } else if (rect.bottom > window.innerHeight - 50) {
+ window.scrollBy(0, rect.bottom - window.innerHeight + 50)
+ }
+ }
}
}
}
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 62a55505..e582a80d 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,9 +1,25 @@
<template>
- <div class="status-el base00-background base03-border" v-if="!status.deleted" v-bind:class="[{ 'base01-background': focused }, { 'status-conversation': inConversation }]" >
+ <div class="status-el base00-background" v-if="compact">
+ <div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
+ <div v-if="loggedIn">
+ <div class='status-actions'>
+ <div>
+ <a href="#" v-on:click.prevent="toggleReplying">
+ <i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i>
+ </a>
+ </div>
+ <retweet-button :status=status></retweet-button>
+ <favorite-button :status=status></favorite-button>
+ </div>
+ </div>
+ <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying" v-if="replying"/>
+ </div>
+ <div class="status-el base00-background base03-border" v-else-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" >
<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>
- <a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a>
+ <small class="muteWords">{{muteWordHits.join(', ')}}</small>
+ <a href="#" class="unmute" @click.prevent="toggleMute"><i class="fa icon-eye-off"></i></a>
</div>
</template>
<template v-if="!muted">
@@ -12,13 +28,14 @@
<i class='fa icon-retweet retweeted'></i>
</div>
<div class="media-body">
- Retweeted by {{retweeter}}
+ Repeated by <a :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
</div>
</div>
<div class="media status container">
<div class="media-left">
<a :href="status.user.statusnet_profile_url">
- <img @click.prevent="toggleUserExpanded" class='avatar' :src="status.user.profile_image_url_original">
+ <img @click.prevent="toggleUserExpanded" :class="{retweeted: retweet}" class='avatar' :src="status.user.profile_image_url_original">
+ <img v-if="retweet" class='avatar-retweeter' :src="statusoid.user.profile_image_url_original"></img>
</a>
</div>
<div class="media-body">
@@ -26,40 +43,45 @@
<user-card-content :user="status.user"></user-card-content>
</div>
<div class="user-content">
- <h4 class="media-heading">
- {{status.user.name}}
- <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
- <small v-if="status.in_reply_to_screen_name"> &gt;
- <router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
- {{status.in_reply_to_screen_name}}
- </router-link>
- </small>
- <template v-if="isReply">
+ <div class="media-heading">
+ <div class="name-and-links">
+ <h4 class="user-name">{{status.user.name}}</h4>
+ <div class="links">
+ <h4>
+ <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
+ <small v-if="status.in_reply_to_screen_name"> &gt;
+ <router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
+ {{status.in_reply_to_screen_name}}
+ </router-link>
+ </small>
+ <template v-if="isReply && !expandable">
+ <small>
+ <a href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"><i class="icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i></a>
+ </small>
+ </template>
+ -
<small>
- <router-link :to="{ name: 'conversation', params: { id: status.in_reply_to_status_id } }">
- <i class="icon-reply"></i>
- </router-link>
+ <router-link :to="{ name: 'conversation', params: { id: status.id } }">
+ <timeago :since="status.created_at" :auto-update="60"></timeago>
+ </router-link>
+ </small>
+ </h4>
+ </div>
+ <h4 class="replies" v-if="inConversation">
+ <small v-if="replies.length">Replies:</small>
+ <small v-for="reply in replies">
+ <a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a>
</small>
- </template>
- -
- <small>
- <router-link :to="{ name: 'conversation', params: { id: status.id } }">
- <timeago :since="status.created_at" :auto-update="60"></timeago>
- </router-link>
- </small>
- <template v-if="expandable">
- -
- <small>
- <a href="#" @click.prevent="toggleExpanded" ><i class="icon-plus-squared"></i></a>
- </small>
- <small v-if="status.user.muted">
- <a href="#" @click.prevent="toggleMute" ><i class="icon-eye-off"></i></a>
- </small>
- </template>
- <small v-if="!status.is_local" class="source_url">
- <a :href="status.external_url" target="_blank" ><i class="icon-binoculars"></i></a>
- </small>
- </h4>
+ </h4>
+ </div>
+ <div class="heading-icons">
+ <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="fa icon-eye-off"></i></a>
+ <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="fa icon-binoculars"></i></a>
+ <template v-if="expandable">
+ <a href="#" @click.prevent="toggleExpanded" class="expand"><i class="fa icon-plus-squared"></i></a>
+ </template>
+ </div>
+ </div>
<div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
@@ -95,24 +117,65 @@
<style lang="scss">
@import '../../_variables.scss';
+
+ status-text-container {
+ display: block;
+}
+
.status-el {
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
border-left-width: 0px;
+ line-height: 18px;
+
+ .notify {
+ .avatar {
+ border-width: 3px;
+ border-style: solid;
+ }
+ }
+
+ .media-body {
+ flex: 1;
+ padding-left: 0.5em;
+ }
+
.user-content {
+
min-height: 52px;
padding-top: 1px;
}
+ .media-heading {
+ display: flex;
+ min-height: 1.4em;
+ margin-bottom: 0.3em;
+
+ small {
+ font-weight: lighter;
+ }
+ h4 {
+ margin-right: 0.4em;
+ }
+ .name-and-links {
+ flex: 1 0;
+ display: flex;
+ flex-wrap: wrap;
+ }
+ .replies {
+ flex-basis: 100%;
+ }
+ }
+
.source_url {
- float: right;
+
}
- .greentext {
- color: green;
+ .expand {
+ margin-right: -0.3em;
}
a {
@@ -129,6 +192,34 @@
margin-top: 0.2em;
margin-bottom: 0.5em;
}
+
+ .media-left {
+ img {
+ margin-top: 0.2em;
+ float: right;
+ margin-right: 0.3em;
+ border-radius: 5px;
+ }
+ }
+
+ .retweet-info {
+ padding: 0.7em 0 0 0.6em;
+
+ .media-left {
+ display: flex;
+
+ i {
+ align-self: center;
+ text-align: right;
+ flex: 1;
+ padding-right: 0.3em;
+ }
+ }
+ }
+ }
+
+ .greentext {
+ color: green;
}
.status-conversation {
@@ -136,7 +227,14 @@
}
.status-actions {
- padding-top: 5px;
+ padding-top: 0.15em;
+ width: 100%;
+ display: flex;
+
+ div, favorite-button {
+ max-width: 6em;
+ flex: 1;
+ }
}
.icon-reply:hover {
@@ -148,7 +246,23 @@
}
.status .avatar {
- width: 48px;
+ width: 48px;
+ height: 48px;
+
+ &.retweeted {
+ width: 40px;
+ height: 40px;
+ margin-right: 8px;
+ margin-bottom: 8px;
+ }
+ }
+
+ .status img.avatar-retweeter {
+ width: 24px;
+ height: 24px;
+ position: absolute;
+ margin-left: 24px;
+ margin-top: 24px;
}
.status.compact .avatar {
@@ -156,14 +270,22 @@
}
.status {
- padding: 0.65em 0.7em 0.8em 0.8em;
+ padding: 0.4em 0.7em 0.45em 0.7em;
border-bottom: 1px solid;
border-bottom-color: inherit;
border-left: 4px rgba(255, 48, 16, 0.65);
border-left-style: inherit;
}
- .muted button {
- margin-left: auto;
+
+ .muted {
+ padding: 0.1em 0.4em 0.1em 0.8em;
+ button {
+ margin-left: auto;
+ }
+
+ .muteWords {
+ margin-left: 10px;
+ }
}
a.unmute {
@@ -188,4 +310,35 @@
flex: 1;
}
+ @media all and (max-width: 960px) {
+ .status-el {
+ .name-and-links {
+ margin-left: -0.25em;
+ }
+ }
+ .status {
+ max-width: 100%;
+ }
+
+ .status .avatar {
+ width: 40px;
+ height: 40px;
+
+ &.retweeted {
+ width: 34px;
+ height: 34px;
+ margin-right: 8px;
+ margin-bottom: 8px;
+ }
+ }
+
+ .status img.avatar-retweeter {
+ width: 22px;
+ height: 22px;
+ position: absolute;
+ margin-left: 18px;
+ margin-top: 18px;
+ }
+ }
+
</style>
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index d5a9adcc..3dc07f9e 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -6,7 +6,8 @@ const Timeline = {
props: [
'timeline',
'timelineName',
- 'title'
+ 'title',
+ 'userId'
],
computed: {
timelineError () { return this.$store.state.statuses.error }
@@ -20,11 +21,14 @@ const Timeline = {
const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0
+ window.onscroll = this.scrollLoad
+
timelineFetcher.fetchAndUpdate({
store,
credentials,
timeline: this.timelineName,
- showImmediately
+ showImmediately,
+ userId: this.userId
})
},
methods: {
@@ -40,8 +44,15 @@ const Timeline = {
credentials,
timeline: this.timelineName,
older: true,
- showImmediately: true
+ showImmediately: true,
+ userId: this.userId
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
+ },
+ scrollLoad (e) {
+ let height = Math.max(document.body.offsetHeight, document.body.scrollHeight)
+ if (this.timeline.loading === false && this.$store.state.config.autoLoad && (window.innerHeight + window.pageYOffset) >= (height - 750)) {
+ this.fetchOlderStatuses()
+ }
}
}
}
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 8c971d53..ff1b108c 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -61,10 +61,13 @@
props: [ 'user' ],
computed: {
headingStyle () {
- let rgb = this.$store.state.config.colors['base00'].match(/\d+/g)
- return {
- backgroundColor: `rgb(${Math.floor(rgb[0] * 0.53)}, ${Math.floor(rgb[1] * 0.56)}, ${Math.floor(rgb[2] * 0.59)})`,
- backgroundImage: `url(${this.user.cover_photo})`
+ let color = this.$store.state.config.colors['base00']
+ if (color) {
+ let rgb = this.$store.state.config.colors['base00'].match(/\d+/g)
+ return {
+ backgroundColor: `rgb(${Math.floor(rgb[0] * 0.53)}, ${Math.floor(rgb[1] * 0.56)}, ${Math.floor(rgb[2] * 0.59)})`,
+ backgroundImage: `url(${this.user.cover_photo})`
+ }
}
},
bodyStyle () {
@@ -79,9 +82,8 @@
return this.$store.state.users.currentUser
},
dailyAvg () {
- return Math.round(
- this.user.statuses_count / ((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
- )
+ const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
+ return Math.round(this.user.statuses_count / days)
}
},
methods: {
@@ -117,7 +119,6 @@
}
.profile-panel-body {
- padding-top: 0em;
top: -0em;
padding-top: 4em;
}
diff --git a/src/components/user_finder/user_finder.js b/src/components/user_finder/user_finder.js
new file mode 100644
index 00000000..03205382
--- /dev/null
+++ b/src/components/user_finder/user_finder.js
@@ -0,0 +1,22 @@
+const UserFinder = {
+ data: () => ({
+ username: undefined,
+ hidden: true
+ }),
+ methods: {
+ findUser (username) {
+ this.$store.state.api.backendInteractor.externalProfile(username)
+ .then((user) => {
+ if (!user.error) {
+ this.$store.commit('addNewUsers', [user])
+ this.$router.push({name: 'user-profile', params: {id: user.id}})
+ }
+ })
+ },
+ toggleHidden () {
+ this.hidden = !this.hidden
+ }
+ }
+}
+
+export default UserFinder
diff --git a/src/components/user_finder/user_finder.vue b/src/components/user_finder/user_finder.vue
new file mode 100644
index 00000000..c23d8ee0
--- /dev/null
+++ b/src/components/user_finder/user_finder.vue
@@ -0,0 +1,23 @@
+<template>
+ <a href="#" v-if="hidden"><i class="icon-user-plus user-finder-icon" @click.prevent="toggleHidden"/></a>
+ <span v-else>
+ <input class="user-finder-input base03-border" @keyup.enter="findUser(username)" v-model="username" placeholder="Find user" id="user-finder-input" type="text"/>
+ <i class="icon-cancel user-finder-icon" @click="toggleHidden"/>
+ </span>
+</template>
+
+<script src="./user_finder.js"></script>
+
+<style lang="scss">
+ .user-finder-icon {
+ margin-right: 0.25em;
+ }
+
+ .user-finder-input {
+ border-width: 1px;
+ border-style: solid;
+ border-color: inherit;
+ border-radius: 5px;
+ padding: 0.1em 0.2em 0.2em 0.2em;
+ }
+</style>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 4d52bc95..5f9d4d08 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,16 +1,30 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
-import { find } from 'lodash'
+import Timeline from '../timeline/timeline.vue'
const UserProfile = {
+ created () {
+ this.$store.commit('clearTimeline', { timeline: 'user' })
+ this.$store.dispatch('startFetching', ['user', this.userId])
+ },
+ destroyed () {
+ this.$store.dispatch('stopFetching', 'user')
+ },
computed: {
+ timeline () { return this.$store.state.statuses.timelines.user },
+ userId () {
+ return this.$route.params.id
+ },
user () {
- const id = this.$route.params.id
- const user = find(this.$store.state.users.users, {id})
- return user
+ if (this.timeline.statuses[0]) {
+ return this.timeline.statuses[0].user
+ } else {
+ return false
+ }
}
},
components: {
- UserCardContent
+ UserCardContent,
+ Timeline
}
}
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 11a61bfc..9241c469 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -1,6 +1,9 @@
<template>
- <div class="user-profile panel panel-default base00-background">
- <user-card-content :user="user"></user-card-content>
+ <div>
+ <div v-if="user" class="user-profile panel panel-default base00-background">
+ <user-card-content :user="user"></user-card-content>
+ </div>
+ <Timeline :title="'User Timeline'" v-bind:timeline="timeline" v-bind:timeline-name="'user'" :user-id="userId"/>
</div>
</template>
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index a47ad7d5..02349e13 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -3,6 +3,8 @@ import objectPath from 'object-path'
import localforage from 'localforage'
import { throttle, each } from 'lodash'
+let loaded = false
+
const defaultReducer = (state, paths) => (
paths.length === 0 ? state : paths.reduce((substate, path) => {
objectPath.set(substate, path, objectPath.get(state, path))
@@ -15,7 +17,11 @@ const defaultStorage = (() => {
})()
const defaultSetState = (key, state, storage) => {
- return storage.setItem(key, state)
+ if (!loaded) {
+ console.log('waiting for old state to be loaded...')
+ } else {
+ return storage.setItem(key, state)
+ }
}
export default function createPersistedState ({
@@ -32,17 +38,23 @@ 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
+ try {
+ 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)
- )
+ store.replaceState(
+ merge({}, store.state, savedState)
+ )
+ }
+ loaded = true
+ } catch (e) {
+ console.log("Couldn't load state")
+ loaded = true
}
})
diff --git a/src/main.js b/src/main.js
index ab0fd6c0..e5ecf228 100644
--- a/src/main.js
+++ b/src/main.js
@@ -24,7 +24,7 @@ Vue.use(VueRouter)
Vue.use(VueTimeago, {
locale: 'en-US',
locales: {
- 'en-US': require('vue-timeago/locales/en-US.json')
+ 'en-US': require('../static/timeago.json')
}
})
@@ -33,6 +33,9 @@ const persistedStateOptions = {
'config.hideAttachments',
'config.hideAttachmentsInConv',
'config.hideNsfw',
+ 'config.autoLoad',
+ 'config.hoverPreview',
+ 'config.muteWords',
'statuses.notifications',
'users.users'
]
diff --git a/src/modules/api.js b/src/modules/api.js
index a32adfde..e61382eb 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -1,4 +1,5 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
+import {isArray} from 'lodash'
const api = {
state: {
@@ -18,9 +19,17 @@ const api = {
},
actions: {
startFetching (store, timeline) {
+ let userId = false
+
+ // This is for user timelines
+ if (isArray(timeline)) {
+ userId = timeline[1]
+ timeline = timeline[0]
+ }
+
// Don't start fetching if we already are.
if (!store.state.fetchers[timeline]) {
- const fetcher = store.state.backendInteractor.startFetching({timeline, store})
+ const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId})
store.commit('addFetcher', {timeline, fetcher})
}
},
diff --git a/src/modules/config.js b/src/modules/config.js
index 05b4ab3b..f7d6e9c8 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -6,7 +6,10 @@ const defaultState = {
colors: {},
hideAttachments: false,
hideAttachmentsInConv: false,
- hideNsfw: true
+ hideNsfw: true,
+ autoLoad: true,
+ hoverPreview: true,
+ muteWords: []
}
const config = {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 051ec71b..c3753c5a 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -32,6 +32,17 @@ export const defaultState = {
minVisibleId: 0,
loading: false
},
+ user: {
+ statuses: [],
+ statusesObject: {},
+ faves: [],
+ visibleStatuses: [],
+ visibleStatusesObject: {},
+ newStatusCount: 0,
+ maxId: 0,
+ minVisibleId: 0,
+ loading: false
+ },
publicAndExternal: {
statuses: [],
statusesObject: {},
@@ -242,6 +253,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const uri = deletion.uri
updateMaxId(deletion)
+ // Remove possible notification
+ const status = find(allStatuses, {uri})
+ if (!status) {
+ return
+ }
+
+ remove(state.notifications, ({action: {id}}) => id === status.id)
+
remove(allStatuses, { uri })
if (timeline) {
remove(timelineObject.statuses, { uri })
@@ -276,6 +295,21 @@ export const mutations = {
oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
},
+ clearTimeline (state, { timeline }) {
+ const emptyTimeline = {
+ statuses: [],
+ statusesObject: {},
+ faves: [],
+ visibleStatuses: [],
+ visibleStatusesObject: {},
+ newStatusCount: 0,
+ maxId: 0,
+ minVisibleId: 0,
+ loading: false
+ }
+
+ state.timelines[timeline] = emptyTimeline
+ },
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = value
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 4dfc0a02..59e3a1c3 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -17,10 +17,14 @@ const FRIENDS_URL = '/api/statuses/friends.json'
const FOLLOWING_URL = '/api/friendships/create.json'
const UNFOLLOWING_URL = '/api/friendships/destroy.json'
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
+const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
+const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
// const USER_URL = '/api/users/show.json'
const oldfetch = window.fetch
+import { map } from 'lodash'
+
let fetch = (url, options) => {
const baseUrl = ''
const fullUrl = baseUrl + url
@@ -35,6 +39,13 @@ const authHeaders = (user) => {
}
}
+const externalProfile = (profileUrl) => {
+ let url = `${EXTERNAL_PROFILE_URL}?profileurl=${profileUrl}`
+ return fetch(url, {
+ method: 'GET'
+ }).then((data) => data.json())
+}
+
const followUser = ({id, credentials}) => {
let url = `${FOLLOWING_URL}?user_id=${id}`
return fetch(url, {
@@ -90,24 +101,34 @@ const setUserMute = ({id, credentials, muted = true}) => {
})
}
-const fetchTimeline = ({timeline, credentials, since = false, until = false}) => {
+const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false}) => {
const timelineUrls = {
public: PUBLIC_TIMELINE_URL,
friends: FRIENDS_TIMELINE_URL,
mentions: MENTIONS_URL,
- 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL
+ 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
+ user: QVITTER_USER_TIMELINE_URL
}
let url = timelineUrls[timeline]
+ let params = []
+
if (since) {
- url += `?since_id=${since}`
+ params.push(['since_id', since])
}
if (until) {
- url += `?max_id=${until}`
+ params.push(['max_id', until])
+ }
+
+ if (userId) {
+ params.push(['user_id', userId])
}
+ const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
+ url += `?${queryString}`
+
return fetch(url, { headers: authHeaders(credentials) }).then((data) => data.json())
}
@@ -198,7 +219,8 @@ const apiService = {
uploadMedia,
fetchAllFollowing,
setUserMute,
- fetchMutes
+ fetchMutes,
+ externalProfile
}
export default apiService
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index bc68d02c..f2d01c70 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -26,8 +26,8 @@ const backendInteractorService = (credentials) => {
return apiService.unfollowUser({credentials, id})
}
- const startFetching = ({timeline, store}) => {
- return timelineFetcherService.startFetching({timeline, store, credentials})
+ const startFetching = ({timeline, store, userId = false}) => {
+ return timelineFetcherService.startFetching({timeline, store, credentials, userId})
}
const setUserMute = ({id, muted = true}) => {
@@ -36,6 +36,8 @@ const backendInteractorService = (credentials) => {
const fetchMutes = () => apiService.fetchMutes({credentials})
+ const externalProfile = (profileUrl) => apiService.externalProfile(profileUrl)
+
const backendInteractorServiceInstance = {
fetchStatus,
fetchConversation,
@@ -46,7 +48,8 @@ const backendInteractorService = (credentials) => {
verifyCredentials: apiService.verifyCredentials,
startFetching,
setUserMute,
- fetchMutes
+ fetchMutes,
+ externalProfile
}
return backendInteractorServiceInstance
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 24aef069..b28de9e7 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -14,7 +14,7 @@ const update = ({store, statuses, timeline, showImmediately}) => {
})
}
-const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false}) => {
+const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false}) => {
const args = { timeline, credentials }
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
@@ -25,14 +25,16 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
args['since'] = timelineData.maxId
}
+ args['userId'] = userId
+
return apiService.fetchTimeline(args)
.then((statuses) => update({store, statuses, timeline, showImmediately}),
() => store.dispatch('setError', { value: true }))
}
-const startFetching = ({ timeline = 'friends', credentials, store }) => {
- fetchAndUpdate({timeline, credentials, store, showImmediately: true})
- const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store })
+const startFetching = ({timeline = 'friends', credentials, store, userId = false}) => {
+ fetchAndUpdate({timeline, credentials, store, showImmediately: true, userId})
+ const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId })
return setInterval(boundFetchAndUpdate, 10000)
}
const timelineFetcher = {