forked from AkkomaGang/akkoma-fe
Merge branch 'develop' into feature/hash-routed
This commit is contained in:
commit
5d8b2eb8b5
29 changed files with 1706 additions and 1531 deletions
2
.babelrc
2
.babelrc
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"presets": ["es2015", "stage-2"],
|
"presets": ["es2015", "stage-2"],
|
||||||
"plugins": ["transform-runtime"],
|
"plugins": ["transform-runtime", "lodash"],
|
||||||
"comments": false
|
"comments": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,9 @@ 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:
|
||||||
|
|
49
COFE_OF_CONDUCT.md
Normal file
49
COFE_OF_CONDUCT.md
Normal 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$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
|
||||||
|
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
|
||||||
|
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""
|
||||||
|
"""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||||
|
```
|
|
@ -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
|
|
@ -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
|
||||||
|
|
10
package.json
10
package.json
|
@ -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",
|
||||||
|
|
|
@ -24,6 +24,9 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
activatePanel (panelName) {
|
activatePanel (panelName) {
|
||||||
this.mobileActivePanel = panelName
|
this.mobileActivePanel = panelName
|
||||||
|
},
|
||||||
|
scrollToTop () {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
56
src/App.scss
56
src/App.scss
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.setting-item {
|
.setting-item {
|
||||||
margin: 1em
|
margin: 1em 1em 1.4em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
76
src/lib/persisted_state.js
Normal file
76
src/lib/persisted_state.js
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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({
|
||||||
|
|
|
@ -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}`
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue