Block and unblock an account (#125)
One of the many features listed in #6
This commit is contained in:
parent
6169b53643
commit
732d1fded4
27
routes/_actions/block.js
Normal file
27
routes/_actions/block.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { store } from '../_store/store'
|
||||
import { blockAccount, unblockAccount } from '../_api/block'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
|
||||
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
||||
let instanceName = store.get('currentInstance')
|
||||
let accessToken = store.get('accessToken')
|
||||
try {
|
||||
if (block) {
|
||||
await blockAccount(instanceName, accessToken, accountId)
|
||||
} else {
|
||||
await unblockAccount(instanceName, accessToken, accountId)
|
||||
}
|
||||
await updateProfileAndRelationship(accountId)
|
||||
if (toastOnSuccess) {
|
||||
if (block) {
|
||||
toast.say('Blocked account')
|
||||
} else {
|
||||
toast.say('Unblocked account')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${block ? 'block' : 'unblock'} account: ` + (e.message || ''))
|
||||
}
|
||||
}
|
12
routes/_api/block.js
Normal file
12
routes/_api/block.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { auth, basename } from './utils'
|
||||
import { postWithTimeout } from '../_utils/ajax'
|
||||
|
||||
export async function blockAccount (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/block`
|
||||
return postWithTimeout(url, null, auth(accessToken))
|
||||
}
|
||||
|
||||
export async function unblockAccount (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unblock`
|
||||
return postWithTimeout(url, null, auth(accessToken))
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
{{#if delegateKey}}
|
||||
<button type="button"
|
||||
title="{{label}}"
|
||||
aria-label="{{label}}"
|
||||
aria-pressed="{{pressable ? !!pressed : ''}}"
|
||||
class="{{computedClass}}"
|
||||
|
@ -12,6 +13,7 @@
|
|||
</button>
|
||||
{{else}}
|
||||
<button type="button"
|
||||
title="{{label}}"
|
||||
aria-label="{{label}}"
|
||||
aria-pressed="{{pressable ? !!pressed : ''}}"
|
||||
class="{{computedClass}}"
|
||||
|
|
|
@ -15,6 +15,7 @@ import { createDialogId } from '../helpers/createDialogId'
|
|||
import { show } from '../helpers/showDialog'
|
||||
import { close } from '../helpers/closeDialog'
|
||||
import { oncreate } from '../helpers/onCreateDialog'
|
||||
import { setAccountBlocked } from '../../../_actions/block'
|
||||
|
||||
export default {
|
||||
oncreate,
|
||||
|
@ -23,12 +24,18 @@ export default {
|
|||
id: createDialogId()
|
||||
}),
|
||||
computed: {
|
||||
items: (account) => (
|
||||
blocking: (relationship) => relationship && relationship.blocking,
|
||||
items: (account, blocking) => (
|
||||
[
|
||||
{
|
||||
key: 'mention',
|
||||
label: 'Mention @' + (account.acct),
|
||||
label: `Mention @${account.acct}`,
|
||||
icon: '#fa-comments'
|
||||
},
|
||||
{
|
||||
key: 'block',
|
||||
label: blocking ? `Unblock @${account.acct}` : `Block @${account.acct}`,
|
||||
icon: blocking ? '#fa-unlock' : '#fa-ban'
|
||||
}
|
||||
]
|
||||
)
|
||||
|
@ -36,7 +43,15 @@ export default {
|
|||
methods: {
|
||||
show,
|
||||
close,
|
||||
async onClick() {
|
||||
onClick(item) {
|
||||
switch (item.key) {
|
||||
case 'mention':
|
||||
return this.onMentionClicked()
|
||||
case 'block':
|
||||
return this.onBlockClicked()
|
||||
}
|
||||
},
|
||||
async onMentionClicked() {
|
||||
let account = this.get('account')
|
||||
this.store.setComposeData('dialog', {
|
||||
text: `@${account.acct} `
|
||||
|
@ -44,6 +59,13 @@ export default {
|
|||
let dialogs = await importDialogs()
|
||||
dialogs.showComposeDialog()
|
||||
this.close()
|
||||
},
|
||||
async onBlockClicked() {
|
||||
let account = this.get('account')
|
||||
let blocking = this.get('blocking')
|
||||
let accountId = account.id
|
||||
this.close()
|
||||
await setAccountBlocked(accountId, !blocking, true)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { doDeleteStatus } from '../../../_actions/delete'
|
|||
import { show } from '../helpers/showDialog'
|
||||
import { close } from '../helpers/closeDialog'
|
||||
import { oncreate } from '../helpers/onCreateDialog'
|
||||
import { setAccountBlocked } from '../../../_actions/block'
|
||||
|
||||
export default {
|
||||
oncreate,
|
||||
|
@ -26,6 +27,7 @@ export default {
|
|||
following: (relationship) => relationship && relationship.following,
|
||||
followRequested: (relationship) => relationship && relationship.requested,
|
||||
accountId: (account) => account && account.id,
|
||||
blocking: (relationship) => relationship.blocking,
|
||||
followLabel: (following, followRequested, account) => {
|
||||
if (typeof following === 'undefined' || !account) {
|
||||
return ''
|
||||
|
@ -34,19 +36,28 @@ export default {
|
|||
? `Unfollow @${account.acct}`
|
||||
: `Follow @${account.acct}`
|
||||
},
|
||||
items: (followLabel, following, followRequested, accountId, verifyCredentialsId) => (
|
||||
blockLabel: (blocking, account) => {
|
||||
return blocking ? `Unblock @${account.acct}` : `Block @${account.acct}`
|
||||
},
|
||||
items: (blockLabel, blocking, followLabel, following, followRequested, accountId, verifyCredentialsId) => (
|
||||
[
|
||||
accountId !== verifyCredentialsId &&
|
||||
{
|
||||
key: 'follow',
|
||||
label: followLabel,
|
||||
icon: following ? '#fa-user-times' : followRequested ? '#fa-hourglass' : '#fa-user-plus'
|
||||
},
|
||||
accountId === verifyCredentialsId &&
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
icon: '#fa-trash'
|
||||
},
|
||||
accountId !== verifyCredentialsId && !blocking &&
|
||||
{
|
||||
key: 'follow',
|
||||
label: followLabel,
|
||||
icon: following ? '#fa-user-times' : followRequested ? '#fa-hourglass' : '#fa-user-plus'
|
||||
},
|
||||
accountId !== verifyCredentialsId &&
|
||||
{
|
||||
key: 'block',
|
||||
label: blockLabel,
|
||||
icon: blocking ? '#fa-unlock' : '#fa-ban'
|
||||
}
|
||||
].filter(Boolean)
|
||||
)
|
||||
|
@ -59,17 +70,32 @@ export default {
|
|||
methods: {
|
||||
show,
|
||||
close,
|
||||
async onClick(item) {
|
||||
if (item.key === 'follow') {
|
||||
let accountId = this.get('accountId')
|
||||
let following = this.get('following')
|
||||
await setAccountFollowed(accountId, !following, true)
|
||||
this.close()
|
||||
} else if (item.key === 'delete') {
|
||||
let statusId = this.get('statusId')
|
||||
await doDeleteStatus(statusId)
|
||||
this.close()
|
||||
onClick(item) {
|
||||
switch (item.key) {
|
||||
case 'delete':
|
||||
return this.onDeleteClicked()
|
||||
case 'follow':
|
||||
return this.onFollowClicked()
|
||||
case 'block':
|
||||
return this.onBlockClicked()
|
||||
}
|
||||
},
|
||||
async onDeleteClicked() {
|
||||
let statusId = this.get('statusId')
|
||||
this.close()
|
||||
await doDeleteStatus(statusId)
|
||||
},
|
||||
async onFollowClicked() {
|
||||
let accountId = this.get('accountId')
|
||||
let following = this.get('following')
|
||||
this.close()
|
||||
await setAccountFollowed(accountId, !following, true)
|
||||
},
|
||||
async onBlockClicked() {
|
||||
let accountId = this.get('accountId')
|
||||
let blocking = this.get('blocking')
|
||||
this.close()
|
||||
await setAccountBlocked(accountId, !blocking, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,15 @@ import AccountProfileOptionsDialog from '../components/AccountProfileOptionsDial
|
|||
import { createDialogElement } from '../helpers/createDialogElement'
|
||||
import { createDialogId } from '../helpers/createDialogId'
|
||||
|
||||
export function showAccountProfileOptionsDialog (account) {
|
||||
export function showAccountProfileOptionsDialog (account, relationship) {
|
||||
let dialog = new AccountProfileOptionsDialog({
|
||||
target: createDialogElement(),
|
||||
data: {
|
||||
id: createDialogId(),
|
||||
label: 'Profile options dialog',
|
||||
title: '',
|
||||
account: account
|
||||
account: account,
|
||||
relationship: relationship
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<AccountProfileHeader :account :relationship :verifyCredentials />
|
||||
<AccountProfileFollow :account :relationship :verifyCredentials />
|
||||
<AccountProfileNote :account />
|
||||
<AccountProfileDetails :account />
|
||||
<AccountProfileDetails :account :relationship />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -119,8 +119,9 @@
|
|||
methods: {
|
||||
async onMoreOptionsClick() {
|
||||
let account = this.get('account')
|
||||
let relationship = this.get('relationship')
|
||||
let dialogs = await importDialogs()
|
||||
dialogs.showAccountProfileOptionsDialog(account)
|
||||
dialogs.showAccountProfileOptionsDialog(account, relationship)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
import { FOLLOW_BUTTON_ANIMATION } from '../../_static/animations'
|
||||
import { store } from '../../_store/store'
|
||||
import { setAccountFollowed } from '../../_actions/follow'
|
||||
import { setAccountBlocked } from '../../_actions/block'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
|
@ -34,12 +35,18 @@
|
|||
let accountId = this.get('accountId')
|
||||
let following = this.get('following')
|
||||
let followRequested = this.get('followRequested')
|
||||
let blocking = this.get('blocking')
|
||||
this.set({animateFollowButton: true}) // TODO: this should be an event, not toggling a boolean
|
||||
let newFollowingValue = !(following || followRequested)
|
||||
if (!account.locked) { // be optimistic, show the user that it succeeded
|
||||
this.set({overrideFollowing: newFollowingValue})
|
||||
if (blocking) { // unblock
|
||||
await setAccountBlocked(accountId, false)
|
||||
} else { // follow/unfollow
|
||||
let newFollowingValue = !(following || followRequested)
|
||||
if (!account.locked) { // be optimistic, show the user that it succeeded
|
||||
this.set({overrideFollowing: newFollowingValue})
|
||||
}
|
||||
await setAccountFollowed(accountId, newFollowingValue)
|
||||
}
|
||||
await setAccountFollowed(accountId, newFollowingValue)
|
||||
|
||||
this.set({animateFollowButton: false}) // let animation play next time
|
||||
}
|
||||
},
|
||||
|
@ -55,11 +62,14 @@
|
|||
}
|
||||
return relationship && relationship.following
|
||||
},
|
||||
blocking: (relationship) => relationship.blocking,
|
||||
followRequested: (relationship, account) => {
|
||||
return relationship && relationship.requested && account && account.locked
|
||||
},
|
||||
followLabel: (following, followRequested) => {
|
||||
if (following) {
|
||||
followLabel: (blocking, following, followRequested) => {
|
||||
if (blocking) {
|
||||
return 'Unblock'
|
||||
} else if (following) {
|
||||
return 'Unfollow'
|
||||
} else if (followRequested) {
|
||||
return 'Unfollow (follow requested)'
|
||||
|
@ -67,8 +77,10 @@
|
|||
return 'Follow'
|
||||
}
|
||||
},
|
||||
followIcon: (following, followRequested) => {
|
||||
if (following) {
|
||||
followIcon: (blocking, following, followRequested) => {
|
||||
if (blocking) {
|
||||
return '#fa-unlock'
|
||||
} else if (following) {
|
||||
return '#fa-user-times'
|
||||
} else if (followRequested) {
|
||||
return '#fa-hourglass'
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
{{'@' + account.acct}}
|
||||
</div>
|
||||
<div class="account-profile-followed-by">
|
||||
{{#if relationship && relationship.followed_by}}
|
||||
{{#if relationship && relationship.blocking}}
|
||||
<span class="account-profile-followed-by-span">Blocked</span>
|
||||
{{elseif relationship && relationship.followed_by}}
|
||||
<span class="account-profile-followed-by-span">Follows you</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
53
tests/spec/113-block-unblock.js
Normal file
53
tests/spec/113-block-unblock.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
accountProfileFollowButton,
|
||||
accountProfileFollowedBy, accountProfileMoreOptionsButton, communityNavButton, getNthSearchResult,
|
||||
getNthStatus, getNthStatusOptionsButton, getNthDialogOptionsOption, getUrl, modalDialog
|
||||
} from '../utils'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
import { foobarRole } from '../roles'
|
||||
import { postAs } from '../serverActions'
|
||||
|
||||
fixture`113-block-unblock.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('Can block and unblock an account from a status', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
let post = 'a very silly statement that should probably get me blocked'
|
||||
await postAs('admin', post)
|
||||
|
||||
await t.expect(getNthStatus(0).innerText).contains(post, {timeout: 20000})
|
||||
.click(getNthStatusOptionsButton(0))
|
||||
.expect(getNthDialogOptionsOption(1).innerText).contains('Unfollow @admin')
|
||||
.expect(getNthDialogOptionsOption(2).innerText).contains('Block @admin')
|
||||
.click(getNthDialogOptionsOption(2))
|
||||
.expect(modalDialog.exists).notOk()
|
||||
.click(communityNavButton)
|
||||
.click($('a[href="/blocked"]'))
|
||||
.expect(getNthSearchResult(1).innerText).contains('@admin')
|
||||
.click(getNthSearchResult(1))
|
||||
.expect(getUrl()).contains('/accounts/1')
|
||||
.expect(accountProfileFollowedBy.innerText).match(/blocked/i)
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unblock')
|
||||
.click(accountProfileFollowButton)
|
||||
.expect(accountProfileFollowedBy.innerText).contains('')
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
|
||||
})
|
||||
|
||||
test('Can block and unblock an account from the account profile page', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.navigateTo('/accounts/5')
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
|
||||
.click(accountProfileMoreOptionsButton)
|
||||
.expect(getNthDialogOptionsOption(1).innerText).contains('Mention @baz')
|
||||
.expect(getNthDialogOptionsOption(2).innerText).contains('Block @baz')
|
||||
.click(getNthDialogOptionsOption(2))
|
||||
.expect(accountProfileFollowedBy.innerText).match(/blocked/i)
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unblock')
|
||||
.click(accountProfileFollowButton)
|
||||
.expect(accountProfileFollowedBy.innerText).contains('')
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
|
||||
.click(accountProfileFollowButton)
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
|
||||
.click(accountProfileFollowButton)
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
|
||||
})
|
|
@ -13,6 +13,7 @@ export const notificationsNavButton = $('nav a[href="/notifications"]')
|
|||
export const homeNavButton = $('nav a[href="/"]')
|
||||
export const localTimelineNavButton = $('nav a[href="/local"]')
|
||||
export const searchNavButton = $('nav a[href="/search"]')
|
||||
export const communityNavButton = $('nav a[href="/community"]')
|
||||
export const formError = $('.form-error-user-error')
|
||||
export const composeInput = $('.compose-box-input')
|
||||
export const composeContentWarning = $('.content-warning-input')
|
||||
|
@ -34,6 +35,7 @@ export const accountProfileUsername = $('.account-profile .account-profile-usern
|
|||
export const accountProfileFollowedBy = $('.account-profile .account-profile-followed-by')
|
||||
export const accountProfileFollowButton = $('.account-profile .account-profile-follow button')
|
||||
export const goBackButton = $('.dynamic-page-go-back')
|
||||
export const accountProfileMoreOptionsButton = $('.account-profile-more-options button')
|
||||
|
||||
export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({
|
||||
innerCount: el => parseInt(el.innerText, 10)
|
||||
|
@ -169,6 +171,10 @@ export function getNthFavoriteButton (n) {
|
|||
return getNthStatus(n).find('.status-toolbar button:nth-child(3)')
|
||||
}
|
||||
|
||||
export function getNthStatusOptionsButton (n) {
|
||||
return getNthStatus(n).find('.status-toolbar button:nth-child(4)')
|
||||
}
|
||||
|
||||
export function getNthFavorited (n) {
|
||||
return getNthFavoriteButton(n).getAttribute('aria-pressed')
|
||||
}
|
||||
|
@ -189,6 +195,10 @@ export function getNthReblogged (n) {
|
|||
return getNthReblogButton(n).getAttribute('aria-pressed')
|
||||
}
|
||||
|
||||
export function getNthDialogOptionsOption (n) {
|
||||
return $(`.modal-dialog li:nth-child(${n}) button`)
|
||||
}
|
||||
|
||||
export function getReblogsCount () {
|
||||
return reblogsCountElement.innerCount
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue