Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop: (88 commits)
  Update font-size of username in UserCardContent component
  Re-do status header a bit, add more consistent spacing to status
  Fix JS error when no statuses returned
  Fix fetching error by tag
  #388: update naming properly
  Fix layout overflow issue
  Add a class to screen name
  Add back accidently removed logic
  Merge all slots of BasicUserCard into one
  Revert "Minor mobile layout improvement for BasicUserCard"
  Minor mobile layout improvement for BasicUserCard
  Use native filter function
  Shorten a classname
  Improve mobile layout of user card
  Update naming
  Add back some css
  Remove legacy class names in BasicUserCard
  Remove UserCard
  Migrate UserCard to FollowCard and FollowRequestCard
  Add FollowRequestCard component
  ...
This commit is contained in:
Henry Jameson 2019-03-02 20:28:10 +02:00
commit a129ef2e07
78 changed files with 1402 additions and 606 deletions

View file

@ -28,6 +28,7 @@
"sass-loader": "^4.0.2", "sass-loader": "^4.0.2",
"vue": "^2.5.13", "vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1", "vue-chat-scroll": "^1.2.1",
"vue-compose": "^0.7.1",
"vue-i18n": "^7.3.2", "vue-i18n": "^7.3.2",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4", "vue-template-compiler": "^2.3.4",

View file

@ -628,6 +628,16 @@ nav {
color: $fallback--faint; color: $fallback--faint;
color: var(--faint, $fallback--faint); color: var(--faint, $fallback--faint);
} }
.faint-link {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
&:hover {
text-decoration: underline;
}
}
@media all and (min-width: 800px) { @media all and (min-width: 800px) {
.logo { .logo {
opacity: 1 !important; opacity: 1 !important;
@ -661,6 +671,10 @@ nav {
border-radius: var(--inputRadius, $fallback--inputRadius); border-radius: var(--inputRadius, $fallback--inputRadius);
} }
.button-icon {
font-size: 1.2em;
}
@keyframes shakeError { @keyframes shakeError {
0% { 0% {
transform: translateX(0); transform: translateX(0);
@ -705,16 +719,6 @@ nav {
margin: 0.5em 0 0.5em 0; margin: 0.5em 0 0.5em 0;
} }
.button-icon {
font-size: 1.2em;
}
.status .status-actions {
div {
max-width: 4em;
}
}
.menu-button { .menu-button {
display: block; display: block;
margin-right: 0.8em; margin-right: 0.8em;

View file

@ -88,7 +88,7 @@
.attachment { .attachment {
position: relative; position: relative;
margin: 0.5em 0.5em 0em 0em; margin-top: 0.5em;
align-self: flex-start; align-self: flex-start;
line-height: 0; line-height: 0;

View file

@ -0,0 +1,28 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = {
props: [
'user'
],
data () {
return {
userExpanded: false
}
},
components: {
UserCardContent,
UserAvatar
},
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
}
}
}
export default BasicUserCard

View file

@ -0,0 +1,79 @@
<template>
<div class="user-card">
<router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link>
<div class="user-card-expanded-content" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="user-card-collapsed-content" v-else>
<div :title="user.name" class="user-card-user-name">
<span v-if="user.name_html" v-html="user.name_html"></span>
<span v-else>{{ user.name }}</span>
</div>
<div>
<router-link class="user-card-screen-name" :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div>
<slot></slot>
</div>
</div>
</template>
<script src="./basic_user_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-card {
display: flex;
flex: 1 0;
padding-top: 0.6em;
padding-right: 1em;
padding-bottom: 0.6em;
padding-left: 1em;
border-bottom: 1px solid;
margin: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
&-collapsed-content {
margin-left: 0.7em;
text-align: left;
flex: 1;
min-width: 0;
}
&-user-name {
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
}
&-expanded-content {
flex: 1;
margin-left: 0.7em;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
}
</style>

View file

@ -0,0 +1,37 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const BlockCard = {
props: ['userId'],
data () {
return {
progress: false
}
},
computed: {
user () {
return this.$store.getters.userById(this.userId)
},
blocked () {
return this.user.statusnet_blocking
}
},
components: {
BasicUserCard
},
methods: {
unblockUser () {
this.progress = true
this.$store.dispatch('unblockUser', this.user.id).then(() => {
this.progress = false
})
},
blockUser () {
this.progress = true
this.$store.dispatch('blockUser', this.user.id).then(() => {
this.progress = false
})
}
}
}
export default BlockCard

View file

@ -0,0 +1,34 @@
<template>
<basic-user-card :user="user">
<div class="block-card-content-container">
<button class="btn btn-default" @click="unblockUser" :disabled="progress" v-if="blocked">
<template v-if="progress">
{{ $t('user_card.unblock_progress') }}
</template>
<template v-else>
{{ $t('user_card.unblock') }}
</template>
</button>
<button class="btn btn-default" @click="blockUser" :disabled="progress" v-else>
<template v-if="progress">
{{ $t('user_card.block_progress') }}
</template>
<template v-else>
{{ $t('user_card.block') }}
</template>
</button>
</div>
</basic-user-card>
</template>
<script src="./block_card.js"></script>
<style lang="scss">
.block-card-content-container {
margin-top: 0.5em;
text-align: right;
button {
width: 10em;
}
}
</style>

View file

@ -0,0 +1,45 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const FollowCard = {
props: [
'user',
'noFollowsYou'
],
data () {
return {
inProgress: false,
requestSent: false,
updated: false
}
},
components: {
BasicUserCard
},
computed: {
isMe () { return this.$store.state.users.currentUser.id === this.user.id },
following () { return this.updated ? this.updated.following : this.user.following },
showFollow () {
return !this.following || this.updated && !this.updated.following
}
},
methods: {
followUser () {
this.inProgress = true
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
this.inProgress = false
this.requestSent = sent
this.updated = updated
})
},
unfollowUser () {
this.inProgress = true
requestUnfollow(this.user, this.$store).then(({ updated }) => {
this.inProgress = false
this.updated = updated
})
}
}
}
export default FollowCard

View file

@ -0,0 +1,53 @@
<template>
<basic-user-card :user="user">
<div class="follow-card-content-container">
<span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
<button
v-if="showFollow"
class="btn btn-default"
@click="followUser"
:disabled="inProgress"
:title="requestSent ? $t('user_card.follow_again') : ''"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="requestSent">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="inProgress">
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</div>
</basic-user-card>
</template>
<script src="./follow_card.js"></script>
<style lang="scss">
.follow-card-content-container {
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
line-height: 1.5em;
.btn {
margin-top: 0.5em;
margin-left: auto;
width: 10em;
}
}
</style>

View file

@ -1,68 +0,0 @@
import UserCard from '../user_card/user_card.vue'
const FollowList = {
data () {
return {
loading: false,
bottomedOut: false,
error: false
}
},
props: ['userId', 'showFollowers'],
created () {
window.addEventListener('scroll', this.scrollLoad)
if (this.entries.length === 0) {
this.fetchEntries()
}
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
this.$store.dispatch('clearFriendsAndFollowers', this.userId)
},
computed: {
user () {
return this.$store.getters.userById(this.userId)
},
entries () {
return this.showFollowers ? this.user.followers : this.user.friends
},
showFollowsYou () {
return !this.showFollowers || (this.showFollowers && this.userId !== this.$store.state.users.currentUser.id)
}
},
methods: {
fetchEntries () {
if (!this.loading) {
const command = this.showFollowers ? 'addFollowers' : 'addFriends'
this.loading = true
this.$store.dispatch(command, this.userId).then(entries => {
this.error = false
this.loading = false
this.bottomedOut = entries.length === 0
}).catch(() => {
this.error = true
this.loading = false
})
}
},
scrollLoad (e) {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
if (this.loading === false &&
this.bottomedOut === false &&
this.$el.offsetHeight > 0 &&
(window.innerHeight + window.pageYOffset) >= (height - 750)
) {
this.fetchEntries()
}
}
},
watch: {
'user': 'fetchEntries'
},
components: {
UserCard
}
}
export default FollowList

View file

@ -1,33 +0,0 @@
<template>
<div class="follow-list">
<user-card
v-for="entry in entries"
:key="entry.id" :user="entry"
:noFollowsYou="!showFollowsYou"
/>
<div class="text-center panel-footer">
<a v-if="error" @click="fetchEntries" class="alert error">
{{$t('general.generic_error')}}
</a>
<i v-else-if="loading" class="icon-spin3 animate-spin"/>
<span v-else-if="bottomedOut"></span>
<a v-else @click="fetchEntries">{{$t('general.more')}}</a>
</div>
</div>
</template>
<script src="./follow_list.js"></script>
<style lang="scss">
.follow-list {
.panel-footer {
padding: 10px;
}
.error {
font-size: 14px;
}
}
</style>

View file

@ -0,0 +1,20 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const FollowRequestCard = {
props: ['user'],
components: {
BasicUserCard
},
methods: {
approveUser () {
this.$store.state.api.backendInteractor.approveUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
denyUser () {
this.$store.state.api.backendInteractor.denyUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
}
}
}
export default FollowRequestCard

View file

@ -0,0 +1,29 @@
<template>
<basic-user-card :user="user">
<div class="follow-request-card-content-container">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
</basic-user-card>
</template>
<script src="./follow_request_card.js"></script>
<style lang="scss">
.follow-request-card-content-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
button {
margin-top: 0.5em;
margin-right: 0.5em;
flex: 1 1;
max-width: 12em;
min-width: 8em;
&:last-child {
margin-right: 0;
}
}
}
</style>

View file

@ -1,22 +1,13 @@
import UserCard from '../user_card/user_card.vue' import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
const FollowRequests = { const FollowRequests = {
components: { components: {
UserCard FollowRequestCard
},
created () {
this.updateRequests()
}, },
computed: { computed: {
requests () { requests () {
return this.$store.state.api.followRequests return this.$store.state.api.followRequests
} }
},
methods: {
updateRequests () {
this.$store.state.api.backendInteractor.fetchFollowRequests()
.then((requests) => { this.$store.commit('setFollowRequests', requests) })
}
} }
} }

View file

@ -4,7 +4,7 @@
{{$t('nav.friend_requests')}} {{$t('nav.friend_requests')}}
</div> </div>
<div class="panel-body"> <div class="panel-body">
<user-card v-for="request in requests" :key="request.id" :user="request" :showFollows="false" :showApproval="true"></user-card> <FollowRequestCard v-for="request in requests" :key="request.id" :user="request"/>
</div> </div>
</div> </div>
</template> </template>

View file

@ -36,6 +36,9 @@
box-sizing: border-box; box-sizing: border-box;
// to make failed images a bit more noticeable on chromium // to make failed images a bit more noticeable on chromium
min-width: 2em; min-width: 2em;
&:last-child {
margin: 0;
}
} }
.image-attachment { .image-attachment {

View file

@ -24,10 +24,6 @@
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
// TODO: clean up the random margins in attachments, this makes preview line
// up with attachments...
margin-right: 0.5em;
.card-image { .card-image {
flex-shrink: 0; flex-shrink: 0;
width: 120px; width: 120px;

View file

@ -0,0 +1,37 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const MuteCard = {
props: ['userId'],
data () {
return {
progress: false
}
},
computed: {
user () {
return this.$store.getters.userById(this.userId)
},
muted () {
return this.user.muted
}
},
components: {
BasicUserCard
},
methods: {
unmuteUser () {
this.progress = true
this.$store.dispatch('unmuteUser', this.user.id).then(() => {
this.progress = false
})
},
muteUser () {
this.progress = true
this.$store.dispatch('muteUser', this.user.id).then(() => {
this.progress = false
})
}
}
}
export default MuteCard

View file

@ -0,0 +1,24 @@
<template>
<basic-user-card :user="user">
<template slot="secondary-area">
<button class="btn btn-default" @click="unmuteUser" :disabled="progress" v-if="muted">
<template v-if="progress">
{{ $t('user_card.unmute_progress') }}
</template>
<template v-else>
{{ $t('user_card.unmute') }}
</template>
</button>
<button class="btn btn-default" @click="muteUser" :disabled="progress" v-else>
<template v-if="progress">
{{ $t('user_card.mute_progress') }}
</template>
<template v-else>
{{ $t('user_card.mute') }}
</template>
</button>
</template>
</basic-user-card>
</template>
<script src="./mute_card.js"></script>

View file

@ -1,10 +1,23 @@
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
const NavPanel = { const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
followRequestFetcher.startFetching({ store, credentials })
}
},
computed: { computed: {
currentUser () { currentUser () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
chat () { chat () {
return this.$store.state.chat.channel return this.$store.state.chat.channel
},
followRequestCount () {
return this.$store.state.api.followRequests.length
} }
} }
} }

View file

@ -20,8 +20,8 @@
<li v-if='currentUser && currentUser.locked'> <li v-if='currentUser && currentUser.locked'>
<router-link :to="{ name: 'friend-requests' }"> <router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests")}} {{ $t("nav.friend_requests")}}
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count"> <span v-if='followRequestCount > 0' class="badge follow-request-count">
{{currentUser.follow_request_count}} {{followRequestCount}}
</span> </span>
</router-link> </router-link>
</li> </li>

View file

@ -9,7 +9,7 @@
<div class='text-fields'> <div class='text-fields'>
<div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }"> <div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
<label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label> <label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label>
<input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' placeholder='e.g. lain'> <input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' :placeholder="$t('registration.username_placeholder')">
</div> </div>
<div class="form-error" v-if="$v.user.username.$dirty"> <div class="form-error" v-if="$v.user.username.$dirty">
<ul> <ul>
@ -21,7 +21,7 @@
<div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }"> <div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
<label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label> <label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
<input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' placeholder='e.g. Lain Iwakura'> <input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' :placeholder="$t('registration.fullname_placeholder')">
</div> </div>
<div class="form-error" v-if="$v.user.fullname.$dirty"> <div class="form-error" v-if="$v.user.fullname.$dirty">
<ul> <ul>
@ -44,8 +44,8 @@
</div> </div>
<div class='form-group'> <div class='form-group'>
<label class='form--label' for='bio'>{{$t('registration.bio')}}</label> <label class='form--label' for='bio'>{{$t('registration.bio')}} ({{$t('general.optional')}})</label>
<input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'> <textarea :disabled="isPending" v-model='user.bio' class='form-control' id='bio' :placeholder="$t('registration.bio_placeholder')"></textarea>
</div> </div>
<div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }"> <div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
@ -139,6 +139,10 @@ $validations-cRed: #f04124;
flex-direction: column; flex-direction: column;
} }
textarea {
min-height: 100px;
}
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -12,6 +12,7 @@ const settings = {
return { return {
hideAttachmentsLocal: user.hideAttachments, hideAttachmentsLocal: user.hideAttachments,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv, hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
maxThumbnails: user.maxThumbnails,
hideNsfwLocal: user.hideNsfw, hideNsfwLocal: user.hideNsfw,
useOneClickNsfw: user.useOneClickNsfw, useOneClickNsfw: user.useOneClickNsfw,
hideISPLocal: user.hideISP, hideISPLocal: user.hideISP,
@ -186,6 +187,10 @@ const settings = {
}, },
useContainFit (value) { useContainFit (value) {
this.$store.dispatch('setOption', { name: 'useContainFit', value }) this.$store.dispatch('setOption', { name: 'useContainFit', value })
},
maxThumbnails (value) {
value = this.maxThumbnails = Math.floor(Math.max(value, 0))
this.$store.dispatch('setOption', { name: 'maxThumbnails', value })
} }
} }
} }

