implement pinned statuses

This commit is contained in:
Nolan Lawson 2018-02-11 10:35:25 -08:00
parent 5adc975bef
commit 3213714f4b
11 changed files with 170 additions and 45 deletions

View File

@ -38,7 +38,9 @@ export async function logOutOfInstance (instanceName) {
loggedInInstances: loggedInInstances, loggedInInstances: loggedInInstances,
instanceThemes: instanceThemes, instanceThemes: instanceThemes,
loggedInInstancesInOrder: loggedInInstancesInOrder, loggedInInstancesInOrder: loggedInInstancesInOrder,
currentInstance: newInstance currentInstance: newInstance,
searchResults: null,
queryInSearch: ''
}) })
store.save() store.save()
toast.say(`Logged out of ${instanceName}`) toast.say(`Logged out of ${instanceName}`)

View File

@ -0,0 +1,23 @@
import { store } from '../_store/store'
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { getPinnedStatuses } from '../_api/pinnedStatuses'
import { database } from '../_database/database'
export async function updatePinnedStatusesForAccount(accountId) {
let instanceName = store.get('currentInstance')
let accessToken = store.get('accessToken')
await cacheFirstUpdateAfter(
() => getPinnedStatuses(instanceName, accessToken, accountId),
() => database.getPinnedStatuses(instanceName, accountId),
statuses => database.insertPinnedStatuses(instanceName, accountId, statuses),
statuses => {
let $pinnedStatuses = store.get('pinnedStatuses')
$pinnedStatuses[instanceName] = $pinnedStatuses[instanceName] || {}
$pinnedStatuses[instanceName][accountId] = statuses
store.set({pinnedStatuses: $pinnedStatuses})
}
)
}

View File

@ -77,10 +77,13 @@
this.set({loading: true}) this.set({loading: true})
try { try {
let results = await search(instanceName, accessToken, queryInSearch) let results = await search(instanceName, accessToken, queryInSearch)
this.store.set({ let currentQueryInSearch = this.store.get('queryInSearch') // avoid race conditions
searchResultsForQuery: queryInSearch, if (currentQueryInSearch === queryInSearch) {
searchResults: results this.store.set({
}) searchResultsForQuery: queryInSearch,
searchResults: results
})
}
} catch (e) { } catch (e) {
toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || '')) toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
console.error(e) console.error(e)

View File

