implement requesting to follow someone

This commit is contained in:
Nolan Lawson 2018-03-14 22:14:06 -07:00
parent c11f2454ad
commit 56f7efb78f
19 changed files with 148 additions and 33 deletions

View File

@ -326,5 +326,12 @@ export const actions = times(30, i => ({
inReplyTo: 'bazthread-thread 2b2', inReplyTo: 'bazthread-thread 2b2',
privacy: 'unlisted' privacy: 'unlisted'
} }
},
{
user: 'LockedAccount',
post: {
text: 'This account is locked',
privacy: 'private'
}
} }
])) ]))

View File

@ -30,5 +30,6 @@ module.exports = [
{id: 'fa-smile', src: 'node_modules/font-awesome-svg-png/white/svg/smile-o.svg', title: 'Custom emoji'}, {id: 'fa-smile', src: 'node_modules/font-awesome-svg-png/white/svg/smile-o.svg', title: 'Custom emoji'},
{id: 'fa-exclamation-triangle', src: 'node_modules/font-awesome-svg-png/white/svg/exclamation-triangle.svg', title: 'Content warning'}, {id: 'fa-exclamation-triangle', src: 'node_modules/font-awesome-svg-png/white/svg/exclamation-triangle.svg', title: 'Content warning'},
{id: 'fa-check', src: 'node_modules/font-awesome-svg-png/white/svg/check.svg', title: 'Check'}, {id: 'fa-check', src: 'node_modules/font-awesome-svg-png/white/svg/check.svg', title: 'Check'},
{id: 'fa-trash', src: 'node_modules/font-awesome-svg-png/white/svg/trash-o.svg', title: 'Delete'} {id: 'fa-trash', src: 'node_modules/font-awesome-svg-png/white/svg/trash-o.svg', title: 'Delete'},
{id: 'fa-hourglass', src: 'node_modules/font-awesome-svg-png/white/svg/hourglass.svg', title: 'Follow requested'}
] ]

Binary file not shown.

Binary file not shown.

View File

