feat: Implement bookmarks, close #1726

This commit is contained in:
charlag 2020-07-25 20:17:08 +02:00 committed by Nolan Lawson
parent 4e8a60ddef
commit 2113ab3d46
12 changed files with 129 additions and 6 deletions

View File

@ -55,5 +55,6 @@ module.exports = [
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' }, { id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' },
{ id: 'fa-crosshairs', src: 'src/thirdparty/font-awesome-svg-png/white/svg/crosshairs.svg' }, { id: 'fa-crosshairs', src: 'src/thirdparty/font-awesome-svg-png/white/svg/crosshairs.svg' },
{ id: 'fa-magic', src: 'src/thirdparty/font-awesome-svg-png/white/svg/magic.svg' }, { id: 'fa-magic', src: 'src/thirdparty/font-awesome-svg-png/white/svg/magic.svg' },
{ id: 'fa-hashtag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hashtag.svg' } { id: 'fa-hashtag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hashtag.svg' },
{ id: 'fa-bookmark', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bookmark.svg' },
] ]

View File

@ -0,0 +1,25 @@
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { bookmarkStatus, unbookmarkStatus } from '../_api/bookmark'
import { database } from '../_database/database'
export async function setStatusBookmarkedOrUnbookmarked (statusId, bookmarked) {
const { currentInstance, accessToken } = store.get()
try {
if (bookmarked) {
await bookmarkStatus(currentInstance, accessToken, statusId)
} else {
await unbookmarkStatus(currentInstance, accessToken, statusId)
}
if (bookmarked) {
toast.say('Bookmarked status')
} else {
toast.say('Unbookmarked status')
}
store.setStatusBookmarked(currentInstance, statusId, bookmarked)
await database.setStatusBookmarked(currentInstance, statusId, bookmarked)
} catch (e) {
console.error(e)
toast.say(`Unable to ${bookmarked ? 'bookmark' : 'unbookmark'} status: ` + (e.message || ''))
}
}

View File

