Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: (85 commits) entity normalizer: hook up in_reply_to_account_acct add BBCode strings fix follow button not updating bug in follow-card refer searched user objects from the global user rep set max-width of textarea in settings page Remove space in the timeline setting copy user_card.vue: Fix .emoji to apply to img Update oc.json Update oc.json Update oc.json Update oc.json replace pencil with wrench icon rebuild fontello with wrench icon added fetch all friends using pagination stop fetching user relationship when user is unauthorized Revert "recover border between basic-user-card using list component" remove extra spacing code readability fix typos clean up ...
This commit is contained in:
commit
659ac384d5
46 changed files with 837 additions and 340 deletions
52
src/components/autosuggest/autosuggest.js
Normal file
52
src/components/autosuggest/autosuggest.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
const debounceMilliseconds = 500
|
||||
|
||||
export default {
|
||||
props: {
|
||||
query: { // function to query results and return a promise
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
filter: { // function to filter results in real time
|
||||
type: Function
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
term: '',
|
||||
timeout: null,
|
||||
results: [],
|
||||
resultsVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filtered () {
|
||||
return this.filter ? this.filter(this.results) : this.results
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
term (val) {
|
||||
this.fetchResults(val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchResults (term) {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.results = []
|
||||
if (term) {
|
||||
this.query(term).then((results) => { this.results = results })
|
||||
}
|
||||
}, debounceMilliseconds)
|
||||
},
|
||||
onInputClick () {
|
||||
this.resultsVisible = true
|
||||
},
|
||||
onClickOutside () {
|
||||
this.resultsVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/components/autosuggest/autosuggest.vue
Normal file
45
src/components/autosuggest/autosuggest.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="autosuggest" v-click-outside="onClickOutside">
|
||||
<input v-model="term" :placeholder="placeholder" @click="onInputClick" class="autosuggest-input" />
|
||||
<div class="autosuggest-results" v-if="resultsVisible && filtered.length > 0">
|
||||
<slot v-for="item in filtered" :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./autosuggest.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.autosuggest {
|
||||
position: relative;
|
||||
|
||||
&-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-results {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
max-height: 400px;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
overflow-y: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -24,19 +24,11 @@
|
|||
<script src="./basic_user_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.basic-user-card {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
margin: 0;
|
||||
padding-top: 0.6em;
|
||||
padding-right: 1em;
|
||||
padding-bottom: 0.6em;
|
||||
padding-left: 1em;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
padding: 0.6em 1em;
|
||||
|
||||
&-collapsed-content {
|
||||
margin-left: 0.7em;
|
||||
|
|
|
|||
75
src/components/checkbox/checkbox.vue
Normal file
75
src/components/checkbox/checkbox.vue
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" :indeterminate.prop="indeterminate">
|
||||
<i class="checkbox-indicator" />
|
||||
<span v-if="!!$slots.default"><slot></slot></span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
model: {
|
||||
prop: 'checked',
|
||||
event: 'change'
|
||||
},
|
||||
props: ['checked', 'indeterminate']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-left: 1.2em;
|
||||
min-height: 1.2em;
|
||||
|
||||
&-indicator::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
content: '✔';
|
||||
transition: color 200ms;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
border-radius: $fallback--checkboxRadius;
|
||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||
box-shadow: 0px 0px 2px black inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
line-height: 1.1em;
|
||||
font-size: 1.1em;
|
||||
color: transparent;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
||||
&:indeterminate + .checkbox-indicator::before {
|
||||
content: '–';
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
||||
&:disabled + .checkbox-indicator::before {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -10,8 +10,7 @@ const FollowCard = {
|
|||
data () {
|
||||
return {
|
||||
inProgress: false,
|
||||
requestSent: false,
|
||||
updated: false
|
||||
requestSent: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -19,10 +18,8 @@ const FollowCard = {
|
|||
RemoteFollow
|
||||
},
|
||||
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
|
||||
isMe () {
|
||||
return this.$store.state.users.currentUser.id === this.user.id
|
||||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
|
|
@ -31,17 +28,15 @@ const FollowCard = {
|
|||
methods: {
|
||||
followUser () {
|
||||
this.inProgress = true
|
||||
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
|
||||
requestFollow(this.user, this.$store).then(({ sent }) => {
|
||||
this.inProgress = false
|
||||
this.requestSent = sent
|
||||
this.updated = updated
|
||||
})
|
||||
},
|
||||
unfollowUser () {
|
||||
this.inProgress = true
|
||||
requestUnfollow(this.user, this.$store).then(({ updated }) => {
|
||||
requestUnfollow(this.user, this.$store).then(() => {
|
||||
this.inProgress = false
|
||||
this.updated = updated
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,34 +4,38 @@
|
|||
<span class="faint" v-if="!noFollowsYou && user.follows_you">
|
||||
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</span>
|
||||
<div class="follow-card-follow-button" v-if="showFollow && !loggedIn">
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
<button
|
||||
v-if="showFollow && loggedIn"
|
||||
class="btn btn-default follow-card-follow-button"
|
||||
@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 follow-card-follow-button 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>
|
||||
<template v-if="!loggedIn">
|
||||
<div class="follow-card-follow-button" v-if="!user.following">
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="!user.following"
|
||||
class="btn btn-default follow-card-follow-button"
|
||||
@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-else class="btn btn-default follow-card-follow-button 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>
|
||||
</template>
|
||||
</div>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
{{$t('nav.friend_requests')}}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FollowRequestCard v-for="request in requests" :key="request.id" :user="request"/>
|
||||
<FollowRequestCard v-for="request in requests" :key="request.id" :user="request" class="list-item"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
42
src/components/list/list.vue
Normal file
42
src/components/list/list.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="list">
|
||||
<div v-for="item in items" class="list-item" :key="getKey(item)">
|
||||
<slot name="item" :item="item" />
|
||||
</div>
|
||||
<div class="list-empty-content faint" v-if="items.length === 0 && !!$slots.empty">
|
||||
<slot name="empty" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: item => item.id
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.list {
|
||||
&-item:not(:last-child) {
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
&-empty-content {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
src/components/progress_button/progress_button.vue
Normal file
35
src/components/progress_button/progress_button.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<button :disabled="progress || disabled" @click="onClick">
|
||||
<template v-if="progress">
|
||||
<slot name="progress" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot />
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean
|
||||
},
|
||||
click: { // click event handler. Must return a promise
|
||||
type: Function,
|
||||
default: () => Promise.resolve()
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
progress: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
this.progress = true
|
||||
this.click().then(() => { this.progress = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
66
src/components/selectable_list/selectable_list.js
Normal file
66
src/components/selectable_list/selectable_list.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import List from '../list/list.vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
const SelectableList = {
|
||||
components: {
|
||||
List,
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: item => item.id
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selected: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allKeys () {
|
||||
return this.items.map(this.getKey)
|
||||
},
|
||||
filteredSelected () {
|
||||
return this.allKeys.filter(key => this.selected.indexOf(key) !== -1)
|
||||
},
|
||||
allSelected () {
|
||||
return this.filteredSelected.length === this.items.length
|
||||
},
|
||||
noneSelected () {
|
||||
return this.filteredSelected.length === 0
|
||||
},
|
||||
someSelected () {
|
||||
return !this.allSelected && !this.noneSelected
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isSelected (item) {
|
||||
return this.filteredSelected.indexOf(this.getKey(item)) !== -1
|
||||
},
|
||||
toggle (checked, item) {
|
||||
const key = this.getKey(item)
|
||||
const oldChecked = this.isSelected(key)
|
||||
if (checked !== oldChecked) {
|
||||
if (checked) {
|
||||
this.selected.push(key)
|
||||
} else {
|
||||
this.selected.splice(this.selected.indexOf(key), 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleAll (value) {
|
||||
if (value) {
|
||||
this.selected = this.allKeys.slice(0)
|
||||
} else {
|
||||
this.selected = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectableList
|
||||
59
src/components/selectable_list/selectable_list.vue
Normal file
59
src/components/selectable_list/selectable_list.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="selectable-list">
|
||||
<div class="selectable-list-header" v-if="items.length > 0">
|
||||
<div class="selectable-list-checkbox-wrapper">
|
||||
<Checkbox :checked="allSelected" @change="toggleAll" :indeterminate="someSelected">{{ $t('selectable_list.select_all') }}</Checkbox>
|
||||
</div>
|
||||
<div class="selectable-list-header-actions">
|
||||
<slot name="header" :selected="filteredSelected" />
|
||||
</div>
|
||||
</div>
|
||||
<List :items="items" :getKey="getKey">
|
||||
<template slot="item" slot-scope="{item}">
|
||||
<div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }">
|
||||
<div class="selectable-list-checkbox-wrapper">
|
||||
<Checkbox :checked="isSelected(item)" @change="checked => toggle(checked, item)" />
|
||||
</div>
|
||||
<slot name="item" :item="item" />
|
||||
</div>
|
||||
</template>
|
||||
<template slot="empty"><slot name="empty" /></template>
|
||||
</List>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./selectable_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.selectable-list {
|
||||
&-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-item-selected-inner {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.6em 0;
|
||||
border-bottom: 2px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
|
||||
&-actions {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-checkbox-wrapper {
|
||||
padding: 0 10px;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -42,9 +42,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
|
||||
<label for="collapseMessageWithSubject">
|
||||
{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}
|
||||
</label>
|
||||
<label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="streaming" v-model="streamingLocal">
|
||||
|
|
@ -330,6 +328,7 @@
|
|||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
||||
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
||||
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
||||
<i class="button-icon icon-pencil usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||
<i class="button-icon icon-wrench usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||
</router-link>
|
||||
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
|
||||
<i class="icon-link-ext usersettings"></i>
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
||||
.emoji {
|
||||
&.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,37 @@
|
|||
import { compose } from 'vue-compose'
|
||||
import get from 'lodash/get'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import FollowCard from '../follow_card/follow_card.vue'
|
||||
import Timeline from '../timeline/timeline.vue'
|
||||
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||
import List from '../list/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.findUser(props.userId), 'followers', []),
|
||||
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
||||
childPropName: 'entries',
|
||||
additionalPropNames: ['userId']
|
||||
}),
|
||||
withList({ getEntryProps: user => ({ user }) })
|
||||
)(FollowCard)
|
||||
const FollowerList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'followerIds', []).map(id => $store.getters.findUser(id)),
|
||||
destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
||||
childPropName: 'items',
|
||||
additionalPropNames: ['userId']
|
||||
})(List)
|
||||
|
||||
const FriendList = compose(
|
||||
withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []),
|
||||
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
||||
childPropName: 'entries',
|
||||
additionalPropNames: ['userId']
|
||||
}),
|
||||
withList({ getEntryProps: user => ({ user }) })
|
||||
)(FollowCard)
|
||||
const FriendList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'friendIds', []).map(id => $store.getters.findUser(id)),
|
||||
destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
||||
childPropName: 'items',
|
||||
additionalPropNames: ['userId']
|
||||
})(List)
|
||||
|
||||
const UserProfile = {
|
||||
data () {
|
||||
return {
|
||||
error: false,
|
||||
fetchedUserId: null
|
||||
userId: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (!this.user.id) {
|
||||
this.fetchUserId()
|
||||
.then(() => this.startUp())
|
||||
} else {
|
||||
this.startUp()
|
||||
}
|
||||
const routeParams = this.$route.params
|
||||
this.load(routeParams.name || routeParams.id)
|
||||
},
|
||||
destroyed () {
|
||||
this.cleanUp()
|
||||
|
|
@ -57,26 +46,12 @@ const UserProfile = {
|
|||
media () {
|
||||
return this.$store.state.statuses.timelines.media
|
||||
},
|
||||
userId () {
|
||||
return this.$route.params.id || this.user.id || this.fetchedUserId
|
||||
},
|
||||
userName () {
|
||||
return this.$route.params.name || this.user.screen_name
|
||||
},
|
||||
isUs () {
|
||||
return this.userId && this.$store.state.users.currentUser.id &&
|
||||
this.userId === this.$store.state.users.currentUser.id
|
||||
},
|
||||
userInStore () {
|
||||
const routeParams = this.$route.params
|
||||
// This needs fetchedUserId so that computed will be refreshed when user is fetched
|
||||
return this.$store.getters.findUser(this.fetchedUserId || routeParams.name || routeParams.id)
|
||||
},
|
||||
user () {
|
||||
if (this.userInStore) {
|
||||
return this.userInStore
|
||||
}
|
||||
return {}
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
isExternal () {
|
||||
return this.$route.name === 'external-user-profile'
|
||||
|
|
@ -89,39 +64,36 @@ const UserProfile = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
startFetchFavorites () {
|
||||
if (this.isUs) {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId: this.userId })
|
||||
}
|
||||
},
|
||||
fetchUserId () {
|
||||
let fetchPromise
|
||||
if (this.userId && !this.$route.params.name) {
|
||||
fetchPromise = this.$store.dispatch('fetchUser', this.userId)
|
||||
load (userNameOrId) {
|
||||
// Check if user data is already loaded in store
|
||||
const user = this.$store.getters.findUser(userNameOrId)
|
||||
if (user) {
|
||||
this.userId = user.id
|
||||
this.fetchTimelines()
|
||||
} else {
|
||||
fetchPromise = this.$store.dispatch('fetchUser', this.userName)
|
||||
this.$store.dispatch('fetchUser', userNameOrId)
|
||||
.then(({ id }) => {
|
||||
this.fetchedUserId = id
|
||||
this.userId = id
|
||||
this.fetchTimelines()
|
||||
})
|
||||
.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')
|
||||
}
|
||||
})
|
||||
}
|
||||
return fetchPromise
|
||||
.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')
|
||||
}
|
||||
})
|
||||
.then(() => this.startUp())
|
||||
},
|
||||
startUp () {
|
||||
if (this.userId) {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId: this.userId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId: this.userId })
|
||||
this.startFetchFavorites()
|
||||
fetchTimelines () {
|
||||
const userId = this.userId
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId })
|
||||
if (this.isUs) {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId })
|
||||
}
|
||||
},
|
||||
cleanUp () {
|
||||
|
|
@ -134,18 +106,16 @@ const UserProfile = {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
// userId can be undefined if we don't know it yet
|
||||
userId (newVal) {
|
||||
'$route.params.id': function (newVal) {
|
||||
if (newVal) {
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
this.load(newVal)
|
||||
}
|
||||
},
|
||||
userName () {
|
||||
if (this.$route.params.name) {
|
||||
this.fetchUserId()
|
||||
'$route.params.name': function (newVal) {
|
||||
if (newVal) {
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
this.load(newVal)
|
||||
}
|
||||
},
|
||||
$route () {
|
||||
|
|
@ -157,7 +127,8 @@ const UserProfile = {
|
|||
Timeline,
|
||||
FollowerList,
|
||||
FriendList,
|
||||
ModerationTools
|
||||
ModerationTools,
|
||||
FollowCard
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="user.id" class="user-profile panel panel-default">
|
||||
<div v-if="user" class="user-profile panel panel-default">
|
||||
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
|
||||
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
|
||||
<Timeline
|
||||
|
|
@ -14,10 +14,18 @@
|
|||
:user-id="userId"
|
||||
/>
|
||||
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
|
||||
<FriendList :userId="userId" />
|
||||
<FriendList :userId="userId">
|
||||
<template slot="item" slot-scope="{item}">
|
||||
<FollowCard :user="item" />
|
||||
</template>
|
||||
</FriendList>
|
||||
</div>
|
||||
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
|
||||
<FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
|
||||
<FollowerList :userId="userId">
|
||||
<template slot="item" slot-scope="{item}">
|
||||
<FollowCard :user="item" :noFollowsYou="isUs" />
|
||||
</template>
|
||||
</FollowerList>
|
||||
</div>
|
||||
<Timeline
|
||||
:label="$t('user_card.media')"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import FollowCard from '../follow_card/follow_card.vue'
|
||||
import userSearchApi from '../../services/new_api/user_search.js'
|
||||
import map from 'lodash/map'
|
||||
|
||||
const userSearch = {
|
||||
components: {
|
||||
FollowCard
|
||||
|
|
@ -10,10 +11,15 @@ const userSearch = {
|
|||
data () {
|
||||
return {
|
||||
username: '',
|
||||
users: [],
|
||||
userIds: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
users () {
|
||||
return this.userIds.map(userId => this.$store.getters.findUser(userId))
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.search(this.query)
|
||||
},
|
||||
|
|
@ -33,10 +39,10 @@ const userSearch = {
|
|||
return
|
||||
}
|
||||
this.loading = true
|
||||
userSearchApi.search({query, store: this.$store})
|
||||
this.$store.dispatch('searchUsers', query)
|
||||
.then((res) => {
|
||||
this.loading = false
|
||||
this.users = res
|
||||
this.userIds = map(res, 'id')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<i class="icon-spin3 animate-spin"/>
|
||||
</div>
|
||||
<div v-else class="panel-body">
|
||||
<FollowCard v-for="user in users" :key="user.id" :user="user"/>
|
||||
<FollowCard v-for="user in users" :key="user.id" :user="user" class="list-item"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { compose } from 'vue-compose'
|
||||
import unescape from 'lodash/unescape'
|
||||
import get from 'lodash/get'
|
||||
import map from 'lodash/map'
|
||||
import reject from 'lodash/reject'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import ImageCropper from '../image_cropper/image_cropper.vue'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
|
|
@ -8,27 +9,24 @@ import ScopeSelector from '../scope_selector/scope_selector.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 SelectableList from '../selectable_list/selectable_list.vue'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import EmojiInput from '../emoji-input/emoji-input.vue'
|
||||
import Autosuggest from '../autosuggest/autosuggest.vue'
|
||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||
import withList from '../../hocs/with_list/with_list'
|
||||
import userSearchApi from '../../services/new_api/user_search.js'
|
||||
|
||||
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 BlockList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
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 MuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const UserSettings = {
|
||||
data () {
|
||||
|
|
@ -73,7 +71,11 @@ const UserSettings = {
|
|||
ImageCropper,
|
||||
BlockList,
|
||||
MuteList,
|
||||
EmojiInput
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
ProgressButton
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
|
@ -334,6 +336,40 @@ const UserSettings = {
|
|||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
||||
this.$store.dispatch('revokeToken', id)
|
||||
}
|
||||
},
|
||||
filterUnblockedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const user = this.$store.getters.findUser(userId)
|
||||
return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
filterUnMutedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const user = this.$store.getters.findUser(userId)
|
||||
return !user || user.muted || user.id === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
queryUserIds (query) {
|
||||
return userSearchApi.search({query, store: this.$store})
|
||||
.then((users) => {
|
||||
this.$store.dispatch('addNewUsers', users)
|
||||
return map(users, 'id')
|
||||
})
|
||||
},
|
||||
blockUsers (ids) {
|
||||
return this.$store.dispatch('blockUsers', ids)
|
||||
},
|
||||
unblockUsers (ids) {
|
||||
return this.$store.dispatch('unblockUsers', ids)
|
||||
},
|
||||
muteUsers (ids) {
|
||||
return this.$store.dispatch('muteUsers', ids)
|
||||
},
|
||||
unmuteUsers (ids) {
|
||||
return this.$store.dispatch('unmuteUsers', ids)
|
||||
},
|
||||
identity (value) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<div class="setting-item" >
|
||||
<h2>{{$t('settings.name_bio')}}</h2>
|
||||
<p>{{$t('settings.name')}}</p>
|
||||
<EmojiInput
|
||||
<EmojiInput
|
||||
type="text"
|
||||
v-model="newName"
|
||||
id="username"
|
||||
|
|
@ -195,15 +195,51 @@
|
|||
</div>
|
||||
|
||||
<div :label="$t('settings.blocks_tab')">
|
||||
<block-list :refresh="true">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest :filter="filterUnblockedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_block')">
|
||||
<BlockCard slot-scope="row" :userId="row.item"/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<BlockList :refresh="true" :getKey="identity">
|
||||
<template slot="header" slot-scope="{selected}">
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => blockUsers(selected)">
|
||||
{{ $t('user_card.block') }}
|
||||
<template slot="progress">{{ $t('user_card.block_progress') }}</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unblockUsers(selected)">
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template slot="progress">{{ $t('user_card.unblock_progress') }}</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="item" slot-scope="{item}"><BlockCard :userId="item" /></template>
|
||||
<template slot="empty">{{$t('settings.no_blocks')}}</template>
|
||||
</block-list>
|
||||
</BlockList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<mute-list :refresh="true">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest :filter="filterUnMutedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_mute')">
|
||||
<MuteCard slot-scope="row" :userId="row.item"/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<MuteList :refresh="true" :getKey="identity">
|
||||
<template slot="header" slot-scope="{selected}">
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => muteUsers(selected)">
|
||||
{{ $t('user_card.mute') }}
|
||||
<template slot="progress">{{ $t('user_card.mute_progress') }}</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unmuteUsers(selected)">
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template slot="progress">{{ $t('user_card.unmute_progress') }}</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="item" slot-scope="{item}"><MuteCard :userId="item" /></template>
|
||||
<template slot="empty">{{$t('settings.no_mutes')}}</template>
|
||||
</mute-list>
|
||||
</MuteList>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
|
|
@ -262,5 +298,19 @@
|
|||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&-usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
&-bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
{{$t('who_to_follow.who_to_follow')}}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FollowCard v-for="user in users" :key="user.id" :user="user"/>
|
||||
<FollowCard v-for="user in users" :key="user.id" :user="user" class="list-item"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue