about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/composer/PhotoCarouselPicker.tsx15
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx19
-rw-r--r--src/view/com/login/CreateAccount.tsx10
-rw-r--r--src/view/com/login/Signin.tsx14
-rw-r--r--src/view/com/modals/CreateScene.tsx18
-rw-r--r--src/view/com/modals/InviteToScene.tsx12
-rw-r--r--src/view/com/notifications/Feed.tsx18
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx6
-rw-r--r--src/view/com/post-thread/PostThread.tsx9
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx6
-rw-r--r--src/view/com/post-thread/PostVotedBy.tsx6
-rw-r--r--src/view/com/post/Post.tsx10
-rw-r--r--src/view/com/post/PostText.tsx4
-rw-r--r--src/view/com/posts/Feed.tsx15
-rw-r--r--src/view/com/posts/FeedItem.tsx6
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx6
-rw-r--r--src/view/com/profile/ProfileFollows.tsx6
-rw-r--r--src/view/com/profile/ProfileHeader.tsx6
-rw-r--r--src/view/com/profile/ProfileMembers.tsx6
-rw-r--r--src/view/com/util/ViewHeader.tsx3
-rw-r--r--src/view/index.ts4
-rw-r--r--src/view/routes.ts4
-rw-r--r--src/view/screens/Home.tsx8
-rw-r--r--src/view/screens/Log.tsx100
-rw-r--r--src/view/screens/Notifications.tsx4
-rw-r--r--src/view/screens/PostThread.tsx5
-rw-r--r--src/view/screens/Profile.tsx13
-rw-r--r--src/view/screens/Settings.tsx20
28 files changed, 266 insertions, 87 deletions
diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/PhotoCarouselPicker.tsx
index 47c4c3746..f7a3d7987 100644
--- a/src/view/com/composer/PhotoCarouselPicker.tsx
+++ b/src/view/com/composer/PhotoCarouselPicker.tsx
@@ -1,7 +1,6 @@
 import React, {useCallback} from 'react'
 import {Image, StyleSheet, TouchableOpacity, ScrollView} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {colors} from '../../lib/styles'
 import {
   openPicker,
   openCamera,
@@ -9,6 +8,7 @@ import {
 } from 'react-native-image-crop-picker'
 import {compressIfNeeded} from '../../../lib/images'
 import {usePalette} from '../../lib/hooks/usePalette'
+import {useStores} from '../../../state'
 
 const IMAGE_PARAMS = {
   width: 1000,
@@ -28,6 +28,7 @@ export const PhotoCarouselPicker = ({
   localPhotos: any
 }) => {
   const pal = usePalette('default')
+  const store = useStores()
   const handleOpenCamera = useCallback(async () => {
     try {
       const cameraRes = await openCamera({
@@ -37,11 +38,11 @@ export const PhotoCarouselPicker = ({
       })
       const uri = await compressIfNeeded(cameraRes, 300000)
       onSelectPhotos([uri, ...selectedPhotos])
-    } catch (err) {
+    } catch (err: any) {
       // ignore
-      console.log('Error using camera', err)
+      store.log.warn('Error using camera', err.toString())
     }
-  }, [selectedPhotos, onSelectPhotos])
+  }, [store.log, selectedPhotos, onSelectPhotos])
 
   const handleSelectPhoto = useCallback(
     async (uri: string) => {
@@ -53,12 +54,12 @@ export const PhotoCarouselPicker = ({
         })
         const finalUri = await compressIfNeeded(cropperRes, 300000)
         onSelectPhotos([finalUri, ...selectedPhotos])
-      } catch (err) {
+      } catch (err: any) {
         // ignore
-        console.log('Error selecting photo', err)
+        store.log.warn('Error selecting photo', err.toString())
       }
     },
-    [selectedPhotos, onSelectPhotos],
+    [store.log, selectedPhotos, onSelectPhotos],
   )
 
   const handleOpenGallery = useCallback(() => {
diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx
index 24926df69..936dcd6db 100644
--- a/src/view/com/discover/SuggestedFollows.tsx
+++ b/src/view/com/discover/SuggestedFollows.tsx
@@ -42,11 +42,12 @@ export const SuggestedFollows = observer(
     )
 
     useEffect(() => {
-      console.log('Fetching suggested actors')
       view
         .setup()
-        .catch((err: any) => console.error('Failed to fetch suggestions', err))
-    }, [view])
+        .catch((err: any) =>
+          store.log.error('Failed to fetch suggestions', err.toString()),
+        )
+    }, [view, store.log])
 
     useEffect(() => {
       if (!view.isLoading && !view.hasError && !view.hasContent) {
@@ -57,14 +58,16 @@ export const SuggestedFollows = observer(
     const onPressTryAgain = () =>
       view
         .setup()
-        .catch((err: any) => console.error('Failed to fetch suggestions', err))
+        .catch((err: any) =>
+          store.log.error('Failed to fetch suggestions', err.toString()),
+        )
 
     const onPressFollow = async (item: SuggestedActor) => {
       try {
         const res = await apilib.follow(store, item.did, item.declaration.cid)
         setFollows({[item.did]: res.uri, ...follows})
-      } catch (e) {
-        console.log(e)
+      } catch (e: any) {
+        store.log.error('Failed fo create follow', {error: e.toString(), item})
         Toast.show('An issue occurred, please try again.')
       }
     }
@@ -72,8 +75,8 @@ export const SuggestedFollows = observer(
       try {
         await apilib.unfollow(store, follows[item.did])
         setFollows(_omit(follows, [item.did]))
-      } catch (e) {
-        console.log(e)
+      } catch (e: any) {
+        store.log.error('Failed fo delete follow', {error: e.toString(), item})
         Toast.show('An issue occurred, please try again.')
       }
     }
diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx
index e6ce2efa6..f07ad7071 100644
--- a/src/view/com/login/CreateAccount.tsx
+++ b/src/view/com/login/CreateAccount.tsx
@@ -44,7 +44,6 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
     let aborted = false
     setError('')
     setServiceDescription(undefined)
-    console.log('Fetching service description', serviceUrl)
     store.session.describeService(serviceUrl).then(
       desc => {
         if (aborted) return
@@ -53,7 +52,10 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
       },
       err => {
         if (aborted) return
-        console.error(err)
+        store.log.warn(
+          `Failed to fetch service description for ${serviceUrl}`,
+          err.toString(),
+        )
         setError(
           'Unable to contact your service. Please check your Internet connection.',
         )
@@ -62,7 +64,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
     return () => {
       aborted = true
     }
-  }, [serviceUrl, store.session])
+  }, [serviceUrl, store.session, store.log])
 
   const onPressSelectService = () => {
     store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl))
@@ -98,7 +100,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
         errMsg =
           'Invite code not accepted. Check that you input it correctly and try again.'
       }
-      console.log(e)
+      store.log.warn('Failed to create account', e.toString())
       setIsProcessing(false)
       setError(errMsg.replace(/^Error:/, ''))
     }
diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx
index f76507d71..0a78b6401 100644
--- a/src/view/com/login/Signin.tsx
+++ b/src/view/com/login/Signin.tsx
@@ -44,7 +44,6 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
   useEffect(() => {
     let aborted = false
     setError('')
-    console.log('Fetching service description', serviceUrl)
     store.session.describeService(serviceUrl).then(
       desc => {
         if (aborted) return
@@ -52,7 +51,10 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
       },
       err => {
         if (aborted) return
-        console.error(err)
+        store.log.warn(
+          `Failed to fetch service description for ${serviceUrl}`,
+          err.toString(),
+        )
         setError(
           'Unable to contact your service. Please check your Internet connection.',
         )
@@ -61,7 +63,7 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
     return () => {
       aborted = true
     }
-  }, [store.session, serviceUrl])
+  }, [store.session, store.log, serviceUrl])
 
   return (
     <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
@@ -169,7 +171,7 @@ const LoginForm = ({
       })
     } catch (e: any) {
       const errMsg = e.toString()
-      console.log(e)
+      store.log.warn('Failed to login', e.toString())
       setIsProcessing(false)
       if (errMsg.includes('Authentication Required')) {
         setError('Invalid username or password')
@@ -305,7 +307,7 @@ const ForgotPasswordForm = ({
       onEmailSent()
     } catch (e: any) {
       const errMsg = e.toString()
-      console.log(e)
+      store.log.warn('Failed to request password reset', e.toString())
       setIsProcessing(false)
       if (isNetworkError(e)) {
         setError(
@@ -417,7 +419,7 @@ const SetNewPasswordForm = ({
       onPasswordSet()
     } catch (e: any) {
       const errMsg = e.toString()
-      console.log(e)
+      store.log.warn('Failed to set new password', e.toString())
       setIsProcessing(false)
       if (isNetworkError(e)) {
         setError(
diff --git a/src/view/com/modals/CreateScene.tsx b/src/view/com/modals/CreateScene.tsx
index 60c240546..c26087850 100644
--- a/src/view/com/modals/CreateScene.tsx
+++ b/src/view/com/modals/CreateScene.tsx
@@ -55,7 +55,13 @@ export function Component({}: {}) {
           displayName,
           description,
         })
-        .catch(e => console.error(e)) // an error here is not critical
+        .catch(e =>
+          // an error here is not critical
+          store.log.error(
+            'Failed to update scene profile during creation',
+            e.toString(),
+          ),
+        )
       // follow the scene
       await store.api.app.bsky.graph.follow
         .create(
@@ -70,7 +76,13 @@ export function Component({}: {}) {
             createdAt: new Date().toISOString(),
           },
         )
-        .catch(e => console.error(e)) // an error here is not critical
+        .catch(e =>
+          // an error here is not critical
+          store.log.error(
+            'Failed to follow scene after creation',
+            e.toString(),
+          ),
+        )
       Toast.show('Scene created')
       store.shell.closeModal()
       store.nav.navigate(`/profile/${fullHandle}`)
@@ -82,7 +94,7 @@ export function Component({}: {}) {
       } else if (e instanceof AppBskyActorCreateScene.HandleNotAvailableError) {
         setError(`The handle "${handle}" is not available.`)
       } else {
-        console.error(e)
+        store.log.error('Failed to create scene', e.toString())
         setError(
           'Failed to create the scene. Check your internet connection and try again.',
         )
diff --git a/src/view/com/modals/InviteToScene.tsx b/src/view/com/modals/InviteToScene.tsx
index a73440179..6fe17b4dc 100644
--- a/src/view/com/modals/InviteToScene.tsx
+++ b/src/view/com/modals/InviteToScene.tsx
@@ -84,9 +84,9 @@ export const Component = observer(function Component({
       )
       setCreatedInvites({[follow.did]: assertionUri, ...createdInvites})
       Toast.show('Invite sent')
-    } catch (e) {
+    } catch (e: any) {
       setError('There was an issue with the invite. Please try again.')
-      console.error(e)
+      store.log.error('Failed to invite user to scene', e.toString())
     }
   }
   const onPressUndo = async (subjectDid: string, assertionUri: string) => {
@@ -98,9 +98,9 @@ export const Component = observer(function Component({
         rkey: urip.rkey,
       })
       setCreatedInvites(_omit(createdInvites, [subjectDid]))
-    } catch (e) {
+    } catch (e: any) {
       setError('There was an issue with the invite. Please try again.')
-      console.error(e)
+      store.log.error('Failed to delete a scene invite', e.toString())
     }
   }
 
@@ -117,9 +117,9 @@ export const Component = observer(function Component({
         ...deletedPendingInvites,
       })
       Toast.show('Invite removed')
-    } catch (e) {
+    } catch (e: any) {
       setError('There was an issue with the invite. Please try again.')
-      console.error(e)
+      store.log.error('Failed to delete an invite', e.toString())
     }
   }
 
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index 91a01db4d..6406a598b 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -36,10 +36,24 @@ export const Feed = observer(function Feed({
     return <FeedItem item={item} />
   }
   const onRefresh = () => {
-    view.refresh().catch(err => console.error('Failed to refresh', err))
+    view
+      .refresh()
+      .catch(err =>
+        view.rootStore.log.error(
+          'Failed to refresh notifications feed',
+          err.toString(),
+        ),
+      )
   }
   const onEndReached = () => {
-    view.loadMore().catch(err => console.error('Failed to load more', err))
+    view
+      .loadMore()
+      .catch(err =>
+        view.rootStore.log.error(
+          'Failed to load more notifications',
+          err.toString(),
+        ),
+      )
   }
   let data
   if (view.hasLoaded) {
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 0efdfe2e4..4ca20aedd 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -22,15 +22,15 @@ export const PostRepostedBy = observer(function PostRepostedBy({
 
   useEffect(() => {
     if (view?.params.uri === uri) {
-      console.log('Reposted by doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching Reposted by', uri)
     const newView = new RepostedByViewModel(store, {uri})
     setView(newView)
     newView
       .setup()
-      .catch(err => console.error('Failed to fetch reposted by', err))
+      .catch(err =>
+        store.log.error('Failed to fetch reposted by', err.toString()),
+      )
   }, [uri, view?.params.uri, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 8c22cc8b7..187fe6c11 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -18,7 +18,14 @@ export const PostThread = observer(function PostThread({
   const ref = useRef<FlatList>(null)
   const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
   const onRefresh = () => {
-    view?.refresh().catch(err => console.error('Failed to refresh', err))
+    view
+      ?.refresh()
+      .catch(err =>
+        view.rootStore.log.error(
+          'Failed to refresh posts thread',
+          err.toString(),
+        ),
+      )
   }
   const onLayout = () => {
     const index = posts.findIndex(post => post._isHighlightedPost)
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index ae2bd6681..456a6f465 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -72,12 +72,12 @@ export const PostThreadItem = observer(function PostThreadItem({
   const onPressToggleRepost = () => {
     item
       .toggleRepost()
-      .catch(e => console.error('Failed to toggle repost', record, e))
+      .catch(e => store.log.error('Failed to toggle repost', e.toString()))
   }
   const onPressToggleUpvote = () => {
     item
       .toggleUpvote()
-      .catch(e => console.error('Failed to toggle upvote', record, e))
+      .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
   }
   const onCopyPostText = () => {
     Clipboard.setString(record.text)
@@ -90,7 +90,7 @@ export const PostThreadItem = observer(function PostThreadItem({
         Toast.show('Post deleted')
       },
       e => {
-        console.error(e)
+        store.log.error('Failed to delete post', e.toString())
         Toast.show('Failed to delete post, please try again')
       },
     )
diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostVotedBy.tsx
index 96a335919..21559e432 100644
--- a/src/view/com/post-thread/PostVotedBy.tsx
+++ b/src/view/com/post-thread/PostVotedBy.tsx
@@ -24,13 +24,13 @@ export const PostVotedBy = observer(function PostVotedBy({
 
   useEffect(() => {
     if (view?.params.uri === uri) {
-      console.log('Voted by doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching voted by', uri)
     const newView = new VotesViewModel(store, {uri, direction})
     setView(newView)
-    newView.setup().catch(err => console.error('Failed to fetch voted by', err))
+    newView
+      .setup()
+      .catch(err => store.log.error('Failed to fetch voted by', err.toString()))
   }, [uri, view?.params.uri, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index e82498a7d..d55027a94 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -47,7 +47,9 @@ export const Post = observer(function Post({
     }
     const newView = new PostThreadViewModel(store, {uri, depth: 0})
     setView(newView)
-    newView.setup().catch(err => console.error('Failed to fetch post', err))
+    newView
+      .setup()
+      .catch(err => store.log.error('Failed to fetch post', err.toString()))
   }, [initView, uri, view?.params.uri, store])
 
   // deleted
@@ -110,12 +112,12 @@ export const Post = observer(function Post({
   const onPressToggleRepost = () => {
     item
       .toggleRepost()
-      .catch(e => console.error('Failed to toggle repost', record, e))
+      .catch(e => store.log.error('Failed to toggle repost', e.toString()))
   }
   const onPressToggleUpvote = () => {
     item
       .toggleUpvote()
-      .catch(e => console.error('Failed to toggle upvote', record, e))
+      .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
   }
   const onCopyPostText = () => {
     Clipboard.setString(record.text)
@@ -128,7 +130,7 @@ export const Post = observer(function Post({
         Toast.show('Post deleted')
       },
       e => {
-        console.error(e)
+        store.log.error('Failed to delete post', e.toString())
         Toast.show('Failed to delete post, please try again')
       },
     )
diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx
index 436768292..4e8761eb5 100644
--- a/src/view/com/post/PostText.tsx
+++ b/src/view/com/post/PostText.tsx
@@ -23,7 +23,9 @@ export const PostText = observer(function PostText({
     }
     const newModel = new PostModel(store, uri)
     setModel(newModel)
-    newModel.setup().catch(err => console.error('Failed to fetch post', err))
+    newModel
+      .setup()
+      .catch(err => store.log.error('Failed to fetch post', err.toString()))
   }, [uri, model?.uri, store])
 
   // loading
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 02141acef..61ecf0a8f 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -53,10 +53,21 @@ export const Feed = observer(function Feed({
     }
   }
   const onRefresh = () => {
-    feed.refresh().catch(err => console.error('Failed to refresh', err))
+    feed
+      .refresh()
+      .catch(err =>
+        feed.rootStore.log.error(
+          'Failed to refresh posts feed',
+          err.toString(),
+        ),
+      )
   }
   const onEndReached = () => {
-    feed.loadMore().catch(err => console.error('Failed to load more', err))
+    feed
+      .loadMore()
+      .catch(err =>
+        feed.rootStore.log.error('Failed to load more posts', err.toString()),
+      )
   }
   let data
   if (feed.hasLoaded) {
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 150143a26..dcc4e28d7 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -69,12 +69,12 @@ export const FeedItem = observer(function ({
   const onPressToggleRepost = () => {
     item
       .toggleRepost()
-      .catch(e => console.error('Failed to toggle repost', record, e))
+      .catch(e => store.log.error('Failed to toggle repost', e.toString()))
   }
   const onPressToggleUpvote = () => {
     item
       .toggleUpvote()
-      .catch(e => console.error('Failed to toggle upvote', record, e))
+      .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
   }
   const onCopyPostText = () => {
     Clipboard.setString(record.text)
@@ -87,7 +87,7 @@ export const FeedItem = observer(function ({
         Toast.show('Post deleted')
       },
       e => {
-        console.error(e)
+        store.log.error('Failed to delete post', e.toString())
         Toast.show('Failed to delete post, please try again')
       },
     )
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 175a582ce..409df05cb 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -23,15 +23,15 @@ export const ProfileFollowers = observer(function ProfileFollowers({
 
   useEffect(() => {
     if (view?.params.user === name) {
-      console.log('User followers doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching user followers', name)
     const newView = new UserFollowersViewModel(store, {user: name})
     setView(newView)
     newView
       .setup()
-      .catch(err => console.error('Failed to fetch user followers', err))
+      .catch(err =>
+        store.log.error('Failed to fetch user followers', err.toString()),
+      )
   }, [name, view?.params.user, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 2d40af243..f63cc0107 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -23,15 +23,15 @@ export const ProfileFollows = observer(function ProfileFollows({
 
   useEffect(() => {
     if (view?.params.user === name) {
-      console.log('User follows doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching user follows', name)
     const newView = new UserFollowsViewModel(store, {user: name})
     setView(newView)
     newView
       .setup()
-      .catch(err => console.error('Failed to fetch user follows', err))
+      .catch(err =>
+        store.log.error('Failed to fetch user follows', err.toString()),
+      )
   }, [name, view?.params.user, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 5f0fb6fe2..5a87401b4 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -52,7 +52,7 @@ export const ProfileHeader = observer(function ProfileHeader({
           }`,
         )
       },
-      err => console.error('Failed to toggle follow', err),
+      err => store.log.error('Failed to toggle follow', err.toString()),
     )
   }
   const onPressEditProfile = () => {
@@ -94,7 +94,7 @@ export const ProfileHeader = observer(function ProfileHeader({
       await view.muteAccount()
       Toast.show('Account muted')
     } catch (e: any) {
-      console.error(e)
+      store.log.error('Failed to mute account', e.toString())
       Toast.show(`There was an issue! ${e.toString()}`)
     }
   }
@@ -103,7 +103,7 @@ export const ProfileHeader = observer(function ProfileHeader({
       await view.unmuteAccount()
       Toast.show('Account unmuted')
     } catch (e: any) {
-      console.error(e)
+      store.log.error('Failed to unmute account', e.toString())
       Toast.show(`There was an issue! ${e.toString()}`)
     }
   }
diff --git a/src/view/com/profile/ProfileMembers.tsx b/src/view/com/profile/ProfileMembers.tsx
index 0e34865b9..bcba2a4da 100644
--- a/src/view/com/profile/ProfileMembers.tsx
+++ b/src/view/com/profile/ProfileMembers.tsx
@@ -16,13 +16,13 @@ export const ProfileMembers = observer(function ProfileMembers({
 
   useEffect(() => {
     if (view?.params.actor === name) {
-      console.log('Members doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching members', name)
     const newView = new MembersViewModel(store, {actor: name})
     setView(newView)
-    newView.setup().catch(err => console.error('Failed to fetch members', err))
+    newView
+      .setup()
+      .catch(err => store.log.error('Failed to fetch members', err.toString()))
   }, [name, view?.params.actor, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index a714c2db4..2e584b764 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -45,8 +45,7 @@ export const ViewHeader = observer(function ViewHeader({
   }
   const onPressReconnect = () => {
     store.session.connect().catch(e => {
-      // log for debugging but ignore otherwise
-      console.log(e)
+      store.log.warn('Failed to reconnect to server', e)
     })
   }
   if (typeof canGoBack === 'undefined') {
diff --git a/src/view/index.ts b/src/view/index.ts
index b38c0aa50..5602784f3 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -4,6 +4,7 @@ import {faAddressCard} from '@fortawesome/free-regular-svg-icons/faAddressCard'
 import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown'
 import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
 import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
+import {faAngleUp} from '@fortawesome/free-solid-svg-icons/faAngleUp'
 import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
 import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight'
 import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp'
@@ -38,6 +39,7 @@ import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
 import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
 import {faImage as farImage} from '@fortawesome/free-regular-svg-icons/faImage'
 import {faImage} from '@fortawesome/free-solid-svg-icons/faImage'
+import {faInfo} from '@fortawesome/free-solid-svg-icons/faInfo'
 import {faLink} from '@fortawesome/free-solid-svg-icons/faLink'
 import {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
 import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
@@ -71,6 +73,7 @@ export function setup() {
     faAngleDown,
     faAngleLeft,
     faAngleRight,
+    faAngleUp,
     faArrowLeft,
     faArrowRight,
     faArrowUp,
@@ -105,6 +108,7 @@ export function setup() {
     faHouse,
     faImage,
     farImage,
+    faInfo,
     faLink,
     faLock,
     faMagnifyingGlass,
diff --git a/src/view/routes.ts b/src/view/routes.ts
index 3717e0f05..0a2883e69 100644
--- a/src/view/routes.ts
+++ b/src/view/routes.ts
@@ -16,6 +16,7 @@ import {ProfileFollows} from './screens/ProfileFollows'
 import {ProfileMembers} from './screens/ProfileMembers'
 import {Settings} from './screens/Settings'
 import {Debug} from './screens/Debug'
+import {Log} from './screens/Log'
 
 export type ScreenParams = {
   navIdx: [number, number]
@@ -72,7 +73,8 @@ export const routes: Route[] = [
     'retweet',
     r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/reposted-by'),
   ],
-  [Debug, 'Debug', 'house', r('/debug')],
+  [Debug, 'Debug', 'house', r('/sys/debug')],
+  [Log, 'Log', 'house', r('/sys/log')],
 ]
 
 export function match(url: string): MatchResult {
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 118ba9ed8..dbf665837 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -35,9 +35,9 @@ export const Home = observer(function Home({
     if (store.me.mainFeed.isLoading) {
       return
     }
-    console.log('Polling home feed')
+    store.log.debug('Polling home feed')
     store.me.mainFeed.checkForLatest().catch(e => {
-      console.error('Failed to poll feed', e)
+      store.log.error('Failed to poll feed', e.toString())
     })
   }
 
@@ -49,12 +49,12 @@ export const Home = observer(function Home({
     }
 
     if (hasSetup) {
-      console.log('Updating home feed')
+      store.log.debug('Updating home feed')
       store.me.mainFeed.update()
       doPoll()
     } else {
       store.nav.setTitle(navIdx, 'Home')
-      console.log('Fetching home feed')
+      store.log.debug('Fetching home feed')
       store.me.mainFeed.setup().then(() => {
         if (aborted) return
         setHasSetup(true)
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
new file mode 100644
index 000000000..56337435f
--- /dev/null
+++ b/src/view/screens/Log.tsx
@@ -0,0 +1,100 @@
+import React, {useEffect} from 'react'
+import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useStores} from '../../state'
+import {ScreenParams} from '../routes'
+import {s} from '../lib/styles'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {Text} from '../com/util/text/Text'
+import {usePalette} from '../lib/hooks/usePalette'
+import {ago} from '../../lib/strings'
+
+export const Log = observer(function Log({navIdx, visible}: ScreenParams) {
+  const pal = usePalette('default')
+  const store = useStores()
+  const [expanded, setExpanded] = React.useState<string[]>([])
+
+  useEffect(() => {
+    if (!visible) {
+      return
+    }
+    store.shell.setMinimalShellMode(false)
+    store.nav.setTitle(navIdx, 'Log')
+  }, [visible, store])
+
+  const toggler = (id: string) => () => {
+    if (expanded.includes(id)) {
+      setExpanded(expanded.filter(v => v !== id))
+    } else {
+      setExpanded([...expanded, id])
+    }
+  }
+
+  return (
+    <View style={[s.flex1]}>
+      <ViewHeader title="Log" />
+      <ScrollView style={s.flex1}>
+        {store.log.entries
+          .slice(0)
+          .reverse()
+          .map(entry => {
+            return (
+              <View key={`entry-${entry.id}`}>
+                <TouchableOpacity
+                  style={[styles.entry, pal.border, pal.view]}
+                  onPress={toggler(entry.id)}>
+                  {entry.type === 'debug' ? (
+                    <FontAwesomeIcon icon="info" />
+                  ) : (
+                    <FontAwesomeIcon icon="exclamation" style={s.red3} />
+                  )}
+                  <Text type="body2" style={[styles.summary, pal.text]}>
+                    {entry.summary}
+                  </Text>
+                  {!!entry.details ? (
+                    <FontAwesomeIcon
+                      icon={
+                        expanded.includes(entry.id) ? 'angle-up' : 'angle-down'
+                      }
+                      style={s.mr5}
+                    />
+                  ) : undefined}
+                  <Text type="body2" style={[styles.ts, pal.textLight]}>
+                    {entry.ts ? ago(entry.ts) : ''}
+                  </Text>
+                </TouchableOpacity>
+                {expanded.includes(entry.id) ? (
+                  <View style={[pal.btn, styles.details]}>
+                    <Text type="body1" style={pal.text}>
+                      {entry.details}
+                    </Text>
+                  </View>
+                ) : undefined}
+              </View>
+            )
+          })}
+        <View style={{height: 100}} />
+      </ScrollView>
+    </View>
+  )
+})
+
+const styles = StyleSheet.create({
+  entry: {
+    flexDirection: 'row',
+    borderTopWidth: 1,
+    paddingVertical: 10,
+    paddingHorizontal: 6,
+  },
+  summary: {
+    flex: 1,
+  },
+  ts: {
+    width: 40,
+  },
+  details: {
+    paddingVertical: 10,
+    paddingHorizontal: 6,
+  },
+})
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 2257dd221..5a4d9c223 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -14,12 +14,12 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
     if (!visible) {
       return
     }
-    console.log('Updating notifications feed')
+    store.log.debug('Updating notifications feed')
     store.me.refreshMemberships() // needed for the invite notifications
     store.me.notifications
       .update()
       .catch(e => {
-        console.error('Error while updating notifications feed', e)
+        store.log.error('Error while updating notifications feed', e.toString())
       })
       .then(() => {
         store.me.notifications.updateReadState()
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 4caf144bf..86fde1374 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -31,7 +31,6 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
     setTitle()
     store.shell.setMinimalShellMode(false)
     if (!view.hasLoaded && !view.isLoading) {
-      console.log('Fetching post thread', uri)
       view.setup().then(
         () => {
           if (!aborted) {
@@ -39,14 +38,14 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
           }
         },
         err => {
-          console.error('Failed to fetch thread', err)
+          store.log.error('Failed to fetch thread', err.toString())
         },
       )
     }
     return () => {
       aborted = true
     }
-  }, [visible, store.nav, name])
+  }, [visible, store.nav, store.log, name])
 
   return (
     <View style={{flex: 1}}>
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 8dd2dbe33..af011f837 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -40,10 +40,8 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
       return
     }
     if (hasSetup) {
-      console.log('Updating profile for', params.name)
       uiState.update()
     } else {
-      console.log('Fetching profile for', params.name)
       store.nav.setTitle(navIdx, params.name)
       uiState.setup().then(() => {
         if (aborted) return
@@ -64,12 +62,19 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
   const onRefresh = () => {
     uiState
       .refresh()
-      .catch((err: any) => console.error('Failed to refresh', err))
+      .catch((err: any) =>
+        store.log.error('Failed to refresh user profile', err.toString()),
+      )
   }
   const onEndReached = () => {
     uiState
       .loadMore()
-      .catch((err: any) => console.error('Failed to load more', err))
+      .catch((err: any) =>
+        store.log.error(
+          'Failed to load more entries in user profile',
+          err.toString(),
+        ),
+      )
   }
   const onPressTryAgain = () => {
     uiState.setup()
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index d7565e9c8..39597152d 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -3,7 +3,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {useStores} from '../../state'
 import {ScreenParams} from '../routes'
-import {s, colors} from '../lib/styles'
+import {s} from '../lib/styles'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Link} from '../com/util/Link'
 import {Text} from '../com/util/text/Text'
@@ -32,7 +32,7 @@ export const Settings = observer(function Settings({
   return (
     <View style={[s.flex1]}>
       <ViewHeader title="Settings" />
-      <View style={[s.mt10, s.pl10, s.pr10]}>
+      <View style={[s.mt10, s.pl10, s.pr10, s.flex1]}>
         <View style={[s.flexRow]}>
           <Text style={pal.text}>Signed in as</Text>
           <View style={s.flex1} />
@@ -61,9 +61,23 @@ export const Settings = observer(function Settings({
             </View>
           </View>
         </Link>
-        <Link href="/debug" title="Debug tools">
+        <View style={s.flex1} />
+        <Text type="overline1" style={[s.mb5]}>
+          Advanced
+        </Text>
+        <Link
+          style={[pal.view, s.p10, s.mb2]}
+          href="/sys/log"
+          title="System log">
+          <Text style={pal.link}>System log</Text>
+        </Link>
+        <Link
+          style={[pal.view, s.p10, s.mb2]}
+          href="/sys/debug"
+          title="Debug tools">
           <Text style={pal.link}>Debug tools</Text>
         </Link>
+        <View style={{height: 100}} />
       </View>
     </View>
   )