View file

@ -136,6 +136,10 @@
<input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal"> <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
<label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label> <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
</li> </li>
<li>
<label for="maxThumbnails">{{$t('settings.max_thumbnails')}}</label>
<input class="number-input" type="number" id="maxThumbnails" v-model.number="maxThumbnails" min="0" step="1">
</li>
<li> <li>
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal"> <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label> <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
@ -146,7 +150,7 @@
<label for="preloadImage">{{$t('settings.preload_images')}}</label> <label for="preloadImage">{{$t('settings.preload_images')}}</label>
</li> </li>
<li> <li>
<input type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw"> <input :disabled="!hideNsfwLocal" type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw">
<label for="useOneClickNsfw">{{$t('settings.use_one_click_nsfw')}}</label> <label for="useOneClickNsfw">{{$t('settings.use_one_click_nsfw')}}</label>
</li> </li>
</ul> </ul>
@ -316,6 +320,10 @@
min-width: 10em; min-width: 10em;
padding: 0 2em; padding: 0 2em;
} }
.number-input {
max-width: 6em;
}
} }
.select-multiple { .select-multiple {
display: flex; display: flex;

View file

@ -32,6 +32,9 @@ const SideDrawer = {
}, },
sitename () { sitename () {
return this.$store.state.instance.name return this.$store.state.instance.name
},
followRequestCount () {
return this.$store.state.api.followRequests.length
} }
}, },
methods: { methods: {

View file

@ -45,8 +45,8 @@
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer"> <li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
<router-link to='/friend-requests'> <router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }} {{ $t("nav.friend_requests") }}
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count"> <span v-if='followRequestCount > 0' class="badge follow-request-count">
{{currentUser.follow_request_count}} {{followRequestCount}}
</span> </span>
</router-link> </router-link>

View file

