diff --git a/src/components/Note/NoteFooter/NoteFooter.tsx b/src/components/Note/NoteFooter/NoteFooter.tsx
index 19890f5..5ad47bb 100644
--- a/src/components/Note/NoteFooter/NoteFooter.tsx
+++ b/src/components/Note/NoteFooter/NoteFooter.tsx
@@ -9,6 +9,7 @@ import { useIntl } from '@cookbook/solid-intl';
import { truncateNumber } from '../../../lib/notifications';
import { canUserReceiveZaps, zapNote } from '../../../lib/zap';
+import CustomZap from '../../CustomZap/CustomZap';
import { useSettingsContext } from '../../../contexts/SettingsContext';
import zapMD from '../../../assets/lottie/zap_md.json';
@@ -42,8 +43,16 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
const [isRepostMenuVisible, setIsRepostMenuVisible] = createSignal(false);
+ const [showZapAnim, setShowZapAnim] = createSignal(false);
+ const [hideZapIcon, setHideZapIcon] = createSignal(false);
+ const [zappedNow, setZappedNow] = createSignal(false);
+ const [zappedAmount, setZappedAmount] = createSignal(0);
+ const [isZapping, setIsZapping] = createSignal(false);
+
+ let quickZapDelay = 0;
let footerDiv: HTMLDivElement | undefined;
let noteContextMenu: HTMLDivElement | undefined;
+ let repostMenu: HTMLDivElement | undefined;
const repostMenuItems: MenuItem[] = [
{
@@ -58,6 +67,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
},
];
+
const onClickOutside = (e: MouseEvent) => {
if (
!document?.getElementById(`repost_menu_${props.note.post.id}`)?.contains(e.target as Node)
@@ -155,44 +165,49 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
}
};
- let quickZapDelay = 0;
- const [isZapping, setIsZapping] = createSignal(false);
+ const onConfirmZap = (zapOption: ZapOption) => {
+ app?.actions.closeCustomZapModal();
+ setZappedAmount(() => zapOption.amount || 0);
+ setZappedNow(true);
+ setZapped(true);
+ animateZap();
+ };
+
+ const onSuccessZap = (zapOption: ZapOption) => {
+ app?.actions.closeCustomZapModal();
+ setIsZapping(false);
+ setZappedNow(false);
+ setShowZapAnim(false);
+ setHideZapIcon(false);
+ setZapped(true);
+ };
+
+ const onFailZap = (zapOption: ZapOption) => {
+ setZappedAmount(() => -(zapOption.amount || 0));
+ setZappedNow(true);
+ app?.actions.closeCustomZapModal();
+ setIsZapping(false);
+ setShowZapAnim(false);
+ setHideZapIcon(false);
+ setZapped(props.note.post.noteActions.zapped);
+ };
+
+ const onCancelZap = (zapOption: ZapOption) => {
+ setZappedAmount(() => -(zapOption.amount || 0));
+ setZappedNow(true);
+ app?.actions.closeCustomZapModal();
+ setIsZapping(false);
+ setShowZapAnim(false);
+ setHideZapIcon(false);
+ setZapped(props.note.post.noteActions.zapped);
+ };
const customZapInfo: CustomZapInfo = {
note: props.note,
- onConfirm: (zapOption: ZapOption) => {
- app?.actions.closeCustomZapModal();
- setZappedAmount(() => zapOption.amount || 0);
- setZappedNow(true);
- setZapped(true);
- animateZap();
- },
- onSuccess: (zapOption: ZapOption) => {
- app?.actions.closeCustomZapModal();
- setIsZapping(false);
- setZappedNow(false);
- setShowZapAnim(false);
- setHideZapIcon(false);
- setZapped(true);
- },
- onFail: (zapOption: ZapOption) => {
- setZappedAmount(() => -(zapOption.amount || 0));
- setZappedNow(true);
- app?.actions.closeCustomZapModal();
- setIsZapping(false);
- setShowZapAnim(false);
- setHideZapIcon(false);
- setZapped(props.note.post.noteActions.zapped);
- },
- onCancel: (zapOption: ZapOption) => {
- setZappedAmount(() => -(zapOption.amount || 0));
- setZappedNow(true);
- app?.actions.closeCustomZapModal();
- setIsZapping(false);
- setShowZapAnim(false);
- setHideZapIcon(false);
- setZapped(props.note.post.noteActions.zapped);
- },
+ onConfirm: onConfirmZap,
+ onSuccess: onSuccessZap,
+ onFail: onFailZap,
+ onCancel: onCancelZap,
};
const startZap = (e: MouseEvent | TouchEvent) => {
@@ -246,9 +261,6 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
}
};
- const [zappedNow, setZappedNow] = createSignal(false);
- const [zappedAmount, setZappedAmount] = createSignal(0);
-
const animateZap = () => {
setShowZapAnim(true);
setTimeout(() => {
@@ -359,12 +371,6 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
});
- const [showZapAnim, setShowZapAnim] = createSignal(false);
- const [hideZapIcon, setHideZapIcon] = createSignal(false);
-
-
- let repostMenu: HTMLDivElement | undefined;
-
const determineOrient = () => {
const coor = getScreenCordinates(repostMenu);
const height = 100;
diff --git a/src/constants.ts b/src/constants.ts
index d14fb5f..8b4f140 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -107,6 +107,7 @@ export enum Kind {
MuteList = 10_000,
RelayList = 10_002,
+ Bookmarks = 10_003,
CategorizedPeople = 30_000,
Settings = 30_078,
diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx
index 90f7d8f..f3e3c35 100644
--- a/src/contexts/AccountContext.tsx
+++ b/src/contexts/AccountContext.tsx
@@ -28,11 +28,10 @@ import { sendContacts, sendLike, sendMuteList, triggerImportEvents } from "../li
// @ts-ignore Bad types in nostr-tools
import { generatePrivateKey, Relay, getPublicKey as nostrGetPubkey, nip19 } from "nostr-tools";
import { APP_ID } from "../App";
-import { getLikes, getFilterlists, getProfileContactList, getProfileMuteList, getUserProfiles, sendFilterlists, getAllowlist, sendAllowList, getRelays, sendRelays, extractRelayConfigFromTags } from "../lib/profile";
-import { clearSec, getStorage, getStoredProfile, readEmojiHistory, readSecFromStorage, saveEmojiHistory, saveFollowing, saveLikes, saveMuted, saveMuteList, saveRelaySettings, setStoredProfile, storeSec } from "../lib/localStore";
+import { getLikes, getFilterlists, getProfileContactList, getProfileMuteList, getUserProfiles, sendFilterlists, getAllowlist, sendAllowList, getRelays, sendRelays, extractRelayConfigFromTags, getBookmarks } from "../lib/profile";
+import { clearSec, getStorage, getStoredProfile, readBookmarks, readEmojiHistory, readSecFromStorage, saveBookmarks, saveEmojiHistory, saveFollowing, saveLikes, saveMuted, saveMuteList, saveRelaySettings, setStoredProfile, storeSec } from "../lib/localStore";
import { connectRelays, connectToRelay, getDefaultRelays, getPreConfiguredRelays } from "../lib/relays";
import { getPublicKey } from "../lib/nostrAPI";
-import { generateKeys } from "../lib/PrimalNostr";
import EnterPinModal from "../components/EnterPinModal/EnterPinModal";
import CreateAccountModal from "../components/CreateAccountModal/CreateAccountModal";
import LoginModal from "../components/LoginModal/LoginModal";
@@ -43,6 +42,7 @@ import { account as tAccount, followWarning, forgotPin } from "../translations";
import { getMembershipStatus } from "../lib/membership";
import ConfirmModal from "../components/ConfirmModal/ConfirmModal";
+
export type AccountContextStore = {
likes: string[],
defaultRelays: string[],
@@ -74,6 +74,7 @@ export type AccountContextStore = {
showLogin: boolean,
emojiHistory: EmojiOption[],
membershipStatus: MembershipStatus,
+ bookmarks: string[],
actions: {
showNewNoteForm: () => void,
hideNewNoteForm: () => void,
@@ -101,6 +102,8 @@ export type AccountContextStore = {
showGetStarted: () => void,
saveEmoji: (emoji: EmojiOption) => void,
checkNostrKey: () => void,
+ fetchBookmarks: () => void,
+ updateBookmarks: (bookmarks: string[]) => void,
},
}
@@ -134,6 +137,7 @@ const initialData = {
showLogin: false,
emojiHistory: [],
membershipStatus: {},
+ bookmarks: [],
};
export const AccountContext = createContext
();
@@ -285,6 +289,10 @@ export function AccountProvider(props: { children: JSXElement }) {
updateStore('publicKey', () => pubkey);
localStorage.setItem('pubkey', pubkey);
checkMembershipStatus();
+
+ const bks = readBookmarks(pubkey);
+ updateStore('bookmarks', () => [...bks]);
+ fetchBookmarks();
}
else {
updateStore('publicKey', () => undefined);
@@ -1301,10 +1309,19 @@ export function AccountProvider(props: { children: JSXElement }) {
};
const checkNostrKey = () => {
+ if (store.publicKey) return;
updateStore('isKeyLookupDone', () => false);
fetchNostrKey();
};
+ const fetchBookmarks = () => {
+ getBookmarks(store.publicKey, `user_bookmarks_${APP_ID}`);
+ }
+
+ const updateBookmarks = (bookmarks: string[]) => {
+ updateStore('bookmarks', () => [...bookmarks]);
+ };
+
// EFFECTS --------------------------------------
createEffect(() => {
@@ -1483,6 +1500,29 @@ export function AccountProvider(props: { children: JSXElement }) {
}
}
+ if (subId === `user_bookmarks_${APP_ID}`) {
+ if (type === 'EVENT' && content && content.kind === Kind.Bookmarks) {
+ if (!content.created_at || content.created_at < store.followingSince) {
+ return;
+ }
+
+ const notes = content.tags.reduce((acc, t) => {
+ if (t[0] === 'e') {
+ return [...acc, t[1]];
+ }
+ return [...acc];
+ }, []);
+
+ updateStore('bookmarks', () => [...notes]);
+ }
+
+ return;
+ }
+
+ if (type === 'EOSE') {
+ saveBookmarks(store.publicKey, store.bookmarks);
+ }
+
};
// STORES ---------------------------------------
@@ -1517,6 +1557,8 @@ const [store, updateStore] = createStore({
showGetStarted,
saveEmoji,
checkNostrKey,
+ fetchBookmarks,
+ updateBookmarks,
},
});
diff --git a/src/lib/feed.ts b/src/lib/feed.ts
index 10fc937..9dfaa91 100644
--- a/src/lib/feed.ts
+++ b/src/lib/feed.ts
@@ -67,19 +67,21 @@ export const getEvents = (user_pubkey: string | undefined, eventIds: string[], s
};
-export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | undefined, subid: string, notes: 'authored' | 'replies', until = 0, limit = 20) => {
+export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | undefined, subid: string, notes: 'authored' | 'replies' | 'bookmarks', until = 0, limit = 20, offset = 0) => {
if (!pubkey) {
return;
}
- const start = until === 0 ? 'since' : 'until';
-
- let payload = { pubkey, limit, notes, [start]: until } ;
+ let payload = { pubkey, limit, notes } ;
if (user_pubkey) {
payload.user_pubkey = user_pubkey;
}
+ if (until > 0) payload.until = until;
+
+ if (offset > 0) payload.offset = offset;
+
sendMessage(JSON.stringify([
"REQ",
subid,
diff --git a/src/lib/localStore.ts b/src/lib/localStore.ts
index 91dd068..c4b02dc 100644
--- a/src/lib/localStore.ts
+++ b/src/lib/localStore.ts
@@ -13,6 +13,7 @@ export type LocalStore = {
theme: string,
homeSidebarSelection: SelectionOption | undefined,
userProfile: PrimalUser | undefined,
+ bookmarks: string[],
recomended: {
profiles: PrimalUser[],
stats: Record,
@@ -63,6 +64,7 @@ export const emptyStorage: LocalStore = {
noteDraftUserRefs: {},
uploadTime: defaultUploadTime,
selectedFeed: undefined,
+ bookmarks: [],
}
export const storageName = (pubkey?: string) => {
@@ -423,3 +425,21 @@ export const saveStoredFeed = (pubkey: string | undefined, feed: PrimalFeed) =>
setStorage(pubkey, store);
};
+
+export const saveBookmarks = (pubkey: string | undefined, bookmarks: string[]) => {
+ if (!pubkey) return;
+
+ const store = getStorage(pubkey);
+
+ store.bookmarks = [ ...bookmarks ];
+
+ setStorage(pubkey, store);
+};
+
+export const readBookmarks = (pubkey: string | undefined) => {
+ if (!pubkey) return [];
+
+ const store = getStorage(pubkey)
+
+ return store.bookmarks || [];
+};
diff --git a/src/lib/profile.ts b/src/lib/profile.ts
index bf21dcf..a222216 100644
--- a/src/lib/profile.ts
+++ b/src/lib/profile.ts
@@ -376,6 +376,27 @@ export const sendRelays = async (relays: Relay[], relaySettings: NostrRelays) =>
return await sendEvent(event, relays, relaySettings);
};
+export const sendBookmarks = async (tags: string[][], date: number, content: string, relays: Relay[], relaySettings?: NostrRelays) => {
+ const event = {
+ content,
+ kind: Kind.Bookmarks,
+ tags: [...tags],
+ created_at: date,
+ };
+
+ return await sendEvent(event, relays, relaySettings);
+};
+
+export const getBookmarks = async (pubkey: string | undefined, subid: string) => {
+ if (!pubkey) return;
+
+ sendMessage(JSON.stringify([
+ "REQ",
+ subid,
+ {cache: ["get_bookmarks", { pubkey }]},
+ ]));
+};
+
export const extractRelayConfigFromTags = (tags: string[][]) => {
return tags.reduce((acc, tag) => {
if (tag[0] !== 'r') return acc;
diff --git a/src/pages/Bookmarks.module.scss b/src/pages/Bookmarks.module.scss
new file mode 100644
index 0000000..7847259
--- /dev/null
+++ b/src/pages/Bookmarks.module.scss
@@ -0,0 +1,26 @@
+.bookmarkFeed {
+ border-top: 1px solid var(--devider);
+ padding-top: 20px;
+ margin-bottom: 48px;
+}
+
+.loader {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 64px;
+ margin-top: 20px;
+}
+
+.noBookmarks {
+ font-weight: 400;
+ font-size: 20px;
+ line-height: 20px;
+ color: var(--text-secondary);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 40px;
+}
diff --git a/src/pages/Bookmarks.tsx b/src/pages/Bookmarks.tsx
new file mode 100644
index 0000000..2194ea5
--- /dev/null
+++ b/src/pages/Bookmarks.tsx
@@ -0,0 +1,278 @@
+import { useIntl } from '@cookbook/solid-intl';
+import { Component, createEffect, For, on, onCleanup, onMount, Show, untrack } from 'solid-js';
+import { createStore } from 'solid-js/store';
+import { APP_ID } from '../App';
+import Loader from '../components/Loader/Loader';
+import Note from '../components/Note/Note';
+import PageCaption from '../components/PageCaption/PageCaption';
+import PageTitle from '../components/PageTitle/PageTitle';
+import Paginator from '../components/Paginator/Paginator';
+import { Kind } from '../constants';
+import { useAccountContext } from '../contexts/AccountContext';
+import { getEvents, getUserFeed } from '../lib/feed';
+import { setLinkPreviews } from '../lib/notes';
+import { subscribeTo } from '../sockets';
+import { convertToNotes, parseEmptyReposts } from '../stores/note';
+import { bookmarks as tBookmarks } from '../translations';
+import { NostrEventContent, NostrUserContent, NostrNoteContent, NostrStatsContent, NostrMentionContent, NostrNoteActionsContent, NoteActions, FeedPage, PrimalNote, NostrFeedRange, PageRange } from '../types/primal';
+import styles from './Bookmarks.module.scss';
+
+export type BookmarkStore = {
+ fetchingInProgress: boolean,
+ page: FeedPage,
+ notes: PrimalNote[],
+ noteIds: string[],
+ offset: number,
+ pageRange: PageRange,
+ reposts: Record | undefined,
+ firstLoad: boolean,
+}
+
+const emptyStore: BookmarkStore = {
+ fetchingInProgress: false,
+ page: {
+ messages: [],
+ users: {},
+ postStats: {},
+ mentions: {},
+ noteActions: {},
+ },
+ notes: [],
+ noteIds: [],
+ pageRange: {
+ since: 0,
+ until: 0,
+ order_by: 'created_at',
+ },
+ reposts: {},
+ offset: 0,
+ firstLoad: true,
+};
+
+let since: number = 0;
+
+const Bookmarks: Component = () => {
+ const account = useAccountContext();
+ const intl = useIntl();
+
+ const pageSize = 20;
+
+ const [store, updateStore] = createStore({ ...emptyStore });
+
+ createEffect(on(() => account?.isKeyLookupDone, (v) => {
+ if (v && account?.publicKey) {
+ updateStore(() => ({ ...emptyStore }));
+ fetchBookmarks(account.publicKey);
+ }
+ }));
+
+ onCleanup(() => {
+ updateStore(() => ({ ...emptyStore }));
+ });
+
+ const fetchBookmarks = (pubkey: string | undefined, until = 0) => {
+ if (store.fetchingInProgress || !pubkey) return;
+
+ const subId = `bookmark_feed_${until}_${APP_ID}`;
+
+ const unsub = subscribeTo(subId, (type, _, content) => {
+ if (type === 'EOSE') {
+ const reposts = parseEmptyReposts(store.page);
+ const ids = Object.keys(reposts);
+
+ if (ids.length === 0) {
+ savePage(store.page);
+ unsub();
+ return;
+ }
+
+ updateStore('reposts', () => reposts);
+
+ fetchReposts(ids);
+
+ unsub();
+ return;
+ }
+
+ if (type === 'EVENT') {
+ content && updatePage(content);
+ }
+ });
+
+ updateStore('fetchingInProgress', () => true);
+ getUserFeed(pubkey, pubkey, subId, 'bookmarks', until, pageSize, store.offset);
+ }
+
+ const fetchNextPage = () => since > 0 && fetchBookmarks(account?.publicKey, since);
+
+ const fetchReposts = (ids: string[]) => {
+ const subId = `bookmark_reposts_${APP_ID}`;
+
+ const unsub = subscribeTo(subId, (type, _, content) => {
+ if (type === 'EOSE') {
+ savePage(store.page);
+ unsub();
+ return;
+ }
+
+ if (type === 'EVENT') {
+ const repostId = (content as NostrNoteContent).id;
+ const reposts = store.reposts || {};
+ const parent = store.page.messages.find(m => m.id === reposts[repostId]);
+
+ if (parent) {
+ updateStore('page', 'messages', (msg) => msg.id === parent.id, 'content', () => JSON.stringify(content));
+ }
+
+ return;
+ }
+ });
+
+ getEvents(account?.publicKey, ids, subId);
+ };
+
+ const updatePage = (content: NostrEventContent) => {
+ if (content.kind === Kind.Metadata) {
+ const user = content as NostrUserContent;
+
+ updateStore('page', 'users',
+ (usrs) => ({ ...usrs, [user.pubkey]: { ...user } })
+ );
+ return;
+ }
+
+ if ([Kind.Text, Kind.Repost].includes(content.kind)) {
+ const message = content as NostrNoteContent;
+
+ updateStore('page', 'messages',
+ (msgs) => [ ...msgs, { ...message }]
+ );
+
+ return;
+ }
+
+ if (content.kind === Kind.NoteStats) {
+ const statistic = content as NostrStatsContent;
+ const stat = JSON.parse(statistic.content);
+
+ updateStore('page', 'postStats',
+ (stats) => ({ ...stats, [stat.event_id]: { ...stat } })
+ );
+ return;
+ }
+
+ if (content.kind === Kind.Mentions) {
+ const mentionContent = content as NostrMentionContent;
+ const mention = JSON.parse(mentionContent.content);
+
+ updateStore('page', 'mentions',
+ (mentions) => ({ ...mentions, [mention.id]: { ...mention } })
+ );
+ return;
+ }
+
+ if (content.kind === Kind.NoteActions) {
+ const noteActionContent = content as NostrNoteActionsContent;
+ const noteActions = JSON.parse(noteActionContent.content) as NoteActions;
+
+ updateStore('page', 'noteActions',
+ (actions) => ({ ...actions, [noteActions.event_id]: { ...noteActions } })
+ );
+ return;
+ }
+
+ if (content.kind === Kind.FeedRange) {
+ const noteActionContent = content as NostrFeedRange;
+ const range = JSON.parse(noteActionContent.content) as PageRange;
+
+ updateStore('pageRange', () => ({ ...range }));
+ since = range.until;
+ return;
+ }
+
+ if (content.kind === Kind.LinkMetadata) {
+ const metadata = JSON.parse(content.content);
+
+ const data = metadata.resources[0];
+ if (!data) {
+ return;
+ }
+
+ const preview = {
+ url: data.url,
+ title: data.md_title,
+ description: data.md_description,
+ mediaType: data.mimetype,
+ contentType: data.mimetype,
+ images: [data.md_image],
+ favicons: [data.icon_url],
+ };
+
+ setLinkPreviews(() => ({ [data.url]: preview }));
+ return;
+ }
+ };
+
+ const savePage = (page: FeedPage) => {
+ const newPosts = convertToNotes(page);
+
+ saveNotes(newPosts);
+ };
+
+ const saveNotes = (newNotes: PrimalNote[]) => {
+ const notesToAdd = newNotes.filter(n => !store.noteIds.includes(n.post.id));
+
+ const lastTimestamp = store.pageRange.since;
+ const offset = notesToAdd.reduce((acc, n) => n.post.created_at === lastTimestamp ? acc+1 : acc, 0);
+
+ const ids = notesToAdd.map(m => m.post.id)
+
+ ids.length > 0 && updateStore('noteIds', () => [...ids]);
+
+ updateStore('offset', () => offset);
+ updateStore('notes', (notes) => [ ...notes, ...notesToAdd ]);
+ updateStore('page', () => ({
+ messages: [],
+ users: {},
+ postStats: {},
+ mentions: {},
+ noteActions: {},
+ }));
+ updateStore('fetchingInProgress', () => false);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {intl.formatMessage(tBookmarks.noBookmarks)}
+
+
+
+
+ {(note) =>
+
+ }
+
+
+
+
+
+
+ {}
+
+
+
+ >
+ );
+}
+
+export default Bookmarks;
diff --git a/src/palette.scss b/src/palette.scss
index b7dcf51..8bf1040 100644
--- a/src/palette.scss
+++ b/src/palette.scss
@@ -34,6 +34,7 @@
--active-zap: #ffa02f;
--active-liked: #f800c1;
--active-reposted: #66e205;
+ --active-bookmarked: #0C7DD8;
--background-modal: #00000070;
--profile-indicator-border: var(--background-site);
@@ -104,6 +105,7 @@
--active-zap: #ffa02f;
--active-liked: #f800c1;
--active-reposted: #66e205;
+ --active-bookmarked: #0C7DD8;
--background-modal: #00000070;
--profile-indicator-border: var(--background-site);
@@ -173,6 +175,7 @@
--active-zap: #ffa02f;
--active-liked: #CA079F;
--active-reposted: #52CE0A;
+ --active-bookmarked: #0C7DD8;
--background-modal: #f5f5f570;
--profile-indicator-border: var(--accent);
@@ -243,6 +246,7 @@
--active-zap: #ffa02f;
--active-liked: #CA079F;
--active-reposted: #52CE0A;
+ --active-bookmarked: #0C7DD8;
--background-modal: #f5f5f570;
--profile-indicator-border: var(--accent);
diff --git a/src/translations.ts b/src/translations.ts
index acc3830..4ecfa8b 100644
--- a/src/translations.ts
+++ b/src/translations.ts
@@ -714,6 +714,11 @@ export const navBar = {
defaultMessage: 'Messages',
description: 'Label for the nav bar item link to Messages page',
},
+ bookmarks: {
+ id: 'navbar.bookmarks',
+ defaultMessage: 'Bookmarks',
+ description: 'Label for the nav bar item link to Bookmarks page',
+ },
notifications: {
id: 'navbar.notifications',
defaultMessage: 'Notifications',
@@ -2070,7 +2075,7 @@ export const followWarning = {
description: {
id: 'followWarning.description',
defaultMessage: 'If you continue, you will end up following just one nostr account. Are you sure you want to continue?',
- description: 'Explanation of what happens when follow erro occurs',
+ description: 'Explanation of what happens when follow error occurs',
},
confirm: {
id: 'followWarning.confirm',
@@ -2084,6 +2089,61 @@ export const followWarning = {
},
};
+export const bookmarks = {
+ pageTitle: {
+ id: 'bookmarks.pageTitle',
+ defaultMessage: 'Bookmarks',
+ description: 'Bookmarks page title',
+ },
+ noBookmarks: {
+ id: 'bookmarks.noBookmarks',
+ defaultMessage: 'You don\'t have any bookmarks',
+ description: 'No bookmarks caption',
+ },
+ confirm: {
+ title: {
+ id: 'bookmarks.confirm.title',
+ defaultMessage: 'Saving First Bookmark',
+ description: 'Follow error modal title',
+ },
+ description: {
+ id: 'bookmarks.confirm.description',
+ defaultMessage: 'You are about to save your first public bookmark. These bookmarks can be seen by other nostr users. Do you wish to continue?',
+ description: 'Explanation of what happens when bookmark error occurs',
+ },
+ confirm: {
+ id: 'bookmarks.confirm.confirm',
+ defaultMessage: 'Save Bookmark',
+ description: 'Confirm forgot pin action',
+ },
+ abort: {
+ id: 'bookmarks.confirm.abort',
+ defaultMessage: 'Cancel',
+ description: 'Abort forgot pin action',
+ },
+ titleZero: {
+ id: 'bookmarks.confirm.title',
+ defaultMessage: 'Removing Last Bookmark',
+ description: 'Follow error modal title',
+ },
+ descriptionZero: {
+ id: 'bookmarks.confirm.description',
+ defaultMessage: 'You are about to remove your last public bookmark. Do you wish to continue?',
+ description: 'Explanation of what happens when bookmark error occurs',
+ },
+ confirmZero: {
+ id: 'bookmarks.confirm.confirm',
+ defaultMessage: 'Remove Bookmark',
+ description: 'Confirm forgot pin action',
+ },
+ abortZero: {
+ id: 'bookmarks.confirm.abort',
+ defaultMessage: 'Cancel',
+ description: 'Abort forgot pin action',
+ },
+ }
+}
+
export const lnInvoice = {
pay: {
id: 'lnInvoice.pay',
diff --git a/src/types/primal.d.ts b/src/types/primal.d.ts
index a959a24..075ce9c 100644
--- a/src/types/primal.d.ts
+++ b/src/types/primal.d.ts
@@ -200,6 +200,13 @@ export type PrimalUserRelays = {
tags: string[][],
};
+export type NostrBookmarks = {
+ kind: Kind.Bookmarks,
+ content: string,
+ created_at?: number,
+ tags: string[][],
+};
+
export type NostrEventContent =
NostrNoteContent |
NostrUserContent |
@@ -227,7 +234,8 @@ export type NostrEventContent =
NostrUserFollwerCounts |
NostrUserZaps |
NostrSuggestedUsers |
- PrimalUserRelays;
+ PrimalUserRelays |
+ NostrBookmarks;
export type NostrEvent = [
type: "EVENT",
@@ -703,3 +711,9 @@ export type LnbcInvoice = {
expiry: number,
route_hints: LnbcRouteHint[],
};
+
+export type PageRange = {
+ since: number,
+ until: number,
+ order_by: string,
+};