approve/reject follow requests, unblock, unmute (#230)
* approve/reject follow requests, unblock, unmute * make tests less flaky
This commit is contained in:
parent
e342eadbd0
commit
ffb00fcc5c
|
@ -31,8 +31,7 @@ Lint:
|
|||
|
||||
Automatically fix most linting issues:
|
||||
|
||||
npx standard --fix
|
||||
npx standard --fix --plugin html 'routes/**/*.html'
|
||||
npm run lint-fix
|
||||
|
||||
## Testing
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"version": "0.2.3",
|
||||
"scripts": {
|
||||
"lint": "standard && standard --plugin html 'routes/**/*.html'",
|
||||
"lint-fix": "standard --fix && standard --fix --plugin html 'routes/**/*.html'",
|
||||
"dev": "run-s build-svg build-inline-script serve-dev",
|
||||
"serve-dev": "run-p --race build-sass-watch serve",
|
||||
"serve": "node server.js",
|
||||
|
|
|
@ -2,6 +2,7 @@ import { store } from '../_store/store'
|
|||
import { blockAccount, unblockAccount } from '../_api/block'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
|
||||
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
|
@ -19,6 +20,7 @@ export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
|||
toast.say('Unblocked account')
|
||||
}
|
||||
}
|
||||
emit('refreshAccountsList')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${block ? 'block' : 'unblock'} account: ` + (e.message || ''))
|
||||
|
|
|
@ -2,6 +2,7 @@ import { store } from '../_store/store'
|
|||
import { muteAccount, unmuteAccount } from '../_api/mute'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
|
||||
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
|
@ -19,6 +20,7 @@ export async function setAccountMuted (accountId, mute, toastOnSuccess) {
|
|||
toast.say('Unmuted account')
|
||||
}
|
||||
}
|
||||
emit('refreshAccountsList')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${mute ? 'mute' : 'unmute'} account: ` + (e.message || ''))
|
||||
|
|
29
routes/_actions/requests.js
Normal file
29
routes/_actions/requests.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { store } from '../_store/store'
|
||||
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { toast } from '../_utils/toast'
|
||||
|
||||
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
|
||||
let {
|
||||
currentInstance,
|
||||
accessToken
|
||||
} = store.get()
|
||||
try {
|
||||
if (approved) {
|
||||
await approveFollowRequest(currentInstance, accessToken, accountId)
|
||||
} else {
|
||||
await rejectFollowRequest(currentInstance, accessToken, accountId)
|
||||
}
|
||||
if (toastOnSuccess) {
|
||||
if (approved) {
|
||||
toast.say('Approved follow request')
|
||||
} else {
|
||||
toast.say('Rejected follow request')
|
||||
}
|
||||
}
|
||||
emit('refreshAccountsList')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Unable to ${approved ? 'approve' : 'reject'} account: ` + (e.message || ''))
|
||||
}
|
||||
}
|
12
routes/_api/requests.js
Normal file
12
routes/_api/requests.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { postWithTimeout } from '../_utils/ajax'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export async function approveFollowRequest (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${accountId}/authorize`
|
||||
return postWithTimeout(url, null, auth(accessToken))
|
||||
}
|
||||
|
||||
export async function rejectFollowRequest (instanceName, accessToken, accountId) {
|
||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${accountId}/reject`
|
||||
return postWithTimeout(url, null, auth(accessToken))
|
||||
}
|
|
@ -4,7 +4,11 @@
|
|||
{{elseif accounts && accounts.length}}
|
||||
<ul class="accounts-results">
|
||||
{{#each accounts as account}}
|
||||
<AccountSearchResult :account />
|
||||
<AccountSearchResult
|
||||
:account
|
||||
actions={{accountActions}}
|
||||
on:click="onClickAction(event)"
|
||||
/>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
@ -31,19 +35,19 @@
|
|||
import LoadingPage from '../_components/LoadingPage.html'
|
||||
import AccountSearchResult from '../_components/search/AccountSearchResult.html'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { on } from '../_utils/eventBus'
|
||||
|
||||
// TODO: paginate
|
||||
export default {
|
||||
async oncreate () {
|
||||
let { accountsFetcher } = this.get()
|
||||
try {
|
||||
// TODO: paginate
|
||||
let accounts = await accountsFetcher()
|
||||
this.set({ accounts: accounts })
|
||||
await this.refreshAccounts()
|
||||
} catch (e) {
|
||||
toast.say('Error: ' + (e.name || '') + ' ' + (e.message || ''))
|
||||
} finally {
|
||||
this.set({loading: false})
|
||||
}
|
||||
on('refreshAccountsList', this, () => this.refreshAccounts())
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
|
@ -53,6 +57,17 @@
|
|||
components: {
|
||||
LoadingPage,
|
||||
AccountSearchResult
|
||||
},
|
||||
methods: {
|
||||
onClickAction (event) {
|
||||
let { action, accountId } = event
|
||||
action.onclick(accountId)
|
||||
},
|
||||
async refreshAccounts () {
|
||||
let { accountsFetcher } = this.get()
|
||||
let accounts = await accountsFetcher()
|
||||
this.set({ accounts: accounts })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -9,7 +9,7 @@
|
|||
{{#if pinnable}}
|
||||
<IconButton pressable="true"
|
||||
pressed="{{$pinnedPage === href}}"
|
||||
label="Pin page"
|
||||
label="{{$pinnedPage === href ? 'Unpin timeline' : 'Pin timeline'}}"
|
||||
href="#fa-thumb-tack"
|
||||
on:click="onPinClick(event)" />
|
||||
{{/if}}
|
||||
|
|
|
@ -7,16 +7,28 @@
|
|||
<div class="search-result-account-username">
|
||||
{{'@' + account.acct}}
|
||||
</div>
|
||||
{{#if actions && actions.length}}
|
||||
<div class="search-result-account-buttons">
|
||||
{{#each actions as action}}
|
||||
<IconButton
|
||||
label="{{action.label}}"
|
||||
on:click="onButtonClick(event, action, account.id)"
|
||||
href="{{action.icon}}"
|
||||
big="true"
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</SearchResult>
|
||||
<style>
|
||||
.search-result-account {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"avatar name"
|
||||
"avatar username";
|
||||
"avatar name buttons"
|
||||
"avatar username buttons";
|
||||
grid-column-gap: 20px;
|
||||
grid-template-columns: max-content 1fr;
|
||||
grid-template-columns: max-content 1fr max-content;
|
||||
align-items: center;
|
||||
}
|
||||
:global(.search-result-account-avatar) {
|
||||
|
@ -36,19 +48,45 @@
|
|||
text-overflow: ellipsis;
|
||||
color: var(--deemphasized-text-color);
|
||||
}
|
||||
.search-result-account-buttons {
|
||||
grid-area: buttons;
|
||||
display: flex;
|
||||
}
|
||||
:global(.search-result-account-buttons .icon-button) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
:global(.search-result-account-buttons .icon-button:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.search-result-account {
|
||||
grid-column-gap: 10px;
|
||||
}
|
||||
:global(.search-result-account-buttons .icon-button) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import Avatar from '../Avatar.html'
|
||||
import SearchResult from './SearchResult.html'
|
||||
import IconButton from '../IconButton.html'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
SearchResult
|
||||
SearchResult,
|
||||
IconButton
|
||||
},
|
||||
methods: {
|
||||
onButtonClick (event, action, accountId) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.fire('click', {
|
||||
action,
|
||||
accountId
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,12 +1,22 @@
|
|||
<DynamicPageBanner title="Blocked users" icon="#fa-ban" />
|
||||
<AccountsListPage :accountsFetcher />
|
||||
<AccountsListPage :accountsFetcher :accountActions />
|
||||
<script>
|
||||
import AccountsListPage from '.././_components/AccountsListPage.html'
|
||||
import { store } from '.././_store/store'
|
||||
import { getBlockedAccounts } from '.././_api/blockedAndMuted'
|
||||
import DynamicPageBanner from '.././_components/DynamicPageBanner.html'
|
||||
import { setAccountBlocked } from '../_actions/block'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
accountActions: [
|
||||
{
|
||||
icon: '#fa-unlock',
|
||||
label: 'Unblock',
|
||||
onclick: (accountId) => setAccountBlocked(accountId, false, true)
|
||||
}
|
||||
]
|
||||
}),
|
||||
computed: {
|
||||
accountsFetcher: ($currentInstance, $accessToken) => () => getBlockedAccounts($currentInstance, $accessToken)
|
||||
},
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
<DynamicPageBanner title="Muted users" icon="#fa-volume-off" />
|
||||
<AccountsListPage :accountsFetcher />
|
||||
<AccountsListPage :accountsFetcher :accountActions />
|
||||
<script>
|
||||
import AccountsListPage from '.././_components/AccountsListPage.html'
|
||||
import { store } from '.././_store/store'
|
||||
import { getMutedAccounts } from '.././_api/blockedAndMuted'
|
||||
import DynamicPageBanner from '.././_components/DynamicPageBanner.html'
|
||||
import { setAccountMuted } from '../_actions/mute'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
accountActions: [
|
||||
{
|
||||
icon: '#fa-volume-up',
|
||||
label: 'Unmute',
|
||||
onclick: (accountId) => setAccountMuted(accountId, false, true)
|
||||
}
|
||||
]
|
||||
}),
|
||||
computed: {
|
||||
accountsFetcher: ($currentInstance, $accessToken) => () => getMutedAccounts($currentInstance, $accessToken)
|
||||
},
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
<DynamicPageBanner title="Follow requests" icon="#fa-user-plus" />
|
||||
<AccountsListPage :accountsFetcher />
|
||||
<AccountsListPage :accountsFetcher :accountActions />
|
||||
<script>
|
||||
import AccountsListPage from '.././_components/AccountsListPage.html'
|
||||
import { store } from '.././_store/store'
|
||||
import { getFollowRequests } from '../_actions/followRequests'
|
||||
import DynamicPageBanner from '.././_components/DynamicPageBanner.html'
|
||||
import { setFollowRequestApprovedOrRejected } from '../_actions/requests'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
accountActions: [
|
||||
{
|
||||
icon: '#fa-check',
|
||||
label: 'Approve',
|
||||
onclick: (accountId) => setFollowRequestApprovedOrRejected(accountId, true, true)
|
||||
},
|
||||
{
|
||||
icon: '#fa-times',
|
||||
label: 'Reject',
|
||||
onclick: (accountId) => setFollowRequestApprovedOrRejected(accountId, false, true)
|
||||
}
|
||||
]
|
||||
}),
|
||||
computed: {
|
||||
statusId: params => params.statusId,
|
||||
accountsFetcher: ($currentInstance, $accessToken, statusId) => () => getFollowRequests($currentInstance, $accessToken, statusId)
|
||||
accountsFetcher: ($currentInstance, $accessToken) => () => getFollowRequests($currentInstance, $accessToken)
|
||||
},
|
||||
store: () => store,
|
||||
components: {
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
authorizeInput, emailInput, getUrl, instanceInput, mastodonLogInButton,
|
||||
passwordInput
|
||||
} from './utils'
|
||||
import { users } from './users'
|
||||
|
||||
function login (t, username, password) {
|
||||
return t.typeText(instanceInput, 'localhost:3000', {paste: true})
|
||||
|
@ -18,5 +19,9 @@ function login (t, username, password) {
|
|||
}
|
||||
|
||||
export const foobarRole = Role('http://localhost:4002/settings/instances/add', async t => {
|
||||
await login(t, 'foobar@localhost:3000', 'foobarfoobar')
|
||||
await login(t, users.foobar.email, users.foobar.password)
|
||||
})
|
||||
|
||||
export const lockedAccountRole = Role('http://localhost:4002/settings/instances/add', async t => {
|
||||
await login(t, users.LockedAccount.email, users.LockedAccount.password)
|
||||
})
|
||||
|
|
|
@ -5,6 +5,7 @@ import { users } from './users'
|
|||
import { postStatus } from '../routes/_api/statuses'
|
||||
import { deleteStatus } from '../routes/_api/delete'
|
||||
import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/followRequests'
|
||||
import { followAccount, unfollowAccount } from '../routes/_api/follow'
|
||||
|
||||
global.fetch = fetch
|
||||
global.File = FileApi.File
|
||||
|
@ -37,3 +38,11 @@ export async function getFollowRequestsAs (username) {
|
|||
export async function authorizeFollowRequestAs (username, id) {
|
||||
return authorizeFollowRequest(instanceName, users[username].accessToken, id)
|
||||
}
|
||||
|
||||
export async function followAs (username, userToFollow) {
|
||||
return followAccount(instanceName, users[username].accessToken, users[userToFollow].id)
|
||||
}
|
||||
|
||||
export async function unfollowAs (username, userToFollow) {
|
||||
return unfollowAccount(instanceName, users[username].accessToken, users[userToFollow].id)
|
||||
}
|
||||
|
|
76
tests/spec/116-follow-requests.js
Normal file
76
tests/spec/116-follow-requests.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { lockedAccountRole } from '../roles'
|
||||
import { followAs, unfollowAs } from '../serverActions'
|
||||
import {
|
||||
communityNavButton, followersButton, getNthSearchResult, getSearchResultByHref, getUrl, goBack,
|
||||
homeNavButton, sleep
|
||||
} from '../utils'
|
||||
import { users } from '../users'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
|
||||
fixture`116-follow-requests.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
const timeout = 30000
|
||||
|
||||
test('Can approve and reject follow requests', async t => {
|
||||
await t.useRole(lockedAccountRole)
|
||||
|
||||
// necessary for re-running this test in local testing
|
||||
await Promise.all([
|
||||
unfollowAs('admin', 'LockedAccount'),
|
||||
unfollowAs('baz', 'LockedAccount'),
|
||||
unfollowAs('quux', 'LockedAccount')
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
followAs('admin', 'LockedAccount'),
|
||||
followAs('baz', 'LockedAccount'),
|
||||
followAs('quux', 'LockedAccount')
|
||||
])
|
||||
|
||||
await sleep(2000)
|
||||
|
||||
const approveAdminButton = () => getSearchResultByHref(`/accounts/${users.admin.id}`).find('button:nth-child(1)')
|
||||
const rejectBazButton = () => getSearchResultByHref(`/accounts/${users.baz.id}`).find('button:nth-child(2)')
|
||||
const approveQuuxButton = () => getSearchResultByHref(`/accounts/${users.quux.id}`).find('button:nth-child(1)')
|
||||
|
||||
await t.click(communityNavButton)
|
||||
.click($('a[href="/requests"]'))
|
||||
// no guaranteed order on these
|
||||
.expect(getNthSearchResult(1).innerText).match(/(@admin|@baz|@quux)/)
|
||||
.expect(getNthSearchResult(2).innerText).match(/(@admin|@baz|@quux)/)
|
||||
.expect(getNthSearchResult(3).innerText).match(/(@admin|@baz|@quux)/)
|
||||
.expect(getNthSearchResult(4).exists).notOk()
|
||||
// approve admin
|
||||
.expect(approveAdminButton().getAttribute('aria-label')).eql('Approve')
|
||||
.hover(approveAdminButton())
|
||||
.click(approveAdminButton())
|
||||
.expect(getNthSearchResult(1).innerText).match(/(@baz|@quux)/, {timeout})
|
||||
.expect(getNthSearchResult(2).innerText).match(/(@baz|@quux)/)
|
||||
.expect(getNthSearchResult(3).exists).notOk()
|
||||
await goBack()
|
||||
await t
|
||||
.click($('a[href="/requests"]'))
|
||||
// reject baz
|
||||
.expect(rejectBazButton().getAttribute('aria-label')).eql('Reject')
|
||||
.hover(rejectBazButton())
|
||||
.click(rejectBazButton())
|
||||
.expect(getNthSearchResult(1).innerText).contains('@quux', {timeout})
|
||||
.expect(getNthSearchResult(2).exists).notOk()
|
||||
await goBack()
|
||||
await t
|
||||
.click($('a[href="/requests"]'))
|
||||
// approve quux
|
||||
.expect(approveQuuxButton().getAttribute('aria-label')).eql('Approve')
|
||||
.hover(approveQuuxButton())
|
||||
.click(approveQuuxButton())
|
||||
.expect(getNthSearchResult(1).exists).notOk({timeout})
|
||||
// check our follow list to make sure they follow us
|
||||
.click(homeNavButton)
|
||||
.click($('.compose-box-avatar'))
|
||||
.expect(getUrl()).contains(`/accounts/${users.LockedAccount.id}`)
|
||||
.click(followersButton)
|
||||
.expect(getNthSearchResult(1).innerText).match(/(@admin|@quux)/)
|
||||
.expect(getNthSearchResult(2).innerText).match(/(@admin|@quux)/)
|
||||
.expect(getNthSearchResult(3).exists).notOk()
|
||||
})
|
|
@ -1,36 +1,42 @@
|
|||
export const users = {
|
||||
admin: {
|
||||
username: 'admin',
|
||||
email: 'admin@localhost:3000',
|
||||
password: 'mastodonadmin',
|
||||
accessToken: 'f954c8de1fcc0080ff706fa2516d05b60de0d8f5b536255a85ef85a6c32e4afb',
|
||||
id: 1
|
||||
},
|
||||
foobar: {
|
||||
username: 'foobar',
|
||||
email: 'foobar@localhost:3000',
|
||||
password: 'foobarfoobar',
|
||||
accessToken: 'b48d72074a467e77a18eafc0d52e373dcf2492bcb3fefadc302a81300ec69002',
|
||||
id: 2
|
||||
},
|
||||
quux: {
|
||||
username: 'quux',
|
||||
email: 'quux@localhost:3000',
|
||||
password: 'quuxquuxquux',
|
||||
accessToken: '894d3583dbf7d0f4f4784a06db86bdadb6ef0d99453d15afbc03e0c103bd78af',
|
||||
id: 3
|
||||
},
|
||||
ExternalLinks: {
|
||||
username: 'ExternalLinks',
|
||||
email: 'ExternalLinks@localhost:3000',
|
||||
password: 'ExternalLinksExternalLink',
|
||||
accessToken: 'e9a463ba1729ae0049a97a312af702cb3d08d84de1cc8d6da3fad90af068117b',
|
||||
id: 4
|
||||
},
|
||||
baz: {
|
||||
username: 'baz',
|
||||
email: 'baz@localhost:3000',
|
||||
password: 'bazbazbaz',
|
||||
accessToken: '0639238783efdfde849304bc89ec0c4b60b5ef5f261f60859fcd597de081cfdc',
|
||||
id: 5
|
||||
},
|
||||
LockedAccount: {
|
||||
username: 'LockedAccount',
|
||||
email: 'LockedAccount@localhost:3000',
|
||||
password: 'LockedAccountLockedAccount',
|
||||
accessToken: '39ed9aeffa4b25eda4940f22f29fea66e625c6282c2a8bf0430203c9779e9e98',
|
||||
id: 6
|
||||
|
|
|
@ -132,6 +132,10 @@ export function getNthAutosuggestionResult (n) {
|
|||
return $(`.compose-autosuggest-list-item:nth-child(${n}) button`)
|
||||
}
|
||||
|
||||
export function getSearchResultByHref (href) {
|
||||
return $(`.search-result a[href="${href}"]`)
|
||||
}
|
||||
|
||||
export function getNthSearchResult (n) {
|
||||
return $(`.search-result:nth-child(${n}) a`)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue