Merge branch 'develop' into feature/hash-routed

This commit is contained in:
Roger Braun 2017-02-20 18:58:18 +01:00
commit 5d8b2eb8b5
29 changed files with 1706 additions and 1531 deletions

View file

@ -1,5 +1,5 @@
{ {
"presets": ["es2015", "stage-2"], "presets": ["es2015", "stage-2"],
"plugins": ["transform-runtime"], "plugins": ["transform-runtime", "lodash"],
"comments": false "comments": false
} }

View file

@ -23,16 +23,16 @@ before_script:
# This folder is cached between builds # This folder is cached between builds
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache # http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache: #cache:
paths: # paths:
- node_modules/ # - node_modules/
test: test:
script: script:
- npm install -g yarn - npm install -g yarn
- yarn - yarn
- npm run unit - npm run unit
build: build:
script: script:
- npm install -g yarn - npm install -g yarn

49
COFE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,49 @@
```
o$$$$$$oo
o$" "$oo
$ o""""$o "$o
"$ o "o "o $
"$ $o $ $ o$
"$ o$"$ o$
"$ooooo$$ $ o$
o$ """ $ " $$$ " $
o$ $o $$" " "
$$ $ " $ $$$o"$ o o$"
$" o "" $ $" " o" $$
$o " " $ o$" o" o$"
"$o $$ $ o" o$$"
""o$o"$" $oo" o$"
o$$ $ $$$ o$$
o" o oo"" "" "$o
o$o" "" $
$" " o" " " " "o
$$ " " o$ o$o " $
o$ $ $ o$$ " " ""
o $ $" " "o o$
$ o $o$oo$""
$o $ o o o"$$
$o o $ $ "$o
$o $ o $ $ "o
$ $ "o $ "o"$o
$ " o $ o $$
$o$o$o$o$$o$$$o$$o$o$$o$$o$$$o$o$o$o$o$o$o$o$o$ooo
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ " $$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$"
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ooooo$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""""
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
"$o$o$o$o$o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""
"""""""""""""""""""""""""""""""""""""""""""""""""""""
```

View file

@ -2,5 +2,8 @@ Contributors of this project.
- Constance Variable (lambadalambda@social.heldscal.la): Code - Constance Variable (lambadalambda@social.heldscal.la): Code
- Coco Snuss (cocosnuss@social.heldscal.la): Code - Coco Snuss (cocosnuss@social.heldscal.la): Code
- wakarimasen (wakarimasen@soykaf.com): NSFW hiding image - wakarimasen (wakarimasen@shitposter.club): NSFW hiding image
- dtluna (dtluna@social.heldscal.la): Code - dtluna (dtluna@social.heldscal.la): Code
- sonyam (sonyam@social.heldscal.la): Default background image
- hakui (hakui@freezepeach.xyz): CSS and styling
- shpuld (shpuld@shitposter.club): CSS and styling

View file

