Update branch and fix conflicts.
This commit is contained in:
commit
3785a863cb
47 changed files with 889 additions and 285 deletions
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
10
package.json
10
package.json
|
@ -8,6 +8,7 @@
|
||||||
"dev": "node build/dev-server.js",
|
"dev": "node build/dev-server.js",
|
||||||
"build": "node build/build.js",
|
"build": "node build/build.js",
|
||||||
"unit": "karma start test/unit/karma.conf.js --single-run",
|
"unit": "karma start test/unit/karma.conf.js --single-run",
|
||||||
|
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||||
"e2e": "node test/e2e/runner.js",
|
"e2e": "node test/e2e/runner.js",
|
||||||
"test": "npm run unit && npm run e2e",
|
"test": "npm run unit && npm run e2e",
|
||||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
|
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||||
|
@ -22,12 +23,11 @@
|
||||||
"object-path": "^0.11.3",
|
"object-path": "^0.11.3",
|
||||||
"sanitize-html": "^1.13.0",
|
"sanitize-html": "^1.13.0",
|
||||||
"sass-loader": "^4.0.2",
|
"sass-loader": "^4.0.2",
|
||||||
"tributejs": "^2.1.0",
|
"vue": "^2.3.4",
|
||||||
"vue": "^2.1.0",
|
"vue-router": "^2.5.3",
|
||||||
"vue-router": "^2.2.0",
|
"vue-template-compiler": "^2.3.4",
|
||||||
"vue-template-compiler": "^2.1.10",
|
|
||||||
"vue-timeago": "^3.1.2",
|
"vue-timeago": "^3.1.2",
|
||||||
"vuex": "^2.1.0"
|
"vuex": "^2.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^6.4.0",
|
"autoprefixer": "^6.4.0",
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import UserPanel from './components/user_panel/user_panel.vue'
|
import UserPanel from './components/user_panel/user_panel.vue'
|
||||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||||
import Notifications from './components/notifications/notifications.vue'
|
import Notifications from './components/notifications/notifications.vue'
|
||||||
|
import UserFinder from './components/user_finder/user_finder.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: {
|
components: {
|
||||||
UserPanel,
|
UserPanel,
|
||||||
NavPanel,
|
NavPanel,
|
||||||
Notifications
|
Notifications,
|
||||||
|
UserFinder
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
mobileActivePanel: 'timeline'
|
mobileActivePanel: 'timeline'
|
||||||
|
|
61
src/App.scss
61
src/App.scss
|
@ -52,6 +52,8 @@ button{
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
line-height: 21px;
|
||||||
|
height: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gaps > .item {
|
.gaps > .item {
|
||||||
|
@ -134,11 +136,6 @@ main-router {
|
||||||
background-color: rgba(0,0,0,0.1);
|
background-color: rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-body {
|
|
||||||
flex: 1;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container > * {
|
.container > * {
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
}
|
}
|
||||||
|
@ -147,60 +144,6 @@ main-router {
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-actions {
|
|
||||||
width: 50%;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
div, favorite-button {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status-text-container {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-el {
|
|
||||||
line-height: 18px;
|
|
||||||
|
|
||||||
.notify {
|
|
||||||
.avatar {
|
|
||||||
border-width: 3px;
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-left {
|
|
||||||
img {
|
|
||||||
margin-top: 0.2em;
|
|
||||||
float: right;
|
|
||||||
margin-right: 0.3em;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.retweet-info {
|
|
||||||
padding: 0.7em 0 0 0.6em;
|
|
||||||
|
|
||||||
.media-left {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
i {
|
|
||||||
align-self: center;
|
|
||||||
text-align: right;
|
|
||||||
flex: 1;
|
|
||||||
padding-right: 0.3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-heading {
|
|
||||||
small {
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
margin-bottom: 0.3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nav {
|
nav {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<router-link :to="{ name: 'root'}">{{sitename}}</router-link>
|
<router-link :to="{ name: 'root'}">{{sitename}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class='item right'>
|
<div class='item right'>
|
||||||
|
<user-finder></user-finder>
|
||||||
<router-link :to="{ name: 'settings'}"><i class="icon-cog"></i></router-link>
|
<router-link :to="{ name: 'settings'}"><i class="icon-cog"></i></router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<img class="base03-border" referrerpolicy="no-referrer" :src="attachment.large_thumb_url || attachment.url"/>
|
<img class="base03-border" referrerpolicy="no-referrer" :src="attachment.large_thumb_url || attachment.url"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<video v-if="type === 'video' && !hidden" :src="attachment.url" controls></video>
|
<video v-if="type === 'video' && !hidden" :src="attachment.url" controls loop></video>
|
||||||
|
|
||||||
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
|
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { filter, sortBy } from 'lodash'
|
import { find, filter, sortBy } from 'lodash'
|
||||||
import { statusType } from '../../modules/statuses.js'
|
import { statusType } from '../../modules/statuses.js'
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
|
|
||||||
|
@ -10,7 +10,12 @@ const sortAndFilterConversation = (conversation) => {
|
||||||
const conversation = {
|
const conversation = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
highlight: null
|
highlight: null,
|
||||||
|
preview: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
status: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
|
@ -27,7 +32,6 @@ const conversation = {
|
||||||
const conversationId = this.status.statusnet_conversation_id
|
const conversationId = this.status.statusnet_conversation_id
|
||||||
const statuses = this.$store.state.statuses.allStatuses
|
const statuses = this.$store.state.statuses.allStatuses
|
||||||
const conversation = filter(statuses, { statusnet_conversation_id: conversationId })
|
const conversation = filter(statuses, { statusnet_conversation_id: conversationId })
|
||||||
|
|
||||||
return sortAndFilterConversation(conversation)
|
return sortAndFilterConversation(conversation)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -46,6 +50,7 @@ const conversation = {
|
||||||
const conversationId = this.status.statusnet_conversation_id
|
const conversationId = this.status.statusnet_conversation_id
|
||||||
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
|
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
|
||||||
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
|
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
|
||||||
|
.then(() => this.setHighlight(this.statusoid.id))
|
||||||
} else {
|
} else {
|
||||||
const id = this.$route.params.id
|
const id = this.$route.params.id
|
||||||
this.$store.state.api.backendInteractor.fetchStatus({id})
|
this.$store.state.api.backendInteractor.fetchStatus({id})
|
||||||
|
@ -53,7 +58,21 @@ const conversation = {
|
||||||
.then(() => this.fetchConversation())
|
.then(() => this.fetchConversation())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
focused: function (id) {
|
getReplies (id) {
|
||||||
|
let res = []
|
||||||
|
id = Number(id)
|
||||||
|
let i
|
||||||
|
for (i = 0; i < this.conversation.length; i++) {
|
||||||
|
if (Number(this.conversation[i].in_reply_to_status_id) === id) {
|
||||||
|
res.push({
|
||||||
|
name: `#${i}`,
|
||||||
|
id: this.conversation[i].id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
focused (id) {
|
||||||
if (this.statusoid.retweeted_status) {
|
if (this.statusoid.retweeted_status) {
|
||||||
return (id === this.statusoid.retweeted_status.id)
|
return (id === this.statusoid.retweeted_status.id)
|
||||||
} else {
|
} else {
|
||||||
|
@ -62,6 +81,15 @@ const conversation = {
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
this.highlight = Number(id)
|
this.highlight = Number(id)
|
||||||
|
},
|
||||||
|
setPreview (id, x, y) {
|
||||||
|
if (id) {
|
||||||
|
this.preview.x = x
|
||||||
|
this.preview.y = y
|
||||||
|
this.preview.status = find(this.conversation, { id: id })
|
||||||
|
} else {
|
||||||
|
this.preview.status = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
<status v-for="status in conversation" @goto="setHighlight" :key="status.id" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true' :highlight="highlight"></status>
|
<status v-for="status in conversation" @goto="setHighlight" :key="status.id" @preview="setPreview" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true' :highlight="highlight" :replies="getReplies(status.id)"></status>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-preview base00-background base03-border" :style="{ left: preview.x + 'px', top: preview.y + 'px'}" v-if="preview.status">
|
||||||
|
<img class="avatar" :src="preview.status.user.profile_image_url_original">
|
||||||
|
<div class="text">
|
||||||
|
<h4>
|
||||||
|
{{ preview.status.user.name }}
|
||||||
|
<small><a>{{ preview.status.user.screen_name}}</a></small>
|
||||||
|
</h4>
|
||||||
|
<div @click.prevent="linkClicked" class="status-content" v-html="preview.status.statusnet_html"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,4 +31,30 @@
|
||||||
border-bottom-style: solid;
|
border-bottom-style: solid;
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-preview {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 35em;
|
||||||
|
padding: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
||||||
|
.avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
small {
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
padding: 0 0.5em 0.5em 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import Status from '../status/status.vue'
|
||||||
|
|
||||||
import { sortBy, take, filter } from 'lodash'
|
import { sortBy, take, filter } from 'lodash'
|
||||||
|
|
||||||
const Notifications = {
|
const Notifications = {
|
||||||
|
@ -23,6 +25,9 @@ const Notifications = {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
Status
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
unseenCount (count) {
|
unseenCount (count) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
|
|
|
@ -45,19 +45,23 @@
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
line-height:18px;
|
line-height:18px;
|
||||||
|
|
||||||
.icon-retweet {
|
.icon-retweet.lit {
|
||||||
color: $green;
|
color: $green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-reply {
|
.icon-reply.lit {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
word-break: break-all;
|
||||||
margin: 0 0 0.3em;
|
margin: 0 0 0.3em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
line-height:20px;
|
line-height:20px;
|
||||||
|
small {
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
padding: 0.3em 0.8em 0.5em;
|
padding: 0.3em 0.8em 0.5em;
|
||||||
|
|
|
@ -7,23 +7,34 @@
|
||||||
<button @click.prevent="markAsSeen" class="base06 base02-background read-button">Read!</button>
|
<button @click.prevent="markAsSeen" class="base06 base02-background read-button">Read!</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body base03-border">
|
<div class="panel-body base03-border">
|
||||||
<div v-for="notification in visibleNotifications" class="notification" :class='{"unseen": !notification.seen}'>
|
<div v-for="notification in visibleNotifications" :key="notification" class="notification" :class='{"unseen": !notification.seen}'>
|
||||||
<a :href="notification.action.user.statusnet_profile_url">
|
<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' style="width: 100%;">
|
||||||
<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</router-link></h1>
|
<h1>
|
||||||
<p>{{ notification.status.text }}</p>
|
<span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
|
||||||
|
<i class="fa icon-star"></i>
|
||||||
|
<small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
||||||
|
</h1>
|
||||||
|
<div v-html="notification.status.statusnet_html"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="notification.type === 'repeat'">
|
<div v-if="notification.type === 'repeat'">
|
||||||
<h1>{{ notification.action.user.name }}<br><i class="fa icon-retweet"></i> repeated your <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">status</router-link></h1>
|
<h1>
|
||||||
<p>{{ notification.status.text }}</p>
|
<span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
|
||||||
|
<i class="fa icon-retweet lit"></i>
|
||||||
|
<small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
||||||
|
</h1>
|
||||||
|
<div v-html="notification.status.statusnet_html"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="notification.type === 'mention'">
|
<div v-if="notification.type === 'mention'">
|
||||||
<h1>{{ notification.action.user.name }}<br><i class="fa icon-reply"></i> <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">mentioned</router-link> you</h1>
|
<h1>
|
||||||
<p>{{ notification.status.text }}</p>
|
<span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
|
||||||
|
<i class="fa icon-reply lit"></i>
|
||||||
|
<small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
||||||
|
</h1>
|
||||||
|
<status :compact="true" :statusoid="notification.status"></status>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
||||||
import MediaUpload from '../media_upload/media_upload.vue'
|
import MediaUpload from '../media_upload/media_upload.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import Tribute from '../../../node_modules/tributejs/src/Tribute.js'
|
import Completion from '../../services/completion/completion.js'
|
||||||
require('../../../node_modules/tributejs/scss/tribute.scss')
|
import { take, filter, reject, map, uniqBy } from 'lodash'
|
||||||
|
|
||||||
import { merge, reject, map, uniqBy } from 'lodash'
|
|
||||||
|
|
||||||
const buildMentionsString = ({user, attentions}, currentUser) => {
|
const buildMentionsString = ({user, attentions}, currentUser) => {
|
||||||
let allAttentions = [...attentions]
|
let allAttentions = [...attentions]
|
||||||
|
@ -21,51 +19,6 @@ const buildMentionsString = ({user, attentions}, currentUser) => {
|
||||||
return mentions.join(' ') + ' '
|
return mentions.join(' ') + ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultCollection = {
|
|
||||||
// symbol that starts the lookup
|
|
||||||
trigger: '@',
|
|
||||||
|
|
||||||
// element to target for @mentions
|
|
||||||
iframe: null,
|
|
||||||
|
|
||||||
// class added in the flyout menu for active item
|
|
||||||
selectClass: 'highlight',
|
|
||||||
|
|
||||||
// function called on select that returns the content to insert
|
|
||||||
selectTemplate: function (item) {
|
|
||||||
return '@' + item.original.screen_name
|
|
||||||
},
|
|
||||||
|
|
||||||
// template for displaying item in menu
|
|
||||||
menuItemTemplate: function (item) {
|
|
||||||
return `<img src="${item.original.profile_image_url}"></img> <div class='name'>${item.string}</div>`
|
|
||||||
},
|
|
||||||
|
|
||||||
// template for when no match is found (optional),
|
|
||||||
// If no template is provided, menu is hidden.
|
|
||||||
noMatchTemplate: null,
|
|
||||||
|
|
||||||
// specify an alternative parent container for the menu
|
|
||||||
menuContainer: document.body,
|
|
||||||
|
|
||||||
// column to search against in the object (accepts function or string)
|
|
||||||
lookup: ({name, screen_name}) => `${name} (@${screen_name})`, // eslint-disable-line camelcase
|
|
||||||
|
|
||||||
// column that contains the content to insert by default
|
|
||||||
fillAttr: 'screen_name',
|
|
||||||
|
|
||||||
// REQUIRED: array of objects to match
|
|
||||||
values: [],
|
|
||||||
|
|
||||||
// specify whether a space is required before the trigger character
|
|
||||||
requireLeadingSpace: true,
|
|
||||||
|
|
||||||
// specify whether a space is allowed in the middle of mentions
|
|
||||||
allowSpaces: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const tribute = new Tribute({ collection: [] })
|
|
||||||
|
|
||||||
const PostStatusForm = {
|
const PostStatusForm = {
|
||||||
props: [
|
props: [
|
||||||
'replyTo',
|
'replyTo',
|
||||||
|
@ -89,30 +42,48 @@ const PostStatusForm = {
|
||||||
newStatus: {
|
newStatus: {
|
||||||
status: statusText,
|
status: statusText,
|
||||||
files: []
|
files: []
|
||||||
}
|
},
|
||||||
|
caret: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
candidates () {
|
||||||
|
if (this.textAtCaret.charAt(0) === '@') {
|
||||||
|
const matchedUsers = filter(this.users, (user) => (user.name + user.screen_name).match(this.textAtCaret.slice(1)))
|
||||||
|
if (matchedUsers.length <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}) => ({
|
||||||
|
screen_name: screen_name,
|
||||||
|
name: name,
|
||||||
|
img: profile_image_url_original
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textAtCaret () {
|
||||||
|
return (this.wordAtCaret || {}).word || ''
|
||||||
|
},
|
||||||
|
wordAtCaret () {
|
||||||
|
const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {}
|
||||||
|
return word
|
||||||
|
},
|
||||||
users () {
|
users () {
|
||||||
return this.$store.state.users.users
|
return this.$store.state.users.users
|
||||||
},
|
|
||||||
completions () {
|
|
||||||
let users = this.users
|
|
||||||
users = merge({values: users}, defaultCollection)
|
|
||||||
return [users]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
completions () {
|
|
||||||
tribute.collection = this.completions
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
const textarea = this.$el.querySelector('textarea')
|
|
||||||
tribute.collection = this.completions
|
|
||||||
tribute.attach(textarea)
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
|
replace (replacement) {
|
||||||
|
this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
|
||||||
|
const el = this.$el.querySelector('textarea')
|
||||||
|
el.focus()
|
||||||
|
this.caret = 0
|
||||||
|
},
|
||||||
|
setCaret ({target: {selectionStart}}) {
|
||||||
|
this.caret = selectionStart
|
||||||
|
},
|
||||||
postStatus (newStatus) {
|
postStatus (newStatus) {
|
||||||
statusPoster.postStatus({
|
statusPoster.postStatus({
|
||||||
status: newStatus.status,
|
status: newStatus.status,
|
||||||
|
@ -125,6 +96,8 @@ const PostStatusForm = {
|
||||||
files: []
|
files: []
|
||||||
}
|
}
|
||||||
this.$emit('posted')
|
this.$emit('posted')
|
||||||
|
let el = this.$el.querySelector('textarea')
|
||||||
|
el.style.height = '16px'
|
||||||
},
|
},
|
||||||
addMediaFile (fileInfo) {
|
addMediaFile (fileInfo) {
|
||||||
this.newStatus.files.push(fileInfo)
|
this.newStatus.files.push(fileInfo)
|
||||||
|
@ -151,6 +124,13 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
fileDrag (e) {
|
fileDrag (e) {
|
||||||
e.dataTransfer.dropEffect = 'copy'
|
e.dataTransfer.dropEffect = 'copy'
|
||||||
|
},
|
||||||
|
resize (e) {
|
||||||
|
e.target.style.height = 'auto'
|
||||||
|
e.target.style.height = `${e.target.scrollHeight - 10}px`
|
||||||
|
if (e.target.value === '') {
|
||||||
|
e.target.style.height = '16px'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<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 base03-border" >
|
||||||
<textarea v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control" @keyup.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag"></textarea>
|
<textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" placeholder="Just landed in L.A." rows="1" class="form-control" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize"></textarea>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative;" v-if="candidates">
|
||||||
|
<div class="autocomplete-panel base05-background">
|
||||||
|
<div v-for="candidate in candidates" @click="replace('@' + candidate.screen_name + ' ')" class="autocomplete base01">
|
||||||
|
<img :src="candidate.img"></img>
|
||||||
|
<span>
|
||||||
|
@{{candidate.screen_name}}
|
||||||
|
<small class="base02">{{candidate.name}}</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='form-bottom'>
|
||||||
|
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload>
|
||||||
|
<button :disabled="submitDisabled" type="submit" class="btn btn-default base05 base01-background">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="attachments">
|
<div class="attachments">
|
||||||
<div class="attachment" v-for="file in newStatus.files">
|
<div class="attachment" v-for="file in newStatus.files">
|
||||||
|
@ -13,10 +28,6 @@
|
||||||
<a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
|
<a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-bottom'>
|
|
||||||
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload>
|
|
||||||
<button :disabled="submitDisabled" type="submit" class="btn btn-default base05 base01-background">Submit</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -44,14 +55,20 @@
|
||||||
.form-bottom {
|
.form-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
flex: 2;
|
width: 10em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachments {
|
.attachments {
|
||||||
padding: 0.5em;
|
padding: 0 0.5em;
|
||||||
|
|
||||||
|
.attachment {
|
||||||
|
position: relative;
|
||||||
|
margin: 0.5em 0.8em 0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -91,11 +108,56 @@
|
||||||
form textarea {
|
form textarea {
|
||||||
border: solid;
|
border: solid;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: silver;
|
border-color: inherit;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
line-height:16px;
|
line-height:16px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
resize: vertical;
|
resize: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
form textarea:focus {
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cancel {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-panel {
|
||||||
|
margin: 0 0.5em 0 0.5em;
|
||||||
|
border-radius: 5px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
min-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.2em 0.4em 0.2em 0.4em;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
line-height: 24px;
|
||||||
|
margin: 0 0.1em 0 0.2em;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,10 @@ const settings = {
|
||||||
hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv,
|
hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv,
|
||||||
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
||||||
muteWordsString: this.$store.state.config.muteWords.join('\n'),
|
muteWordsString: this.$store.state.config.muteWords.join('\n'),
|
||||||
previewfile: null
|
previewfile: null,
|
||||||
|
autoLoadLocal: this.$store.state.config.autoLoad,
|
||||||
|
hoverPreviewLocal: this.$store.state.config.hoverPreview,
|
||||||
|
muteWordsString: this.$store.state.config.muteWords.join('\n')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -58,6 +61,12 @@ const settings = {
|
||||||
hideNsfwLocal (value) {
|
hideNsfwLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||||
},
|
},
|
||||||
|
autoLoadLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'autoLoad', value })
|
||||||
|
},
|
||||||
|
hoverPreviewLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
|
||||||
|
},
|
||||||
muteWordsString (value) {
|
muteWordsString (value) {
|
||||||
value = filter(value.split('\n'), (word) => trim(word).length > 0)
|
value = filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||||
this.$store.dispatch('setOption', { name: 'muteWords', value })
|
this.$store.dispatch('setOption', { name: 'muteWords', value })
|
||||||
|
|
|
@ -40,6 +40,14 @@
|
||||||
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
|
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
|
||||||
<label for="hideNsfw">Enable clickthrough NSFW attachment hiding</label>
|
<label for="hideNsfw">Enable clickthrough NSFW attachment hiding</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" id="autoLoad" v-model="autoLoadLocal">
|
||||||
|
<label for="autoLoad">Enable automatic loading when scrolled to the bottom</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
|
||||||
|
<label for="hoverPreview">Enable reply-link preview on mouse hover</label>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,9 @@ const Status = {
|
||||||
'expandable',
|
'expandable',
|
||||||
'inConversation',
|
'inConversation',
|
||||||
'focused',
|
'focused',
|
||||||
'highlight'
|
'highlight',
|
||||||
|
'compact',
|
||||||
|
'replies'
|
||||||
],
|
],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
replying: false,
|
replying: false,
|
||||||
|
@ -86,9 +88,9 @@ const Status = {
|
||||||
toggleReplying () {
|
toggleReplying () {
|
||||||
this.replying = !this.replying
|
this.replying = !this.replying
|
||||||
},
|
},
|
||||||
gotoOriginal () {
|
gotoOriginal (id) {
|
||||||
// only handled by conversation, not status_or_conversation
|
// only handled by conversation, not status_or_conversation
|
||||||
this.$emit('goto', this.status.in_reply_to_status_id)
|
this.$emit('goto', id)
|
||||||
},
|
},
|
||||||
toggleExpanded () {
|
toggleExpanded () {
|
||||||
this.$emit('toggleExpanded')
|
this.$emit('toggleExpanded')
|
||||||
|
@ -98,6 +100,15 @@ const Status = {
|
||||||
},
|
},
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
this.userExpanded = !this.userExpanded
|
this.userExpanded = !this.userExpanded
|
||||||
|
},
|
||||||
|
replyEnter (id, event) {
|
||||||
|
if (this.$store.state.config.hoverPreview) {
|
||||||
|
let rect = event.target.getBoundingClientRect()
|
||||||
|
this.$emit('preview', Number(id), rect.left + 20, rect.top + 20 + window.pageYOffset)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replyLeave () {
|
||||||
|
this.$emit('preview', 0, 0, 0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -107,9 +118,8 @@ const Status = {
|
||||||
let rect = this.$el.getBoundingClientRect()
|
let rect = this.$el.getBoundingClientRect()
|
||||||
if (rect.top < 100) {
|
if (rect.top < 100) {
|
||||||
window.scrollBy(0, rect.top - 200)
|
window.scrollBy(0, rect.top - 200)
|
||||||
} else if (rect.bottom > window.innerHeight - 100) {
|
} else if (rect.bottom > window.innerHeight - 50) {
|
||||||
// will be useful when scrolling down to replies or root posts is in
|
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
||||||
window.scrollBy(0, rect.bottom + 200)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="status-el base00-background base03-border" v-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" >
|
<div class="status-el base00-background" v-if="compact">
|
||||||
|
<div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
|
||||||
|
<div v-if="loggedIn">
|
||||||
|
<div class='status-actions'>
|
||||||
|
<div>
|
||||||
|
<a href="#" v-on:click.prevent="toggleReplying">
|
||||||
|
<i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<retweet-button :status=status></retweet-button>
|
||||||
|
<favorite-button :status=status></favorite-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying" v-if="replying"/>
|
||||||
|
</div>
|
||||||
|
<div class="status-el base00-background base03-border" v-else-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" >
|
||||||
<template v-if="muted">
|
<template v-if="muted">
|
||||||
<div class="media status container muted">
|
<div class="media status container muted">
|
||||||
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
|
<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>
|
<small class="muteWords">{{muteWordHits.join(', ')}}</small>
|
||||||
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a>
|
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="fa icon-eye-off"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!muted">
|
<template v-if="!muted">
|
||||||
|
@ -13,13 +28,14 @@
|
||||||
<i class='fa icon-retweet retweeted'></i>
|
<i class='fa icon-retweet retweeted'></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
Retweeted by {{retweeter}}
|
Repeated by <a :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media status container">
|
<div class="media status container">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<a :href="status.user.statusnet_profile_url">
|
<a :href="status.user.statusnet_profile_url">
|
||||||
<img @click.prevent="toggleUserExpanded" class='avatar' :src="status.user.profile_image_url_original">
|
<img @click.prevent="toggleUserExpanded" :class="{retweeted: retweet}" class='avatar' :src="status.user.profile_image_url_original">
|
||||||
|
<img v-if="retweet" class='avatar-retweeter' :src="statusoid.user.profile_image_url_original"></img>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
|
@ -27,38 +43,45 @@
|
||||||
<user-card-content :user="status.user"></user-card-content>
|
<user-card-content :user="status.user"></user-card-content>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-content">
|
<div class="user-content">
|
||||||
<h4 class="media-heading">
|
<div class="media-heading">
|
||||||
{{status.user.name}}
|
<div class="name-and-links">
|
||||||
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
|
<h4 class="user-name">{{status.user.name}}</h4>
|
||||||
<small v-if="status.in_reply_to_screen_name"> >
|
<div class="links">
|
||||||
<router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
|
<h4>
|
||||||
{{status.in_reply_to_screen_name}}
|
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
|
||||||
</router-link>
|
<small v-if="status.in_reply_to_screen_name"> >
|
||||||
</small>
|
<router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
|
||||||
<template v-if="isReply && !expandable">
|
{{status.in_reply_to_screen_name}}
|
||||||
<small>
|
</router-link>
|
||||||
<a href="#" @click.prevent="gotoOriginal" ><i class="icon-reply"></i></a>
|
</small>
|
||||||
</small>
|
<template v-if="isReply && !expandable">
|
||||||
</template>
|
<small>
|
||||||
-
|
<a href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"><i class="icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i></a>
|
||||||
<small>
|
</small>
|
||||||
<router-link :to="{ name: 'conversation', params: { id: status.id } }">
|
</template>
|
||||||
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
-
|
||||||
</router-link>
|
<small>
|
||||||
</small>
|
<router-link :to="{ name: 'conversation', params: { id: status.id } }">
|
||||||
<template v-if="expandable">
|
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
||||||
-
|
</router-link>
|
||||||
<small>
|
</small>
|
||||||
<a href="#" @click.prevent="toggleExpanded" ><i class="icon-plus-squared"></i></a>
|
</h4>
|
||||||
</small>
|
</div>
|
||||||
</template>
|
<h4 class="replies" v-if="inConversation">
|
||||||
<small v-if="unmuted">
|
<small v-if="replies.length">Replies:</small>
|
||||||
<a href="#" @click.prevent="toggleMute" ><i class="icon-eye-off"></i></a>
|
<small v-for="reply in replies">
|
||||||
</small>
|
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}} </a>
|
||||||
<small v-if="!status.is_local" class="source_url">
|
</small>
|
||||||
<a :href="status.external_url" target="_blank" ><i class="icon-binoculars"></i></a>
|
</h4>
|
||||||
</small>
|
</div>
|
||||||
</h4>
|
<div class="heading-icons">
|
||||||
|
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="fa icon-eye-off"></i></a>
|
||||||
|
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="fa icon-binoculars"></i></a>
|
||||||
|
<template v-if="expandable">
|
||||||
|
<a href="#" @click.prevent="toggleExpanded" class="expand"><i class="fa icon-plus-squared"></i></a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
|
<div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
|
||||||
|
|
||||||
|
@ -94,24 +117,65 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
status-text-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.status-el {
|
.status-el {
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
border-left-width: 0px;
|
border-left-width: 0px;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
.notify {
|
||||||
|
.avatar {
|
||||||
|
border-width: 3px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-body {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.user-content {
|
.user-content {
|
||||||
|
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
padding-top: 1px;
|
padding-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source_url {
|
.media-heading {
|
||||||
float: right;
|
display: flex;
|
||||||
|
min-height: 1.4em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin-right: 0.4em;
|
||||||
|
}
|
||||||
|
.name-and-links {
|
||||||
|
flex: 1 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.replies {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.greentext {
|
.source_url {
|
||||||
color: green;
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand {
|
||||||
|
margin-right: -0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -128,6 +192,34 @@
|
||||||
margin-top: 0.2em;
|
margin-top: 0.2em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media-left {
|
||||||
|
img {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
float: right;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.retweet-info {
|
||||||
|
padding: 0.7em 0 0 0.6em;
|
||||||
|
|
||||||
|
.media-left {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
i {
|
||||||
|
align-self: center;
|
||||||
|
text-align: right;
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 0.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.greentext {
|
||||||
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-conversation {
|
.status-conversation {
|
||||||
|
@ -135,7 +227,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-actions {
|
.status-actions {
|
||||||
padding-top: 5px;
|
padding-top: 0.15em;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
div, favorite-button {
|
||||||
|
max-width: 6em;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-reply:hover {
|
.icon-reply:hover {
|
||||||
|
@ -147,7 +246,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status .avatar {
|
.status .avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
|
&.retweeted {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status img.avatar-retweeter {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.compact .avatar {
|
.status.compact .avatar {
|
||||||
|
@ -155,14 +270,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
padding: 0.65em 0.7em 0.8em 0.8em;
|
padding: 0.4em 0.7em 0.45em 0.7em;
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-bottom-color: inherit;
|
border-bottom-color: inherit;
|
||||||
border-left: 4px rgba(255, 48, 16, 0.65);
|
border-left: 4px rgba(255, 48, 16, 0.65);
|
||||||
border-left-style: inherit;
|
border-left-style: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
padding: 0.1em 0.7em 0.1em 0.8em;
|
padding: 0.1em 0.4em 0.1em 0.8em;
|
||||||
button {
|
button {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
@ -194,4 +310,35 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 960px) {
|
||||||
|
.status-el {
|
||||||
|
.name-and-links {
|
||||||
|
margin-left: -0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status .avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
&.retweeted {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status img.avatar-retweeter {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 18px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,7 +6,8 @@ const Timeline = {
|
||||||
props: [
|
props: [
|
||||||
'timeline',
|
'timeline',
|
||||||
'timelineName',
|
'timelineName',
|
||||||
'title'
|
'title',
|
||||||
|
'userId'
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
timelineError () { return this.$store.state.statuses.error }
|
timelineError () { return this.$store.state.statuses.error }
|
||||||
|
@ -20,11 +21,14 @@ const Timeline = {
|
||||||
const credentials = store.state.users.currentUser.credentials
|
const credentials = store.state.users.currentUser.credentials
|
||||||
const showImmediately = this.timeline.visibleStatuses.length === 0
|
const showImmediately = this.timeline.visibleStatuses.length === 0
|
||||||
|
|
||||||
|
window.onscroll = this.scrollLoad
|
||||||
|
|
||||||
timelineFetcher.fetchAndUpdate({
|
timelineFetcher.fetchAndUpdate({
|
||||||
store,
|
store,
|
||||||
credentials,
|
credentials,
|
||||||
timeline: this.timelineName,
|
timeline: this.timelineName,
|
||||||
showImmediately
|
showImmediately,
|
||||||
|
userId: this.userId
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -40,8 +44,15 @@ const Timeline = {
|
||||||
credentials,
|
credentials,
|
||||||
timeline: this.timelineName,
|
timeline: this.timelineName,
|
||||||
older: true,
|
older: true,
|
||||||
showImmediately: true
|
showImmediately: true,
|
||||||
|
userId: this.userId
|
||||||
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
|
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
|
||||||
|
},
|
||||||
|
scrollLoad (e) {
|
||||||
|
let height = Math.max(document.body.offsetHeight, document.body.scrollHeight)
|
||||||
|
if (this.timeline.loading === false && this.$store.state.config.autoLoad && (window.innerHeight + window.pageYOffset) >= (height - 750)) {
|
||||||
|
this.fetchOlderStatuses()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,10 +61,13 @@
|
||||||
props: [ 'user' ],
|
props: [ 'user' ],
|
||||||
computed: {
|
computed: {
|
||||||
headingStyle () {
|
headingStyle () {
|
||||||
let rgb = this.$store.state.config.colors['base00'].match(/\d+/g)
|
let color = this.$store.state.config.colors['base00']
|
||||||
return {
|
if (color) {
|
||||||
backgroundColor: `rgb(${Math.floor(rgb[0] * 0.53)}, ${Math.floor(rgb[1] * 0.56)}, ${Math.floor(rgb[2] * 0.59)})`,
|
let rgb = this.$store.state.config.colors['base00'].match(/\d+/g)
|
||||||
backgroundImage: `url(${this.user.cover_photo})`
|
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 () {
|
bodyStyle () {
|
||||||
|
|
22
src/components/user_finder/user_finder.js
Normal file
22
src/components/user_finder/user_finder.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const UserFinder = {
|
||||||
|
data: () => ({
|
||||||
|
username: undefined,
|
||||||
|
hidden: true
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
findUser (username) {
|
||||||
|
this.$store.state.api.backendInteractor.externalProfile(username)
|
||||||
|
.then((user) => {
|
||||||
|
if (!user.error) {
|
||||||
|
this.$store.commit('addNewUsers', [user])
|
||||||
|
this.$router.push({name: 'user-profile', params: {id: user.id}})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toggleHidden () {
|
||||||
|
this.hidden = !this.hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserFinder
|
23
src/components/user_finder/user_finder.vue
Normal file
23
src/components/user_finder/user_finder.vue
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<a href="#" v-if="hidden"><i class="icon-user-plus user-finder-icon" @click.prevent="toggleHidden"/></a>
|
||||||
|
<span v-else>
|
||||||
|
<input class="user-finder-input base03-border" @keyup.enter="findUser(username)" v-model="username" placeholder="Find user" id="user-finder-input" type="text"/>
|
||||||
|
<i class="icon-cancel user-finder-icon" @click="toggleHidden"/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./user_finder.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.user-finder-icon {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-finder-input {
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: inherit;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.1em 0.2em 0.2em 0.2em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,16 +1,30 @@
|
||||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||||
import { find } from 'lodash'
|
import Timeline from '../timeline/timeline.vue'
|
||||||
|
|
||||||
const UserProfile = {
|
const UserProfile = {
|
||||||
|
created () {
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||||
|
this.$store.dispatch('startFetching', ['user', this.userId])
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.$store.dispatch('stopFetching', 'user')
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
timeline () { return this.$store.state.statuses.timelines.user },
|
||||||
|
userId () {
|
||||||
|
return this.$route.params.id
|
||||||
|
},
|
||||||
user () {
|
user () {
|
||||||
const id = this.$route.params.id
|
if (this.timeline.statuses[0]) {
|
||||||
const user = find(this.$store.state.users.users, {id})
|
return this.timeline.statuses[0].user
|
||||||
return user
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
UserCardContent
|
UserCardContent,
|
||||||
|
Timeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="user-profile panel panel-default base00-background">
|
<div>
|
||||||
<user-card-content :user="user"></user-card-content>
|
<div v-if="user" class="user-profile panel panel-default base00-background">
|
||||||
|
<user-card-content :user="user"></user-card-content>
|
||||||
|
</div>
|
||||||
|
<Timeline :title="'User Timeline'" v-bind:timeline="timeline" v-bind:timeline-name="'user'" :user-id="userId"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ Vue.use(VueRouter)
|
||||||
Vue.use(VueTimeago, {
|
Vue.use(VueTimeago, {
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
locales: {
|
locales: {
|
||||||
'en-US': require('vue-timeago/locales/en-US.json')
|
'en-US': require('../static/timeago.json')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -34,6 +34,8 @@ const persistedStateOptions = {
|
||||||
'config.hideAttachments',
|
'config.hideAttachments',
|
||||||
'config.hideAttachmentsInConv',
|
'config.hideAttachmentsInConv',
|
||||||
'config.hideNsfw',
|
'config.hideNsfw',
|
||||||
|
'config.autoLoad',
|
||||||
|
'config.hoverPreview',
|
||||||
'config.muteWords',
|
'config.muteWords',
|
||||||
'statuses.notifications',
|
'statuses.notifications',
|
||||||
'users.users'
|
'users.users'
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
|
import {isArray} from 'lodash'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
state: {
|
state: {
|
||||||
|
@ -18,9 +19,17 @@ const api = {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
startFetching (store, timeline) {
|
startFetching (store, timeline) {
|
||||||
|
let userId = false
|
||||||
|
|
||||||
|
// This is for user timelines
|
||||||
|
if (isArray(timeline)) {
|
||||||
|
userId = timeline[1]
|
||||||
|
timeline = timeline[0]
|
||||||
|
}
|
||||||
|
|
||||||
// Don't start fetching if we already are.
|
// Don't start fetching if we already are.
|
||||||
if (!store.state.fetchers[timeline]) {
|
if (!store.state.fetchers[timeline]) {
|
||||||
const fetcher = store.state.backendInteractor.startFetching({timeline, store})
|
const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId})
|
||||||
store.commit('addFetcher', {timeline, fetcher})
|
store.commit('addFetcher', {timeline, fetcher})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,8 @@ const defaultState = {
|
||||||
hideAttachments: false,
|
hideAttachments: false,
|
||||||
hideAttachmentsInConv: false,
|
hideAttachmentsInConv: false,
|
||||||
hideNsfw: true,
|
hideNsfw: true,
|
||||||
|
autoLoad: true,
|
||||||
|
hoverPreview: true,
|
||||||
muteWords: []
|
muteWords: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { remove, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max, isArray } from 'lodash'
|
import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max, isArray } from 'lodash'
|
||||||
import apiService from '../services/api/api.service.js'
|
import apiService from '../services/api/api.service.js'
|
||||||
// import parse from '../services/status_parser/status_parser.js'
|
// import parse from '../services/status_parser/status_parser.js'
|
||||||
|
|
||||||
|
@ -32,6 +32,17 @@ export const defaultState = {
|
||||||
minVisibleId: 0,
|
minVisibleId: 0,
|
||||||
loading: false
|
loading: false
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
statuses: [],
|
||||||
|
statusesObject: {},
|
||||||
|
faves: [],
|
||||||
|
visibleStatuses: [],
|
||||||
|
visibleStatusesObject: {},
|
||||||
|
newStatusCount: 0,
|
||||||
|
maxId: 0,
|
||||||
|
minVisibleId: 0,
|
||||||
|
loading: false
|
||||||
|
},
|
||||||
publicAndExternal: {
|
publicAndExternal: {
|
||||||
statuses: [],
|
statuses: [],
|
||||||
statusesObject: {},
|
statusesObject: {},
|
||||||
|
@ -57,11 +68,15 @@ export const defaultState = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNsfw = (status) => {
|
||||||
|
const nsfwRegex = /#nsfw/i
|
||||||
|
return includes(status.tags, 'nsfw') || !!status.text.match(nsfwRegex)
|
||||||
|
}
|
||||||
|
|
||||||
export const prepareStatus = (status) => {
|
export const prepareStatus = (status) => {
|
||||||
// Parse nsfw tags
|
// Parse nsfw tags
|
||||||
if (status.nsfw === undefined) {
|
if (status.nsfw === undefined) {
|
||||||
const nsfwRegex = /#nsfw/i
|
status.nsfw = isNsfw(status)
|
||||||
status.nsfw = !!status.text.match(nsfwRegex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set deleted flag
|
// Set deleted flag
|
||||||
|
@ -242,6 +257,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
||||||
const uri = deletion.uri
|
const uri = deletion.uri
|
||||||
updateMaxId(deletion)
|
updateMaxId(deletion)
|
||||||
|
|
||||||
|
// Remove possible notification
|
||||||
|
const status = find(allStatuses, {uri})
|
||||||
|
if (!status) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(state.notifications, ({action: {id}}) => id === status.id)
|
||||||
|
|
||||||
remove(allStatuses, { uri })
|
remove(allStatuses, { uri })
|
||||||
if (timeline) {
|
if (timeline) {
|
||||||
remove(timelineObject.statuses, { uri })
|
remove(timelineObject.statuses, { uri })
|
||||||
|
@ -276,6 +299,21 @@ export const mutations = {
|
||||||
oldTimeline.visibleStatusesObject = {}
|
oldTimeline.visibleStatusesObject = {}
|
||||||
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
|
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
|
||||||
},
|
},
|
||||||
|
clearTimeline (state, { timeline }) {
|
||||||
|
const emptyTimeline = {
|
||||||
|
statuses: [],
|
||||||
|
statusesObject: {},
|
||||||
|
faves: [],
|
||||||
|
visibleStatuses: [],
|
||||||
|
visibleStatusesObject: {},
|
||||||
|
newStatusCount: 0,
|
||||||
|
maxId: 0,
|
||||||
|
minVisibleId: 0,
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
|
||||||
|
state.timelines[timeline] = emptyTimeline
|
||||||
|
},
|
||||||
setFavorited (state, { status, value }) {
|
setFavorited (state, { status, value }) {
|
||||||
const newStatus = state.allStatusesObject[status.id]
|
const newStatus = state.allStatusesObject[status.id]
|
||||||
newStatus.favorited = value
|
newStatus.favorited = value
|
||||||
|
|
|
@ -19,9 +19,11 @@ const UNFOLLOWING_URL = '/api/friendships/destroy.json'
|
||||||
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
|
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
|
||||||
const REGISTRATION_URL = '/api/account/register.json'
|
const REGISTRATION_URL = '/api/account/register.json'
|
||||||
const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
|
const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
|
||||||
|
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
|
||||||
|
const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
|
||||||
// const USER_URL = '/api/users/show.json'
|
// const USER_URL = '/api/users/show.json'
|
||||||
|
|
||||||
import { each } from 'lodash'
|
import { each, map } from 'lodash'
|
||||||
|
|
||||||
const oldfetch = window.fetch
|
const oldfetch = window.fetch
|
||||||
|
|
||||||
|
@ -88,6 +90,13 @@ const authHeaders = (user) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const externalProfile = (profileUrl) => {
|
||||||
|
let url = `${EXTERNAL_PROFILE_URL}?profileurl=${profileUrl}`
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'GET'
|
||||||
|
}).then((data) => data.json())
|
||||||
|
}
|
||||||
|
|
||||||
const followUser = ({id, credentials}) => {
|
const followUser = ({id, credentials}) => {
|
||||||
let url = `${FOLLOWING_URL}?user_id=${id}`
|
let url = `${FOLLOWING_URL}?user_id=${id}`
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
|
@ -143,24 +152,34 @@ const setUserMute = ({id, credentials, muted = true}) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTimeline = ({timeline, credentials, since = false, until = false}) => {
|
const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false}) => {
|
||||||
const timelineUrls = {
|
const timelineUrls = {
|
||||||
public: PUBLIC_TIMELINE_URL,
|
public: PUBLIC_TIMELINE_URL,
|
||||||
friends: FRIENDS_TIMELINE_URL,
|
friends: FRIENDS_TIMELINE_URL,
|
||||||
mentions: MENTIONS_URL,
|
mentions: MENTIONS_URL,
|
||||||
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL
|
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
|
||||||
|
user: QVITTER_USER_TIMELINE_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = timelineUrls[timeline]
|
let url = timelineUrls[timeline]
|
||||||
|
|
||||||
|
let params = []
|
||||||
|
|
||||||
if (since) {
|
if (since) {
|
||||||
url += `?since_id=${since}`
|
params.push(['since_id', since])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (until) {
|
if (until) {
|
||||||
url += `?max_id=${until}`
|
params.push(['max_id', until])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
params.push(['user_id', userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
|
||||||
|
url += `?${queryString}`
|
||||||
|
|
||||||
return fetch(url, { headers: authHeaders(credentials) }).then((data) => data.json())
|
return fetch(url, { headers: authHeaders(credentials) }).then((data) => data.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +272,8 @@ const apiService = {
|
||||||
setUserMute,
|
setUserMute,
|
||||||
fetchMutes,
|
fetchMutes,
|
||||||
register,
|
register,
|
||||||
updateAvatar
|
updateAvatar,
|
||||||
|
externalProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
@ -26,8 +26,8 @@ const backendInteractorService = (credentials) => {
|
||||||
return apiService.unfollowUser({credentials, id})
|
return apiService.unfollowUser({credentials, id})
|
||||||
}
|
}
|
||||||
|
|
||||||
const startFetching = ({timeline, store}) => {
|
const startFetching = ({timeline, store, userId = false}) => {
|
||||||
return timelineFetcherService.startFetching({timeline, store, credentials})
|
return timelineFetcherService.startFetching({timeline, store, credentials, userId})
|
||||||
}
|
}
|
||||||
|
|
||||||
const setUserMute = ({id, muted = true}) => {
|
const setUserMute = ({id, muted = true}) => {
|
||||||
|
@ -38,6 +38,7 @@ const backendInteractorService = (credentials) => {
|
||||||
|
|
||||||
const register = (params) => apiService.register(params)
|
const register = (params) => apiService.register(params)
|
||||||
const updateAvatar = ({params}) => apiService.updateAvatar({credentials, params})
|
const updateAvatar = ({params}) => apiService.updateAvatar({credentials, params})
|
||||||
|
const externalProfile = (profileUrl) => apiService.externalProfile(profileUrl)
|
||||||
|
|
||||||
const backendInteractorServiceInstance = {
|
const backendInteractorServiceInstance = {
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
|
@ -51,7 +52,8 @@ const backendInteractorService = (credentials) => {
|
||||||
setUserMute,
|
setUserMute,
|
||||||
fetchMutes,
|
fetchMutes,
|
||||||
register,
|
register,
|
||||||
updateAvatar
|
updateAvatar,
|
||||||
|
externalProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
return backendInteractorServiceInstance
|
return backendInteractorServiceInstance
|
||||||
|
|
70
src/services/completion/completion.js
Normal file
70
src/services/completion/completion.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { reduce, find } from 'lodash'
|
||||||
|
|
||||||
|
export const replaceWord = (str, toReplace, replacement) => {
|
||||||
|
return str.slice(0, toReplace.start) + replacement + str.slice(toReplace.end)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wordAtPosition = (str, pos) => {
|
||||||
|
const words = splitIntoWords(str)
|
||||||
|
const wordsWithPosition = addPositionToWords(words)
|
||||||
|
|
||||||
|
return find(wordsWithPosition, ({start, end}) => start <= pos && end > pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addPositionToWords = (words) => {
|
||||||
|
return reduce(words, (result, word) => {
|
||||||
|
const data = {
|
||||||
|
word,
|
||||||
|
start: 0,
|
||||||
|
end: word.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
const previous = result.pop()
|
||||||
|
|
||||||
|
data.start += previous.end
|
||||||
|
data.end += previous.end
|
||||||
|
|
||||||
|
result.push(previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(data)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const splitIntoWords = (str) => {
|
||||||
|
// Split at word boundaries
|
||||||
|
const regex = /\b/
|
||||||
|
const triggers = /[@#]+$/
|
||||||
|
|
||||||
|
let split = str.split(regex)
|
||||||
|
|
||||||
|
// Add trailing @ and # to the following word.
|
||||||
|
const words = reduce(split, (result, word) => {
|
||||||
|
if (result.length > 0) {
|
||||||
|
let previous = result.pop()
|
||||||
|
const matches = previous.match(triggers)
|
||||||
|
if (matches) {
|
||||||
|
previous = previous.replace(triggers, '')
|
||||||
|
word = matches[0] + word
|
||||||
|
}
|
||||||
|
result.push(previous)
|
||||||
|
}
|
||||||
|
result.push(word)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
const completion = {
|
||||||
|
wordAtPosition,
|
||||||
|
addPositionToWords,
|
||||||
|
splitIntoWords,
|
||||||
|
replaceWord
|
||||||
|
}
|
||||||
|
|
||||||
|
export default completion
|
|
@ -14,7 +14,7 @@ const update = ({store, statuses, timeline, showImmediately}) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false}) => {
|
const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false}) => {
|
||||||
const args = { timeline, credentials }
|
const args = { timeline, credentials }
|
||||||
const rootState = store.rootState || store.state
|
const rootState = store.rootState || store.state
|
||||||
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
|
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
|
||||||
|
@ -25,14 +25,16 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
|
||||||
args['since'] = timelineData.maxId
|
args['since'] = timelineData.maxId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args['userId'] = userId
|
||||||
|
|
||||||
return apiService.fetchTimeline(args)
|
return apiService.fetchTimeline(args)
|
||||||
.then((statuses) => update({store, statuses, timeline, showImmediately}),
|
.then((statuses) => update({store, statuses, timeline, showImmediately}),
|
||||||
() => store.dispatch('setError', { value: true }))
|
() => store.dispatch('setError', { value: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const startFetching = ({ timeline = 'friends', credentials, store }) => {
|
const startFetching = ({timeline = 'friends', credentials, store, userId = false}) => {
|
||||||
fetchAndUpdate({timeline, credentials, store, showImmediately: true})
|
fetchAndUpdate({timeline, credentials, store, showImmediately: true, userId})
|
||||||
const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store })
|
const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId })
|
||||||
return setInterval(boundFetchAndUpdate, 10000)
|
return setInterval(boundFetchAndUpdate, 10000)
|
||||||
}
|
}
|
||||||
const timelineFetcher = {
|
const timelineFetcher = {
|
||||||
|
|
|
@ -77,6 +77,18 @@
|
||||||
"css": "cog",
|
"css": "cog",
|
||||||
"code": 59399,
|
"code": 59399,
|
||||||
"src": "fontawesome"
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "1bafeeb1808a5fe24484c7890096901a",
|
||||||
|
"css": "user-plus",
|
||||||
|
"code": 62004,
|
||||||
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "559647a6f430b3aeadbecd67194451dd",
|
||||||
|
"css": "menu",
|
||||||
|
"code": 61641,
|
||||||
|
"src": "fontawesome"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
4
static/font/css/fontello-codes.css
vendored
4
static/font/css/fontello-codes.css
vendored
|
@ -9,5 +9,7 @@
|
||||||
.icon-cog:before { content: '\e807'; } /* '' */
|
.icon-cog:before { content: '\e807'; } /* '' */
|
||||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||||
|
.icon-menu:before { content: '\f0c9'; } /* '' */
|
||||||
.icon-reply:before { content: '\f112'; } /* '' */
|
.icon-reply:before { content: '\f112'; } /* '' */
|
||||||
.icon-binoculars:before { content: '\f1e5'; } /* '' */
|
.icon-binoculars:before { content: '\f1e5'; } /* '' */
|
||||||
|
.icon-user-plus:before { content: '\f234'; } /* '' */
|
16
static/font/css/fontello-embedded.css
vendored
16
static/font/css/fontello-embedded.css
vendored
File diff suppressed because one or more lines are too long
4
static/font/css/fontello-ie7-codes.css
vendored
4
static/font/css/fontello-ie7-codes.css
vendored
|
@ -9,5 +9,7 @@
|
||||||
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
4
static/font/css/fontello-ie7.css
vendored
4
static/font/css/fontello-ie7.css
vendored
|
@ -20,5 +20,7 @@
|
||||||
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
18
static/font/css/fontello.css
vendored
18
static/font/css/fontello.css
vendored
|
@ -1,11 +1,11 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'fontello';
|
font-family: 'fontello';
|
||||||
src: url('../font/fontello.eot?90538621');
|
src: url('../font/fontello.eot?79576261');
|
||||||
src: url('../font/fontello.eot?90538621#iefix') format('embedded-opentype'),
|
src: url('../font/fontello.eot?79576261#iefix') format('embedded-opentype'),
|
||||||
url('../font/fontello.woff2?90538621') format('woff2'),
|
url('../font/fontello.woff2?79576261') format('woff2'),
|
||||||
url('../font/fontello.woff?90538621') format('woff'),
|
url('../font/fontello.woff?79576261') format('woff'),
|
||||||
url('../font/fontello.ttf?90538621') format('truetype'),
|
url('../font/fontello.ttf?79576261') format('truetype'),
|
||||||
url('../font/fontello.svg?90538621#fontello') format('svg');
|
url('../font/fontello.svg?79576261#fontello') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'fontello';
|
font-family: 'fontello';
|
||||||
src: url('../font/fontello.svg?90538621#fontello') format('svg');
|
src: url('../font/fontello.svg?79576261#fontello') format('svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
@ -65,5 +65,7 @@
|
||||||
.icon-cog:before { content: '\e807'; } /* '' */
|
.icon-cog:before { content: '\e807'; } /* '' */
|
||||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||||
|
.icon-menu:before { content: '\f0c9'; } /* '' */
|
||||||
.icon-reply:before { content: '\f112'; } /* '' */
|
.icon-reply:before { content: '\f112'; } /* '' */
|
||||||
.icon-binoculars:before { content: '\f1e5'; } /* '' */
|
.icon-binoculars:before { content: '\f1e5'; } /* '' */
|
||||||
|
.icon-user-plus:before { content: '\f234'; } /* '' */
|
|
@ -229,11 +229,11 @@ body {
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'fontello';
|
font-family: 'fontello';
|
||||||
src: url('./font/fontello.eot?15442171');
|
src: url('./font/fontello.eot?13861244');
|
||||||
src: url('./font/fontello.eot?15442171#iefix') format('embedded-opentype'),
|
src: url('./font/fontello.eot?13861244#iefix') format('embedded-opentype'),
|
||||||
url('./font/fontello.woff?15442171') format('woff'),
|
url('./font/fontello.woff?13861244') format('woff'),
|
||||||
url('./font/fontello.ttf?15442171') format('truetype'),
|
url('./font/fontello.ttf?13861244') format('truetype'),
|
||||||
url('./font/fontello.svg?15442171#fontello') format('svg');
|
url('./font/fontello.svg?13861244#fontello') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
@ -315,8 +315,12 @@ body {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div title="Code: 0xe832" class="the-icons span3"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
|
<div title="Code: 0xe832" class="the-icons span3"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
|
||||||
<div title="Code: 0xe834" class="the-icons span3"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
|
<div title="Code: 0xe834" class="the-icons span3"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
|
||||||
|
<div title="Code: 0xf0c9" class="the-icons span3"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
|
||||||
<div title="Code: 0xf112" class="the-icons span3"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
|
<div title="Code: 0xf112" class="the-icons span3"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
<div title="Code: 0xf1e5" class="the-icons span3"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
|
<div title="Code: 0xf1e5" class="the-icons span3"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
|
||||||
|
<div title="Code: 0xf234" class="the-icons span3"><i class="demo-icon icon-user-plus"></i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
|
<div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
|
||||||
|
|
Binary file not shown.
|
@ -26,9 +26,13 @@
|
||||||
|
|
||||||
<glyph glyph-name="spin4" unicode="" d="M498 850c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
|
<glyph glyph-name="spin4" unicode="" d="M498 850c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
|
||||||
|
|
||||||
|
<glyph glyph-name="menu" unicode="" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
|
||||||
|
|
||||||
<glyph glyph-name="reply" unicode="" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
|
<glyph glyph-name="reply" unicode="" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
|
||||||
|
|
||||||
<glyph glyph-name="binoculars" unicode="" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
|
<glyph glyph-name="binoculars" unicode="" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
|
||||||
|
|
||||||
|
<glyph glyph-name="user-plus" unicode="" d="M393 350q-89 0-152 63t-62 151 62 152 152 63 151-63 63-152-63-151-151-63z m536-71h196q7 0 13-6t5-12v-107q0-8-5-13t-13-5h-196v-197q0-7-6-12t-12-6h-107q-8 0-13 6t-5 12v197h-197q-7 0-12 5t-6 13v107q0 7 6 12t12 6h197v196q0 7 5 13t13 5h107q7 0 12-5t6-13v-196z m-411-125q0-29 21-51t50-21h143v-133q-38-28-95-28h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q11 0 22-10 44-34 86-51t92-17 92 17 86 51q11 10 22 10 73 0 121-54h-125q-29 0-50-21t-21-50v-107z" horiz-adv-x="1142.9" />
|
||||||
</font>
|
</font>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
10
static/timeago.json
Normal file
10
static/timeago.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[
|
||||||
|
"now",
|
||||||
|
["%ss", "%ss"],
|
||||||
|
["%smin", "%smin"],
|
||||||
|
["%sh", "%sh"],
|
||||||
|
["%sd", "%sd"],
|
||||||
|
["%sw", "%sw"],
|
||||||
|
["%sm", "%sm"],
|
||||||
|
["%sy", "%sy"]
|
||||||
|
]
|
|
@ -125,18 +125,19 @@ describe('The Statuses module', () => {
|
||||||
it('removes statuses by tag on deletion', () => {
|
it('removes statuses by tag on deletion', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: 1})
|
||||||
|
const otherStatus = makeMockStatus({id: 3})
|
||||||
status.uri = 'xxx'
|
status.uri = 'xxx'
|
||||||
const deletion = makeMockStatus({id: 2, is_post_verb: false})
|
const deletion = makeMockStatus({id: 2, is_post_verb: false})
|
||||||
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
||||||
deletion.uri = 'xxx'
|
deletion.uri = 'xxx'
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' })
|
||||||
mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' })
|
||||||
|
|
||||||
expect(state.allStatuses).to.eql([])
|
expect(state.allStatuses).to.eql([otherStatus])
|
||||||
expect(state.timelines.public.statuses).to.eql([])
|
expect(state.timelines.public.statuses).to.eql([otherStatus])
|
||||||
expect(state.timelines.public.visibleStatuses).to.eql([])
|
expect(state.timelines.public.visibleStatuses).to.eql([otherStatus])
|
||||||
expect(state.timelines.public.maxId).to.eql(2)
|
expect(state.timelines.public.maxId).to.eql(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not update the maxId when the noIdUpdate flag is set', () => {
|
it('does not update the maxId when the noIdUpdate flag is set', () => {
|
||||||
|
@ -319,6 +320,36 @@ describe('The Statuses module', () => {
|
||||||
expect(state.notifications[0].type).to.eql('mention')
|
expect(state.notifications[0].type).to.eql('mention')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('removes a notification when the notice gets removed', () => {
|
||||||
|
const user = { id: 1 }
|
||||||
|
const state = cloneDeep(defaultState)
|
||||||
|
const status = makeMockStatus({id: 1})
|
||||||
|
const otherStatus = makeMockStatus({id: 3})
|
||||||
|
const mentionedStatus = makeMockStatus({id: 2})
|
||||||
|
mentionedStatus.attentions = [user]
|
||||||
|
mentionedStatus.uri = 'xxx'
|
||||||
|
otherStatus.attentions = [user]
|
||||||
|
|
||||||
|
const deletion = makeMockStatus({id: 4, is_post_verb: false})
|
||||||
|
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
||||||
|
deletion.uri = 'xxx'
|
||||||
|
|
||||||
|
mutations.addNewStatuses(state, { statuses: [status, otherStatus], user })
|
||||||
|
|
||||||
|
expect(state.notifications.length).to.eql(1)
|
||||||
|
|
||||||
|
mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
|
||||||
|
expect(state.allStatuses.length).to.eql(3)
|
||||||
|
expect(state.notifications.length).to.eql(2)
|
||||||
|
expect(state.notifications[1].status).to.eql(mentionedStatus)
|
||||||
|
expect(state.notifications[1].action).to.eql(mentionedStatus)
|
||||||
|
expect(state.notifications[1].type).to.eql('mention')
|
||||||
|
|
||||||
|
mutations.addNewStatuses(state, { statuses: [deletion], user })
|
||||||
|
expect(state.allStatuses.length).to.eql(2)
|
||||||
|
expect(state.notifications.length).to.eql(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('adds the message to mentions when you are mentioned', () => {
|
it('adds the message to mentions when you are mentioned', () => {
|
||||||
const user = { id: 1 }
|
const user = { id: 1 }
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
|
|
70
test/unit/specs/services/completion/completion.spec.js
Normal file
70
test/unit/specs/services/completion/completion.spec.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { replaceWord, addPositionToWords, wordAtPosition, splitIntoWords } from '../../../../../src/services/completion/completion.js'
|
||||||
|
|
||||||
|
describe('addPositiontoWords', () => {
|
||||||
|
it('adds the position to a word list', () => {
|
||||||
|
const words = ['hey', 'this', 'is', 'fun']
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
word: 'hey',
|
||||||
|
start: 0,
|
||||||
|
end: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
word: 'this',
|
||||||
|
start: 3,
|
||||||
|
end: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
word: 'is',
|
||||||
|
start: 7,
|
||||||
|
end: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
word: 'fun',
|
||||||
|
start: 9,
|
||||||
|
end: 12
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const res = addPositionToWords(words)
|
||||||
|
|
||||||
|
expect(res).to.eql(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('splitIntoWords', () => {
|
||||||
|
it('splits at whitespace boundaries', () => {
|
||||||
|
const str = 'This is a #nice @test for you, @idiot.'
|
||||||
|
const expected = ['This', ' ', 'is', ' ', 'a', ' ', '#nice', ' ', '@test', ' ', 'for', ' ', 'you', ', ', '@idiot', '.']
|
||||||
|
const res = splitIntoWords(str)
|
||||||
|
|
||||||
|
expect(res).to.eql(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('wordAtPosition', () => {
|
||||||
|
it('returns the word for a given string and postion, plus the start and end position of that word', () => {
|
||||||
|
const str = 'Hey this is fun'
|
||||||
|
|
||||||
|
const { word, start, end } = wordAtPosition(str, 4)
|
||||||
|
|
||||||
|
expect(word).to.eql('this')
|
||||||
|
expect(start).to.eql(4)
|
||||||
|
expect(end).to.eql(8)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('replaceWord', () => {
|
||||||
|
it('replaces a word (with start and end) with another word in a given string', () => {
|
||||||
|
const str = 'hey @take, how are you'
|
||||||
|
const wordsWithPosition = addPositionToWords(splitIntoWords(str))
|
||||||
|
const toReplace = wordsWithPosition[2]
|
||||||
|
|
||||||
|
expect(toReplace.word).to.eql('@take')
|
||||||
|
|
||||||
|
const expected = 'hey @takeshitakenji, how are you'
|
||||||
|
const res = replaceWord(str, toReplace, '@takeshitakenji')
|
||||||
|
expect(res).to.eql(expected)
|
||||||
|
})
|
||||||
|
})
|
28
yarn.lock
28
yarn.lock
|
@ -5500,10 +5500,6 @@ tough-cookie@~2.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode "^1.4.1"
|
punycode "^1.4.1"
|
||||||
|
|
||||||
tributejs@^2.1.0:
|
|
||||||
version "2.3.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-2.3.3.tgz#ec3b9ae3edd0f7e2bc5ca56d11ae43fdd7a8cd28"
|
|
||||||
|
|
||||||
trim-newlines@^1.0.0:
|
trim-newlines@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
||||||
|
@ -5717,9 +5713,9 @@ vue-loader@^11.1.0:
|
||||||
vue-style-loader "^2.0.0"
|
vue-style-loader "^2.0.0"
|
||||||
vue-template-es2015-compiler "^1.2.2"
|
vue-template-es2015-compiler "^1.2.2"
|
||||||
|
|
||||||
vue-router@^2.2.0:
|
vue-router@^2.5.3:
|
||||||
version "2.2.1"
|
version "2.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.2.1.tgz#b027f9fac2cf13462725e843d6dc631b6aa077f6"
|
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.5.3.tgz#073783f564b6aece6c8a59c63e298dc2aabfb51b"
|
||||||
|
|
||||||
vue-style-loader@^2.0.0:
|
vue-style-loader@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
@ -5728,9 +5724,9 @@ vue-style-loader@^2.0.0:
|
||||||
hash-sum "^1.0.2"
|
hash-sum "^1.0.2"
|
||||||
loader-utils "^0.2.7"
|
loader-utils "^0.2.7"
|
||||||
|
|
||||||
vue-template-compiler@^2.1.10:
|
vue-template-compiler@^2.3.4:
|
||||||
version "2.1.10"
|
version "2.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.1.10.tgz#cb89643adc395e97435585522e43d0a9b1913257"
|
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.3.4.tgz#5a88ac2c5e4d5d6218e6aa80e7e221fb7e67894c"
|
||||||
dependencies:
|
dependencies:
|
||||||
de-indent "^1.0.2"
|
de-indent "^1.0.2"
|
||||||
he "^1.1.0"
|
he "^1.1.0"
|
||||||
|
@ -5743,13 +5739,13 @@ vue-timeago@^3.1.2:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/vue-timeago/-/vue-timeago-3.2.0.tgz#73fd0635de6ea4ecfbbce035b2e44035d806fba1"
|
resolved "https://registry.yarnpkg.com/vue-timeago/-/vue-timeago-3.2.0.tgz#73fd0635de6ea4ecfbbce035b2e44035d806fba1"
|
||||||
|
|
||||||
vue@^2.1.0:
|
vue@^2.3.4:
|
||||||
version "2.1.10"
|
version "2.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.3.4.tgz#5ec3b87a191da8090bbef56b7cfabd4158038171"
|
||||||
|
|
||||||
vuex@^2.1.0:
|
vuex@^2.3.1:
|
||||||
version "2.1.2"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.1.2.tgz#15d2da62dd6ff59c071f0a91cd4f434eacf6ca6c"
|
resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.3.1.tgz#cde8e997c1f9957719bc7dea154f9aa691d981a6"
|
||||||
|
|
||||||
watchpack@^0.2.1:
|
watchpack@^0.2.1:
|
||||||
version "0.2.9"
|
version "0.2.9"
|
||||||
|
|
Loading…
Reference in a new issue