@ -23,7 +23,7 @@ const Status = {
'highlight', 'highlight',
'compact', 'compact',
'replies', 'replies',
'noReplyLinks', 'isPreview',
'noHeading', 'noHeading',
'inlineExpanded' 'inlineExpanded'
], ],
@ -40,8 +40,7 @@ const Status = {
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject ? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject, : !this.$store.state.config.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter, betterShadow: this.$store.state.interface.browserSupport.cssFilter
maxAttachments: 9
} }
}, },
computed: { computed: {
@ -225,7 +224,7 @@ const Status = {
attachmentSize () { attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) || if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) || (this.$store.state.config.hideAttachmentsInConv && this.inConversation) ||
(this.status.attachments.length > this.maxAttachments)) { (this.status.attachments.length > this.maxThumbnails)) {
return 'hide' return 'hide'
} else if (this.compact) { } else if (this.compact) {
return 'small' return 'small'
@ -249,6 +248,9 @@ const Status = {
return this.status.attachments.filter( return this.status.attachments.filter(
file => !fileType.fileMatchesSomeType(this.galleryTypes, file) file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
) )
},
maxThumbnails () {
return this.$store.state.config.maxThumbnails
} }
}, },
components: { components: {

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"> <div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks"> <template v-if="muted && !isPreview">
<div class="media status container muted"> <div class="media status container muted">
<small> <small>
<router-link :to="userProfileLink"> <router-link :to="userProfileLink">
@ -13,7 +13,7 @@
</template> </template>
<template v-else> <template v-else>
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info"> <div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<UserAvatar v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/> <UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint"> <div class="media-body faint">
<span class="user-name"> <span class="user-name">
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/> <router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
@ -31,57 +31,69 @@
</router-link> </router-link>
</div> </div>
<div class="status-body"> <div class="status-body">
<div class="usercard media-body" v-if="userExpanded"> <div class="usercard" v-if="userExpanded">
<user-card-content :user="status.user" :switcher="false"></user-card-content> <user-card-content :user="status.user" :switcher="false"></user-card-content>
</div> </div>
<div v-if="!noHeading" class="media-body container media-heading"> <div v-if="!noHeading" class="media-heading">
<div class="media-heading-left"> <div class="heading-name-row">
<div class="name-and-links"> <div class="name-and-account-name">
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4> <h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
<h4 class="user-name" v-else>{{status.user.name}}</h4> <h4 class="user-name" v-else>{{status.user.name}}</h4>
<span class="links"> <router-link class="account-name" :to="userProfileLink">
<router-link :to="userProfileLink"> {{status.user.screen_name}}
{{status.user.screen_name}} </router-link>
</router-link> </div>
<span v-if="isReply" class="faint reply-info">
<i class="icon-right-open"></i> <span class="heading-right">
<router-link :to="replyProfileLink"> <router-link class="timeago faint-link" :to="{ name: 'conversation', params: { id: status.id } }">
{{replyToName}} <timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link> </router-link>
</span> <div class="button-icon visibility-icon" v-if="status.visibility">
<a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" :aria-label="$t('tool_tip.reply')"> <i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
<i class="button-icon icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i> </div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local && !isPreview" class="source_url" title="Source">
<i class="button-icon icon-link-ext-alt"></i>
</a>
<template v-if="expandable && !isPreview">
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="button-icon icon-plus-squared"></i>
</a> </a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="button-icon icon-eye-off"></i></a>
</span>
</div>
<div class="heading-reply-row">
<div v-if="isReply" class="reply-to-and-accountname">
<a class="reply-to"
href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
:aria-label="$t('tool_tip.reply')"
@mouseenter.prevent.stop="replyEnter(status.in_reply_to_status_id, $event)"
@mouseleave.prevent.stop="replyLeave()"
>
<i class="button-icon icon-reply" v-if="!isPreview"></i>
<span class="faint-link reply-to-text">{{$t('status.reply_to')}}</span>
</a>
<router-link :to="replyProfileLink">
{{replyToName}}
</router-link>
<span class="faint replies-separator" v-if="replies.length">
-
</span> </span>
</div> </div>
<h4 class="replies" v-if="inConversation && !noReplyLinks"> <div class="replies" v-if="inConversation && !isPreview">
<small v-if="replies.length">Replies:</small> <span class="faint" v-if="replies.length">{{$t('status.replies_list')}}</span>
<small class="reply-link" v-bind:key="reply.id" v-for="reply in replies"> <span class="reply-link faint" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a> <a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}</a>
</small> </span>
</h4>
</div>
<div class="media-heading-right">
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
</div> </div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url" title="Source">
<i class="button-icon icon-link-ext-alt"></i>
</a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="button-icon icon-plus-squared"></i>
</a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="button-icon icon-eye-off"></i></a>
</div> </div>
</div> </div>
<div v-if="showPreview" class="status-preview-container"> <div v-if="showPreview" class="status-preview-container">
<status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status> <status class="status-preview" v-if="preview" :isPreview="true" :statusoid="preview" :compact=true></status>
<div class="status-preview status-preview-loading" v-else> <div class="status-preview status-preview-loading" v-else>
<i class="icon-spin4 animate-spin"></i> <i class="icon-spin4 animate-spin"></i>
</div> </div>
@ -123,7 +135,7 @@
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" /> <link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
</div> </div>
<div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'> <div v-if="!noHeading && !isPreview" class='status-actions media-body'>
<div v-if="loggedIn"> <div v-if="loggedIn">
<a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')"> <a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')">
<i class="button-icon icon-reply" :class="{'icon-reply-active': replying}"></i> <i class="button-icon icon-reply" :class="{'icon-reply-active': replying}"></i>
@ -147,6 +159,8 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
$status-margin: 0.75em;
.status-body { .status-body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -202,13 +216,16 @@
} }
} }
.media-left {
margin-right: $status-margin;
}
.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;
min-width: 0; min-width: 0;
border-color: $fallback--border; border-color: $fallback--border;
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
@ -229,22 +246,34 @@
.media-body { .media-body {
flex: 1; flex: 1;
padding: 0; padding: 0;
margin: 0 0 0.25em 0.8em;
} }
.usercard { .usercard {
margin-bottom: .7em margin: 0;
margin-bottom: $status-margin;
}
.user-name {
white-space: nowrap;
font-size: 14px;
overflow: hidden;
flex-shrink: 0;
max-width: 85%;
font-weight: bold;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
} }
.media-heading { .media-heading {
flex-wrap: nowrap;
line-height: 18px;
}
.media-heading-left {
padding: 0; padding: 0;
vertical-align: bottom; vertical-align: bottom;
flex-basis: 100%; flex-basis: 100%;
margin-bottom: 0.5em;
a { a {
display: inline-block; display: inline-block;
@ -254,83 +283,102 @@
small { small {
font-weight: lighter; font-weight: lighter;
} }
h4 {
white-space: nowrap; .heading-name-row {
font-size: 14px;
margin-right: 0.25em;
overflow: hidden;
text-overflow: ellipsis;
}
.name-and-links {
padding: 0; padding: 0;
flex: 1 0;
display: flex; display: flex;
flex-wrap: wrap; justify-content: space-between;
align-items: baseline; line-height: 18px;
.name-and-account-name {
display: flex;
min-width: 0;
}
.user-name { .user-name {
margin-right: .45em; flex-shrink: 1;
margin-right: 0.4em;
overflow: hidden;
text-overflow: ellipsis;
}
img { .account-name {
width: 14px; min-width: 1.6em;
height: 14px; margin-right: 0.4em;
vertical-align: middle; white-space: nowrap;
object-fit: contain overflow: hidden;
} text-overflow: ellipsis;
flex: 1 1 0;
} }
} }
.links { .heading-right {
display: flex; display: flex;
flex-shrink: 0;
}
.timeago {
margin-right: 0.2em;
}
.heading-reply-row {
align-content: baseline;
font-size: 12px; font-size: 12px;
color: $fallback--link; line-height: 18px;
color: var(--link, $fallback--link);
max-width: 100%; max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: stretch;
a { a {
max-width: 100%; max-width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
& > span { }
text-overflow: ellipsis;
overflow: hidden; .reply-to-and-accountname {
white-space: nowrap; display: flex;
} height: 18px;
& > a:last-child { margin-right: 0.5em;
flex-shrink: 0; overflow: hidden;
max-width: 100%;
.icon-reply {
transform: scaleX(-1);
} }
} }
.reply-info { .reply-info {
display: flex; display: flex;
} }
.reply-to {
display: flex;
}
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
margin: 0 0.4em 0 0.2em;
}
.replies-separator {
margin-left: 0.4em;
}
.replies { .replies {
line-height: 16px; line-height: 18px;
}
.reply-link {
margin-right: 0.2em;
}
}
.media-heading-right {
display: inline-flex;
flex-shrink: 0;
flex-wrap: nowrap;
margin-left: .25em;
align-self: baseline;
.timeago {
margin-right: 0.2em;
font-size: 12px; font-size: 12px;
align-self: last baseline; display: flex;
flex-wrap: wrap;
& > * {
margin-right: 0.4em;
}
} }
> * { .reply-link {
margin-left: 0.2em; height: 17px;
}
a:hover i {
color: $fallback--text;
color: var(--text, $fallback--text);
} }
} }
@ -366,8 +414,8 @@
} }
.status-content { .status-content {
margin-right: 0.5em;
font-family: var(--postFont, sans-serif); font-family: var(--postFont, sans-serif);
line-height: 1.4em;
img, video { img, video {
max-width: 100%; max-width: 100%;
@ -390,9 +438,11 @@
} }
p { p {
margin: 0; margin: 0 0 1em 0;
margin-top: 0.2em; }
margin-bottom: 0.5em;
p:last-child {
margin: 0 0 0 0;
} }
h1 { h1 {
@ -417,7 +467,7 @@
} }
.retweet-info { .retweet-info {
padding: 0.4em 0.6em 0 0.6em; padding: 0.4em $status-margin;
margin: 0; margin: 0;
.avatar.still-image { .avatar.still-image {
@ -488,10 +538,10 @@
.status-actions { .status-actions {
width: 100%; width: 100%;
display: flex; display: flex;
margin-top: $status-margin;
div, favorite-button { div, favorite-button {
padding-top: 0.25em; max-width: 4em;
max-width: 6em;
flex: 1; flex: 1;
} }
} }
@ -517,9 +567,9 @@
.status { .status {
display: flex; display: flex;
padding: 0.6em; padding: $status-margin;
&.is-retweet { &.is-retweet {
padding-top: 0.1em; padding-top: 0;
} }
} }

View file

@ -1,7 +1,6 @@
import Status from '../status/status.vue' import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue' import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
import UserCard from '../user_card/user_card.vue'
import { throttle } from 'lodash' import { throttle } from 'lodash'
const Timeline = { const Timeline = {
@ -44,8 +43,7 @@ const Timeline = {
}, },
components: { components: {
Status, Status,
StatusOrConversation, StatusOrConversation
UserCard
}, },
created () { created () {
const store = this.$store const store = this.$store
@ -54,6 +52,8 @@ const Timeline = {
window.addEventListener('scroll', this.scrollLoad) window.addEventListener('scroll', this.scrollLoad)
if (this.timelineName === 'friends' && !credentials) { return false }
timelineFetcher.fetchAndUpdate({ timelineFetcher.fetchAndUpdate({
store, store,
credentials, credentials,
@ -68,14 +68,21 @@ const Timeline = {
document.addEventListener('visibilitychange', this.handleVisibilityChange, false) document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
this.unfocused = document.hidden this.unfocused = document.hidden
} }
window.addEventListener('keydown', this.handleShortKey)
}, },
destroyed () { destroyed () {
window.removeEventListener('scroll', this.scrollLoad) window.removeEventListener('scroll', this.scrollLoad)
window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
}, },
methods: { methods: {
handleShortKey (e) {
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses () { showNewStatuses () {
if (this.newStatusCount === 0) return
if (this.timeline.flushMarker !== 0) { if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { timeline: this.timelineName }) this.$store.commit('clearTimeline', { timeline: this.timelineName })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
@ -99,7 +106,7 @@ const Timeline = {
tag: this.tag tag: this.tag
}).then(statuses => { }).then(statuses => {
store.commit('setLoading', { timeline: this.timelineName, value: false }) store.commit('setLoading', { timeline: this.timelineName, value: false })
if (statuses.length === 0) { if (statuses && statuses.length === 0) {
this.bottomedOut = true this.bottomedOut = true
} }
}) })

View file

@ -1,64 +0,0 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const UserCard = {
props: [
'user',
'noFollowsYou',
'showApproval'
],
data () {
return {
userExpanded: false,
followRequestInProgress: false,
followRequestSent: false,
updated: false
}
},
components: {
UserCardContent,
UserAvatar
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
following () { return this.updated ? this.updated.following : this.user.following },
showFollow () {
return !this.showApproval && (!this.following || this.updated && !this.updated.following)
}
},
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
approveUser () {
this.$store.state.api.backendInteractor.approveUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
denyUser () {
this.$store.state.api.backendInteractor.denyUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
},
followUser () {
this.followRequestInProgress = true
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
this.followRequestInProgress = false
this.followRequestSent = sent
this.updated = updated
})
},
unfollowUser () {
this.followRequestInProgress = true
requestUnfollow(this.user, this.$store).then(({ updated }) => {
this.followRequestInProgress = false
this.updated = updated
})
}
}
}
export default UserCard

View file

@ -1,159 +0,0 @@
<template>
<div class="card">
<router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link>
<div class="user-card-main-content">
<div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-if="!userExpanded">
<div :title="user.name" class="user-name">
<span v-if="user.name_html" v-html="user.name_html"></span>
<span v-else>{{ user.name }}</span>
</div>
<div class="user-link-action">
<router-link class='user-screen-name' :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div>
</div>
<div class="follow-box" v-if="!userExpanded">
<span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
<button
v-if="showFollow"
class="btn btn-default"
@click="followUser"
:disabled="followRequestInProgress"
:title="followRequestSent ? $t('user_card.follow_again') : ''"
>
<template v-if="followRequestInProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="followRequestSent">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="followRequestInProgress">
<template v-if="followRequestInProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</div>
<div class="approval" v-if="showApproval">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
</div>
</div>
</template>
<script src="./user_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-card-main-content {
display: flex;
flex-direction: column;
flex: 1 1 100%;
margin-left: 0.7em;
min-width: 0;
}
.name-and-screen-name {
text-align: left;
width: 100%;
.user-name {
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
}
.user-link-action {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
}
.card {
display: flex;
flex: 1 0;
padding-top: 0.6em;
padding-right: 1em;
padding-bottom: 0.6em;
padding-left: 1em;
border-bottom: 1px solid;
margin: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
.avatar {
padding: 0;
}
.follow-box {
text-align: center;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
line-height: 1.5em;
.btn {
margin-top: 0.5em;
margin-left: auto;
width: 10em;
}
}
}
.usercard {
width: fill-available;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
.approval {
display: flex;
flex-direction: row;
flex-wrap: wrap;
button {
margin-top: 0.5em;
margin-right: 0.5em;
flex: 1 1;
max-width: 12em;
min-width: 8em;
}
}
</style>

View file

@ -222,6 +222,14 @@
overflow: hidden; overflow: hidden;
flex: 1 1 auto; flex: 1 1 auto;
margin-right: 1em; margin-right: 1em;
font-size: 15px;
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
} }
.user-screen-name { .user-screen-name {
@ -386,4 +394,24 @@
} }
} }
.usercard {
width: fill-available;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
</style> </style>

View file

@ -8,6 +8,7 @@ const UserFinder = {
methods: { methods: {
findUser (username) { findUser (username) {
this.$router.push({ name: 'user-search', query: { query: username } }) this.$router.push({ name: 'user-search', query: { query: username } })
this.$refs.userSearchInput.focus()
}, },
toggleHidden () { toggleHidden () {
this.hidden = !this.hidden this.hidden = !this.hidden

View file

@ -4,7 +4,7 @@
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" /> <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a> <a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
<template v-else> <template v-else>
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/> <input class="user-finder-input" ref="userSearchInput" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<button class="btn search-button" @click="findUser(username)"> <button class="btn search-button" @click="findUser(username)">
<i class="icon-search"/> <i class="icon-search"/>
</button> </button>

View file

@ -1,9 +1,39 @@
import { compose } from 'vue-compose'
import get from 'lodash/get'
import UserCardContent from '../user_card_content/user_card_content.vue' import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
import FollowList from '../follow_list/follow_list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more'
import withList from '../../hocs/with_list/with_list'
const FollowerList = compose(
withLoadMore({
fetch: (props, $store) => $store.dispatch('addFollowers', props.userId),
select: (props, $store) => get($store.getters.userById(props.userId), 'followers', []),
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
childPropName: 'entries',
additionalPropNames: ['userId']
}),
withList({ getEntryProps: user => ({ user }) })
)(FollowCard)
const FriendList = compose(
withLoadMore({
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
select: (props, $store) => get($store.getters.userById(props.userId), 'friends', []),
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
childPropName: 'entries',
additionalPropNames: ['userId']
}),
withList({ getEntryProps: user => ({ user }) })
)(FollowCard)
const UserProfile = { const UserProfile = {
data () {
return {
error: false
}
},
created () { created () {
this.$store.commit('clearTimeline', { timeline: 'user' }) this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.commit('clearTimeline', { timeline: 'favorites' })
@ -13,6 +43,16 @@ const UserProfile = {
this.startFetchFavorites() this.startFetchFavorites()
if (!this.user.id) { if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy) this.$store.dispatch('fetchUser', this.fetchBy)
.catch((reason) => {
const errorMessage = get(reason, 'error.error')
if (errorMessage === 'No user with such user_id') { // Known error
this.error = this.$t('user_profile.profile_does_not_exist')
} else if (errorMessage) {
this.error = errorMessage
} else {
this.error = this.$t('user_profile.profile_loading_error')
}
})
} }
}, },
destroyed () { destroyed () {
@ -105,9 +145,9 @@ const UserProfile = {
}, },
components: { components: {
UserCardContent, UserCardContent,
UserCard,
Timeline, Timeline,
FollowList FollowerList,
FriendList
} }
} }

View file

@ -18,16 +18,10 @@
:user-id="fetchBy" :user-id="fetchBy"
/> />
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count"> <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
<FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" /> <FriendList :userId="userId" />
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div> </div>
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count"> <div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
<FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" /> <FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div> </div>
<Timeline <Timeline
:label="$t('user_card.media')" :label="$t('user_card.media')"
@ -55,7 +49,8 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<i class="icon-spin3 animate-spin"></i> <span v-if="error">{{ error }}</span>
<i class="icon-spin3 animate-spin" v-else></i>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,8 +1,8 @@
import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import userSearchApi from '../../services/new_api/user_search.js' import userSearchApi from '../../services/new_api/user_search.js'
const userSearch = { const userSearch = {
components: { components: {
UserCard FollowCard
}, },
props: [ props: [
'query' 'query'
@ -10,7 +10,8 @@ const userSearch = {
data () { data () {
return { return {
username: '', username: '',
users: [] users: [],
loading: false
} }
}, },
mounted () { mounted () {
@ -24,14 +25,17 @@ const userSearch = {
methods: { methods: {
newQuery (query) { newQuery (query) {
this.$router.push({ name: 'user-search', query: { query } }) this.$router.push({ name: 'user-search', query: { query } })
this.$refs.userSearchInput.focus()
}, },
search (query) { search (query) {
if (!query) { if (!query) {
this.users = [] this.users = []
return return
} }
this.loading = true
userSearchApi.search({query, store: this.$store}) userSearchApi.search({query, store: this.$store})
.then((res) => { .then((res) => {
this.loading = false
this.users = res this.users = res
}) })
} }

View file

@ -4,13 +4,16 @@
{{$t('nav.user_search')}} {{$t('nav.user_search')}}
</div> </div>
<div class="user-search-input-container"> <div class="user-search-input-container">
<input class="user-finder-input" @keyup.enter="newQuery(username)" v-model="username" :placeholder="$t('finder.find_user')"/> <input class="user-finder-input" ref="userSearchInput" @keyup.enter="newQuery(username)" v-model="username" :placeholder="$t('finder.find_user')"/>
<button class="btn search-button" @click="newQuery(username)"> <button class="btn search-button" @click="newQuery(username)">
<i class="icon-search"/> <i class="icon-search"/>
</button> </button>
</div> </div>
<div class="panel-body"> <div v-if="loading" class="text-center loading-icon">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card> <i class="icon-spin3 animate-spin"/>
</div>
<div v-else class="panel-body">
<FollowCard v-for="user in users" :key="user.id" :user="user"/>
</div> </div>
</div> </div>
</template> </template>
@ -27,4 +30,8 @@
margin-left: 0.5em; margin-left: 0.5em;
} }
} }
.loading-icon {
padding: 1em;
}
</style> </style>

View file

@ -1,9 +1,32 @@
import { unescape } from 'lodash' import { compose } from 'vue-compose'
import unescape from 'lodash/unescape'
import get from 'lodash/get'
import TabSwitcher from '../tab_switcher/tab_switcher.js' import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue' import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue' import StyleSwitcher from '../style_switcher/style_switcher.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
import withList from '../../hocs/with_list/with_list'
const BlockList = compose(
withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'entries'
}),
withList({ getEntryProps: userId => ({ userId }) })
)(BlockCard)
const MuteList = compose(
withSubscription({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'entries'
}),
withList({ getEntryProps: userId => ({ userId }) })
)(MuteCard)
const UserSettings = { const UserSettings = {
data () { data () {
@ -38,10 +61,15 @@ const UserSettings = {
activeTab: 'profile' activeTab: 'profile'
} }
}, },
created () {
this.$store.dispatch('fetchTokens')
},
components: { components: {
StyleSwitcher, StyleSwitcher,
TabSwitcher, TabSwitcher,
ImageCropper ImageCropper,
BlockList,
MuteList
}, },
computed: { computed: {
user () { user () {
@ -63,6 +91,15 @@ const UserSettings = {
}, },
currentSaveStateNotice () { currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice return this.$store.state.interface.settings.currentSaveStateNotice
},
oauthTokens () {
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
}
})
} }
}, },
methods: { methods: {
@ -282,6 +319,11 @@ const UserSettings = {
logout () { logout () {
this.$store.dispatch('logout') this.$store.dispatch('logout')
this.$router.replace('/') this.$router.replace('/')
},
revokeToken (id) {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id)
}
} }
} }
} }

View file

@ -121,6 +121,30 @@
<p v-if="changePasswordError">{{changePasswordError}}</p> <p v-if="changePasswordError">{{changePasswordError}}</p>
</div> </div>
<div class="setting-item">
<h2>{{$t('settings.oauth_tokens')}}</h2>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{$t('settings.app_name')}}</th>
<th>{{$t('settings.valid_until')}}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
<td>{{oauthToken.appName}}</td>
<td>{{oauthToken.validUntil}}</td>
<td class="actions">
<button class="btn btn-default" @click="revokeToken(oauthToken.id)">
{{$t('settings.revoke_token')}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.delete_account')}}</h2> <h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p> <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
@ -162,6 +186,12 @@
<h2>{{$t('settings.follow_export_processing')}}</h2> <h2>{{$t('settings.follow_export_processing')}}</h2>
</div> </div>
</div> </div>
<div :label="$t('settings.blocks_tab')">
<block-list :refresh="true">
<template slot="empty">{{$t('settings.no_blocks')}}</template>
</block-list>
</div>
</tab-switcher> </tab-switcher>
</div> </div>
</div> </div>
@ -207,5 +237,17 @@
border-radius: $fallback--avatarRadius; border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius); border-radius: var(--avatarRadius, $fallback--avatarRadius);
} }
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
} }
</style> </style>

View file

@ -1,9 +1,9 @@
import apiService from '../../services/api/api.service.js' import apiService from '../../services/api/api.service.js'
import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue'
const WhoToFollow = { const WhoToFollow = {
components: { components: {
UserCard FollowCard
}, },
data () { data () {
return { return {

View file

@ -4,7 +4,7 @@
{{$t('who_to_follow.who_to_follow')}} {{$t('who_to_follow.who_to_follow')}}
</div> </div>
<div class="panel-body"> <div class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card> <FollowCard v-for="user in users" :key="user.id" :user="user"/>
</div> </div>
</div> </div>
</template> </template>

View file

@ -0,0 +1,40 @@
import Vue from 'vue'
import map from 'lodash/map'
import isEmpty from 'lodash/isEmpty'
import './with_list.scss'
const defaultEntryPropsGetter = entry => ({ entry })
const defaultKeyGetter = entry => entry.id
const withList = ({
getEntryProps = defaultEntryPropsGetter, // function to accept entry and index values and return props to be passed into the item component
getKey = defaultKeyGetter // funciton to accept entry and index values and return key prop value
}) => (ItemComponent) => (
Vue.component('withList', {
props: [
'entries', // array of entry
'entryProps', // additional props to be passed into each entry
'entryListeners' // additional event listeners to be passed into each entry
],
render (createElement) {
return (
<div class="with-list">
{map(this.entries, (entry, index) => {
const props = {
key: getKey(entry, index),
props: {
...this.$props.entryProps,
...getEntryProps(entry, index)
},
on: this.$props.entryListeners
}
return <ItemComponent {...props} />
})}
{isEmpty(this.entries) && this.$slots.empty && <div class="with-list-empty-content faint">{this.$slots.empty}</div>}
</div>
)
}
})
)
export default withList

View file

@ -0,0 +1,6 @@
.with-list {
&-empty-content {
text-align: center;
padding: 10px;
}
}

View file

@ -0,0 +1,94 @@
import Vue from 'vue'
import isEmpty from 'lodash/isEmpty'
import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_load_more.scss'
const withLoadMore = ({
fetch, // function to fetch entries and return a promise
select, // function to select data from store
destroy, // function called at "destroyed" lifecycle
childPropName = 'entries', // name of the prop to be passed into the wrapped component
additionalPropNames = [] // additional prop name list of the wrapper component
}) => (WrappedComponent) => {
const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withLoadMore', {
render (createElement) {
const props = {
props: {
...this.$props,
[childPropName]: this.entries
},
on: this.$listeners,
scopedSlots: this.$scopedSlots
}
const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value))
return (
<div class="with-load-more">
<WrappedComponent {...props}>
{children}
</WrappedComponent>
<div class="with-load-more-footer">
{this.error && <a onClick={this.fetchEntries} class="alert error">{this.$t('general.generic_error')}</a>}
{!this.error && this.loading && <i class="icon-spin3 animate-spin"/>}
{!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>}
</div>
</div>
)
},
props,
data () {
return {
loading: false,
bottomedOut: false,
error: false
}
},
computed: {
entries () {
return select(this.$props, this.$store) || []
}
},
created () {
window.addEventListener('scroll', this.scrollLoad)
if (this.entries.length === 0) {
this.fetchEntries()
}
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
destroy && destroy(this.$props, this.$store)
},
methods: {
fetchEntries () {
if (!this.loading) {
this.loading = true
this.error = false
fetch(this.$props, this.$store)
.then((newEntries) => {
this.loading = false
this.bottomedOut = isEmpty(newEntries)
})
.catch(() => {
this.loading = false
this.error = true
})
}
},
scrollLoad (e) {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
if (this.loading === false &&
this.bottomedOut === false &&
this.$el.offsetHeight > 0 &&
(window.innerHeight + window.pageYOffset) >= (height - 750)
) {
this.fetchEntries()
}
}
}
})
}
export default withLoadMore

View file

@ -0,0 +1,10 @@
.with-load-more {
&-footer {
padding: 10px;
text-align: center;
.error {
font-size: 14px;
}
}
}

View file

@ -0,0 +1,84 @@
import Vue from 'vue'
import isEmpty from 'lodash/isEmpty'
import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_subscription.scss'
const withSubscription = ({
fetch, // function to fetch entries and return a promise
select, // function to select data from store
childPropName = 'content', // name of the prop to be passed into the wrapped component
additionalPropNames = [] // additional prop name list of the wrapper component
}) => (WrappedComponent) => {
const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withSubscription', {
props: [
...props,
'refresh' // boolean saying to force-fetch data whenever created
],
render (createElement) {
if (!this.error && !this.loading) {
const props = {
props: {
...this.$props,
[childPropName]: this.fetchedData
},
on: this.$listeners,
scopedSlots: this.$scopedSlots
}
const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value))
return (
<div class="with-subscription">
<WrappedComponent {...props}>
{children}
</WrappedComponent>
</div>
)
} else {
return (
<div class="with-subscription-loading">
{this.error
? <a onClick={this.fetchData} class="alert error">{this.$t('general.generic_error')}</a>
: <i class="icon-spin3 animate-spin"/>
}
</div>
)
}
},
data () {
return {
loading: false,
error: false
}
},
computed: {
fetchedData () {
return select(this.$props, this.$store)
}
},
created () {
if (this.refresh || isEmpty(this.fetchedData)) {
this.fetchData()
}
},
methods: {
fetchData () {
if (!this.loading) {
this.loading = true
this.error = false
fetch(this.$props, this.$store)
.then(() => {
this.loading = false
})
.catch(() => {
this.error = true
this.loading = false
})
}
}
}
})
}
export default withSubscription

View file

@ -0,0 +1,10 @@
.with-subscription {
&-loading {
padding: 10px;
text-align: center;
.error {
font-size: 14px;
}
}
}

View file

@ -134,6 +134,11 @@
"notification_visibility_mentions": "الإشارات", "notification_visibility_mentions": "الإشارات",
"notification_visibility_repeats": "", "notification_visibility_repeats": "",
"nsfw_clickthrough": "", "nsfw_clickthrough": "",
"oauth_tokens": "رموز OAuth",
"token": "رمز",
"refresh_token": "رمز التحديث",
"valid_until": "صالح حتى",
"revoke_token": "سحب",
"panelRadius": "", "panelRadius": "",
"pause_on_unfocused": "", "pause_on_unfocused": "",
"presets": "النماذج", "presets": "النماذج",

View file

@ -132,6 +132,11 @@
"notification_visibility_repeats": "Republica una entrada meva", "notification_visibility_repeats": "Republica una entrada meva",
"no_rich_text_description": "Neteja el formatat de text de totes les entrades", "no_rich_text_description": "Neteja el formatat de text de totes les entrades",
"nsfw_clickthrough": "Amaga el contingut NSFW darrer d'una imatge clicable", "nsfw_clickthrough": "Amaga el contingut NSFW darrer d'una imatge clicable",
"oauth_tokens": "Llistats OAuth",
"token": "Token",
"refresh_token": "Actualitza el token",
"valid_until": "Vàlid fins",
"revoke_token": "Revocar",
"panelRadius": "Panells", "panelRadius": "Panells",
"pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus", "pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus",
"presets": "Temes", "presets": "Temes",

View file

@ -159,6 +159,11 @@
"hide_follows_description": "Zeige nicht, wem ich folge", "hide_follows_description": "Zeige nicht, wem ich folge",
"hide_followers_description": "Zeige nicht, wer mir folgt", "hide_followers_description": "Zeige nicht, wer mir folgt",
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind", "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
"oauth_tokens": "OAuth-Token",
"token": "Zeichen",
"refresh_token": "Token aktualisieren",
"valid_until": "Gültig bis",
"revoke_token": "Widerrufen",
"panelRadius": "Panel", "panelRadius": "Panel",
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist", "pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
"presets": "Voreinstellungen", "presets": "Voreinstellungen",

View file

@ -19,7 +19,8 @@
"apply": "Apply", "apply": "Apply",
"submit": "Submit", "submit": "Submit",
"more": "More", "more": "More",
"generic_error": "An error occured" "generic_error": "An error occured",
"optional": "optional"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Crop picture", "crop_picture": "Crop picture",
@ -92,6 +93,9 @@
"token": "Invite token", "token": "Invite token",
"captcha": "CAPTCHA", "captcha": "CAPTCHA",
"new_captcha": "Click the image to get a new captcha", "new_captcha": "Click the image to get a new captcha",
"username_placeholder": "e.g. lain",
"fullname_placeholder": "e.g. Lain Iwakura",
"bio_placeholder": "e.g.\nHi, I'm Lain\nIm an anime girl living in suburban Japan. You may know me from the Wired.",
"validations": { "validations": {
"username_required": "cannot be left blank", "username_required": "cannot be left blank",
"fullname_required": "cannot be left blank", "fullname_required": "cannot be left blank",
@ -102,6 +106,7 @@
} }
}, },
"settings": { "settings": {
"app_name": "App name",
"attachmentRadius": "Attachments", "attachmentRadius": "Attachments",
"attachments": "Attachments", "attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom", "autoload": "Enable automatic loading when scrolled to the bottom",
@ -110,6 +115,7 @@
"avatarRadius": "Avatars", "avatarRadius": "Avatars",
"background": "Background", "background": "Background",
"bio": "Bio", "bio": "Bio",
"blocks_tab": "Blocks",
"btnRadius": "Buttons", "btnRadius": "Buttons",
"cBlue": "Blue (Reply, follow)", "cBlue": "Blue (Reply, follow)",
"cGreen": "Green (Retweet)", "cGreen": "Green (Retweet)",
@ -144,6 +150,7 @@
"general": "General", "general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations", "hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline", "hide_attachments_in_tl": "Hide attachments in timeline",
"max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel", "hide_isp": "Hide instance-specific panel",
"preload_images": "Preload images", "preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click", "use_one_click_nsfw": "Open NSFW attachments with just one click",
@ -164,6 +171,7 @@
"lock_account_description": "Restrict your account to approved followers only", "lock_account_description": "Restrict your account to approved followers only",
"loop_video": "Loop videos", "loop_video": "Loop videos",
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"mutes_tab": "Mutes",
"play_videos_in_modal": "Play videos directly in the media viewer", "play_videos_in_modal": "Play videos directly in the media viewer",
"use_contain_fit": "Don't crop the attachment in thumbnails", "use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name", "name": "Name",
@ -175,11 +183,18 @@
"notification_visibility_mentions": "Mentions", "notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats", "notification_visibility_repeats": "Repeats",
"no_rich_text_description": "Strip rich text formatting from all posts", "no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
"hide_follows_description": "Don't show who I'm following", "hide_follows_description": "Don't show who I'm following",
"hide_followers_description": "Don't show who's following me", "hide_followers_description": "Don't show who's following me",
"show_admin_badge": "Show Admin badge in my profile", "show_admin_badge": "Show Admin badge in my profile",
"show_moderator_badge": "Show Moderator badge in my profile", "show_moderator_badge": "Show Moderator badge in my profile",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Refresh Token",
"valid_until": "Valid Until",
"revoke_token": "Revoke",
"panelRadius": "Panels", "panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused", "pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets", "presets": "Presets",
@ -345,6 +360,10 @@
"no_more_statuses": "No more statuses", "no_more_statuses": "No more statuses",
"no_statuses": "No statuses" "no_statuses": "No statuses"
}, },
"status": {
"reply_to": "Reply to",
"replies_list": "Replies:"
},
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",
"block": "Block", "block": "Block",
@ -366,10 +385,18 @@
"muted": "Muted", "muted": "Muted",
"per_day": "per day", "per_day": "per day",
"remote_follow": "Remote follow", "remote_follow": "Remote follow",
"statuses": "Statuses" "statuses": "Statuses",
"unblock": "Unblock",
"unblock_progress": "Unblocking...",
"block_progress": "Blocking...",
"unmute": "Unmute",
"unmute_progress": "Unmuting...",
"mute_progress": "Muting..."
}, },
"user_profile": { "user_profile": {
"timeline_title": "User Timeline" "timeline_title": "User Timeline",
"profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile."
}, },
"who_to_follow": { "who_to_follow": {
"more": "More", "more": "More",

View file

@ -171,6 +171,11 @@
"show_admin_badge": "Mostrar la placa de administrador en mi perfil", "show_admin_badge": "Mostrar la placa de administrador en mi perfil",
"show_moderator_badge": "Mostrar la placa de moderador en mi perfil", "show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
"oauth_tokens": "Tokens de OAuth",
"token": "Token",
"refresh_token": "Actualizar el token",
"valid_until": "Válido hasta",
"revoke_token": "Revocar",
"panelRadius": "Paneles", "panelRadius": "Paneles",
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.", "pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
"presets": "Por defecto", "presets": "Por defecto",

View file

@ -133,6 +133,7 @@
"general": "Yleinen", "general": "Yleinen",
"hide_attachments_in_convo": "Piilota liitteet keskusteluissa", "hide_attachments_in_convo": "Piilota liitteet keskusteluissa",
"hide_attachments_in_tl": "Piilota liitteet aikajanalla", "hide_attachments_in_tl": "Piilota liitteet aikajanalla",
"max_thumbnails": "Suurin sallittu määrä liitteitä esikatselussa",
"hide_isp": "Piilota palvelimenkohtainen ruutu", "hide_isp": "Piilota palvelimenkohtainen ruutu",
"preload_images": "Esilataa kuvat", "preload_images": "Esilataa kuvat",
"use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella", "use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella",
@ -165,6 +166,11 @@
"no_rich_text_description": "Älä näytä tekstin muotoilua.", "no_rich_text_description": "Älä näytä tekstin muotoilua.",
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani", "hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse", "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
"oauth_tokens": "OAuth-merkit",
"token": "Token",
"refresh_token": "Päivitä token",
"valid_until": "Voimassa asti",
"revoke_token": "Peruuttaa",
"panelRadius": "Ruudut", "panelRadius": "Ruudut",
"pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta", "pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
"presets": "Valmiit teemat", "presets": "Valmiit teemat",
@ -215,6 +221,10 @@
"up_to_date": "Ajantasalla", "up_to_date": "Ajantasalla",
"no_more_statuses": "Ei enempää viestejä" "no_more_statuses": "Ei enempää viestejä"
}, },
"status": {
"reply_to": "Vastaus",
"replies_list": "Vastaukset:"
},
"user_card": { "user_card": {
"approve": "Hyväksy", "approve": "Hyväksy",
"block": "Estä", "block": "Estä",

View file

@ -137,6 +137,11 @@
"notification_visibility_mentions": "Mentionnés", "notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages", "notification_visibility_repeats": "Partages",
"nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible", "nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible",
"oauth_tokens": "Jetons OAuth",
"token": "Jeton",
"refresh_token": "Refresh Token",
"valid_until": "Valable jusque",
"revoke_token": "Révoquer",
"panelRadius": "Fenêtres", "panelRadius": "Fenêtres",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré", "pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
"presets": "Thèmes prédéfinis", "presets": "Thèmes prédéfinis",

View file

@ -134,6 +134,11 @@
"notification_visibility_repeats": "Atphostáil", "notification_visibility_repeats": "Atphostáil",
"no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post", "no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post",
"nsfw_clickthrough": "Cumasaigh an ceangaltán NSFW cliceáil ar an gcnaipe", "nsfw_clickthrough": "Cumasaigh an ceangaltán NSFW cliceáil ar an gcnaipe",
"oauth_tokens": "Tocanna OAuth",
"token": "Token",
"refresh_token": "Athnuachan Comórtas",
"valid_until": "Bailí Go dtí",
"revoke_token": "Athghairm",
"panelRadius": "Painéil", "panelRadius": "Painéil",
"pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte", "pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte",
"presets": "Réamhshocruithe", "presets": "Réamhshocruithe",

View file

@ -129,6 +129,11 @@
"notification_visibility_mentions": "אזכורים", "notification_visibility_mentions": "אזכורים",
"notification_visibility_repeats": "חזרות", "notification_visibility_repeats": "חזרות",
"nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר", "nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר",
"oauth_tokens": "אסימוני OAuth",
"token": "אסימון",
"refresh_token": "רענון האסימון",
"valid_until": "בתוקף עד",
"revoke_token": "בטל",
"panelRadius": "פאנלים", "panelRadius": "פאנלים",
"pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס", "pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס",
"presets": "ערכים קבועים מראש", "presets": "ערכים קבועים מראש",

View file

@ -93,6 +93,11 @@
"notification_visibility_mentions": "Menzioni", "notification_visibility_mentions": "Menzioni",
"notification_visibility_repeats": "Condivisioni", "notification_visibility_repeats": "Condivisioni",
"no_rich_text_description": "Togli la formattazione del testo da tutti i post", "no_rich_text_description": "Togli la formattazione del testo da tutti i post",
"oauth_tokens": "Token OAuth",
"token": "Token",
"refresh_token": "Aggiorna token",
"valid_until": "Valido fino a",
"revoke_token": "Revocare",
"panelRadius": "Pannelli", "panelRadius": "Pannelli",
"pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano", "pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano",
"presets": "Valori predefiniti", "presets": "Valori predefiniti",

View file

@ -171,6 +171,11 @@
"show_admin_badge": "アドミンのしるしをみる", "show_admin_badge": "アドミンのしるしをみる",
"show_moderator_badge": "モデレーターのしるしをみる", "show_moderator_badge": "モデレーターのしるしをみる",
"nsfw_clickthrough": "NSFWなファイルをかくす", "nsfw_clickthrough": "NSFWなファイルをかくす",
"oauth_tokens": "OAuthトークン",
"token": "トークン",
"refresh_token": "トークンを更新",
"valid_until": "まで有効",
"revoke_token": "取り消す",
"panelRadius": "パネル", "panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
"presets": "プリセット", "presets": "プリセット",

View file

@ -159,6 +159,11 @@
"hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음", "hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
"hide_followers_description": "나를 따르는 사람을 보여주지 마라.", "hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
"oauth_tokens": "OAuth 토큰",
"token": "토큰",
"refresh_token": "토큰 새로 고침",
"valid_until": "까지 유효하다",
"revoke_token": "취소",
"panelRadius": "패널", "panelRadius": "패널",
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기", "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
"presets": "프리셋", "presets": "프리셋",

View file

@ -132,6 +132,11 @@
"notification_visibility_repeats": "Gjentakelser", "notification_visibility_repeats": "Gjentakelser",
"no_rich_text_description": "Fjern all formatering fra statuser", "no_rich_text_description": "Fjern all formatering fra statuser",
"nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende", "nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende",
"oauth_tokens": "OAuth Tokens",
"token": "Pollett",
"refresh_token": "Refresh Token",
"valid_until": "Gyldig til",
"revoke_token": "Tilbakekall",
"panelRadius": "Panel", "panelRadius": "Panel",
"pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus", "pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus",
"presets": "Forhåndsdefinerte tema", "presets": "Forhåndsdefinerte tema",

View file

@ -159,6 +159,11 @@
"no_rich_text_description": "Strip rich text formattering van alle posts", "no_rich_text_description": "Strip rich text formattering van alle posts",
"hide_network_description": "Toon niet wie mij volgt en wie ik volg.", "hide_network_description": "Toon niet wie mij volgt en wie ik volg.",
"nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in", "nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in",
"oauth_tokens": "OAuth-tokens",
"token": "Token",
"refresh_token": "Token vernieuwen",
"valid_until": "Geldig tot",
"revoke_token": "Intrekken",
"panelRadius": "Panelen", "panelRadius": "Panelen",
"pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is", "pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is",
"presets": "Presets", "presets": "Presets",

View file

@ -142,6 +142,7 @@
"notification_visibility_mentions": "Mencions", "notification_visibility_mentions": "Mencions",
"notification_visibility_repeats": "Repeticions", "notification_visibility_repeats": "Repeticions",
"no_rich_text_description": "Netejar lo format tèxte de totas las publicacions", "no_rich_text_description": "Netejar lo format tèxte de totas las publicacions",
"oauth_tokens": "Llistats OAuth",
"pause_on_unfocused": "Pausar la difusion quand longlet es pas seleccionat", "pause_on_unfocused": "Pausar la difusion quand longlet es pas seleccionat",
"profile_tab": "Perfil", "profile_tab": "Perfil",
"replies_in_timeline": "Responsas del flux", "replies_in_timeline": "Responsas del flux",

View file

@ -86,6 +86,11 @@
"name_bio": "Imię i bio", "name_bio": "Imię i bio",
"new_password": "Nowe hasło", "new_password": "Nowe hasło",
"nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)", "nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
"oauth_tokens": "Tokeny OAuth",
"token": "Token",
"refresh_token": "Odśwież token",
"valid_until": "Ważne do",
"revoke_token": "Odwołać",
"panelRadius": "Panele", "panelRadius": "Panele",
"presets": "Gotowe motywy", "presets": "Gotowe motywy",
"profile_background": "Tło profilu", "profile_background": "Tło profilu",

View file

@ -132,6 +132,11 @@
"show_admin_badge": "Показывать значок администратора в моем профиле", "show_admin_badge": "Показывать значок администратора в моем профиле",
"show_moderator_badge": "Показывать значок модератора в моем профиле", "show_moderator_badge": "Показывать значок модератора в моем профиле",
"nsfw_clickthrough": "Включить скрытие NSFW вложений", "nsfw_clickthrough": "Включить скрытие NSFW вложений",
"oauth_tokens": "OAuth токены",
"token": "Токен",
"refresh_token": "Рефреш токен",
"valid_until": "Годен до",
"revoke_token": "Удалить",
"panelRadius": "Панели", "panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
"presets": "Пресеты", "presets": "Пресеты",

View file

@ -134,6 +134,11 @@
"notification_visibility_repeats": "转发", "notification_visibility_repeats": "转发",
"no_rich_text_description": "不显示富文本格式", "no_rich_text_description": "不显示富文本格式",
"nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开", "nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开",
"oauth_tokens": "OAuth令牌",
"token": "代币",
"refresh_token": "刷新令牌",
"valid_until": "有效期至",
"revoke_token": "撤消",
"panelRadius": "面板", "panelRadius": "面板",
"pause_on_unfocused": "在离开页面时暂停时间线推送", "pause_on_unfocused": "在离开页面时暂停时间线推送",
"presets": "预置", "presets": "预置",

View file

@ -11,6 +11,7 @@ import configModule from './modules/config.js'
import chatModule from './modules/chat.js' import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js' import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js' import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -64,7 +65,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
config: configModule, config: configModule,
chat: chatModule, chat: chatModule,
oauth: oauthModule, oauth: oauthModule,
mediaViewer: mediaViewerModule mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule
}, },
plugins: [persistedState, pushNotifications], plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.

View file

@ -8,6 +8,7 @@ const defaultState = {
collapseMessageWithSubject: undefined, // instance default collapseMessageWithSubject: undefined, // instance default
hideAttachments: false, hideAttachments: false,
hideAttachmentsInConv: false, hideAttachmentsInConv: false,
maxThumbnails: 16,
hideNsfw: true, hideNsfw: true,
preloadImage: true, preloadImage: true,
loopVideo: true, loopVideo: true,

View file

@ -0,0 +1,26 @@
const oauthTokens = {
state: {
tokens: []
},
actions: {
fetchTokens ({rootState, commit}) {
rootState.api.backendInteractor.fetchOAuthTokens().then((tokens) => {
commit('swapTokens', tokens)
})
},
revokeToken ({rootState, commit, state}, id) {
rootState.api.backendInteractor.revokeOAuthToken(id).then((response) => {
if (response.status === 201) {
commit('swapTokens', state.tokens.filter(token => token.id !== id))
}
})
}
},
mutations: {
swapTokens (state, tokens) {
state.tokens = tokens
}
}
}
export default oauthTokens

View file

@ -126,7 +126,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
// This makes sure that user timeline won't get data meant for other // This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could // user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile // return data late after user already viewing different user profile
if (timeline === 'user' && timelineObject.userId !== userId) { if ((timeline === 'user' || timeline === 'media') && timelineObject.userId !== userId) {
return return
} }
@ -303,6 +303,8 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
setTimeout(notification.close.bind(notification), 5000) setTimeout(notification.close.bind(notification), 5000)
} }
} }
} else if (notification.seen) {
state.notifications.idStore[notification.id].seen = true
} }
}) })
} }

