Implement logging system

zio/stable
Paul Frazee 2023-01-02 17:38:13 -06:00
parent 99cec71ed7
commit f6a0e634d7
39 changed files with 442 additions and 125 deletions

View File

@ -24,19 +24,19 @@ export async function setupState() {
try { try {
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {} data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
rootStore.hydrate(data) rootStore.hydrate(data)
} catch (e) { } catch (e: any) {
console.error('Failed to load state from storage', e) rootStore.log.error('Failed to load state from storage', e.toString())
} }
console.log('Initial hydrate', rootStore.me) rootStore.log.debug('Initial hydrate')
rootStore.session rootStore.session
.connect() .connect()
.then(() => { .then(() => {
console.log('Session connected', rootStore.me) rootStore.log.debug('Session connected')
return rootStore.fetchStateUpdate() return rootStore.fetchStateUpdate()
}) })
.catch(e => { .catch((e: any) => {
console.log('Failed initial connect', e) rootStore.log.warn('Failed initial connect', e.toString())
}) })
// @ts-ignore .on() is correct -prf // @ts-ignore .on() is correct -prf
api.sessionManager.on('session', () => { api.sessionManager.on('session', () => {

View File

@ -99,7 +99,7 @@ export async function post(
) { ) {
encoding = 'image/jpeg' encoding = 'image/jpeg'
} else { } else {
console.error( store.log.warn(
'Unexpected image format for thumbnail, skipping', 'Unexpected image format for thumbnail, skipping',
thumbLocal.uri, thumbLocal.uri,
) )
@ -126,7 +126,10 @@ export async function post(
}, },
} as AppBskyEmbedExternal.Main } as AppBskyEmbedExternal.Main
} catch (e: any) { } 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(),
)
} }
} }
} }

View File

@ -405,7 +405,6 @@ export class FeedModel {
cursor = this.feed[res.data.feed.length - 1] cursor = this.feed[res.data.feed.length - 1]
? ts(this.feed[res.data.feed.length - 1]) ? ts(this.feed[res.data.feed.length - 1])
: undefined : undefined
console.log(numToFetch, cursor, res.data.feed.length)
} while (numToFetch > 0) } while (numToFetch > 0)
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {

View File

@ -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(),
})
}
}

View File

