feat: Implement bookmarks, close #1726
This commit is contained in:
parent
4e8a60ddef
commit
2113ab3d46
|
@ -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' },
|
||||||
]
|
]
|
||||||
|
|
25
src/routes/_actions/bookmark.js
Normal file
25
src/routes/_actions/bookmark.js
Normal 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 || ''))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
12
src/routes/_api/bookmark.js
Normal file
12
src/routes/_api/bookmark.js
Normal 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 })
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
32
src/routes/_pages/bookmarks.html
Normal file
32
src/routes/_pages/bookmarks.html
Normal 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>
|
|
@ -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}
|
||||||
|
|
|
@ -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
20
src/routes/bookmarks.html
Normal 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>
|
|
@ -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 |
Loading…
Reference in a new issue