feat: add account page filters (#1124)

* start on account page filters

fixes #1021

* making progress

* more progress, just need style now

* fix lint

* fix style and add test
This commit is contained in:
Nolan Lawson 2019-03-30 21:48:49 -07:00 committed by GitHub
parent 4d11e0ffbe
commit 6744de59f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 305 additions and 58 deletions

View file

@ -27,11 +27,11 @@ export function getTimeline (instanceName, accessToken, timeline, maxId, since,
let url = `${basename(instanceName)}/api/v1/${timelineUrlName}` let url = `${basename(instanceName)}/api/v1/${timelineUrlName}`
if (timeline.startsWith('tag/')) { if (timeline.startsWith('tag/')) {
url += '/' + timeline.split('/').slice(-1)[0] url += '/' + timeline.split('/')[1]
} else if (timeline.startsWith('account/')) { } else if (timeline.startsWith('account/')) {
url += '/' + timeline.split('/').slice(-1)[0] + '/statuses' url += '/' + timeline.split('/')[1] + '/statuses'
} else if (timeline.startsWith('list/')) { } else if (timeline.startsWith('list/')) {
url += '/' + timeline.split('/').slice(-1)[0] url += '/' + timeline.split('/')[1]
} }
let params = {} let params = {}
@ -51,6 +51,14 @@ export function getTimeline (instanceName, accessToken, timeline, maxId, since,
params.local = true params.local = true
} }
if (timeline.startsWith('account/')) {
if (timeline.endsWith('media')) {
params.only_media = true
} else {
params.exclude_replies = !timeline.endsWith('/with_replies')
}
}
url += '?' + paramsString(params) url += '?' + paramsString(params)
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT }) return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })

View file

@ -0,0 +1,107 @@
<nav aria-label="Filters" class="account-profile-filters">
<ul>
{#each filterTabs as filterTab (filterTab.href)}
<li class="{filter === filterTab.filter ? 'current-filter' : 'not-current-filter'}">
<a aria-label="{filterTab.label} { filter === filterTab.filter ? '(Current)' : ''}"
href={filterTab.href}
rel="prefetch">
{filterTab.label}
</a>
</li>
{/each}
</ul>
</nav>
<style>
li {
flex: 1;
text-align: center;
}
/* reset */
ul, li {
margin: 0;
padding: 0;
}
ul {
list-style: none;
display: flex;
margin: 5px 0;
box-sizing: border-box;
}
li {
border: 1px solid var(--main-border);
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background: var(--tab-bg);
}
li:not(:first-child) {
border-left: none;
}
li:hover {
background: var(--button-bg-hover);
}
li.not-current-filter {
background: var(--tab-bg-non-selected);
}
li.current-filter {
border-bottom: none;
}
li.current-filter:hover {
background: var(--tab-bg-hover);
}
li.not-current-filter:hover {
background: var(--tab-bg-hover-non-selected);
}
li:active {
background: var(--tab-bg-active);
}
a {
padding: 10px;
color: var(--body-text-color);
font-size: 1.1em;
flex: 1;
}
a:hover {
text-decoration: none;
}
</style>
<script>
export default {
computed: {
filterTabs: ({ account }) => (
[
{
filter: '',
label: 'Toots',
href: `/accounts/${account.id}`
},
{
filter: 'with_replies',
label: 'Toots and replies',
href: `/accounts/${account.id}/with_replies`
},
{
filter: 'media',
label: 'Media',
href: `/accounts/${account.id}/media`
}
]
)
}
}
</script>

View file

@ -0,0 +1,66 @@
{#if $isUserLoggedIn}
<TimelinePage {timeline} >
<DynamicPageBanner title="" ariaTitle="Profile page for {accountName}"/>
{#if $currentAccountProfile && $currentVerifyCredentials}
<AccountProfile account={$currentAccountProfile}
relationship={$currentAccountRelationship}
verifyCredentials={$currentVerifyCredentials}
/>
<AccountProfileFilters account={$currentAccountProfile} {filter} />
{/if}
{#if !filter}
<PinnedStatuses {accountId} />
{/if}
</TimelinePage>
{:else}
<HiddenFromSSR>
<FreeTextLayout>
<h1>Profile</h1>
<p>A user timeline will appear here when logged in.</p>
</FreeTextLayout>
</HiddenFromSSR>
{/if}
<script>
import TimelinePage from '../TimelinePage.html'
import FreeTextLayout from '../FreeTextLayout.html'
import { store } from '../../_store/store.js'
import HiddenFromSSR from '../HiddenFromSSR'
import DynamicPageBanner from '../DynamicPageBanner.html'
import { updateProfileAndRelationship, clearProfileAndRelationship } from '../../_actions/accounts'
import AccountProfile from './AccountProfile.html'
import PinnedStatuses from '../timeline/PinnedStatuses.html'
import AccountProfileFilters from './AccountProfileFilters.html'
export default {
oncreate () {
let { accountId } = this.get()
clearProfileAndRelationship()
updateProfileAndRelationship(accountId)
},
store: () => store,
computed: {
profileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.acct)) || ''
},
shortProfileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.username)) || ''
},
accountName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ($currentAccountProfile.display_name || $currentAccountProfile.username)) || ''
},
timeline: ({ accountId, filter }) => (
`account/${accountId}` + (filter ? `/${filter}` : '')
)
},
components: {
TimelinePage,
FreeTextLayout,
HiddenFromSSR,
DynamicPageBanner,
AccountProfile,
PinnedStatuses,
AccountProfileFilters
}
}
</script>