@ -7,8 +7,8 @@
aria-setsize="{{length}}" aria-setsize="{{length}}"
aria-label="Status by {{originalStatus.account.display_name || originalStatus.account.username}}" aria-label="Status by {{originalStatus.account.display_name || originalStatus.account.username}}"
on:recalculateHeight> on:recalculateHeight>
{{#if (notification && (notification.type === 'reblog' || notification.type === 'favourite')) || status.reblog}} {{#if (notification && (notification.type === 'reblog' || notification.type === 'favourite')) || status.reblog || timelineType === 'pinned'}}
<StatusHeader :notification :status :isStatusInNotification /> <StatusHeader :notification :status :isStatusInNotification :timelineType />
{{/if}} {{/if}}
<StatusAuthorName status="{{originalStatus}}" :isStatusInOwnThread :isStatusInNotification /> <StatusAuthorName status="{{originalStatus}}" :isStatusInOwnThread :isStatusInNotification />
<StatusAuthorHandle status="{{originalStatus}}" :isStatusInNotification /> <StatusAuthorHandle status="{{originalStatus}}" :isStatusInNotification />

View File

@ -1,21 +1,24 @@
<div class="status-header {{isStatusInNotification ? 'status-in-notification' : ''}}"> <div class="status-header {{isStatusInNotification ? 'status-in-notification' : ''}}">
<svg> <svg>
<use xlink:href="{{getIcon(notification, status)}}"/> <use xlink:href="{{icon}}"/>
</svg> </svg>
<span> <span>
<a href="/accounts/{{getAccount(notification, status).id}}" {{#if timelineType === 'pinned'}}
focus-key="{{focusKey}}" Pinned toot
> {{else}}
{{getAccount(notification, status).display_name || ('@' + getAccount(notification, status).username)}} <a href="/accounts/{{account.id}}"
</a> focus-key="{{focusKey}}" >
{{#if notification && notification.type === 'reblog'}} {{account.display_name || ('@' + account.username)}}
boosted your status </a>
{{elseif notification && notification.type === 'favourite'}} {{#if notification && notification.type === 'reblog'}}
favorited your status boosted your status
{{elseif notification && notification.type === 'follow'}} {{elseif notification && notification.type === 'favourite'}}
followed you favorited your status
{{elseif status && status.reblog}} {{elseif notification && notification.type === 'follow'}}
boosted followed you
{{elseif status && status.reblog}}
boosted
{{/if}}
{{/if}} {{/if}}
</span> </span>
</div> </div>
@ -66,18 +69,18 @@
export default { export default {
computed: { computed: {
statusId: (status) => status.id, statusId: (status) => status.id,
focusKey: (statusId) => `status-header-${statusId}` focusKey: (statusId) => `status-header-${statusId}`,
}, icon: (notification, status, timelineType) => {
helpers: { if (timelineType === 'pinned') {
getIcon(notification, status) { return '#fa-thumb-tack'
if ((notification && notification.type === 'reblog') || (status && status.reblog)) { } else if ((notification && notification.type === 'reblog') || (status && status.reblog)) {
return '#fa-retweet' return '#fa-retweet'
} else if (notification && notification.type === 'follow') { } else if (notification && notification.type === 'follow') {
return '#fa-user-plus' return '#fa-user-plus'
} }
return '#fa-star' return '#fa-star'
}, },
getAccount(notification, status) { account: (notification, status) => {
if (notification && notification.account) { if (notification && notification.account) {
return notification.account return notification.account
} }

View File

@ -0,0 +1,35 @@
<div role="feed" aria-label="Pinned toots" classes="pinned-statuses">
{{#if pinnedStatuses}}
{{#each pinnedStatuses as status, index}}
<Status :status
timelineType="pinned"
timelineValue="{{accountId}}"
:index
length="{{pinnedStatuses.length}}"
/>
{{/each}}
{{/if}}
</div>
<script>
import { store } from '../../_store/store'
import Status from '../status/Status.html'
import LoadingPage from '../../_components/LoadingPage.html'
import { updatePinnedStatusesForAccount } from '../../_actions/pinnedStatuses'
export default {
async oncreate() {
let accountId = this.get('accountId')
await updatePinnedStatusesForAccount(accountId)
},
computed: {
pinnedStatuses: ($pinnedStatuses, $currentInstance, accountId) => {
return $pinnedStatuses[$currentInstance] && $pinnedStatuses[$currentInstance][accountId]
}
},
store: () => store,
components: {
Status,
LoadingPage
}
}
</script>

View File

@ -5,3 +5,4 @@ export const ACCOUNTS_STORE = 'accounts'
export const RELATIONSHIPS_STORE = 'relationships' export const RELATIONSHIPS_STORE = 'relationships'
export const NOTIFICATIONS_STORE = 'notifications' export const NOTIFICATIONS_STORE = 'notifications'
export const NOTIFICATION_TIMELINES_STORE = 'notification_timelines' export const NOTIFICATION_TIMELINES_STORE = 'notification_timelines'
export const PINNED_STATUSES_STORE = 'pinned_statuses'

View File

@ -5,13 +5,14 @@ import {
ACCOUNTS_STORE, ACCOUNTS_STORE,
RELATIONSHIPS_STORE, RELATIONSHIPS_STORE,
NOTIFICATIONS_STORE, NOTIFICATIONS_STORE,
NOTIFICATION_TIMELINES_STORE NOTIFICATION_TIMELINES_STORE,
PINNED_STATUSES_STORE
} from './constants' } from './constants'
const openReqs = {} const openReqs = {}
const databaseCache = {} const databaseCache = {}
const DB_VERSION = 1 const DB_VERSION = 2
export function getDatabase (instanceName) { export function getDatabase (instanceName) {
if (!instanceName) { if (!instanceName) {
@ -30,15 +31,20 @@ export function getDatabase (instanceName) {
} }
req.onupgradeneeded = (e) => { req.onupgradeneeded = (e) => {
let db = req.result let db = req.result
db.createObjectStore(META_STORE, {keyPath: 'key'}) if (e.oldVersion < 1) {
db.createObjectStore(STATUSES_STORE, {keyPath: 'id'}) db.createObjectStore(META_STORE, {keyPath: 'key'})
db.createObjectStore(ACCOUNTS_STORE, {keyPath: 'id'}) db.createObjectStore(STATUSES_STORE, {keyPath: 'id'})
db.createObjectStore(RELATIONSHIPS_STORE, {keyPath: 'id'}) db.createObjectStore(ACCOUNTS_STORE, {keyPath: 'id'})
db.createObjectStore(NOTIFICATIONS_STORE, {keyPath: 'id'}) db.createObjectStore(RELATIONSHIPS_STORE, {keyPath: 'id'})
db.createObjectStore(STATUS_TIMELINES_STORE, {keyPath: 'id'}) db.createObjectStore(NOTIFICATIONS_STORE, {keyPath: 'id'})
db.createObjectStore(STATUS_TIMELINES_STORE, {keyPath: 'id'})
.createIndex('statusId', 'statusId') .createIndex('statusId', 'statusId')
db.createObjectStore(NOTIFICATION_TIMELINES_STORE, {keyPath: 'id'}) db.createObjectStore(NOTIFICATION_TIMELINES_STORE, {keyPath: 'id'})
.createIndex('notificationId', 'notificationId') .createIndex('notificationId', 'notificationId')
}
if (e.oldVersion < 2) {
db.createObjectStore(PINNED_STATUSES_STORE, {keyPath: 'id'})
}
} }
req.onsuccess = () => resolve(req.result) req.onsuccess = () => resolve(req.result)
}) })

View File

@ -1,10 +1,10 @@
import { toReversePaddedBigInt } from './utils' import { toPaddedBigInt, toReversePaddedBigInt } from './utils'
import { dbPromise, getDatabase } from './databaseLifecycle' import { dbPromise, getDatabase } from './databaseLifecycle'
import { accountsCache, getInCache, hasInCache, notificationsCache, setInCache, statusesCache } from './cache' import { accountsCache, getInCache, hasInCache, notificationsCache, setInCache, statusesCache } from './cache'
import { import {
ACCOUNTS_STORE, ACCOUNTS_STORE,
NOTIFICATION_TIMELINES_STORE, NOTIFICATION_TIMELINES_STORE,
NOTIFICATIONS_STORE, NOTIFICATIONS_STORE, PINNED_STATUSES_STORE,
STATUS_TIMELINES_STORE, STATUS_TIMELINES_STORE,
STATUSES_STORE STATUSES_STORE
} from './constants' } from './constants'
@ -57,6 +57,14 @@ function cloneForStorage (obj) {
return res return res
} }
function cacheStatus(status, instanceName) {
setInCache(statusesCache, instanceName, status.id, status)
setInCache(accountsCache, instanceName, status.account.id, status.account)
if (status.reblog) {
setInCache(accountsCache, instanceName, status.reblog.account.id, status.reblog.account)
}
}
// //
// pagination // pagination
// //
@ -214,11 +222,7 @@ async function insertTimelineNotifications (instanceName, timeline, notification
async function insertTimelineStatuses (instanceName, timeline, statuses) { async function insertTimelineStatuses (instanceName, timeline, statuses) {
for (let status of statuses) { for (let status of statuses) {
setInCache(statusesCache, instanceName, status.id, status) cacheStatus(status, instanceName)
setInCache(accountsCache, instanceName, status.account.id, status.account)
if (status.reblog) {
setInCache(accountsCache, instanceName, status.reblog.account.id, status.reblog.account)
}
} }
const db = await getDatabase(instanceName) const db = await getDatabase(instanceName)
let storeNames = [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE] let storeNames = [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
@ -271,3 +275,47 @@ export async function getNotification (instanceName, id) {
setInCache(notificationsCache, instanceName, id, result) setInCache(notificationsCache, instanceName, id, result)
return result return result
} }
//
// pinned statuses
//
export async function insertPinnedStatuses (instanceName, accountId, statuses) {
for (let status of statuses) {
cacheStatus(status, instanceName)
}
const db = await getDatabase(instanceName)
let storeNames = [PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
await dbPromise(db, storeNames, 'readwrite', (stores) => {
let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
statuses.forEach((status, i) => {
storeStatus(statusesStore, accountsStore, status)
pinnedStatusesStore.put({
id: accountId + '\u0000' + toPaddedBigInt(i),
statusId: status.id
})
})
})
}
export async function getPinnedStatuses (instanceName, accountId) {
let storeNames = [PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
const db = await getDatabase(instanceName)
return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
let keyRange = IDBKeyRange.bound(
accountId + '\u0000',
accountId + '\u0000\uffff'
)
pinnedStatusesStore.getAll(keyRange).onsuccess = e => {
let pinnedResults = e.target.result
let res = new Array(pinnedResults.length)
pinnedResults.forEach((pinnedResult, i) => {
fetchStatus(statusesStore, accountsStore, pinnedResult.statusId, status => {
res[i] = status
})
})
callback(res)
}
})
}

View File

@ -34,7 +34,8 @@ const store = new PinaforeStore({
autoplayGifs: false, autoplayGifs: false,
markMediaAsSensitive: false, markMediaAsSensitive: false,
pinnedPages: {}, pinnedPages: {},
instanceLists: {} instanceLists: {},
pinnedStatuses: {}
}) })
mixins(PinaforeStore) mixins(PinaforeStore)

View File

@ -13,6 +13,7 @@
verifyCredentials="{{$currentVerifyCredentials}}" verifyCredentials="{{$currentVerifyCredentials}}"
/> />
{{/if}} {{/if}}
<PinnedStatuses accountId="{{params.accountId}}" />
<LazyTimeline timeline='account/{{params.accountId}}' /> <LazyTimeline timeline='account/{{params.accountId}}' />
{{else}} {{else}}
<HiddenFromSSR> <HiddenFromSSR>
@ -34,6 +35,7 @@
import { updateProfileAndRelationship } from '../_actions/accounts' import { updateProfileAndRelationship } from '../_actions/accounts'
import AccountProfile from '../_components/AccountProfile.html' import AccountProfile from '../_components/AccountProfile.html'
import { updateVerifyCredentialsForInstance } from '../_actions/instances' import { updateVerifyCredentialsForInstance } from '../_actions/instances'
import PinnedStatuses from '../_components/timeline/PinnedStatuses.html'
export default { export default {
oncreate() { oncreate() {
@ -57,7 +59,8 @@
FreeTextLayout, FreeTextLayout,
HiddenFromSSR, HiddenFromSSR,
DynamicPageBanner, DynamicPageBanner,
AccountProfile AccountProfile,
PinnedStatuses
} }
} }
</script> </script>