Block and unblock an account (#125)

One of the many features listed in #6
This commit is contained in:
Nolan Lawson 2018-04-14 18:47:55 -07:00 committed by GitHub
parent 6169b53643
commit 732d1fded4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 201 additions and 33 deletions

27
routes/_actions/block.js Normal file
View 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
View 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))
}

View file

@ -1,5 +1,6 @@
{{#if delegateKey}} {{#if delegateKey}}
<button type="button" <button type="button"
title="{{label}}"
aria-label="{{label}}" aria-label="{{label}}"
aria-pressed="{{pressable ? !!pressed : ''}}" aria-pressed="{{pressable ? !!pressed : ''}}"
class="{{computedClass}}" class="{{computedClass}}"
@ -12,6 +13,7 @@
</button> </button>
{{else}} {{else}}
<button type="button" <button type="button"
title="{{label}}"
aria-label="{{label}}" aria-label="{{label}}"
aria-pressed="{{pressable ? !!pressed : ''}}" aria-pressed="{{pressable ? !!pressed : ''}}"
class="{{computedClass}}" class="{{computedClass}}"

View file

@ -15,6 +15,7 @@ import { createDialogId } from '../helpers/createDialogId'
import { show } from '../helpers/showDialog' import { show } from '../helpers/showDialog'
import { close } from '../helpers/closeDialog' import { close } from '../helpers/closeDialog'
import { oncreate } from '../helpers/onCreateDialog' import { oncreate } from '../helpers/onCreateDialog'
import { setAccountBlocked } from '../../../_actions/block'
export default { export default {
oncreate, oncreate,
@ -23,12 +24,18 @@ export default {
id: createDialogId() id: createDialogId()
}), }),
computed: { computed: {
items: (account) => ( blocking: (relationship) => relationship && relationship.blocking,
items: (account, blocking) => (
[ [
{ {
key: 'mention', key: 'mention',
label: 'Mention @' + (account.acct), label: `Mention @${account.acct}`,
icon: '#fa-comments' 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: { methods: {
show, show,
close, 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') let account = this.get('account')
this.store.setComposeData('dialog', { this.store.setComposeData('dialog', {
text: `@${account.acct} ` text: `@${account.acct} `
@ -44,6 +59,13 @@ export default {
let dialogs = await importDialogs() let dialogs = await importDialogs()
dialogs.showComposeDialog() dialogs.showComposeDialog()
this.close() 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: { components: {

View file

@ -15,6 +15,7 @@ import { doDeleteStatus } from '../../../_actions/delete'
import { show } from '../helpers/showDialog' import { show } from '../helpers/showDialog'
import { close } from '../helpers/closeDialog' import { close } from '../helpers/closeDialog'
import { oncreate } from '../helpers/onCreateDialog' import { oncreate } from '../helpers/onCreateDialog'
import { setAccountBlocked } from '../../../_actions/block'
export default { export default {
oncreate, oncreate,
@ -26,6 +27,7 @@ export default {
following: (relationship) => relationship && relationship.following, following: (relationship) => relationship && relationship.following,
followRequested: (relationship) => relationship && relationship.requested, followRequested: (relationship) => relationship && relationship.requested,
accountId: (account) => account && account.id, accountId: (account) => account && account.id,
blocking: (relationship) => relationship.blocking,
followLabel: (following, followRequested, account) => { followLabel: (following, followRequested, account) => {
if (typeof following === 'undefined' || !account) { if (typeof following === 'undefined' || !account) {
return '' return ''
@ -34,19 +36,28 @@ export default {
? `Unfollow @${account.acct}` ? `Unfollow @${account.acct}`
: `Follow @${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 && accountId === verifyCredentialsId &&
{ {
key: 'delete', key: 'delete',
label: 'Delete', label: 'Delete',
icon: '#fa-trash' 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) ].filter(Boolean)
) )
@ -59,17 +70,32 @@ export default {
methods: { methods: {
show, show,
close, close,
async onClick(item) { onClick(item) {
if (item.key === 'follow') { switch (item.key) {
let accountId = this.get('accountId') case 'delete':
let following = this.get('following') return this.onDeleteClicked()
await setAccountFollowed(accountId, !following, true) case 'follow':
this.close() return this.onFollowClicked()
} else if (item.key === 'delete') { case 'block':
let statusId = this.get('statusId') return this.onBlockClicked()
await doDeleteStatus(statusId)
this.close()
} }
},
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)
} }
} }
} }

View file

@ -2,14 +2,15 @@ import AccountProfileOptionsDialog from '../components/AccountProfileOptionsDial
import { createDialogElement } from '../helpers/createDialogElement' import { createDialogElement } from '../helpers/createDialogElement'
import { createDialogId } from '../helpers/createDialogId' import { createDialogId } from '../helpers/createDialogId'
export function showAccountProfileOptionsDialog (account) { export function showAccountProfileOptionsDialog (account, relationship) {
let dialog = new AccountProfileOptionsDialog({ let dialog = new AccountProfileOptionsDialog({
target: createDialogElement(), target: createDialogElement(),
data: { data: {
id: createDialogId(), id: createDialogId(),
label: 'Profile options dialog', label: 'Profile options dialog',
title: '', title: '',
account: account account: account,
relationship: relationship
} }
}) })
dialog.show() dialog.show()

View file

@ -5,7 +5,7 @@
<AccountProfileHeader :account :relationship :verifyCredentials /> <AccountProfileHeader :account :relationship :verifyCredentials />
<AccountProfileFollow :account :relationship :verifyCredentials /> <AccountProfileFollow :account :relationship :verifyCredentials />
<AccountProfileNote :account /> <AccountProfileNote :account />
<AccountProfileDetails :account /> <AccountProfileDetails :account :relationship />
</div> </div>
</div> </div>
</div> </div>

View file

@ -119,8 +119,9 @@
methods: { methods: {
async onMoreOptionsClick() { async onMoreOptionsClick() {
let account = this.get('account') let account = this.get('account')
let relationship = this.get('relationship')
let dialogs = await importDialogs() let dialogs = await importDialogs()
dialogs.showAccountProfileOptionsDialog(account) dialogs.showAccountProfileOptionsDialog(account, relationship)
} }
}, },
components: { components: {

View file

@ -24,6 +24,7 @@
import { FOLLOW_BUTTON_ANIMATION } from '../../_static/animations' import { FOLLOW_BUTTON_ANIMATION } from '../../_static/animations'
import { store } from '../../_store/store' import { store } from '../../_store/store'
import { setAccountFollowed } from '../../_actions/follow' import { setAccountFollowed } from '../../_actions/follow'
import { setAccountBlocked } from '../../_actions/block'
export default { export default {
methods: { methods: {
@ -34,12 +35,18 @@
let accountId = this.get('accountId') let accountId = this.get('accountId')
let following = this.get('following') let following = this.get('following')
let followRequested = this.get('followRequested') let followRequested = this.get('followRequested')
let blocking = this.get('blocking')
this.set({animateFollowButton: true}) // TODO: this should be an event, not toggling a boolean this.set({animateFollowButton: true}) // TODO: this should be an event, not toggling a boolean
let newFollowingValue = !(following || followRequested) if (blocking) { // unblock
if (!account.locked) { // be optimistic, show the user that it succeeded await setAccountBlocked(accountId, false)
this.set({overrideFollowing: newFollowingValue}) } 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 this.set({animateFollowButton: false}) // let animation play next time
} }
}, },
@ -55,11 +62,14 @@
} }
return relationship && relationship.following return relationship && relationship.following
}, },
blocking: (relationship) => relationship.blocking,
followRequested: (relationship, account) => { followRequested: (relationship, account) => {
return relationship && relationship.requested && account && account.locked return relationship && relationship.requested && account && account.locked
}, },
followLabel: (following, followRequested) => { followLabel: (blocking, following, followRequested) => {
if (following) { if (blocking) {
return 'Unblock'
} else if (following) {
return 'Unfollow' return 'Unfollow'
} else if (followRequested) { } else if (followRequested) {
return 'Unfollow (follow requested)' return 'Unfollow (follow requested)'
@ -67,8 +77,10 @@
return 'Follow' return 'Follow'
} }
}, },
followIcon: (following, followRequested) => { followIcon: (blocking, following, followRequested) => {
if (following) { if (blocking) {
return '#fa-unlock'
} else if (following) {
return '#fa-user-times' return '#fa-user-times'
} else if (followRequested) { } else if (followRequested) {
return '#fa-hourglass' return '#fa-hourglass'

View file

@ -10,7 +10,9 @@
{{'@' + account.acct}} {{'@' + account.acct}}
</div> </div>
<div class="account-profile-followed-by"> <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> <span class="account-profile-followed-by-span">Follows you</span>
{{/if}} {{/if}}
</div> </div>

View 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')
})

View file

@ -13,6 +13,7 @@ export const notificationsNavButton = $('nav a[href="/notifications"]')
export const homeNavButton = $('nav a[href="/"]') export const homeNavButton = $('nav a[href="/"]')
export const localTimelineNavButton = $('nav a[href="/local"]') export const localTimelineNavButton = $('nav a[href="/local"]')
export const searchNavButton = $('nav a[href="/search"]') export const searchNavButton = $('nav a[href="/search"]')
export const communityNavButton = $('nav a[href="/community"]')
export const formError = $('.form-error-user-error') export const formError = $('.form-error-user-error')
export const composeInput = $('.compose-box-input') export const composeInput = $('.compose-box-input')
export const composeContentWarning = $('.content-warning-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 accountProfileFollowedBy = $('.account-profile .account-profile-followed-by')
export const accountProfileFollowButton = $('.account-profile .account-profile-follow button') export const accountProfileFollowButton = $('.account-profile .account-profile-follow button')
export const goBackButton = $('.dynamic-page-go-back') 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({ export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({
innerCount: el => parseInt(el.innerText, 10) innerCount: el => parseInt(el.innerText, 10)
@ -169,6 +171,10 @@ export function getNthFavoriteButton (n) {
return getNthStatus(n).find('.status-toolbar button:nth-child(3)') 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) { export function getNthFavorited (n) {
return getNthFavoriteButton(n).getAttribute('aria-pressed') return getNthFavoriteButton(n).getAttribute('aria-pressed')
} }
@ -189,6 +195,10 @@ export function getNthReblogged (n) {
return getNthReblogButton(n).getAttribute('aria-pressed') return getNthReblogButton(n).getAttribute('aria-pressed')
} }
export function getNthDialogOptionsOption (n) {
return $(`.modal-dialog li:nth-child(${n}) button`)
}
export function getReblogsCount () { export function getReblogsCount () {
return reblogsCountElement.innerCount return reblogsCountElement.innerCount
} }