View file

@ -1,59 +1,10 @@
{#if $isUserLoggedIn} <AccountProfilePage accountId={params.accountId} filter="" />
<TimelinePage timeline="account/{params.accountId}">
<DynamicPageBanner title="" ariaTitle="Profile page for {accountName}"/>
{#if $currentAccountProfile && $currentVerifyCredentials}
<AccountProfile account={$currentAccountProfile}
relationship={$currentAccountRelationship}
verifyCredentials={$currentVerifyCredentials}
/>
{/if}
<PinnedStatuses accountId={params.accountId} />
</TimelinePage>
{:else}
<HiddenFromSSR>
<FreeTextLayout>
<h1>Profile</h1>
<p>A user timeline will appear here when logged in.</p>
</FreeTextLayout>
</HiddenFromSSR>
{/if}
<script> <script>
import TimelinePage from '../../../_components/TimelinePage.html' import AccountProfilePage from '../../../_components/profile/AccountProfilePage.html'
import FreeTextLayout from '../../../_components/FreeTextLayout.html'
import { store } from '../../../_store/store.js'
import HiddenFromSSR from '../../../_components/HiddenFromSSR'
import DynamicPageBanner from '../../../_components/DynamicPageBanner.html'
import { updateProfileAndRelationship, clearProfileAndRelationship } from '../../../_actions/accounts'
import AccountProfile from '../../../_components/profile/AccountProfile.html'
import PinnedStatuses from '../../../_components/timeline/PinnedStatuses.html'
export default { export default {
oncreate () {
let { params } = this.get()
let { accountId } = params
clearProfileAndRelationship()
updateProfileAndRelationship(accountId)
},
store: () => store,
computed: {
profileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.acct)) || ''
},
shortProfileName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ('@' + $currentAccountProfile.username)) || ''
},
accountName: ({ $currentAccountProfile }) => {
return ($currentAccountProfile && ($currentAccountProfile.display_name || $currentAccountProfile.username)) || ''
}
},
components: { components: {
TimelinePage, AccountProfilePage
FreeTextLayout,
HiddenFromSSR,
DynamicPageBanner,
AccountProfile,
PinnedStatuses
} }
} }
</script> </script>

View file

@ -0,0 +1,10 @@
<AccountProfilePage accountId={params.accountId} filter="media" />
<script>
import AccountProfilePage from '../../../_components/profile/AccountProfilePage.html'
export default {
components: {
AccountProfilePage
}
}
</script>

View file

@ -0,0 +1,10 @@
<AccountProfilePage accountId={params.accountId} filter="with_replies" />
<script>
import AccountProfilePage from '../../../_components/profile/AccountProfilePage.html'
export default {
components: {
AccountProfilePage
}
}
</script>

View file

