From 2113ab3d461f6809bf28f5cc6eae045c4faeb86b Mon Sep 17 00:00:00 2001 From: charlag Date: Sat, 25 Jul 2020 20:17:08 +0200 Subject: [PATCH] feat: Implement bookmarks, close #1726 --- bin/svgs.js | 3 +- src/routes/_actions/bookmark.js | 25 +++++++++++++++ src/routes/_actions/timeline.js | 2 +- src/routes/_api/bookmark.js | 12 +++++++ src/routes/_api/timelines.js | 3 ++ .../components/StatusOptionsDialog.html | 16 +++++++++- .../_database/timelines/updateStatus.js | 6 ++++ src/routes/_pages/bookmarks.html | 32 +++++++++++++++++++ src/routes/_pages/community/index.html | 6 ++++ src/routes/_store/mixins/statusMixins.js | 7 +++- src/routes/bookmarks.html | 20 ++++++++++++ .../white/svg/bookmark.svg | 3 +- 12 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 src/routes/_actions/bookmark.js create mode 100644 src/routes/_api/bookmark.js create mode 100644 src/routes/_pages/bookmarks.html create mode 100644 src/routes/bookmarks.html diff --git a/bin/svgs.js b/bin/svgs.js index 65c488cf..ee2bb94d 100644 --- a/bin/svgs.js +++ b/bin/svgs.js @@ -55,5 +55,6 @@ module.exports = [ { 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-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' }, ] diff --git a/src/routes/_actions/bookmark.js b/src/routes/_actions/bookmark.js new file mode 100644 index 00000000..06431865 --- /dev/null +++ b/src/routes/_actions/bookmark.js @@ -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 || '')) + } +} diff --git a/src/routes/_actions/timeline.js b/src/routes/_actions/timeline.js index 0be5ea9e..3a3909f9 100644 --- a/src/routes/_actions/timeline.js +++ b/src/routes/_actions/timeline.js @@ -183,7 +183,7 @@ async function fetchTimelineItemsAndPossiblyFallBack () { online } = 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 // these in IndexedDB because of "internal ID" system Mastodon uses to paginate these await fetchPagedItems(currentInstance, accessToken, currentTimeline) diff --git a/src/routes/_api/bookmark.js b/src/routes/_api/bookmark.js new file mode 100644 index 00000000..3fde63d2 --- /dev/null +++ b/src/routes/_api/bookmark.js @@ -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 }) +} diff --git a/src/routes/_api/timelines.js b/src/routes/_api/timelines.js index 55270a96..fe436dfa 100644 --- a/src/routes/_api/timelines.js +++ b/src/routes/_api/timelines.js @@ -15,6 +15,8 @@ function getTimelineUrlPath (timeline) { return 'favourites' case 'direct': return 'conversations' + case 'bookmarks': + return 'bookmarks' } if (timeline.startsWith('tag/')) { return 'timelines/tag' @@ -23,6 +25,7 @@ function getTimelineUrlPath (timeline) { } else if (timeline.startsWith('list/')) { return 'timelines/list' } + throw new Error(`Invalid timeline type: ${timeline}`) } export async function getTimeline (instanceName, accessToken, timeline, maxId, since, limit) { diff --git a/src/routes/_components/dialog/components/StatusOptionsDialog.html b/src/routes/_components/dialog/components/StatusOptionsDialog.html index 05509e9d..d8d75588 100644 --- a/src/routes/_components/dialog/components/StatusOptionsDialog.html +++ b/src/routes/_components/dialog/components/StatusOptionsDialog.html @@ -18,6 +18,7 @@ import { close } from '../helpers/closeDialog' import { oncreate } from '../helpers/onCreateDialog' import { setAccountBlocked } from '../../../_actions/block' import { setStatusPinnedOrUnpinned } from '../../../_actions/pin' +import { setStatusBookmarkedOrUnbookmarked } from '../../../_actions/bookmark' import { setConversationMuted } from '../../../_actions/muteConversation' import { copyText } from '../../../_actions/copyText' import { deleteAndRedraft } from '../../../_actions/deleteAndRedraft' @@ -82,10 +83,11 @@ export default { muteConversationLabel: ({ mutingConversation }) => mutingConversation ? 'Unmute conversation' : 'Mute conversation', muteConversationIcon: ({ mutingConversation }) => mutingConversation ? '#fa-volume-up' : '#fa-volume-off', isPublicOrUnlisted: ({ visibility }) => visibility === 'public' || visibility === 'unlisted', + bookmarkLabel: ({ status }) => status.bookmarked ? 'Unbookmark' : 'Bookmark', items: ({ blockLabel, blocking, blockIcon, muteLabel, muteIcon, followLabel, followIcon, following, followRequested, pinLabel, isUser, visibility, mentionsUser, mutingConversation, - muteConversationLabel, muteConversationIcon, supportsWebShare, isPublicOrUnlisted + muteConversationLabel, muteConversationIcon, supportsWebShare, isPublicOrUnlisted, bookmarkLabel }) => ([ isUser && { key: 'delete', @@ -136,6 +138,11 @@ export default { key: 'copy', label: 'Copy link to toot', icon: '#fa-link' + }, + { + key: 'bookmark', + label: bookmarkLabel, + icon: '#fa-bookmark' } ].filter(Boolean)) }, @@ -169,6 +176,8 @@ export default { return this.onShare() case 'report': return this.onReport() + case 'bookmark': + return this.onBookmark() } }, async onDeleteClicked () { @@ -221,6 +230,11 @@ export default { const { status, account } = this.get() this.close() await reportStatusOrAccount(({ status, account })) + }, + async onBookmark () { + const { status } = this.get() + this.close() + await setStatusBookmarkedOrUnbookmarked(status.id, !status.bookmarked) } } } diff --git a/src/routes/_database/timelines/updateStatus.js b/src/routes/_database/timelines/updateStatus.js index 8d4b63c3..3da25a5b 100644 --- a/src/routes/_database/timelines/updateStatus.js +++ b/src/routes/_database/timelines/updateStatus.js @@ -51,3 +51,9 @@ export async function setStatusMuted (instanceName, statusId, muted) { status.muted = muted }) } + +export async function setStatusBookmarked (instanceName, statusId, bookmarked) { + return updateStatus(instanceName, statusId, status => { + status.bookmarked = bookmarked + }) +} diff --git a/src/routes/_pages/bookmarks.html b/src/routes/_pages/bookmarks.html new file mode 100644 index 00000000..2c041eea --- /dev/null +++ b/src/routes/_pages/bookmarks.html @@ -0,0 +1,32 @@ +{#if $isUserLoggedIn} + + {#if $pinnedPage !== '/bookmarks'} + + {/if} + +{:else} + + +

