From 9e45228823cd0fa7eb9388b0eb7780b9609edf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 23 Sep 2024 23:10:32 +0200 Subject: [PATCH 01/10] Bookmark folders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/boot/after_store.js | 1 + src/boot/routes.js | 5 +- .../bookmark_folder_card.js | 16 +++++ .../bookmark_folder_card.vue | 38 +++++++++++ .../bookmark_folders/bookmark_folders.js | 27 ++++++++ .../bookmark_folders/bookmark_folders.vue | 33 ++++++++++ .../bookmark_folders_menu_content.js | 16 +++++ .../bookmark_folders_menu_content.vue | 19 ++++++ .../bookmark_timeline/bookmark_timeline.js | 23 +++++-- .../bookmark_timeline/bookmark_timeline.vue | 1 + src/components/nav_panel/nav_panel.js | 15 ++++- src/components/nav_panel/nav_panel.vue | 33 ++++++++++ src/components/navigation/filter.js | 12 +++- src/components/navigation/navigation.js | 3 +- .../navigation/navigation_entry.vue | 33 +++++++++- src/components/timeline/timeline.js | 3 + src/components/timeline_menu/timeline_menu.js | 25 +++++-- .../timeline_menu/timeline_menu.vue | 4 ++ src/i18n/en.json | 5 ++ src/main.js | 5 +- src/modules/api.js | 17 ++++- src/modules/bookmark_folders.js | 66 +++++++++++++++++++ src/modules/instance.js | 1 + src/modules/users.js | 2 + src/services/api/api.service.js | 52 ++++++++++++++- .../backend_interactor_service.js | 9 ++- .../bookmark_folders_fetcher.service.js | 22 +++++++ .../timeline_fetcher.service.js | 9 ++- 28 files changed, 464 insertions(+), 31 deletions(-) create mode 100644 src/components/bookmark_folder_card/bookmark_folder_card.js create mode 100644 src/components/bookmark_folder_card/bookmark_folder_card.vue create mode 100644 src/components/bookmark_folders/bookmark_folders.js create mode 100644 src/components/bookmark_folders/bookmark_folders.vue create mode 100644 src/components/bookmark_folders_menu/bookmark_folders_menu_content.js create mode 100644 src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue create mode 100644 src/modules/bookmark_folders.js create mode 100644 src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 6cad05f6..5b610dd3 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -253,6 +253,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') }) + store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') }) diff --git a/src/boot/routes.js b/src/boot/routes.js index 31e3dbb0..ff93bb51 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -26,6 +26,7 @@ import ListsEdit from 'components/lists_edit/lists_edit.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue' import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' +import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { @@ -86,7 +87,9 @@ export default (store) => { { name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline }, { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }, { name: 'lists-new', path: '/lists/new', component: ListsEdit }, - { name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute } + { name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute }, + { name: 'bookmark-folders', path: '/bookmark_folders', component: BookmarkFolders }, + { name: 'bookmark-folder', path: '/bookmarks/:id', component: BookmarkTimeline } ] if (store.state.instance.pleromaChatMessagesAvailable) { diff --git a/src/components/bookmark_folder_card/bookmark_folder_card.js b/src/components/bookmark_folder_card/bookmark_folder_card.js new file mode 100644 index 00000000..9df28e19 --- /dev/null +++ b/src/components/bookmark_folder_card/bookmark_folder_card.js @@ -0,0 +1,16 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEllipsisH +) + +const BookmarkFolderCard = { + props: [ + 'folder' + ] +} + +export default BookmarkFolderCard diff --git a/src/components/bookmark_folder_card/bookmark_folder_card.vue b/src/components/bookmark_folder_card/bookmark_folder_card.vue new file mode 100644 index 00000000..10875e78 --- /dev/null +++ b/src/components/bookmark_folder_card/bookmark_folder_card.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/components/bookmark_folders/bookmark_folders.js b/src/components/bookmark_folders/bookmark_folders.js new file mode 100644 index 00000000..9f1f1fed --- /dev/null +++ b/src/components/bookmark_folders/bookmark_folders.js @@ -0,0 +1,27 @@ +import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue' + +const BookmarkFolders = { + data () { + return { + isNew: false + } + }, + components: { + BookmarkFolderCard + }, + computed: { + bookmarkFolders () { + return this.$store.state.bookmarkFolders.allFolders + } + }, + methods: { + cancelNewFolder () { + this.isNew = false + }, + newFolder () { + this.isNew = true + } + } +} + +export default BookmarkFolders diff --git a/src/components/bookmark_folders/bookmark_folders.vue b/src/components/bookmark_folders/bookmark_folders.vue new file mode 100644 index 00000000..733e4cab --- /dev/null +++ b/src/components/bookmark_folders/bookmark_folders.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js new file mode 100644 index 00000000..d5f82f46 --- /dev/null +++ b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js @@ -0,0 +1,16 @@ +import { mapState } from 'vuex' +import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js' + +export const BookmarkFoldersMenuContent = { + components: { + NavigationEntry + }, + computed: { + ...mapState({ + folders: getBookmarkFolderEntries + }) + } +} + +export default BookmarkFoldersMenuContent diff --git a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue new file mode 100644 index 00000000..d603cd01 --- /dev/null +++ b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js index 5ac43d90..9571d630 100644 --- a/src/components/bookmark_timeline/bookmark_timeline.js +++ b/src/components/bookmark_timeline/bookmark_timeline.js @@ -1,16 +1,31 @@ import Timeline from '../timeline/timeline.vue' const Bookmarks = { - computed: { - timeline () { - return this.$store.state.statuses.timelines.bookmarks - } + created () { + this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null }) }, components: { Timeline }, + computed: { + folderId () { + return this.$route.params.id + }, + timeline () { + return this.$store.state.statuses.timelines.bookmarks + } + }, + watch: { + folderId () { + this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) + this.$store.dispatch('stopFetchingTimeline', 'bookmarks') + this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null }) + } + }, unmounted () { this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) + this.$store.dispatch('stopFetchingTimeline', 'bookmarks') } } diff --git a/src/components/bookmark_timeline/bookmark_timeline.vue b/src/components/bookmark_timeline/bookmark_timeline.vue index 8da6884b..7a096004 100644 --- a/src/components/bookmark_timeline/bookmark_timeline.vue +++ b/src/components/bookmark_timeline/bookmark_timeline.vue @@ -3,6 +3,7 @@ :title="$t('nav.bookmarks')" :timeline="timeline" :timeline-name="'bookmarks'" + :bookmark-folder-id="bookmarkFolderId" /> diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 8c9c3b11..11863e97 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -1,3 +1,4 @@ +import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue' import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue' import { mapState, mapGetters } from 'vuex' import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js' @@ -41,6 +42,7 @@ const NavPanel = { created () { }, components: { + BookmarkFoldersMenuContent, ListsMenuContent, NavigationEntry, NavigationPins, @@ -51,6 +53,7 @@ const NavPanel = { editMode: false, showTimelines: false, showLists: false, + showBookmarkFolders: false, timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })), rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k })) } @@ -62,6 +65,9 @@ const NavPanel = { toggleLists () { this.showLists = !this.showLists }, + toggleBookmarkFolders () { + this.showBookmarkFolders = !this.showBookmarkFolders + }, toggleEditMode () { this.editMode = !this.editMode }, @@ -90,7 +96,8 @@ const NavPanel = { pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, supportsAnnouncements: state => state.announcements.supportsAnnouncements, pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems), - collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav + collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav, + bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable }), timelinesItems () { return filterNavigation( @@ -102,7 +109,8 @@ const NavPanel = { hasAnnouncements: this.supportsAnnouncements, isFederating: this.federating, isPrivate: this.privateMode, - currentUser: this.currentUser + currentUser: this.currentUser, + supportsBookmarkFolders: this.bookmarkFolders } ) }, @@ -116,7 +124,8 @@ const NavPanel = { hasAnnouncements: this.supportsAnnouncements, isFederating: this.federating, isPrivate: this.privateMode, - currentUser: this.currentUser + currentUser: this.currentUser, + supportsBookmarkFolders: this.bookmarkFolders } ) }, diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index bf608936..a19e9ba5 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -83,6 +83,39 @@ class="timelines" /> + + + + + + + { +export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser, supportsBookmarkFolders }) => { return list.filter(({ criteria, anon, anonRoute }) => { const set = new Set(criteria || []) if (!isFederating && set.has('federating')) return false @@ -7,6 +7,7 @@ export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFede if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false if (!hasChats && set.has('chats')) return false if (!hasAnnouncements && set.has('announcements')) return false + if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) return false return true }) } @@ -17,3 +18,12 @@ export const getListEntries = state => state.lists.allLists.map(list => ({ labelRaw: list.title, iconLetter: list.title[0] })) + +export const getBookmarkFolderEntries = state => state.bookmarkFolders.allFolders.map(folder => ({ + name: 'bookmark-folder-' + folder.id, + routeObject: { name: 'bookmark-folder', params: { id: folder.id } }, + labelRaw: folder.name, + iconEmoji: folder.emoji, + iconEmojiUrl: folder.emoji_url, + iconLetter: folder.name[0] +})) diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index face430e..9fc264ba 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -32,7 +32,8 @@ export const TIMELINES = { bookmarks: { route: 'bookmarks', icon: 'bookmark', - label: 'nav.bookmarks' + label: 'nav.bookmarks', + criteria: ['!supportsBookmarkFolders'] }, favorites: { routeObject: { name: 'user-profile', query: { tab: 'favorites' } }, diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index 4ea54ee3..728de1be 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -22,11 +22,25 @@ :icon="item.icon" /> + {{ item.iconLetter }} + v-else-if="item.iconEmoji" + class="menu-icon iconEmoji" + > + + {{ item.iconEmoji }} + + {{ item.iconLetter }} {{ item.labelRaw || $t(item.label) }} @@ -110,5 +124,18 @@ .badge { margin: 0 var(--__horizontal-gap); } + + .iconEmoji { + display: inline-block; + text-align: center; + object-fit: contain; + vertical-align: middle; + height: var(--__line-height); + width: var(--__line-height); + + > span { + font-size: 1.5rem; + } + } } diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 59170f49..9cde9b04 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -26,6 +26,7 @@ const Timeline = { 'userId', 'listId', 'statusId', + 'bookmarkFolderId', 'tag', 'embedded', 'count', @@ -123,6 +124,7 @@ const Timeline = { userId: this.userId, listId: this.listId, statusId: this.statusId, + bookmarkFolderId: this.bookmarkFolderId, tag: this.tag }) }, @@ -186,6 +188,7 @@ const Timeline = { userId: this.userId, listId: this.listId, statusId: this.statusId, + bookmarkFolderId: this.bookmarkFolderId, tag: this.tag }).then(({ statuses }) => { if (statuses && statuses.length === 0) { diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index c4586b32..a09bbcca 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -2,6 +2,7 @@ import Popover from '../popover/popover.vue' import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import { mapState } from 'vuex' import { ListsMenuContent } from '../lists_menu/lists_menu_content.vue' +import { BookmarkFoldersMenuContent } from '../bookmark_folders_menu/bookmark_folders_menu_content.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { TIMELINES } from 'src/components/navigation/navigation.js' import { filterNavigation } from 'src/components/navigation/filter.js' @@ -13,10 +14,10 @@ library.add(faChevronDown) // Route -> i18n key mapping, exported and not in the computed // because nav panel benefits from the same information. -export const timelineNames = () => { +export const timelineNames = (supportsBookmarkFolders) => { return { friends: 'nav.home_timeline', - bookmarks: 'nav.bookmarks', + bookmarks: supportsBookmarkFolders ? 'nav.all_bookmarks' : 'nav.bookmarks', dms: 'nav.dms', 'public-timeline': 'nav.public_tl', 'public-external-timeline': 'nav.twkn', @@ -28,7 +29,8 @@ const TimelineMenu = { components: { Popover, NavigationEntry, - ListsMenuContent + ListsMenuContent, + BookmarkFoldersMenuContent }, data () { return { @@ -36,7 +38,7 @@ const TimelineMenu = { } }, created () { - if (timelineNames()[this.$route.name]) { + if (timelineNames(this.bookmarkFolders)[this.$route.name]) { this.$store.dispatch('setLastTimeline', this.$route.name) } }, @@ -45,10 +47,15 @@ const TimelineMenu = { const route = this.$route.name return route === 'lists-timeline' }, + useBookmarkFoldersMenu () { + const route = this.$route.name + return this.bookmarkFolders && (route === 'bookmark-folder' || route === 'bookmarks') + }, ...mapState({ currentUser: state => state.users.currentUser, privateMode: state => state.instance.private, - federating: state => state.instance.federating + federating: state => state.instance.federating, + bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable }), timelinesList () { return filterNavigation( @@ -57,7 +64,8 @@ const TimelineMenu = { hasChats: this.pleromaChatMessagesAvailable, isFederating: this.federating, isPrivate: this.privateMode, - currentUser: this.currentUser + currentUser: this.currentUser, + supportsBookmarkFolders: this.bookmarkFolders } ) } @@ -89,7 +97,10 @@ const TimelineMenu = { if (route === 'lists-timeline') { return this.$store.getters.findListTitle(this.$route.params.id) } - const i18nkey = timelineNames()[this.$route.name] + if (route === 'bookmark-folder') { + return this.$store.getters.findBookmarkFolderName(this.$route.params.id) + } + const i18nkey = timelineNames(this.bookmarkFolders)[this.$route.name] return i18nkey ? this.$t(i18nkey) : route } } diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue index d755b9dd..bdcfb73d 100644 --- a/src/components/timeline_menu/timeline_menu.vue +++ b/src/components/timeline_menu/timeline_menu.vue @@ -15,6 +15,10 @@ :show-pin="false" class="timelines" /> +
    folder.id === folderId) + } +} + +const actions = { + setBookmarkFolders ({ commit }, value) { + commit('setBookmarkFolders', value) + }, + createBookmarkFolder ({ rootState, commit }, { name, emoji }) { + return rootState.api.backendInteractor.createBookmarkFolder({ name, emoji }) + .then((folder) => { + commit('setBookmarkFolder', folder) + return folder + }) + }, + setBookmarkFolder ({ rootState, commit }, { folderId, name, emoji }) { + return rootState.api.backendInteractor.updateBookmarkFolder({ folderId, name, emoji }) + .then((folder) => { + commit('setBookmarkFolder', folder) + return folder + }) + }, + deleteBookmarkFolder ({ rootState, commit }, { folderId }) { + rootState.api.backendInteractor.deleteBookmarkFolder({ folderId }) + commit('deleteBookmarkFolder', { folderId }) + } +} + +export const getters = { + findBookmarkFolderName: state => id => { + const folder = state.allFolders.find(folder => folder.id === id) + + if (!folder) return + return folder.name + } +} + +const bookmarkFolders = { + state: defaultState, + mutations, + actions, + getters +} + +export default bookmarkFolders diff --git a/src/modules/instance.js b/src/modules/instance.js index 994f60a5..dac16793 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -140,6 +140,7 @@ const defaultState = { shoutAvailable: false, pleromaChatMessagesAvailable: false, pleromaCustomEmojiReactionsAvailable: false, + pleromaBookmarkFoldersAvailable: false, gopherAvailable: false, mediaProxyAvailable: false, suggestionsEnabled: false, diff --git a/src/modules/users.js b/src/modules/users.js index b8f49f15..47b43c6f 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -579,6 +579,7 @@ const users = { store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.dispatch('stopFetchingNotifications') store.dispatch('stopFetchingLists') + store.dispatch('stopFetchingBookmarkFolders') store.dispatch('stopFetchingFollowRequests') store.commit('clearNotifications') store.commit('resetStatuses') @@ -635,6 +636,7 @@ const users = { } dispatch('startFetchingLists') + dispatch('startFetchingBookmarkFolders') if (user.locked) { dispatch('startFetchingFollowRequests') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index fa417193..c22042aa 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -110,6 +110,8 @@ const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcemen const PLEROMA_SCROBBLES_URL = id => `/api/v1/pleroma/accounts/${id}/scrobbles` const PLEROMA_STATUS_QUOTES_URL = id => `/api/v1/pleroma/statuses/${id}/quotes` const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id}/favourites` +const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders' +const PLEROMA_BOOKMARK_FOLDER_URL = id => `/api/v1/pleroma/bookmark_folders/${id}` const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config' const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions' @@ -690,7 +692,8 @@ const fetchTimeline = ({ tag = false, withMuted = false, replyVisibility = 'all', - includeTypes = [] + includeTypes = [], + bookmarkFolderId = false }) => { const timelineUrls = { public: MASTODON_PUBLIC_TIMELINE, @@ -760,6 +763,9 @@ const fetchTimeline = ({ params.push(['include_types[]', type]) }) } + if (timeline === 'bookmarks' && bookmarkFolderId) { + params.push(['folder_id', bookmarkFolderId]) + } params.push(['limit', 20]) @@ -1893,6 +1899,44 @@ const deleteEmojiFile = ({ packName, shortcode }) => { return fetch(`${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, { method: 'DELETE' }) } +const fetchBookmarkFolders = ({ credentials }) => { + const url = PLEROMA_BOOKMARK_FOLDERS_URL + return fetch(url, { headers: authHeaders(credentials) }) + .then((data) => data.json()) +} + +const createBookmarkFolder = ({ name, emoji, credentials }) => { + const url = PLEROMA_BOOKMARK_FOLDERS_URL + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(url, { + headers, + method: 'POST', + body: JSON.stringify({ name, emoji }) + }).then((data) => data.json()) +} + +const updateBookmarkFolder = ({ folderId, name, emoji, credentials }) => { + const url = PLEROMA_BOOKMARK_FOLDER_URL(folderId) + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(url, { + headers, + method: 'PUT', + body: JSON.stringify({ name, emoji }) + }) +} + +const deleteBookmarkFolder = ({ folderId, credentials }) => { + const url = PLEROMA_BOOKMARK_FOLDER_URL(folderId) + return fetch(url, { + method: 'DELETE', + headers: authHeaders(credentials) + }) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -2023,7 +2067,11 @@ const apiService = { updateEmojiFile, deleteEmojiFile, listRemoteEmojiPacks, - downloadRemoteEmojiPack + downloadRemoteEmojiPack, + fetchBookmarkFolders, + createBookmarkFolder, + updateBookmarkFolder, + deleteBookmarkFolder } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 8ceb897d..49785d08 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -3,10 +3,11 @@ import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' +import bookmarkFoldersFetcher from '../../services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js' const backendInteractorService = credentials => ({ - startFetchingTimeline ({ timeline, store, userId = false, listId = false, statusId = false, tag }) { - return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, statusId, tag }) + startFetchingTimeline ({ timeline, store, userId = false, listId = false, statusId = false, bookmarkFolderId = false, tag }) { + return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, statusId, bookmarkFolderId, tag }) }, fetchTimeline (args) { @@ -29,6 +30,10 @@ const backendInteractorService = credentials => ({ return listsFetcher.startFetching({ store, credentials }) }, + startFetchingBookmarkFolders ({ store }) { + return bookmarkFoldersFetcher.startFetching({ store, credentials }) + }, + startUserSocket ({ store }) { const serv = store.rootState.instance.server.replace('http', 'ws') const url = serv + getMastodonSocketURI({ credentials, stream: 'user' }) diff --git a/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js b/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js new file mode 100644 index 00000000..2181ab67 --- /dev/null +++ b/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js @@ -0,0 +1,22 @@ +import apiService from '../api/api.service.js' +import { promiseInterval } from '../promise_interval/promise_interval.js' + +const fetchAndUpdate = ({ store, credentials }) => { + return apiService.fetchBookmarkFolders({ credentials }) + .then(bookmarkFolders => { + store.commit('setBookmarkFolders', bookmarkFolders) + }, () => {}) + .catch(() => {}) +} + +const startFetching = ({ credentials, store }) => { + const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) + boundFetchAndUpdate() + return promiseInterval(boundFetchAndUpdate, 240000) +} + +const bookmarkFoldersFetcher = { + startFetching +} + +export default bookmarkFoldersFetcher diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 2fed14bc..e288f99e 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -25,6 +25,7 @@ const fetchAndUpdate = ({ userId = false, listId = false, statusId = false, + bookmarkFolderId = false, tag = false, until, since @@ -49,6 +50,7 @@ const fetchAndUpdate = ({ args.userId = userId args.listId = listId args.statusId = statusId + args.bookmarkFolderId = bookmarkFolderId args.tag = tag args.withMuted = !hideMutedPosts if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) { @@ -80,15 +82,16 @@ const fetchAndUpdate = ({ }) } -const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, statusId = false, tag = false }) => { +const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, statusId = false, bookmarkFolderId = false, tag = false }) => { const rootState = store.rootState || store.state const timelineData = rootState.statuses.timelines[camelCase(timeline)] const showImmediately = timelineData.visibleStatuses.length === 0 timelineData.userId = userId timelineData.listId = listId - fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, statusId, tag }) + timelineData.bookmarkFolderId = bookmarkFolderId + fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, statusId, bookmarkFolderId, tag }) const boundFetchAndUpdate = () => - fetchAndUpdate({ timeline, credentials, store, userId, listId, statusId, tag }) + fetchAndUpdate({ timeline, credentials, store, userId, listId, statusId, bookmarkFolderId, tag }) return promiseInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { From 89fbaa159f29a7d175a5323eded29e20c9f3a752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 24 Sep 2024 00:05:33 +0200 Subject: [PATCH 02/10] Allow adding bookmarks to folders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/extra_buttons/extra_buttons.js | 7 +++- .../extra_buttons/extra_buttons.vue | 4 ++ .../status_bookmark_folder_menu.js | 40 +++++++++++++++++++ .../status_bookmark_folder_menu.vue | 38 ++++++++++++++++++ src/i18n/en.json | 3 +- src/modules/statuses.js | 4 +- src/services/api/api.service.js | 7 +++- .../entity_normalizer.service.js | 1 + 8 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.js create mode 100644 src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index e2c88ceb..b3e1cb2b 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -1,6 +1,7 @@ import Popover from '../popover/popover.vue' import genRandomSeed from '../../services/random_seed/random_seed.service.js' import ConfirmModal from '../confirm_modal/confirm_modal.vue' +import StatusBookmarkFolderMenu from '../status_bookmark_folder_menu/status_bookmark_folder_menu.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisH, @@ -36,7 +37,8 @@ const ExtraButtons = { props: ['status'], components: { Popover, - ConfirmModal + ConfirmModal, + StatusBookmarkFolderMenu }, data () { return { @@ -145,6 +147,9 @@ const ExtraButtons = { canBookmark () { return !!this.currentUser }, + bookmarkFolders () { + return this.$store.state.instance.pleromaBookmarkFoldersAvailable + }, statusLink () { return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` }, diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 7b38d974..c030de0c 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -87,6 +87,10 @@ icon="bookmark" />{{ $t("status.unbookmark") }} + + @@ -97,9 +130,49 @@ } .panel-body { + display: flex; + gap: 0.5em; + } + + .emoji-picker-panel { + position: absolute; + z-index: 20; + margin-top: 2px; + + &.hide { + display: none; + } + } + + .input-emoji { + height: 2.5em; + width: 2.5em; + padding: 0; + + .iconEmoji { + display: inline-block; + text-align: center; + object-fit: contain; + vertical-align: middle; + height: 2.5em; + width: 2.5em; + + > span { + font-size: 1.5rem; + line-height: 2.5rem; + } + } + + img.iconEmoji { + padding: 0.25em; + box-sizing: border-box; + } + } + + .input-wrap { display: flex; flex-direction: column; - overflow: hidden; + gap: 0.5em; } .go-back-button { diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index 9ea5c877..d3d6563a 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -180,7 +180,7 @@ const EmojiPicker = { if (!this.keepOpen) { this.$refs.popover.hidePopover() } - this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) + this.$emit('emoji', { insertion: value, insertionUrl: emoji.imageUrl, keepOpen: this.keepOpen }) }, onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) { const target = this.$refs['emoji-groups'].$el diff --git a/src/i18n/en.json b/src/i18n/en.json index 5a9bee32..cc55a6d0 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1409,15 +1409,16 @@ "tooltip": "This domain contains non-ascii characters." }, "bookmark_folders": { - "create": "Create folder", + "select_folder": "Select bookmark folder", "creating_folder": "Creating bookmark folder", - "delete": "Delete folder", "editing_folder": "Editing folder {folderName}", - "error": "Error manipulating bookmark folders: {0}", + "emoji": "Emoji", "name": "Folder name", "new": "New Folder", + "create": "Create folder", + "delete": "Delete folder", + "update_folder": "Save changes", "really_delete": "Do you really want to delete the folder?", - "select_folder": "Select bookmark folder", - "update_folder": "Save changes" + "error": "Error manipulating bookmark folders: {0}" } } diff --git a/src/modules/bookmark_folders.js b/src/modules/bookmark_folders.js index 5dde58a7..c276adf4 100644 --- a/src/modules/bookmark_folders.js +++ b/src/modules/bookmark_folders.js @@ -8,14 +8,14 @@ export const mutations = { setBookmarkFolders (state, value) { state.allFolders = value }, - setBookmarkFolder (state, { id, name, emoji, emojiUrl }) { + setBookmarkFolder (state, { id, name, emoji, emoji_url: emojiUrl }) { const entry = find(state.allFolders, { id }) if (!entry) { - state.allFolders.push({ id, name, emoji, emojiUrl }) + state.allFolders.push({ id, name, emoji, emoji_url: emojiUrl }) } else { entry.name = name entry.emoji = emoji - entry.emojiUrl = emojiUrl + entry.emoji_url = emojiUrl } }, deleteBookmarkFolder (state, { folderId }) { From 44a7f8a4858065038cb9198e2a0bab0d1aabfdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 26 Sep 2024 02:05:55 +0200 Subject: [PATCH 09/10] Styles improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/bookmark_folder_card/bookmark_folder_card.vue | 4 +++- src/components/bookmark_folders/bookmark_folders.vue | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/bookmark_folder_card/bookmark_folder_card.vue b/src/components/bookmark_folder_card/bookmark_folder_card.vue index b773c9fb..50863071 100644 --- a/src/components/bookmark_folder_card/bookmark_folder_card.vue +++ b/src/components/bookmark_folder_card/bookmark_folder_card.vue @@ -41,12 +41,14 @@