@ -23,9 +23,17 @@ export function timelineComputations (store) {
store.compute('currentTimelineType', ['currentTimeline'], currentTimeline => ( store.compute('currentTimelineType', ['currentTimeline'], currentTimeline => (
currentTimeline && currentTimeline.split('/')[0]) currentTimeline && currentTimeline.split('/')[0])
) )
store.compute('currentTimelineValue', ['currentTimeline'], currentTimeline => ( store.compute('currentTimelineValue', ['currentTimeline'], currentTimeline => {
currentTimeline && currentTimeline.split('/').slice(-1)[0]) if (!currentTimeline) {
) return void 0
}
let split = currentTimeline.split('/')
let len = split.length
if (split[len - 1] === 'with_replies' || split[len - 1] === 'media') {
return split[len - 2]
}
return split[len - 1]
})
store.compute('firstTimelineItemId', ['timelineItemSummaries'], (timelineItemSummaries) => ( store.compute('firstTimelineItemId', ['timelineItemSummaries'], (timelineItemSummaries) => (
getFirstIdFromItemSummaries(timelineItemSummaries) getFirstIdFromItemSummaries(timelineItemSummaries)
)) ))

View file

@ -0,0 +1,20 @@
<Title name="Profile with media" />
<LazyPage {pageComponent} {params} />
<script>
import Title from '../../_components/Title.html'
import LazyPage from '../../_components/LazyPage.html'
import pageComponent from '../../_pages/accounts/[accountId]/media.html'
export default {
components: {
Title,
LazyPage
},
data: () => ({
pageComponent
})
}
</script>

View file

@ -0,0 +1,20 @@
<Title name="Profile with replies" />
<LazyPage {pageComponent} {params} />
<script>
import Title from '../../_components/Title.html'
import LazyPage from '../../_components/LazyPage.html'
import pageComponent from '../../_pages/accounts/[accountId]/with_replies.html'
export default {
components: {
Title,
LazyPage
},
data: () => ({
pageComponent
})
}
</script>

View file

@ -100,4 +100,10 @@
--file-drop-mask: #{rgba(255, 255, 255, 0.8)}; --file-drop-mask: #{rgba(255, 255, 255, 0.8)};
--banner-fill: #{$main-theme-color}; --banner-fill: #{$main-theme-color};
--tab-bg: #{$main-bg-color};
--tab-bg-non-selected: #{darken($main-bg-color, 3%)};
--tab-bg-active: #{darken($main-bg-color, 25%)};
--tab-bg-hover: #{darken($main-bg-color, 4%)};
--tab-bg-hover-non-selected: #{darken($main-bg-color, 7%)};
} }

View file

@ -38,4 +38,10 @@
--settings-list-item-bg-hover: #{lighten($main-bg-color, 3%)}; --settings-list-item-bg-hover: #{lighten($main-bg-color, 3%)};
--banner-fill: #{lighten($main-theme-color, 10%)}; --banner-fill: #{lighten($main-theme-color, 10%)};
--tab-bg: #{$main-bg-color};
--tab-bg-non-selected: #{darken($main-bg-color, 2%)};
--tab-bg-active: #{lighten($main-bg-color, 15%)};
--tab-bg-hover: #{lighten($main-bg-color, 1%)};
--tab-bg-hover-non-selected: #{darken($main-bg-color, 1%)};
} }

View file

@ -0,0 +1,31 @@
import {
accountProfileFilterMedia, accountProfileFilterStatuses,
accountProfileFilterStatusesAndReplies,
avatarInComposeBox,
getNthPinnedStatus, getNthStatus,
getUrl
} from '../utils'
import { loginAsFoobar } from '../roles'
fixture`031-account-filters.js`
.page`http://localhost:4002`
test('Basic account filters test', async t => {
await loginAsFoobar(t)
await t
.click(avatarInComposeBox)
.expect(getUrl()).contains('/accounts/2')
.expect(getNthPinnedStatus(1).innerText).contains('this is unlisted')
.expect(getNthStatus(1).innerText).contains('this is unlisted')
.click(accountProfileFilterStatusesAndReplies)
.expect(getUrl()).contains('/accounts/2/with_replies')
.expect(getNthPinnedStatus(1).exists).notOk()
.expect(getNthStatus(1).innerText).contains('this is unlisted')
.click(accountProfileFilterMedia)
.expect(getNthPinnedStatus(1).exists).notOk()
.expect(getNthStatus(1).innerText).contains('kitten CW')
.click(accountProfileFilterStatuses)
.expect(getUrl()).contains('/accounts/2')
.expect(getNthPinnedStatus(1).innerText).contains('this is unlisted')
.expect(getNthStatus(1).innerText).contains('this is unlisted')
})

View file

@ -60,6 +60,10 @@ export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolb
export const postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button') export const postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button')
export const accountProfileFilterStatuses = $('.account-profile-filters li:nth-child(1)')
export const accountProfileFilterStatusesAndReplies = $('.account-profile-filters li:nth-child(2)')
export const accountProfileFilterMedia = $('.account-profile-filters li:nth-child(3)')
export function getComposeModalNthMediaAltInput (n) { export function getComposeModalNthMediaAltInput (n) {
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt input`) return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt input`)
} }