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

This commit is contained in:
Your New SJW Waifu 2019-10-18 11:07:50 -05:00
commit f883a8875d
20 changed files with 306 additions and 185 deletions

View File

@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased] ## [Unreleased]
### Added ### Added
- Ability to hide/show repeats from user
- User profile button clutter organized into a menu
- Emoji picker - Emoji picker
- Started changelog anew - Started changelog anew
### Changed ### Changed

View File

@ -47,7 +47,7 @@ export default (store) => {
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings }, { name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset }, { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },

View File

@ -0,0 +1,35 @@
import ProgressButton from '../progress_button/progress_button.vue'
const AccountActions = {
props: [
'user'
],
data () {
return { }
},
components: {
ProgressButton
},
methods: {
showRepeats () {
this.$store.dispatch('showReblogs', this.user.id)
},
hideRepeats () {
this.$store.dispatch('hideReblogs', this.user.id)
},
blockUser () {
this.$store.dispatch('blockUser', this.user.id)
},
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}
export default AccountActions

View File

@ -0,0 +1,93 @@
<template>
<div class="account-actions">
<v-popover
trigger="click"
class="account-tools-popover"
:container="false"
placement="bottom-end"
:offset="5"
>
<div slot="popover">
<div class="dropdown-menu">
<button
class="btn btn-default btn-block dropdown-item"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
<template v-if="user.following">
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.showing_reblogs"
class="btn btn-default dropdown-item"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
<button
v-if="!user.showing_reblogs"
class="btn btn-default dropdown-item"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
</button>
</template>
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.statusnet_blocking"
class="btn btn-default btn-block dropdown-item"
@click="unblockUser"
>
{{ $t('user_card.unblock') }}
</button>
<button
v-else
class="btn btn-default btn-block dropdown-item"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
<button
class="btn btn-default btn-block dropdown-item"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
</div>
</div>
<div class="btn btn-default ellipsis-button">
<i class="icon-ellipsis trigger-button" />
</div>
</v-popover>
</div>
</template>
<script src="./account_actions.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import '../popper/popper.scss';
.account-actions {
margin: 0 .8em;
}
.account-actions button.dropdown-item {
margin-left: 0;
}
.account-actions .trigger-button {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
opacity: .8;
cursor: pointer;
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
</style>

View File

@ -0,0 +1,53 @@
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['user', 'labelFollowing', 'buttonClass'],
data () {
return {
inProgress: false
}
},
computed: {
isPressed () {
return this.inProgress || this.user.following
},
title () {
if (this.inProgress || this.user.following) {
return this.$t('user_card.follow_unfollow')
} else if (this.user.requested) {
return this.$t('user_card.follow_again')
} else {
return this.$t('user_card.follow')
}
},
label () {
if (this.inProgress) {
return this.$t('user_card.follow_progress')
} else if (this.user.following) {
return this.labelFollowing || this.$t('user_card.following')
} else if (this.user.requested) {
return this.$t('user_card.follow_sent')
} else {
return this.$t('user_card.follow')
}
}
},
methods: {
onClick () {
this.user.following ? this.unfollow() : this.follow()
},
follow () {
this.inProgress = true
requestFollow(this.user, this.$store).then(() => {
this.inProgress = false
})
},
unfollow () {
const store = this.$store
this.inProgress = true
requestUnfollow(this.user, store).then(() => {
this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
})
}
}
}

View File

@ -0,0 +1,13 @@
<template>
<button
class="btn btn-default follow-button"
:class="{ pressed: isPressed }"
:disabled="inProgress"
:title="title"
@click="onClick"
>
{{ label }}
</button>
</template>
<script src="./follow_button.js"></script>

View File

@ -1,20 +1,16 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import FollowButton from '../follow_button/follow_button.vue'
const FollowCard = { const FollowCard = {
props: [ props: [
'user', 'user',
'noFollowsYou' 'noFollowsYou'
], ],
data () {
return {
inProgress: false
}
},
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow RemoteFollow,
FollowButton
}, },
computed: { computed: {
isMe () { isMe () {
@ -23,20 +19,6 @@ const FollowCard = {
loggedIn () { loggedIn () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
} }
},
methods: {
followUser () {
this.inProgress = true
requestFollow(this.user, this.$store).then(() => {
this.inProgress = false
})
},
unfollowUser () {
this.inProgress = true
requestUnfollow(this.user, this.$store).then(() => {
this.inProgress = false
})
}
} }
} }

View File

@ -16,36 +16,11 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<button <FollowButton
v-if="!user.following" :user="user"
class="btn btn-default follow-card-follow-button" class="follow-card-follow-button"
:disabled="inProgress" :label-following="$t('user_card.follow_unfollow')"
:title="user.requested ? $t('user_card.follow_again') : ''" />
@click="followUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="user.requested">
{{ $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"
:disabled="inProgress"
@click="unfollowUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</template> </template>
</div> </div>
</basic-user-card> </basic-user-card>

View File

@ -59,6 +59,8 @@ const LoginForm = {
if (result.error) { if (result.error) {
if (result.error === 'mfa_required') { if (result.error === 'mfa_required') {
this.requireMFA({ app: app, settings: result }) this.requireMFA({ app: app, settings: result })
} else if (result.identifier === 'password_reset_required') {
this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } })
} else { } else {
this.error = result.error this.error = result.error
this.focusOnPasswordInput() this.focusOnPasswordInput()

View File

@ -25,6 +25,12 @@ const passwordReset = {
this.$router.push({ name: 'root' }) this.$router.push({ name: 'root' })
} }
}, },
props: {
passwordResetRequested: {
default: false,
type: Boolean
}
},
methods: { methods: {
dismissError () { dismissError () {
this.error = null this.error = null

View File

@ -10,7 +10,10 @@
> >
<div class="container"> <div class="container">
<div v-if="!mailerEnabled"> <div v-if="!mailerEnabled">
<p> <p v-if="passwordResetRequested">
{{ $t('password_reset.password_reset_required_but_mailer_is_disabled') }}
</p>
<p v-else>
{{ $t('password_reset.password_reset_disabled') }} {{ $t('password_reset.password_reset_disabled') }}
</p> </p>
</div> </div>
@ -25,6 +28,12 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<p
v-if="passwordResetRequested"
class="password-reset-required error"
>
{{ $t('password_reset.password_reset_required') }}
</p>
<p> <p>
{{ $t('password_reset.instruction') }} {{ $t('password_reset.instruction') }}
</p> </p>
@ -104,6 +113,11 @@
margin: 0.3em 0.0em 1em; margin: 0.3em 0.0em 1em;
} }
.password-reset-required {
background-color: var(--alertError, $fallback--alertError);
padding: 10px 0;
}
.notice-dismissible { .notice-dismissible {
padding-right: 2rem; padding-right: 2rem;
} }

View File

@ -840,6 +840,11 @@ $status-margin: 0.75em;
&.button-icon-active { &.button-icon-active {
color: $fallback--cBlue; color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue); color: var(--cBlue, $fallback--cBlue);
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled) {
cursor: pointer; cursor: pointer;
} }
} }

View File

@ -1,13 +1,16 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
export default { export default {
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ], props: [
'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
],
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,
@ -96,30 +99,11 @@ export default {
UserAvatar, UserAvatar,
RemoteFollow, RemoteFollow,
ModerationTools, ModerationTools,
ProgressButton AccountActions,
ProgressButton,
FollowButton
}, },
methods: { methods: {
followUser () {
const store = this.$store
this.followRequestInProgress = true
requestFollow(this.user, store).then(() => {
this.followRequestInProgress = false
})
},
unfollowUser () {
const store = this.$store
this.followRequestInProgress = true
requestUnfollow(this.user, store).then(() => {
this.followRequestInProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
})
},
blockUser () {
this.$store.dispatch('blockUser', this.user.id)
},
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
muteUser () { muteUser () {
this.$store.dispatch('muteUser', this.user.id) this.$store.dispatch('muteUser', this.user.id)
}, },
@ -147,10 +131,10 @@ export default {
} }
}, },
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
}, user.id, user.screen_name,
reportUser () { this.$store.state.instance.restrictedNicknames
this.$store.dispatch('openUserReportingModal', this.user.id) )
}, },
zoomAvatar () { zoomAvatar () {
const attachment = { const attachment = {
@ -159,9 +143,6 @@ export default {
} }
this.$store.dispatch('setMedia', [attachment]) this.$store.dispatch('setMedia', [attachment])
this.$store.dispatch('setCurrent', attachment) this.$store.dispatch('setCurrent', attachment)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
} }
} }
} }

