update branch and fix merge conflicts
This commit is contained in:
commit
e0e8965c08
52 changed files with 876 additions and 242 deletions
|
@ -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'
|
||||
|
|
100
src/App.scss
100
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;
|
||||
|
|
13
src/App.vue
13
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">
|
||||
|
|
|
@ -26,7 +26,7 @@ const Attachment = {
|
|||
autoHeight () {
|
||||
if (this.type === 'image' && this.nsfw) {
|
||||
return {
|
||||
'min-height': '311px'
|
||||
'min-height': '109px'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
<h2>Theme</h2>
|
||||
<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">
|
||||
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"> >
|
||||
<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">
|
||||
<small>
|
||||
<router-link :to="{ name: 'conversation', params: { id: status.in_reply_to_status_id } }">
|
||||
<i class="icon-reply"></i>
|
||||
</router-link>
|
||||
<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"> >
|
||||
<router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
|
||||
{{status.in_reply_to_screen_name}}
|
||||
</router-link>
|
||||
</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>
|
||||
<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.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}} </a>
|
||||
</small>
|
||||
</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;
|
||||
}
|
||||
|
||||
.source_url {
|
||||
float: right;
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
|
||||
.greentext {
|
||||
color: green;
|
||||
.source_url {
|
||||
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
22
src/components/user_finder/user_finder.js
Normal file
22
src/components/user_finder/user_finder.js
Normal file
|
@ -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
|
23
src/components/user_finder/user_finder.vue
Normal file
23
src/components/user_finder/user_finder.vue
Normal file
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -6,7 +6,10 @@ const defaultState = {
|
|||
colors: {},
|
||||
hideAttachments: false,
|
||||
hideAttachmentsInConv: false,
|
||||
hideNsfw: true
|
||||
hideNsfw: true,
|
||||
autoLoad: true,
|
||||
hoverPreview: true,
|
||||
muteWords: []
|
||||
}
|
||||
|
||||
const config = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue