Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Fix a bunch of type errors and add a type-check to the github workflows (#837)

* Add yarn type-check

* Rename to yarn typecheck

* Fix a collection of type errors

* Add typecheck to automated tests

* add `dist` to exluded folders tsconfig

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>

authored by

Paul Frazee
Ansh Nanda
and committed by
GitHub
e8843ded 46c9de7c

+168 -82
+3 -1
.github/workflows/lint.yml
··· 18 uses: actions/checkout@v3 19 - name: Yarn install 20 run: yarn 21 - - name: Typescript & Lint check 22 run: yarn lint 23 testing: 24 name: Run tests 25 runs-on: ubuntu-latest
··· 18 uses: actions/checkout@v3 19 - name: Yarn install 20 run: yarn 21 + - name: Lint check 22 run: yarn lint 23 + - name: Type check 24 + run: yarn typecheck 25 testing: 26 name: Run tests 27 runs-on: ubuntu-latest
+1
package.json
··· 16 "test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit", 17 "test-coverage": "jest --coverage", 18 "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 19 "e2e:mock-server": "ts-node __e2e__/mock-server.ts", 20 "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", 21 "e2e:build": "detox build -c ios.sim.debug",
··· 16 "test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit", 17 "test-coverage": "jest --coverage", 18 "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 19 + "typecheck": "tsc --project ./tsconfig.check.json", 20 "e2e:mock-server": "ts-node __e2e__/mock-server.ts", 21 "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", 22 "e2e:build": "detox build -c ios.sim.debug",
+1 -1
src/lib/api/api-polyfill.ts
··· 11 interface FetchHandlerResponse { 12 status: number 13 headers: Record<string, string> 14 - body: ArrayBuffer | undefined 15 } 16 17 async function fetchHandler(
··· 11 interface FetchHandlerResponse { 12 status: number 13 headers: Record<string, string> 14 + body: any 15 } 16 17 async function fetchHandler(
+25 -16
src/lib/api/feed-manip.ts
··· 74 } 75 76 flattenReplyParent() { 77 - if (this.items[0].reply?.parent) { 78 - this.isFlattenedReply = true 79 - this.items.splice(0, 0, {post: this.items[0].reply?.parent}) 80 } 81 } 82 } ··· 130 131 // turn non-threads with reply parents into threads 132 for (const slice of slices) { 133 - if ( 134 - !slice.isThread && 135 - !slice.items[0].reason && 136 - slice.items[0].reply?.parent && 137 - !this.seenUris.has(slice.items[0].reply?.parent.uri) && 138 - !soonToBeSeenUris.has(slice.items[0].reply?.parent.uri) 139 - ) { 140 - const uri = slice.items[0].reply?.parent.uri 141 - slice.flattenReplyParent() 142 - soonToBeSeenUris.add(uri) 143 } 144 } 145 ··· 231 } 232 233 function getSelfReplyUri(item: FeedViewPost): string | undefined { 234 - return item.reply?.parent.author.did === item.post.author.did 235 - ? item.reply?.parent.uri 236 - : undefined 237 }
··· 74 } 75 76 flattenReplyParent() { 77 + if (this.items[0].reply) { 78 + const reply = this.items[0].reply 79 + if (AppBskyFeedDefs.isPostView(reply.parent)) { 80 + this.isFlattenedReply = true 81 + this.items.splice(0, 0, {post: reply.parent}) 82 + } 83 } 84 } 85 } ··· 133 134 // turn non-threads with reply parents into threads 135 for (const slice of slices) { 136 + if (!slice.isThread && !slice.items[0].reason && slice.items[0].reply) { 137 + const reply = slice.items[0].reply 138 + if ( 139 + AppBskyFeedDefs.isPostView(reply.parent) && 140 + !this.seenUris.has(reply.parent.uri) && 141 + !soonToBeSeenUris.has(reply.parent.uri) 142 + ) { 143 + const uri = reply.parent.uri 144 + slice.flattenReplyParent() 145 + soonToBeSeenUris.add(uri) 146 + } 147 } 148 } 149 ··· 235 } 236 237 function getSelfReplyUri(item: FeedViewPost): string | undefined { 238 + if (item.reply) { 239 + if (AppBskyFeedDefs.isPostView(item.reply.parent)) { 240 + return item.reply.parent.author.did === item.post.author.did 241 + ? item.reply.parent.uri 242 + : undefined 243 + } 244 + } 245 + return undefined 246 }
+1 -1
src/lib/hooks/useTimer.ts
··· 4 * Helper hook to run persistent timers on views 5 */ 6 export function useTimer(time: number, handler: () => void) { 7 - const timer = React.useRef(undefined) 8 9 // function to restart the timer 10 const reset = React.useCallback(() => {
··· 4 * Helper hook to run persistent timers on views 5 */ 6 export function useTimer(time: number, handler: () => void) { 7 + const timer = React.useRef<undefined | NodeJS.Timeout>(undefined) 8 9 // function to restart the timer 10 const reset = React.useCallback(() => {
+18 -7
src/state/models/content/list-membership.ts
··· 9 value: AppBskyGraphListitem.Record 10 } 11 12 export class ListMembershipModel { 13 // data 14 memberships: Membership[] = [] ··· 32 // it needs to be replaced with server side list membership queries 33 // -prf 34 let cursor 35 - let records = [] 36 for (let i = 0; i < 100; i++) { 37 - const res = await this.rootStore.agent.app.bsky.graph.listitem.list({ 38 - repo: this.rootStore.me.did, 39 - cursor, 40 - limit: PAGE_SIZE, 41 - }) 42 records = records.concat( 43 res.records.filter(record => record.value.subject === this.subject), 44 ) ··· 99 }) 100 } 101 102 - async updateTo(uris: string) { 103 for (const uri of uris) { 104 await this.add(uri) 105 }
··· 9 value: AppBskyGraphListitem.Record 10 } 11 12 + interface ListitemRecord { 13 + uri: string 14 + value: AppBskyGraphListitem.Record 15 + } 16 + 17 + interface ListitemListResponse { 18 + cursor?: string 19 + records: ListitemRecord[] 20 + } 21 + 22 export class ListMembershipModel { 23 // data 24 memberships: Membership[] = [] ··· 42 // it needs to be replaced with server side list membership queries 43 // -prf 44 let cursor 45 + let records: ListitemRecord[] = [] 46 for (let i = 0; i < 100; i++) { 47 + const res: ListitemListResponse = 48 + await this.rootStore.agent.app.bsky.graph.listitem.list({ 49 + repo: this.rootStore.me.did, 50 + cursor, 51 + limit: PAGE_SIZE, 52 + }) 53 records = records.concat( 54 res.records.filter(record => record.value.subject === this.subject), 55 ) ··· 110 }) 111 } 112 113 + async updateTo(uris: string[]) { 114 for (const uri of uris) { 115 await this.add(uri) 116 }
+32 -7
src/state/models/content/list.ts
··· 4 AppBskyGraphGetList as GetList, 5 AppBskyGraphDefs as GraphDefs, 6 AppBskyGraphList, 7 } from '@atproto/api' 8 import {Image as RNImage} from 'react-native-image-crop-picker' 9 import {RootStoreModel} from '../root-store' ··· 12 import {bundleAsync} from 'lib/async/bundle' 13 14 const PAGE_SIZE = 30 15 16 export class ListModel { 17 // state ··· 33 name, 34 description, 35 avatar, 36 - }: {name: string; description: string; avatar: RNImage | undefined}, 37 ) { 38 const record: AppBskyGraphList.Record = { 39 purpose: 'app.bsky.graph.defs#modlist', ··· 124 description: string 125 avatar: RNImage | null | undefined 126 }) { 127 if (!this.isOwner) { 128 throw new Error('Cannot edit this list') 129 } ··· 157 } 158 159 async delete() { 160 // fetch all the listitem records that belong to this list 161 let cursor 162 - let records = [] 163 for (let i = 0; i < 100; i++) { 164 - const res = await this.rootStore.agent.app.bsky.graph.listitem.list({ 165 - repo: this.rootStore.me.did, 166 - cursor, 167 - limit: PAGE_SIZE, 168 - }) 169 records = records.concat( 170 res.records.filter(record => record.value.list === this.uri), 171 ) ··· 193 } 194 195 async subscribe() { 196 await this.rootStore.agent.app.bsky.graph.muteActorList({ 197 list: this.list.uri, 198 }) ··· 200 } 201 202 async unsubscribe() { 203 await this.rootStore.agent.app.bsky.graph.unmuteActorList({ 204 list: this.list.uri, 205 })
··· 4 AppBskyGraphGetList as GetList, 5 AppBskyGraphDefs as GraphDefs, 6 AppBskyGraphList, 7 + AppBskyGraphListitem, 8 } from '@atproto/api' 9 import {Image as RNImage} from 'react-native-image-crop-picker' 10 import {RootStoreModel} from '../root-store' ··· 13 import {bundleAsync} from 'lib/async/bundle' 14 15 const PAGE_SIZE = 30 16 + 17 + interface ListitemRecord { 18 + uri: string 19 + value: AppBskyGraphListitem.Record 20 + } 21 + 22 + interface ListitemListResponse { 23 + cursor?: string 24 + records: ListitemRecord[] 25 + } 26 27 export class ListModel { 28 // state ··· 44 name, 45 description, 46 avatar, 47 + }: {name: string; description: string; avatar: RNImage | null | undefined}, 48 ) { 49 const record: AppBskyGraphList.Record = { 50 purpose: 'app.bsky.graph.defs#modlist', ··· 135 description: string 136 avatar: RNImage | null | undefined 137 }) { 138 + if (!this.list) { 139 + return 140 + } 141 if (!this.isOwner) { 142 throw new Error('Cannot edit this list') 143 } ··· 171 } 172 173 async delete() { 174 + if (!this.list) { 175 + return 176 + } 177 + 178 // fetch all the listitem records that belong to this list 179 let cursor 180 + let records: ListitemRecord[] = [] 181 for (let i = 0; i < 100; i++) { 182 + const res: ListitemListResponse = 183 + await this.rootStore.agent.app.bsky.graph.listitem.list({ 184 + repo: this.rootStore.me.did, 185 + cursor, 186 + limit: PAGE_SIZE, 187 + }) 188 records = records.concat( 189 res.records.filter(record => record.value.list === this.uri), 190 ) ··· 212 } 213 214 async subscribe() { 215 + if (!this.list) { 216 + return 217 + } 218 await this.rootStore.agent.app.bsky.graph.muteActorList({ 219 list: this.list.uri, 220 }) ··· 222 } 223 224 async unsubscribe() { 225 + if (!this.list) { 226 + return 227 + } 228 await this.rootStore.agent.app.bsky.graph.unmuteActorList({ 229 list: this.list.uri, 230 })
+10 -6
src/state/models/discovery/foafs.ts
··· 1 - import {AppBskyActorDefs} from '@atproto/api' 2 import {makeAutoObservable, runInAction} from 'mobx' 3 import sampleSize from 'lodash.samplesize' 4 import {bundleAsync} from 'lib/async/bundle' ··· 43 { 44 let cursor 45 for (let i = 0; i < 10; i++) { 46 - const res = await this.rootStore.agent.getFollows({ 47 - actor: this.rootStore.me.did, 48 - cursor, 49 - limit: 100, 50 - }) 51 this.rootStore.me.follows.hydrateProfiles(res.data.follows) 52 if (!res.data.cursor) { 53 break
··· 1 + import { 2 + AppBskyActorDefs, 3 + AppBskyGraphGetFollows as GetFollows, 4 + } from '@atproto/api' 5 import {makeAutoObservable, runInAction} from 'mobx' 6 import sampleSize from 'lodash.samplesize' 7 import {bundleAsync} from 'lib/async/bundle' ··· 46 { 47 let cursor 48 for (let i = 0; i < 10; i++) { 49 + const res: GetFollows.Response = 50 + await this.rootStore.agent.getFollows({ 51 + actor: this.rootStore.me.did, 52 + cursor, 53 + limit: 100, 54 + }) 55 this.rootStore.me.follows.hydrateProfiles(res.data.follows) 56 if (!res.data.cursor) { 57 break
+1 -1
src/state/models/feeds/post.ts
··· 67 } 68 69 get rootUri(): string { 70 - if (this.reply?.root.uri) { 71 return this.reply.root.uri 72 } 73 return this.post.uri
··· 67 } 68 69 get rootUri(): string { 70 + if (typeof this.reply?.root.uri === 'string') { 71 return this.reply.root.uri 72 } 73 return this.post.uri
+7 -6
src/state/models/lists/lists-list.ts
··· 61 } 62 this._xLoading(replace) 63 try { 64 - let res 65 if (this.source === 'my-modlists') { 66 res = { 67 success: true, ··· 170 171 let cursor 172 for (let i = 0; i < 100; i++) { 173 - const res = await store.agent.app.bsky.graph.getLists({ 174 actor: did, 175 cursor, 176 limit: 50, ··· 199 200 let cursor 201 for (let i = 0; i < 100; i++) { 202 - const res = await store.agent.app.bsky.graph.getListMutes({ 203 - cursor, 204 - limit: 50, 205 - }) 206 cursor = res.data.cursor 207 acc.data.lists = acc.data.lists.concat(res.data.lists) 208 if (!cursor) {
··· 61 } 62 this._xLoading(replace) 63 try { 64 + let res: GetLists.Response 65 if (this.source === 'my-modlists') { 66 res = { 67 success: true, ··· 170 171 let cursor 172 for (let i = 0; i < 100; i++) { 173 + const res: GetLists.Response = await store.agent.app.bsky.graph.getLists({ 174 actor: did, 175 cursor, 176 limit: 50, ··· 199 200 let cursor 201 for (let i = 0; i < 100; i++) { 202 + const res: GetListMutes.Response = 203 + await store.agent.app.bsky.graph.getListMutes({ 204 + cursor, 205 + limit: 50, 206 + }) 207 cursor = res.data.cursor 208 acc.data.lists = acc.data.lists.concat(res.data.lists) 209 if (!cursor) {
+9 -3
src/state/models/ui/shell.ts
··· 8 import {ListModel} from '../content/list' 9 import {GalleryModel} from '../media/gallery' 10 11 export interface ConfirmModal { 12 name: 'confirm' 13 title: string ··· 189 } 190 191 export class ShellUiModel { 192 - colorMode = 'system' 193 minimalShellMode = false 194 isDrawerOpen = false 195 isDrawerSwipeDisabled = false ··· 216 217 hydrate(v: unknown) { 218 if (isObj(v)) { 219 - if (hasProp(v, 'colorMode') && typeof v.colorMode === 'string') { 220 this.colorMode = v.colorMode 221 } 222 } 223 } 224 225 - setColorMode(mode: string) { 226 this.colorMode = mode 227 } 228
··· 8 import {ListModel} from '../content/list' 9 import {GalleryModel} from '../media/gallery' 10 11 + export type ColorMode = 'system' | 'light' | 'dark' 12 + 13 + export function isColorMode(v: unknown): v is ColorMode { 14 + return v === 'system' || v === 'light' || v === 'dark' 15 + } 16 + 17 export interface ConfirmModal { 18 name: 'confirm' 19 title: string ··· 195 } 196 197 export class ShellUiModel { 198 + colorMode: ColorMode = 'system' 199 minimalShellMode = false 200 isDrawerOpen = false 201 isDrawerSwipeDisabled = false ··· 222 223 hydrate(v: unknown) { 224 if (isObj(v)) { 225 + if (hasProp(v, 'colorMode') && isColorMode(v.colorMode)) { 226 this.colorMode = v.colorMode 227 } 228 } 229 } 230 231 + setColorMode(mode: ColorMode) { 232 this.colorMode = mode 233 } 234
+4 -1
src/view/com/composer/useExternalLinkFetch.ts
··· 1 import {useState, useEffect} from 'react' 2 import {useStores} from 'state/index' 3 import * as apilib from 'lib/api/index' 4 import {getLinkMeta} from 'lib/link-meta/link-meta' 5 import {getPostAsQuote, getFeedAsEmbed} from 'lib/link-meta/bsky' ··· 90 setExtLink({ 91 ...extLink, 92 isLoading: false, // done 93 - localThumb, 94 }) 95 }) 96 return cleanup
··· 1 import {useState, useEffect} from 'react' 2 import {useStores} from 'state/index' 3 + import {ImageModel} from 'state/models/media/image' 4 import * as apilib from 'lib/api/index' 5 import {getLinkMeta} from 'lib/link-meta/link-meta' 6 import {getPostAsQuote, getFeedAsEmbed} from 'lib/link-meta/bsky' ··· 91 setExtLink({ 92 ...extLink, 93 isLoading: false, // done 94 + localThumb: localThumb 95 + ? new ImageModel(store, localThumb) 96 + : undefined, 97 }) 98 }) 99 return cleanup
+6 -6
src/view/com/lightbox/Lightbox.tsx
··· 27 } 28 29 let altText = '' 30 - let uri 31 if (lightbox.name === 'images') { 32 - const opts = store.shell.activeLightbox as models.ImagesLightbox 33 uri = opts.images[imageIndex].uri 34 - altText = opts.images[imageIndex].alt 35 - } else if (store.shell.activeLightbox.name === 'profile-image') { 36 - const opts = store.shell.activeLightbox as models.ProfileImageLightbox 37 - uri = opts.profileView.avatar 38 } 39 40 return (
··· 27 } 28 29 let altText = '' 30 + let uri = '' 31 if (lightbox.name === 'images') { 32 + const opts = lightbox as models.ImagesLightbox 33 uri = opts.images[imageIndex].uri 34 + altText = opts.images[imageIndex].alt || '' 35 + } else if (lightbox.name === 'profile-image') { 36 + const opts = lightbox as models.ProfileImageLightbox 37 + uri = opts.profileView.avatar || '' 38 } 39 40 return (
+4 -4
src/view/com/modals/CreateOrEditMuteList.tsx
··· 44 const {track} = useAnalytics() 45 46 const [isProcessing, setProcessing] = useState<boolean>(false) 47 - const [name, setName] = useState<string>(list?.list.name || '') 48 const [description, setDescription] = useState<string>( 49 - list?.list.description || '', 50 ) 51 - const [avatar, setAvatar] = useState<string | undefined>(list?.list.avatar) 52 const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() 53 54 const onPressCancel = useCallback(() => { ··· 59 async (img: RNImage | null) => { 60 if (!img) { 61 setNewAvatar(null) 62 - setAvatar(null) 63 return 64 } 65 track('CreateMuteList:AvatarSelected')
··· 44 const {track} = useAnalytics() 45 46 const [isProcessing, setProcessing] = useState<boolean>(false) 47 + const [name, setName] = useState<string>(list?.list?.name || '') 48 const [description, setDescription] = useState<string>( 49 + list?.list?.description || '', 50 ) 51 + const [avatar, setAvatar] = useState<string | undefined>(list?.list?.avatar) 52 const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() 53 54 const onPressCancel = useCallback(() => { ··· 59 async (img: RNImage | null) => { 60 if (!img) { 61 setNewAvatar(null) 62 + setAvatar(undefined) 63 return 64 } 65 track('CreateMuteList:AvatarSelected')
+1 -1
src/view/com/modals/ListAddRemoveUser.tsx
··· 36 const pal = usePalette('default') 37 const palPrimary = usePalette('primary') 38 const palInverted = usePalette('inverted') 39 - const [selected, setSelected] = React.useState([]) 40 41 const listsList: ListsListModel = React.useMemo( 42 () => new ListsListModel(store, store.me.did),
··· 36 const pal = usePalette('default') 37 const palPrimary = usePalette('primary') 38 const palInverted = usePalette('inverted') 39 + const [selected, setSelected] = React.useState<string[]>([]) 40 41 const listsList: ListsListModel = React.useMemo( 42 () => new ListsListModel(store, store.me.did),
+4 -2
src/view/com/profile/ProfileCard.tsx
··· 1 - import React from 'react' 2 import {StyleSheet, View} from 'react-native' 3 import {observer} from 'mobx-react-lite' 4 import {AppBskyActorDefs} from '@atproto/api' ··· 32 noBorder?: boolean 33 followers?: AppBskyActorDefs.ProfileView[] | undefined 34 overrideModeration?: boolean 35 - renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => JSX.Element 36 }) => { 37 const store = useStores() 38 const pal = usePalette('default')
··· 1 + import * as React from 'react' 2 import {StyleSheet, View} from 'react-native' 3 import {observer} from 'mobx-react-lite' 4 import {AppBskyActorDefs} from '@atproto/api' ··· 32 noBorder?: boolean 33 followers?: AppBskyActorDefs.ProfileView[] | undefined 34 overrideModeration?: boolean 35 + renderButton?: ( 36 + profile: AppBskyActorDefs.ProfileViewBasic, 37 + ) => React.ReactNode 38 }) => { 39 const store = useStores() 40 const pal = usePalette('default')
+2 -2
src/view/com/profile/ProfileHeader.tsx
··· 587 // Word wrapping appears fine on 588 // mobile but overflows on desktop 589 handle: isNative 590 - ? undefined 591 : { 592 - // eslint-disable-next-line 593 wordBreak: 'break-all', 594 }, 595
··· 587 // Word wrapping appears fine on 588 // mobile but overflows on desktop 589 handle: isNative 590 + ? {} 591 : { 592 + // @ts-ignore web only -prf 593 wordBreak: 'break-all', 594 }, 595
+1
src/view/com/util/BlurView.web.tsx
··· 15 }: React.PropsWithChildren<BlurViewProps>) => { 16 // @ts-ignore using an RNW-specific attribute here -prf 17 let blur = `blur(${blurAmount || 10}px` 18 style = addStyle(style, {backdropFilter: blur, WebkitBackdropFilter: blur}) 19 if (blurType === 'dark') { 20 style = addStyle(style, styles.dark)
··· 15 }: React.PropsWithChildren<BlurViewProps>) => { 16 // @ts-ignore using an RNW-specific attribute here -prf 17 let blur = `blur(${blurAmount || 10}px` 18 + // @ts-ignore using an RNW-specific attribute here -prf 19 style = addStyle(style, {backdropFilter: blur, WebkitBackdropFilter: blur}) 20 if (blurType === 'dark') { 21 style = addStyle(style, styles.dark)
+17 -11
src/view/com/util/Html.tsx
··· 1 - import React from 'react' 2 import {StyleSheet, View} from 'react-native' 3 import {usePalette} from 'lib/hooks/usePalette' 4 import {Text} from './text/Text' ··· 9 * These utilities are used to define long documents in an html-like 10 * DSL. See for instance /locale/en/privacy-policy.tsx 11 */ 12 13 export function H1({children}: React.PropsWithChildren<{}>) { 14 const pal = usePalette('default') ··· 55 ) 56 } 57 58 - export function UL({ 59 - children, 60 - isChild, 61 - }: React.PropsWithChildren<{isChild: boolean}>) { 62 return ( 63 <View style={[styles.ul, isChild && styles.ulChild]}> 64 {markChildProps(children)} ··· 66 ) 67 } 68 69 - export function OL({ 70 - children, 71 - isChild, 72 - }: React.PropsWithChildren<{isChild: boolean}>) { 73 return ( 74 <View style={[styles.ol, isChild && styles.olChild]}> 75 {markChildProps(children)} ··· 122 ) 123 } 124 125 - function markChildProps(children) { 126 return React.Children.map(children, child => { 127 if (React.isValidElement(child)) { 128 - return React.cloneElement(child, {isChild: true}) 129 } 130 return child 131 })
··· 1 + import * as React from 'react' 2 import {StyleSheet, View} from 'react-native' 3 import {usePalette} from 'lib/hooks/usePalette' 4 import {Text} from './text/Text' ··· 9 * These utilities are used to define long documents in an html-like 10 * DSL. See for instance /locale/en/privacy-policy.tsx 11 */ 12 + 13 + interface IsChildProps { 14 + isChild?: boolean 15 + } 16 + 17 + // type ReactNodeWithIsChildProp = 18 + // | React.ReactElement<IsChildProps> 19 + // | React.ReactElement<IsChildProps>[] 20 + // | React.ReactNode 21 22 export function H1({children}: React.PropsWithChildren<{}>) { 23 const pal = usePalette('default') ··· 64 ) 65 } 66 67 + export function UL({children, isChild}: React.PropsWithChildren<IsChildProps>) { 68 return ( 69 <View style={[styles.ul, isChild && styles.ulChild]}> 70 {markChildProps(children)} ··· 72 ) 73 } 74 75 + export function OL({children, isChild}: React.PropsWithChildren<IsChildProps>) { 76 return ( 77 <View style={[styles.ol, isChild && styles.olChild]}> 78 {markChildProps(children)} ··· 125 ) 126 } 127 128 + function markChildProps(children: React.ReactNode) { 129 return React.Children.map(children, child => { 130 if (React.isValidElement(child)) { 131 + return React.cloneElement<IsChildProps>( 132 + child as React.ReactElement<IsChildProps>, 133 + {isChild: true}, 134 + ) 135 } 136 return child 137 })
+3 -1
src/view/com/util/UserInfoText.tsx
··· 70 numberOfLines={1} 71 href={`/profile/${profile.handle}`} 72 text={`${prefix || ''}${sanitizeDisplayName( 73 - profile[attr] || profile.handle, 74 )}`} 75 /> 76 )
··· 70 numberOfLines={1} 71 href={`/profile/${profile.handle}`} 72 text={`${prefix || ''}${sanitizeDisplayName( 73 + typeof profile[attr] === 'string' && profile[attr] 74 + ? (profile[attr] as string) 75 + : profile.handle, 76 )}`} 77 /> 78 )
+10 -3
src/view/screens/Settings.tsx
··· 38 import {isDesktopWeb} from 'platform/detection' 39 import {pluralize} from 'lib/strings/helpers' 40 import {formatCount} from 'view/com/util/numeric/format' 41 42 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> 43 export const SettingsScreen = withAuthRequired( ··· 299 value="system" 300 label="System" 301 left 302 - onChange={(v: string) => store.shell.setColorMode(v)} 303 /> 304 <SelectableBtn 305 current={store.shell.colorMode} 306 value="light" 307 label="Light" 308 - onChange={(v: string) => store.shell.setColorMode(v)} 309 /> 310 <SelectableBtn 311 current={store.shell.colorMode} 312 value="dark" 313 label="Dark" 314 right 315 - onChange={(v: string) => store.shell.setColorMode(v)} 316 /> 317 </View> 318 </View>
··· 38 import {isDesktopWeb} from 'platform/detection' 39 import {pluralize} from 'lib/strings/helpers' 40 import {formatCount} from 'view/com/util/numeric/format' 41 + import {isColorMode} from 'state/models/ui/shell' 42 43 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> 44 export const SettingsScreen = withAuthRequired( ··· 300 value="system" 301 label="System" 302 left 303 + onChange={(v: string) => 304 + store.shell.setColorMode(isColorMode(v) ? v : 'system') 305 + } 306 /> 307 <SelectableBtn 308 current={store.shell.colorMode} 309 value="light" 310 label="Light" 311 + onChange={(v: string) => 312 + store.shell.setColorMode(isColorMode(v) ? v : 'system') 313 + } 314 /> 315 <SelectableBtn 316 current={store.shell.colorMode} 317 value="dark" 318 label="Dark" 319 right 320 + onChange={(v: string) => 321 + store.shell.setColorMode(isColorMode(v) ? v : 'system') 322 + } 323 /> 324 </View> 325 </View>
+4 -2
src/view/shell/desktop/LeftNav.tsx
··· 34 SatelliteDishIconSolid, 35 } from 'lib/icons' 36 import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers' 37 - import {NavigationProp} from 'lib/routes/types' 38 import {router} from '../../../routes' 39 40 const ProfileCard = observer(() => { ··· 100 let isCurrent = 101 currentRouteInfo.name === 'Profile' 102 ? isTab(currentRouteInfo.name, pathName) && 103 - currentRouteInfo.params.name === store.me.handle 104 : isTab(currentRouteInfo.name, pathName) 105 const {onPress} = useLinkProps({to: href}) 106 const onPressWrapped = React.useCallback( ··· 122 <PressableWithHover 123 style={styles.navItemWrapper} 124 hoverStyle={pal.viewLight} 125 onPress={onPressWrapped} 126 // @ts-ignore web only -prf 127 href={href}
··· 34 SatelliteDishIconSolid, 35 } from 'lib/icons' 36 import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers' 37 + import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types' 38 import {router} from '../../../routes' 39 40 const ProfileCard = observer(() => { ··· 100 let isCurrent = 101 currentRouteInfo.name === 'Profile' 102 ? isTab(currentRouteInfo.name, pathName) && 103 + (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === 104 + store.me.handle 105 : isTab(currentRouteInfo.name, pathName) 106 const {onPress} = useLinkProps({to: href}) 107 const onPressWrapped = React.useCallback( ··· 123 <PressableWithHover 124 style={styles.navItemWrapper} 125 hoverStyle={pal.viewLight} 126 + // @ts-ignore the function signature differs on web -prf 127 onPress={onPressWrapped} 128 // @ts-ignore web only -prf 129 href={href}
+4
tsconfig.check.json
···
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "exclude": ["__e2e__", "dist"], 4 + }