first stab at account pages

This commit is contained in:
Nolan Lawson 2018-01-22 21:16:27 -08:00
parent 5d9eba58be
commit ab291a2c7e
10 changed files with 199 additions and 57 deletions

View File

@ -0,0 +1,44 @@
<div class="dynamic-page-banner">
<h1 class="dynamic-page-title">{{title}}</h1>
<button type="button"
class="dynamic-page-go-back"
on:click="onGoBack(event)">Back</button>
</div>
<style>
.dynamic-page-banner {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 20px 20px;
}
h1.dynamic-page-title {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
button.dynamic-page-go-back {
font-size: 1.3em;
color: var(--anchor-text);
border: 0;
padding: 0;
background: none;
}
button.dynamic-page-go-back:hover {
text-decoration: underline;
}
button.dynamic-page-go-back::before {
content: '<';
margin-right: 5px;
}
</style>
<script>
export default {
methods: {
onGoBack(e) {
e.preventDefault()
window.history.back();
}
}
}
</script>

View File

@ -50,6 +50,9 @@
font-size: 16px;
color: var(--nav-text-color);
padding-left: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 767px) {

View File

@ -32,8 +32,8 @@
</div>
{{/if}}
{{#if !status.spoiler_text || spoilerShown}}
<div class="status-content">
{{{hydratedContent}}}
<div class="status-content" ref:contentNode>
{{{emojifiedContent}}}
</div>
{{/if}}
{{#if originalMediaAttachments && originalMediaAttachments.length}}
@ -295,6 +295,9 @@
const relativeFormat = new IntlRelativeFormat('en-US');
export default {
oncreate() {
this.hashtagifyContent()
},
components: {
Avatar,
Media,
@ -311,15 +314,9 @@
originalStatus: (status) => status.reblog ? status.reblog : status,
originalAccount: (originalStatus) => originalStatus.account,
originalMediaAttachments: (originalStatus) => originalStatus.media_attachments,
hydratedContent: (originalStatus) => {
emojifiedContent: (originalStatus) => {
let status = originalStatus
let content = status.content
if (status.tags && status.tags.length) {
for (let tag of status.tags) {
let {name, url} = tag
content = replaceAll(content, url, `/tags/${name}`)
}
}
if (status.emojis && status.emojis.length) {
for (let emoji of status.emojis) {
let { shortcode, url } = emoji
@ -337,11 +334,34 @@
methods: {
onClickSpoilerButton() {
this.set({spoilerShown: !this.get('spoilerShown')})
this.hashtagifyContent()
this.fire('recalculateHeight')
},
onClickSensitiveMediaButton() {
this.set({sensitiveShown: !this.get('sensitiveShown')})
this.fire('recalculateHeight')
},
hashtagifyContent() {
if (!this.refs.contentNode) {
return
}
let status = this.get('originalStatus')
mark('hydrateHashtags')
if (status.tags && status.tags.length) {
let anchorTags = Array.from(this.refs.contentNode.querySelectorAll(
'a[class~=hashtag][href^=http]'))
for (let tag of status.tags) {
let { name } = tag
for (let anchorTag of anchorTags) {
if (anchorTag.getAttribute('href').endsWith(`/tags/${name}`)) {
anchorTag.setAttribute('href', `/tags/${name}`)
anchorTag.removeAttribute('target')
anchorTag.removeAttribute('rel')
}
}
}
}
stop('hydrateHashtags')
}
}
}

View File

@ -38,6 +38,22 @@
this.fire('initialized')
}))
})
this.observe('statuses', statuses => {
let cachedAccountNames = this.store.get('cachedAccountNames') || {}
for (let status of statuses) {
cachedAccountNames[status.account.id] = {
username: status.account.username,
acct: status.account.acct
}
if (status.reblog) {
cachedAccountNames[status.reblog.account.id] = {
username: status.reblog.account.username,
acct: status.reblog.account.acct
}
}
}
this.store.set({'cachedAccountNames': cachedAccountNames})
})
},
data: () => ({
StatusListItem: StatusListItem,
@ -58,6 +74,9 @@
} else if (timeline.startsWith('tag/')) {
let tag = timeline.split('/').slice(-1)[0]
return `#${tag} timeline for ${$currentInstance}`
} else if (timeline.startsWith('account/')) {
let account = timeline.split('/').slice(-1)[0]
return `Account #${account} on ${$currentInstance}`
}
}
},

View File

@ -1,6 +1,8 @@
/*
import worker from 'workerize-loader!./databaseCore'
const database = process.browser && worker()
export const database = process.browser && worker()
*/
export {
database
}
import * as dbCore from './databaseCore'
export { dbCore as database }

View File

@ -1,24 +1,29 @@
import { get, paramsString } from '../ajax'
import { basename } from './utils'
function getTimelineUrlName(timeline) {
function getTimelineUrlPath(timeline) {
switch (timeline) {
case 'local':
case 'federated':
return 'public'
return 'timelines/public'
case 'home':
return 'home'
default:
return 'tag'
return 'timelines/home'
}
if (timeline.startsWith('tag/')) {
return 'timelines/tag'
} else if (timeline.startsWith('account/')) {
return 'accounts'
}
}
export function getTimeline(instanceName, accessToken, timeline, maxId, since) {
let timelineUrlName = getTimelineUrlName(timeline)
let url = `${basename(instanceName)}/api/v1/timelines/${timelineUrlName}`
let timelineUrlName = getTimelineUrlPath(timeline)
let url = `${basename(instanceName)}/api/v1/${timelineUrlName}`
if (timeline.startsWith('tag/')) {
url += '/' + timeline.split('/').slice(-1)[0]
} else if (timeline.startsWith('account/')) {
url += '/' + timeline.split('/').slice(-1)[0] +'/statuses'
}
let params = {}

View File

@ -6,4 +6,11 @@ export function getVerifyCredentials(instanceName, accessToken) {
return get(url, {
'Authorization': `Bearer ${accessToken}`
})
}
export function getAccount(instanceName, accessToken, accountId) {
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}`
return get(url, {
'Authorization': `Bearer ${accessToken}`
})
}

View File

@ -1,5 +1,9 @@
import { Store } from 'svelte/store.js'
const DONT_STORE_THESE_KEYS = [
'cachedAccountNames'
]
const LS = process.browser && localStorage
class LocalStorageStore extends Store {
@ -18,7 +22,8 @@ class LocalStorageStore extends Store {
this.set(newState)
this.onchange((state, changed) => {
Object.keys(changed).forEach(change => {
if (!this._computed[change]) { // TODO: better way to ignore computed values?
if (!DONT_STORE_THESE_KEYS.includes(change) &&
!this._computed[change]) { // TODO: better way to ignore computed values?
this.lastChanged[change] = true
}
})
@ -74,6 +79,12 @@ store.compute(
}, loggedInInstances[currentInstance])
})
store.compute(
'accessToken',
['currentInstanceData'],
(currentInstanceData) => currentInstanceData.access_token
)
store.compute(
'currentTheme',
['currentInstance', 'instanceThemes'],

View File

@ -0,0 +1,63 @@
<:Head>
<title>{{'Pinafore ' + (cachedProfileName || profileName || '')}}</title>
</:Head>
<Layout page='tags'
dynamicPage="{{cachedProfileName || profileName || ''}}"
dynamicHref="/accounts/{{params.accountId}}"
dynamicLabel="{{cachedShortProfileName || shortProfileName || ''}}"
dynamicIcon="#fa-user" >
{{#if $isUserLoggedIn}}
<DynamicPageBanner title="{{cachedProfileName || profileName || ''}}" />
<LazyTimeline timeline='account/{{params.accountId}}' />
{{else}}
<HiddenFromSSR>
<FreeTextLayout>
<h1>Profile</h1>
<p>A user timeline will appear here when logged in.</p>
</FreeTextLayout>
</HiddenFromSSR>
{{/if}}
</Layout>
<script>
import Layout from '../_components/Layout.html'
import LazyTimeline from '../_components/LazyTimeline.html'
import FreeTextLayout from '../_components/FreeTextLayout.html'
import { store } from '../_utils/store.js'
import HiddenFromSSR from '../_components/HiddenFromSSR'
import DynamicPageBanner from '../_components/DynamicPageBanner.html'
import { getAccount } from '../_utils/mastodon/user'
export default {
async oncreate() {
let currentInstance = this.store.get('currentInstance')
let accessToken = this.store.get('accessToken')
let accountId = this.get('params').accountId
let account = await getAccount(currentInstance, accessToken, accountId)
this.set({account: account})
},
store: () => store,
computed: {
profileName: (account) => {
return account && ('@' + account.acct)
},
shortProfileName: (account) => {
return account && ('@' + account.username)
},
cachedProfileName: ($cachedAccountNames, params) => {
return $cachedAccountNames && $cachedAccountNames[params.accountId] && ('@' + $cachedAccountNames[params.accountId].acct)
},
cachedShortProfileName: ($cachedAccountNames, params) => {
return $cachedAccountNames && $cachedAccountNames[params.accountId] && ('@' + $cachedAccountNames[params.accountId].username)
}
},
components: {
Layout,
LazyTimeline,
FreeTextLayout,
HiddenFromSSR,
DynamicPageBanner
}
}
</script>

View File

@ -8,10 +8,7 @@
dynamicLabel="{{'#' + params.tagName}}"
dynamicIcon="#fa-hashtag" >
{{#if $isUserLoggedIn}}
<div class="dynamic-page-banner">
<h1 class="dynamic-page-title">{{'#' + params.tagName}}</h1>
<button type="button" class="dynamic-page-go-back" on:click="onGoBack(event)">Back</button>
</div>
<DynamicPageBanner title="{{'#' + params.tagName}}"/>
<LazyTimeline timeline='tag/{{params.tagName}}' />
{{else}}
<HiddenFromSSR>
@ -23,37 +20,13 @@
</HiddenFromSSR>
{{/if}}
</Layout>
<style>
.dynamic-page-banner {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 20px 20px;
}
h1.dynamic-page-title {
margin: 0;
}
button.dynamic-page-go-back {
font-size: 1.3em;
color: var(--anchor-text);
border: 0;
padding: 0;
background: none;
}
button.dynamic-page-go-back:hover {
text-decoration: underline;
}
button.dynamic-page-go-back::before {
content: '<';
margin-right: 5px;
}
</style>
<script>
import Layout from '../_components/Layout.html'
import LazyTimeline from '../_components/LazyTimeline.html'
import FreeTextLayout from '../_components/FreeTextLayout.html'
import { store } from '../_utils/store.js'
import HiddenFromSSR from '../_components/HiddenFromSSR'
import DynamicPageBanner from '../_components/DynamicPageBanner.html'
export default {
store: () => store,
@ -61,13 +34,8 @@
Layout,
LazyTimeline,
FreeTextLayout,
HiddenFromSSR
},
methods: {
onGoBack(e) {
e.preventDefault()
window.history.back();
}
HiddenFromSSR,
DynamicPageBanner
}
}
</script>