@ -104,13 +104,22 @@ export class MeModel {
}) })
await Promise.all([ await Promise.all([
this.memberships?.setup().catch(e => { 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 => { 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 => { 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 { } else {

View File

@ -149,7 +149,10 @@ export class NotificationsViewItemModel implements GroupedNotification {
depth: 0, depth: 0,
}) })
await this.additionalPost.setup().catch(e => { 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(), seenAt: new Date().toISOString(),
}) })
this.rootStore.me.clearNotificationCount() this.rootStore.me.clearNotificationCount()
} catch (e) { } catch (e: any) {
console.log('Failed to update notifications read state', e) this.rootStore.log.warn(
'Failed to update notifications read state',
e.toString(),
)
} }
} }
@ -350,7 +356,6 @@ export class NotificationsViewModel {
this._updateAll(res) this._updateAll(res)
numToFetch -= res.data.notifications.length numToFetch -= res.data.notifications.length
cursor = this.notifications[res.data.notifications.length - 1].indexedAt cursor = this.notifications[res.data.notifications.length - 1].indexedAt
console.log(numToFetch, cursor, res.data.notifications.length)
} while (numToFetch > 0) } while (numToFetch > 0)
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {
@ -379,9 +384,9 @@ export class NotificationsViewModel {
itemModels.push(itemModel) itemModels.push(itemModel)
} }
await Promise.all(promises).catch(e => { await Promise.all(promises).catch(e => {
console.error( this.rootStore.log.error(
'Uncaught failure during notifications-view _appendAll()', 'Uncaught failure during notifications-view _appendAll()',
e, e.toString(),
) )
}) })
runInAction(() => { runInAction(() => {

View File

@ -114,20 +114,28 @@ export class ProfileUiModel {
await Promise.all([ await Promise.all([
this.profile this.profile
.setup() .setup()
.catch(err => console.error('Failed to fetch profile', err)), .catch(err =>
this.rootStore.log.error('Failed to fetch profile', err.toString()),
),
this.feed this.feed
.setup() .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) { if (this.isUser) {
await this.memberships await this.memberships
.setup() .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) { if (this.isScene) {
await this.members await this.members
.setup() .setup()
.catch(err => console.error('Failed to fetch members', err)) .catch(err =>
this.rootStore.log.error('Failed to fetch members', err.toString()),
)
} }
} }

View File

@ -203,7 +203,6 @@ export class ProfileViewModel {
} }
private _replaceAll(res: GetProfile.Response) { private _replaceAll(res: GetProfile.Response) {
console.log(res.data)
this.did = res.data.did this.did = res.data.did
this.handle = res.data.handle this.handle = res.data.handle
Object.assign(this.declaration, res.data.declaration) Object.assign(this.declaration, res.data.declaration)

View File

@ -6,6 +6,7 @@ import {makeAutoObservable} from 'mobx'
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api' import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
import {createContext, useContext} from 'react' import {createContext, useContext} from 'react'
import {isObj, hasProp} from '../lib/type-guards' import {isObj, hasProp} from '../lib/type-guards'
import {LogModel} from './log'
import {SessionModel} from './session' import {SessionModel} from './session'
import {NavigationModel} from './navigation' import {NavigationModel} from './navigation'
import {ShellUiModel} from './shell-ui' import {ShellUiModel} from './shell-ui'
@ -16,6 +17,7 @@ import {OnboardModel} from './onboard'
import {isNetworkError} from '../../lib/errors' import {isNetworkError} from '../../lib/errors'
export class RootStoreModel { export class RootStoreModel {
log = new LogModel()
session = new SessionModel(this) session = new SessionModel(this)
nav = new NavigationModel() nav = new NavigationModel()
shell = new ShellUiModel() shell = new ShellUiModel()
@ -53,16 +55,17 @@ export class RootStoreModel {
await this.session.connect() await this.session.connect()
} }
await this.me.fetchStateUpdate() await this.me.fetchStateUpdate()
} catch (e: unknown) { } catch (e: any) {
if (isNetworkError(e)) { if (isNetworkError(e)) {
this.session.setOnline(false) // connection lost 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 { serialize(): unknown {
return { return {
log: this.log.serialize(),
session: this.session.serialize(), session: this.session.serialize(),
me: this.me.serialize(), me: this.me.serialize(),
nav: this.nav.serialize(), nav: this.nav.serialize(),
@ -73,8 +76,8 @@ export class RootStoreModel {
hydrate(v: unknown) { hydrate(v: unknown) {
if (isObj(v)) { if (isObj(v)) {
if (hasProp(v, 'session')) { if (hasProp(v, 'log')) {
this.session.hydrate(v.session) this.log.hydrate(v.log)
} }
if (hasProp(v, 'me')) { if (hasProp(v, 'me')) {
this.me.hydrate(v.me) this.me.hydrate(v.me)

View File

@ -121,11 +121,11 @@ export class SessionModel {
try { try {
const serviceUri = new URL(this.data.service) const serviceUri = new URL(this.data.service)
this.rootStore.api.xrpc.uri = serviceUri this.rootStore.api.xrpc.uri = serviceUri
} catch (e) { } catch (e: any) {
console.error( this.rootStore.log.error(
`Invalid service URL: ${this.data.service}. Resetting session.`, `Invalid service URL: ${this.data.service}. Resetting session.`,
e.toString(),
) )
console.error(e)
this.clear() this.clear()
return false return false
} }
@ -160,7 +160,10 @@ export class SessionModel {
this.rootStore.me.clear() this.rootStore.me.clear()
} }
this.rootStore.me.load().catch(e => { 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 return // success
} }
@ -204,7 +207,10 @@ export class SessionModel {
this.configureApi() this.configureApi()
this.setOnline(true, false) this.setOnline(true, false)
this.rootStore.me.load().catch(e => { 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.rootStore.onboard.start()
this.configureApi() this.configureApi()
this.rootStore.me.load().catch(e => { 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() { async logout() {
if (this.hasSession) { if (this.hasSession) {
this.rootStore.api.com.atproto.session.delete().catch((e: any) => { 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() this.rootStore.clearAll()

View File

@ -98,8 +98,11 @@ export class SuggestedInvitesView {
try { try {
// TODO need to fetch all! // TODO need to fetch all!
await this.sceneAssertionsView.setup() await this.sceneAssertionsView.setup()
} catch (e) { } catch (e: any) {
console.error(e) this.rootStore.log.error(
'Failed to fetch current scene members in suggested invites',
e.toString(),
)
this._xIdle( this._xIdle(
'Failed to fetch the current scene members. Check your internet connection and try again.', 'Failed to fetch the current scene members. Check your internet connection and try again.',
) )
@ -107,8 +110,11 @@ export class SuggestedInvitesView {
} }
try { try {
await this.myFollowsView.setup() await this.myFollowsView.setup()
} catch (e) { } catch (e: any) {
console.error(e) this.rootStore.log.error(
'Failed to fetch current followers in suggested invites',
e.toString(),
)
this._xIdle( this._xIdle(
'Failed to fetch the your current followers. Check your internet connection and try again.', 'Failed to fetch the your current followers. Check your internet connection and try again.',
) )

View File

@ -1,7 +1,6 @@
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {Image, StyleSheet, TouchableOpacity, ScrollView} from 'react-native' import {Image, StyleSheet, TouchableOpacity, ScrollView} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors} from '../../lib/styles'
import { import {
openPicker, openPicker,
openCamera, openCamera,
@ -9,6 +8,7 @@ import {
} from 'react-native-image-crop-picker' } from 'react-native-image-crop-picker'
import {compressIfNeeded} from '../../../lib/images' import {compressIfNeeded} from '../../../lib/images'
import {usePalette} from '../../lib/hooks/usePalette' import {usePalette} from '../../lib/hooks/usePalette'
import {useStores} from '../../../state'
const IMAGE_PARAMS = { const IMAGE_PARAMS = {
width: 1000, width: 1000,
@ -28,6 +28,7 @@ export const PhotoCarouselPicker = ({
localPhotos: any localPhotos: any
}) => { }) => {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const handleOpenCamera = useCallback(async () => { const handleOpenCamera = useCallback(async () => {
try { try {
const cameraRes = await openCamera({ const cameraRes = await openCamera({
@ -37,11 +38,11 @@ export const PhotoCarouselPicker = ({
}) })
const uri = await compressIfNeeded(cameraRes, 300000) const uri = await compressIfNeeded(cameraRes, 300000)
onSelectPhotos([uri, ...selectedPhotos]) onSelectPhotos([uri, ...selectedPhotos])
} catch (err) { } catch (err: any) {
// ignore // ignore
console.log('Error using camera', err) store.log.warn('Error using camera', err.toString())
} }
}, [selectedPhotos, onSelectPhotos]) }, [store.log, selectedPhotos, onSelectPhotos])
const handleSelectPhoto = useCallback( const handleSelectPhoto = useCallback(
async (uri: string) => { async (uri: string) => {
@ -53,12 +54,12 @@ export const PhotoCarouselPicker = ({
}) })
const finalUri = await compressIfNeeded(cropperRes, 300000) const finalUri = await compressIfNeeded(cropperRes, 300000)
onSelectPhotos([finalUri, ...selectedPhotos]) onSelectPhotos([finalUri, ...selectedPhotos])
} catch (err) { } catch (err: any) {
// ignore // ignore
console.log('Error selecting photo', err) store.log.warn('Error selecting photo', err.toString())
} }
}, },
[selectedPhotos, onSelectPhotos], [store.log, selectedPhotos, onSelectPhotos],
) )
const handleOpenGallery = useCallback(() => { const handleOpenGallery = useCallback(() => {

View File

@ -42,11 +42,12 @@ export const SuggestedFollows = observer(
) )
useEffect(() => { useEffect(() => {
console.log('Fetching suggested actors')
view view
.setup() .setup()
.catch((err: any) => console.error('Failed to fetch suggestions', err)) .catch((err: any) =>
}, [view]) store.log.error('Failed to fetch suggestions', err.toString()),
)
}, [view, store.log])
useEffect(() => { useEffect(() => {
if (!view.isLoading && !view.hasError && !view.hasContent) { if (!view.isLoading && !view.hasError && !view.hasContent) {
@ -57,14 +58,16 @@ export const SuggestedFollows = observer(
const onPressTryAgain = () => const onPressTryAgain = () =>
view view
.setup() .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) => { const onPressFollow = async (item: SuggestedActor) => {
try { try {
const res = await apilib.follow(store, item.did, item.declaration.cid) const res = await apilib.follow(store, item.did, item.declaration.cid)
setFollows({[item.did]: res.uri, ...follows}) setFollows({[item.did]: res.uri, ...follows})
} catch (e) { } catch (e: any) {
console.log(e) store.log.error('Failed fo create follow', {error: e.toString(), item})
Toast.show('An issue occurred, please try again.') Toast.show('An issue occurred, please try again.')
} }
} }
@ -72,8 +75,8 @@ export const SuggestedFollows = observer(
try { try {
await apilib.unfollow(store, follows[item.did]) await apilib.unfollow(store, follows[item.did])
setFollows(_omit(follows, [item.did])) setFollows(_omit(follows, [item.did]))
} catch (e) { } catch (e: any) {
console.log(e) store.log.error('Failed fo delete follow', {error: e.toString(), item})
Toast.show('An issue occurred, please try again.') Toast.show('An issue occurred, please try again.')
} }
} }

View File

@ -44,7 +44,6 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
let aborted = false let aborted = false
setError('') setError('')
setServiceDescription(undefined) setServiceDescription(undefined)
console.log('Fetching service description', serviceUrl)
store.session.describeService(serviceUrl).then( store.session.describeService(serviceUrl).then(
desc => { desc => {
if (aborted) return if (aborted) return
@ -53,7 +52,10 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
}, },
err => { err => {
if (aborted) return if (aborted) return
console.error(err) store.log.warn(
`Failed to fetch service description for ${serviceUrl}`,
err.toString(),
)
setError( setError(
'Unable to contact your service. Please check your Internet connection.', 'Unable to contact your service. Please check your Internet connection.',
) )
@ -62,7 +64,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
return () => { return () => {
aborted = true aborted = true
} }
}, [serviceUrl, store.session]) }, [serviceUrl, store.session, store.log])
const onPressSelectService = () => { const onPressSelectService = () => {
store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl)) store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl))
@ -98,7 +100,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
errMsg = errMsg =
'Invite code not accepted. Check that you input it correctly and try again.' '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) setIsProcessing(false)
setError(errMsg.replace(/^Error:/, '')) setError(errMsg.replace(/^Error:/, ''))
} }

View File

@ -44,7 +44,6 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
useEffect(() => { useEffect(() => {
let aborted = false let aborted = false
setError('') setError('')
console.log('Fetching service description', serviceUrl)
store.session.describeService(serviceUrl).then( store.session.describeService(serviceUrl).then(
desc => { desc => {
if (aborted) return if (aborted) return
@ -52,7 +51,10 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
}, },
err => { err => {
if (aborted) return if (aborted) return
console.error(err) store.log.warn(
`Failed to fetch service description for ${serviceUrl}`,
err.toString(),
)
setError( setError(
'Unable to contact your service. Please check your Internet connection.', 'Unable to contact your service. Please check your Internet connection.',
) )
@ -61,7 +63,7 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
return () => { return () => {
aborted = true aborted = true
} }
}, [store.session, serviceUrl]) }, [store.session, store.log, serviceUrl])
return ( return (
<KeyboardAvoidingView behavior="padding" style={{flex: 1}}> <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
@ -169,7 +171,7 @@ const LoginForm = ({
}) })
} catch (e: any) { } catch (e: any) {
const errMsg = e.toString() const errMsg = e.toString()
console.log(e) store.log.warn('Failed to login', e.toString())
setIsProcessing(false) setIsProcessing(false)
if (errMsg.includes('Authentication Required')) { if (errMsg.includes('Authentication Required')) {
setError('Invalid username or password') setError('Invalid username or password')
@ -305,7 +307,7 @@ const ForgotPasswordForm = ({
onEmailSent() onEmailSent()
} catch (e: any) { } catch (e: any) {
const errMsg = e.toString() const errMsg = e.toString()
console.log(e) store.log.warn('Failed to request password reset', e.toString())
setIsProcessing(false) setIsProcessing(false)
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(
@ -417,7 +419,7 @@ const SetNewPasswordForm = ({
onPasswordSet() onPasswordSet()
} catch (e: any) { } catch (e: any) {
const errMsg = e.toString() const errMsg = e.toString()
console.log(e) store.log.warn('Failed to set new password', e.toString())
setIsProcessing(false) setIsProcessing(false)
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(

View File

@ -55,7 +55,13 @@ export function Component({}: {}) {
displayName, displayName,
description, 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 // follow the scene
await store.api.app.bsky.graph.follow await store.api.app.bsky.graph.follow
.create( .create(
@ -70,7 +76,13 @@ export function Component({}: {}) {
createdAt: new Date().toISOString(), 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') Toast.show('Scene created')
store.shell.closeModal() store.shell.closeModal()
store.nav.navigate(`/profile/${fullHandle}`) store.nav.navigate(`/profile/${fullHandle}`)
@ -82,7 +94,7 @@ export function Component({}: {}) {
} else if (e instanceof AppBskyActorCreateScene.HandleNotAvailableError) { } else if (e instanceof AppBskyActorCreateScene.HandleNotAvailableError) {
setError(`The handle "${handle}" is not available.`) setError(`The handle "${handle}" is not available.`)
} else { } else {
console.error(e) store.log.error('Failed to create scene', e.toString())
setError( setError(
'Failed to create the scene. Check your internet connection and try again.', 'Failed to create the scene. Check your internet connection and try again.',
) )

View File

@ -84,9 +84,9 @@ export const Component = observer(function Component({
) )
setCreatedInvites({[follow.did]: assertionUri, ...createdInvites}) setCreatedInvites({[follow.did]: assertionUri, ...createdInvites})
Toast.show('Invite sent') Toast.show('Invite sent')
} catch (e) { } catch (e: any) {
setError('There was an issue with the invite. Please try again.') 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) => { const onPressUndo = async (subjectDid: string, assertionUri: string) => {
@ -98,9 +98,9 @@ export const Component = observer(function Component({
rkey: urip.rkey, rkey: urip.rkey,
}) })
setCreatedInvites(_omit(createdInvites, [subjectDid])) setCreatedInvites(_omit(createdInvites, [subjectDid]))
} catch (e) { } catch (e: any) {
setError('There was an issue with the invite. Please try again.') 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, ...deletedPendingInvites,
}) })
Toast.show('Invite removed') Toast.show('Invite removed')
} catch (e) { } catch (e: any) {
setError('There was an issue with the invite. Please try again.') setError('There was an issue with the invite. Please try again.')
console.error(e) store.log.error('Failed to delete an invite', e.toString())
} }
} }

View File

@ -36,10 +36,24 @@ export const Feed = observer(function Feed({
return <FeedItem item={item} /> return <FeedItem item={item} />
} }
const onRefresh = () => { 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 = () => { 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 let data
if (view.hasLoaded) { if (view.hasLoaded) {

View File

@ -22,15 +22,15 @@ export const PostRepostedBy = observer(function PostRepostedBy({
useEffect(() => { useEffect(() => {
if (view?.params.uri === uri) { if (view?.params.uri === uri) {
console.log('Reposted by doing nothing')
return // no change needed? or trigger refresh? return // no change needed? or trigger refresh?
} }
console.log('Fetching Reposted by', uri)
const newView = new RepostedByViewModel(store, {uri}) const newView = new RepostedByViewModel(store, {uri})
setView(newView) setView(newView)
newView newView
.setup() .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]) }, [uri, view?.params.uri, store])
const onRefresh = () => { const onRefresh = () => {

View File

@ -18,7 +18,14 @@ export const PostThread = observer(function PostThread({
const ref = useRef<FlatList>(null) const ref = useRef<FlatList>(null)
const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
const onRefresh = () => { 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 onLayout = () => {
const index = posts.findIndex(post => post._isHighlightedPost) const index = posts.findIndex(post => post._isHighlightedPost)

View File

@ -72,12 +72,12 @@ export const PostThreadItem = observer(function PostThreadItem({
const onPressToggleRepost = () => { const onPressToggleRepost = () => {
item item
.toggleRepost() .toggleRepost()
.catch(e => console.error('Failed to toggle repost', record, e)) .catch(e => store.log.error('Failed to toggle repost', e.toString()))
} }
const onPressToggleUpvote = () => { const onPressToggleUpvote = () => {
item item
.toggleUpvote() .toggleUpvote()
.catch(e => console.error('Failed to toggle upvote', record, e)) .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
} }
const onCopyPostText = () => { const onCopyPostText = () => {
Clipboard.setString(record.text) Clipboard.setString(record.text)
@ -90,7 +90,7 @@ export const PostThreadItem = observer(function PostThreadItem({
Toast.show('Post deleted') Toast.show('Post deleted')
}, },
e => { e => {
console.error(e) store.log.error('Failed to delete post', e.toString())
Toast.show('Failed to delete post, please try again') Toast.show('Failed to delete post, please try again')
}, },
) )

View File

@ -24,13 +24,13 @@ export const PostVotedBy = observer(function PostVotedBy({
useEffect(() => { useEffect(() => {
if (view?.params.uri === uri) { if (view?.params.uri === uri) {
console.log('Voted by doing nothing')
return // no change needed? or trigger refresh? return // no change needed? or trigger refresh?
} }
console.log('Fetching voted by', uri)
const newView = new VotesViewModel(store, {uri, direction}) const newView = new VotesViewModel(store, {uri, direction})
setView(newView) 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]) }, [uri, view?.params.uri, store])
const onRefresh = () => { const onRefresh = () => {

View File

@ -47,7 +47,9 @@ export const Post = observer(function Post({
} }
const newView = new PostThreadViewModel(store, {uri, depth: 0}) const newView = new PostThreadViewModel(store, {uri, depth: 0})
setView(newView) 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]) }, [initView, uri, view?.params.uri, store])
// deleted // deleted
@ -110,12 +112,12 @@ export const Post = observer(function Post({
const onPressToggleRepost = () => { const onPressToggleRepost = () => {
item item
.toggleRepost() .toggleRepost()
.catch(e => console.error('Failed to toggle repost', record, e)) .catch(e => store.log.error('Failed to toggle repost', e.toString()))
} }
const onPressToggleUpvote = () => { const onPressToggleUpvote = () => {
item item
.toggleUpvote() .toggleUpvote()
.catch(e => console.error('Failed to toggle upvote', record, e)) .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
} }
const onCopyPostText = () => { const onCopyPostText = () => {
Clipboard.setString(record.text) Clipboard.setString(record.text)
@ -128,7 +130,7 @@ export const Post = observer(function Post({
Toast.show('Post deleted') Toast.show('Post deleted')
}, },
e => { e => {
console.error(e) store.log.error('Failed to delete post', e.toString())
Toast.show('Failed to delete post, please try again') Toast.show('Failed to delete post, please try again')
}, },
) )

View File

@ -23,7 +23,9 @@ export const PostText = observer(function PostText({
} }
const newModel = new PostModel(store, uri) const newModel = new PostModel(store, uri)
setModel(newModel) 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]) }, [uri, model?.uri, store])
// loading // loading

View File

@ -53,10 +53,21 @@ export const Feed = observer(function Feed({
} }
} }
const onRefresh = () => { 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 = () => { 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 let data
if (feed.hasLoaded) { if (feed.hasLoaded) {

View File

@ -69,12 +69,12 @@ export const FeedItem = observer(function ({
const onPressToggleRepost = () => { const onPressToggleRepost = () => {
item item
.toggleRepost() .toggleRepost()
.catch(e => console.error('Failed to toggle repost', record, e)) .catch(e => store.log.error('Failed to toggle repost', e.toString()))
} }
const onPressToggleUpvote = () => { const onPressToggleUpvote = () => {
item item
.toggleUpvote() .toggleUpvote()
.catch(e => console.error('Failed to toggle upvote', record, e)) .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
} }
const onCopyPostText = () => { const onCopyPostText = () => {
Clipboard.setString(record.text) Clipboard.setString(record.text)
@ -87,7 +87,7 @@ export const FeedItem = observer(function ({
Toast.show('Post deleted') Toast.show('Post deleted')
}, },
e => { e => {
console.error(e) store.log.error('Failed to delete post', e.toString())
Toast.show('Failed to delete post, please try again') Toast.show('Failed to delete post, please try again')
}, },
) )

View File

@ -23,15 +23,15 @@ export const ProfileFollowers = observer(function ProfileFollowers({
useEffect(() => { useEffect(() => {
if (view?.params.user === name) { if (view?.params.user === name) {
console.log('User followers doing nothing')
return // no change needed? or trigger refresh? return // no change needed? or trigger refresh?
} }
console.log('Fetching user followers', name)
const newView = new UserFollowersViewModel(store, {user: name}) const newView = new UserFollowersViewModel(store, {user: name})
setView(newView) setView(newView)
newView newView
.setup() .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]) }, [name, view?.params.user, store])
const onRefresh = () => { const onRefresh = () => {

View File

@ -23,15 +23,15 @@ export const ProfileFollows = observer(function ProfileFollows({
useEffect(() => { useEffect(() => {
if (view?.params.user === name) { if (view?.params.user === name) {
console.log('User follows doing nothing')
return // no change needed? or trigger refresh? return // no change needed? or trigger refresh?
} }
console.log('Fetching user follows', name)
const newView = new UserFollowsViewModel(store, {user: name}) const newView = new UserFollowsViewModel(store, {user: name})
setView(newView) setView(newView)
newView newView
.setup() .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]) }, [name, view?.params.user, store])
const onRefresh = () => { const onRefresh = () => {

View File

@ -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 = () => { const onPressEditProfile = () => {
@ -94,7 +94,7 @@ export const ProfileHeader = observer(function ProfileHeader({
await view.muteAccount() await view.muteAccount()
Toast.show('Account muted') Toast.show('Account muted')
} catch (e: any) { } catch (e: any) {
console.error(e) store.log.error('Failed to mute account', e.toString())
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(`There was an issue! ${e.toString()}`)
} }
} }
@ -103,7 +103,7 @@ export const ProfileHeader = observer(function ProfileHeader({
await view.unmuteAccount() await view.unmuteAccount()
Toast.show('Account unmuted') Toast.show('Account unmuted')
} catch (e: any) { } catch (e: any) {
console.error(e) store.log.error('Failed to unmute account', e.toString())
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(`There was an issue! ${e.toString()}`)
} }
} }

View File

@ -16,13 +16,13 @@ export const ProfileMembers = observer(function ProfileMembers({
useEffect(() => { useEffect(() => {
if (view?.params.actor === name) { if (view?.params.actor === name) {
console.log('Members doing nothing')
return // no change needed? or trigger refresh? return // no change needed? or trigger refresh?
} }
console.log('Fetching members', name)
const newView = new MembersViewModel(store, {actor: name}) const newView = new MembersViewModel(store, {actor: name})
setView(newView) 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]) }, [name, view?.params.actor, store])
const onRefresh = () => { const onRefresh = () => {

View File

@ -45,8 +45,7 @@ export const ViewHeader = observer(function ViewHeader({
} }
const onPressReconnect = () => { const onPressReconnect = () => {
store.session.connect().catch(e => { store.session.connect().catch(e => {
// log for debugging but ignore otherwise store.log.warn('Failed to reconnect to server', e)
console.log(e)
}) })
} }
if (typeof canGoBack === 'undefined') { if (typeof canGoBack === 'undefined') {

View File

@ -4,6 +4,7 @@ import {faAddressCard} from '@fortawesome/free-regular-svg-icons/faAddressCard'
import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown' import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown'
import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft' import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight' 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 {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight' import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight'
import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp' 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 {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
import {faImage as farImage} from '@fortawesome/free-regular-svg-icons/faImage' import {faImage as farImage} from '@fortawesome/free-regular-svg-icons/faImage'
import {faImage} from '@fortawesome/free-solid-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 {faLink} from '@fortawesome/free-solid-svg-icons/faLink'
import {faLock} from '@fortawesome/free-solid-svg-icons/faLock' import {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
@ -71,6 +73,7 @@ export function setup() {
faAngleDown, faAngleDown,
faAngleLeft, faAngleLeft,
faAngleRight, faAngleRight,
faAngleUp,
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faArrowUp, faArrowUp,
@ -105,6 +108,7 @@ export function setup() {
faHouse, faHouse,
faImage, faImage,
farImage, farImage,
faInfo,
faLink, faLink,
faLock, faLock,
faMagnifyingGlass, faMagnifyingGlass,

View File

@ -16,6 +16,7 @@ import {ProfileFollows} from './screens/ProfileFollows'
import {ProfileMembers} from './screens/ProfileMembers' import {ProfileMembers} from './screens/ProfileMembers'
import {Settings} from './screens/Settings' import {Settings} from './screens/Settings'
import {Debug} from './screens/Debug' import {Debug} from './screens/Debug'
import {Log} from './screens/Log'
export type ScreenParams = { export type ScreenParams = {
navIdx: [number, number] navIdx: [number, number]
@ -72,7 +73,8 @@ export const routes: Route[] = [
'retweet', 'retweet',
r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/reposted-by'), 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 { export function match(url: string): MatchResult {

View File

@ -35,9 +35,9 @@ export const Home = observer(function Home({
if (store.me.mainFeed.isLoading) { if (store.me.mainFeed.isLoading) {
return return
} }
console.log('Polling home feed') store.log.debug('Polling home feed')
store.me.mainFeed.checkForLatest().catch(e => { 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) { if (hasSetup) {
console.log('Updating home feed') store.log.debug('Updating home feed')
store.me.mainFeed.update() store.me.mainFeed.update()
doPoll() doPoll()
} else { } else {
store.nav.setTitle(navIdx, 'Home') store.nav.setTitle(navIdx, 'Home')
console.log('Fetching home feed') store.log.debug('Fetching home feed')
store.me.mainFeed.setup().then(() => { store.me.mainFeed.setup().then(() => {
if (aborted) return if (aborted) return
setHasSetup(true) setHasSetup(true)

View File

@ -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,
},
})

View File

@ -14,12 +14,12 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
if (!visible) { if (!visible) {
return return
} }
console.log('Updating notifications feed') store.log.debug('Updating notifications feed')
store.me.refreshMemberships() // needed for the invite notifications store.me.refreshMemberships() // needed for the invite notifications
store.me.notifications store.me.notifications
.update() .update()
.catch(e => { .catch(e => {
console.error('Error while updating notifications feed', e) store.log.error('Error while updating notifications feed', e.toString())
}) })
.then(() => { .then(() => {
store.me.notifications.updateReadState() store.me.notifications.updateReadState()

View File

@ -31,7 +31,6 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
setTitle() setTitle()
store.shell.setMinimalShellMode(false) store.shell.setMinimalShellMode(false)
if (!view.hasLoaded && !view.isLoading) { if (!view.hasLoaded && !view.isLoading) {
console.log('Fetching post thread', uri)
view.setup().then( view.setup().then(
() => { () => {
if (!aborted) { if (!aborted) {
@ -39,14 +38,14 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
} }
}, },
err => { err => {
console.error('Failed to fetch thread', err) store.log.error('Failed to fetch thread', err.toString())
}, },
) )
} }
return () => { return () => {
aborted = true aborted = true
} }
}, [visible, store.nav, name]) }, [visible, store.nav, store.log, name])
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>

View File

@ -40,10 +40,8 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
return return
} }
if (hasSetup) { if (hasSetup) {
console.log('Updating profile for', params.name)
uiState.update() uiState.update()
} else { } else {
console.log('Fetching profile for', params.name)
store.nav.setTitle(navIdx, params.name) store.nav.setTitle(navIdx, params.name)
uiState.setup().then(() => { uiState.setup().then(() => {
if (aborted) return if (aborted) return
@ -64,12 +62,19 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
const onRefresh = () => { const onRefresh = () => {
uiState uiState
.refresh() .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 = () => { const onEndReached = () => {
uiState uiState
.loadMore() .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 = () => { const onPressTryAgain = () => {
uiState.setup() uiState.setup()

View File

@ -3,7 +3,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {useStores} from '../../state' import {useStores} from '../../state'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {s, colors} from '../lib/styles' import {s} from '../lib/styles'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Link} from '../com/util/Link' import {Link} from '../com/util/Link'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
@ -32,7 +32,7 @@ export const Settings = observer(function Settings({
return ( return (
<View style={[s.flex1]}> <View style={[s.flex1]}>
<ViewHeader title="Settings" /> <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]}> <View style={[s.flexRow]}>
<Text style={pal.text}>Signed in as</Text> <Text style={pal.text}>Signed in as</Text>
<View style={s.flex1} /> <View style={s.flex1} />
@ -61,9 +61,23 @@ export const Settings = observer(function Settings({
</View> </View>
</View> </View>
</Link> </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> <Text style={pal.link}>Debug tools</Text>
</Link> </Link>
<View style={{height: 100}} />
</View> </View>
</View> </View>
) )