@ -2,6 +2,7 @@ import { store } from '../_store/store'
import { followAccount, unfollowAccount } from '../_api/follow' import { followAccount, unfollowAccount } from '../_api/follow'
import { database } from '../_database/database' import { database } from '../_database/database'
import { toast } from '../_utils/toast' import { toast } from '../_utils/toast'
import { updateProfileAndRelationship } from './accounts'
export async function setAccountFollowed (accountId, follow, toastOnSuccess) { export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
let instanceName = store.get('currentInstance') let instanceName = store.get('currentInstance')
@ -12,11 +13,18 @@ export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
} else { } else {
await unfollowAccount(instanceName, accessToken, accountId) await unfollowAccount(instanceName, accessToken, accountId)
} }
await updateProfileAndRelationship(accountId)
let relationship = await database.getRelationship(instanceName, accountId) let relationship = await database.getRelationship(instanceName, accountId)
relationship.following = follow
await database.setRelationship(instanceName, relationship)
if (toastOnSuccess) { if (toastOnSuccess) {
toast.say(`${follow ? 'Followed' : 'Unfollowed'}`) if (follow) {
if (relationship.requested) {
toast.say('Requested to follow account')
} else {
toast.say('Followed account')
}
} else {
toast.say('Unfollowed account')
}
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View File

@ -0,0 +1,17 @@
import { getWithTimeout, postWithTimeout } from '../_utils/ajax'
import { auth, basename } from '../_api/utils'
export async function getFollowRequests (instanceName, accessToken) {
let url = `${basename(instanceName)}/api/v1/follow_requests`
return getWithTimeout(url, auth(accessToken))
}
export async function authorizeFollowRequest (instanceName, accessToken, id) {
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
return postWithTimeout(url, null, auth(accessToken))
}
export async function rejectFollowRequest (instanceName, accessToken, id) {
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
return postWithTimeout(url, null, auth(accessToken))
}

View File

@ -40,10 +40,10 @@ async function addTimelineItems (instanceName, timelineName, items, stale) {
} }
export async function addTimelineItemIds (instanceName, timelineName, newIds, newStale) { export async function addTimelineItemIds (instanceName, timelineName, newIds, newStale) {
let oldIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || [] let oldIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds')
let oldStale = store.getForTimeline(instanceName, timelineName, 'timelineItemIdsAreStale') let oldStale = store.getForTimeline(instanceName, timelineName, 'timelineItemIdsAreStale')
let mergedIds = mergeArrays(oldIds, newIds) let mergedIds = mergeArrays(oldIds || [], newIds)
if (!isEqual(oldIds, mergedIds)) { if (!isEqual(oldIds, mergedIds)) {
store.setForTimeline(instanceName, timelineName, {timelineItemIds: mergedIds}) store.setForTimeline(instanceName, timelineName, {timelineItemIds: mergedIds})

View File

@ -24,8 +24,8 @@
<div class="account-profile-follow"> <div class="account-profile-follow">
{{#if verifyCredentials && relationship && verifyCredentials.id !== relationship.id}} {{#if verifyCredentials && relationship && verifyCredentials.id !== relationship.id}}
<IconButton <IconButton
label="{{following ? 'Unfollow' : 'Follow'}}" label="{{followLabel}}"
href="{{following ? '#fa-user-times' : '#fa-user-plus'}}" href="{{followIcon}}"
pressable="true" pressable="true"
pressed="{{following}}" pressed="{{following}}"
big="true" big="true"
@ -201,14 +201,34 @@
} }
return note return note
}, },
following: (relationship) => relationship && relationship.following following: (relationship) => relationship && relationship.following,
followRequested: (relationship) => relationship && relationship.requested,
followLabel: (following, followRequested) => {
if (following) {
return 'Unfollow'
} else if (followRequested) {
return 'Unfollow (follow requested)'
} else {
return 'Follow'
}
},
followIcon: (following, followRequested) => {
if (following) {
return '#fa-user-times'
} else if (followRequested) {
return '#fa-hourglass'
} else {
return '#fa-user-plus'
}
}
}, },
methods: { methods: {
async onFollowButtonClick() { async onFollowButtonClick() {
let accountId = this.get('profile').id let accountId = this.get('profile').id
let instanceName = this.store.get('currentInstance') let instanceName = this.store.get('currentInstance')
let following = this.get('following') let following = this.get('following')
await setAccountFollowed(accountId, !following) let followRequested = this.get('followRequested')
await setAccountFollowed(accountId, !(following || followRequested))
this.set({relationship: await database.getRelationship(instanceName, accountId)}) this.set({relationship: await database.getRelationship(instanceName, accountId)})
} }
}, },

View File

@ -14,22 +14,25 @@ export default {
account: ($currentAccountProfile) => $currentAccountProfile, account: ($currentAccountProfile) => $currentAccountProfile,
verifyCredentials: ($currentVerifyCredentials) => $currentVerifyCredentials, verifyCredentials: ($currentVerifyCredentials) => $currentVerifyCredentials,
verifyCredentialsId: (verifyCredentials) => verifyCredentials.id, verifyCredentialsId: (verifyCredentials) => verifyCredentials.id,
following: (relationship) => relationship && !!relationship.following, following: (relationship) => relationship && relationship.following,
followRequested: (relationship) => relationship && relationship.requested,
accountName: (account) => account && (account.display_name || account.acct), accountName: (account) => account && (account.display_name || account.acct),
accountId: (account) => account && account.id, accountId: (account) => account && account.id,
followLabel: (following, accountName) => { followLabel: (following, followRequested, accountName) => {
if (typeof following === 'undefined' || !accountName) { if (typeof following === 'undefined' || !accountName) {
return '' return ''
} }
return following ? `Unfollow ${accountName}` : `Follow ${accountName}` return (following || followRequested)
? `Unfollow ${accountName}`
: `Follow ${accountName}`
}, },
items: (followLabel, following, accountId, verifyCredentialsId) => ( items: (followLabel, following, followRequested, accountId, verifyCredentialsId) => (
[ [
accountId !== verifyCredentialsId && accountId !== verifyCredentialsId &&
{ {
key: 'follow', key: 'follow',
label: followLabel, label: followLabel,
icon: following ? '#fa-user-times' : '#fa-user-plus' icon: following ? '#fa-user-times' : followRequested ? '#fa-hourglass' : '#fa-user-plus'
}, },
accountId === verifyCredentialsId && accountId === verifyCredentialsId &&
{ {

View File

@ -1,5 +1,5 @@
<:Head> <:Head>
<title>Pinafore {{profileName}}</title> <title>Pinafore Profile</title>
</:Head> </:Head>
<Layout page='tags'> <Layout page='tags'>
<LazyPage :pageComponent :params /> <LazyPage :pageComponent :params />

View File

@ -1,5 +1,5 @@
<:Head> <:Head>
<title>Pinafore {{listTitle}}</title> <title>Pinafore List</title>
</:Head> </:Head>
<Layout page='lists'> <Layout page='lists'>
<LazyPage :pageComponent :params /> <LazyPage :pageComponent :params />

View File

@ -1,5 +1,5 @@
<:Head> <:Head>
<title>Pinafore {{params.instanceName}}</title> <title>Pinafore Instance</title>
</:Head> </:Head>
<Layout page='settings'> <Layout page='settings'>
<LazyPage :pageComponent :params /> <LazyPage :pageComponent :params />

View File

@ -1,5 +1,5 @@
<:Head> <:Head>
<title>Pinafore #{{params.tagName}}</title> <title>Pinafore Hashtag</title>
</:Head> </:Head>
<Layout page='tags'> <Layout page='tags'>
<LazyPage :pageComponent :params /> <LazyPage :pageComponent :params />

View File

@ -96,6 +96,7 @@ body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-o
<symbol id="fa-exclamation-triangle" viewBox="0 0 1792 1792"><title>Content warning</title><path d="M1024 1375v-190q0-14-9.5-23.5T992 1152H800q-13 0-22.5 9.5T768 1185v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11H786q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17H128q-34 0-63.5-17T18 1601q-37-63-2-126L784 67q17-31 47-49t65-18 65 18 47 49z"></path></symbol> <symbol id="fa-exclamation-triangle" viewBox="0 0 1792 1792"><title>Content warning</title><path d="M1024 1375v-190q0-14-9.5-23.5T992 1152H800q-13 0-22.5 9.5T768 1185v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11H786q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17H128q-34 0-63.5-17T18 1601q-37-63-2-126L784 67q17-31 47-49t65-18 65 18 47 49z"></path></symbol>
<symbol id="fa-check" viewBox="0 0 1792 1792"><title>Check</title><path d="M1671 566q0 40-28 68l-724 724-136 136q-28 28-68 28t-68-28l-136-136-362-362q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 295 656-657q28-28 68-28t68 28l136 136q28 28 28 68z"></path></symbol> <symbol id="fa-check" viewBox="0 0 1792 1792"><title>Check</title><path d="M1671 566q0 40-28 68l-724 724-136 136q-28 28-68 28t-68-28l-136-136-362-362q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 295 656-657q28-28 68-28t68 28l136 136q28 28 28 68z"></path></symbol>
<symbol id="fa-trash" viewBox="0 0 1792 1792"><title>Delete</title><path d="M704 736v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23V736q0-14 9-23t23-9h64q14 0 23 9t9 23zm256 0v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23V736q0-14 9-23t23-9h64q14 0 23 9t9 23zm256 0v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23V736q0-14 9-23t23-9h64q14 0 23 9t9 23zm128 724V512H448v948q0 22 7 40.5t14.5 27 10.5 8.5h832q3 0 10.5-8.5t14.5-27 7-40.5zM672 384h448l-48-117q-7-9-17-11H738q-10 2-17 11zm928 32v64q0 14-9 23t-23 9h-96v948q0 83-47 143.5t-113 60.5H480q-66 0-113-58.5T320 1464V512h-96q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h309l70-167q15-37 54-63t79-26h320q40 0 79 26t54 63l70 167h309q14 0 23 9t9 23z"></path></symbol> <symbol id="fa-trash" viewBox="0 0 1792 1792"><title>Delete</title><path d="M704 736v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23V736q0-14 9-23t23-9h64q14 0 23 9t9 23zm256 0v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23V736q0-14 9-23t23-9h64q14 0 23 9t9 23zm256 0v576q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23V736q0-14 9-23t23-9h64q14 0 23 9t9 23zm128 724V512H448v948q0 22 7 40.5t14.5 27 10.5 8.5h832q3 0 10.5-8.5t14.5-27 7-40.5zM672 384h448l-48-117q-7-9-17-11H738q-10 2-17 11zm928 32v64q0 14-9 23t-23 9h-96v948q0 83-47 143.5t-113 60.5H480q-66 0-113-58.5T320 1464V512h-96q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h309l70-167q15-37 54-63t79-26h320q40 0 79 26t54 63l70 167h309q14 0 23 9t9 23z"></path></symbol>
<symbol id="fa-hourglass" viewBox="0 0 1792 1792"><title>Follow requested</title><path d="M1632 1600q14 0 23 9t9 23v128q0 14-9 23t-23 9H160q-14 0-23-9t-9-23v-128q0-14 9-23t23-9h1472zm-1374-64q3-55 16-107t30-95 46-87 53.5-76 64.5-69.5 66-60 70.5-55T671 939t65-43q-43-28-65-43t-66.5-47.5-70.5-55-66-60-64.5-69.5-53.5-76-46-87-30-95-16-107h1276q-3 55-16 107t-30 95-46 87-53.5 76-64.5 69.5-66 60-70.5 55T1121 853t-65 43q43 28 65 43t66.5 47.5 70.5 55 66 60 64.5 69.5 53.5 76 46 87 30 95 16 107H258zM1632 0q14 0 23 9t9 23v128q0 14-9 23t-23 9H160q-14 0-23-9t-9-23V32q0-14 9-23t23-9h1472z"></path></symbol>
</svg><!-- end insert svg here --> </svg><!-- end insert svg here -->
</svg> </svg>
<!-- The application will be rendered inside this element, <!-- The application will be rendered inside this element,

View File

@ -4,6 +4,7 @@ import FileApi from 'file-api'
import { users } from './users' import { users } from './users'
import { postStatus } from '../routes/_api/statuses' import { postStatus } from '../routes/_api/statuses'
import { deleteStatus } from '../routes/_api/delete' import { deleteStatus } from '../routes/_api/delete'
import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/followRequests'
global.fetch = fetch global.fetch = fetch
global.File = FileApi.File global.File = FileApi.File
@ -28,3 +29,11 @@ export async function postReplyAsAdmin (text, inReplyTo) {
export async function deleteAsAdmin (statusId) { export async function deleteAsAdmin (statusId) {
return deleteStatus(instanceName, users.admin.accessToken, statusId) return deleteStatus(instanceName, users.admin.accessToken, statusId)
} }
export async function getFollowRequestsAsLockedAccount () {
return getFollowRequests(instanceName, users.LockedAccount.accessToken)
}
export async function authorizeFollowRequestAsLockedAccount (id) {
return authorizeFollowRequest(instanceName, users.LockedAccount.accessToken, id)
}

View File

@ -1,5 +1,9 @@
import { Selector as $ } from 'testcafe' import { Selector as $ } from 'testcafe'
import { getUrl, validateTimeline } from '../utils' import {
accountProfileFollowButton,
accountProfileFollowedBy, accountProfileName, accountProfileUsername, getUrl,
validateTimeline
} from '../utils'
import { foobarRole } from '../roles' import { foobarRole } from '../roles'
import { quuxStatuses } from '../fixtures' import { quuxStatuses } from '../fixtures'
@ -10,32 +14,32 @@ test('shows account profile', async t => {
await t.useRole(foobarRole) await t.useRole(foobarRole)
.click($('.status-author-name').withText(('quux'))) .click($('.status-author-name').withText(('quux')))
.expect(getUrl()).contains('/accounts/3') .expect(getUrl()).contains('/accounts/3')
.expect($('.account-profile .account-profile-name').innerText).contains('quux') .expect(accountProfileName.innerText).contains('quux')
.expect($('.account-profile .account-profile-username').innerText).contains('@quux') .expect(accountProfileUsername.innerText).contains('@quux')
.expect($('.account-profile .account-profile-followed-by').innerText).match(/follows you/i) .expect(accountProfileFollowedBy.innerText).match(/follows you/i)
.expect($('.account-profile .account-profile-follow button').getAttribute('aria-label')).eql('Follow') .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
.expect($('.account-profile .account-profile-follow button').getAttribute('aria-pressed')).eql('false') .expect(accountProfileFollowButton.getAttribute('aria-pressed')).eql('false')
}) })
test('shows account profile 2', async t => { test('shows account profile 2', async t => {
await t.useRole(foobarRole) await t.useRole(foobarRole)
.click($('.status-author-name').withText(('admin'))) .click($('.status-author-name').withText(('admin')))
.expect(getUrl()).contains('/accounts/1') .expect(getUrl()).contains('/accounts/1')
.expect($('.account-profile .account-profile-name').innerText).contains('admin') .expect(accountProfileName.innerText).contains('admin')
.expect($('.account-profile .account-profile-username').innerText).contains('@admin') .expect(accountProfileUsername.innerText).contains('@admin')
.expect($('.account-profile .account-profile-followed-by').innerText).match(/follows you/i) .expect(accountProfileFollowedBy.innerText).match(/follows you/i)
.expect($('.account-profile .account-profile-follow button').getAttribute('aria-label')).eql('Unfollow') .expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
.expect($('.account-profile .account-profile-follow button').getAttribute('aria-pressed')).eql('true') .expect(accountProfileFollowButton.getAttribute('aria-pressed')).eql('true')
}) })
test('shows account profile 3', async t => { test('shows account profile 3', async t => {
await t.useRole(foobarRole) await t.useRole(foobarRole)
.click($('.mention').withText(('foobar'))) .click($('.mention').withText(('foobar')))
.expect(getUrl()).contains('/accounts/2') .expect(getUrl()).contains('/accounts/2')
.expect($('.account-profile .account-profile-name').innerText).contains('foobar') .expect(accountProfileName.innerText).contains('foobar')
.expect($('.account-profile .account-profile-username').innerText).contains('@foobar') .expect(accountProfileUsername.innerText).contains('@foobar')
// can't follow or be followed by your own account // can't follow or be followed by your own account
.expect($('.account-profile .account-profile-followed-by').innerText).eql('') .expect(accountProfileFollowedBy.innerText).eql('')
.expect($('.account-profile .account-profile-follow').innerText).eql('') .expect($('.account-profile .account-profile-follow').innerText).eql('')
}) })

View File

@ -0,0 +1,35 @@
import { foobarRole } from '../roles'
import {
accountProfileFollowButton,
getNthStatus,
sleep
} from '../utils'
import {
authorizeFollowRequestAsLockedAccount, getFollowRequestsAsLockedAccount
} from '../serverActions'
fixture`106-follow-requests.js`
.page`http://localhost:4002`
test('can request to follow an account', async t => {
await t.useRole(foobarRole)
.navigateTo('/accounts/6')
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
.click(accountProfileFollowButton)
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow (follow requested)')
.click(accountProfileFollowButton)
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
.click(accountProfileFollowButton)
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow (follow requested)')
let requests = await getFollowRequestsAsLockedAccount()
await authorizeFollowRequestAsLockedAccount(requests.slice(-1)[0].id)
await sleep(2000)
await t.navigateTo('/accounts/6')
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
.expect(getNthStatus(0).innerText).contains('This account is locked')
.click(accountProfileFollowButton)
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
})

View File

@ -28,5 +28,11 @@ export const users = {
password: 'bazbazbaz', password: 'bazbazbaz',
accessToken: '0639238783efdfde849304bc89ec0c4b60b5ef5f261f60859fcd597de081cfdc', accessToken: '0639238783efdfde849304bc89ec0c4b60b5ef5f261f60859fcd597de081cfdc',
id: 5 id: 5
},
LockedAccount: {
username: 'LockedAccount',
password: 'LockedAccountLockedAccount',
accessToken: '39ed9aeffa4b25eda4940f22f29fea66e625c6282c2a8bf0430203c9779e9e98',
id: 6
} }
} }

View File

@ -27,6 +27,10 @@ export const logInToInstanceLink = $('a[href="/settings/instances/add"]')
export const searchInput = $('.search-input') export const searchInput = $('.search-input')
export const postStatusButton = $('.compose-box-button') export const postStatusButton = $('.compose-box-button')
export const showMoreButton = $('.more-items-header button') export const showMoreButton = $('.more-items-header button')
export const accountProfileName = $('.account-profile .account-profile-name')
export const accountProfileUsername = $('.account-profile .account-profile-username')
export const accountProfileFollowedBy = $('.account-profile .account-profile-followed-by')
export const accountProfileFollowButton = $('.account-profile .account-profile-follow 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)