Skip to content

Commit 83e6950

Browse files
authored
Merge pull request #32 from typelets/feature/public-notes
feat: add public notes sharing with security hardening
2 parents a6374b1 + 49a803c commit 83e6950

File tree

19 files changed

+1800
-61
lines changed

19 files changed

+1800
-61
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ Available on iOS and Android with the same powerful features and encryption.
8888
- 🌐 **Cross-platform** - Responsive web app that works seamlessly on desktop, tablet, and mobile
8989
- 🖨️ **Print support** - Clean printing with proper formatting
9090

91+
### 🌐 Public Notes (Sharing)
92+
- 🔗 **Shareable links** - Publish any note with a unique, unguessable URL
93+
- 👤 **Optional author attribution** - Add your name or publish anonymously
94+
- 🎨 **Full formatting preserved** - Rich text, code blocks, images, and diagrams render beautifully
95+
- 📑 **Table of contents** - Collapsible TOC for easy navigation
96+
- 🌙 **Theme toggle** - Readers can switch between light and dark mode
97+
- 📱 **Mobile-friendly** - Responsive design for all screen sizes
98+
- 🔄 **Auto-sync** - Changes to your note automatically update the public version
99+
-**Instant unpublish** - Remove public access at any time (hard delete)
100+
- 🛡️ **Security hardened** - DOMPurify sanitization, rate limiting, no internal IDs exposed
101+
102+
> ⚠️ **Important:** Publishing a note bypasses end-to-end encryption. An unencrypted copy is stored on our servers and anyone with the link can view it. Use this feature only for content you intend to share publicly.
103+
91104
### ⚡ Executable Code Blocks
92105

93106
![Execute Code Demo](https://github.com/typelets/typelets-app/blob/main/assets/execute-code-demo.gif)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,11 @@
8888
"@tiptap/react": "^3.4.4",
8989
"@tiptap/starter-kit": "^3.4.4",
9090
"@tiptap/suggestion": "^3.4.4",
91+
"@types/dompurify": "^3.2.0",
9192
"@unhead/react": "^2.0.17",
9293
"class-variance-authority": "^0.7.1",
9394
"clsx": "^2.1.1",
95+
"dompurify": "^3.3.0",
9496
"highlight.js": "^11.11.1",
9597
"lowlight": "^3.3.0",
9698
"lucide-react": "^0.544.0",

pnpm-lock.yaml

Lines changed: 56 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { codeExecutionService } from '@/services/codeExecutionService';
1919
import { clearUserEncryptionData } from '@/lib/encryption';
2020
import { MonacoThemeProvider } from '@/contexts/MonacoThemeContext';
2121
import MainApp from '@/pages/MainApp';
22+
import PublicNotePage from '@/pages/PublicNotePage';
2223

2324
function AppContent() {
2425
const { getToken, isSignedIn } = useAuth();
@@ -46,6 +47,7 @@ function AppContent() {
4647

4748
const isSignInPage = window.location.pathname === '/sign-in';
4849
const isSignUpPage = window.location.pathname === '/sign-up';
50+
const isPublicNotePage = window.location.pathname.startsWith('/p/');
4951

5052
// Check if user wants to force web version
5153
const urlParams = new URLSearchParams(window.location.search);
@@ -58,10 +60,15 @@ function AppContent() {
5860
localStorage.setItem('forceWebVersion', 'true');
5961
}
6062

61-
if (isMobileDevice && !isSignedIn && !forceWeb) {
63+
if (isMobileDevice && !isSignedIn && !forceWeb && !isPublicNotePage) {
6264
return <MobileAppDownload />;
6365
}
6466

67+
// Public note pages don't require authentication
68+
if (isPublicNotePage) {
69+
return <PublicNotePage />;
70+
}
71+
6572
if (isSignInPage) {
6673
return (
6774
<div className="flex min-h-screen items-center justify-center">

src/components/editor/index.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
PanelRightClose,
1313
PanelRightOpen,
1414
RefreshCw,
15+
Globe,
1516
} from 'lucide-react';
1617
import { Button } from '@/components/ui/button';
1718
import { ButtonGroup } from '@/components/ui/button-group';
@@ -39,6 +40,7 @@ import { editorStyles } from './config/editor-styles';
3940
import { EmptyState } from '@/components/editor/Editor/EmptyState';
4041
import { Toolbar } from '@/components/editor/Editor/Toolbar';
4142
import MoveNoteModal from '@/components/editor/modals/MoveNoteModal';
43+
import PublishNoteModal from '@/components/editor/modals/PublishNoteModal';
4244
import FileUpload from '@/components/editor/FileUpload';
4345
import { StatusBar } from '@/components/editor/Editor/StatusBar';
4446
import { useEditorState } from '@/components/editor/hooks/useEditorState';
@@ -101,6 +103,8 @@ interface NoteEditorProps {
101103
onUnhideNote: (noteId: string) => void;
102104
onRefreshNote?: (noteId: string) => void;
103105
onSelectNote?: (note: Note) => void; // Navigate to a linked note
106+
onPublishNote?: (noteId: string, authorName?: string) => Promise<unknown>;
107+
onUnpublishNote?: (noteId: string) => Promise<boolean>;
104108
userId?: string;
105109
isNotesPanelOpen?: boolean;
106110
onToggleNotesPanel?: () => void;
@@ -128,6 +132,8 @@ export default function Index({
128132
onUnhideNote,
129133
onRefreshNote,
130134
onSelectNote,
135+
onPublishNote,
136+
onUnpublishNote,
131137
userId = 'current-user',
132138
isNotesPanelOpen,
133139
onToggleNotesPanel,
@@ -140,6 +146,7 @@ export default function Index({
140146
onWsDisconnect,
141147
}: NoteEditorProps) {
142148
const [isMoveModalOpen, setIsMoveModalOpen] = useState(false);
149+
const [isPublishModalOpen, setIsPublishModalOpen] = useState(false);
143150
const [showAttachments, setShowAttachments] = useState(false);
144151
const [isUploading, setIsUploading] = useState(false);
145152
const [uploadProgress, setUploadProgress] = useState(0);
@@ -1084,6 +1091,12 @@ export default function Index({
10841091
<Printer className="mr-2 h-4 w-4" />
10851092
Print
10861093
</DropdownMenuItem>
1094+
{onPublishNote && onUnpublishNote && (
1095+
<DropdownMenuItem onClick={() => setIsPublishModalOpen(true)}>
1096+
<Globe className="mr-2 h-4 w-4" />
1097+
{note.isPublished ? 'Manage' : 'Publish'}
1098+
</DropdownMenuItem>
1099+
)}
10871100
<DropdownMenuItem onClick={() => onArchiveNote(note.id)}>
10881101
<Archive className="mr-2 h-4 w-4" />
10891102
Archive
@@ -1204,6 +1217,16 @@ export default function Index({
12041217
noteTitle={note.title}
12051218
/>
12061219
)}
1220+
1221+
{note && onPublishNote && onUnpublishNote && (
1222+
<PublishNoteModal
1223+
isOpen={isPublishModalOpen}
1224+
onClose={() => setIsPublishModalOpen(false)}
1225+
note={note}
1226+
onPublish={onPublishNote}
1227+
onUnpublish={onUnpublishNote}
1228+
/>
1229+
)}
12071230
</div>
12081231
</>
12091232
);

0 commit comments

Comments
 (0)