first stab at account pages
This commit is contained in:
parent
5d9eba58be
commit
ab291a2c7e
44
routes/_components/DynamicPageBanner.html
Normal file
44
routes/_components/DynamicPageBanner.html
Normal 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>
|
|
@ -50,6 +50,9 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--nav-text-color);
|
color: var(--nav-text-color);
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
|
|
@ -32,8 +32,8 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if !status.spoiler_text || spoilerShown}}
|
{{#if !status.spoiler_text || spoilerShown}}
|
||||||
<div class="status-content">
|
<div class="status-content" ref:contentNode>
|
||||||
{{{hydratedContent}}}
|
{{{emojifiedContent}}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if originalMediaAttachments && originalMediaAttachments.length}}
|
{{#if originalMediaAttachments && originalMediaAttachments.length}}
|
||||||
|
@ -295,6 +295,9 @@
|
||||||
const relativeFormat = new IntlRelativeFormat('en-US');
|
const relativeFormat = new IntlRelativeFormat('en-US');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
oncreate() {
|
||||||
|
this.hashtagifyContent()
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar,
|
||||||
Media,
|
Media,
|
||||||
|
@ -311,15 +314,9 @@
|
||||||
originalStatus: (status) => status.reblog ? status.reblog : status,
|
originalStatus: (status) => status.reblog ? status.reblog : status,
|
||||||
originalAccount: (originalStatus) => originalStatus.account,
|
originalAccount: (originalStatus) => originalStatus.account,
|
||||||
originalMediaAttachments: (originalStatus) => originalStatus.media_attachments,
|
originalMediaAttachments: (originalStatus) => originalStatus.media_attachments,
|
||||||
hydratedContent: (originalStatus) => {
|
emojifiedContent: (originalStatus) => {
|
||||||
let status = originalStatus
|
let status = originalStatus
|
||||||
let content = status.content
|
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) {
|
if (status.emojis && status.emojis.length) {
|
||||||
for (let emoji of status.emojis) {
|
for (let emoji of status.emojis) {
|
||||||
let { shortcode, url } = emoji
|
let { shortcode, url } = emoji
|
||||||
|
@ -337,11 +334,34 @@
|
||||||
methods: {
|
methods: {
|
||||||
onClickSpoilerButton() {
|
onClickSpoilerButton() {
|
||||||
this.set({spoilerShown: !this.get('spoilerShown')})
|
this.set({spoilerShown: !this.get('spoilerShown')})
|
||||||
|
this.hashtagifyContent()
|
||||||
this.fire('recalculateHeight')
|
this.fire('recalculateHeight')
|
||||||
},
|
},
|
||||||
onClickSensitiveMediaButton() {
|
onClickSensitiveMediaButton() {
|
||||||
this.set({sensitiveShown: !this.get('sensitiveShown')})
|
this.set({sensitiveShown: !this.get('sensitiveShown')})
|
||||||
this.fire('recalculateHeight')
|
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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,22 @@
|
||||||
this.fire('initialized')
|
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: () => ({
|
data: () => ({
|
||||||
StatusListItem: StatusListItem,
|
StatusListItem: StatusListItem,
|
||||||
|
@ -58,6 +74,9 @@
|
||||||
} else if (timeline.startsWith('tag/')) {
|
} else if (timeline.startsWith('tag/')) {
|
||||||
let tag = timeline.split('/').slice(-1)[0]
|
let tag = timeline.split('/').slice(-1)[0]
|
||||||
return `#${tag} timeline for ${$currentInstance}`
|
return `#${tag} timeline for ${$currentInstance}`
|
||||||
|
} else if (timeline.startsWith('account/')) {
|
||||||
|
let account = timeline.split('/').slice(-1)[0]
|
||||||
|
return `Account #${account} on ${$currentInstance}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
/*
|
||||||
import worker from 'workerize-loader!./databaseCore'
|
import worker from 'workerize-loader!./databaseCore'
|
||||||
const database = process.browser && worker()
|
export const database = process.browser && worker()
|
||||||
|
*/
|
||||||
|
|
||||||
export {
|
import * as dbCore from './databaseCore'
|
||||||
database
|
|
||||||
}
|
export { dbCore as database }
|
|
@ -1,24 +1,29 @@
|
||||||
import { get, paramsString } from '../ajax'
|
import { get, paramsString } from '../ajax'
|
||||||
import { basename } from './utils'
|
import { basename } from './utils'
|
||||||
|
|
||||||
function getTimelineUrlName(timeline) {
|
function getTimelineUrlPath(timeline) {
|
||||||
switch (timeline) {
|
switch (timeline) {
|
||||||
case 'local':
|
case 'local':
|
||||||
case 'federated':
|
case 'federated':
|
||||||
return 'public'
|
return 'timelines/public'
|
||||||
case 'home':
|
case 'home':
|
||||||
return 'home'
|
return 'timelines/home'
|
||||||
default:
|
}
|
||||||
return 'tag'
|
if (timeline.startsWith('tag/')) {
|
||||||
|
return 'timelines/tag'
|
||||||
|
} else if (timeline.startsWith('account/')) {
|
||||||
|
return 'accounts'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTimeline(instanceName, accessToken, timeline, maxId, since) {
|
export function getTimeline(instanceName, accessToken, timeline, maxId, since) {
|
||||||
let timelineUrlName = getTimelineUrlName(timeline)
|
let timelineUrlName = getTimelineUrlPath(timeline)
|
||||||
let url = `${basename(instanceName)}/api/v1/timelines/${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('/').slice(-1)[0]
|
||||||
|
} else if (timeline.startsWith('account/')) {
|
||||||
|
url += '/' + timeline.split('/').slice(-1)[0] +'/statuses'
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = {}
|
let params = {}
|
||||||
|
|
|
@ -6,4 +6,11 @@ export function getVerifyCredentials(instanceName, accessToken) {
|
||||||
return get(url, {
|
return get(url, {
|
||||||
'Authorization': `Bearer ${accessToken}`
|
'Authorization': `Bearer ${accessToken}`
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccount(instanceName, accessToken, accountId) {
|
||||||
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}`
|
||||||
|
return get(url, {
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
import { Store } from 'svelte/store.js'
|
import { Store } from 'svelte/store.js'
|
||||||
|
|
||||||
|
const DONT_STORE_THESE_KEYS = [
|
||||||
|
'cachedAccountNames'
|
||||||
|
]
|
||||||
|
|
||||||
const LS = process.browser && localStorage
|
const LS = process.browser && localStorage
|
||||||
class LocalStorageStore extends Store {
|
class LocalStorageStore extends Store {
|
||||||
|
|
||||||
|
@ -18,7 +22,8 @@ class LocalStorageStore extends Store {
|
||||||
this.set(newState)
|
this.set(newState)
|
||||||
this.onchange((state, changed) => {
|
this.onchange((state, changed) => {
|
||||||
Object.keys(changed).forEach(change => {
|
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
|
this.lastChanged[change] = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -74,6 +79,12 @@ store.compute(
|
||||||
}, loggedInInstances[currentInstance])
|
}, loggedInInstances[currentInstance])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
store.compute(
|
||||||
|
'accessToken',
|
||||||
|
['currentInstanceData'],
|
||||||
|
(currentInstanceData) => currentInstanceData.access_token
|
||||||
|
)
|
||||||
|
|
||||||
store.compute(
|
store.compute(
|
||||||
'currentTheme',
|
'currentTheme',
|
||||||
['currentInstance', 'instanceThemes'],
|
['currentInstance', 'instanceThemes'],
|
||||||
|
|
63
routes/accounts/[accountId].html
Normal file
63
routes/accounts/[accountId].html
Normal 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>
|
|
@ -8,10 +8,7 @@
|
||||||
dynamicLabel="{{'#' + params.tagName}}"
|
dynamicLabel="{{'#' + params.tagName}}"
|
||||||
dynamicIcon="#fa-hashtag" >
|
dynamicIcon="#fa-hashtag" >
|
||||||
{{#if $isUserLoggedIn}}
|
{{#if $isUserLoggedIn}}
|
||||||
<div class="dynamic-page-banner">
|
<DynamicPageBanner title="{{'#' + params.tagName}}"/>
|
||||||
<h1 class="dynamic-page-title">{{'#' + params.tagName}}</h1>
|
|
||||||
<button type="button" class="dynamic-page-go-back" on:click="onGoBack(event)">Back</button>
|
|
||||||
</div>
|
|
||||||
<LazyTimeline timeline='tag/{{params.tagName}}' />
|
<LazyTimeline timeline='tag/{{params.tagName}}' />
|
||||||
{{else}}
|
{{else}}
|
||||||
<HiddenFromSSR>
|
<HiddenFromSSR>
|
||||||
|
@ -23,37 +20,13 @@
|
||||||
</HiddenFromSSR>
|
</HiddenFromSSR>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</Layout>
|
</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>
|
<script>
|
||||||
import Layout from '../_components/Layout.html'
|
import Layout from '../_components/Layout.html'
|
||||||
import LazyTimeline from '../_components/LazyTimeline.html'
|
import LazyTimeline from '../_components/LazyTimeline.html'
|
||||||
import FreeTextLayout from '../_components/FreeTextLayout.html'
|
import FreeTextLayout from '../_components/FreeTextLayout.html'
|
||||||
import { store } from '../_utils/store.js'
|
import { store } from '../_utils/store.js'
|
||||||
import HiddenFromSSR from '../_components/HiddenFromSSR'
|
import HiddenFromSSR from '../_components/HiddenFromSSR'
|
||||||
|
import DynamicPageBanner from '../_components/DynamicPageBanner.html'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
store: () => store,
|
store: () => store,
|
||||||
|
@ -61,13 +34,8 @@
|
||||||
Layout,
|
Layout,
|
||||||
LazyTimeline,
|
LazyTimeline,
|
||||||
FreeTextLayout,
|
FreeTextLayout,
|
||||||
HiddenFromSSR
|
HiddenFromSSR,
|
||||||
},
|
DynamicPageBanner
|
||||||
methods: {
|
|
||||||
onGoBack(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
window.history.back();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
Loading…
Reference in a new issue