Bookmarks

+ +

Your bookmarks will appear here when logged in.

+
+
+{/if} + diff --git a/src/routes/_pages/community/index.html b/src/routes/_pages/community/index.html index c06b26a3..e5e4b55e 100644 --- a/src/routes/_pages/community/index.html +++ b/src/routes/_pages/community/index.html @@ -36,6 +36,12 @@ pinnable="true" pinIndex={3} /> + {#if listsLength} diff --git a/src/routes/_store/mixins/statusMixins.js b/src/routes/_store/mixins/statusMixins.js index 8eafaebe..ca83e21c 100644 --- a/src/routes/_store/mixins/statusMixins.js +++ b/src/routes/_store/mixins/statusMixins.js @@ -3,7 +3,8 @@ function getStatusModifications (store, instanceName) { statusModifications[instanceName] = statusModifications[instanceName] || { favorites: {}, reblogs: {}, - pins: {} + pins: {}, + bookmarks: {} } return statusModifications } @@ -26,4 +27,8 @@ export function statusMixins (Store) { Store.prototype.setStatusPinned = function (instanceName, statusId, pinned) { setStatusModification(this, instanceName, statusId, 'pins', pinned) } + + Store.prototype.setStatusBookmarked = function (instanceName, statusId, bookmarked) { + setStatusModification(this, instanceName, statusId, 'bookmarks', bookmarked) + } } diff --git a/src/routes/bookmarks.html b/src/routes/bookmarks.html new file mode 100644 index 00000000..71710427 --- /dev/null +++ b/src/routes/bookmarks.html @@ -0,0 +1,20 @@ + + + <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> diff --git a/src/thirdparty/font-awesome-svg-png/white/svg/bookmark.svg b/src/thirdparty/font-awesome-svg-png/white/svg/bookmark.svg index 2a409d8e..33ff1026 100644 --- a/src/thirdparty/font-awesome-svg-png/white/svg/bookmark.svg +++ b/src/thirdparty/font-awesome-svg-png/white/svg/bookmark.svg @@ -1,2 +1 @@ -<?xml version="1.0" encoding="utf-8"?> -<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> \ No newline at end of file +<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>