View File

@ -66,8 +66,11 @@
> >
<i class="icon-link-ext usersettings" /> <i class="icon-link-ext usersettings" />
</a> </a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
/>
</div> </div>
<div class="bottom-line"> <div class="bottom-line">
<router-link <router-link
class="user-screen-name" class="user-screen-name"
@ -135,72 +138,27 @@
v-if="loggedIn && isOtherUser" v-if="loggedIn && isOtherUser"
class="user-interactions" class="user-interactions"
> >
<div v-if="!user.following"> <div class="btn-group">
<button <FollowButton :user="user" />
class="btn btn-default btn-block" <template v-if="user.following">
:disabled="followRequestInProgress" <ProgressButton
:title="user.requested ? $t('user_card.follow_again') : ''" v-if="!user.subscribed"
@click="followUser" class="btn btn-default"
> :click="subscribeUser"
<template v-if="followRequestInProgress"> :title="$t('user_card.subscribe')"
{{ $t('user_card.follow_progress') }} >
</template> <i class="icon-bell-alt" />
<template v-else-if="user.requested"> </ProgressButton>
{{ $t('user_card.follow_sent') }} <ProgressButton
</template> v-else
<template v-else> class="btn btn-default pressed"
{{ $t('user_card.follow') }} :click="unsubscribeUser"
</template> :title="$t('user_card.unsubscribe')"
</button> >
<i class="icon-bell-ringing-o" />
</ProgressButton>
</template>
</div> </div>
<div v-else-if="followRequestInProgress">
<button
class="btn btn-default btn-block pressed"
disabled
:title="$t('user_card.follow_unfollow')"
@click="unfollowUser"
>
{{ $t('user_card.follow_progress') }}
</button>
</div>
<div
v-else
class="btn-group"
>
<button
class="btn btn-default pressed"
:title="$t('user_card.follow_unfollow')"
@click="unfollowUser"
>
{{ $t('user_card.following') }}
</button>
<ProgressButton
v-if="!user.subscribed"
class="btn btn-default"
:click="subscribeUser"
:title="$t('user_card.subscribe')"
>
<i class="icon-bell-alt" />
</ProgressButton>
<ProgressButton
v-else
class="btn btn-default pressed"
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
<i class="icon-bell-ringing-o" />
</ProgressButton>
</div>
<div>
<button
class="btn btn-default btn-block"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
</div>
<div> <div>
<button <button
v-if="user.muted" v-if="user.muted"
@ -217,33 +175,6 @@
{{ $t('user_card.mute') }} {{ $t('user_card.mute') }}
</button> </button>
</div> </div>
<div>
<button
v-if="user.statusnet_blocking"
class="btn btn-default btn-block pressed"
@click="unblockUser"
>
{{ $t('user_card.blocked') }}
</button>
<button
v-else
class="btn btn-default btn-block"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
</div>
<div>
<button
class="btn btn-default btn-block"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
</div>
<ModerationTools <ModerationTools
v-if="loggedIn.role === &quot;admin&quot;" v-if="loggedIn.role === &quot;admin&quot;"
:user="user" :user="user"
@ -587,13 +518,12 @@
position: relative; position: relative;
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between;
margin-right: -.75em; margin-right: -.75em;
> * { > * {
flex: 1 0 0;
margin: 0 .75em .6em 0; margin: 0 .75em .6em 0;
white-space: nowrap; white-space: nowrap;
min-width: 95px;
} }
button { button {

View File

@ -555,6 +555,8 @@
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting...", "unmute_progress": "Unmuting...",
"mute_progress": "Muting...", "mute_progress": "Muting...",
"hide_repeats": "Hide repeats",
"show_repeats": "Show repeats",
"admin_menu": { "admin_menu": {
"moderation": "Moderation", "moderation": "Moderation",
"grant_admin": "Grant Admin", "grant_admin": "Grant Admin",
@ -630,6 +632,8 @@
"return_home": "Return to the home page", "return_home": "Return to the home page",
"not_found": "We couldn't find that email or username.", "not_found": "We couldn't find that email or username.",
"too_many_requests": "You have reached the limit of attempts, try again later.", "too_many_requests": "You have reached the limit of attempts, try again later.",
"password_reset_disabled": "Password reset is disabled. Please contact your instance administrator." "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
"password_reset_required": "You must reset your password to log in.",
"password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
} }
} }

