| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | import type {StableLocationData} from './types'; |
| 9 | |
| 10 | import {atom} from 'jotai'; |
| 11 | import {tracker} from './analytics'; |
| 12 | import serverAPI from './ClientToServerAPI'; |
| 13 | import {localStorageBackedAtom, readAtom, writeAtom} from './jotaiUtils'; |
| 14 | import {latestCommits} from './serverAPIState'; |
| 15 | import {registerDisposable} from './utils'; |
| 16 | |
| 17 | export const REMOTE_MASTER_BOOKMARK = 'remote/master'; |
| 18 | |
| 19 | export type MasterBookmarkVisibility = 'auto' | 'show' | 'hide'; |
| 20 | |
| 21 | export type BookmarksData = { |
| 22 | /** These bookmarks should be hidden from the automatic set of remote bookmarks */ |
| 23 | hiddenRemoteBookmarks: Array<string>; |
| 24 | /** These stables should be requested by the server to fetch additional stables */ |
| 25 | additionalStables?: Array<string>; |
| 26 | /** Whether to use the recommended bookmark instead of user-selected bookmarks */ |
| 27 | useRecommendedBookmark?: boolean; |
| 28 | /** |
| 29 | * Master bookmark visibility setting. |
| 30 | * - 'auto': Use sitevar config to decide (default when GK enabled) |
| 31 | * - 'show': Always show master bookmark (user override) |
| 32 | * - 'hide': Always hide master bookmark (user override) |
| 33 | */ |
| 34 | masterBookmarkVisibility?: MasterBookmarkVisibility; |
| 35 | }; |
| 36 | export const bookmarksDataStorage = localStorageBackedAtom<BookmarksData>('isl.bookmarks', { |
| 37 | hiddenRemoteBookmarks: [], |
| 38 | additionalStables: [], |
| 39 | useRecommendedBookmark: true, |
| 40 | }); |
| 41 | export const hiddenRemoteBookmarksAtom = atom(get => { |
| 42 | return new Set(get(bookmarksDataStorage).hiddenRemoteBookmarks); |
| 43 | }); |
| 44 | |
| 45 | /** Result of fetch from the server. Stables are automatically included in list of commits */ |
| 46 | export const fetchedStablesAtom = atom<StableLocationData | undefined>(undefined); |
| 47 | |
| 48 | export function addManualStable(stable: string) { |
| 49 | // save this as a persisted stable we'd like to always fetch going forward |
| 50 | writeAtom(bookmarksDataStorage, data => ({ |
| 51 | ...data, |
| 52 | additionalStables: [...(data.additionalStables ?? []), stable], |
| 53 | })); |
| 54 | // write the stable to the fetched state, so it shows a loading spinner |
| 55 | writeAtom(fetchedStablesAtom, last => |
| 56 | last |
| 57 | ? { |
| 58 | ...last, |
| 59 | manual: {...(last?.manual ?? {}), [stable]: null}, |
| 60 | } |
| 61 | : undefined, |
| 62 | ); |
| 63 | // refetch using the new manual stable |
| 64 | fetchStableLocations(); |
| 65 | } |
| 66 | |
| 67 | export function removeManualStable(stable: string) { |
| 68 | writeAtom(bookmarksDataStorage, data => ({ |
| 69 | ...data, |
| 70 | additionalStables: (data.additionalStables ?? []).filter(s => s !== stable), |
| 71 | })); |
| 72 | writeAtom(fetchedStablesAtom, last => { |
| 73 | if (last) { |
| 74 | const manual = {...(last.manual ?? {})}; |
| 75 | delete manual[stable]; |
| 76 | return {...last, manual}; |
| 77 | } |
| 78 | }); |
| 79 | // refetch without this stable, so it's excluded from `sl log` |
| 80 | fetchStableLocations(); |
| 81 | } |
| 82 | |
| 83 | registerDisposable( |
| 84 | serverAPI, |
| 85 | serverAPI.onMessageOfType('fetchedStables', data => { |
| 86 | writeAtom(fetchedStablesAtom, data.stables); |
| 87 | }), |
| 88 | import.meta.hot, |
| 89 | ); |
| 90 | fetchStableLocations(); // fetch on startup |
| 91 | |
| 92 | registerDisposable( |
| 93 | serverAPI, |
| 94 | serverAPI.onMessageOfType('fetchedRecommendedBookmarks', data => { |
| 95 | writeAtom(recommendedBookmarksAtom, new Set(data.bookmarks)); |
| 96 | |
| 97 | const bookmarksData = readAtom(bookmarksDataStorage); |
| 98 | tracker.track('RecommendedBookmarksStatus', { |
| 99 | extras: { |
| 100 | enabled: bookmarksData.useRecommendedBookmark ?? false, |
| 101 | recommendedBookmarks: data.bookmarks, |
| 102 | }, |
| 103 | }); |
| 104 | }), |
| 105 | import.meta.hot, |
| 106 | ); |
| 107 | |
| 108 | export function fetchStableLocations() { |
| 109 | const data = readAtom(bookmarksDataStorage); |
| 110 | const additionalStables = data.additionalStables ?? []; |
| 111 | serverAPI.postMessage({type: 'fetchAndSetStables', additionalStables}); |
| 112 | } |
| 113 | |
| 114 | export const remoteBookmarks = atom(get => { |
| 115 | // Note: `latestDag` will have already filtered out hidden bookmarks, |
| 116 | // so we need to use latestCommits, which is not filtered. |
| 117 | const commits = get(latestCommits).filter(commit => commit.phase === 'public'); |
| 118 | commits.sort((a, b) => b.date.valueOf() - a.date.valueOf()); |
| 119 | return commits.flatMap(commit => commit.remoteBookmarks); |
| 120 | }); |
| 121 | |
| 122 | /** |
| 123 | * For determining if reminders to use recommended bookmarks should be shown |
| 124 | */ |
| 125 | export const recommendedBookmarksReminder = localStorageBackedAtom<{ |
| 126 | shouldShow: boolean; |
| 127 | lastShown: number; |
| 128 | }>('isl.recommended-bookmarks-reminder', { |
| 129 | shouldShow: true, |
| 130 | lastShown: 0, |
| 131 | }); |
| 132 | |
| 133 | /** |
| 134 | * For determining if recommended bookmarks onboarding tip should be shown |
| 135 | */ |
| 136 | export const recommendedBookmarksOnboarding = localStorageBackedAtom<boolean>( |
| 137 | 'isl.recommended-bookmarks-onboarding', |
| 138 | true, |
| 139 | ); |
| 140 | |
| 141 | export const recommendedBookmarksAtom = atom<Set<string>>(new Set<string>()); |
| 142 | |
| 143 | /** Checks if recommended bookmarks are available in remoteBookmarks */ |
| 144 | export const recommendedBookmarksAvailableAtom = atom(get => { |
| 145 | const recommendedBookmarks = get(recommendedBookmarksAtom); |
| 146 | const allRemoteBookmarks = get(remoteBookmarks); |
| 147 | return ( |
| 148 | recommendedBookmarks.size > 0 && |
| 149 | [...recommendedBookmarks].some(b => allRemoteBookmarks.includes(b)) |
| 150 | ); |
| 151 | }); |
| 152 | |