aboutsummaryrefslogtreecommitdiff
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
parent44923afbee23ef7bd22c20d25bf7776b284f5f88 (diff)
parent7d46e3965d06e039537066eeb5fac99ebcab978d (diff)
update branch and fix merge conflicts
-rw-r--r--README.md2
-rw-r--r--index.html2
-rw-r--r--package.json8
-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
-rw-r--r--static/config.json2
-rw-r--r--static/css/base16-pleroma-dark.css33
-rw-r--r--static/css/base16-pleroma-light.css33
-rw-r--r--static/css/themes.json2
-rw-r--r--static/font/config.json12
-rw-r--r--static/font/css/fontello-codes.css4
-rw-r--r--static/font/css/fontello-embedded.css16
-rw-r--r--static/font/css/fontello-ie7-codes.css4
-rw-r--r--static/font/css/fontello-ie7.css4
-rw-r--r--static/font/css/fontello.css18
-rw-r--r--static/font/demo.html14
-rw-r--r--static/font/font/fontello.eotbin8428 -> 9052 bytes
-rw-r--r--static/font/font/fontello.svg4
-rw-r--r--static/font/font/fontello.ttfbin8260 -> 8884 bytes
-rw-r--r--static/font/font/fontello.woffbin5124 -> 5524 bytes
-rw-r--r--static/font/font/fontello.woff2bin4384 -> 4720 bytes
-rw-r--r--static/timeago.json10
-rw-r--r--test/unit/specs/modules/statuses.spec.js41
-rw-r--r--yarn.lock24
52 files changed, 874 insertions, 240 deletions
diff --git a/README.md b/README.md
index bdab5251..114228d8 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
> A Qvitter-style frontend for certain GS servers.
-![screenshot](http://i.imgur.com/3q30Zxt.jpg)
+![screenshot](https://my.mixtape.moe/kjzioz.PNG)
# FOR ADMINS
diff --git a/index.html b/index.html
index 668b21bb..b7654cc1 100644
--- a/index.html
+++ b/index.html
@@ -1,5 +1,5 @@
<!DOCTYPE html>
-<html>
+<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
diff --git a/package.json b/package.json
index 669a5932..d04c3e22 100644
--- a/package.json
+++ b/package.json
@@ -23,11 +23,11 @@
"object-path": "^0.11.3",
"sanitize-html": "^1.13.0",
"sass-loader": "^4.0.2",
- "vue": "^2.1.0",
- "vue-router": "^2.2.0",
- "vue-template-compiler": "^2.1.10",
+ "vue": "^2.3.4",
+ "vue-router": "^2.5.3",
+ "vue-template-compiler": "^2.3.4",
"vue-timeago": "^3.1.2",
- "vuex": "^2.1.0"
+ "vuex": "^2.3.1"
},
"devDependencies": {
"autoprefixer": "^6.4.0",
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 = {
diff --git a/static/config.json b/static/config.json
index fb8d4015..3b6d56c4 100644
--- a/static/config.json
+++ b/static/config.json
@@ -1,6 +1,6 @@
{
"name": "Pleroma FE",
- "theme": "base16-ashes.css",
+ "theme": "base16-pleroma-dark.css",
"background": "/static/bg.jpg",
"logo": "/static/logo.png"
}
diff --git a/static/css/base16-pleroma-dark.css b/static/css/base16-pleroma-dark.css
new file mode 100644
index 00000000..8190d2a7
--- /dev/null
+++ b/static/css/base16-pleroma-dark.css
@@ -0,0 +1,33 @@
+.base00-background { background-color: #161c20; }
+.base01-background { background-color: #282e32; }
+.base02-background { background-color: #343a3f; }
+.base03-background { background-color: #4e5256; }
+.base04-background { background-color: #ababab; }
+.base05-background { background-color: #b9b9b9; }
+.base06-background { background-color: #d0d0d0; }
+.base07-background { background-color: #e7e7e7; }
+.base08-background { background-color: #baaa9c; }
+.base09-background { background-color: #999999; }
+.base0A-background { background-color: #a0a0a0; }
+.base0B-background { background-color: #8e8e8e; }
+.base0C-background { background-color: #868686; }
+.base0D-background { background-color: #686868; }
+.base0E-background { background-color: #747474; }
+.base0F-background { background-color: #5e5e5e; }
+
+.base00 { color: #161c20; }
+.base01 { color: #282e32; }
+.base02 { color: #36393e; }
+.base03 { color: #4e5256; }
+.base04 { color: #ababab; }
+.base05 { color: #b9b9b9; }
+.base06 { color: #d0d0d0; }
+.base07 { color: #e7e7e7; }
+.base08 { color: #baaa9c; }
+.base09 { color: #999999; }
+.base0A { color: #a0a0a0; }
+.base0B { color: #8e8e8e; }
+.base0C { color: #868686; }
+.base0D { color: #686868; }
+.base0E { color: #747474; }
+.base0F { color: #5e5e5e; }
diff --git a/static/css/base16-pleroma-light.css b/static/css/base16-pleroma-light.css
new file mode 100644
index 00000000..1a85689a
--- /dev/null
+++ b/static/css/base16-pleroma-light.css
@@ -0,0 +1,33 @@
+.base00-background { background-color: #f2f4f6; }
+.base01-background { background-color: #dde2e6; }
+.base02-background { background-color: #c0c6cb; }
+.base03-background { background-color: #a4a4a4; }
+.base04-background { background-color: #545454; }
+.base05-background { background-color: #304055; }
+.base06-background { background-color: #040404; }
+.base07-background { background-color: #000000; }
+.base08-background { background-color: #e92f2f; }
+.base09-background { background-color: #e09448; }
+.base0A-background { background-color: #dddd13; }
+.base0B-background { background-color: #0ed839; }
+.base0C-background { background-color: #23edda; }
+.base0D-background { background-color: #3b48e3; }
+.base0E-background { background-color: #f996e2; }
+.base0F-background { background-color: #69542d; }
+
+.base00 { color: #f2f4f6; }
+.base01 { color: #dde2e6; }
+.base02 { color: #c0c6cb; }
+.base03 { color: #a4a4a4; }
+.base04 { color: #545454; }
+.base05 { color: #304055; }
+.base06 { color: #040404; }
+.base07 { color: #000000; }
+.base08 { color: #e46f0f; }
+.base09 { color: #e09448; }
+.base0A { color: #dddd13; }
+.base0B { color: #0ed839; }
+.base0C { color: #23edda; }
+.base0D { color: #3b48e3; }
+.base0E { color: #f996e2; }
+.base0F { color: #69542d; }
diff --git a/static/css/themes.json b/static/css/themes.json
index e3c35d6d..ea8e5a0c 100644
--- a/static/css/themes.json
+++ b/static/css/themes.json
@@ -1,4 +1,6 @@
[
+"base16-pleroma-dark.css",
+"base16-pleroma-light.css",
"base16-3024.css",
"base16-apathy.css",
"base16-ashes.css",
diff --git a/static/font/config.json b/static/font/config.json
index 58eb1943..7c58cada 100644
--- a/static/font/config.json
+++ b/static/font/config.json
@@ -77,6 +77,18 @@
"css": "cog",
"code": 59399,
"src": "fontawesome"
+ },
+ {
+ "uid": "1bafeeb1808a5fe24484c7890096901a",
+ "css": "user-plus",
+ "code": 62004,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "559647a6f430b3aeadbecd67194451dd",
+ "css": "menu",
+ "code": 61641,
+ "src": "fontawesome"
}
]
} \ No newline at end of file
diff --git a/static/font/css/fontello-codes.css b/static/font/css/fontello-codes.css
index 3658db77..3e658de9 100644
--- a/static/font/css/fontello-codes.css
+++ b/static/font/css/fontello-codes.css
@@ -9,5 +9,7 @@
.icon-cog:before { content: '\e807'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
+.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */
-.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
+.icon-binoculars:before { content: '\f1e5'; } /* '' */
+.icon-user-plus:before { content: '\f234'; } /* '' */ \ No newline at end of file
diff --git a/static/font/css/fontello-embedded.css b/static/font/css/fontello-embedded.css
index 360bf238..7dedc03f 100644
--- a/static/font/css/fontello-embedded.css
+++ b/static/font/css/fontello-embedded.css
@@ -1,15 +1,15 @@
@font-face {
font-family: 'fontello';
- src: url('../font/fontello.eot?36468641');
- src: url('../font/fontello.eot?36468641#iefix') format('embedded-opentype'),
- url('../font/fontello.svg?36468641#fontello') format('svg');
+ src: url('../font/fontello.eot?46746090');
+ src: url('../font/fontello.eot?46746090#iefix') format('embedded-opentype'),
+ url('../font/fontello.svg?46746090#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'fontello';
- src: url('data:application/octet-stream;base64,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') format('woff'),
- url('data:application/octet-stream;base64,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') format('truetype');
+ src: url('data:application/octet-stream;base64,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') format('woff'),
+ url('data:application/octet-stream;base64,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') format('truetype');
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
@@ -17,7 +17,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
- src: url('../font/fontello.svg?36468641#fontello') format('svg');
+ src: url('../font/fontello.svg?46746090#fontello') format('svg');
}
}
*/
@@ -62,5 +62,7 @@
.icon-cog:before { content: '\e807'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
+.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */
-.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
+.icon-binoculars:before { content: '\f1e5'; } /* '' */
+.icon-user-plus:before { content: '\f234'; } /* '' */ \ No newline at end of file
diff --git a/static/font/css/fontello-ie7-codes.css b/static/font/css/fontello-ie7-codes.css
index 9bd3bc9e..dfab853a 100644
--- a/static/font/css/fontello-ie7-codes.css
+++ b/static/font/css/fontello-ie7-codes.css
@@ -9,5 +9,7 @@
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
+.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
-.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); } \ No newline at end of file
+.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); }
+.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); } \ No newline at end of file
diff --git a/static/font/css/fontello-ie7.css b/static/font/css/fontello-ie7.css
index a5745239..3e93ecd2 100644
--- a/static/font/css/fontello-ie7.css
+++ b/static/font/css/fontello-ie7.css
@@ -20,5 +20,7 @@
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
+.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
-.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); } \ No newline at end of file
+.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); }
+.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); } \ No newline at end of file
diff --git a/static/font/css/fontello.css b/static/font/css/fontello.css
index 7b1fbd0c..81250ae3 100644
--- a/static/font/css/fontello.css
+++ b/static/font/css/fontello.css
@@ -1,11 +1,11 @@
@font-face {
font-family: 'fontello';
- src: url('../font/fontello.eot?90538621');
- src: url('../font/fontello.eot?90538621#iefix') format('embedded-opentype'),
- url('../font/fontello.woff2?90538621') format('woff2'),
- url('../font/fontello.woff?90538621') format('woff'),
- url('../font/fontello.ttf?90538621') format('truetype'),
- url('../font/fontello.svg?90538621#fontello') format('svg');
+ src: url('../font/fontello.eot?79576261');
+ src: url('../font/fontello.eot?79576261#iefix') format('embedded-opentype'),
+ url('../font/fontello.woff2?79576261') format('woff2'),
+ url('../font/fontello.woff?79576261') format('woff'),
+ url('../font/fontello.ttf?79576261') format('truetype'),
+ url('../font/fontello.svg?79576261#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
- src: url('../font/fontello.svg?90538621#fontello') format('svg');
+ src: url('../font/fontello.svg?79576261#fontello') format('svg');
}
}
*/
@@ -65,5 +65,7 @@
.icon-cog:before { content: '\e807'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
+.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */
-.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
+.icon-binoculars:before { content: '\f1e5'; } /* '' */
+.icon-user-plus:before { content: '\f234'; } /* '' */ \ No newline at end of file
diff --git a/static/font/demo.html b/static/font/demo.html
index 98b49a84..02fb5d79 100644
--- a/static/font/demo.html
+++ b/static/font/demo.html
@@ -229,11 +229,11 @@ body {
}
@font-face {
font-family: 'fontello';
- src: url('./font/fontello.eot?15442171');
- src: url('./font/fontello.eot?15442171#iefix') format('embedded-opentype'),
- url('./font/fontello.woff?15442171') format('woff'),
- url('./font/fontello.ttf?15442171') format('truetype'),
- url('./font/fontello.svg?15442171#fontello') format('svg');
+ src: url('./font/fontello.eot?13861244');
+ src: url('./font/fontello.eot?13861244#iefix') format('embedded-opentype'),
+ url('./font/fontello.woff?13861244') format('woff'),
+ url('./font/fontello.ttf?13861244') format('truetype'),
+ url('./font/fontello.svg?13861244#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -315,8 +315,12 @@ body {
<div class="row">
<div title="Code: 0xe832" class="the-icons span3"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
<div title="Code: 0xe834" class="the-icons span3"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
+ <div title="Code: 0xf0c9" class="the-icons span3"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
<div title="Code: 0xf112" class="the-icons span3"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
+ </div>
+ <div class="row">
<div title="Code: 0xf1e5" class="the-icons span3"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
+ <div title="Code: 0xf234" class="the-icons span3"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
</div>
</div>
<div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
diff --git a/static/font/font/fontello.eot b/static/font/font/fontello.eot
index 4050fa3b..4573d823 100644
--- a/static/font/font/fontello.eot
+++ b/static/font/font/fontello.eot
Binary files differ
diff --git a/static/font/font/fontello.svg b/static/font/font/fontello.svg
index f1e8b9fc..98105a87 100644
--- a/static/font/font/fontello.svg
+++ b/static/font/font/fontello.svg
@@ -26,9 +26,13 @@
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 850c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
+<glyph glyph-name="menu" unicode="&#xf0c9;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
+
<glyph glyph-name="reply" unicode="&#xf112;" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="user-plus" unicode="&#xf234;" d="M393 350q-89 0-152 63t-62 151 62 152 152 63 151-63 63-152-63-151-151-63z m536-71h196q7 0 13-6t5-12v-107q0-8-5-13t-13-5h-196v-197q0-7-6-12t-12-6h-107q-8 0-13 6t-5 12v197h-197q-7 0-12 5t-6 13v107q0 7 6 12t12 6h197v196q0 7 5 13t13 5h107q7 0 12-5t6-13v-196z m-411-125q0-29 21-51t50-21h143v-133q-38-28-95-28h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q11 0 22-10 44-34 86-51t92-17 92 17 86 51q11 10 22 10 73 0 121-54h-125q-29 0-50-21t-21-50v-107z" horiz-adv-x="1142.9" />
</font>
</defs>
</svg> \ No newline at end of file
diff --git a/static/font/font/fontello.ttf b/static/font/font/fontello.ttf
index bec32f07..c3bfb92a 100644
--- a/static/font/font/fontello.ttf
+++ b/static/font/font/fontello.ttf
Binary files differ
diff --git a/static/font/font/fontello.woff b/static/font/font/fontello.woff
index 245e1d2f..dced1f8c 100644
--- a/static/font/font/fontello.woff
+++ b/static/font/font/fontello.woff
Binary files differ
diff --git a/static/font/font/fontello.woff2 b/static/font/font/fontello.woff2
index 9ec54aa4..b91fcbd7 100644
--- a/static/font/font/fontello.woff2
+++ b/static/font/font/fontello.woff2
Binary files differ
diff --git a/static/timeago.json b/static/timeago.json
new file mode 100644
index 00000000..b6f669c2
--- /dev/null
+++ b/static/timeago.json
@@ -0,0 +1,10 @@
+[
+ "now",
+ ["%ss", "%ss"],
+ ["%smin", "%smin"],
+ ["%sh", "%sh"],
+ ["%sd", "%sd"],
+ ["%sw", "%sw"],
+ ["%sm", "%sm"],
+ ["%sy", "%sy"]
+]
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index 891423ca..d25cc108 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -125,18 +125,19 @@ describe('The Statuses module', () => {
it('removes statuses by tag on deletion', () => {
const state = cloneDeep(defaultState)
const status = makeMockStatus({id: 1})
+ const otherStatus = makeMockStatus({id: 3})
status.uri = 'xxx'
const deletion = makeMockStatus({id: 2, is_post_verb: false})
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
deletion.uri = 'xxx'
- mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' })
mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' })
- expect(state.allStatuses).to.eql([])
- expect(state.timelines.public.statuses).to.eql([])
- expect(state.timelines.public.visibleStatuses).to.eql([])
- expect(state.timelines.public.maxId).to.eql(2)
+ expect(state.allStatuses).to.eql([otherStatus])
+ expect(state.timelines.public.statuses).to.eql([otherStatus])
+ expect(state.timelines.public.visibleStatuses).to.eql([otherStatus])
+ expect(state.timelines.public.maxId).to.eql(3)
})
it('does not update the maxId when the noIdUpdate flag is set', () => {
@@ -319,6 +320,36 @@ describe('The Statuses module', () => {
expect(state.notifications[0].type).to.eql('mention')
})
+ it('removes a notification when the notice gets removed', () => {
+ const user = { id: 1 }
+ const state = cloneDeep(defaultState)
+ const status = makeMockStatus({id: 1})
+ const otherStatus = makeMockStatus({id: 3})
+ const mentionedStatus = makeMockStatus({id: 2})
+ mentionedStatus.attentions = [user]
+ mentionedStatus.uri = 'xxx'
+ otherStatus.attentions = [user]
+
+ const deletion = makeMockStatus({id: 4, is_post_verb: false})
+ deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
+ deletion.uri = 'xxx'
+
+ mutations.addNewStatuses(state, { statuses: [status, otherStatus], user })
+
+ expect(state.notifications.length).to.eql(1)
+
+ mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
+ expect(state.allStatuses.length).to.eql(3)
+ expect(state.notifications.length).to.eql(2)
+ expect(state.notifications[1].status).to.eql(mentionedStatus)
+ expect(state.notifications[1].action).to.eql(mentionedStatus)
+ expect(state.notifications[1].type).to.eql('mention')
+
+ mutations.addNewStatuses(state, { statuses: [deletion], user })
+ expect(state.allStatuses.length).to.eql(2)
+ expect(state.notifications.length).to.eql(1)
+ })
+
it('adds the message to mentions when you are mentioned', () => {
const user = { id: 1 }
const state = cloneDeep(defaultState)
diff --git a/yarn.lock b/yarn.lock
index 6249966e..b0c3e63e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5713,9 +5713,9 @@ vue-loader@^11.1.0:
vue-style-loader "^2.0.0"
vue-template-es2015-compiler "^1.2.2"
-vue-router@^2.2.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.2.1.tgz#b027f9fac2cf13462725e843d6dc631b6aa077f6"
+vue-router@^2.5.3:
+ version "2.5.3"
+ resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.5.3.tgz#073783f564b6aece6c8a59c63e298dc2aabfb51b"
vue-style-loader@^2.0.0:
version "2.0.0"
@@ -5724,9 +5724,9 @@ vue-style-loader@^2.0.0:
hash-sum "^1.0.2"
loader-utils "^0.2.7"
-vue-template-compiler@^2.1.10:
- version "2.1.10"
- resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.1.10.tgz#cb89643adc395e97435585522e43d0a9b1913257"
+vue-template-compiler@^2.3.4:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.3.4.tgz#5a88ac2c5e4d5d6218e6aa80e7e221fb7e67894c"
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
@@ -5739,13 +5739,13 @@ vue-timeago@^3.1.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/vue-timeago/-/vue-timeago-3.2.0.tgz#73fd0635de6ea4ecfbbce035b2e44035d806fba1"
-vue@^2.1.0:
- version "2.1.10"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b"
+vue@^2.3.4:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.3.4.tgz#5ec3b87a191da8090bbef56b7cfabd4158038171"
-vuex@^2.1.0:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.1.2.tgz#15d2da62dd6ff59c071f0a91cd4f434eacf6ca6c"
+vuex@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.3.1.tgz#cde8e997c1f9957719bc7dea154f9aa691d981a6"
watchpack@^0.2.1:
version "0.2.9"