@ -2,6 +2,8 @@
> A Qvitter-style frontend for certain GS servers. > A Qvitter-style frontend for certain GS servers.
![screenshot](http://i.imgur.com/3q30Zxt.jpg)
# FOR ADMINS # FOR ADMINS
You don't need to build Pleroma yourself. Check out https://gitgud.io/lambadalambda/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma and Qvitter at the same time. You don't need to build Pleroma yourself. Check out https://gitgud.io/lambadalambda/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma and Qvitter at the same time.
@ -10,7 +12,8 @@ You don't need to build Pleroma yourself. Check out https://gitgud.io/lambadalam
``` bash ``` bash
# install dependencies # install dependencies
npm install npm install -g yarn
yarn
# serve with hot reload at localhost:8080 # serve with hot reload at localhost:8080
npm run dev npm run dev

View file

@ -14,17 +14,19 @@
}, },
"dependencies": { "dependencies": {
"babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-lodash": "^3.2.11",
"diff": "^3.0.1", "diff": "^3.0.1",
"karma-mocha-reporter": "^2.2.1", "karma-mocha-reporter": "^2.2.1",
"node-sass": "^3.10.1", "node-sass": "^3.10.1",
"object-path": "^0.11.3",
"pako": "^1.0.4",
"sanitize-html": "^1.13.0", "sanitize-html": "^1.13.0",
"sass-loader": "^4.0.2", "sass-loader": "^4.0.2",
"tributejs": "^2.1.0", "tributejs": "^2.1.0",
"vue": "^2.0.1", "vue": "^2.1.0",
"vue-router": "^2.0.1", "vue-router": "^2.2.0",
"vue-timeago": "^3.1.2", "vue-timeago": "^3.1.2",
"vuex": "^2.0.0", "vuex": "^2.1.0"
"vuex-persistedstate": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.4.0", "autoprefixer": "^6.4.0",

View file

@ -24,6 +24,9 @@ export default {
methods: { methods: {
activatePanel (panelName) { activatePanel (panelName) {
this.mobileActivePanel = panelName this.mobileActivePanel = panelName
},
scrollToTop () {
window.scrollTo(0, 0)
} }
} }
} }

View file

@ -29,6 +29,15 @@ a {
text-decoration: none; text-decoration: none;
} }
button{
border: none;
border-radius: 5px;
&:hover {
background-color: white;
}
}
.container { .container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -63,7 +72,7 @@ nav {
padding-right: 20px; padding-right: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-basis: 920px; flex-basis: 970px;
margin: auto; margin: auto;
height: 50px; height: 50px;
background-repeat: no-repeat; background-repeat: no-repeat;
@ -99,10 +108,10 @@ main-router {
.panel-heading { .panel-heading {
border-radius: 0.5em 0.5em 0 0; border-radius: 0.5em 0.5em 0 0;
background-size: cover; background-size: cover;
padding-top: 0.3em; padding: 0.6em 0;
padding-bottom: 0.3em;
text-align: center; text-align: center;
font-size: 1.3em; font-size: 1.3em;
line-height: 24px;
} }
.panel-footer { .panel-footer {
@ -110,6 +119,7 @@ main-router {
} }
.panel-body > p { .panel-body > p {
line-height: 18px;
padding: 1em; padding: 1em;
margin: 0; margin: 0;
} }
@ -117,7 +127,7 @@ main-router {
#content { #content {
margin: auto; margin: auto;
max-width: 920px; max-width: 980px;
border-radius: 1em; border-radius: 1em;
padding-bottom: 1em; padding-bottom: 1em;
background-color: rgba(0,0,0,0.1); background-color: rgba(0,0,0,0.1);
@ -125,7 +135,7 @@ main-router {
.media-body { .media-body {
flex: 1; flex: 1;
padding-left: 0.3em; padding-left: 0.5em;
} }
.container > * { .container > * {
@ -133,28 +143,37 @@ main-router {
} }
.user-info { .user-info {
color: white;
padding: 1em; padding: 1em;
img { img {
border: 3px solid; border: 2px solid;
border-radius: 0.5em border-radius: 0.5em
} }
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
.user-name{
margin-top: 0.2em;
}
.user-screen-name { .user-screen-name {
margin-top: 0.3em;
font-weight: lighter; font-weight: lighter;
padding-right: 0.1em;
} }
} }
.user-counts { .user-counts {
display: flex; display: flex;
padding: 1em 1em 0em 1em; line-height:16px;
padding: 1em 1.5em 0em 1em;
text-align: center;
} }
.user-count { .user-count {
flex: 1; flex: 1;
h5 { h5 {
font-weight: lighter; font-size:1em;
margin: 0; font-weight: bolder;
margin: 0 0 0.25em;
} }
} }
@ -196,7 +215,7 @@ status-text-container {
} }
.retweet-info { .retweet-info {
padding: 0.3em; padding: 0.7em 0 0 0.6em;
.media-left { .media-left {
display: flex; display: flex;
@ -214,6 +233,7 @@ status-text-container {
small { small {
font-weight: lighter; font-weight: lighter;
} }
margin-bottom: 0.3em;
} }
} }
nav { nav {
@ -228,13 +248,13 @@ nav {
} }
.main { .main {
flex: 2; flex: 1;
flex-basis: 500px; flex-basis: 65%;
} }
.sidebar { .sidebar {
flex: 1; flex: 1;
flex-basis: 300px; flex-basis: 35%;
} }
.mobile-shown { .mobile-shown {
@ -261,6 +281,14 @@ nav {
.panel-switcher { .panel-switcher {
display: flex; display: flex;
} }
.container {
padding: 0 0 0 0;
}
.panel {
margin: 0.5em 0 0.5em 0;
}
} }
.item.right { .item.right {

View file

@ -1,6 +1,6 @@
<template> <template>
<div id="app" v-bind:style="style" class="base02-background"> <div id="app" v-bind:style="style" class="base02-background">
<nav class='container base01-background base04'> <nav class='container base01-background base04' @click="scrollToTop()">
<div class='inner-nav' :style="logoStyle"> <div class='inner-nav' :style="logoStyle">
<div class='item'> <div class='item'>
<router-link :to="{ name: 'root'}">{{sitename}}</router-link> <router-link :to="{ name: 'root'}">{{sitename}}</router-link>

View file

@ -20,6 +20,11 @@ const Attachment = {
} }
}, },
methods: { methods: {
linkClicked ({target}) {
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
toggleHidden () { toggleHidden () {
this.showHidden = !this.showHidden this.showHidden = !this.showHidden
} }

View file

@ -18,7 +18,7 @@
<span v-if="type === 'unknown'">Don't know how to display this...</span> <span v-if="type === 'unknown'">Don't know how to display this...</span>
<div v-if="type === 'html' && attachment.oembed" class="oembed"> <div @click.prevent="linkClicked" v-if="type === 'html' && attachment.oembed" class="oembed">
<div v-if="attachment.thumb_url" class="image"> <div v-if="attachment.thumb_url" class="image">
<img :src="attachment.thumb_url"></img> <img :src="attachment.thumb_url"></img>
</div> </div>
@ -39,7 +39,7 @@
.attachment { .attachment {
flex: 1 0 30%; flex: 1 0 30%;
display: flex; display: flex;
margin: 0.2em; margin: 0.5em 0.8em 0.6em 0.1em;
align-self: flex-start; align-self: flex-start;
&.html { &.html {
@ -79,6 +79,7 @@
img { img {
width: 100%; width: 100%;
} }
margin-right: 15px;
} }
.oembed { .oembed {
@ -91,6 +92,8 @@
img { img {
border: 0px; border: 0px;
border-radius: 0; border-radius: 0;
height: 100%;
object-fit: cover;
} }
} }

View file

@ -39,8 +39,7 @@
.nav-panel li { .nav-panel li {
border-bottom: 1px solid; border-bottom: 1px solid;
padding: 0.5em; padding: 0.8em 0.85em;
padding-left: 1em;
} }
.nav-panel li:last-child { .nav-panel li:last-child {

View file

@ -1,14 +1,40 @@
import { sortBy, take } from 'lodash' import { sortBy, take, filter } from 'lodash'
const Notifications = { const Notifications = {
data () { data () {
return { return {
visibleNotificationCount: 20 visibleNotificationCount: 10
} }
}, },
computed: { computed: {
notifications () {
return this.$store.state.statuses.notifications
},
unseenNotifications () {
return filter(this.notifications, ({seen}) => !seen)
},
visibleNotifications () { visibleNotifications () {
return take(sortBy(this.$store.state.statuses.notifications, ({action}) => -action.id), this.visibleNotificationCount) // Don't know why, but sortBy([seen, -action.id]) doesn't work.
let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id)
sortedNotifications = sortBy(sortedNotifications, 'seen')
return take(sortedNotifications, this.visibleNotificationCount)
},
unseenCount () {
return this.unseenNotifications.length
}
},
watch: {
unseenCount (count) {
if (count > 0) {
this.$store.dispatch('setPageTitle', `(${count})`)
} else {
this.$store.dispatch('setPageTitle', '')
}
}
},
methods: {
markAsSeen () {
this.$store.commit('markNotificationsAsSeen', this.visibleNotifications)
} }
} }
} }

View file

@ -1,13 +1,13 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.notification { .notification {
padding: 0.5em; padding: 0.4em 0 0 0.7em;
padding-left: 1em;
display: flex; display: flex;
border-bottom: 1px solid silver; border-bottom: 1px solid silver;
.text { .text {
min-width: 0px; min-width: 0px;
word-wrap: break-word; word-wrap: break-word;
line-height:18px;
.icon-retweet { .icon-retweet {
color: $green; color: $green;
@ -18,21 +18,22 @@
} }
h1 { h1 {
margin: 0; margin: 0 0 0.3em;
padding: 0; padding: 0;
font-size: 1em; font-size: 1em;
line-height:20px;
} }
padding-left: 0.5em; padding: 0.3em 0.8em 0.5em;
p { p {
margin: 0; margin: 0;
margin-top: 0; margin-top: 0;
margin-bottom: 0.5em; margin-bottom: 0.3em;
} }
} }
.avatar { .avatar {
padding-top: 3px; padding-top: 0.3em;
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 50%; border-radius: 50%;

View file

@ -1,13 +1,14 @@
<template> <template>
<div class="notifications"> <div class="notifications">
<div class="panel panel-default base00-background"> <div class="panel panel-default base00-background">
<div class="panel-heading base01-background base04">Notifications ({{visibleNotifications.length}})</div> <div class="panel-heading base01-background base04">Notifications ({{unseenCount}}) <button @click.prevent="markAsSeen">Read!</button></div>
<div class="panel-body"> <div class="panel-body">
<div v-for="notification in visibleNotifications" class="notification"> <div v-for="notification in visibleNotifications" class="notification" :class='{"base01-background": notification.seen}'>
<a :href="notification.action.user.statusnet_profile_url"> <a :href="notification.action.user.statusnet_profile_url">
<img class='avatar' :src="notification.action.user.profile_image_url_original"> <img class='avatar' :src="notification.action.user.profile_image_url_original">
</a> </a>
<div class='text'> <div class='text'>
<timeago :since="notification.action.created_at" :auto-update="240"></timeago>
<div v-if="notification.type === 'favorite'"> <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> <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> <p>{{ notification.status.text }}</p>

View file

@ -2,7 +2,7 @@
<div class="post-status-form"> <div class="post-status-form">
<form @submit.prevent="postStatus(newStatus)"> <form @submit.prevent="postStatus(newStatus)">
<div class="form-group" > <div class="form-group" >
<textarea v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control"></textarea> <textarea v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control" @keyup.ctrl.enter="postStatus(newStatus)"></textarea>
</div> </div>
<div class="attachments"> <div class="attachments">
<div class="attachment" v-for="file in newStatus.files"> <div class="attachment" v-for="file in newStatus.files">
@ -57,13 +57,22 @@
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.5em; padding: 0.6em;
} }
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.3em 0.5em 0.6em;
line-height:24px;
}
form textarea {
border: none;
border-radius: 2px;
line-height:16px;
padding: 0.5em; padding: 0.5em;
resize: vertical;
} }
.btn { .btn {

View file

@ -17,6 +17,6 @@
<style> <style>
.setting-item { .setting-item {
margin: 1em margin: 1em 1em 1.4em;
} }
</style> </style>

View file

@ -40,6 +40,14 @@ const Status = {
UserCardContent UserCardContent
}, },
methods: { methods: {
linkClicked ({target}) {
if (target.tagName === 'SPAN') {
target = target.parentNode
}
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
toggleReplying () { toggleReplying () {
this.replying = !this.replying this.replying = !this.replying
}, },

View file

@ -54,7 +54,7 @@
</small> </small>
</h4> </h4>
<div class="status-content" v-html="status.statusnet_html"></div> <div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
<div v-if='status.attachments' class='attachments'> <div v-if='status.attachments' class='attachments'>
<attachment :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments"> <attachment :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments">
@ -94,6 +94,7 @@
.user-content { .user-content {
min-height: 52px; min-height: 52px;
padding-top: 1px;
} }
.source_url { .source_url {
@ -110,8 +111,7 @@
} }
.status-content { .status-content {
margin-top: 3px; margin: 3px 15px 4px 0;
margin-bottom: 3px;
} }
p { p {
@ -138,8 +138,7 @@
} }
.status { .status {
padding: 0.5em; padding: 0.65em 0.7em 0.8em 0.8em;
padding-right: 1em;
border-bottom: 1px solid; border-bottom: 1px solid;
} }
.muted button { .muted button {

View file

@ -3,30 +3,34 @@
<div class="base00-background panel-heading text-center" v-bind:style="style"> <div class="base00-background panel-heading text-center" v-bind:style="style">
<div class='user-info'> <div class='user-info'>
<img :src="user.profile_image_url"> <img :src="user.profile_image_url">
<div v-if='user.muted' class='muteinfo'>Muted</div>
<div class='muteinfo' v-if='isOtherUser'>
<button @click="toggleMute">Mute/Unmute</button>
</div>
<span class="glyphicon glyphicon-user"></span> <span class="glyphicon glyphicon-user"></span>
<div class='user-name'>{{user.name}}</div> <div class='user-name'>{{user.name}}</div>
<div class='user-screen-name'>@{{user.screen_name}}</div> <div class='user-screen-name'>@{{user.screen_name}}</div>
<div v-if="isOtherUser" class="following-info"> <div v-if="isOtherUser" class="user-interactions">
<div v-if="user.follows_you" class="following"> <div v-if="user.follows_you" class="following base06">
Follows you! Follows you!
</div> </div>
<div class="followed"> <div class="follow">
<span v-if="user.following"> <span v-if="user.following">
Following them! <!--Following them!-->
<button @click="unfollowUser"> <button @click="unfollowUser" class="base06 base01-background base06-border">
Unfollow! Unfollow
</button> </button>
</span> </span>
<span v-if="!user.following" > <span v-if="!user.following">
<button @click="followUser"> <button @click="followUser" class="base01 base04-background base01-border">
Follow! Follow
</button> </button>
</span> </span>
</div> </div>
<div class='mute' v-if='isOtherUser'>
<span v-if='user.muted'>
<button @click="toggleMute" class="base04 base01-background base06-border">Unmute</button>
</span>
<span v-if='!user.muted'>
<button @click="toggleMute" class="base01 base04-background base01-border">Mute</button>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -78,6 +82,7 @@
toggleMute () { toggleMute () {
const store = this.$store const store = this.$store
store.commit('setMuted', {user: this.user, muted: !this.user.muted}) store.commit('setMuted', {user: this.user, muted: !this.user.muted})
store.state.api.backendInteractor.setUserMute(this.user)
} }
} }
} }

View file

@ -13,12 +13,39 @@
} }
.user-info { .user-info {
.following-info { .user-interactions {
display: flex; display: flex;
flex-flow: row wrap;
justify-content: center;
div { div {
flex: 1; flex: 1;
} }
margin-top: 0.5em;
margin-bottom: -1.2em;
.following {
font-size: 14px;
flex: 0 0 100%;
margin-bottom: 0.5em;
}
.mute {
max-width: 200px;
}
.follow {
max-width: 200px;
}
button {
width: 80%;
height: 100%;
border: 1px solid;
}
}
.user-screen-name {
margin-top: 0.4em;
} }
} }
</style> </style>

View file

@ -0,0 +1,76 @@
import merge from 'lodash.merge'
import objectPath from 'object-path'
import { throttle } from 'lodash'
import { inflate, deflate } from 'pako'
const defaultReducer = (state, paths) => (
paths.length === 0 ? state : paths.reduce((substate, path) => {
objectPath.set(substate, path, objectPath.get(state, path))
return substate
}, {})
)
const defaultStorage = (() => {
const hasLocalStorage = typeof window !== 'undefined' && window.localStorage
if (hasLocalStorage) {
return window.localStorage
}
class InternalStorage {
setItem (key, item) {
this[key] = item
return item
}
getItem (key) {
return this[key]
}
removeItem (key) {
delete this[key]
}
clear () {
Object.keys(this).forEach(key => delete this[key])
}
}
return new InternalStorage()
})()
const defaultSetState = (key, state, storage) => {
return storage.setItem(key, deflate(JSON.stringify(state), { to: 'string' }))
}
export default function createPersistedState ({
key = 'vuex',
paths = [],
getState = (key, storage) => {
let value = storage.getItem(key)
try {
value = inflate(value, { to: 'string' })
} catch (e) {
console.log("Couldn't inflate value... Maybe upgrading")
}
return value && value !== 'undefined' ? JSON.parse(value) : undefined
},
setState = throttle(defaultSetState, 5000),
reducer = defaultReducer,
storage = defaultStorage,
subscriber = store => handler => store.subscribe(handler)
} = {}) {
return store => {
const savedState = getState(key, storage)
if (typeof savedState === 'object') {
store.replaceState(
merge({}, store.state, savedState)
)
}
subscriber(store)((mutation, state) => {
try {
setState(key, reducer(state, paths), storage)
} catch (e) {
console.log("Couldn't persist state:")
console.log(e)
}
})
}
}

View file

@ -17,7 +17,7 @@ import configModule from './modules/config.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import createPersistedState from 'vuex-persistedstate' import createPersistedState from './lib/persisted_state.js'
Vue.use(Vuex) Vue.use(Vuex)
Vue.use(VueRouter) Vue.use(VueRouter)
@ -29,7 +29,7 @@ Vue.use(VueTimeago, {
}) })
const persistedStateOptions = { const persistedStateOptions = {
paths: ['users.users'] paths: ['users.users', 'statuses.notifications']
} }
const store = new Vuex.Store({ const store = new Vuex.Store({

View file

@ -13,11 +13,14 @@ const config = {
} }
}, },
actions: { actions: {
setOption ({ commit }, { name, value }) { setPageTitle ({state}, option = '') {
document.title = `${option} ${state.name}`
},
setOption ({ commit, dispatch }, { name, value }) {
commit('setOption', {name, value}) commit('setOption', {name, value})
switch (name) { switch (name) {
case 'name': case 'name':
document.title = value dispatch('setPageTitle')
break break
case 'theme': case 'theme':
const fullPath = `/static/css/${value}` const fullPath = `/static/css/${value}`

View file

@ -173,7 +173,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
} }
const addNotification = ({type, status, action}) => { const addNotification = ({type, status, action}) => {
state.notifications.push({type, status, action}) // Only add a new notification if we don't have one for the same action
if (!find(state.notifications, (oldNotification) => oldNotification.action.id === action.id)) {
state.notifications.push({type, status, action, seen: false})
}
} }
const favoriteStatus = (favorite) => { const favoriteStatus = (favorite) => {
@ -276,6 +279,11 @@ export const mutations = {
setNsfw (state, { id, nsfw }) { setNsfw (state, { id, nsfw }) {
const newStatus = find(state.allStatuses, { id }) const newStatus = find(state.allStatuses, { id })
newStatus.nsfw = nsfw newStatus.nsfw = nsfw
},
markNotificationsAsSeen (state, notifications) {
each(notifications, (notification) => {
notification.seen = true
})
} }
} }

View file

@ -82,6 +82,12 @@ const users = {
// Start getting fresh tweets. // Start getting fresh tweets.
store.dispatch('startFetching', 'friends') store.dispatch('startFetching', 'friends')
// Get user mutes and follower info
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
each(mutedUsers, (user) => { user.muted = true })
store.commit('addNewUsers', mutedUsers)
})
// Fetch our friends // Fetch our friends
store.rootState.api.backendInteractor.fetchFriends() store.rootState.api.backendInteractor.fetchFriends()
.then((friends) => commit('addNewUsers', friends)) .then((friends) => commit('addNewUsers', friends))

View file

@ -16,6 +16,7 @@ const MENTIONS_URL = '/api/statuses/mentions.json'
const FRIENDS_URL = '/api/statuses/friends.json' const FRIENDS_URL = '/api/statuses/friends.json'
const FOLLOWING_URL = '/api/friendships/create.json' const FOLLOWING_URL = '/api/friendships/create.json'
const UNFOLLOWING_URL = '/api/friendships/destroy.json' const UNFOLLOWING_URL = '/api/friendships/destroy.json'
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
// const USER_URL = '/api/users/show.json' // const USER_URL = '/api/users/show.json'
const oldfetch = window.fetch const oldfetch = window.fetch
@ -58,7 +59,7 @@ const fetchFriends = ({credentials}) => {
const fetchAllFollowing = ({username, credentials}) => { const fetchAllFollowing = ({username, credentials}) => {
const url = `${ALL_FOLLOWING_URL}/${username}.json` const url = `${ALL_FOLLOWING_URL}/${username}.json`
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json().users) .then((data) => data.json())
} }
const fetchMentions = ({username, sinceId = 0, credentials}) => { const fetchMentions = ({username, sinceId = 0, credentials}) => {
@ -79,6 +80,22 @@ const fetchStatus = ({id, credentials}) => {
.then((data) => data.json()) .then((data) => data.json())
} }
const setUserMute = ({id, credentials, muted = true}) => {
const form = new FormData()
const muteInteger = muted ? 1 : 0
form.append('namespace', 'qvitter')
form.append('data', muteInteger)
form.append('topic', `mute:${id}`)
return fetch(QVITTER_USER_PREF_URL, {
method: 'POST',
headers: authHeaders(credentials),
body: form
})
}
const fetchTimeline = ({timeline, credentials, since = false, until = false}) => { const fetchTimeline = ({timeline, credentials, since = false, until = false}) => {
const timelineUrls = { const timelineUrls = {
public: PUBLIC_TIMELINE_URL, public: PUBLIC_TIMELINE_URL,
@ -162,6 +179,14 @@ const uploadMedia = ({formData, credentials}) => {
.then((text) => (new DOMParser()).parseFromString(text, 'application/xml')) .then((text) => (new DOMParser()).parseFromString(text, 'application/xml'))
} }
const fetchMutes = ({credentials}) => {
const url = '/api/qvitter/mutes.json'
return fetch(url, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const apiService = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -177,7 +202,9 @@ const apiService = {
postStatus, postStatus,
deleteStatus, deleteStatus,
uploadMedia, uploadMedia,
fetchAllFollowing fetchAllFollowing,
setUserMute,
fetchMutes
} }
export default apiService export default apiService

View file

@ -34,6 +34,12 @@ const backendInteractorService = (credentials) => {
return timelineFetcherService.startFetching({timeline, store, credentials}) return timelineFetcherService.startFetching({timeline, store, credentials})
} }
const setUserMute = ({id, muted = true}) => {
return apiService.setUserMute({id, muted, credentials})
}
const fetchMutes = () => apiService.fetchMutes({credentials})
const backendInteractorServiceInstance = { const backendInteractorServiceInstance = {
fetchStatus, fetchStatus,
fetchConversation, fetchConversation,
@ -43,7 +49,9 @@ const backendInteractorService = (credentials) => {
unfollowUser, unfollowUser,
fetchAllFollowing, fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials, verifyCredentials: apiService.verifyCredentials,
startFetching startFetching,
setUserMute,
fetchMutes
} }
return backendInteractorServiceInstance return backendInteractorServiceInstance

2794
yarn.lock

File diff suppressed because it is too large Load diff