a tool for shared writing and social publishing

Merge branch 'main' into feature/at-mentions

+220 -53
+56
actions/deleteLeaflet.ts
··· 16 16 export async function deleteLeaflet(permission_token: PermissionToken) { 17 17 const client = await pool.connect(); 18 18 const db = drizzle(client); 19 + 20 + // Get the current user's identity 21 + let identity = await getIdentityData(); 22 + 23 + // Check publication and document ownership in one query 24 + let { data: tokenData } = await supabaseServerClient 25 + .from("permission_tokens") 26 + .select( 27 + ` 28 + id, 29 + leaflets_in_publications(publication, publications!inner(identity_did)), 30 + leaflets_to_documents(document, documents!inner(uri)) 31 + `, 32 + ) 33 + .eq("id", permission_token.id) 34 + .single(); 35 + 36 + if (tokenData) { 37 + // Check if leaflet is in a publication 38 + const leafletInPubs = tokenData.leaflets_in_publications || []; 39 + if (leafletInPubs.length > 0) { 40 + if (!identity) { 41 + throw new Error( 42 + "Unauthorized: You must be logged in to delete a leaflet in a publication", 43 + ); 44 + } 45 + const isOwner = leafletInPubs.some( 46 + (pub: any) => pub.publications.identity_did === identity.atp_did, 47 + ); 48 + if (!isOwner) { 49 + throw new Error( 50 + "Unauthorized: You must own the publication to delete this leaflet", 51 + ); 52 + } 53 + } 54 + 55 + // Check if there's a standalone published document 56 + const leafletDocs = tokenData.leaflets_to_documents || []; 57 + if (leafletDocs.length > 0) { 58 + if (!identity) { 59 + throw new Error( 60 + "Unauthorized: You must be logged in to delete a published leaflet", 61 + ); 62 + } 63 + for (let leafletDoc of leafletDocs) { 64 + const docUri = leafletDoc.documents?.uri; 65 + // Extract the DID from the document URI (format: at://did:plc:xxx/...) 66 + if (docUri && identity.atp_did && !docUri.includes(identity.atp_did)) { 67 + throw new Error( 68 + "Unauthorized: You must own the published document to delete this leaflet", 69 + ); 70 + } 71 + } 72 + } 73 + } 74 + 19 75 await db.transaction(async (tx) => { 20 76 let [token] = await tx 21 77 .select()
+14 -6
app/(home-pages)/home/LeafletList/LeafletOptions.tsx
··· 86 86 const pubStatus = useLeafletPublicationStatus(); 87 87 const toaster = useToaster(); 88 88 const { setArchived } = useArchiveMutations(); 89 + const { identity } = useIdentityData(); 89 90 const tokenId = pubStatus?.token.id; 90 91 const itemType = pubStatus?.draftInPublication ? "Draft" : "Leaflet"; 92 + 93 + // Check if this is a published post/document and if user is the owner 94 + const isPublishedPostOwner = 95 + !!identity?.atp_did && !!pubStatus?.documentUri?.includes(identity.atp_did); 96 + const canDelete = !pubStatus?.documentUri || isPublishedPostOwner; 91 97 92 98 return ( 93 99 <> ··· 133 139 <ArchiveSmall /> 134 140 {!props.archived ? " Archive" : "Unarchive"} {itemType} 135 141 </MenuItem> 136 - <DeleteForeverMenuItem 137 - onSelect={(e) => { 138 - e.preventDefault(); 139 - props.setState("areYouSure"); 140 - }} 141 - /> 142 + {canDelete && ( 143 + <DeleteForeverMenuItem 144 + onSelect={(e) => { 145 + e.preventDefault(); 146 + props.setState("areYouSure"); 147 + }} 148 + /> 149 + )} 142 150 </> 143 151 ); 144 152 };
+1 -1
components/ActionBar/Publications.tsx
··· 151 151 <PubListEmptyIllo /> 152 152 </div> 153 153 <div className="pt-1 font-bold">Publish on AT Proto</div> 154 - {identity && !identity.atp_did ? ( 154 + {identity && identity.atp_did ? ( 155 155 // has ATProto account and no pubs 156 156 <> 157 157 <div className="pb-2 text-secondary text-xs">
+62 -44
package-lock.json
··· 48 48 "inngest": "^3.40.1", 49 49 "ioredis": "^5.6.1", 50 50 "katex": "^0.16.22", 51 + "l": "^0.6.0", 51 52 "linkifyjs": "^4.2.0", 52 53 "luxon": "^3.7.2", 53 54 "multiformats": "^13.3.2", 54 - "next": "16.0.3", 55 + "next": "^16.0.7", 55 56 "pg": "^8.16.3", 56 57 "prosemirror-commands": "^1.5.2", 57 58 "prosemirror-inputrules": "^1.4.0", ··· 59 60 "prosemirror-model": "^1.21.0", 60 61 "prosemirror-schema-basic": "^1.2.2", 61 62 "prosemirror-state": "^1.4.3", 62 - "react": "19.2.0", 63 + "react": "19.2.1", 63 64 "react-aria-components": "^1.8.0", 64 65 "react-day-picker": "^9.3.0", 65 66 "react-dom": "19.2.0", ··· 2734 2735 } 2735 2736 }, 2736 2737 "node_modules/@next/env": { 2737 - "version": "16.0.3", 2738 - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz", 2739 - "integrity": "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ==" 2738 + "version": "16.0.7", 2739 + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz", 2740 + "integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==", 2741 + "license": "MIT" 2740 2742 }, 2741 2743 "node_modules/@next/eslint-plugin-next": { 2742 2744 "version": "16.0.3", ··· 2804 2806 } 2805 2807 }, 2806 2808 "node_modules/@next/swc-darwin-arm64": { 2807 - "version": "16.0.3", 2808 - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.3.tgz", 2809 - "integrity": "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg==", 2809 + "version": "16.0.7", 2810 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz", 2811 + "integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==", 2810 2812 "cpu": [ 2811 2813 "arm64" 2812 2814 ], 2815 + "license": "MIT", 2813 2816 "optional": true, 2814 2817 "os": [ 2815 2818 "darwin" ··· 2819 2822 } 2820 2823 }, 2821 2824 "node_modules/@next/swc-darwin-x64": { 2822 - "version": "16.0.3", 2823 - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.3.tgz", 2824 - "integrity": "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg==", 2825 + "version": "16.0.7", 2826 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz", 2827 + "integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==", 2825 2828 "cpu": [ 2826 2829 "x64" 2827 2830 ], 2831 + "license": "MIT", 2828 2832 "optional": true, 2829 2833 "os": [ 2830 2834 "darwin" ··· 2834 2838 } 2835 2839 }, 2836 2840 "node_modules/@next/swc-linux-arm64-gnu": { 2837 - "version": "16.0.3", 2838 - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.3.tgz", 2839 - "integrity": "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A==", 2841 + "version": "16.0.7", 2842 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz", 2843 + "integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==", 2840 2844 "cpu": [ 2841 2845 "arm64" 2842 2846 ], 2847 + "license": "MIT", 2843 2848 "optional": true, 2844 2849 "os": [ 2845 2850 "linux" ··· 2849 2854 } 2850 2855 }, 2851 2856 "node_modules/@next/swc-linux-arm64-musl": { 2852 - "version": "16.0.3", 2853 - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.3.tgz", 2854 - "integrity": "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg==", 2857 + "version": "16.0.7", 2858 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz", 2859 + "integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==", 2855 2860 "cpu": [ 2856 2861 "arm64" 2857 2862 ], 2863 + "license": "MIT", 2858 2864 "optional": true, 2859 2865 "os": [ 2860 2866 "linux" ··· 2864 2870 } 2865 2871 }, 2866 2872 "node_modules/@next/swc-linux-x64-gnu": { 2867 - "version": "16.0.3", 2868 - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.3.tgz", 2869 - "integrity": "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ==", 2873 + "version": "16.0.7", 2874 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz", 2875 + "integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==", 2870 2876 "cpu": [ 2871 2877 "x64" 2872 2878 ], 2879 + "license": "MIT", 2873 2880 "optional": true, 2874 2881 "os": [ 2875 2882 "linux" ··· 2879 2886 } 2880 2887 }, 2881 2888 "node_modules/@next/swc-linux-x64-musl": { 2882 - "version": "16.0.3", 2883 - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.3.tgz", 2884 - "integrity": "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA==", 2889 + "version": "16.0.7", 2890 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz", 2891 + "integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==", 2885 2892 "cpu": [ 2886 2893 "x64" 2887 2894 ], 2895 + "license": "MIT", 2888 2896 "optional": true, 2889 2897 "os": [ 2890 2898 "linux" ··· 2894 2902 } 2895 2903 }, 2896 2904 "node_modules/@next/swc-win32-arm64-msvc": { 2897 - "version": "16.0.3", 2898 - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.3.tgz", 2899 - "integrity": "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g==", 2905 + "version": "16.0.7", 2906 + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz", 2907 + "integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==", 2900 2908 "cpu": [ 2901 2909 "arm64" 2902 2910 ], 2911 + "license": "MIT", 2903 2912 "optional": true, 2904 2913 "os": [ 2905 2914 "win32" ··· 2909 2918 } 2910 2919 }, 2911 2920 "node_modules/@next/swc-win32-x64-msvc": { 2912 - "version": "16.0.3", 2913 - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.3.tgz", 2914 - "integrity": "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg==", 2921 + "version": "16.0.7", 2922 + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz", 2923 + "integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==", 2915 2924 "cpu": [ 2916 2925 "x64" 2917 2926 ], 2927 + "license": "MIT", 2918 2928 "optional": true, 2919 2929 "os": [ 2920 2930 "win32" ··· 13360 13370 "json-buffer": "3.0.1" 13361 13371 } 13362 13372 }, 13373 + "node_modules/l": { 13374 + "version": "0.6.0", 13375 + "resolved": "https://registry.npmjs.org/l/-/l-0.6.0.tgz", 13376 + "integrity": "sha512-rB5disIyfKRBQ1xcedByHCcAmPWy2NPnjWo5u4mVVIPtathROHyfHjkloqSBT49mLnSRnupkpoIUOFCL7irCVQ==", 13377 + "license": "MIT" 13378 + }, 13363 13379 "node_modules/language-subtag-registry": { 13364 13380 "version": "0.3.23", 13365 13381 "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", ··· 15108 15124 } 15109 15125 }, 15110 15126 "node_modules/next": { 15111 - "version": "16.0.3", 15112 - "resolved": "https://registry.npmjs.org/next/-/next-16.0.3.tgz", 15113 - "integrity": "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w==", 15127 + "version": "16.0.7", 15128 + "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", 15129 + "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", 15130 + "license": "MIT", 15114 15131 "dependencies": { 15115 - "@next/env": "16.0.3", 15132 + "@next/env": "16.0.7", 15116 15133 "@swc/helpers": "0.5.15", 15117 15134 "caniuse-lite": "^1.0.30001579", 15118 15135 "postcss": "8.4.31", ··· 15125 15142 "node": ">=20.9.0" 15126 15143 }, 15127 15144 "optionalDependencies": { 15128 - "@next/swc-darwin-arm64": "16.0.3", 15129 - "@next/swc-darwin-x64": "16.0.3", 15130 - "@next/swc-linux-arm64-gnu": "16.0.3", 15131 - "@next/swc-linux-arm64-musl": "16.0.3", 15132 - "@next/swc-linux-x64-gnu": "16.0.3", 15133 - "@next/swc-linux-x64-musl": "16.0.3", 15134 - "@next/swc-win32-arm64-msvc": "16.0.3", 15135 - "@next/swc-win32-x64-msvc": "16.0.3", 15145 + "@next/swc-darwin-arm64": "16.0.7", 15146 + "@next/swc-darwin-x64": "16.0.7", 15147 + "@next/swc-linux-arm64-gnu": "16.0.7", 15148 + "@next/swc-linux-arm64-musl": "16.0.7", 15149 + "@next/swc-linux-x64-gnu": "16.0.7", 15150 + "@next/swc-linux-x64-musl": "16.0.7", 15151 + "@next/swc-win32-arm64-msvc": "16.0.7", 15152 + "@next/swc-win32-x64-msvc": "16.0.7", 15136 15153 "sharp": "^0.34.4" 15137 15154 }, 15138 15155 "peerDependencies": { ··· 16321 16338 } 16322 16339 }, 16323 16340 "node_modules/react": { 16324 - "version": "19.2.0", 16325 - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", 16326 - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", 16341 + "version": "19.2.1", 16342 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", 16343 + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", 16344 + "license": "MIT", 16327 16345 "engines": { 16328 16346 "node": ">=0.10.0" 16329 16347 }
+3 -2
package.json
··· 58 58 "inngest": "^3.40.1", 59 59 "ioredis": "^5.6.1", 60 60 "katex": "^0.16.22", 61 + "l": "^0.6.0", 61 62 "linkifyjs": "^4.2.0", 62 63 "luxon": "^3.7.2", 63 64 "multiformats": "^13.3.2", 64 - "next": "16.0.3", 65 + "next": "^16.0.7", 65 66 "pg": "^8.16.3", 66 67 "prosemirror-commands": "^1.5.2", 67 68 "prosemirror-inputrules": "^1.4.0", ··· 69 70 "prosemirror-model": "^1.21.0", 70 71 "prosemirror-schema-basic": "^1.2.2", 71 72 "prosemirror-state": "^1.4.3", 72 - "react": "19.2.0", 73 + "react": "19.2.1", 73 74 "react-aria-components": "^1.8.0", 74 75 "react-day-picker": "^9.3.0", 75 76 "react-dom": "19.2.0",
+9
supabase/database.types.ts
··· 1158 1158 } 1159 1159 Returns: Database["public"]["CompositeTypes"]["pull_result"] 1160 1160 } 1161 + search_tags: { 1162 + Args: { 1163 + search_query: string 1164 + } 1165 + Returns: { 1166 + name: string 1167 + document_count: number 1168 + }[] 1169 + } 1161 1170 } 1162 1171 Enums: { 1163 1172 rsvp_status: "GOING" | "NOT_GOING" | "MAYBE"
+30
supabase/migrations/20251204120000_add_tags_support.sql
··· 1 + -- Create GIN index on the tags array in the JSONB data field 2 + -- This allows efficient querying of documents by tag 3 + CREATE INDEX IF NOT EXISTS idx_documents_tags 4 + ON "public"."documents" USING gin ((data->'tags')); 5 + 6 + -- Function to search and aggregate tags from documents 7 + -- This does the aggregation in the database rather than fetching all documents 8 + CREATE OR REPLACE FUNCTION search_tags(search_query text) 9 + RETURNS TABLE (name text, document_count bigint) AS $$ 10 + BEGIN 11 + RETURN QUERY 12 + SELECT 13 + LOWER(tag::text) as name, 14 + COUNT(DISTINCT d.uri) as document_count 15 + FROM 16 + "public"."documents" d, 17 + jsonb_array_elements_text(d.data->'tags') as tag 18 + WHERE 19 + CASE 20 + WHEN search_query = '' THEN true 21 + ELSE LOWER(tag::text) LIKE '%' || search_query || '%' 22 + END 23 + GROUP BY 24 + LOWER(tag::text) 25 + ORDER BY 26 + COUNT(DISTINCT d.uri) DESC, 27 + LOWER(tag::text) ASC 28 + LIMIT 20; 29 + END; 30 + $$ LANGUAGE plpgsql STABLE;
+7
supabase/migrations/20251204130000_add_tags_to_drafts.sql
··· 1 + -- Add tags column to leaflets_in_publications for publication drafts 2 + ALTER TABLE "public"."leaflets_in_publications" 3 + ADD COLUMN "tags" text[] DEFAULT ARRAY[]::text[]; 4 + 5 + -- Add tags column to leaflets_to_documents for standalone document drafts 6 + ALTER TABLE "public"."leaflets_to_documents" 7 + ADD COLUMN "tags" text[] DEFAULT ARRAY[]::text[];
+38
supabase/migrations/20251204140000_update_pull_data_with_tags.sql
··· 1 + set check_function_bodies = off; 2 + 3 + CREATE OR REPLACE FUNCTION public.pull_data(token_id uuid, client_group_id text) 4 + RETURNS pull_result 5 + LANGUAGE plpgsql 6 + AS $function$DECLARE 7 + result pull_result; 8 + BEGIN 9 + -- Get client group data as JSON array 10 + SELECT json_agg(row_to_json(rc)) 11 + FROM replicache_clients rc 12 + WHERE rc.client_group = client_group_id 13 + INTO result.client_groups; 14 + 15 + -- Get facts as JSON array 16 + SELECT json_agg(row_to_json(f)) 17 + FROM permission_tokens pt, 18 + get_facts(pt.root_entity) f 19 + WHERE pt.id = token_id 20 + INTO result.facts; 21 + 22 + -- Get publication data - try leaflets_in_publications first, then leaflets_to_documents 23 + SELECT json_agg(row_to_json(lip)) 24 + FROM leaflets_in_publications lip 25 + WHERE lip.leaflet = token_id 26 + INTO result.publications; 27 + 28 + -- If no publication data found, try leaflets_to_documents (for standalone documents) 29 + IF result.publications IS NULL THEN 30 + SELECT json_agg(row_to_json(ltd)) 31 + FROM leaflets_to_documents ltd 32 + WHERE ltd.leaflet = token_id 33 + INTO result.publications; 34 + END IF; 35 + 36 + RETURN result; 37 + END;$function$ 38 + ;