feat: show unread follow requests on community page (#1493)
* feat: show unread follow requests on community page fixes #477 * fixup * fixup
This commit is contained in:
parent
3496d7e4ea
commit
d3fb67bec3
|
@ -1,17 +1,25 @@
|
|||
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||
import { auth, basename } from '../_api/utils'
|
||||
import { store } from '../_store/store'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import { database } from '../_database/database'
|
||||
import { getFollowRequests } from '../_api/followRequests'
|
||||
|
||||
export async function getFollowRequests (instanceName, accessToken) {
|
||||
const url = `${basename(instanceName)}/api/v1/follow_requests`
|
||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
||||
}
|
||||
export async function updateFollowRequestCountIfLockedAccount (instanceName) {
|
||||
const { verifyCredentials, loggedInInstances } = store.get()
|
||||
|
||||
export async function authorizeFollowRequest (instanceName, accessToken, id) {
|
||||
const url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
|
||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
}
|
||||
if (!verifyCredentials[instanceName].locked) {
|
||||
return
|
||||
}
|
||||
|
||||
export async function rejectFollowRequest (instanceName, accessToken, id) {
|
||||
const url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
|
||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
const accessToken = loggedInInstances[instanceName].access_token
|
||||
|
||||
await cacheFirstUpdateAfter(
|
||||
async () => (await getFollowRequests(instanceName, accessToken)).length,
|
||||
() => database.getFollowRequestCount(instanceName),
|
||||
followReqsCount => database.setFollowRequestCount(instanceName, followReqsCount),
|
||||
followReqsCount => {
|
||||
const { followRequestCounts } = store.get()
|
||||
followRequestCounts[instanceName] = followReqsCount
|
||||
store.set({ followRequestCounts })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
17
src/routes/_api/followRequests.js
Normal file
17
src/routes/_api/followRequests.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||
import { auth, basename } from './utils'
|
||||
|
||||
export async function getFollowRequests (instanceName, accessToken) {
|
||||
const url = `${basename(instanceName)}/api/v1/follow_requests`
|
||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
||||
}
|
||||
|
||||
export async function authorizeFollowRequest (instanceName, accessToken, id) {
|
||||
const url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
|
||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
}
|
||||
|
||||
export async function rejectFollowRequest (instanceName, accessToken, id) {
|
||||
const url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
|
||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
}
|
|
@ -148,18 +148,24 @@
|
|||
// special case – these should both highlight the notifications tab icon
|
||||
(name === 'notifications' && page === 'notifications/mentions')
|
||||
},
|
||||
ariaLabel: ({ selected, name, label, $numberOfNotifications }) => {
|
||||
ariaLabel: ({ selected, name, label, $numberOfNotifications, $numberOfFollowRequests }) => {
|
||||
let res = label
|
||||
if (selected) {
|
||||
res += ' (current page)'
|
||||
}
|
||||
if (name === 'notifications' && $numberOfNotifications) {
|
||||
res += ` (${$numberOfNotifications} notification${$numberOfNotifications === 1 ? '' : 's'})`
|
||||
} else if (name === 'community' && $numberOfFollowRequests) {
|
||||
res += ` (${$numberOfFollowRequests} follow request${$numberOfFollowRequests === 1 ? '' : 's'})`
|
||||
}
|
||||
return res
|
||||
},
|
||||
showBadge: ({ name, $hasNotifications }) => name === 'notifications' && $hasNotifications,
|
||||
badgeNumber: ({ name, $numberOfNotifications }) => name === 'notifications' && $numberOfNotifications
|
||||
showBadge: ({ name, $hasNotifications, $hasFollowRequests }) => (
|
||||
(name === 'notifications' && $hasNotifications) || (name === 'community' && $hasFollowRequests)
|
||||
),
|
||||
badgeNumber: ({ name, $numberOfNotifications, $numberOfFollowRequests }) => (
|
||||
(name === 'notifications' && $numberOfNotifications) || (name === 'community' && $numberOfFollowRequests)
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
onClick (e) {
|
||||
|
|
|
@ -55,3 +55,11 @@ export async function getCustomEmoji (instanceName) {
|
|||
export async function setCustomEmoji (instanceName, value) {
|
||||
return setMetaProperty(instanceName, 'customEmoji', value)
|
||||
}
|
||||
|
||||
export async function getFollowRequestCount (instanceName) {
|
||||
return getMetaProperty(instanceName, 'followRequestCount')
|
||||
}
|
||||
|
||||
export async function setFollowRequestCount (instanceName, value) {
|
||||
return setMetaProperty(instanceName, 'followRequestCount', value)
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<PageList label="Instance settings">
|
||||
{#if isLockedAccount}
|
||||
<PageListItem href="/requests"
|
||||
label="Follow requests"
|
||||
label={followRequestsLabel}
|
||||
icon="#fa-user-plus"
|
||||
/>
|
||||
{/if}
|
||||
|
@ -109,12 +109,16 @@
|
|||
import PageList from '../../_components/community/PageList.html'
|
||||
import PageListItem from '../../_components/community/PageListItem.html'
|
||||
import { updateListsForInstance } from '../../_actions/lists'
|
||||
import { updateFollowRequestCountIfLockedAccount } from '../../_actions/followRequests'
|
||||
|
||||
export default {
|
||||
async oncreate () {
|
||||
const { currentInstance } = this.store.get()
|
||||
if (currentInstance) {
|
||||
await updateListsForInstance(currentInstance)
|
||||
await Promise.all([
|
||||
updateListsForInstance(currentInstance),
|
||||
updateFollowRequestCountIfLockedAccount(currentInstance)
|
||||
])
|
||||
}
|
||||
},
|
||||
store: () => store,
|
||||
|
@ -125,7 +129,10 @@
|
|||
PageListItem
|
||||
},
|
||||
computed: {
|
||||
isLockedAccount: ({ $currentVerifyCredentials }) => $currentVerifyCredentials && $currentVerifyCredentials.locked
|
||||
isLockedAccount: ({ $currentVerifyCredentials }) => $currentVerifyCredentials && $currentVerifyCredentials.locked,
|
||||
followRequestsLabel: ({ $hasFollowRequests, $numberOfFollowRequests }) => (
|
||||
`Follow requests${$hasFollowRequests ? ` (${$numberOfFollowRequests})` : ''}`
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -5,9 +5,18 @@
|
|||
<script>
|
||||
import AccountsListPage from '../_components/AccountsListPage.html'
|
||||
import { store } from '../_store/store'
|
||||
import { getFollowRequests } from '../_actions/followRequests'
|
||||
import { getFollowRequests } from '../_api/followRequests'
|
||||
import DynamicPageBanner from '../_components/DynamicPageBanner.html'
|
||||
import { setFollowRequestApprovedOrRejected } from '../_actions/requests'
|
||||
import { database } from '../_database/database'
|
||||
|
||||
// sneakily update the follow reqs count in the cache, since we just fetched it
|
||||
function updateFollowReqsCount ($currentInstance, followReqs) {
|
||||
/* no await */ database.setFollowRequestCount($currentInstance, followReqs.length)
|
||||
const { followRequestCounts } = store.get()
|
||||
followRequestCounts[$currentInstance] = followReqs.length
|
||||
store.set({ followRequestCounts })
|
||||
}
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
|
@ -25,7 +34,11 @@
|
|||
]
|
||||
}),
|
||||
computed: {
|
||||
accountsFetcher: ({ $currentInstance, $accessToken }) => () => getFollowRequests($currentInstance, $accessToken)
|
||||
accountsFetcher: ({ $currentInstance, $accessToken }) => async () => {
|
||||
const followReqs = await getFollowRequests($currentInstance, $accessToken)
|
||||
updateFollowReqsCount($currentInstance, followReqs)
|
||||
return followReqs
|
||||
}
|
||||
},
|
||||
store: () => store,
|
||||
components: {
|
||||
|
|
|
@ -181,4 +181,14 @@ export function timelineComputations (store) {
|
|||
currentPage !== 'notifications' && !!numberOfNotifications
|
||||
)
|
||||
)
|
||||
|
||||
store.compute('numberOfFollowRequests',
|
||||
['followRequestCounts', 'currentInstance'],
|
||||
(followRequestCounts, currentInstance) => get(followRequestCounts, [currentInstance], 0)
|
||||
)
|
||||
|
||||
store.compute('hasFollowRequests',
|
||||
['numberOfFollowRequests'],
|
||||
(numberOfFollowRequests) => !!numberOfFollowRequests
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { setupCustomEmojiForInstance } from '../../_actions/emoji'
|
|||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||
import { mark, stop } from '../../_utils/marks'
|
||||
import { store } from '../store'
|
||||
import { updateFollowRequestCountIfLockedAccount } from '../../_actions/followRequests'
|
||||
|
||||
// stream to watch for home timeline updates and notifications
|
||||
let currentInstanceStream
|
||||
|
@ -48,7 +49,10 @@ async function refreshInstanceData (instanceName) {
|
|||
// these are the only critical ones
|
||||
await Promise.all([
|
||||
updateInstanceInfo(instanceName),
|
||||
updateVerifyCredentialsForInstance(instanceName)
|
||||
updateVerifyCredentialsForInstance(instanceName).then(() => {
|
||||
// Once we have the verifyCredentials (so we know if the account is locked), lazily update the follow requests
|
||||
scheduleIdleTask(() => updateFollowRequestCountIfLockedAccount(instanceName))
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ const persistedState = {
|
|||
|
||||
const nonPersistedState = {
|
||||
customEmoji: {},
|
||||
followRequestCounts: {},
|
||||
instanceInfos: {},
|
||||
instanceLists: {},
|
||||
online: !process.browser || navigator.onLine,
|
||||
|
|
|
@ -4,7 +4,7 @@ import FileApi from 'file-api'
|
|||
import { users } from './users'
|
||||
import { postStatus } from '../src/routes/_api/statuses'
|
||||
import { deleteStatus } from '../src/routes/_api/delete'
|
||||
import { authorizeFollowRequest, getFollowRequests } from '../src/routes/_actions/followRequests'
|
||||
import { authorizeFollowRequest, getFollowRequests } from '../src/routes/_api/followRequests'
|
||||
import { followAccount, unfollowAccount } from '../src/routes/_api/follow'
|
||||
import { updateCredentials } from '../src/routes/_api/updateCredentials'
|
||||
import { reblogStatus } from '../src/routes/_api/reblog'
|
||||
|
|
|
@ -13,15 +13,23 @@ fixture`116-follow-requests.js`
|
|||
|
||||
const timeout = 30000
|
||||
|
||||
test('Can approve and reject follow requests', async t => {
|
||||
await loginAsLockedAccount(t)
|
||||
const requestsButton = $('a[href="/requests"]')
|
||||
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)')
|
||||
|
||||
async function resetFollows () {
|
||||
// necessary for re-running this test in local testing
|
||||
await Promise.all([
|
||||
unfollowAs('admin', 'LockedAccount'),
|
||||
unfollowAs('baz', 'LockedAccount'),
|
||||
unfollowAs('quux', 'LockedAccount')
|
||||
])
|
||||
}
|
||||
|
||||
test('Can approve and reject follow requests', async t => {
|
||||
await loginAsLockedAccount(t)
|
||||
await resetFollows()
|
||||
|
||||
await Promise.all([
|
||||
followAs('admin', 'LockedAccount'),
|
||||
|
@ -31,12 +39,10 @@ test('Can approve and reject follow requests', async t => {
|
|||
|
||||
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"]'))
|
||||
await t
|
||||
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (3 follow requests)')
|
||||
.click(communityNavButton)
|
||||
.click(requestsButton)
|
||||
// no guaranteed order on these
|
||||
.expect(getNthSearchResult(1).innerText).match(/(@admin|@baz|@quux)/)
|
||||
.expect(getNthSearchResult(2).innerText).match(/(@admin|@baz|@quux)/)
|
||||
|
@ -51,7 +57,7 @@ test('Can approve and reject follow requests', async t => {
|
|||
.expect(getNthSearchResult(3).exists).notOk()
|
||||
await goBack()
|
||||
await t
|
||||
.click($('a[href="/requests"]'))
|
||||
.click(requestsButton)
|
||||
// reject baz
|
||||
.expect(rejectBazButton().getAttribute('aria-label')).eql('Reject')
|
||||
.hover(rejectBazButton())
|
||||
|
@ -60,7 +66,7 @@ test('Can approve and reject follow requests', async t => {
|
|||
.expect(getNthSearchResult(2).exists).notOk()
|
||||
await goBack()
|
||||
await t
|
||||
.click($('a[href="/requests"]'))
|
||||
.click(requestsButton)
|
||||
// approve quux
|
||||
.expect(approveQuuxButton().getAttribute('aria-label')).eql('Approve')
|
||||
.hover(approveQuuxButton())
|
||||
|
@ -75,3 +81,43 @@ test('Can approve and reject follow requests', async t => {
|
|||
.expect(getNthSearchResult(2).innerText).match(/(@admin|@quux)/)
|
||||
.expect(getNthSearchResult(3).exists).notOk()
|
||||
})
|
||||
|
||||
test('Shows unresolved follow requests', async t => {
|
||||
await resetFollows()
|
||||
await sleep(2000)
|
||||
await Promise.all([
|
||||
followAs('admin', 'LockedAccount'),
|
||||
followAs('baz', 'LockedAccount')
|
||||
])
|
||||
await sleep(2000)
|
||||
await loginAsLockedAccount(t)
|
||||
|
||||
await t
|
||||
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (2 follow requests)')
|
||||
.click(communityNavButton)
|
||||
.expect(requestsButton.innerText).contains('Follow requests (2)')
|
||||
.click(requestsButton)
|
||||
.expect(getUrl()).contains('/requests')
|
||||
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (2 follow requests)')
|
||||
.click(approveAdminButton())
|
||||
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (1 follow request)')
|
||||
.click(rejectBazButton())
|
||||
.expect(communityNavButton.getAttribute('aria-label')).eql('Community')
|
||||
await goBack()
|
||||
await t
|
||||
.expect(requestsButton.innerText).contains('Follow requests')
|
||||
})
|
||||
|
||||
test('Shows unresolved follow requests immediately upon opening community page', async t => {
|
||||
await resetFollows()
|
||||
await sleep(2000)
|
||||
await loginAsLockedAccount(t)
|
||||
await sleep(2000)
|
||||
await followAs('admin', 'LockedAccount')
|
||||
await sleep(2000)
|
||||
await t
|
||||
.expect(communityNavButton.getAttribute('aria-label')).eql('Community')
|
||||
.click(communityNavButton)
|
||||
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (current page) (1 follow request)')
|
||||
.expect(requestsButton.innerText).contains('Follow requests (1)')
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue