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 { store } from '../_store/store'
|
||||||
import { auth, basename } from '../_api/utils'
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
|
import { database } from '../_database/database'
|
||||||
|
import { getFollowRequests } from '../_api/followRequests'
|
||||||
|
|
||||||
export async function getFollowRequests (instanceName, accessToken) {
|
export async function updateFollowRequestCountIfLockedAccount (instanceName) {
|
||||||
const url = `${basename(instanceName)}/api/v1/follow_requests`
|
const { verifyCredentials, loggedInInstances } = store.get()
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function authorizeFollowRequest (instanceName, accessToken, id) {
|
if (!verifyCredentials[instanceName].locked) {
|
||||||
const url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
|
return
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export async function rejectFollowRequest (instanceName, accessToken, id) {
|
const accessToken = loggedInInstances[instanceName].access_token
|
||||||
const url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
|
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
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 })
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// special case – these should both highlight the notifications tab icon
|
||||||
(name === 'notifications' && page === 'notifications/mentions')
|
(name === 'notifications' && page === 'notifications/mentions')
|
||||||
},
|
},
|
||||||
ariaLabel: ({ selected, name, label, $numberOfNotifications }) => {
|
ariaLabel: ({ selected, name, label, $numberOfNotifications, $numberOfFollowRequests }) => {
|
||||||
let res = label
|
let res = label
|
||||||
if (selected) {
|
if (selected) {
|
||||||
res += ' (current page)'
|
res += ' (current page)'
|
||||||
}
|
}
|
||||||
if (name === 'notifications' && $numberOfNotifications) {
|
if (name === 'notifications' && $numberOfNotifications) {
|
||||||
res += ` (${$numberOfNotifications} notification${$numberOfNotifications === 1 ? '' : 's'})`
|
res += ` (${$numberOfNotifications} notification${$numberOfNotifications === 1 ? '' : 's'})`
|
||||||
|
} else if (name === 'community' && $numberOfFollowRequests) {
|
||||||
|
res += ` (${$numberOfFollowRequests} follow request${$numberOfFollowRequests === 1 ? '' : 's'})`
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
showBadge: ({ name, $hasNotifications }) => name === 'notifications' && $hasNotifications,
|
showBadge: ({ name, $hasNotifications, $hasFollowRequests }) => (
|
||||||
badgeNumber: ({ name, $numberOfNotifications }) => name === 'notifications' && $numberOfNotifications
|
(name === 'notifications' && $hasNotifications) || (name === 'community' && $hasFollowRequests)
|
||||||
|
),
|
||||||
|
badgeNumber: ({ name, $numberOfNotifications, $numberOfFollowRequests }) => (
|
||||||
|
(name === 'notifications' && $numberOfNotifications) || (name === 'community' && $numberOfFollowRequests)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick (e) {
|
onClick (e) {
|
||||||
|
|
|
@ -55,3 +55,11 @@ export async function getCustomEmoji (instanceName) {
|
||||||
export async function setCustomEmoji (instanceName, value) {
|
export async function setCustomEmoji (instanceName, value) {
|
||||||
return setMetaProperty(instanceName, 'customEmoji', 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">
|
<PageList label="Instance settings">
|
||||||
{#if isLockedAccount}
|
{#if isLockedAccount}
|
||||||
<PageListItem href="/requests"
|
<PageListItem href="/requests"
|
||||||
label="Follow requests"
|
label={followRequestsLabel}
|
||||||
icon="#fa-user-plus"
|
icon="#fa-user-plus"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -109,12 +109,16 @@
|
||||||
import PageList from '../../_components/community/PageList.html'
|
import PageList from '../../_components/community/PageList.html'
|
||||||
import PageListItem from '../../_components/community/PageListItem.html'
|
import PageListItem from '../../_components/community/PageListItem.html'
|
||||||
import { updateListsForInstance } from '../../_actions/lists'
|
import { updateListsForInstance } from '../../_actions/lists'
|
||||||
|
import { updateFollowRequestCountIfLockedAccount } from '../../_actions/followRequests'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async oncreate () {
|
async oncreate () {
|
||||||
const { currentInstance } = this.store.get()
|
const { currentInstance } = this.store.get()
|
||||||
if (currentInstance) {
|
if (currentInstance) {
|
||||||
await updateListsForInstance(currentInstance)
|
await Promise.all([
|
||||||
|
updateListsForInstance(currentInstance),
|
||||||
|
updateFollowRequestCountIfLockedAccount(currentInstance)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
|
@ -125,7 +129,10 @@
|
||||||
PageListItem
|
PageListItem
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isLockedAccount: ({ $currentVerifyCredentials }) => $currentVerifyCredentials && $currentVerifyCredentials.locked
|
isLockedAccount: ({ $currentVerifyCredentials }) => $currentVerifyCredentials && $currentVerifyCredentials.locked,
|
||||||
|
followRequestsLabel: ({ $hasFollowRequests, $numberOfFollowRequests }) => (
|
||||||
|
`Follow requests${$hasFollowRequests ? ` (${$numberOfFollowRequests})` : ''}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,9 +5,18 @@
|
||||||
<script>
|
<script>
|
||||||
import AccountsListPage from '../_components/AccountsListPage.html'
|
import AccountsListPage from '../_components/AccountsListPage.html'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { getFollowRequests } from '../_actions/followRequests'
|
import { getFollowRequests } from '../_api/followRequests'
|
||||||
import DynamicPageBanner from '../_components/DynamicPageBanner.html'
|
import DynamicPageBanner from '../_components/DynamicPageBanner.html'
|
||||||
import { setFollowRequestApprovedOrRejected } from '../_actions/requests'
|
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 {
|
export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
@ -25,7 +34,11 @@
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
accountsFetcher: ({ $currentInstance, $accessToken }) => () => getFollowRequests($currentInstance, $accessToken)
|
accountsFetcher: ({ $currentInstance, $accessToken }) => async () => {
|
||||||
|
const followReqs = await getFollowRequests($currentInstance, $accessToken)
|
||||||
|
updateFollowReqsCount($currentInstance, followReqs)
|
||||||
|
return followReqs
|
||||||
|
}
|
||||||
},
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -181,4 +181,14 @@ export function timelineComputations (store) {
|
||||||
currentPage !== 'notifications' && !!numberOfNotifications
|
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 { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||||
import { mark, stop } from '../../_utils/marks'
|
import { mark, stop } from '../../_utils/marks'
|
||||||
import { store } from '../store'
|
import { store } from '../store'
|
||||||
|
import { updateFollowRequestCountIfLockedAccount } from '../../_actions/followRequests'
|
||||||
|
|
||||||
// stream to watch for home timeline updates and notifications
|
// stream to watch for home timeline updates and notifications
|
||||||
let currentInstanceStream
|
let currentInstanceStream
|
||||||
|
@ -48,7 +49,10 @@ async function refreshInstanceData (instanceName) {
|
||||||
// these are the only critical ones
|
// these are the only critical ones
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
updateInstanceInfo(instanceName),
|
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 = {
|
const nonPersistedState = {
|
||||||
customEmoji: {},
|
customEmoji: {},
|
||||||
|
followRequestCounts: {},
|
||||||
instanceInfos: {},
|
instanceInfos: {},
|
||||||
instanceLists: {},
|
instanceLists: {},
|
||||||
online: !process.browser || navigator.onLine,
|
online: !process.browser || navigator.onLine,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import FileApi from 'file-api'
|
||||||
import { users } from './users'
|
import { users } from './users'
|
||||||
import { postStatus } from '../src/routes/_api/statuses'
|
import { postStatus } from '../src/routes/_api/statuses'
|
||||||
import { deleteStatus } from '../src/routes/_api/delete'
|
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 { followAccount, unfollowAccount } from '../src/routes/_api/follow'
|
||||||
import { updateCredentials } from '../src/routes/_api/updateCredentials'
|
import { updateCredentials } from '../src/routes/_api/updateCredentials'
|
||||||
import { reblogStatus } from '../src/routes/_api/reblog'
|
import { reblogStatus } from '../src/routes/_api/reblog'
|
||||||
|
|
|
@ -13,15 +13,23 @@ fixture`116-follow-requests.js`
|
||||||
|
|
||||||
const timeout = 30000
|
const timeout = 30000
|
||||||
|
|
||||||
test('Can approve and reject follow requests', async t => {
|
const requestsButton = $('a[href="/requests"]')
|
||||||
await loginAsLockedAccount(t)
|
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
|
// necessary for re-running this test in local testing
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
unfollowAs('admin', 'LockedAccount'),
|
unfollowAs('admin', 'LockedAccount'),
|
||||||
unfollowAs('baz', 'LockedAccount'),
|
unfollowAs('baz', 'LockedAccount'),
|
||||||
unfollowAs('quux', 'LockedAccount')
|
unfollowAs('quux', 'LockedAccount')
|
||||||
])
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Can approve and reject follow requests', async t => {
|
||||||
|
await loginAsLockedAccount(t)
|
||||||
|
await resetFollows()
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
followAs('admin', 'LockedAccount'),
|
followAs('admin', 'LockedAccount'),
|
||||||
|
@ -31,12 +39,10 @@ test('Can approve and reject follow requests', async t => {
|
||||||
|
|
||||||
await sleep(2000)
|
await sleep(2000)
|
||||||
|
|
||||||
const approveAdminButton = () => getSearchResultByHref(`/accounts/${users.admin.id}`).find('button:nth-child(1)')
|
await t
|
||||||
const rejectBazButton = () => getSearchResultByHref(`/accounts/${users.baz.id}`).find('button:nth-child(2)')
|
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (3 follow requests)')
|
||||||
const approveQuuxButton = () => getSearchResultByHref(`/accounts/${users.quux.id}`).find('button:nth-child(1)')
|
.click(communityNavButton)
|
||||||
|
.click(requestsButton)
|
||||||
await t.click(communityNavButton)
|
|
||||||
.click($('a[href="/requests"]'))
|
|
||||||
// no guaranteed order on these
|
// no guaranteed order on these
|
||||||
.expect(getNthSearchResult(1).innerText).match(/(@admin|@baz|@quux)/)
|
.expect(getNthSearchResult(1).innerText).match(/(@admin|@baz|@quux)/)
|
||||||
.expect(getNthSearchResult(2).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()
|
.expect(getNthSearchResult(3).exists).notOk()
|
||||||
await goBack()
|
await goBack()
|
||||||
await t
|
await t
|
||||||
.click($('a[href="/requests"]'))
|
.click(requestsButton)
|
||||||
// reject baz
|
// reject baz
|
||||||
.expect(rejectBazButton().getAttribute('aria-label')).eql('Reject')
|
.expect(rejectBazButton().getAttribute('aria-label')).eql('Reject')
|
||||||
.hover(rejectBazButton())
|
.hover(rejectBazButton())
|
||||||
|
@ -60,7 +66,7 @@ test('Can approve and reject follow requests', async t => {
|
||||||
.expect(getNthSearchResult(2).exists).notOk()
|
.expect(getNthSearchResult(2).exists).notOk()
|
||||||
await goBack()
|
await goBack()
|
||||||
await t
|
await t
|
||||||
.click($('a[href="/requests"]'))
|
.click(requestsButton)
|
||||||
// approve quux
|
// approve quux
|
||||||
.expect(approveQuuxButton().getAttribute('aria-label')).eql('Approve')
|
.expect(approveQuuxButton().getAttribute('aria-label')).eql('Approve')
|
||||||
.hover(approveQuuxButton())
|
.hover(approveQuuxButton())
|
||||||
|
@ -75,3 +81,43 @@ test('Can approve and reject follow requests', async t => {
|
||||||
.expect(getNthSearchResult(2).innerText).match(/(@admin|@quux)/)
|
.expect(getNthSearchResult(2).innerText).match(/(@admin|@quux)/)
|
||||||
.expect(getNthSearchResult(3).exists).notOk()
|
.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 New Issue