View File

@ -60,6 +60,18 @@ const unmuteUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
const hideReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
})
}
const showReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
export const mutations = { export const mutations = {
setMuted (state, { user: { id }, muted }) { setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
@ -135,6 +147,7 @@ export const mutations = {
user.muted = relationship.muting user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking user.statusnet_blocking = relationship.blocking
user.subscribed = relationship.subscribing user.subscribed = relationship.subscribing
user.showing_reblogs = relationship.showing_reblogs
} }
}) })
}, },
@ -272,6 +285,12 @@ const users = {
unmuteUser (store, id) { unmuteUser (store, id) {
return unmuteUser(store, id) return unmuteUser(store, id)
}, },
hideReblogs (store, id) {
return hideReblogs(store, id)
},
showReblogs (store, id) {
return showReblogs(store, id)
},
muteUsers (store, ids = []) { muteUsers (store, ids = []) {
return Promise.all(ids.map(id => muteUser(store, id))) return Promise.all(ids.map(id => muteUser(store, id)))
}, },

View File

@ -219,10 +219,16 @@ const authHeaders = (accessToken) => {
} }
} }
const followUser = ({ id, credentials }) => { const followUser = ({ id, credentials, ...options }) => {
let url = MASTODON_FOLLOW_URL(id) let url = MASTODON_FOLLOW_URL(id)
const form = {}
if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs }
return fetch(url, { return fetch(url, {
headers: authHeaders(credentials), body: JSON.stringify(form),
headers: {
...authHeaders(credentials),
'Content-Type': 'application/json'
},
method: 'POST' method: 'POST'
}).then((data) => data.json()) }).then((data) => data.json())
} }

View File

@ -31,8 +31,8 @@ const backendInteractorService = credentials => {
return apiService.fetchUserRelationship({ id, credentials }) return apiService.fetchUserRelationship({ id, credentials })
} }
const followUser = (id) => { const followUser = ({ id, reblogs }) => {
return apiService.followUser({ credentials, id }) return apiService.followUser({ credentials, id, reblogs })
} }
const unfollowUser = (id) => { const unfollowUser = (id) => {

View File

@ -69,6 +69,7 @@ export const parseUser = (data) => {
output.following = relationship.following output.following = relationship.following
output.statusnet_blocking = relationship.blocking output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting output.muted = relationship.muting
output.showing_reblogs = relationship.showing_reblogs
output.subscribed = relationship.subscribing output.subscribed = relationship.subscribing
} }

View File

@ -14,7 +14,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
}) })
export const requestFollow = (user, store) => new Promise((resolve, reject) => { export const requestFollow = (user, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.followUser(user.id) store.state.api.backendInteractor.followUser({ id: user.id })
.then((updated) => { .then((updated) => {
store.commit('updateUserRelationship', [updated]) store.commit('updateUserRelationship', [updated])