diff options
39 files changed, 442 insertions, 125 deletions
diff --git a/src/state/index.ts b/src/state/index.ts index aa24eb27e..a8b95cc65 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -24,19 +24,19 @@ export async function setupState() { try { data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {} rootStore.hydrate(data) - } catch (e) { - console.error('Failed to load state from storage', e) + } catch (e: any) { + rootStore.log.error('Failed to load state from storage', e.toString()) } - console.log('Initial hydrate', rootStore.me) + rootStore.log.debug('Initial hydrate') rootStore.session .connect() .then(() => { - console.log('Session connected', rootStore.me) + rootStore.log.debug('Session connected') return rootStore.fetchStateUpdate() }) - .catch(e => { - console.log('Failed initial connect', e) + .catch((e: any) => { + rootStore.log.warn('Failed initial connect', e.toString()) }) // @ts-ignore .on() is correct -prf api.sessionManager.on('session', () => { diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index 31df20468..6bbc43271 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -99,7 +99,7 @@ export async function post( ) { encoding = 'image/jpeg' } else { - console.error( + store.log.warn( 'Unexpected image format for thumbnail, skipping', thumbLocal.uri, ) @@ -126,7 +126,10 @@ export async function post( }, } as AppBskyEmbedExternal.Main } catch (e: any) { - console.error('Failed to fetch link meta', link.value, e) + store.log.warn( + `Failed to fetch link meta for ${link.value}`, + e.toString(), + ) } } } diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index b5a3b29d7..c8827b1fb 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -405,7 +405,6 @@ export class FeedModel { cursor = this.feed[res.data.feed.length - 1] ? ts(this.feed[res.data.feed.length - 1]) : undefined - console.log(numToFetch, cursor, res.data.feed.length) } while (numToFetch > 0) this._xIdle() } catch (e: any) { diff --git a/src/state/models/log.ts b/src/state/models/log.ts new file mode 100644 index 000000000..42172a3b1 --- /dev/null +++ b/src/state/models/log.ts @@ -0,0 +1,94 @@ +import {makeAutoObservable} from 'mobx' +import {isObj, hasProp} from '../lib/type-guards' + +interface LogEntry { + id: string + type?: string + summary?: string + details?: string + ts?: number +} + +let _lastTs: string +let _lastId: string +function genId(): string { + let candidate = String(Date.now()) + if (_lastTs === candidate) { + const id = _lastId + 'x' + _lastId = id + return id + } + _lastTs = candidate + _lastId = candidate + return candidate +} + +export class LogModel { + entries: LogEntry[] = [] + + constructor() { + makeAutoObservable(this, {serialize: false, hydrate: false}) + } + + serialize(): unknown { + return { + entries: this.entries.slice(-100), + } + } + + hydrate(v: unknown) { + if (isObj(v)) { + if (hasProp(v, 'entries') && Array.isArray(v.entries)) { + this.entries = v.entries.filter( + e => isObj(e) && hasProp(e, 'id') && typeof e.id === 'string', + ) + } + } + } + + private add(entry: LogEntry) { + this.entries.push(entry) + } + + debug(summary: string, details?: any) { + if (details && typeof details !== 'string') { + details = JSON.stringify(details, null, 2) + } + console.debug(summary, details || '') + this.add({ + id: genId(), + type: 'debug', + summary, + details, + ts: Date.now(), + }) + } + + warn(summary: string, details?: any) { + if (details && typeof details !== 'string') { + details = JSON.stringify(details, null, 2) + } + console.warn(summary, details || '') + this.add({ + id: genId(), + type: 'warn', + summary, + details, + ts: Date.now(), + }) + } + + error(summary: string, details?: any) { + if (details && typeof details !== 'string') { + details = JSON.stringify(details, null, 2) + } + console.error(summary, details || '') + this.add({ + id: genId(), + type: 'error', + summary, + details, + ts: Date.now(), + }) + } +} diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 9591acb80..ae1e6aed2 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -104,13 +104,22 @@ export class MeModel { }) await Promise.all([ this.memberships?.setup().catch(e => { - console.error('Failed to setup memberships model', e) + this.rootStore.log.error( + 'Failed to setup memberships model', + e.toString(), + ) }), this.mainFeed.setup().catch(e => { - console.error('Failed to setup main feed model', e) + this.rootStore.log.error( + 'Failed to setup main feed model', + e.toString(), + ) }), this.notifications.setup().catch(e => { - console.error('Failed to setup notifications model', e) + this.rootStore.log.error( + 'Failed to setup notifications model', + e.toString(), + ) }), ]) } else { diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts index c1ee78d41..38a8ca133 100644 --- a/src/state/models/notifications-view.ts +++ b/src/state/models/notifications-view.ts @@ -149,7 +149,10 @@ export class NotificationsViewItemModel implements GroupedNotification { depth: 0, }) await this.additionalPost.setup().catch(e => { - console.error('Failed to load post needed by notification', e) + this.rootStore.log.error( + 'Failed to load post needed by notification', + e.toString(), + ) }) } } @@ -262,8 +265,11 @@ export class NotificationsViewModel { seenAt: new Date().toISOString(), }) this.rootStore.me.clearNotificationCount() - } catch (e) { - console.log('Failed to update notifications read state', e) + } catch (e: any) { + this.rootStore.log.warn( + 'Failed to update notifications read state', + e.toString(), + ) } } @@ -350,7 +356,6 @@ export class NotificationsViewModel { this._updateAll(res) numToFetch -= res.data.notifications.length cursor = this.notifications[res.data.notifications.length - 1].indexedAt - console.log(numToFetch, cursor, res.data.notifications.length) } while (numToFetch > 0) this._xIdle() } catch (e: any) { @@ -379,9 +384,9 @@ export class NotificationsViewModel { itemModels.push(itemModel) } await Promise.all(promises).catch(e => { - console.error( + this.rootStore.log.error( 'Uncaught failure during notifications-view _appendAll()', - e, + e.toString(), ) }) runInAction(() => { diff --git a/src/state/models/profile-ui.ts b/src/state/models/profile-ui.ts index d0eb1f858..081160e65 100644 --- a/src/state/models/profile-ui.ts +++ b/src/state/models/profile-ui.ts @@ -114,20 +114,28 @@ export class ProfileUiModel { await Promise.all([ this.profile .setup() - .catch(err => console.error('Failed to fetch profile', err)), + .catch(err => + this.rootStore.log.error('Failed to fetch profile', err.toString()), + ), this.feed .setup() - .catch(err => console.error('Failed to fetch feed', err)), + .catch(err => + this.rootStore.log.error('Failed to fetch feed', err.toString()), + ), ]) if (this.isUser) { await this.memberships .setup() - .catch(err => console.error('Failed to fetch members', err)) + .catch(err => + this.rootStore.log.error('Failed to fetch members', err.toString()), + ) } if (this.isScene) { await this.members .setup() - .catch(err => console.error('Failed to fetch members', err)) + .catch(err => + this.rootStore.log.error('Failed to fetch members', err.toString()), + ) } } diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts index 2a69a1345..1c825a482 100644 --- a/src/state/models/profile-view.ts +++ b/src/state/models/profile-view.ts @@ -203,7 +203,6 @@ export class ProfileViewModel { } private _replaceAll(res: GetProfile.Response) { - console.log(res.data) this.did = res.data.did this.handle = res.data.handle Object.assign(this.declaration, res.data.declaration) diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 54578b4a5..0166b67e6 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -6,6 +6,7 @@ import {makeAutoObservable} from 'mobx' import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' import {createContext, useContext} from 'react' import {isObj, hasProp} from '../lib/type-guards' +import {LogModel} from './log' import {SessionModel} from './session' import {NavigationModel} from './navigation' import {ShellUiModel} from './shell-ui' @@ -16,6 +17,7 @@ import {OnboardModel} from './onboard' import {isNetworkError} from '../../lib/errors' export class RootStoreModel { + log = new LogModel() session = new SessionModel(this) nav = new NavigationModel() shell = new ShellUiModel() @@ -53,16 +55,17 @@ export class RootStoreModel { await this.session.connect() } await this.me.fetchStateUpdate() - } catch (e: unknown) { + } catch (e: any) { if (isNetworkError(e)) { this.session.setOnline(false) // connection lost } - console.error('Failed to fetch latest state', e) + this.log.error('Failed to fetch latest state', e.toString()) } } serialize(): unknown { return { + log: this.log.serialize(), session: this.session.serialize(), me: this.me.serialize(), nav: this.nav.serialize(), @@ -73,8 +76,8 @@ export class RootStoreModel { hydrate(v: unknown) { if (isObj(v)) { - if (hasProp(v, 'session')) { - this.session.hydrate(v.session) + if (hasProp(v, 'log')) { + this.log.hydrate(v.log) } if (hasProp(v, 'me')) { this.me.hydrate(v.me) diff --git a/src/state/models/session.ts b/src/state/models/session.ts index febffcec3..3efb5d2a6 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -121,11 +121,11 @@ export class SessionModel { try { const serviceUri = new URL(this.data.service) this.rootStore.api.xrpc.uri = serviceUri - } catch (e) { - console.error( + } catch (e: any) { + this.rootStore.log.error( `Invalid service URL: ${this.data.service}. Resetting session.`, + e.toString(), ) - console.error(e) this.clear() return false } @@ -160,7 +160,10 @@ export class SessionModel { this.rootStore.me.clear() } this.rootStore.me.load().catch(e => { - console.error('Failed to fetch local user information', e) + this.rootStore.log.error( + 'Failed to fetch local user information', + e.toString(), + ) }) return // success } @@ -204,7 +207,10 @@ export class SessionModel { this.configureApi() this.setOnline(true, false) this.rootStore.me.load().catch(e => { - console.error('Failed to fetch local user information', e) + this.rootStore.log.error( + 'Failed to fetch local user information', + e.toString(), + ) }) } } @@ -240,7 +246,10 @@ export class SessionModel { this.rootStore.onboard.start() this.configureApi() this.rootStore.me.load().catch(e => { - console.error('Failed to fetch local user information', e) + this.rootStore.log.error( + 'Failed to fetch local user information', + e.toString(), + ) }) } } @@ -248,7 +257,10 @@ export class SessionModel { async logout() { if (this.hasSession) { this.rootStore.api.com.atproto.session.delete().catch((e: any) => { - console.error('(Minor issue) Failed to delete session on the server', e) + this.rootStore.log.warn( + '(Minor issue) Failed to delete session on the server', + e, + ) }) } this.rootStore.clearAll() diff --git a/src/state/models/suggested-invites-view.ts b/src/state/models/suggested-invites-view.ts index 33ca7396e..eb0665bca 100644 --- a/src/state/models/suggested-invites-view.ts +++ b/src/state/models/suggested-invites-view.ts @@ -98,8 +98,11 @@ export class SuggestedInvitesView { try { // TODO need to fetch all! await this.sceneAssertionsView.setup() - } catch (e) { - console.error(e) + } catch (e: any) { + this.rootStore.log.error( + 'Failed to fetch current scene members in suggested invites', + e.toString(), + ) this._xIdle( 'Failed to fetch the current scene members. Check your internet connection and try again.', ) @@ -107,8 +110,11 @@ export class SuggestedInvitesView { } try { await this.myFollowsView.setup() - } catch (e) { - console.error(e) + } catch (e: any) { + this.rootStore.log.error( + 'Failed to fetch current followers in suggested invites', + e.toString(), + ) this._xIdle( 'Failed to fetch the your current followers. Check your internet connection and try again.', ) 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> ) |