aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/index.js8
-rw-r--r--package.json1
-rw-r--r--src/App.scss20
-rw-r--r--src/components/attachment/attachment.vue12
-rw-r--r--src/components/chat/chat.js21
-rw-r--r--src/components/chat/chat.vue59
-rw-r--r--src/components/delete_button/delete_button.vue2
-rw-r--r--src/components/favorite_button/favorite_button.vue4
-rw-r--r--src/components/media_upload/media_upload.vue4
-rw-r--r--src/components/nav_panel/nav_panel.js3
-rw-r--r--src/components/nav_panel/nav_panel.vue5
-rw-r--r--src/components/notifications/notifications.scss4
-rw-r--r--src/components/notifications/notifications.vue4
-rw-r--r--src/components/post_status_form/post_status_form.js8
-rw-r--r--src/components/post_status_form/post_status_form.vue5
-rw-r--r--src/components/retweet_button/retweet_button.vue4
-rw-r--r--src/components/status/status.vue16
-rw-r--r--src/components/style_switcher/style_switcher.js9
-rw-r--r--src/components/timeline/timeline.js17
-rw-r--r--src/components/timeline/timeline.vue2
-rw-r--r--src/components/user_card/user_card.vue11
-rw-r--r--src/components/user_card_content/user_card_content.js64
-rw-r--r--src/components/user_card_content/user_card_content.vue65
-rw-r--r--src/components/user_settings/user_settings.vue6
-rw-r--r--src/i18n/messages.js8
-rw-r--r--src/main.js103
-rw-r--r--src/modules/api.js22
-rw-r--r--src/modules/chat.js33
-rw-r--r--src/modules/statuses.js27
-rw-r--r--src/modules/users.js4
-rw-r--r--src/services/api/api.service.js14
-rw-r--r--src/services/color_convert/color_convert.js34
-rw-r--r--src/services/style_setter/style_setter.js33
-rw-r--r--src/services/timeline_fetcher/timeline_fetcher.service.js13
-rw-r--r--static/config.json4
-rw-r--r--yarn.lock4
36 files changed, 457 insertions, 196 deletions
diff --git a/config/index.js b/config/index.js
index 6652048c..c48d91b8 100644
--- a/config/index.js
+++ b/config/index.js
@@ -23,9 +23,15 @@ module.exports = {
assetsPublicPath: '/',
proxyTable: {
'/api': {
- target: 'https://social.heldscal.la/',
+ target: 'htts://localhost:4000/',
changeOrigin: true,
cookieDomainRewrite: 'localhost'
+ },
+ '/socket': {
+ target: 'htts://localhost:4000/',
+ changeOrigin: true,
+ cookieDomainRewrite: 'localhost',
+ ws: true
}
},
// CSS Sourcemaps off by default because relative paths are "buggy"
diff --git a/package.json b/package.json
index e8d84274..4e98647b 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"localforage": "^1.5.0",
"node-sass": "^3.10.1",
"object-path": "^0.11.3",
+ "phoenix": "^1.3.0",
"sanitize-html": "^1.13.0",
"sass-loader": "^4.0.2",
"vue": "^2.3.4",
diff --git a/src/App.scss b/src/App.scss
index 9aa3ee98..95a653ce 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -16,7 +16,13 @@ h4 {
}
#content {
- padding-top: 60px;
+ box-sizing: border-box;
+ padding-top: 60px;
+ margin: auto;
+ min-height: 100vh;
+ max-width: 980px;
+ background-color: rgba(0,0,0,0.15);
+ align-content: flex-start;
}
.text-center {
@@ -157,15 +163,6 @@ main-router {
margin: 0;
}
-
-#content {
- margin: auto;
- min-height: 100vh;
- max-width: 980px;
- padding-bottom: 1em;
- background-color: rgba(0,0,0,0.1);
-}
-
.container > * {
min-width: 0px;
}
@@ -210,10 +207,11 @@ nav {
.panel-switcher {
display: none;
width: 100%;
-
+ height: 46px;
button {
display: block;
flex: 1;
+ max-height: 32px;
margin: 0.5em;
padding: 0.5em;
}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 20d10cce..d6a51ffd 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -34,6 +34,13 @@
display: flex;
flex-wrap: wrap;
margin-right: -0.7em;
+
+ .attachment.media-upload-container {
+ flex: 0 0 auto;
+ max-height: 300px;
+ max-width: 100%;
+ }
+
.attachment {
flex: 1 0 30%;
margin: 0.5em 0.7em 0.6em 0.0em;
@@ -82,9 +89,7 @@
img.media-upload {
margin-bottom: -2px;
max-height: 300px;
- width: 100%;
- height: 100%;
- flex: 1;
+ max-width: 100%;
}
.oembed {
@@ -126,6 +131,7 @@
width: 100%;
height: 100%; /* If this isn't here, chrome will stretch the images */
max-height: 500px;
+ image-orientation: from-image;
}
}
}
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
new file mode 100644
index 00000000..ef326d4a
--- /dev/null
+++ b/src/components/chat/chat.js
@@ -0,0 +1,21 @@
+const chat = {
+ data () {
+ return {
+ currentMessage: '',
+ channel: null
+ }
+ },
+ computed: {
+ messages () {
+ return this.$store.state.chat.messages
+ }
+ },
+ methods: {
+ submit (message) {
+ this.$store.state.chat.channel.push('new_msg', {text: message}, 10000)
+ this.currentMessage = ''
+ }
+ }
+}
+
+export default chat
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
new file mode 100644
index 00000000..6c1e2c38
--- /dev/null
+++ b/src/components/chat/chat.vue
@@ -0,0 +1,59 @@
+<template>
+ <div class="chat-panel panel panel-default">
+ <div class="panel-heading timeline-heading base02-background base04">
+ <div class="title">
+ {{$t('chat.title')}}
+ </div>
+ </div>
+ <div class="panel-body base01-background">
+ <div class="chat-window">
+ <div class="chat-message" v-for="message in messages" :key="message.id">
+ <span class="chat-avatar">
+ <img :src="message.author.avatar" />
+ {{message.author.username}}:
+ </span>
+ <span class="chat-text">
+ {{message.text}}
+ </span>
+ </div>
+ </div>
+ <div class="chat-input">
+ <form @submit.prevent="submit(currentMessage)">
+ <input v-model="currentMessage" type="text" >
+ </form>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./chat.js"></script>
+
+
+<style lang="scss">
+ .chat-window {
+ max-height: 80vh;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+ .chat-message {
+ padding: 0.2em 0.5em
+ }
+ .chat-avatar {
+ img {
+ height: 32px;
+ width: 32px;
+ border-radius: 5px;
+ margin-right: 0.5em;
+ }
+ }
+ .chat-input {
+ display: flex;
+ form {
+ flex: auto;
+ input {
+ margin: 0.5em;
+ width: fill-available;
+ }
+ }
+ }
+</style>
diff --git a/src/components/delete_button/delete_button.vue b/src/components/delete_button/delete_button.vue
index 304f8a63..845ac777 100644
--- a/src/components/delete_button/delete_button.vue
+++ b/src/components/delete_button/delete_button.vue
@@ -1,7 +1,7 @@
<template>
<div v-if="canDelete">
<a href="#" v-on:click.prevent="deleteStatus()">
- <i class='fa icon-cancel delete-status'></i>
+ <i class='base09 icon-cancel delete-status'></i>
</a>
</div>
</template>
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index 0abece31..dcf28e35 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -1,6 +1,6 @@
<template>
<div>
- <i :class='classes' class='favorite-button fa' @click.prevent='favorite()'/>
+ <i :class='classes' class='favorite-button base09' @click.prevent='favorite()'/>
<span v-if='status.fave_num > 0'>{{status.fave_num}}</span>
</div>
</template>
@@ -15,7 +15,7 @@
color: orange;
}
}
- .icon-star {
+ .favorite-button.icon-star {
color: orange;
}
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index b839067b..9e6ad608 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,8 +1,8 @@
<template>
<div class="media-upload" @drop.prevent @dragover.prevent="fileDrag" @drop="fileDrop">
<label class="btn btn-default">
- <i class="fa icon-spin4 animate-spin" v-if="uploading"></i>
- <i class="fa icon-upload" v-if="!uploading"></i>
+ <i class="base09 icon-spin4 animate-spin" v-if="uploading"></i>
+ <i class="base09 icon-upload" v-if="!uploading"></i>
<input type=file style="position: fixed; top: -100em"></input>
</label>
</div>
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index baeaaede..ea5d7ea4 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -2,6 +2,9 @@ const NavPanel = {
computed: {
currentUser () {
return this.$store.state.users.currentUser
+ },
+ chat () {
+ return this.$store.state.chat.channel
}
}
}
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index aea841e9..ccc772a8 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -7,6 +7,11 @@
{{ $t("nav.timeline") }}
</router-link>
</li>
+ <li v-if='chat && currentUser'>
+ <router-link class="base00-background" to='/chat'>
+ {{ $t("nav.chat") }}
+ </router-link>
+ </li>
<li v-if='currentUser'>
<router-link class="base00-background" :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
{{ $t("nav.mentions") }}
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index db7b0843..241f10b4 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -59,6 +59,10 @@
color: $blue;
}
+ .icon-star.lit {
+ color: orange;
+ }
+
.status-content {
margin: 0;
max-height: 300px;
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 64624873..b341fcef 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -4,7 +4,7 @@
<div class="panel-heading base02-background base04">
<span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
{{$t('notifications.notifications')}}
- <button @click.prevent="markAsSeen" class="base04 base02-background read-button">{{$t('notifications.read')}}</button>
+ <button v-if="unseenCount" @click.prevent="markAsSeen" class="base04 base02-background read-button">{{$t('notifications.read')}}</button>
</div>
<div class="panel-body base03-border">
<div v-for="notification in visibleNotifications" :key="notification" class="notification" :class='{"unseen": !notification.seen}'>
@@ -17,7 +17,7 @@
<div v-if="notification.type === 'favorite'">
<h1>
<span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
- <i class="fa icon-star"></i>
+ <i class="fa icon-star 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 class="notification-gradient" :style="hiderStyle"></div>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 2eb091f4..8c0cd5ee 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -157,6 +157,14 @@ const PostStatusForm = {
type (fileInfo) {
return fileTypeService.fileType(fileInfo.mimetype)
},
+ paste (e) {
+ if (e.clipboardData.files.length > 0) {
+ // Strangely, files property gets emptied after event propagation
+ // Trying to wrap it in array doesn't work. Plus I doubt it's possible
+ // to hold more than one file in clipboard.
+ this.dropFiles = [e.clipboardData.files[0]]
+ }
+ },
fileDrop (e) {
if (e.dataTransfer.files.length > 0) {
e.preventDefault() // allow dropping text like before
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 8a2ec24d..8e436428 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -2,7 +2,7 @@
<div class="post-status-form">
<form @submit.prevent="postStatus(newStatus)">
<div class="form-group base03-border" >
- <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize"></textarea>
+ <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize" @paste="paste"></textarea>
</div>
<div style="position:relative;" v-if="candidates">
<div class="autocomplete-panel base05-background">
@@ -26,7 +26,7 @@
<i class="icon-cancel" @click="clearError"></i>
</div>
<div class="attachments">
- <div class="attachment base03-border" v-for="file in newStatus.files">
+ <div class="media-upload-container attachment base03-border" 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>
@@ -41,6 +41,7 @@
<script src="./post_status_form.js"></script>
<style lang="scss">
+
.tribute-container {
ul {
padding: 0px;
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index d923c5c4..edbfef32 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -1,6 +1,6 @@
<template>
<div>
- <i :class='classes' class='icon-retweet fa' v-on:click.prevent='retweet()'></i>
+ <i :class='classes' class='icon-retweet base09' v-on:click.prevent='retweet()'></i>
<span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
</div>
</template>
@@ -16,7 +16,7 @@
color: $green;
}
}
- .retweeted {
+ .icon-retweet.retweeted {
color: $green;
}
</style>
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 5e3df8ba..d6c8cdb3 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -5,7 +5,7 @@
<div class='status-actions'>
<div>
<a href="#" v-on:click.prevent="toggleReplying">
- <i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i>
+ <i class="base09 icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
<retweet-button :status=status></retweet-button>
@@ -19,7 +19,7 @@
<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>
<small class="muteWords">{{muteWordHits.join(', ')}}</small>
- <a href="#" class="unmute" @click.prevent="toggleMute"><i class="fa icon-eye-off"></i></a>
+ <a href="#" class="unmute" @click.prevent="toggleMute"><i class="base09 icon-eye-off"></i></a>
</div>
</template>
<template v-if="!muted">
@@ -75,10 +75,10 @@
</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>
+ <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="base09 icon-eye-off"></i></a>
+ <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="base09 icon-binoculars"></i></a>
<template v-if="expandable">
- <a href="#" @click.prevent="toggleExpanded" class="expand"><i class="fa icon-plus-squared"></i></a>
+ <a href="#" @click.prevent="toggleExpanded" class="expand"><i class="base09 icon-plus-squared"></i></a>
</template>
</div>
</div>
@@ -94,7 +94,7 @@
</div>
</div>
<div class="status-preview status-preview-loading base00-background base03-border" v-else-if="showPreview">
- <i class="fa icon-spin4 animate-spin"></i>
+ <i class="base09 icon-spin4 animate-spin"></i>
</div>
<div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
@@ -109,7 +109,7 @@
<div class='status-actions'>
<div>
<a href="#" v-on:click.prevent="toggleReplying">
- <i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i>
+ <i class="base09 icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
<retweet-button :status=status></retweet-button>
@@ -324,7 +324,7 @@
color: $blue;
}
- .icon-reply-active {
+ .icon-reply.icon-reply-active {
color: $blue;
}
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index b1359d13..a762f914 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,3 +1,5 @@
+import { rgbstr2hex } from '../../services/color_convert/color_convert.js'
+
export default {
data () {
return {
@@ -19,13 +21,6 @@ export default {
})
},
mounted () {
- const rgbstr2hex = (rgb) => {
- if (rgb[0] === '#') {
- return rgb
- }
- rgb = rgb.match(/\d+/g)
- return `#${((Number(rgb[0]) << 16) + (Number(rgb[1]) << 8) + Number(rgb[2])).toString(16)}`
- }
this.bgColorLocal = rgbstr2hex(this.$store.state.config.colors['base00'])
this.fgColorLocal = rgbstr2hex(this.$store.state.config.colors['base02'])
this.textColorLocal = rgbstr2hex(this.$store.state.config.colors['base05'])
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index be0aefc1..660a8752 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -29,6 +29,13 @@ const Timeline = {
},
newStatusCount () {
return this.timeline.newStatusCount
+ },
+ newStatusCountStr () {
+ if (this.timeline.flushMarker !== 0) {
+ return ''
+ } else {
+ return ` (${this.newStatusCount})`
+ }
}
},
components: {
@@ -64,8 +71,14 @@ const Timeline = {
},
methods: {
showNewStatuses () {
- this.$store.commit('showNewStatuses', { timeline: this.timelineName })
- this.paused = false
+ if (this.timeline.flushMarker !== 0) {
+ this.$store.commit('clearTimeline', { timeline: this.timelineName })
+ this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
+ this.fetchOlderStatuses()
+ } else {
+ this.$store.commit('showNewStatuses', { timeline: this.timelineName })
+ this.paused = false
+ }
},
fetchOlderStatuses () {
const store = this.$store
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 0e2ed92c..9d2e1ea1 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -5,7 +5,7 @@
{{title}}
</div>
<button @click.prevent="showNewStatuses" class="base05 base02-background loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
- {{$t('timeline.show_new')}} ({{timeline.newStatusCount}})
+ {{$t('timeline.show_new')}}{{newStatusCountStr}}
</button>
<div @click.prevent class="base06 error loadmore-text" v-if="timelineError">
{{$t('timeline.error_fetching')}}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index ba315faa..dd14d1b4 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -59,13 +59,16 @@
}
.usercard {
- width: -webkit-fill-available;
- width: -moz-webkit-fill-available;
- stretch: fill;
+ width: fill-available;
margin: 0.2em 0 0.7em 0;
- border-radius: 5px;
+ border-radius: 10px;
border-style: solid;
border-color: inherit;
border-width: 1px;
+ overflow: hidden;
+
+ p {
+ margin-bottom: 0;
+ }
}
</style>
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
new file mode 100644
index 00000000..6e67a321
--- /dev/null
+++ b/src/components/user_card_content/user_card_content.js
@@ -0,0 +1,64 @@
+import { hex2rgb } from '../../services/color_convert/color_convert.js'
+
+export default {
+ props: [ 'user', 'switcher' ],
+ computed: {
+ headingStyle () {
+ const color = this.$store.state.config.colors['base00']
+ if (color) {
+ const rgb = hex2rgb(color)
+ console.log(rgb)
+ 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 () {
+ return {
+ background: `linear-gradient(to bottom, rgba(0, 0, 0, 0), ${this.$store.state.config.colors['base00']} 80%)`
+ }
+ },
+ isOtherUser () {
+ return this.user.id !== this.$store.state.users.currentUser.id
+ },
+ loggedIn () {
+ return this.$store.state.users.currentUser
+ },
+ dailyAvg () {
+ 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: {
+ followUser () {
+ const store = this.$store
+ store.state.api.backendInteractor.followUser(this.user.id)
+ .then((followedUser) => store.commit('addNewUsers', [followedUser]))
+ },
+ unfollowUser () {
+ const store = this.$store
+ store.state.api.backendInteractor.unfollowUser(this.user.id)
+ .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
+ },
+ blockUser () {
+ const store = this.$store
+ store.state.api.backendInteractor.blockUser(this.user.id)
+ .then((blockedUser) => store.commit('addNewUsers', [blockedUser]))
+ },
+ unblockUser () {
+ const store = this.$store
+ store.state.api.backendInteractor.unblockUser(this.user.id)
+ .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser]))
+ },
+ toggleMute () {
+ const store = this.$store
+ store.commit('setMuted', {user: this.user, muted: !this.user.muted})
+ store.state.api.backendInteractor.setUserMute(this.user)
+ },
+ setProfileView (v) {
+ const store = this.$store
+ store.commit('setProfileView', { v })
+ }
+ }
+}
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 5635a177..4c40c55f 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -84,69 +84,7 @@
</div>
</template>
-<script>
- export default {
- props: [ 'user', 'switcher' ],
- computed: {
- headingStyle () {
- 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 () {
- return {
- background: `linear-gradient(to bottom, rgba(0, 0, 0, 0), ${this.$store.state.config.colors['base00']} 80%)`
- }
- },
- isOtherUser () {
- return this.user.id !== this.$store.state.users.currentUser.id
- },
- loggedIn () {
- return this.$store.state.users.currentUser
- },
- dailyAvg () {
- 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: {
- followUser () {
- const store = this.$store
- store.state.api.backendInteractor.followUser(this.user.id)
- .then((followedUser) => store.commit('addNewUsers', [followedUser]))
- },
- unfollowUser () {
- const store = this.$store
- store.state.api.backendInteractor.unfollowUser(this.user.id)
- .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
- },
- blockUser () {
- const store = this.$store
- store.state.api.backendInteractor.blockUser(this.user.id)
- .then((blockedUser) => store.commit('addNewUsers', [blockedUser]))
- },
- unblockUser () {
- const store = this.$store
- store.state.api.backendInteractor.unblockUser(this.user.id)
- .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser]))
- },
- toggleMute () {
- const store = this.$store
- store.commit('setMuted', {user: this.user, muted: !this.user.muted})
- store.state.api.backendInteractor.setUserMute(this.user)
- },
- setProfileView (v) {
- const store = this.$store
- store.commit('setProfileView', { v })
- }
- }
- }
-</script>
+<script src="./user_card_content.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@@ -164,7 +102,6 @@
.profile-panel-body {
top: -0em;
padding-top: 4em;
-
word-wrap: break-word;
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 74b0ff2a..515fd253 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -22,7 +22,7 @@
<div>
<input type="file" @change="uploadFile(0, $event)" ></input>
</div>
- <i class="fa icon-spin4 animate-spin" v-if="uploading[0]"></i>
+ <i class="base09 icon-spin4 animate-spin" v-if="uploading[0]"></i>
<button class="btn btn-default base05 base02-background" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
@@ -35,7 +35,7 @@
<div>
<input type="file" @change="uploadFile(1, $event)" ></input>
</div>
- <i class="fa icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
+ <i class="base09 icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
<button class="btn btn-default base05 base02-background" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
@@ -46,7 +46,7 @@
<div>
<input type="file" @change="uploadFile(2, $event)" ></input>
</div>
- <i class="fa icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
+ <i class="base09 icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
<button class="btn btn-default base05 base02-background" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
</div>
</div>
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index 9aeffdfa..4c5be151 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -1,5 +1,9 @@
const de = {
+ chat: {
+ title: 'Chat'
+ },
nav: {
+ chat: 'Lokaler Chat',
timeline: 'Zeitleiste',
mentions: 'Erwähnungen',
public_tl: 'Lokale Zeitleiste',
@@ -179,7 +183,11 @@ const fi = {
}
const en = {
+ chat: {
+ title: 'Chat'
+ },
nav: {
+ chat: 'Local Chat',
timeline: 'Timeline',
mentions: 'Mentions',
public_tl: 'Public Timeline',
diff --git a/src/main.js b/src/main.js
index 14cb27eb..51e0f7eb 100644
--- a/src/main.js
+++ b/src/main.js
@@ -12,11 +12,13 @@ import UserProfile from './components/user_profile/user_profile.vue'
import Settings from './components/settings/settings.vue'
import Registration from './components/registration/registration.vue'
import UserSettings from './components/user_settings/user_settings.vue'
+import Chat from './components/chat/chat.vue'
import statusesModule from './modules/statuses.js'
import usersModule from './modules/users.js'
import apiModule from './modules/api.js'
import configModule from './modules/config.js'
+import chatModule from './modules/chat.js'
import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n'
@@ -57,35 +59,12 @@ const store = new Vuex.Store({
statuses: statusesModule,
users: usersModule,
api: apiModule,
- config: configModule
+ config: configModule,
+ chat: chatModule
},
plugins: [createPersistedState(persistedStateOptions)],
- strict: process.env.NODE_ENV !== 'production'
-})
-
-const routes = [
- { name: 'root', path: '/', redirect: '/main/all' },
- { path: '/main/all', component: PublicAndExternalTimeline },
- { path: '/main/public', component: PublicTimeline },
- { path: '/main/friends', component: FriendsTimeline },
- { path: '/tag/:tag', component: TagTimeline },
- { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
- { name: 'user-profile', path: '/users/:id', component: UserProfile },
- { name: 'mentions', path: '/:username/mentions', component: Mentions },
- { name: 'settings', path: '/settings', component: Settings },
- { name: 'registration', path: '/registration', component: Registration },
- { name: 'user-settings', path: '/user-settings', component: UserSettings }
-]
-
-const router = new VueRouter({
- mode: 'history',
- routes,
- scrollBehavior: (to, from, savedPosition) => {
- if (to.matched.some(m => m.meta.dontScroll)) {
- return false
- }
- return savedPosition || { x: 0, y: 0 }
- }
+ strict: false // Socket modifies itself, let's ignore this for now.
+ // strict: process.env.NODE_ENV !== 'production'
})
const i18n = new VueI18n({
@@ -94,23 +73,53 @@ const i18n = new VueI18n({
messages
})
-/* eslint-disable no-new */
-new Vue({
- router,
- store,
- i18n,
- el: '#app',
- render: h => h(App)
-})
-
window.fetch('/static/config.json')
.then((res) => res.json())
- .then(({name, theme, background, logo, registrationOpen}) => {
+ .then((data) => {
+ const {name, theme, background, logo, registrationOpen} = data
store.dispatch('setOption', { name: 'name', value: name })
store.dispatch('setOption', { name: 'theme', value: theme })
store.dispatch('setOption', { name: 'background', value: background })
store.dispatch('setOption', { name: 'logo', value: logo })
store.dispatch('setOption', { name: 'registrationOpen', value: registrationOpen })
+ if (data['chatDisabled']) {
+ store.dispatch('disableChat')
+ }
+
+ const routes = [
+ { name: 'root', path: '/', redirect: data['defaultPath'] || '/main/all' },
+ { path: '/main/all', component: PublicAndExternalTimeline },
+ { path: '/main/public', component: PublicTimeline },
+ { path: '/main/friends', component: FriendsTimeline },
+ { path: '/tag/:tag', component: TagTimeline },
+ { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
+ { name: 'user-profile', path: '/users/:id', component: UserProfile },
+ { name: 'mentions', path: '/:username/mentions', component: Mentions },
+ { name: 'settings', path: '/settings', component: Settings },
+ { name: 'registration', path: '/registration', component: Registration },
+ { name: 'user-settings', path: '/user-settings', component: UserSettings },
+ { name: 'chat', path: '/chat', component: Chat }
+ ]
+
+ const router = new VueRouter({
+ mode: 'history',
+ routes,
+ scrollBehavior: (to, from, savedPosition) => {
+ if (to.matched.some(m => m.meta.dontScroll)) {
+ return false
+ }
+ return savedPosition || { x: 0, y: 0 }
+ }
+ })
+
+ /* eslint-disable no-new */
+ new Vue({
+ router,
+ store,
+ i18n,
+ el: '#app',
+ render: h => h(App)
+ })
})
window.fetch('/static/terms-of-service.html')
@@ -120,13 +129,19 @@ window.fetch('/static/terms-of-service.html')
})
window.fetch('/api/pleroma/emoji.json')
- .then((res) => res.json())
- .then((values) => {
- const emoji = Object.keys(values).map((key) => {
- return { shortcode: key, image_url: values[key] }
- })
- store.dispatch('setOption', { name: 'customEmoji', value: emoji })
- })
+ .then(
+ (res) => res.json()
+ .then(
+ (values) => {
+ const emoji = Object.keys(values).map((key) => {
+ return { shortcode: key, image_url: values[key] }
+ })
+ store.dispatch('setOption', { name: 'emoji', value: emoji })
+ },
+ (failure) => {}
+ ),
+ (error) => console.log(error)
+ )
window.fetch('/static/emoji.json')
.then((res) => res.json())
diff --git a/src/modules/api.js b/src/modules/api.js
index e61382eb..c91fb97b 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -1,10 +1,13 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import {isArray} from 'lodash'
+import { Socket } from 'phoenix'
const api = {
state: {
backendInteractor: backendInteractorService(),
- fetchers: {}
+ fetchers: {},
+ socket: null,
+ chatDisabled: false
},
mutations: {
setBackendInteractor (state, backendInteractor) {
@@ -15,6 +18,12 @@ const api = {
},
removeFetcher (state, {timeline}) {
delete state.fetchers[timeline]
+ },
+ setSocket (state, socket) {
+ state.socket = socket
+ },
+ setChatDisabled (state, value) {
+ state.chatDisabled = value
}
},
actions: {
@@ -37,6 +46,17 @@ const api = {
const fetcher = store.state.fetchers[timeline]
window.clearInterval(fetcher)
store.commit('removeFetcher', {timeline})
+ },
+ initializeSocket (store, token) {
+ // Set up websocket connection
+ if (!store.state.chatDisabled) {
+ let socket = new Socket('/socket', {params: {token: token}})
+ socket.connect()
+ store.dispatch('initializeChat', socket)
+ }
+ },
+ disableChat (store) {
+ store.commit('setChatDisabled', true)
}
}
}
diff --git a/src/modules/chat.js b/src/modules/chat.js
new file mode 100644
index 00000000..b1244ebe
--- /dev/null
+++ b/src/modules/chat.js
@@ -0,0 +1,33 @@
+const chat = {
+ state: {
+ messages: [],
+ channel: null
+ },
+ mutations: {
+ setChannel (state, channel) {
+ state.channel = channel
+ },
+ addMessage (state, message) {
+ state.messages.push(message)
+ state.messages = state.messages.slice(-19, 20)
+ },
+ setMessages (state, messages) {
+ state.messages = messages.slice(-19, 20)
+ }
+ },
+ actions: {
+ initializeChat (store, socket) {
+ const channel = socket.channel('chat:public')
+ channel.on('new_msg', (msg) => {
+ store.commit('addMessage', msg)
+ })
+ channel.on('messages', ({messages}) => {
+ store.commit('setMessages', messages)
+ })
+ channel.join()
+ store.commit('setChannel', channel)
+ }
+ }
+}
+
+export default chat
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index d954b023..18191424 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -22,7 +22,8 @@ export const defaultState = {
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
public: {
statuses: [],
@@ -36,7 +37,8 @@ export const defaultState = {
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
user: {
statuses: [],
@@ -50,7 +52,8 @@ export const defaultState = {
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
publicAndExternal: {
statuses: [],
@@ -64,7 +67,8 @@ export const defaultState = {
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
friends: {
statuses: [],
@@ -78,7 +82,8 @@ export const defaultState = {
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
tag: {
statuses: [],
@@ -92,7 +97,8 @@ export const defaultState = {
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
}
}
}
@@ -381,7 +387,8 @@ export const mutations = {
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
}
state.timelines[timeline] = emptyTimeline
@@ -422,6 +429,9 @@ export const mutations = {
each(notifications, (notification) => {
notification.seen = true
})
+ },
+ queueFlush (state, { timeline, id }) {
+ state.timelines[timeline].flushMarker = id
}
}
@@ -458,6 +468,9 @@ const statuses = {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ },
+ queueFlush ({ rootState, commit }, { timeline, id }) {
+ commit('queueFlush', { timeline, id })
}
},
mutations
diff --git a/src/modules/users.js b/src/modules/users.js
index 30f8dc27..8303ecc1 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -97,6 +97,10 @@ const users = {
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(userCredentials))
+ if (user.token) {
+ store.dispatch('initializeSocket', user.token)
+ }
+
// Start getting fresh tweets.
store.dispatch('startFetching', 'friends')
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 5de0a457..5b078bc8 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -43,6 +43,16 @@ let fetch = (url, options) => {
return oldfetch(fullUrl, options)
}
+// from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
+let utoa = (str) => {
+ // first we use encodeURIComponent to get percent-encoded UTF-8,
+ // then we convert the percent encodings into raw bytes which
+ // can be fed into btoa.
+ return btoa(encodeURIComponent(str)
+ .replace(/%([0-9A-F]{2})/g,
+ (match, p1) => { return String.fromCharCode('0x' + p1) }))
+}
+
// Params
// cropH
// cropW
@@ -156,7 +166,7 @@ const register = (params) => {
const authHeaders = (user) => {
if (user && user.username && user.password) {
- return { 'Authorization': `Basic ${btoa(`${user.username}:${user.password}`)}` }
+ return { 'Authorization': `Basic ${utoa(`${user.username}:${user.password}`)}` }
} else {
return { }
}
@@ -281,6 +291,8 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
url += `/${tag}.json`
}
+ params.push(['count', 20])
+
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js
new file mode 100644
index 00000000..13dd8979
--- /dev/null
+++ b/src/services/color_convert/color_convert.js
@@ -0,0 +1,34 @@
+import { map } from 'lodash'
+
+const rgb2hex = (r, g, b) => {
+ [r, g, b] = map([r, g, b], (val) => {
+ val = Math.ceil(val)
+ val = val < 0 ? 0 : val
+ val = val > 255 ? 255 : val
+ return val
+ })
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
+}
+
+const hex2rgb = (hex) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null
+}
+
+const rgbstr2hex = (rgb) => {
+ if (rgb[0] === '#') {
+ return rgb
+ }
+ rgb = rgb.match(/\d+/g)
+ return `#${((Number(rgb[0]) << 16) + (Number(rgb[1]) << 8) + Number(rgb[2])).toString(16)}`
+}
+
+export {
+ rgb2hex,
+ hex2rgb,
+ rgbstr2hex
+}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 62296e79..6863bd0a 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,4 +1,5 @@
-import { times, map } from 'lodash'
+import { times } from 'lodash'
+import { rgb2hex, hex2rgb } from '../color_convert/color_convert.js'
// While this is not used anymore right now, I left it in if we want to do custom
// styles that aren't just colors, so user can pick from a few different distinct
@@ -56,16 +57,6 @@ const setStyle = (href, commit) => {
cssEl.addEventListener('load', setDynamic)
}
-const rgb2hex = (r, g, b) => {
- [r, g, b] = map([r, g, b], (val) => {
- val = Math.ceil(val)
- val = val < 0 ? 0 : val
- val = val > 255 ? 255 : val
- return val
- })
- return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
-}
-
const setColors = (col, commit) => {
const head = document.head
const body = document.body
@@ -82,6 +73,7 @@ const setColors = (col, commit) => {
if (isDark) {
mod = mod * -1
}
+
colors['base00'] = rgb2hex(col.bg.r, col.bg.g, col.bg.b) // background
colors['base01'] = rgb2hex((col.bg.r + col.fg.r) / 2, (col.bg.g + col.fg.g) / 2, (col.bg.b + col.fg.b) / 2) // hilighted bg
colors['base02'] = rgb2hex(col.fg.r, col.fg.g, col.fg.b) // panels & buttons
@@ -91,11 +83,13 @@ const setColors = (col, commit) => {
colors['base06'] = rgb2hex(col.text.r - mod, col.text.g - mod, col.text.b - mod) // strong text
colors['base07'] = rgb2hex(col.text.r - mod * 2, col.text.g - mod * 2, col.text.b - mod * 2)
colors['base08'] = rgb2hex(col.link.r, col.link.g, col.link.b) // links
+ colors['base09'] = rgb2hex((col.bg.r + col.text.r) / 2, (col.bg.g + col.text.g) / 2, (col.bg.b + col.text.b) / 2) // icons
- times(9, (n) => {
- const color = colors[`base0${8 - n}`]
- styleSheet.insertRule(`.base0${8 - n} { color: ${color}`, 'index-max')
- styleSheet.insertRule(`.base0${8 - n}-background { background-color: ${color}`, 'index-max')
+ const num = 10
+ times(num, (n) => {
+ const color = colors[`base0${num - 1 - n}`]
+ styleSheet.insertRule(`.base0${num - 1 - n} { color: ${color}`, 'index-max')
+ styleSheet.insertRule(`.base0${num - 1 - n}-background { background-color: ${color}`, 'index-max')
})
styleSheet.insertRule(`a { color: ${colors['base08']}`, 'index-max')
@@ -108,15 +102,6 @@ const setColors = (col, commit) => {
commit('setOption', { name: 'customTheme', value: col })
}
-const hex2rgb = (hex) => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
- return result ? {
- r: parseInt(result[1], 16),
- g: parseInt(result[2], 16),
- b: parseInt(result[3], 16)
- } : null
-}
-
const setPreset = (val, commit) => {
window.fetch('/static/styles.json')
.then((data) => data.json())
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 6b76eb54..a4a80df0 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -29,12 +29,19 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
args['tag'] = tag
return apiService.fetchTimeline(args)
- .then((statuses) => update({store, statuses, timeline, showImmediately}),
- () => store.dispatch('setError', { value: true }))
+ .then((statuses) => {
+ if (!older && statuses.length >= 20) {
+ store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
+ }
+ update({store, statuses, timeline, showImmediately})
+ }, () => store.dispatch('setError', { value: true }))
}
const startFetching = ({timeline = 'friends', credentials, store, userId = false, tag = false}) => {
- fetchAndUpdate({timeline, credentials, store, showImmediately: true, userId, tag})
+ const rootState = store.rootState || store.state
+ const timelineData = rootState.statuses.timelines[camelCase(timeline)]
+ const showImmediately = timelineData.visibleStatuses.length === 0
+ fetchAndUpdate({timeline, credentials, store, showImmediately, userId, tag})
const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag })
return setInterval(boundFetchAndUpdate, 10000)
}
diff --git a/static/config.json b/static/config.json
index 8b596992..880efca8 100644
--- a/static/config.json
+++ b/static/config.json
@@ -3,5 +3,7 @@
"theme": "pleroma-dark",
"background": "/static/bg.jpg",
"logo": "/static/logo.png",
- "registrationOpen": false
+ "registrationOpen": false,
+ "defaultPath": "/main/all",
+ "chatDisabled": false
}
diff --git a/yarn.lock b/yarn.lock
index d0d2dde9..3fcd29ab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4226,6 +4226,10 @@ phantomjs-prebuilt@^2.1.3, phantomjs-prebuilt@^2.1.7:
request-progress "~2.0.1"
which "~1.2.10"
+phoenix@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.3.0.tgz#1df2c27f986ee295e37c9983ec28ebac1d7f4a3e"
+
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"