View file

@ -72,19 +72,31 @@ export const mutations = {
}, },
// Because frontend doesn't have a reason to keep these stuff in memory // Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile. // outside of viewing someones user profile.
clearFriendsAndFollowers (state, userKey) { clearFriends (state, userId) {
const user = state.usersObject[userKey] const user = state.usersObject[userId]
if (!user) { if (!user) {
return return
} }
user.friends = [] user.friends = []
user.followers = []
user.friendsPage = 0 user.friendsPage = 0
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
if (!user) {
return
}
user.followers = []
user.followersPage = 0 user.followersPage = 0
}, },
addNewUsers (state, users) { addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user)) each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
}, },
saveBlocks (state, blockIds) {
state.currentUser.blockIds = blockIds
},
saveMutes (state, muteIds) {
state.currentUser.muteIds = muteIds
},
setUserForStatus (state, status) { setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id] status.user = state.usersObject[status.user.id]
}, },
@ -134,7 +146,39 @@ const users = {
getters, getters,
actions: { actions: {
fetchUser (store, id) { fetchUser (store, id) {
store.rootState.api.backendInteractor.fetchUser({ id }) return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', [user]))
},
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
.then((blocks) => {
store.commit('saveBlocks', map(blocks, 'id'))
store.commit('addNewUsers', blocks)
return blocks
})
},
blockUser (store, id) {
return store.rootState.api.backendInteractor.blockUser(id)
.then((user) => store.commit('addNewUsers', [user]))
},
unblockUser (store, id) {
return store.rootState.api.backendInteractor.unblockUser(id)
.then((user) => store.commit('addNewUsers', [user]))
},
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
.then((mutedUsers) => {
each(mutedUsers, (user) => { user.muted = true })
store.commit('addNewUsers', mutedUsers)
store.commit('saveMutes', map(mutedUsers, 'id'))
})
},
muteUser (store, id) {
return store.state.api.backendInteractor.setUserMute({ id, muted: true })
.then((user) => store.commit('addNewUsers', [user]))
},
unmuteUser (store, id) {
return store.state.api.backendInteractor.setUserMute({ id, muted: false })
.then((user) => store.commit('addNewUsers', [user])) .then((user) => store.commit('addNewUsers', [user]))
}, },
addFriends ({ rootState, commit }, fetchBy) { addFriends ({ rootState, commit }, fetchBy) {
@ -151,20 +195,19 @@ const users = {
}) })
}, },
addFollowers ({ rootState, commit }, fetchBy) { addFollowers ({ rootState, commit }, fetchBy) {
return new Promise((resolve, reject) => { const user = rootState.users.usersObject[fetchBy]
const user = rootState.users.usersObject[fetchBy] const page = user.followersPage || 1
const page = user.followersPage || 1 return rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
rootState.api.backendInteractor.fetchFollowers({ id: user.id, page }) .then((followers) => {
.then((followers) => { commit('addFollowers', { id: user.id, followers, page })
commit('addFollowers', { id: user.id, followers, page }) return followers
resolve(followers) })
}).catch(() => {
reject()
})
})
}, },
clearFriendsAndFollowers ({ commit }, userKey) { clearFriends ({ commit }, userId) {
commit('clearFriendsAndFollowers', userKey) commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
}, },
registerPushNotifications (store) { registerPushNotifications (store) {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials
@ -263,6 +306,8 @@ const users = {
const user = data const user = data
// user.credentials = userCredentials // user.credentials = userCredentials
user.credentials = accessToken user.credentials = accessToken
user.blockIds = []
user.muteIds = []
commit('setCurrentUser', user) commit('setCurrentUser', user)
commit('addNewUsers', [user]) commit('addNewUsers', [user])
@ -279,11 +324,8 @@ const users = {
// Start getting fresh posts. // Start getting fresh posts.
store.dispatch('startFetching', { timeline: 'friends' }) store.dispatch('startFetching', { timeline: 'friends' })
// Get user mutes and follower info // Get user mutes
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => { store.dispatch('fetchMutes')
each(mutedUsers, (user) => { user.muted = true })
store.commit('addNewUsers', mutedUsers)
})
// Fetch our friends // Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) store.rootState.api.backendInteractor.fetchFriends({ id: user.id })

View file

@ -18,6 +18,7 @@ const MENTIONS_URL = '/api/statuses/mentions.json'
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json' const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
const FOLLOWERS_URL = '/api/statuses/followers.json' const FOLLOWERS_URL = '/api/statuses/followers.json'
const FRIENDS_URL = '/api/statuses/friends.json' const FRIENDS_URL = '/api/statuses/friends.json'
const BLOCKS_URL = '/api/statuses/blocks.json'
const FOLLOWING_URL = '/api/friendships/create.json' const FOLLOWING_URL = '/api/friendships/create.json'
const UNFOLLOWING_URL = '/api/friendships/destroy.json' const UNFOLLOWING_URL = '/api/friendships/destroy.json'
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json' const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
@ -46,6 +47,7 @@ const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
import { each, map } from 'lodash' import { each, map } from 'lodash'
import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch' import 'whatwg-fetch'
import { StatusCodeError } from '../errors/errors'
const oldfetch = window.fetch const oldfetch = window.fetch
@ -243,7 +245,15 @@ const denyUser = ({id, credentials}) => {
const fetchUser = ({id, credentials}) => { const fetchUser = ({id, credentials}) => {
let url = `${USER_URL}?user_id=${id}` let url = `${USER_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json()) .then((response) => {
return new Promise((resolve, reject) => response.json()
.then((json) => {
if (!response.ok) {
return reject(new StatusCodeError(response.status, json, { url }, response))
}
return resolve(json)
}))
})
.then((data) => parseUser(data)) .then((data) => parseUser(data))
} }
@ -518,6 +528,34 @@ const fetchMutes = ({credentials}) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
const fetchBlocks = ({page, credentials}) => {
return fetch(BLOCKS_URL, {
headers: authHeaders(credentials)
}).then((data) => {
if (data.ok) {
return data.json()
}
throw new Error('Error fetching blocks', data)
})
}
const fetchOAuthTokens = ({credentials}) => {
const url = '/api/oauth_tokens.json'
return fetch(url, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const revokeOAuthToken = ({id, credentials}) => {
const url = `/api/oauth_tokens/${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'DELETE'
})
}
const suggestions = ({credentials}) => { const suggestions = ({credentials}) => {
return fetch(SUGGESTIONS_URL, { return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials) headers: authHeaders(credentials)
@ -559,6 +597,9 @@ const apiService = {
fetchAllFollowing, fetchAllFollowing,
setUserMute, setUserMute,
fetchMutes, fetchMutes,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -54,8 +54,8 @@ const backendInteractorService = (credentials) => {
return apiService.denyUser({credentials, id}) return apiService.denyUser({credentials, id})
} }
const startFetching = ({timeline, store, userId = false}) => { const startFetching = ({timeline, store, userId = false, tag}) => {
return timelineFetcherService.startFetching({timeline, store, credentials, userId}) return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag})
} }
const setUserMute = ({id, muted = true}) => { const setUserMute = ({id, muted = true}) => {
@ -63,7 +63,10 @@ const backendInteractorService = (credentials) => {
} }
const fetchMutes = () => apiService.fetchMutes({credentials}) const fetchMutes = () => apiService.fetchMutes({credentials})
const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials}) const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
const getCaptcha = () => apiService.getCaptcha() const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register(params) const register = (params) => apiService.register(params)
@ -94,6 +97,9 @@ const backendInteractorService = (credentials) => {
startFetching, startFetching,
setUserMute, setUserMute,
fetchMutes, fetchMutes,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -0,0 +1,10 @@
import isFunction from 'lodash/isFunction'
const getComponentOptions = (Component) => (isFunction(Component)) ? Component.options : Component
const getComponentProps = (Component) => getComponentOptions(Component).props
export {
getComponentOptions,
getComponentProps
}

View file

@ -0,0 +1,14 @@
export function StatusCodeError (statusCode, body, options, response) {
this.name = 'StatusCodeError'
this.statusCode = statusCode
this.message = statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body)
this.error = body // legacy attribute
this.options = options
this.response = response
if (Error.captureStackTrace) { // required for non-V8 environments
Error.captureStackTrace(this)
}
}
StatusCodeError.prototype = Object.create(Error.prototype)
StatusCodeError.prototype.constructor = StatusCodeError

View file

@ -0,0 +1,21 @@
import apiService from '../api/api.service.js'
const fetchAndUpdate = ({ store, credentials }) => {
return apiService.fetchFollowRequests({ credentials })
.then((requests) => {
store.commit('setFollowRequests', requests)
}, () => {})
.catch(() => {})
}
const startFetching = ({credentials, store}) => {
fetchAndUpdate({ credentials, store })
const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
return setInterval(boundFetchAndUpdate, 10000)
}
const followRequestFetcher = {
startFetching
}
export default followRequestFetcher

View file

@ -16,7 +16,17 @@ const fetchAndUpdate = ({store, credentials, older = false}) => {
args['until'] = timelineData.minId args['until'] = timelineData.minId
} }
} else { } else {
args['since'] = timelineData.maxId // load unread notifications repeadedly to provide consistency between browser tabs
const notifications = timelineData.data
const unread = notifications.filter(n => !n.seen).map(n => n.id)
if (!unread.length) {
args['since'] = timelineData.maxId
} else {
args['since'] = Math.min(...unread) - 1
if (timelineData.maxId !== Math.max(...unread)) {
args['until'] = Math.max(...unread, args['since'] + 20)
}
}
} }
args['timeline'] = 'notifications' args['timeline'] = 'notifications'

View file

@ -38,11 +38,7 @@
dom-event-types "^1.0.0" dom-event-types "^1.0.0"
lodash "^4.17.4" lodash "^4.17.4"
abbrev@1: abbrev@1, abbrev@1.0.x:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
abbrev@1.0.x:
version "1.0.9" version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
@ -1385,14 +1381,10 @@ color-convert@^1.3.0, color-convert@^1.9.0:
dependencies: dependencies:
color-name "1.1.3" color-name "1.1.3"
color-name@1.1.3: color-name@1.1.3, color-name@^1.0.0:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
color-name@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
color-string@^0.3.0: color-string@^0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
@ -2431,14 +2423,10 @@ extract-zip@^1.6.5, extract-zip@^1.6.7:
mkdirp "0.5.1" mkdirp "0.5.1"
yauzl "2.4.1" yauzl "2.4.1"
extsprintf@1.3.0: extsprintf@1.3.0, extsprintf@^1.2.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
fast-deep-equal@^2.0.1: fast-deep-equal@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
@ -3024,18 +3012,12 @@ https-proxy-agent@1:
debug "2" debug "2"
extend "3" extend "3"
iconv-lite@0.4.23: iconv-lite@0.4.23, iconv-lite@^0.4.4:
version "0.4.23" version "0.4.23"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3" safer-buffer ">= 2.1.2 < 3"
iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
dependencies:
safer-buffer ">= 2.1.2 < 3"
icss-replace-symbols@^1.1.0: icss-replace-symbols@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@ -3480,11 +3462,7 @@ js-beautify@^1.6.3:
mkdirp "~0.5.0" mkdirp "~0.5.0"
nopt "~4.0.1" nopt "~4.0.1"
"js-tokens@^3.0.0 || ^4.0.0": "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
js-tokens@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
@ -4237,7 +4215,7 @@ minimatch@3.0.3:
dependencies: dependencies:
brace-expansion "^1.0.0" brace-expansion "^1.0.0"
minimist@0.0.8: minimist@0.0.8, minimist@~0.0.1:
version "0.0.8" version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@ -4245,10 +4223,6 @@ minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
minipass@^2.2.1, minipass@^2.3.4: minipass@^2.2.1, minipass@^2.3.4:
version "2.3.5" version "2.3.5"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
@ -4679,7 +4653,7 @@ os-locale@^1.4.0:
dependencies: dependencies:
lcid "^1.0.0" lcid "^1.0.0"
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@ -4787,10 +4761,6 @@ path-is-inside@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
path-parse@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
path-to-regexp@0.1.7: path-to-regexp@0.1.7:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
@ -5224,14 +5194,10 @@ punycode@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
q@1.4.1: q@1.4.1, q@^1.1.2:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
qjobs@^1.1.4: qjobs@^1.1.4:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
@ -5546,16 +5512,10 @@ resolve-url@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
resolve@1.1.x: resolve@1.1.x, resolve@^1.1.6:
version "1.1.7" version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
resolve@^1.1.6:
version "1.9.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.9.0.tgz#a14c6fdfa8f92a7df1d996cb7105fa744658ea06"
dependencies:
path-parse "^1.0.6"
restore-cursor@^1.0.1: restore-cursor@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
@ -5607,14 +5567,10 @@ safe-regex@^1.1.0:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
samsam@1.1.2: samsam@1.1.2, samsam@~1.1:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
samsam@~1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621"
sanitize-html@^1.13.0: sanitize-html@^1.13.0:
version "1.20.0" version "1.20.0"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.0.tgz#9a602beb1c9faf960fb31f9890f61911cc4d9156" resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.0.tgz#9a602beb1c9faf960fb31f9890f61911cc4d9156"
@ -5988,18 +5944,14 @@ static-extend@^0.1.1:
define-property "^0.2.5" define-property "^0.2.5"
object-copy "^0.1.0" object-copy "^0.1.0"
"statuses@>= 1.4.0 < 2": "statuses@>= 1.4.0 < 2", statuses@~1.4.0:
version "1.5.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
statuses@~1.3.1: statuses@~1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
statuses@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
stream-browserify@^2.0.1: stream-browserify@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
@ -6088,7 +6040,7 @@ strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
supports-color@3.1.2: supports-color@3.1.2, supports-color@^3.1.0:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
dependencies: dependencies:
@ -6098,7 +6050,7 @@ supports-color@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
supports-color@^3.1.0, supports-color@^3.2.3: supports-color@^3.2.3:
version "3.2.3" version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
dependencies: dependencies:
@ -6196,18 +6148,12 @@ timers-browserify@^2.0.2:
dependencies: dependencies:
setimmediate "^1.0.4" setimmediate "^1.0.4"
tmp@0.0.31: tmp@0.0.31, tmp@0.0.x:
version "0.0.31" version "0.0.31"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
dependencies: dependencies:
os-tmpdir "~1.0.1" os-tmpdir "~1.0.1"
tmp@0.0.x:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
dependencies:
os-tmpdir "~1.0.2"
to-array@0.1.4: to-array@0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
@ -6476,6 +6422,16 @@ vue-chat-scroll@^1.2.1:
version "1.3.5" version "1.3.5"
resolved "https://registry.yarnpkg.com/vue-chat-scroll/-/vue-chat-scroll-1.3.5.tgz#a5ee5bae5058f614818a96eac5ee3be4394a2f68" resolved "https://registry.yarnpkg.com/vue-chat-scroll/-/vue-chat-scroll-1.3.5.tgz#a5ee5bae5058f614818a96eac5ee3be4394a2f68"
vue-compose@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/vue-compose/-/vue-compose-0.7.1.tgz#1c11c4cd5e2c8f2743b03fce8ab43d78aabc20b3"
dependencies:
vue-hoc "0.x.x"
vue-hoc@0.x.x:
version "0.4.7"
resolved "https://registry.yarnpkg.com/vue-hoc/-/vue-hoc-0.4.7.tgz#4d3322ba89b8b0e42b19045ef536c21d948a4fac"
vue-hot-reload-api@^2.0.11: vue-hot-reload-api@^2.0.11:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.1.tgz#b2d3d95402a811602380783ea4f566eb875569a2" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.1.tgz#b2d3d95402a811602380783ea4f566eb875569a2"