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:
commit
a129ef2e07
78 changed files with 1402 additions and 606 deletions
|
@ -28,6 +28,7 @@
|
|||
"sass-loader": "^4.0.2",
|
||||
"vue": "^2.5.13",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-compose": "^0.7.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.4",
|
||||
|
|
26
src/App.scss
26
src/App.scss
|
@ -628,6 +628,16 @@ nav {
|
|||
color: $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) {
|
||||
.logo {
|
||||
opacity: 1 !important;
|
||||
|
@ -661,6 +671,10 @@ nav {
|
|||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
@keyframes shakeError {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
|
@ -705,16 +719,6 @@ nav {
|
|||
margin: 0.5em 0 0.5em 0;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.status .status-actions {
|
||||
div {
|
||||
max-width: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
display: block;
|
||||
margin-right: 0.8em;
|
||||
|
@ -723,7 +727,7 @@ nav {
|
|||
|
||||
.login-hint {
|
||||
text-align: center;
|
||||
|
||||
|
||||
@media all and (min-width: 801px) {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
|
||||
.attachment {
|
||||
position: relative;
|
||||
margin: 0.5em 0.5em 0em 0em;
|
||||
margin-top: 0.5em;
|
||||
align-self: flex-start;
|
||||
line-height: 0;
|
||||
|
||||
|
|
28
src/components/basic_user_card/basic_user_card.js
Normal file
28
src/components/basic_user_card/basic_user_card.js
Normal 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
|
79
src/components/basic_user_card/basic_user_card.vue
Normal file
79
src/components/basic_user_card/basic_user_card.vue
Normal 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>
|
37
src/components/block_card/block_card.js
Normal file
37
src/components/block_card/block_card.js
Normal 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
|
34
src/components/block_card/block_card.vue
Normal file
34
src/components/block_card/block_card.vue
Normal 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>
|
45
src/components/follow_card/follow_card.js
Normal file
45
src/components/follow_card/follow_card.js
Normal 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
|
53
src/components/follow_card/follow_card.vue
Normal file
53
src/components/follow_card/follow_card.vue
Normal 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>
|
|
@ -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
|
|
@ -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>
|
20
src/components/follow_request_card/follow_request_card.js
Normal file
20
src/components/follow_request_card/follow_request_card.js
Normal 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
|
29
src/components/follow_request_card/follow_request_card.vue
Normal file
29
src/components/follow_request_card/follow_request_card.vue
Normal 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>
|
|
@ -1,22 +1,13 @@
|
|||
import UserCard from '../user_card/user_card.vue'
|
||||
import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
|
||||
|
||||
const FollowRequests = {
|
||||
components: {
|
||||
UserCard
|
||||
},
|
||||
created () {
|
||||
this.updateRequests()
|
||||
FollowRequestCard
|
||||
},
|
||||
computed: {
|
||||
requests () {
|
||||
return this.$store.state.api.followRequests
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateRequests () {
|
||||
this.$store.state.api.backendInteractor.fetchFollowRequests()
|
||||
.then((requests) => { this.$store.commit('setFollowRequests', requests) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{$t('nav.friend_requests')}}
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
box-sizing: border-box;
|
||||
// to make failed images a bit more noticeable on chromium
|
||||
min-width: 2em;
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.image-attachment {
|
||||
|
|
|
@ -24,10 +24,6 @@
|
|||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
// TODO: clean up the random margins in attachments, this makes preview line
|
||||
// up with attachments...
|
||||
margin-right: 0.5em;
|
||||
|
||||
.card-image {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
|
|
37
src/components/mute_card/mute_card.js
Normal file
37
src/components/mute_card/mute_card.js
Normal 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
|
24
src/components/mute_card/mute_card.vue
Normal file
24
src/components/mute_card/mute_card.vue
Normal 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>
|
|
@ -1,10 +1,23 @@
|
|||
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
|
||||
|
||||
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: {
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
chat () {
|
||||
return this.$store.state.chat.channel
|
||||
},
|
||||
followRequestCount () {
|
||||
return this.$store.state.api.followRequests.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<li v-if='currentUser && currentUser.locked'>
|
||||
<router-link :to="{ name: 'friend-requests' }">
|
||||
{{ $t("nav.friend_requests")}}
|
||||
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count">
|
||||
{{currentUser.follow_request_count}}
|
||||
<span v-if='followRequestCount > 0' class="badge follow-request-count">
|
||||
{{followRequestCount}}
|
||||
</span>
|
||||
</router-link>
|
||||
</li>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class='text-fields'>
|
||||
<div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
|
||||
<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 class="form-error" v-if="$v.user.username.$dirty">
|
||||
<ul>
|
||||
|
@ -21,7 +21,7 @@
|
|||
|
||||
<div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
|
||||
<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 class="form-error" v-if="$v.user.fullname.$dirty">
|
||||
<ul>
|
||||
|
@ -44,8 +44,8 @@
|
|||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<label class='form--label' for='bio'>{{$t('registration.bio')}}</label>
|
||||
<input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'>
|
||||
<label class='form--label' for='bio'>{{$t('registration.bio')}} ({{$t('general.optional')}})</label>
|
||||
<textarea :disabled="isPending" v-model='user.bio' class='form-control' id='bio' :placeholder="$t('registration.bio_placeholder')"></textarea>
|
||||
</div>
|
||||
|
||||
<div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
|
||||
|
@ -139,6 +139,10 @@ $validations-cRed: #f04124;
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -12,6 +12,7 @@ const settings = {
|
|||
return {
|
||||
hideAttachmentsLocal: user.hideAttachments,
|
||||
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
|
||||
maxThumbnails: user.maxThumbnails,
|
||||
hideNsfwLocal: user.hideNsfw,
|
||||
useOneClickNsfw: user.useOneClickNsfw,
|
||||
hideISPLocal: user.hideISP,
|
||||
|
@ -186,6 +187,10 @@ const settings = {
|
|||
},
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,6 +136,10 @@
|
|||
<input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
|
||||
<label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
|
||||
</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>
|
||||
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
|
||||
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
|
||||
|
@ -146,7 +150,7 @@
|
|||
<label for="preloadImage">{{$t('settings.preload_images')}}</label>
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -316,6 +320,10 @@
|
|||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6em;
|
||||
}
|
||||
}
|
||||
.select-multiple {
|
||||
display: flex;
|
||||
|
|
|
@ -32,6 +32,9 @@ const SideDrawer = {
|
|||
},
|
||||
sitename () {
|
||||
return this.$store.state.instance.name
|
||||
},
|
||||
followRequestCount () {
|
||||
return this.$store.state.api.followRequests.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -45,8 +45,8 @@
|
|||
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
|
||||
<router-link to='/friend-requests'>
|
||||
{{ $t("nav.friend_requests") }}
|
||||
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count">
|
||||
{{currentUser.follow_request_count}}
|
||||
<span v-if='followRequestCount > 0' class="badge follow-request-count">
|
||||
{{followRequestCount}}
|
||||
</span>
|
||||
|
||||
</router-link>
|
||||
|
|
|
@ -23,7 +23,7 @@ const Status = {
|
|||
'highlight',
|
||||
'compact',
|
||||
'replies',
|
||||
'noReplyLinks',
|
||||
'isPreview',
|
||||
'noHeading',
|
||||
'inlineExpanded'
|
||||
],
|
||||
|
@ -40,8 +40,7 @@ const Status = {
|
|||
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
|
||||
? !this.$store.state.instance.collapseMessageWithSubject
|
||||
: !this.$store.state.config.collapseMessageWithSubject,
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
|
||||
maxAttachments: 9
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -225,7 +224,7 @@ const Status = {
|
|||
attachmentSize () {
|
||||
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
|
||||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) ||
|
||||
(this.status.attachments.length > this.maxAttachments)) {
|
||||
(this.status.attachments.length > this.maxThumbnails)) {
|
||||
return 'hide'
|
||||
} else if (this.compact) {
|
||||
return 'small'
|
||||
|
@ -249,6 +248,9 @@ const Status = {
|
|||
return this.status.attachments.filter(
|
||||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
maxThumbnails () {
|
||||
return this.$store.state.config.maxThumbnails
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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">
|
||||
<small>
|
||||
<router-link :to="userProfileLink">
|
||||
|
@ -13,7 +13,7 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<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">
|
||||
<span class="user-name">
|
||||
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
|
||||
|
@ -31,57 +31,69 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div v-if="!noHeading" class="media-body container media-heading">
|
||||
<div class="media-heading-left">
|
||||
<div class="name-and-links">
|
||||
<div v-if="!noHeading" class="media-heading">
|
||||
<div class="heading-name-row">
|
||||
<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-else>{{status.user.name}}</h4>
|
||||
<span class="links">
|
||||
<router-link :to="userProfileLink">
|
||||
{{status.user.screen_name}}
|
||||
</router-link>
|
||||
<span v-if="isReply" class="faint reply-info">
|
||||
<i class="icon-right-open"></i>
|
||||
<router-link :to="replyProfileLink">
|
||||
{{replyToName}}
|
||||
</router-link>
|
||||
</span>
|
||||
<a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" :aria-label="$t('tool_tip.reply')">
|
||||
<i class="button-icon icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i>
|
||||
<router-link class="account-name" :to="userProfileLink">
|
||||
{{status.user.screen_name}}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<span class="heading-right">
|
||||
<router-link class="timeago faint-link" :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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
<h4 class="replies" v-if="inConversation && !noReplyLinks">
|
||||
<small v-if="replies.length">Replies:</small>
|
||||
<small class="reply-link" v-bind:key="reply.id" v-for="reply in replies">
|
||||
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}} </a>
|
||||
</small>
|
||||
</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 class="replies" v-if="inConversation && !isPreview">
|
||||
<span class="faint" v-if="replies.length">{{$t('status.replies_list')}}</span>
|
||||
<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}}</a>
|
||||
</span>
|
||||
</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 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>
|
||||
<i class="icon-spin4 animate-spin"></i>
|
||||
</div>
|
||||
|
@ -123,7 +135,7 @@
|
|||
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
|
||||
</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">
|
||||
<a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')">
|
||||
<i class="button-icon icon-reply" :class="{'icon-reply-active': replying}"></i>
|
||||
|
@ -147,6 +159,8 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
.status-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
@ -202,13 +216,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.media-left {
|
||||
margin-right: $status-margin;
|
||||
}
|
||||
|
||||
.status-el {
|
||||
hyphens: auto;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
border-left-width: 0px;
|
||||
line-height: 18px;
|
||||
min-width: 0;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
|
@ -229,22 +246,34 @@
|
|||
.media-body {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
margin: 0 0 0.25em 0.8em;
|
||||
}
|
||||
|
||||
.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 {
|
||||
flex-wrap: nowrap;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.media-heading-left {
|
||||
padding: 0;
|
||||
vertical-align: bottom;
|
||||
flex-basis: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
|
@ -254,83 +283,102 @@
|
|||
small {
|
||||
font-weight: lighter;
|
||||
}
|
||||
h4 {
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
margin-right: 0.25em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.name-and-links {
|
||||
|
||||
.heading-name-row {
|
||||
padding: 0;
|
||||
flex: 1 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
line-height: 18px;
|
||||
|
||||
.name-and-account-name {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-right: .45em;
|
||||
flex-shrink: 1;
|
||||
margin-right: 0.4em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain
|
||||
}
|
||||
.account-name {
|
||||
min-width: 1.6em;
|
||||
margin-right: 0.4em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
.heading-right {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeago {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.heading-reply-row {
|
||||
align-content: baseline;
|
||||
font-size: 12px;
|
||||
color: $fallback--link;
|
||||
color: var(--link, $fallback--link);
|
||||
line-height: 18px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
|
||||
a {
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
& > span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
& > a:last-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reply-to-and-accountname {
|
||||
display: flex;
|
||||
height: 18px;
|
||||
margin-right: 0.5em;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
.icon-reply {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.reply-info {
|
||||
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 {
|
||||
line-height: 16px;
|
||||
}
|
||||
.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;
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
align-self: last baseline;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
& > * {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
a:hover i {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
.reply-link {
|
||||
height: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -366,8 +414,8 @@
|
|||
}
|
||||
|
||||
.status-content {
|
||||
margin-right: 0.5em;
|
||||
font-family: var(--postFont, sans-serif);
|
||||
line-height: 1.4em;
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
|
@ -390,9 +438,11 @@
|
|||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.5em;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -417,7 +467,7 @@
|
|||
}
|
||||
|
||||
.retweet-info {
|
||||
padding: 0.4em 0.6em 0 0.6em;
|
||||
padding: 0.4em $status-margin;
|
||||
margin: 0;
|
||||
|
||||
.avatar.still-image {
|
||||
|
@ -488,10 +538,10 @@
|
|||
.status-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: $status-margin;
|
||||
|
||||
div, favorite-button {
|
||||
padding-top: 0.25em;
|
||||
max-width: 6em;
|
||||
max-width: 4em;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
@ -517,9 +567,9 @@
|
|||
|
||||
.status {
|
||||
display: flex;
|
||||
padding: 0.6em;
|
||||
padding: $status-margin;
|
||||
&.is-retweet {
|
||||
padding-top: 0.1em;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Status from '../status/status.vue'
|
||||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
const Timeline = {
|
||||
|
@ -44,8 +43,7 @@ const Timeline = {
|
|||
},
|
||||
components: {
|
||||
Status,
|
||||
StatusOrConversation,
|
||||
UserCard
|
||||
StatusOrConversation
|
||||
},
|
||||
created () {
|
||||
const store = this.$store
|
||||
|
@ -54,6 +52,8 @@ const Timeline = {
|
|||
|
||||
window.addEventListener('scroll', this.scrollLoad)
|
||||
|
||||
if (this.timelineName === 'friends' && !credentials) { return false }
|
||||
|
||||
timelineFetcher.fetchAndUpdate({
|
||||
store,
|
||||
credentials,
|
||||
|
@ -68,14 +68,21 @@ const Timeline = {
|
|||
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
this.unfocused = document.hidden
|
||||
}
|
||||
window.addEventListener('keydown', this.handleShortKey)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.scrollLoad)
|
||||
window.removeEventListener('keydown', this.handleShortKey)
|
||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||
},
|
||||
methods: {
|
||||
handleShortKey (e) {
|
||||
if (e.key === '.') this.showNewStatuses()
|
||||
},
|
||||
showNewStatuses () {
|
||||
if (this.newStatusCount === 0) return
|
||||
|
||||
if (this.timeline.flushMarker !== 0) {
|
||||
this.$store.commit('clearTimeline', { timeline: this.timelineName })
|
||||
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
|
||||
|
@ -99,7 +106,7 @@ const Timeline = {
|
|||
tag: this.tag
|
||||
}).then(statuses => {
|
||||
store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||
if (statuses.length === 0) {
|
||||
if (statuses && statuses.length === 0) {
|
||||
this.bottomedOut = true
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -222,6 +222,14 @@
|
|||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
margin-right: 1em;
|
||||
font-size: 15px;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
|
@ -8,6 +8,7 @@ const UserFinder = {
|
|||
methods: {
|
||||
findUser (username) {
|
||||
this.$router.push({ name: 'user-search', query: { query: username } })
|
||||
this.$refs.userSearchInput.focus()
|
||||
},
|
||||
toggleHidden () {
|
||||
this.hidden = !this.hidden
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<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>
|
||||
<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)">
|
||||
<i class="icon-search"/>
|
||||
</button>
|
||||
|
|
|
@ -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 UserCard from '../user_card/user_card.vue'
|
||||
import FollowCard from '../follow_card/follow_card.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 = {
|
||||
data () {
|
||||
return {
|
||||
error: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
|
@ -13,6 +43,16 @@ const UserProfile = {
|
|||
this.startFetchFavorites()
|
||||
if (!this.user.id) {
|
||||
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 () {
|
||||
|
@ -105,9 +145,9 @@ const UserProfile = {
|
|||
},
|
||||
components: {
|
||||
UserCardContent,
|
||||
UserCard,
|
||||
Timeline,
|
||||
FollowList
|
||||
FollowerList,
|
||||
FriendList
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,16 +18,10 @@
|
|||
:user-id="fetchBy"
|
||||
/>
|
||||
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
|
||||
<FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
|
||||
<div class="userlist-placeholder" v-else>
|
||||
<i class="icon-spin3 animate-spin"></i>
|
||||
</div>
|
||||
<FriendList :userId="userId" />
|
||||
</div>
|
||||
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
|
||||
<FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
|
||||
<div class="userlist-placeholder" v-else>
|
||||
<i class="icon-spin3 animate-spin"></i>
|
||||
</div>
|
||||
<FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
|
||||
</div>
|
||||
<Timeline
|
||||
:label="$t('user_card.media')"
|
||||
|
@ -55,7 +49,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
|
|
@ -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'
|
||||
const userSearch = {
|
||||
components: {
|
||||
UserCard
|
||||
FollowCard
|
||||
},
|
||||
props: [
|
||||
'query'
|
||||
|
@ -10,7 +10,8 @@ const userSearch = {
|
|||
data () {
|
||||
return {
|
||||
username: '',
|
||||
users: []
|
||||
users: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
@ -24,14 +25,17 @@ const userSearch = {
|
|||
methods: {
|
||||
newQuery (query) {
|
||||
this.$router.push({ name: 'user-search', query: { query } })
|
||||
this.$refs.userSearchInput.focus()
|
||||
},
|
||||
search (query) {
|
||||
if (!query) {
|
||||
this.users = []
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
userSearchApi.search({query, store: this.$store})
|
||||
.then((res) => {
|
||||
this.loading = false
|
||||
this.users = res
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
{{$t('nav.user_search')}}
|
||||
</div>
|
||||
<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)">
|
||||
<i class="icon-search"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
|
||||
<div v-if="loading" class="text-center loading-icon">
|
||||
<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>
|
||||
</template>
|
||||
|
@ -27,4 +30,8 @@
|
|||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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 ImageCropper from '../image_cropper/image_cropper.vue'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
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 = {
|
||||
data () {
|
||||
|
@ -38,10 +61,15 @@ const UserSettings = {
|
|||
activeTab: 'profile'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
},
|
||||
components: {
|
||||
StyleSwitcher,
|
||||
TabSwitcher,
|
||||
ImageCropper
|
||||
ImageCropper,
|
||||
BlockList,
|
||||
MuteList
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
@ -63,6 +91,15 @@ const UserSettings = {
|
|||
},
|
||||
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: {
|
||||
|
@ -282,6 +319,11 @@ const UserSettings = {
|
|||
logout () {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.replace('/')
|
||||
},
|
||||
revokeToken (id) {
|
||||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
||||
this.$store.dispatch('revokeToken', id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,6 +121,30 @@
|
|||
<p v-if="changePasswordError">{{changePasswordError}}</p>
|
||||
</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">
|
||||
<h2>{{$t('settings.delete_account')}}</h2>
|
||||
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
|
||||
|
@ -162,6 +186,12 @@
|
|||
<h2>{{$t('settings.follow_export_processing')}}</h2>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -207,5 +237,17 @@
|
|||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
}
|
||||
|
||||
.oauth-tokens {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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 = {
|
||||
components: {
|
||||
UserCard
|
||||
FollowCard
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{$t('who_to_follow.who_to_follow')}}
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
|
|
40
src/hocs/with_list/with_list.js
Normal file
40
src/hocs/with_list/with_list.js
Normal 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
|
6
src/hocs/with_list/with_list.scss
Normal file
6
src/hocs/with_list/with_list.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
.with-list {
|
||||
&-empty-content {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
94
src/hocs/with_load_more/with_load_more.js
Normal file
94
src/hocs/with_load_more/with_load_more.js
Normal 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
|
10
src/hocs/with_load_more/with_load_more.scss
Normal file
10
src/hocs/with_load_more/with_load_more.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
.with-load-more {
|
||||
&-footer {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
|
||||
.error {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
84
src/hocs/with_subscription/with_subscription.js
Normal file
84
src/hocs/with_subscription/with_subscription.js
Normal 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
|
10
src/hocs/with_subscription/with_subscription.scss
Normal file
10
src/hocs/with_subscription/with_subscription.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
.with-subscription {
|
||||
&-loading {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
|
||||
.error {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -134,6 +134,11 @@
|
|||
"notification_visibility_mentions": "الإشارات",
|
||||
"notification_visibility_repeats": "",
|
||||
"nsfw_clickthrough": "",
|
||||
"oauth_tokens": "رموز OAuth",
|
||||
"token": "رمز",
|
||||
"refresh_token": "رمز التحديث",
|
||||
"valid_until": "صالح حتى",
|
||||
"revoke_token": "سحب",
|
||||
"panelRadius": "",
|
||||
"pause_on_unfocused": "",
|
||||
"presets": "النماذج",
|
||||
|
|
|
@ -132,6 +132,11 @@
|
|||
"notification_visibility_repeats": "Republica una entrada meva",
|
||||
"no_rich_text_description": "Neteja el formatat de text de totes les entrades",
|
||||
"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",
|
||||
"pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus",
|
||||
"presets": "Temes",
|
||||
|
|
|
@ -159,6 +159,11 @@
|
|||
"hide_follows_description": "Zeige nicht, wem ich folge",
|
||||
"hide_followers_description": "Zeige nicht, wer mir folgt",
|
||||
"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",
|
||||
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
|
||||
"presets": "Voreinstellungen",
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
"apply": "Apply",
|
||||
"submit": "Submit",
|
||||
"more": "More",
|
||||
"generic_error": "An error occured"
|
||||
"generic_error": "An error occured",
|
||||
"optional": "optional"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Crop picture",
|
||||
|
@ -92,6 +93,9 @@
|
|||
"token": "Invite token",
|
||||
"captcha": "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\nI’m an anime girl living in suburban Japan. You may know me from the Wired.",
|
||||
"validations": {
|
||||
"username_required": "cannot be left blank",
|
||||
"fullname_required": "cannot be left blank",
|
||||
|
@ -102,6 +106,7 @@
|
|||
}
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "App name",
|
||||
"attachmentRadius": "Attachments",
|
||||
"attachments": "Attachments",
|
||||
"autoload": "Enable automatic loading when scrolled to the bottom",
|
||||
|
@ -110,6 +115,7 @@
|
|||
"avatarRadius": "Avatars",
|
||||
"background": "Background",
|
||||
"bio": "Bio",
|
||||
"blocks_tab": "Blocks",
|
||||
"btnRadius": "Buttons",
|
||||
"cBlue": "Blue (Reply, follow)",
|
||||
"cGreen": "Green (Retweet)",
|
||||
|
@ -144,6 +150,7 @@
|
|||
"general": "General",
|
||||
"hide_attachments_in_convo": "Hide attachments in conversations",
|
||||
"hide_attachments_in_tl": "Hide attachments in timeline",
|
||||
"max_thumbnails": "Maximum amount of thumbnails per post",
|
||||
"hide_isp": "Hide instance-specific panel",
|
||||
"preload_images": "Preload images",
|
||||
"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",
|
||||
"loop_video": "Loop videos",
|
||||
"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",
|
||||
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
||||
"name": "Name",
|
||||
|
@ -175,11 +183,18 @@
|
|||
"notification_visibility_mentions": "Mentions",
|
||||
"notification_visibility_repeats": "Repeats",
|
||||
"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_followers_description": "Don't show who's following me",
|
||||
"show_admin_badge": "Show Admin badge in my profile",
|
||||
"show_moderator_badge": "Show Moderator badge in my profile",
|
||||
"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",
|
||||
"pause_on_unfocused": "Pause streaming when tab is not focused",
|
||||
"presets": "Presets",
|
||||
|
@ -345,6 +360,10 @@
|
|||
"no_more_statuses": "No more statuses",
|
||||
"no_statuses": "No statuses"
|
||||
},
|
||||
"status": {
|
||||
"reply_to": "Reply to",
|
||||
"replies_list": "Replies:"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Approve",
|
||||
"block": "Block",
|
||||
|
@ -366,10 +385,18 @@
|
|||
"muted": "Muted",
|
||||
"per_day": "per day",
|
||||
"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": {
|
||||
"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": {
|
||||
"more": "More",
|
||||
|
|
|
@ -171,6 +171,11 @@
|
|||
"show_admin_badge": "Mostrar la placa de administrador en mi perfil",
|
||||
"show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
|
||||
"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",
|
||||
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
|
||||
"presets": "Por defecto",
|
||||
|
|
|
@ -133,6 +133,7 @@
|
|||
"general": "Yleinen",
|
||||
"hide_attachments_in_convo": "Piilota liitteet keskusteluissa",
|
||||
"hide_attachments_in_tl": "Piilota liitteet aikajanalla",
|
||||
"max_thumbnails": "Suurin sallittu määrä liitteitä esikatselussa",
|
||||
"hide_isp": "Piilota palvelimenkohtainen ruutu",
|
||||
"preload_images": "Esilataa kuvat",
|
||||
"use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella",
|
||||
|
@ -165,6 +166,11 @@
|
|||
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
|
||||
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
|
||||
"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",
|
||||
"pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
|
||||
"presets": "Valmiit teemat",
|
||||
|
@ -215,6 +221,10 @@
|
|||
"up_to_date": "Ajantasalla",
|
||||
"no_more_statuses": "Ei enempää viestejä"
|
||||
},
|
||||
"status": {
|
||||
"reply_to": "Vastaus",
|
||||
"replies_list": "Vastaukset:"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Hyväksy",
|
||||
"block": "Estä",
|
||||
|
|
|
@ -137,6 +137,11 @@
|
|||
"notification_visibility_mentions": "Mentionnés",
|
||||
"notification_visibility_repeats": "Partages",
|
||||
"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",
|
||||
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
|
||||
"presets": "Thèmes prédéfinis",
|
||||
|
|
|
@ -134,6 +134,11 @@
|
|||
"notification_visibility_repeats": "Atphostáil",
|
||||
"no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post",
|
||||
"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",
|
||||
"pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte",
|
||||
"presets": "Réamhshocruithe",
|
||||
|
|
|
@ -129,6 +129,11 @@
|
|||
"notification_visibility_mentions": "אזכורים",
|
||||
"notification_visibility_repeats": "חזרות",
|
||||
"nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר",
|
||||
"oauth_tokens": "אסימוני OAuth",
|
||||
"token": "אסימון",
|
||||
"refresh_token": "רענון האסימון",
|
||||
"valid_until": "בתוקף עד",
|
||||
"revoke_token": "בטל",
|
||||
"panelRadius": "פאנלים",
|
||||
"pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס",
|
||||
"presets": "ערכים קבועים מראש",
|
||||
|
|
|
@ -93,6 +93,11 @@
|
|||
"notification_visibility_mentions": "Menzioni",
|
||||
"notification_visibility_repeats": "Condivisioni",
|
||||
"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",
|
||||
"pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano",
|
||||
"presets": "Valori predefiniti",
|
||||
|
|
|
@ -171,6 +171,11 @@
|
|||
"show_admin_badge": "アドミンのしるしをみる",
|
||||
"show_moderator_badge": "モデレーターのしるしをみる",
|
||||
"nsfw_clickthrough": "NSFWなファイルをかくす",
|
||||
"oauth_tokens": "OAuthトークン",
|
||||
"token": "トークン",
|
||||
"refresh_token": "トークンを更新",
|
||||
"valid_until": "まで有効",
|
||||
"revoke_token": "取り消す",
|
||||
"panelRadius": "パネル",
|
||||
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
|
||||
"presets": "プリセット",
|
||||
|
|
|
@ -159,6 +159,11 @@
|
|||
"hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
|
||||
"hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
|
||||
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
|
||||
"oauth_tokens": "OAuth 토큰",
|
||||
"token": "토큰",
|
||||
"refresh_token": "토큰 새로 고침",
|
||||
"valid_until": "까지 유효하다",
|
||||
"revoke_token": "취소",
|
||||
"panelRadius": "패널",
|
||||
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
|
||||
"presets": "프리셋",
|
||||
|
|
|
@ -132,6 +132,11 @@
|
|||
"notification_visibility_repeats": "Gjentakelser",
|
||||
"no_rich_text_description": "Fjern all formatering fra statuser",
|
||||
"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",
|
||||
"pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus",
|
||||
"presets": "Forhåndsdefinerte tema",
|
||||
|
|
|
@ -159,6 +159,11 @@
|
|||
"no_rich_text_description": "Strip rich text formattering van alle posts",
|
||||
"hide_network_description": "Toon niet wie mij volgt en wie ik volg.",
|
||||
"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",
|
||||
"pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is",
|
||||
"presets": "Presets",
|
||||
|
|
|
@ -142,6 +142,7 @@
|
|||
"notification_visibility_mentions": "Mencions",
|
||||
"notification_visibility_repeats": "Repeticions",
|
||||
"no_rich_text_description": "Netejar lo format tèxte de totas las publicacions",
|
||||
"oauth_tokens": "Llistats OAuth",
|
||||
"pause_on_unfocused": "Pausar la difusion quand l’onglet es pas seleccionat",
|
||||
"profile_tab": "Perfil",
|
||||
"replies_in_timeline": "Responsas del flux",
|
||||
|
|
|
@ -86,6 +86,11 @@
|
|||
"name_bio": "Imię i bio",
|
||||
"new_password": "Nowe hasło",
|
||||
"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",
|
||||
"presets": "Gotowe motywy",
|
||||
"profile_background": "Tło profilu",
|
||||
|
|
|
@ -132,6 +132,11 @@
|
|||
"show_admin_badge": "Показывать значок администратора в моем профиле",
|
||||
"show_moderator_badge": "Показывать значок модератора в моем профиле",
|
||||
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
|
||||
"oauth_tokens": "OAuth токены",
|
||||
"token": "Токен",
|
||||
"refresh_token": "Рефреш токен",
|
||||
"valid_until": "Годен до",
|
||||
"revoke_token": "Удалить",
|
||||
"panelRadius": "Панели",
|
||||
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
|
||||
"presets": "Пресеты",
|
||||
|
|
|
@ -134,6 +134,11 @@
|
|||
"notification_visibility_repeats": "转发",
|
||||
"no_rich_text_description": "不显示富文本格式",
|
||||
"nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开",
|
||||
"oauth_tokens": "OAuth令牌",
|
||||
"token": "代币",
|
||||
"refresh_token": "刷新令牌",
|
||||
"valid_until": "有效期至",
|
||||
"revoke_token": "撤消",
|
||||
"panelRadius": "面板",
|
||||
"pause_on_unfocused": "在离开页面时暂停时间线推送",
|
||||
"presets": "预置",
|
||||
|
|
|
@ -11,6 +11,7 @@ import configModule from './modules/config.js'
|
|||
import chatModule from './modules/chat.js'
|
||||
import oauthModule from './modules/oauth.js'
|
||||
import mediaViewerModule from './modules/media_viewer.js'
|
||||
import oauthTokensModule from './modules/oauth_tokens.js'
|
||||
|
||||
import VueTimeago from 'vue-timeago'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
@ -64,7 +65,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
|||
config: configModule,
|
||||
chat: chatModule,
|
||||
oauth: oauthModule,
|
||||
mediaViewer: mediaViewerModule
|
||||
mediaViewer: mediaViewerModule,
|
||||
oauthTokens: oauthTokensModule
|
||||
},
|
||||
plugins: [persistedState, pushNotifications],
|
||||
strict: false // Socket modifies itself, let's ignore this for now.
|
||||
|
|
|
@ -8,6 +8,7 @@ const defaultState = {
|
|||
collapseMessageWithSubject: undefined, // instance default
|
||||
hideAttachments: false,
|
||||
hideAttachmentsInConv: false,
|
||||
maxThumbnails: 16,
|
||||
hideNsfw: true,
|
||||
preloadImage: true,
|
||||
loopVideo: true,
|
||||
|
|
26
src/modules/oauth_tokens.js
Normal file
26
src/modules/oauth_tokens.js
Normal 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
|
|
@ -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
|
||||
// user. I.e. opening different user profiles makes request which could
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -303,6 +303,8 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
setTimeout(notification.close.bind(notification), 5000)
|
||||
}
|
||||
}
|
||||
} else if (notification.seen) {
|
||||
state.notifications.idStore[notification.id].seen = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -72,19 +72,31 @@ export const mutations = {
|
|||
},
|
||||
// Because frontend doesn't have a reason to keep these stuff in memory
|
||||
// outside of viewing someones user profile.
|
||||
clearFriendsAndFollowers (state, userKey) {
|
||||
const user = state.usersObject[userKey]
|
||||
clearFriends (state, userId) {
|
||||
const user = state.usersObject[userId]
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
user.friends = []
|
||||
user.followers = []
|
||||
user.friendsPage = 0
|
||||
},
|
||||
clearFollowers (state, userId) {
|
||||
const user = state.usersObject[userId]
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
user.followers = []
|
||||
user.followersPage = 0
|
||||
},
|
||||
addNewUsers (state, users) {
|
||||
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) {
|
||||
status.user = state.usersObject[status.user.id]
|
||||
},
|
||||
|
@ -134,7 +146,39 @@ const users = {
|
|||
getters,
|
||||
actions: {
|
||||
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]))
|
||||
},
|
||||
addFriends ({ rootState, commit }, fetchBy) {
|
||||
|
@ -151,20 +195,19 @@ const users = {
|
|||
})
|
||||
},
|
||||
addFollowers ({ rootState, commit }, fetchBy) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const user = rootState.users.usersObject[fetchBy]
|
||||
const page = user.followersPage || 1
|
||||
rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
|
||||
.then((followers) => {
|
||||
commit('addFollowers', { id: user.id, followers, page })
|
||||
resolve(followers)
|
||||
}).catch(() => {
|
||||
reject()
|
||||
})
|
||||
})
|
||||
const user = rootState.users.usersObject[fetchBy]
|
||||
const page = user.followersPage || 1
|
||||
return rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
|
||||
.then((followers) => {
|
||||
commit('addFollowers', { id: user.id, followers, page })
|
||||
return followers
|
||||
})
|
||||
},
|
||||
clearFriendsAndFollowers ({ commit }, userKey) {
|
||||
commit('clearFriendsAndFollowers', userKey)
|
||||
clearFriends ({ commit }, userId) {
|
||||
commit('clearFriends', userId)
|
||||
},
|
||||
clearFollowers ({ commit }, userId) {
|
||||
commit('clearFollowers', userId)
|
||||
},
|
||||
registerPushNotifications (store) {
|
||||
const token = store.state.currentUser.credentials
|
||||
|
@ -263,6 +306,8 @@ const users = {
|
|||
const user = data
|
||||
// user.credentials = userCredentials
|
||||
user.credentials = accessToken
|
||||
user.blockIds = []
|
||||
user.muteIds = []
|
||||
commit('setCurrentUser', user)
|
||||
commit('addNewUsers', [user])
|
||||
|
||||
|
@ -279,11 +324,8 @@ const users = {
|
|||
// Start getting fresh posts.
|
||||
store.dispatch('startFetching', { timeline: 'friends' })
|
||||
|
||||
// Get user mutes and follower info
|
||||
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
|
||||
each(mutedUsers, (user) => { user.muted = true })
|
||||
store.commit('addNewUsers', mutedUsers)
|
||||
})
|
||||
// Get user mutes
|
||||
store.dispatch('fetchMutes')
|
||||
|
||||
// Fetch our friends
|
||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||
|
|
|
@ -18,6 +18,7 @@ const MENTIONS_URL = '/api/statuses/mentions.json'
|
|||
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
|
||||
const FOLLOWERS_URL = '/api/statuses/followers.json'
|
||||
const FRIENDS_URL = '/api/statuses/friends.json'
|
||||
const BLOCKS_URL = '/api/statuses/blocks.json'
|
||||
const FOLLOWING_URL = '/api/friendships/create.json'
|
||||
const UNFOLLOWING_URL = '/api/friendships/destroy.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 { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import 'whatwg-fetch'
|
||||
import { StatusCodeError } from '../errors/errors'
|
||||
|
||||
const oldfetch = window.fetch
|
||||
|
||||
|
@ -243,7 +245,15 @@ const denyUser = ({id, credentials}) => {
|
|||
const fetchUser = ({id, credentials}) => {
|
||||
let url = `${USER_URL}?user_id=${id}`
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -518,6 +528,34 @@ const fetchMutes = ({credentials}) => {
|
|||
}).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}) => {
|
||||
return fetch(SUGGESTIONS_URL, {
|
||||
headers: authHeaders(credentials)
|
||||
|
@ -559,6 +597,9 @@ const apiService = {
|
|||
fetchAllFollowing,
|
||||
setUserMute,
|
||||
fetchMutes,
|
||||
fetchBlocks,
|
||||
fetchOAuthTokens,
|
||||
revokeOAuthToken,
|
||||
register,
|
||||
getCaptcha,
|
||||
updateAvatar,
|
||||
|
|
|
@ -54,8 +54,8 @@ const backendInteractorService = (credentials) => {
|
|||
return apiService.denyUser({credentials, id})
|
||||
}
|
||||
|
||||
const startFetching = ({timeline, store, userId = false}) => {
|
||||
return timelineFetcherService.startFetching({timeline, store, credentials, userId})
|
||||
const startFetching = ({timeline, store, userId = false, tag}) => {
|
||||
return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag})
|
||||
}
|
||||
|
||||
const setUserMute = ({id, muted = true}) => {
|
||||
|
@ -63,7 +63,10 @@ const backendInteractorService = (credentials) => {
|
|||
}
|
||||
|
||||
const fetchMutes = () => apiService.fetchMutes({credentials})
|
||||
const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
|
||||
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
|
||||
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
|
||||
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
|
||||
|
||||
const getCaptcha = () => apiService.getCaptcha()
|
||||
const register = (params) => apiService.register(params)
|
||||
|
@ -94,6 +97,9 @@ const backendInteractorService = (credentials) => {
|
|||
startFetching,
|
||||
setUserMute,
|
||||
fetchMutes,
|
||||
fetchBlocks,
|
||||
fetchOAuthTokens,
|
||||
revokeOAuthToken,
|
||||
register,
|
||||
getCaptcha,
|
||||
updateAvatar,
|
||||
|
|
10
src/services/component_utils/component_utils.js
Normal file
10
src/services/component_utils/component_utils.js
Normal 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
|
||||
}
|
14
src/services/errors/errors.js
Normal file
14
src/services/errors/errors.js
Normal 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
|
|
@ -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
|
|
@ -16,7 +16,17 @@ const fetchAndUpdate = ({store, credentials, older = false}) => {
|
|||
args['until'] = timelineData.minId
|
||||
}
|
||||
} 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'
|
||||
|
|
96
yarn.lock
96
yarn.lock
|
@ -38,11 +38,7 @@
|
|||
dom-event-types "^1.0.0"
|
||||
lodash "^4.17.4"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
abbrev@1.0.x:
|
||||
abbrev@1, abbrev@1.0.x:
|
||||
version "1.0.9"
|
||||
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:
|
||||
color-name "1.1.3"
|
||||
|
||||
color-name@1.1.3:
|
||||
color-name@1.1.3, color-name@^1.0.0:
|
||||
version "1.1.3"
|
||||
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:
|
||||
version "0.3.0"
|
||||
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"
|
||||
yauzl "2.4.1"
|
||||
|
||||
extsprintf@1.3.0:
|
||||
extsprintf@1.3.0, extsprintf@^1.2.0:
|
||||
version "1.3.0"
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
extend "3"
|
||||
|
||||
iconv-lite@0.4.23:
|
||||
iconv-lite@0.4.23, iconv-lite@^0.4.4:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
|
||||
dependencies:
|
||||
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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
nopt "~4.0.1"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
||||
js-tokens@^3.0.2:
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
|
||||
|
@ -4237,7 +4215,7 @@ minimatch@3.0.3:
|
|||
dependencies:
|
||||
brace-expansion "^1.0.0"
|
||||
|
||||
minimist@0.0.8:
|
||||
minimist@0.0.8, minimist@~0.0.1:
|
||||
version "0.0.8"
|
||||
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"
|
||||
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:
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
|
||||
|
@ -4679,7 +4653,7 @@ os-locale@^1.4.0:
|
|||
dependencies:
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
version "0.1.7"
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
version "1.2.0"
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
version "1.20.0"
|
||||
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"
|
||||
object-copy "^0.1.0"
|
||||
|
||||
"statuses@>= 1.4.0 < 2":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
"statuses@>= 1.4.0 < 2", statuses@~1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
|
||||
|
||||
statuses@~1.3.1:
|
||||
version "1.3.1"
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
|
||||
dependencies:
|
||||
|
@ -6098,7 +6050,7 @@ supports-color@^2.0.0:
|
|||
version "2.0.0"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
|
||||
dependencies:
|
||||
|
@ -6196,18 +6148,12 @@ timers-browserify@^2.0.2:
|
|||
dependencies:
|
||||
setimmediate "^1.0.4"
|
||||
|
||||
tmp@0.0.31:
|
||||
tmp@0.0.31, tmp@0.0.x:
|
||||
version "0.0.31"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
|
||||
dependencies:
|
||||
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:
|
||||
version "0.1.4"
|
||||
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"
|
||||
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:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.1.tgz#b2d3d95402a811602380783ea4f566eb875569a2"
|
||||
|
|
Loading…
Add table
Reference in a new issue