@ -183,7 +183,7 @@ async function fetchTimelineItemsAndPossiblyFallBack () {
online online
} = store.get() } = store.get()
if (currentTimeline === 'favorites') { if (currentTimeline === 'favorites' || currentTimeline === 'bookmarks') {
// Always fetch favorites from the network, we currently don't have a good way of storing // Always fetch favorites from the network, we currently don't have a good way of storing
// these in IndexedDB because of "internal ID" system Mastodon uses to paginate these // these in IndexedDB because of "internal ID" system Mastodon uses to paginate these
await fetchPagedItems(currentInstance, accessToken, currentTimeline) await fetchPagedItems(currentInstance, accessToken, currentTimeline)

View File

@ -0,0 +1,12 @@
import { post, WRITE_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'
export async function bookmarkStatus (instanceName, accessToken, statusId) {
const url = `${basename(instanceName)}/api/v1/statuses/${statusId}/bookmark`
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}
export async function unbookmarkStatus (instanceName, accessToken, statusId) {
const url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unbookmark`
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

View File

@ -15,6 +15,8 @@ function getTimelineUrlPath (timeline) {
return 'favourites' return 'favourites'
case 'direct': case 'direct':
return 'conversations' return 'conversations'
case 'bookmarks':
return 'bookmarks'
} }
if (timeline.startsWith('tag/')) { if (timeline.startsWith('tag/')) {
return 'timelines/tag' return 'timelines/tag'
@ -23,6 +25,7 @@ function getTimelineUrlPath (timeline) {
} else if (timeline.startsWith('list/')) { } else if (timeline.startsWith('list/')) {
return 'timelines/list' return 'timelines/list'
} }
throw new Error(`Invalid timeline type: ${timeline}`)
} }
export async function getTimeline (instanceName, accessToken, timeline, maxId, since, limit) { export async function getTimeline (instanceName, accessToken, timeline, maxId, since, limit) {

View File

@ -18,6 +18,7 @@ import { close } from '../helpers/closeDialog'
import { oncreate } from '../helpers/onCreateDialog' import { oncreate } from '../helpers/onCreateDialog'
import { setAccountBlocked } from '../../../_actions/block' import { setAccountBlocked } from '../../../_actions/block'
import { setStatusPinnedOrUnpinned } from '../../../_actions/pin' import { setStatusPinnedOrUnpinned } from '../../../_actions/pin'
import { setStatusBookmarkedOrUnbookmarked } from '../../../_actions/bookmark'
import { setConversationMuted } from '../../../_actions/muteConversation' import { setConversationMuted } from '../../../_actions/muteConversation'
import { copyText } from '../../../_actions/copyText' import { copyText } from '../../../_actions/copyText'
import { deleteAndRedraft } from '../../../_actions/deleteAndRedraft' import { deleteAndRedraft } from '../../../_actions/deleteAndRedraft'
@ -82,10 +83,11 @@ export default {
muteConversationLabel: ({ mutingConversation }) => mutingConversation ? 'Unmute conversation' : 'Mute conversation', muteConversationLabel: ({ mutingConversation }) => mutingConversation ? 'Unmute conversation' : 'Mute conversation',
muteConversationIcon: ({ mutingConversation }) => mutingConversation ? '#fa-volume-up' : '#fa-volume-off', muteConversationIcon: ({ mutingConversation }) => mutingConversation ? '#fa-volume-up' : '#fa-volume-off',
isPublicOrUnlisted: ({ visibility }) => visibility === 'public' || visibility === 'unlisted', isPublicOrUnlisted: ({ visibility }) => visibility === 'public' || visibility === 'unlisted',
bookmarkLabel: ({ status }) => status.bookmarked ? 'Unbookmark' : 'Bookmark',
items: ({ items: ({
blockLabel, blocking, blockIcon, muteLabel, muteIcon, followLabel, followIcon, blockLabel, blocking, blockIcon, muteLabel, muteIcon, followLabel, followIcon,
following, followRequested, pinLabel, isUser, visibility, mentionsUser, mutingConversation, following, followRequested, pinLabel, isUser, visibility, mentionsUser, mutingConversation,
muteConversationLabel, muteConversationIcon, supportsWebShare, isPublicOrUnlisted muteConversationLabel, muteConversationIcon, supportsWebShare, isPublicOrUnlisted, bookmarkLabel
}) => ([ }) => ([
isUser && { isUser && {
key: 'delete', key: 'delete',
@ -136,6 +138,11 @@ export default {
key: 'copy', key: 'copy',
label: 'Copy link to toot', label: 'Copy link to toot',
icon: '#fa-link' icon: '#fa-link'
},
{
key: 'bookmark',
label: bookmarkLabel,
icon: '#fa-bookmark'
} }
].filter(Boolean)) ].filter(Boolean))
}, },
@ -169,6 +176,8 @@ export default {
return this.onShare() return this.onShare()
case 'report': case 'report':
return this.onReport() return this.onReport()
case 'bookmark':
return this.onBookmark()
} }
}, },
async onDeleteClicked () { async onDeleteClicked () {
@ -221,6 +230,11 @@ export default {
const { status, account } = this.get() const { status, account } = this.get()
this.close() this.close()
await reportStatusOrAccount(({ status, account })) await reportStatusOrAccount(({ status, account }))
},
async onBookmark () {
const { status } = this.get()
this.close()
await setStatusBookmarkedOrUnbookmarked(status.id, !status.bookmarked)
} }
} }
} }

View File

@ -51,3 +51,9 @@ export async function setStatusMuted (instanceName, statusId, muted) {
status.muted = muted status.muted = muted
}) })
} }
export async function setStatusBookmarked (instanceName, statusId, bookmarked) {
return updateStatus(instanceName, statusId, status => {
status.bookmarked = bookmarked
})
}

View File

@ -0,0 +1,32 @@
{#if $isUserLoggedIn}
<TimelinePage timeline="bookmarks">
{#if $pinnedPage !== '/bookmarks'}
<DynamicPageBanner title="Bookmarks" icon="#fa-bookmark"/>
{/if}
</TimelinePage>
{:else}
<HiddenFromSSR>
<FreeTextLayout>
<h1>Bookmarks</h1>
<p>Your bookmarks will appear here when logged in.</p>
</FreeTextLayout>
</HiddenFromSSR>
{/if}
<script>
import TimelinePage from '../_components/TimelinePage.html'
import FreeTextLayout from '../_components/FreeTextLayout.html'
import { store } from '../_store/store.js'
import HiddenFromSSR from '../_components/HiddenFromSSR'
import DynamicPageBanner from '../_components/DynamicPageBanner.html'
export default {
store: () => store,
components: {
TimelinePage,
FreeTextLayout,
HiddenFromSSR,
DynamicPageBanner
}
}
</script>

View File

@ -36,6 +36,12 @@
pinnable="true" pinnable="true"
pinIndex={3} pinIndex={3}
/> />
<PageListItem href="/bookmarks"
label="Bookmarks"
icon="#fa-bookmark"
pinnable="true"
pinIndex={4}
/>
</PageList> </PageList>
{#if listsLength} {#if listsLength}

View File

@ -3,7 +3,8 @@ function getStatusModifications (store, instanceName) {
statusModifications[instanceName] = statusModifications[instanceName] || { statusModifications[instanceName] = statusModifications[instanceName] || {
favorites: {}, favorites: {},
reblogs: {}, reblogs: {},
pins: {} pins: {},
bookmarks: {}
} }
return statusModifications return statusModifications
} }
@ -26,4 +27,8 @@ export function statusMixins (Store) {
Store.prototype.setStatusPinned = function (instanceName, statusId, pinned) { Store.prototype.setStatusPinned = function (instanceName, statusId, pinned) {
setStatusModification(this, instanceName, statusId, 'pins', pinned) setStatusModification(this, instanceName, statusId, 'pins', pinned)
} }
Store.prototype.setStatusBookmarked = function (instanceName, statusId, bookmarked) {
setStatusModification(this, instanceName, statusId, 'bookmarks', bookmarked)
}
} }

20
src/routes/bookmarks.html Normal file
View File

@ -0,0 +1,20 @@
<Title name="Bookmarks" />
<LazyPage {pageComponent} {params} />
<script>
import Title from './_components/Title.html'
import LazyPage from './_components/LazyPage.html'
import pageComponent from './_pages/bookmarks.html'
export default {
components: {
Title,
LazyPage
},
data: () => ({
pageComponent
})
}
</script>

View File

@ -1,2 +1 @@
<?xml version="1.0" encoding="utf-8"?> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1792 1792"><path d="M1420 128q23 0 44 9 33 13 52.5 41t19.5 62v1289q0 34-19.5 62t-52.5 41q-19 8-44 8-48 0-83-32l-441-424-441 424q-36 33-83 33-23 0-44-9-33-13-52.5-41t-19.5-62v-1289q0-34 19.5-62t52.5-41q21-9 44-9h1048z" fill="#fff"/></svg>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1420 128q23 0 44 9 33 13 52.5 41t19.5 62v1289q0 34-19.5 62t-52.5 41q-19 8-44 8-48 0-83-32l-441-424-441 424q-36 33-83 33-23 0-44-9-33-13-52.5-41t-19.5-62v-1289q0-34 19.5-62t52.5-41q21-9 44-9h1048z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 356 B

After

Width:  |  Height:  |  Size: 291 B