diff --git a/src/github/feed/boost-helpers.js b/src/github/feed/boost-helpers.js index adba887..5e30cfa 100644 --- a/src/github/feed/boost-helpers.js +++ b/src/github/feed/boost-helpers.js @@ -59,27 +59,3 @@ export const issueIcon = html.span({ classList: "n-github-issue-icon" }); issueIcon.innerHTML = ``; - -export function timeAgo(timestampInSeconds) { - const nowInSeconds = Math.floor(Date.now() / 1000); - const secondsAgo = nowInSeconds - timestampInSeconds; - - let value, unit; - - if (secondsAgo < 60) { - value = secondsAgo; - unit = "second"; - } else if (secondsAgo < 3600) { - value = Math.floor(secondsAgo / 60); - unit = "minute"; - } else if (secondsAgo < 86400) { - value = Math.floor(secondsAgo / 3600); - unit = "hour"; - } else { - value = Math.floor(secondsAgo / 86400); - unit = "day"; - } - - const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); - return rtf.format(-value, unit); -} diff --git a/src/github/feed/boost.js b/src/github/feed/boost.js index c5ed178..ab32ef2 100644 --- a/src/github/feed/boost.js +++ b/src/github/feed/boost.js @@ -6,6 +6,7 @@ import { } from "../../helpers/local-cache.js"; import { getIconComponent } from "../github-connect.js"; import { logger } from "../../helpers/logger.js"; +import { timeAgo } from "../../helpers/time.js"; import { fetchGithubTitle, @@ -14,7 +15,6 @@ import { pickRandomEmoji, prIcon, spanSpaced, - timeAgo, typeToReactions, } from "./boost-helpers.js"; diff --git a/src/helpers/local-cache.js b/src/helpers/local-cache.js index 23cfe73..1d574a3 100644 --- a/src/helpers/local-cache.js +++ b/src/helpers/local-cache.js @@ -1,7 +1,7 @@ import { mergeSettings } from "./utils.js"; export const defaultSettings = { - version: 1, + version: 2, debug: { log: true, namespace: "[N]", @@ -12,7 +12,7 @@ export const defaultSettings = { nip07: { useRelays: false, }, - openNostr: "https://nosta.me", + openNostr: "https://nost.at", }, lightsatsSettings: { apiKey: "", diff --git a/src/helpers/nostr.js b/src/helpers/nostr.js index c05c6c4..f58b568 100644 --- a/src/helpers/nostr.js +++ b/src/helpers/nostr.js @@ -306,15 +306,17 @@ export async function requestSigningFromNip07(messageParams) { * @param {Object} event - The event to insert * @returns {number} - The index where the event was inserted */ -function insertEventIntoAscendingList(sortedArray, event) { +function insertEventIntoDescendingList(sortedArray, event) { const [idx, found] = binarySearch(sortedArray, (b) => { if (event.id === b.id) { return 0; } + if (event.created_at === b.created_at) { return -1; } - return event.created_at - b.created_at; + + return b.created_at - event.created_at; }); if (!found) { @@ -355,7 +357,7 @@ export function fetchLatestNotes({ pubkey, relays, callback }) { notesSet.add(event.id); - const index = insertEventIntoAscendingList(latestNotes, event); + const index = insertEventIntoDescendingList(latestNotes, event); console.log(`inserted at index: ${index}`); diff --git a/src/helpers/time.js b/src/helpers/time.js new file mode 100644 index 0000000..49814be --- /dev/null +++ b/src/helpers/time.js @@ -0,0 +1,23 @@ +export function timeAgo(timestampInSeconds) { + const nowInSeconds = Math.floor(Date.now() / 1000); + const secondsAgo = nowInSeconds - timestampInSeconds; + + let value, unit; + + if (secondsAgo < 60) { + value = secondsAgo; + unit = "second"; + } else if (secondsAgo < 3600) { + value = Math.floor(secondsAgo / 60); + unit = "minute"; + } else if (secondsAgo < 86400) { + value = Math.floor(secondsAgo / 3600); + unit = "hour"; + } else { + value = Math.floor(secondsAgo / 86400); + unit = "day"; + } + + const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); + return rtf.format(-value, unit); +} diff --git a/src/settings/settings.js b/src/settings/settings.js index 5d3309f..3411f10 100644 --- a/src/settings/settings.js +++ b/src/settings/settings.js @@ -99,6 +99,7 @@ async function settingsPage() { state.nostrSettings.nip07.useRelays = defaultSettings.nostrSettings.nip07.useRelays; state.nostrSettings.relays = [...defaultSettings.nostrSettings.relays]; + state.nostrSettings.openNostr = defaultSettings.nostrSettings.openNostr; // Lightsats settings state.lightsatsSettings.apiKey = defaultSettings.lightsatsSettings.apiKey; @@ -153,6 +154,9 @@ async function settingsPage() { toggleNIP07Settings(); }; + document.getElementById("open-nostr").onchange = (e) => + (state.nostrSettings.openNostr = e.target.value); + document.getElementById("nip07-relays").onchange = (e) => (state.nostrSettings.nip07.useRelays = e.target.checked); diff --git a/src/twitter/profile/notes.css b/src/twitter/profile/notes.css index b9eac00..09f11a8 100644 --- a/src/twitter/profile/notes.css +++ b/src/twitter/profile/notes.css @@ -15,20 +15,37 @@ } .n-tw-note { + position: relative; +} + +.n-tw-note-content { background-color: rgb(208, 173, 240); - padding: 10px; + padding: 15px 10px 10px 10px; border-radius: 5px; margin: 10px; color: black; } -.n-tw-note img { +.n-tw-note .ago { + position: absolute; + text-decoration: none; + top: 12px; + right: 22px; + font-size: small; + color: blueviolet; +} + +.n-tw-note .ago:hover { + text-decoration: underline; +} + +.n-tw-note-content img { max-width: 100%; height: auto; margin: 5px 0 5px 0; } -.n-tw-note a { +.n-tw-note-content a { color: white; text-decoration: underline; -} \ No newline at end of file +} diff --git a/src/twitter/profile/profile.js b/src/twitter/profile/profile.js index 672c7eb..1d46446 100644 --- a/src/twitter/profile/profile.js +++ b/src/twitter/profile/profile.js @@ -28,10 +28,10 @@ import { wrapInputTooltip } from "../../components/tooltip/tooltip-wrapper.js"; import { createTwitterButton, - setNostrMode, + setupNostrMode, updateFollowButton, } from "./twitter-helpers.js"; -import { wrapCheckbox } from "../../components/checkbox/checkbox-wrapper.js"; +import { setupNostrProfileLink } from "./twitter-helpers.js"; async function twitterProfilePage() { const settings = await getLocalSettings(); @@ -161,43 +161,15 @@ async function twitterProfilePage() { 'nav[aria-label="Profile timelines"]', ); - const nostrModeOnclick = async (checked) => { - if (checked) { - timelineNavbar.style.display = "none"; - } else { - timelineNavbar.style.display = "flex"; - } - - await setNostrMode({ - enabled: checked, - pageUserPubkey, - pageUserWriteRelays, - openNostr: settings.nostrSettings.openNostr, - }); - }; - - const enableNostrModeCheckbox = wrapCheckbox({ - input: html.input({ - type: "checkbox", - id: "n-tw-enable-nostr-mode", - checked: true, - }), - onclick: nostrModeOnclick, - text: "Enable Nostr Mode", + const { enableNostrModeCheckbox, notesSection } = setupNostrMode({ + timelineNavbar, + pageUserPubkey, + pageUserWriteRelays, + settings, }); - timelineNavbar.insertAdjacentElement("beforebegin", enableNostrModeCheckbox); - - enableNostrModeCheckbox.insertAdjacentElement( - "afterend", - html.div({ - id: "n-tw-notes-section", - style: [["display", "none"]], - }), - ); - // nostr mode is on by default - nostrModeOnclick(true); + enableNostrModeCheckbox.insertAdjacentElement("afterend", notesSection); if (pageUserPubkey === nostrizeUserPubkey) { log("current page user is the nostrize user"); @@ -213,29 +185,7 @@ async function twitterProfilePage() { relays: nostrizeUserRelays, }); - // handle container is the div that contains the username and the handle - const usernamePanel = document.querySelector("div[data-testid='UserName']"); - const handleContainer = - usernamePanel?.childNodes[0]?.childNodes[0]?.childNodes[0]?.childNodes[1]; - const handle = handleContainer?.childNodes[0]; - const handleContent = handle.textContent; - handle.style.display = "none"; - - // nostr profile link - if (handleContainer) { - gui.prepend( - handleContainer, - wrapInputTooltip({ - id: "n-tw-nostr-profile-button", - input: html.link({ - text: handleContent, - href: `${settings.nostrSettings.openNostr}/${pageUserPubkey}`, - targetBlank: true, - }), - tooltipText: `User is on Nostr. Click to open Nostr profile.`, - }), - ); - } + const handleContainer = setupNostrProfileLink(settings, pageUserPubkey); // follows of the account const pageUserFollowSubscription = getFollowSet({ diff --git a/src/twitter/profile/twitter-helpers.js b/src/twitter/profile/twitter-helpers.js index b190af9..61492a5 100644 --- a/src/twitter/profile/twitter-helpers.js +++ b/src/twitter/profile/twitter-helpers.js @@ -2,7 +2,10 @@ import * as gui from "../../imgui-dom/gui.js"; import * as html from "../../imgui-dom/html.js"; import { setupModal } from "../../components/common.js"; import { fetchLatestNotes } from "../../helpers/nostr.js"; -import { delay } from "../../helpers/utils.js"; +import { wrapCheckbox } from "../../components/checkbox/checkbox-wrapper.js"; +import { wrapInputTooltip } from "../../components/tooltip/tooltip-wrapper.js"; +import { timeAgo } from "../../helpers/time.js"; +import { nip19 } from "nostr-tools"; export const updateFollowButton = (button, emojiIcon) => { button.childNodes[0].childNodes[0].textContent = emojiIcon; @@ -93,24 +96,19 @@ function processNoteContent(content, openNostr) { .replace(/\n/g, "
"); } -export async function setNostrMode({ +async function setNostrMode({ enabled, pageUserPubkey, pageUserWriteRelays, openNostr, }) { if (enabled) { - const trimNote = shortenNote(500); - const latestNotes = fetchLatestNotes({ pubkey: pageUserPubkey, relays: pageUserWriteRelays, callback: (event, index) => { notesSection.insertBefore( - html.div({ - classList: "n-tw-note", - innerHTML: trimNote(processNoteContent(event.content, openNostr)), - }), + createNote(event, openNostr), notesSection.children[index], ); }, @@ -123,130 +121,111 @@ export async function setNostrMode({ // filter out notes that are replies to nostr events .filter((e) => !e.tags.some((tag) => tag[0] === "e")) .forEach((note) => { - notesSection.appendChild( - html.div({ - classList: "n-tw-note", - innerHTML: trimNote(processNoteContent(note.content, openNostr)), - }), - ); + notesSection.appendChild(createNote(note, openNostr)); }); attachLoadMoreListeners(); } else { const notesSection = gui.gebid("n-tw-notes-section"); notesSection.style.display = "none"; - - // TODO: set twitter section display to flex } } -export async function addAccountNotesTab( - accountPubkey, - writeRelays, - openNostr, -) { - const tablist = document.querySelector( - "[data-testid='ScrollSnap-SwipeableList']", - ).childNodes[0]; - - // deselect posts tab - const posts = tablist.childNodes[0]; - posts.querySelector("span").nextElementSibling.style.backgroundColor = - "unset"; - - // reset account notes tab - const accountNotes = posts.cloneNode(true); - accountNotes.id = "n-tw-account-notes"; - - // remove link - accountNotes.childNodes[0].setAttribute("href", "javascript:void(0);"); - - gui.prepend(tablist, accountNotes); - - const notesTitle = accountNotes.querySelector("span"); - notesTitle.textContent = "Notes"; - - const selectionIndicator = notesTitle.nextElementSibling; - selectionIndicator.style.backgroundColor = "unset"; - - await notesTabClicked(); +const createNote = (event, openNostr) => { + const eventId = nip19.noteEncode(event.id); + + return html.div({ + classList: "n-tw-note", + children: [ + html.div({ + classList: "n-tw-note-content", + innerHTML: shortenNote(500)( + processNoteContent(event.content, openNostr), + ), + }), + html.link({ + classList: "ago", + text: timeAgo(event.created_at), + href: `${openNostr}/${eventId}`, + targetBlank: true, + }), + ], + }); +}; - const trimNote = shortenNote(500); +export function setupNostrMode({ + timelineNavbar, + pageUserPubkey, + pageUserWriteRelays, + settings, +}) { + const nostrModeOnclick = async (checked) => { + if (checked) { + timelineNavbar.style.display = "none"; + } else { + timelineNavbar.style.display = "flex"; + } - let toHide = document.querySelector('[aria-labelledby="accessible-list-1"]'); + await setNostrMode({ + enabled: checked, + pageUserPubkey, + pageUserWriteRelays, + openNostr: settings.nostrSettings.openNostr, + }); + }; - while (!toHide) { - await delay(100); - toHide = document.querySelector('[aria-labelledby="accessible-list-1"]'); - } + const enableNostrModeCheckbox = wrapCheckbox({ + input: html.input({ + type: "checkbox", + id: "n-tw-enable-nostr-mode", + checked: true, + }), + onclick: nostrModeOnclick, + text: "Enable Nostr Mode", + }); - toHide.style.display = "none"; + timelineNavbar.insertAdjacentElement("beforebegin", enableNostrModeCheckbox); const notesSection = html.div({ id: "n-tw-notes-section", - classList: "n-tw-notes-section", - }); - - toHide.insertAdjacentElement("afterend", notesSection); - - const latestNotes = fetchLatestNotes({ - pubkey: accountPubkey, - relays: writeRelays, - callback: (event, index) => { - if (isAccountNotesTabSelected) { - notesSection.insertBefore( - html.div({ - classList: "tw-note", - innerHTML: trimNote(processNoteContent(event.content, openNostr)), - }), - notesSection.children[index], - ); - } - }, + style: [["display", "none"]], }); - let isAccountNotesTabSelected = false; - - tablist.childNodes.forEach((node) => { - if (node.id === "n-tw-account-notes") { - return; - } - - node.addEventListener("click", () => { - isAccountNotesTabSelected = false; - notesSection.style.display = "none"; - selectionIndicator.style.backgroundColor = "unset"; - toHide.style.display = "flex"; - }); - }); + enableNostrModeCheckbox.insertAdjacentElement("afterend", notesSection); - const notesTabClicked = async (e) => { - e?.preventDefault(); + // nostr mode is on by default + nostrModeOnclick(true); - isAccountNotesTabSelected = true; + return { enableNostrModeCheckbox, notesSection }; +} - selectionIndicator.style.backgroundColor = "rgb(130, 80, 223)"; +export function setupNostrProfileLink(settings, pageUserPubkey) { + const usernamePanel = document.querySelector("div[data-testid='UserName']"); + const handleContainer = + usernamePanel?.childNodes[0]?.childNodes[0]?.childNodes[0]?.childNodes[1]; + const handle = handleContainer?.childNodes[0]; - // clear notes section - notesSection.innerHTML = ""; + if (!handle) { + return handleContainer; + } - latestNotes.forEach((note) => { - notesSection.appendChild( - html.div({ - classList: "tw-note", - innerHTML: trimNote(processNoteContent(note.content, openNostr)), - }), - ); + const handleContent = handle.textContent; + + if (handleContainer) { + const nostrProfileLink = wrapInputTooltip({ + id: "n-tw-nostr-profile-button", + input: html.link({ + text: handleContent, + href: `${settings.nostrSettings.openNostr}/${pageUserPubkey}`, + targetBlank: true, + }), + tooltipText: `User is on Nostr. Click to open Nostr profile.`, }); - }; - accountNotes.addEventListener("click", notesTabClicked); + handle.style.display = "none"; - accountNotes.addEventListener("mouseenter", () => { - accountNotes.style.backgroundColor = "rgb(130, 80, 223)"; - }); + gui.prepend(handleContainer, nostrProfileLink); + } - accountNotes.addEventListener("mouseleave", () => { - accountNotes.style.backgroundColor = "unset"; - }); + return handleContainer; }