From f79632fd612dc89aee1c653183846bcf217c66a9 Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Sat, 23 Aug 2025 22:11:23 -0400 Subject: [PATCH 1/3] enabled share in all notes --- .../note-header/chips/share-chip.tsx | 12 +-- .../note-header/share-button-header.tsx | 51 ++++++++--- .../editor-area/text-selection-popover.tsx | 11 +++ .../toolbar/buttons/share-button.tsx | 85 ++++++++++++++++--- .../components/toolbar/utils/pdf-export.ts | 14 ++- 5 files changed, 140 insertions(+), 33 deletions(-) diff --git a/apps/desktop/src/components/editor-area/note-header/chips/share-chip.tsx b/apps/desktop/src/components/editor-area/note-header/chips/share-chip.tsx index 124308904..c41cf8602 100644 --- a/apps/desktop/src/components/editor-area/note-header/chips/share-chip.tsx +++ b/apps/desktop/src/components/editor-area/note-header/chips/share-chip.tsx @@ -9,11 +9,11 @@ interface ShareChipProps { export function ShareChip({ isVeryNarrow = false }: ShareChipProps) { const [open, setOpen] = useState(false); - const { hasEnhancedNote, handleOpenStateChange } = useShareLogic(); + const { hasShareableNote, shareTitle, handleOpenStateChange } = useShareLogic(); const handleOpenChange = (newOpen: boolean) => { setOpen(newOpen); - if (hasEnhancedNote) { + if (hasShareableNote) { handleOpenStateChange(newOpen); } }; @@ -35,7 +35,7 @@ export function ShareChip({ isVeryNarrow = false }: ShareChipProps) { align="start" sideOffset={7} > - {hasEnhancedNote ? : } + {hasShareableNote ? : } ); @@ -44,16 +44,16 @@ export function ShareChip({ isVeryNarrow = false }: ShareChipProps) { function SharePlaceholderContent() { return (
-
Share Enhanced Note
+
Share Note

- Enhanced Note Required + No Content Available

- Complete your meeting to generate an enhanced note, then share it via PDF, email, Obsidian, and more. + Start taking notes or record a meeting to have content available for sharing via PDF, email, Obsidian, and more.

diff --git a/apps/desktop/src/components/editor-area/note-header/share-button-header.tsx b/apps/desktop/src/components/editor-area/note-header/share-button-header.tsx index aa1467c32..2bcbf4eaf 100644 --- a/apps/desktop/src/components/editor-area/note-header/share-button-header.tsx +++ b/apps/desktop/src/components/editor-area/note-header/share-button-header.tsx @@ -23,6 +23,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { useSession } from "@hypr/utils/contexts"; import { exportToPDF, getAvailableThemes, type ThemeName } from "../../toolbar/utils/pdf-export"; + + interface DirectAction { id: "copy"; title: string; @@ -51,11 +53,13 @@ interface ObsidianFolder { } const exportHandlers = { - copy: async (session: Session): Promise => { + copy: async (session: Session, isViewingRaw: boolean = false): Promise => { try { let textToCopy = ""; - if (session.enhanced_memo_html) { + if (isViewingRaw && session.raw_memo_html) { + textToCopy = html2md(session.raw_memo_html); + } else if (!isViewingRaw && session.enhanced_memo_html) { textToCopy = html2md(session.enhanced_memo_html); } else if (session.raw_memo_html) { textToCopy = html2md(session.raw_memo_html); @@ -71,8 +75,8 @@ const exportHandlers = { } }, - pdf: async (session: Session, theme: ThemeName = "default"): Promise => { - const path = await exportToPDF(session, theme); + pdf: async (session: Session, theme: ThemeName = "default", isViewingRaw: boolean = false): Promise => { + const path = await exportToPDF(session, theme, isViewingRaw); if (path) { await message(`Meeting summary saved to your 'Downloads' folder ("${path}")`); } @@ -82,10 +86,13 @@ const exportHandlers = { email: async ( session: Session, sessionParticipants?: Array<{ full_name: string | null; email: string | null }>, + isViewingRaw: boolean = false, ): Promise => { let bodyContent = "Here is the meeting summary: \n\n"; - if (session.enhanced_memo_html) { + if (isViewingRaw && session.raw_memo_html) { + bodyContent += html2md(session.raw_memo_html); + } else if (!isViewingRaw && session.enhanced_memo_html) { bodyContent += html2md(session.enhanced_memo_html); } else if (session.raw_memo_html) { bodyContent += html2md(session.raw_memo_html); @@ -126,6 +133,7 @@ const exportHandlers = { sessionTags: Tag[] | undefined, sessionParticipants: Array<{ full_name: string | null }> | undefined, includeTranscript: boolean = false, + isViewingRaw: boolean = false, ): Promise => { const [baseFolder, apiKey, baseUrl] = await Promise.all([ obsidianCommands.getBaseFolder(), @@ -148,7 +156,14 @@ const exportHandlers = { finalPath = await join(selectedFolder, filename); } - let convertedMarkdown = session.enhanced_memo_html ? html2md(session.enhanced_memo_html) : ""; + let convertedMarkdown = ""; + if (isViewingRaw && session.raw_memo_html) { + convertedMarkdown = html2md(session.raw_memo_html); + } else if (!isViewingRaw && session.enhanced_memo_html) { + convertedMarkdown = html2md(session.enhanced_memo_html); + } else if (session.raw_memo_html) { + convertedMarkdown = html2md(session.raw_memo_html); + } // Add transcript if requested if (includeTranscript && session.words && session.words.length > 0) { @@ -328,13 +343,20 @@ export function useShareLogic() { const { userId } = useHypr(); const param = useParams({ from: "/app/note/$id", shouldThrow: true }); const session = useSession(param.id, (s) => s.session); + const showRaw = useSession(param.id, (s) => s.showRaw); const [expandedId, setExpandedId] = useState(null); const [selectedObsidianFolder, setSelectedObsidianFolder] = useState("default"); const [selectedPdfTheme, setSelectedPdfTheme] = useState("default"); const [includeTranscript, setIncludeTranscript] = useState(false); const [copySuccess, setCopySuccess] = useState(false); + + // Determine what content is available and what to share const hasEnhancedNote = !!session?.enhanced_memo_html; + const hasRawNote = !!session?.raw_memo_html; + const hasShareableNote = hasEnhancedNote || hasRawNote; + const isViewingRaw = showRaw || !hasEnhancedNote; + const shareTitle = isViewingRaw ? "Share Raw Note" : "Share Enhanced Note"; const isObsidianConfigured = useQuery({ queryKey: ["integration", "obsidian", "enabled"], @@ -431,17 +453,17 @@ export function useShareLogic() { let result: ExportResult | null = null; if (optionId === "copy") { - result = await exportHandlers.copy(session); + result = await exportHandlers.copy(session, isViewingRaw); } else if (optionId === "pdf") { - result = await exportHandlers.pdf(session, selectedPdfTheme); + result = await exportHandlers.pdf(session, selectedPdfTheme, isViewingRaw); } else if (optionId === "email") { try { // fetch participants directly, bypassing cache const freshParticipants = await dbCommands.sessionListParticipants(param.id); - result = await exportHandlers.email(session, freshParticipants); + result = await exportHandlers.email(session, freshParticipants, isViewingRaw); } catch (participantError) { console.warn("Failed to fetch participants, sending email without them:", participantError); - result = await exportHandlers.email(session, undefined); + result = await exportHandlers.email(session, undefined, isViewingRaw); } } else if (optionId === "obsidian") { sessionTags.refetch(); @@ -466,6 +488,7 @@ export function useShareLogic() { sessionTagsData, sessionParticipantsData, includeTranscript, + isViewingRaw, ); } @@ -531,6 +554,9 @@ export function useShareLogic() { return { session, hasEnhancedNote, + hasShareableNote, + shareTitle, + isViewingRaw, expandedId, selectedObsidianFolder, setSelectedObsidianFolder, @@ -552,8 +578,9 @@ export function useShareLogic() { } // Reusable Share Content Component -export function SharePopoverContent() { +export function SharePopoverContent({ shareTitle: propShareTitle }: { shareTitle?: string }) { const { + shareTitle, expandedId, selectedObsidianFolder, setSelectedObsidianFolder, @@ -572,7 +599,7 @@ export function SharePopoverContent() { return (
-
Share Enhanced Note
+
{propShareTitle || shareTitle}
{/* Direct action buttons */} {directActions.map((action) => { diff --git a/apps/desktop/src/components/editor-area/text-selection-popover.tsx b/apps/desktop/src/components/editor-area/text-selection-popover.tsx index 67255e9ff..8ef4c1874 100644 --- a/apps/desktop/src/components/editor-area/text-selection-popover.tsx +++ b/apps/desktop/src/components/editor-area/text-selection-popover.tsx @@ -49,6 +49,17 @@ export function TextSelectionPopover( } const range = sel.getRangeAt(0); + const commonAncestor = range.commonAncestorContainer; + const container = commonAncestor.nodeType === Node.TEXT_NODE + ? commonAncestor.parentElement + : commonAncestor as Element; + + // block popover in transcript area + if (container?.closest('.tiptap-transcript')) { + setSelection(null); + return; + } + const rect = range.getBoundingClientRect(); const selectedText = sel.toString().trim(); diff --git a/apps/desktop/src/components/toolbar/buttons/share-button.tsx b/apps/desktop/src/components/toolbar/buttons/share-button.tsx index 63bc0374d..d1ea69230 100644 --- a/apps/desktop/src/components/toolbar/buttons/share-button.tsx +++ b/apps/desktop/src/components/toolbar/buttons/share-button.tsx @@ -4,7 +4,7 @@ import { join } from "@tauri-apps/api/path"; import { message } from "@tauri-apps/plugin-dialog"; import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; import { openPath, openUrl } from "@tauri-apps/plugin-opener"; -import { BookText, Check, ChevronDown, ChevronUp, Copy, FileText, HelpCircle, Mail, Share } from "lucide-react"; +import { Check, ChevronDown, ChevronUp, Copy, FileText, HelpCircle, Mail, Share } from "lucide-react"; import { useState } from "react"; import { useHypr } from "@/contexts"; @@ -25,6 +25,42 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { useSession } from "@hypr/utils/contexts"; import { exportToPDF, getAvailableThemes, type ThemeName } from "../utils/pdf-export"; +const ObsidianIcon = ({ size = 20 }: { size?: number }) => ( + + {/* Main base shape */} + + {/* Bottom left highlight - creates internal structure */} + + {/* Top right highlight */} + + {/* Top left highlight */} + + {/* Bottom right highlight */} + + +); + export function ShareButton() { const param = useParams({ from: "/app/note/$id", shouldThrow: false }); return param ? : null; @@ -34,6 +70,7 @@ function ShareButtonInNote() { const { userId } = useHypr(); const param = useParams({ from: "/app/note/$id", shouldThrow: true }); const session = useSession(param.id, (s) => s.session); + const showRaw = useSession(param.id, (s) => s.showRaw); const [open, setOpen] = useState(false); const [expandedId, setExpandedId] = useState(null); @@ -41,7 +78,13 @@ function ShareButtonInNote() { const [selectedPdfTheme, setSelectedPdfTheme] = useState("default"); const [includeTranscript, setIncludeTranscript] = useState(false); const [copySuccess, setCopySuccess] = useState(false); + + // Determine what content is available and what to share const hasEnhancedNote = !!session?.enhanced_memo_html; + const hasRawNote = !!session?.raw_memo_html; + const hasShareableNote = hasEnhancedNote || hasRawNote; + const isViewingRaw = showRaw || !hasEnhancedNote; + const shareTitle = isViewingRaw ? "Share Raw Note" : "Share Enhanced Note"; const isObsidianConfigured = useQuery({ queryKey: ["integration", "obsidian", "enabled"], @@ -103,7 +146,7 @@ function ShareButtonInNote() { ? { id: "obsidian", title: "Obsidian", - icon: , + icon: , description: "Export to Obsidian", docsUrl: "https://docs.hyprnote.com/sharing#obsidian", } @@ -160,17 +203,17 @@ function ShareButtonInNote() { let result: ExportResult | null = null; if (optionId === "copy") { - result = await exportHandlers.copy(session); + result = await exportHandlers.copy(session, isViewingRaw); } else if (optionId === "pdf") { - result = await exportHandlers.pdf(session, selectedPdfTheme); + result = await exportHandlers.pdf(session, selectedPdfTheme, isViewingRaw); } else if (optionId === "email") { try { // fetch participants directly, bypassing cache const freshParticipants = await dbCommands.sessionListParticipants(param.id); - result = await exportHandlers.email(session, freshParticipants); + result = await exportHandlers.email(session, freshParticipants, isViewingRaw); } catch (participantError) { console.warn("Failed to fetch participants, sending email without them:", participantError); - result = await exportHandlers.email(session, undefined); + result = await exportHandlers.email(session, undefined, isViewingRaw); } } else if (optionId === "obsidian") { sessionTags.refetch(); @@ -195,6 +238,7 @@ function ShareButtonInNote() { sessionTagsData, sessionParticipantsData, includeTranscript, + isViewingRaw, ); } @@ -244,7 +288,7 @@ function ShareButtonInNote() {
diff --git a/apps/desktop/src/components/editor-area/note-header/share-button-header.tsx b/apps/desktop/src/components/editor-area/note-header/share-button-header.tsx index 2bcbf4eaf..495734a0e 100644 --- a/apps/desktop/src/components/editor-area/note-header/share-button-header.tsx +++ b/apps/desktop/src/components/editor-area/note-header/share-button-header.tsx @@ -23,8 +23,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { useSession } from "@hypr/utils/contexts"; import { exportToPDF, getAvailableThemes, type ThemeName } from "../../toolbar/utils/pdf-export"; - - interface DirectAction { id: "copy"; title: string; @@ -350,7 +348,7 @@ export function useShareLogic() { const [selectedPdfTheme, setSelectedPdfTheme] = useState("default"); const [includeTranscript, setIncludeTranscript] = useState(false); const [copySuccess, setCopySuccess] = useState(false); - + // Determine what content is available and what to share const hasEnhancedNote = !!session?.enhanced_memo_html; const hasRawNote = !!session?.raw_memo_html; diff --git a/apps/desktop/src/components/editor-area/text-selection-popover.tsx b/apps/desktop/src/components/editor-area/text-selection-popover.tsx index 8ef4c1874..ca33da34c 100644 --- a/apps/desktop/src/components/editor-area/text-selection-popover.tsx +++ b/apps/desktop/src/components/editor-area/text-selection-popover.tsx @@ -50,12 +50,12 @@ export function TextSelectionPopover( const range = sel.getRangeAt(0); const commonAncestor = range.commonAncestorContainer; - const container = commonAncestor.nodeType === Node.TEXT_NODE - ? commonAncestor.parentElement + const container = commonAncestor.nodeType === Node.TEXT_NODE + ? commonAncestor.parentElement : commonAncestor as Element; // block popover in transcript area - if (container?.closest('.tiptap-transcript')) { + if (container?.closest(".tiptap-transcript")) { setSelection(null); return; } diff --git a/apps/desktop/src/components/toolbar/buttons/share-button.tsx b/apps/desktop/src/components/toolbar/buttons/share-button.tsx index d1ea69230..1e5f3ba0a 100644 --- a/apps/desktop/src/components/toolbar/buttons/share-button.tsx +++ b/apps/desktop/src/components/toolbar/buttons/share-button.tsx @@ -26,36 +26,36 @@ import { useSession } from "@hypr/utils/contexts"; import { exportToPDF, getAvailableThemes, type ThemeName } from "../utils/pdf-export"; const ObsidianIcon = ({ size = 20 }: { size?: number }) => ( - {/* Main base shape */} - {/* Bottom left highlight - creates internal structure */} - {/* Top right highlight */} - {/* Top left highlight */} - {/* Bottom right highlight */} - @@ -78,7 +78,7 @@ function ShareButtonInNote() { const [selectedPdfTheme, setSelectedPdfTheme] = useState("default"); const [includeTranscript, setIncludeTranscript] = useState(false); const [copySuccess, setCopySuccess] = useState(false); - + // Determine what content is available and what to share const hasEnhancedNote = !!session?.enhanced_memo_html; const hasRawNote = !!session?.raw_memo_html; diff --git a/apps/desktop/src/components/toolbar/utils/pdf-export.ts b/apps/desktop/src/components/toolbar/utils/pdf-export.ts index d150b40ca..43dd6e4ba 100644 --- a/apps/desktop/src/components/toolbar/utils/pdf-export.ts +++ b/apps/desktop/src/components/toolbar/utils/pdf-export.ts @@ -448,7 +448,7 @@ export const exportToPDF = async ( } else { contentHtml = "No content available"; } - + const segments = htmlToStructuredText(contentHtml); for (const segment of segments) {