this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor into multiple files

alice 73ef85b6 cf9edb05

+257 -237
+3 -6
.eslintrc.cjs
··· 1 1 module.exports = { 2 2 env: { browser: true, es2020: true }, 3 - extends: [ 4 - 'eslint:recommended', 5 - 'plugin:@typescript-eslint/recommended', 6 - 'plugin:react-hooks/recommended', 7 - ], 3 + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended'], 8 4 parser: '@typescript-eslint/parser', 9 5 parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 6 plugins: ['react-refresh'], 11 7 rules: { 12 8 'react-refresh/only-export-components': 'warn', 9 + '@typescript-eslint/no-non-null-assertion': 'off', 13 10 }, 14 - } 11 + };
+3 -2
package.json
··· 10 10 "preview": "vite preview" 11 11 }, 12 12 "dependencies": { 13 + "@atproto/api": "^0.2.7", 13 14 "react": "^18.2.0", 15 + "react-calendar-heatmap": "^1.9.0", 14 16 "react-dom": "^18.2.0", 15 - "@atproto/api": "^0.2.7", 16 - "react-calendar-heatmap": "^1.9.0", 17 17 "react-tooltip": "^5.11.1" 18 18 }, 19 19 "devDependencies": { 20 20 "@types/react": "^18.0.37", 21 + "@types/react-calendar-heatmap": "^1.6.3", 21 22 "@types/react-dom": "^18.0.11", 22 23 "@typescript-eslint/eslint-plugin": "^5.59.0", 23 24 "@typescript-eslint/parser": "^5.59.0",
+9
pnpm-lock.yaml
··· 21 21 '@types/react': 22 22 specifier: ^18.0.37 23 23 version: 18.0.37 24 + '@types/react-calendar-heatmap': 25 + specifier: ^1.6.3 26 + version: 1.6.3 24 27 '@types/react-dom': 25 28 specifier: ^18.0.11 26 29 version: 18.0.11 ··· 509 512 510 513 /@types/prop-types@15.7.5: 511 514 resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} 515 + dev: true 516 + 517 + /@types/react-calendar-heatmap@1.6.3: 518 + resolution: {integrity: sha512-C6hN9Nl0NBqeJEkivwwf/bvWNoPwpwGSNABwYl0yHD20rClSJqRgYoGPKDIn8QqoYHGJsTFJdFbBAZRPlJ/+qg==} 519 + dependencies: 520 + '@types/react': 18.0.37 512 521 dev: true 513 522 514 523 /@types/react-dom@18.0.11:
+145
src/App.tsx
··· 1 + import { useState, useEffect, useMemo } from 'react'; 2 + import CalendarHeatmap from 'react-calendar-heatmap'; 3 + import { Tooltip } from 'react-tooltip'; 4 + import * as bsky from '@atproto/api'; 5 + const { BskyAgent } = bsky; 6 + import type { AtpSessionEvent, AtpSessionData } from '@atproto/api'; 7 + import { getData } from './atproto.tsx'; 8 + 9 + export const App = () => { 10 + const [posts, setPosts] = useState<any>([]); 11 + const [max, setMax] = useState<number>(0); 12 + const [createdAt, setCreatedAt] = useState<Date>(); 13 + const [actor, setActor] = useState<string>(''); 14 + const [updated, setUpdated] = useState<string>(actor); 15 + const [username, setUsername] = useState<string>(''); 16 + const [password, setPassword] = useState<string>(''); 17 + const [loginPressed, setLoginPressed] = useState<boolean>(false); 18 + const [loggedIn, setLoggedIn] = useState<boolean>(false); 19 + const [isLoading, setIsLoading] = useState<boolean>(false); 20 + const [session, setSession] = useState<AtpSessionData>(); 21 + const [agent, setAgent] = useState<bsky.BskyAgent>(); 22 + 23 + useMemo(() => { 24 + const agent = new BskyAgent({ 25 + service: 'https://bsky.social', 26 + persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => { 27 + setSession(sess!); 28 + }, 29 + }); 30 + 31 + setAgent(agent); 32 + }, []); 33 + 34 + useEffect(() => { 35 + let ignore = false; 36 + setPosts([]); 37 + 38 + if (loggedIn) { 39 + setIsLoading(true); 40 + getData(agent!, session!, updated).then((data) => { 41 + if (!ignore) { 42 + setPosts(data.data); 43 + setMax(data.max); 44 + setCreatedAt(new Date(data.createdAt)); 45 + setIsLoading(false); 46 + } 47 + }); 48 + } 49 + 50 + return () => { 51 + ignore = true; 52 + }; 53 + }, [agent, session, updated, loggedIn]); 54 + 55 + useEffect(() => { 56 + let ignore = false; 57 + if (loginPressed && !ignore) { 58 + agent! 59 + .login({ 60 + identifier: username, 61 + password: password, 62 + }) 63 + .then(() => { 64 + setLoggedIn(true); 65 + }); 66 + } 67 + return () => { 68 + ignore = true; 69 + }; 70 + }, [agent, username, password, loginPressed]); 71 + 72 + return ( 73 + <div> 74 + <h1>Bluesky Posts Heatmap</h1> 75 + {loggedIn === false ? ( 76 + <> 77 + <div id="loginMessage">Please log in</div> 78 + <br /> 79 + </> 80 + ) : null} 81 + <div id="login"> 82 + Username:&nbsp; 83 + <input type="text" placeholder="username" onChange={(e) => setUsername(e.target.value)} value={username} /> 84 + <br /> 85 + Password:&nbsp; 86 + <input type="password" placeholder="password" onChange={(e) => setPassword(e.target.value)} value={password} /> 87 + <input 88 + type="button" 89 + value="login" 90 + onClick={() => { 91 + setLoginPressed(true); 92 + setActor(username); 93 + }} 94 + disabled={loggedIn} 95 + /> 96 + <br /> 97 + <br /> 98 + </div> 99 + <div id="actor"> 100 + 🦋&nbsp; 101 + <input type="text" placeholder="Bluesky username" onChange={(e) => setActor(e.target.value)} value={actor} /> 102 + <input 103 + type="button" 104 + value="Get heatmap" 105 + onClick={() => { 106 + setMax(0); 107 + setUpdated(actor); 108 + }} 109 + disabled={!loggedIn || isLoading} 110 + /> 111 + </div> 112 + <div> 113 + <br /> 114 + </div> 115 + {isLoading ? <div>Loading...</div> : null} 116 + {posts.length === 0 ? null : ( 117 + <> 118 + <CalendarHeatmap 119 + startDate={createdAt} 120 + endDate={new Date()} 121 + values={posts} 122 + classForValue={(value) => { 123 + if (!value) { 124 + return 'color-empty'; 125 + } 126 + // return `color-github-${value.count > 0 ? Math.ceil((value.count / max) * 4) : 0}`; 127 + return `color-custom-${value.count > 0 ? Math.ceil((value.count / max) * 17) : 0}`; 128 + }} 129 + tooltipDataAttrs={(value: any) => { 130 + return { 131 + 'data-tooltip-id': 'my-tooltip', 132 + 'data-tooltip-content': value.date !== null ? `${value.date} has ${value.count} posts` : 'no posts', 133 + 'data-tooltip-place': 'top', 134 + }; 135 + }} 136 + showWeekdayLabels={true} 137 + gutterSize={1} 138 + showOutOfRangeDays={true} 139 + /> 140 + <Tooltip id="my-tooltip" /> 141 + </> 142 + )} 143 + </div> 144 + ); 145 + };
+67
src/atproto.tsx
··· 1 + import * as bsky from '@atproto/api'; 2 + import type { AtpSessionData } from '@atproto/api'; 3 + import { getUserCreatedAt, paginateAll } from './helpers.tsx'; 4 + 5 + export const getData = async (agent: bsky.BskyAgent, session: AtpSessionData, actor = '') => { 6 + await agent.resumeSession(session); 7 + 8 + if (actor === '') { 9 + actor = agent.session!.did; 10 + } else { 11 + actor = (await agent.getProfile({ actor })).data.did; 12 + } 13 + 14 + // source: https://github.com/bluesky-social/atproto/blob/efb1cac2bfc8ccb77c0f4910ad9f3de7370fbebb/packages/bsky/tests/views/author-feed.test.ts#L94 15 + const paginator = async (cursor?: string) => { 16 + const res = await agent.getAuthorFeed({ 17 + actor: actor, 18 + cursor, 19 + limit: 100, 20 + }); 21 + return res.data; 22 + }; 23 + 24 + const paginatedAll = await paginateAll(paginator); 25 + 26 + const posts: object[] = []; 27 + 28 + paginatedAll.forEach((res) => { 29 + if (typeof res.feed[0] !== 'undefined') { 30 + posts.push( 31 + ...res.feed.map((e) => ({ 32 + text: (e.post.record as any).text, 33 + uri: e.post.uri.replace('app.bsky.feed.', '').replace('at://', 'https://staging.bsky.app/profile/'), 34 + likeCount: e.post.likeCount, 35 + did: e.post.author.did, 36 + handle: e.post.author.handle, 37 + isOwn: e.post.author.did === actor, 38 + repostCount: e.post.repostCount, 39 + isRepost: e.post.repostCount === 0 ? false : true, 40 + createdAt: (e.post.record as any).createdAt, 41 + })), 42 + ); 43 + } 44 + }); 45 + 46 + const groupedPosts = posts.reduce((acc: any, obj: any) => { 47 + const key = obj.createdAt.slice(0, 10); 48 + if (!acc[key]) { 49 + acc[key] = { date: key, count: 0 }; 50 + } 51 + if (obj.isOwn) acc[key].count++; 52 + return acc; 53 + }, {}); 54 + 55 + // i don't need the outer object, i just need an array with the values 56 + const data = Object.values(groupedPosts); 57 + 58 + const max = Math.max(...data.map((o: any) => o.count)); 59 + 60 + const createdAt = await getUserCreatedAt(actor); 61 + 62 + return { 63 + data, 64 + max, 65 + createdAt, 66 + }; 67 + };
+29
src/helpers.tsx
··· 1 + export const shiftDate = (date: Date, numDays: number) => { 2 + const newDate = new Date(date); 3 + newDate.setDate(newDate.getDate() + numDays); 4 + return newDate; 5 + }; 6 + 7 + // source: https://github.com/bluesky-social/atproto/blob/efb1cac2bfc8ccb77c0f4910ad9f3de7370fbebb/packages/bsky/tests/_util.ts#L314 8 + export const paginateAll = async <T extends { cursor?: string }>( 9 + fn: (cursor?: string) => Promise<T>, 10 + limit = Infinity, 11 + ): Promise<T[]> => { 12 + const results: T[] = []; 13 + let cursor; 14 + do { 15 + const res = await fn(cursor); 16 + results.push(res); 17 + cursor = res.cursor; 18 + } while (cursor && results.length < limit); 19 + return results; 20 + }; 21 + 22 + export const getUserCreatedAt = async (actor: string) => { 23 + // source: https://github.com/mimonelu/klearsky/blob/079746c1c1a03d3a9f0961bdb69bb223dcb106c3/src/composables/main-state.ts#L98 24 + const log = await fetch(`https://plc.directory/${actor}/log/audit`); 25 + const logJson = await log.json(); 26 + const createdAt = logJson[0]?.createdAt; 27 + 28 + return createdAt; 29 + };
+1 -229
src/main.tsx
··· 1 - import React from 'react'; 2 - import { useState, useEffect } from 'react'; 3 1 import { createRoot } from 'react-dom/client'; 4 - import CalendarHeatmap from 'react-calendar-heatmap'; 5 - import { Tooltip } from 'react-tooltip'; 6 2 import 'react-tooltip/dist/react-tooltip.css'; 7 3 import './react-calendar-heatmap.css'; 8 4 import './styles.css'; 9 - import * as bsky from '@atproto/api'; 10 - const { BskyAgent } = bsky; 11 - import type { AtpSessionEvent, AtpSessionData } from '@atproto/api'; 12 - 13 - let session: AtpSessionData; 14 - 15 - const agent = new BskyAgent({ 16 - service: 'https://bsky.social', 17 - persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => { 18 - session = sess!; 19 - }, 20 - }); 21 - 22 - async function getData(actor = '') { 23 - await agent.resumeSession(session); 24 - // source: https://github.com/bluesky-social/atproto/blob/efb1cac2bfc8ccb77c0f4910ad9f3de7370fbebb/packages/bsky/tests/_util.ts#L314 25 - const paginateAll = async <T extends { cursor?: string }>( 26 - fn: (cursor?: string) => Promise<T>, 27 - limit = Infinity, 28 - ): Promise<T[]> => { 29 - const results: T[] = []; 30 - let cursor; 31 - do { 32 - const res = await fn(cursor); 33 - results.push(res); 34 - cursor = res.cursor; 35 - } while (cursor && results.length < limit); 36 - return results; 37 - }; 38 - 39 - if (actor === '') { 40 - actor = agent.session!.did; 41 - } else { 42 - actor = (await agent.getProfile({ actor })).data.did; 43 - } 44 - 45 - // source: https://github.com/bluesky-social/atproto/blob/efb1cac2bfc8ccb77c0f4910ad9f3de7370fbebb/packages/bsky/tests/views/author-feed.test.ts#L94 46 - const paginator = async (cursor?: string) => { 47 - const res = await agent.getAuthorFeed({ 48 - actor: actor, 49 - cursor, 50 - limit: 100, 51 - }); 52 - return res.data; 53 - }; 54 - 55 - const paginatedAll = await paginateAll(paginator); 56 - 57 - const posts: object[] = []; 58 - 59 - paginatedAll.forEach((res) => { 60 - if (typeof res.feed[0] !== 'undefined') { 61 - posts.push( 62 - ...res.feed.map((e) => ({ 63 - text: (e.post.record as any).text, 64 - uri: e.post.uri.replace('app.bsky.feed.', '').replace('at://', 'https://staging.bsky.app/profile/'), 65 - likeCount: e.post.likeCount, 66 - did: e.post.author.did, 67 - handle: e.post.author.handle, 68 - isOwn: e.post.author.did === actor, 69 - repostCount: e.post.repostCount, 70 - isRepost: e.post.repostCount === 0 ? false : true, 71 - createdAt: (e.post.record as any).createdAt, 72 - })), 73 - ); 74 - } 75 - }); 76 - 77 - const groupedPosts = posts.reduce((acc, obj: any) => { 78 - const key = obj.createdAt.slice(0, 10); 79 - if (!acc[key]) { 80 - acc[key] = { date: key, count: 0 }; 81 - } 82 - if (obj.isOwn) acc[key].count++; 83 - return acc; 84 - }, {}); 85 - 86 - // i don't need the outer object, i just need an array with the values 87 - const data = Object.values(groupedPosts); 88 - 89 - const max = Math.max(...data.map((o) => o.count)); 90 - 91 - // source: https://github.com/mimonelu/klearsky/blob/079746c1c1a03d3a9f0961bdb69bb223dcb106c3/src/composables/main-state.ts#L98 92 - const log = await fetch(`https://plc.directory/${actor}/log/audit`); 93 - const logJson = await log.json(); 94 - const createdAt = logJson[0]?.createdAt; 95 - 96 - return { 97 - data, 98 - max, 99 - createdAt, 100 - }; 101 - } 102 - 103 - const today = new Date(); 104 - 105 - function App() { 106 - const [posts, setPosts] = useState<any>([]); 107 - const [max, setMax] = useState<any>(0); 108 - const [createdAt, setCreatedAt] = useState<any>(); 109 - const [actor, setActor] = useState<any>(''); 110 - const [updated, setUpdated] = useState(actor); 111 - const [username, setUsername] = useState<any>(''); 112 - const [password, setPassword] = useState<any>(''); 113 - const [loginPressed, setLoginPressed] = useState<any>(false); 114 - const [loggedIn, setLoggedIn] = useState<any>(false); 115 - const [isLoading, setIsLoading] = useState<any>(false); 116 - 117 - useEffect(() => { 118 - let ignore = false; 119 - setPosts([]); 120 - 121 - if (loggedIn) { 122 - setIsLoading(true); 123 - getData(actor).then((data) => { 124 - if (!ignore) { 125 - setPosts(data.data); 126 - setMax(data.max); 127 - setCreatedAt(new Date(data.createdAt)); 128 - setIsLoading(false); 129 - } 130 - }); 131 - } 132 - 133 - return () => { 134 - ignore = true; 135 - }; 136 - }, [updated, loggedIn]); 137 - 138 - useEffect(() => { 139 - let ignore = false; 140 - if (loginPressed && !ignore) { 141 - agent 142 - .login({ 143 - identifier: username, 144 - password: password, 145 - }) 146 - .then(() => { 147 - setLoggedIn(true); 148 - }); 149 - } 150 - return () => { 151 - ignore = true; 152 - }; 153 - }, [loginPressed]); 154 - return ( 155 - <div> 156 - <h1>Bluesky Posts Heatmap</h1> 157 - {loggedIn === false ? ( 158 - <> 159 - <div id="loginMessage">Please log in</div> 160 - <br /> 161 - </> 162 - ) : null} 163 - <div id="login"> 164 - Username:&nbsp; 165 - <input type="text" placeholder="username" onChange={(e) => setUsername(e.target.value)} value={username} /> 166 - <br /> 167 - Password:&nbsp; 168 - <input type="password" placeholder="password" onChange={(e) => setPassword(e.target.value)} value={password} /> 169 - <input 170 - type="button" 171 - value="login" 172 - onClick={() => { 173 - setLoginPressed(true); 174 - setActor(username); 175 - }} 176 - disabled={loggedIn} 177 - /> 178 - <br /> 179 - <br /> 180 - </div> 181 - <div id="actor"> 182 - 🦋&nbsp; 183 - <input type="text" placeholder="Bluesky username" onChange={(e) => setActor(e.target.value)} value={actor} /> 184 - <input 185 - type="button" 186 - value="Get heatmap" 187 - onClick={() => { 188 - setMax(0); 189 - setUpdated(actor); 190 - }} 191 - disabled={!loggedIn || isLoading} 192 - /> 193 - </div> 194 - <div> 195 - <br /> 196 - </div> 197 - {isLoading ? <div>Loading...</div> : null} 198 - {posts.length === 0 ? null : ( 199 - <> 200 - <CalendarHeatmap 201 - startDate={createdAt} 202 - endDate={today} 203 - values={posts} 204 - classForValue={(value) => { 205 - if (!value) { 206 - return 'color-empty'; 207 - } 208 - // return `color-github-${value.count > 0 ? Math.ceil((value.count / max) * 4) : 0}`; 209 - return `color-custom-${value.count > 0 ? Math.ceil((value.count / max) * 17) : 0}`; 210 - }} 211 - tooltipDataAttrs={(value) => { 212 - return { 213 - 'data-tooltip-id': 'my-tooltip', 214 - 'data-tooltip-content': value.date !== null ? `${value.date} has ${value.count} posts` : 'no posts', 215 - 'data-tooltip-place': 'top', 216 - }; 217 - }} 218 - showWeekdayLabels={true} 219 - gutterSize={1} 220 - showOutOfRangeDays={true} 221 - /> 222 - <Tooltip id="my-tooltip" /> 223 - </> 224 - )} 225 - </div> 226 - ); 227 - } 228 - 229 - function shiftDate(date, numDays) { 230 - const newDate = new Date(date); 231 - newDate.setDate(newDate.getDate() + numDays); 232 - return newDate; 233 - } 5 + import { App } from './App'; 234 6 235 7 const container = document.getElementById('root'); 236 8 const root = createRoot(container!);