implement requesting to follow someone
This commit is contained in:
parent
c11f2454ad
commit
56f7efb78f
|
@ -326,5 +326,12 @@ export const actions = times(30, i => ({
|
|||
inReplyTo: 'bazthread-thread 2b2',
|
||||
privacy: 'unlisted'
|
||||
}
|
||||
},
|
||||
{
|
||||
user: 'LockedAccount',
|
||||
post: {
|
||||
text: 'This account is locked',
|
||||
privacy: 'private'
|
||||
}
|
||||
}
|
||||
]))
|
||||
|
|
|
@ -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-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-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.
|
@ -2,6 +2,7 @@ import { store } from '../_store/store'
|
|||
import { followAccount, unfollowAccount } from '../_api/follow'
|
||||
import { database } from '../_database/database'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
|
||||
export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
|
||||
let instanceName = store.get('currentInstance')
|
||||
|
@ -12,11 +13,18 @@ export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
|
|||
} else {
|
||||
await unfollowAccount(instanceName, accessToken, accountId)
|
||||
}
|
||||
await updateProfileAndRelationship(accountId)
|
||||
let relationship = await database.getRelationship(instanceName, accountId)
|
||||
relationship.following = follow
|
||||
await database.setRelationship(instanceName, relationship)
|
||||
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) {
|
||||
console.error(e)
|
||||
|
|
17
routes/_actions/followRequests.js
Normal file
17
routes/_actions/followRequests.js
Normal 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))
|
||||
}
|
|
@ -40,10 +40,10 @@ async function addTimelineItems (instanceName, timelineName, items, stale) {
|
|||
}
|
||||
|
||||
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 mergedIds = mergeArrays(oldIds, newIds)
|
||||
let mergedIds = mergeArrays(oldIds || [], newIds)
|
||||
|
||||
if (!isEqual(oldIds, mergedIds)) {
|
||||
store.setForTimeline(instanceName, timelineName, {timelineItemIds: mergedIds})
|
||||
|
|
|
@ -24,8 +24,8 @@
|
|||
<div class="account-profile-follow">
|
||||
{{#if verifyCredentials && relationship && verifyCredentials.id !== relationship.id}}
|
||||
<IconButton
|
||||
label="{{following ? 'Unfollow' : 'Follow'}}"
|
||||
href="{{following ? '#fa-user-times' : '#fa-user-plus'}}"
|
||||
label="{{followLabel}}"
|
||||
href="{{followIcon}}"
|
||||
pressable="true"
|
||||
pressed="{{following}}"
|
||||
big="true"
|
||||
|
@ -201,14 +201,34 @@
|
|||
}
|
||||
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: {
|
||||
async onFollowButtonClick() {
|
||||
let accountId = this.get('profile').id
|
||||
let instanceName = this.store.get('currentInstance')
|
||||
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)})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -14,22 +14,25 @@ export default {
|
|||
account: ($currentAccountProfile) => $currentAccountProfile,
|
||||
verifyCredentials: ($currentVerifyCredentials) => $currentVerifyCredentials,
|
||||
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),
|
||||
accountId: (account) => account && account.id,
|
||||
followLabel: (following, accountName) => {
|
||||
followLabel: (following, followRequested, accountName) => {
|
||||
if (typeof following === 'undefined' || !accountName) {
|
||||
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 &&
|
||||
{
|
||||
key: 'follow',
|
||||
label: followLabel,
|
||||
icon: following ? '#fa-user-times' : '#fa-user-plus'
|
||||
icon: following ? '#fa-user-times' : followRequested ? '#fa-hourglass' : '#fa-user-plus'
|
||||
},
|
||||
accountId === verifyCredentialsId &&
|
||||
{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<:Head>
|
||||
<title>Pinafore – {{profileName}}</title>
|
||||
<title>Pinafore – Profile</title>
|
||||
</:Head>
|
||||
<Layout page='tags'>
|
||||
<LazyPage :pageComponent :params />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<:Head>
|
||||
<title>Pinafore – {{listTitle}}</title>
|
||||
<title>Pinafore – List</title>
|
||||
</:Head>
|
||||
<Layout page='lists'>
|
||||
<LazyPage :pageComponent :params />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<:Head>
|
||||
<title>Pinafore – {{params.instanceName}}</title>
|
||||
<title>Pinafore – Instance</title>
|
||||
</:Head>
|
||||
<Layout page='settings'>
|
||||
<LazyPage :pageComponent :params />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<:Head>
|
||||
<title>Pinafore – #{{params.tagName}}</title>
|
||||
<title>Pinafore – Hashtag</title>
|
||||
</:Head>
|
||||
<Layout page='tags'>
|
||||
<LazyPage :pageComponent :params />
|
||||
|
|
|
@ -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-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-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>
|
||||
<!-- The application will be rendered inside this element,
|
||||
|
|
|
@ -4,6 +4,7 @@ import FileApi from 'file-api'
|
|||
import { users } from './users'
|
||||
import { postStatus } from '../routes/_api/statuses'
|
||||
import { deleteStatus } from '../routes/_api/delete'
|
||||
import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/followRequests'
|
||||
|
||||
global.fetch = fetch
|
||||
global.File = FileApi.File
|
||||
|
@ -28,3 +29,11 @@ export async function postReplyAsAdmin (text, inReplyTo) {
|
|||
export async function deleteAsAdmin (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)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Selector as $ } from 'testcafe'
|
||||
import { getUrl, validateTimeline } from '../utils'
|
||||
import {
|
||||
accountProfileFollowButton,
|
||||
accountProfileFollowedBy, accountProfileName, accountProfileUsername, getUrl,
|
||||
validateTimeline
|
||||
} from '../utils'
|
||||
import { foobarRole } from '../roles'
|
||||
import { quuxStatuses } from '../fixtures'
|
||||
|
||||
|
@ -10,32 +14,32 @@ test('shows account profile', async t => {
|
|||
await t.useRole(foobarRole)
|
||||
.click($('.status-author-name').withText(('quux')))
|
||||
.expect(getUrl()).contains('/accounts/3')
|
||||
.expect($('.account-profile .account-profile-name').innerText).contains('quux')
|
||||
.expect($('.account-profile .account-profile-username').innerText).contains('@quux')
|
||||
.expect($('.account-profile .account-profile-followed-by').innerText).match(/follows you/i)
|
||||
.expect($('.account-profile .account-profile-follow button').getAttribute('aria-label')).eql('Follow')
|
||||
.expect($('.account-profile .account-profile-follow button').getAttribute('aria-pressed')).eql('false')
|
||||
.expect(accountProfileName.innerText).contains('quux')
|
||||
.expect(accountProfileUsername.innerText).contains('@quux')
|
||||
.expect(accountProfileFollowedBy.innerText).match(/follows you/i)
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Follow')
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-pressed')).eql('false')
|
||||
})
|
||||
|
||||
test('shows account profile 2', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.click($('.status-author-name').withText(('admin')))
|
||||
.expect(getUrl()).contains('/accounts/1')
|
||||
.expect($('.account-profile .account-profile-name').innerText).contains('admin')
|
||||
.expect($('.account-profile .account-profile-username').innerText).contains('@admin')
|
||||
.expect($('.account-profile .account-profile-followed-by').innerText).match(/follows you/i)
|
||||
.expect($('.account-profile .account-profile-follow button').getAttribute('aria-label')).eql('Unfollow')
|
||||
.expect($('.account-profile .account-profile-follow button').getAttribute('aria-pressed')).eql('true')
|
||||
.expect(accountProfileName.innerText).contains('admin')
|
||||
.expect(accountProfileUsername.innerText).contains('@admin')
|
||||
.expect(accountProfileFollowedBy.innerText).match(/follows you/i)
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-label')).eql('Unfollow')
|
||||
.expect(accountProfileFollowButton.getAttribute('aria-pressed')).eql('true')
|
||||
})
|
||||
|
||||
test('shows account profile 3', async t => {
|
||||
await t.useRole(foobarRole)
|
||||
.click($('.mention').withText(('foobar')))
|
||||
.expect(getUrl()).contains('/accounts/2')
|
||||
.expect($('.account-profile .account-profile-name').innerText).contains('foobar')
|
||||
.expect($('.account-profile .account-profile-username').innerText).contains('@foobar')
|
||||
.expect(accountProfileName.innerText).contains('foobar')
|
||||
.expect(accountProfileUsername.innerText).contains('@foobar')
|
||||
// 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('')
|
||||
})
|
||||
|
||||
|
|
35
tests/spec/106-follow-requests.js
Normal file
35
tests/spec/106-follow-requests.js
Normal 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')
|
||||
})
|
|
@ -28,5 +28,11 @@ export const users = {
|
|||
password: 'bazbazbaz',
|
||||
accessToken: '0639238783efdfde849304bc89ec0c4b60b5ef5f261f60859fcd597de081cfdc',
|
||||
id: 5
|
||||
},
|
||||
LockedAccount: {
|
||||
username: 'LockedAccount',
|
||||
password: 'LockedAccountLockedAccount',
|
||||
accessToken: '39ed9aeffa4b25eda4940f22f29fea66e625c6282c2a8bf0430203c9779e9e98',
|
||||
id: 6
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,10 @@ export const logInToInstanceLink = $('a[href="/settings/instances/add"]')
|
|||
export const searchInput = $('.search-input')
|
||||
export const postStatusButton = $('.compose-box-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({
|
||||
innerCount: el => parseInt(el.innerText, 10)
|
||||
|
|
Loading…
Reference in a new issue