rendering identity component

This commit is contained in:
balzack 2024-09-20 22:59:40 -07:00
parent 533336d21e
commit 2ef4d337d2
8 changed files with 240 additions and 29 deletions

View File

@ -0,0 +1,48 @@
import {StyleSheet} from 'react-native';
export const styles = StyleSheet.create({
identity: {
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-begin',
},
identityData: {
flexGrow: 1,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
padding: 4,
},
anchor: {
backgroundColor: 'red',
width: 1,
height: 1,
},
image: {
position: 'relative',
width: 48,
height: 48,
},
logo: {
aspectRatio: 1,
resizeMode: 'contain',
borderRadius: 4,
width: null,
height: null,
},
details: {
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
paddingLeft: 8,
paddingRight: 8,
width: 100,
},
name: {
fontSize: 14,
},
username: {
fontSize: 16,
},
});

View File

@ -0,0 +1,36 @@
import { useState } from 'react';
import { TouchableOpacity, SafeAreaView, View, Image } from 'react-native';
import { Icon, Text, Menu } from 'react-native-paper';
import { styles } from './Identity.styled';
import { useIdentity } from './useIdentity.hook';
export function Identity({ openSettings }) {
const [menu, setMenu] = useState(false);
const { state, actions } = useIdentity();
return (
<SafeAreaView style={styles.identity}>
<TouchableOpacity style={styles.identityData} activeOpacity={1} onPress={() => setMenu(true)}>
<View style={styles.image}>
<Image style={styles.logo} resizeMode={'contain'} source={{ uri: state.imageUrl }} />
</View>
<View style={styles.details}>
{state.profile.name && (
<Text style={styles.name} adjustsFontSizeToFit={true} numberOfLines={1}>{state.profile.name}</Text>
)}
<Text style={styles.username} adjustsFontSizeToFit={true} numberOfLines={1}>{`${state.profile.handle}${state.profile.node ? '/' + state.profile.node : ''}`}</Text>
</View>
<Icon size={18} source="chevron-right" />
</TouchableOpacity>
<Menu
visible={menu}
onDismiss={() => setMenu(false)}
anchorPosition="top"
anchor={<View style={styles.anchor}><Text> </Text></View>}>
<Menu.Item leadingIcon="cog-outline" onPress={() => {setMenu(false); openSettings()}} title={state.strings.settings} />
<Menu.Item leadingIcon="contacts-outline" onPress={() => {}} title={state.strings.contacts} />
<Menu.Item leadingIcon="logout" onPress={() => {}} title={state.strings.logout} />
</Menu>
</SafeAreaView>
)
}

View File

@ -0,0 +1,49 @@
import { useEffect, useState, useContext, useRef } from 'react'
import { AppContext } from '../context/AppContext'
import { DisplayContext } from '../context/DisplayContext';
import { ContextType } from '../context/ContextType'
export function useIdentity() {
const display = useContext(DisplayContext) as ContextType
const app = useContext(AppContext) as ContextType
const [state, setState] = useState({
all: false,
strings: display.state.strings,
profile: {} as Profile,
profileSet: false,
imageUrl: null,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateState = (value: any) => {
setState((s) => ({ ...s, ...value }))
}
useEffect(() => {
const identity = app.state.session?.getIdentity()
if (!identity) {
console.log('session not set in identity hook')
} else {
const setProfile = (profile: Profile) => {
updateState({
profile,
profileSet: true,
imageUrl: identity.getProfileImageUrl(),
})
}
identity.addProfileListener(setProfile)
return () => {
identity.removeProfileListener(setProfile)
}
}
}, [])
const actions = {
logout: async () => {
await app.actions.accountLogout();
}
}
return { state, actions }
}

View File

@ -12,12 +12,14 @@ export const styles = StyleSheet.create({
left: { left: {
height: '100%', height: '100%',
width: '33%', width: '33%',
minWidth: 325, maxWidth: 300,
backgroundColor: 'yellow',
}, },
right: { right: {
height: '100%', height: '100%',
display: 'flex', display: 'flex',
flexGrow: 1, flexGrow: 1,
}, },
channels: {
flexGrow: 1,
},
}); });

View File

@ -1,14 +1,15 @@
import React, {useState, useContext} from 'react'; import React, {useState, useContext} from 'react';
import {View, SafeAreaView, useColorScheme} from 'react-native'; import {View, useColorScheme} from 'react-native';
import {styles} from './Session.styled'; import {styles} from './Session.styled';
import {BottomNavigation, Button, Text} from 'react-native-paper'; import {BottomNavigation, Surface, Menu, Button, Text} from 'react-native-paper';
import {DisplayContext} from '../context/DisplayContext';
import {Settings} from '../settings/Settings'; import {Settings} from '../settings/Settings';
import {Channels} from '../channels/Channels'; import {Channels} from '../channels/Channels';
import {Contacts} from '../contacts/Contacts'; import {Contacts} from '../contacts/Contacts';
import {Registry} from '../registry/Registry'; import {Registry} from '../registry/Registry';
import {Profile} from '../profile/Profile'; import {Profile} from '../profile/Profile';
import {Details} from '../details/Details'; import {Details} from '../details/Details';
import {Identity} from '../identity/Identity';
import {useSession} from './useSession.hook';
import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native'; import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native';
import {createDrawerNavigator} from '@react-navigation/drawer'; import {createDrawerNavigator} from '@react-navigation/drawer';
@ -24,8 +25,8 @@ const ProfileDrawer = createDrawerNavigator();
const DetailsDrawer = createDrawerNavigator(); const DetailsDrawer = createDrawerNavigator();
export function Session() { export function Session() {
const {state, actions} = useSession();
const scheme = useColorScheme(); const scheme = useColorScheme();
const display = useContext(DisplayContext);
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
const [routes] = useState([ const [routes] = useState([
{ {
@ -47,7 +48,7 @@ export function Session() {
unfocusedIcon: 'cog-outline', unfocusedIcon: 'cog-outline',
}, },
]); ]);
const sessionNav = {settings: null}; const sessionNav = {strings: state.strings};
const renderScene = BottomNavigation.SceneMap({ const renderScene = BottomNavigation.SceneMap({
channels: ChannelsRoute, channels: ChannelsRoute,
@ -57,7 +58,7 @@ export function Session() {
return ( return (
<View style={styles.screen}> <View style={styles.screen}>
{display.state.layout !== 'large' && ( {state.layout !== 'large' && (
<BottomNavigation <BottomNavigation
labeled={false} labeled={false}
navigationState={{index, routes}} navigationState={{index, routes}}
@ -65,7 +66,7 @@ export function Session() {
renderScene={renderScene} renderScene={renderScene}
/> />
)} )}
{display.state.layout === 'large' && ( {state.layout === 'large' && (
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}> <NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
<DetailsScreen nav={sessionNav} /> <DetailsScreen nav={sessionNav} />
</NavigationContainer> </NavigationContainer>
@ -169,14 +170,20 @@ function SettingsScreen({nav}) {
} }
function HomeScreen({nav}) { function HomeScreen({nav}) {
const [menu, setMenu] = useState(false);
return ( return (
<SafeAreaView style={styles.frame}> <View style={styles.frame}>
<View style={styles.left}> <View style={styles.left}>
<Text onPress={() => nav.settings.openDrawer()}>IDENTITY</Text> <Surface elevation={2} mode="flat">
<Identity openSettings={nav.settings.openDrawer} />
</Surface>
<Surface style={styles.channels} elevation={1} mode="flat">
</Surface>
</View> </View>
<View style={styles.right}> <View style={styles.right}>
<Text>CONVERSATION</Text> <Text>CONVERSATION</Text>
</View> </View>
</SafeAreaView> </View>
); );
} }

View File

@ -0,0 +1,34 @@
import { useEffect, useState, useContext, useRef } from 'react'
import { AppContext } from '../context/AppContext'
import { DisplayContext } from '../context/DisplayContext'
import { ContextType } from '../context/ContextType'
const DEBOUNCE_MS = 1000
export function useSession() {
const display = useContext(DisplayContext) as ContextType
const app = useContext(AppContext) as ContextType
const [state, setState] = useState({
layout: null,
strings: {},
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateState = (value: any) => {
setState((s) => ({ ...s, ...value }))
}
useEffect(() => {
const { layout, strings } = display.state;
updateState({ layout, strings });
}, [display.state.layout, display.state.strings]);
const actions = {
logout: async () => {
await app.actions.accountLogout();
}
}
return { state, actions }
}

View File

@ -57,6 +57,13 @@ export const styles = StyleSheet.create({
right: 0, right: 0,
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
modalControls: {
display: 'flex',
flexDirection: 'row',
gap: 16,
paddingTop: 16,
justifyContent: 'flex-end',
},
header: { header: {
fontSize: 22, fontSize: 22,
textAlign: 'center', textAlign: 'center',
@ -83,7 +90,7 @@ export const styles = StyleSheet.create({
}, },
editDetails: { editDetails: {
position: 'absolute', position: 'absolute',
bottom: -12, bottom: -11,
right: 12, right: 12,
zIndex: 3, zIndex: 3,
}, },

View File

@ -11,8 +11,9 @@ export function Settings() {
const { state, actions } = useSettings(); const { state, actions } = useSettings();
const [alert, setAlert] = useState(false); const [alert, setAlert] = useState(false);
const [details, setDetails] = useState(false); const [details, setDetails] = useState(false);
const [savingDetails, setSavingDetails] = useState(false);
const SelectImage = async () => { const selectImage = async () => {
try { try {
const full = await ImagePicker.openPicker({ mediaType: 'photo', width: 256, height: 256 }); const full = await ImagePicker.openPicker({ mediaType: 'photo', width: 256, height: 256 });
const crop = await ImagePicker.openCropper({ path: full.path, width: 256, height: 256, cropperCircleOverlay: true, includeBase64: true }); const crop = await ImagePicker.openCropper({ path: full.path, width: 256, height: 256, cropperCircleOverlay: true, includeBase64: true });
@ -29,19 +30,37 @@ export function Settings() {
} }
} }
const saveDetails = async () => {
if (!savingDetails) {
setSavingDetails(true);
try {
await actions.setDetails();
setDetails(false);
}
catch (err) {
console.log(err);
setDetails(false);
setAlert(true);
}
setSavingDetails(false);
}
}
return ( return (
<> <>
<ScrollView bounces={false}> <ScrollView bounces={false}>
<SafeAreaView style={styles.settings}> <SafeAreaView style={styles.settings}>
<Text style={styles.header} adjustsFontSizeToFit={true} numberOfLines={1}>{`${state.profile.handle}${state.profile.node ? '/' + state.profile.node : ''}`}</Text> <Text style={styles.header} adjustsFontSizeToFit={true} numberOfLines={1}>{`${state.profile.handle}${state.profile.node ? '/' + state.profile.node : ''}`}</Text>
<TouchableOpacity style={styles.image} onPress={SelectImage}> <View style={styles.image}>
<Image style={styles.logo} resizeMode={'contain'} source={{ uri: state.imageUrl }} /> <Image style={styles.logo} resizeMode={'contain'} source={{ uri: state.imageUrl }} />
<View style={styles.editBar}> <View style={styles.editBar}>
<TouchableOpacity onPress={selectImage}>
<Surface style={styles.editBorder} elevation={0}> <Surface style={styles.editBorder} elevation={0}>
<Text style={styles.editLogo}>{state.strings.edit}</Text> <Text style={styles.editLogo}>{state.strings.edit}</Text>
</Surface> </Surface>
</TouchableOpacity>
</View> </View>
</TouchableOpacity> </View>
<View style={styles.divider}> <View style={styles.divider}>
<Divider style={styles.line} bold={true} /> <Divider style={styles.line} bold={true} />
@ -55,7 +74,7 @@ export function Settings() {
<Text style={styles.nameUnset}>{state.strings.name}</Text> <Text style={styles.nameUnset}>{state.strings.name}</Text>
)} )}
{state.profile.name && ( {state.profile.name && (
<Text style={styles.nameSet}>{state.profile.name}</Text> <Text style={styles.nameSet} adjustsFontSizeToFit={true} numberOfLines={1}>{state.profile.name}</Text>
)} )}
<View style={styles.attribute}> <View style={styles.attribute}>
<View style={styles.icon}> <View style={styles.icon}>
@ -91,9 +110,11 @@ export function Settings() {
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
<Modal <Modal
animationType="fade"
transparent={true}
visible={alert} visible={alert}
onDismiss={() => setAlert(false)} supportedOrientations={['portrait', 'landscape']}
contentContainerStyle={styles.modal}> onRequestClose={() => setAlert(false)}>
<View style={styles.modal}> <View style={styles.modal}>
<BlurView <BlurView
style={styles.blur} style={styles.blur}
@ -101,16 +122,18 @@ export function Settings() {
blurAmount={2} blurAmount={2}
reducedTransparencyFallbackColor="dark" reducedTransparencyFallbackColor="dark"
/> />
<Surface elevation={1} mode="flat" style={styles.content}> <View style={styles.content}>
<Text variant="titleLarge">{state.strings.error}</Text> <Surface elevation={1} mode="flat" style={styles.surface}>
<Text variant="titleSmall">{state.strings.tryAgain}</Text> <Text variant="titleLarge">{state.strings.error}</Text>
<Button <Text variant="titleSmall">{state.strings.tryAgain}</Text>
mode="text" <Button
style={styles.close} mode="text"
onPress={() => setAlert(false)}> style={styles.close}
{state.strings.close} onPress={() => setAlert(false)}>
</Button> {state.strings.close}
</Surface> </Button>
</Surface>
</View>
</View> </View>
</Modal> </Modal>
<Modal <Modal
@ -163,6 +186,11 @@ export function Settings() {
left={<TextInput.Icon style={styles.inputIcon} icon="book-open-outline" />} left={<TextInput.Icon style={styles.inputIcon} icon="book-open-outline" />}
onChangeText={value => actions.setDescription(value)} onChangeText={value => actions.setDescription(value)}
/> />
<View style={styles.modalControls}>
<Button mode="outlined" onPress={() => setDetails(false)}>{ state.strings.cancel }</Button>
<Button mode="contained" loading={savingDetails} onPress={saveDetails}>{ state.strings.save }</Button>
</View>
</Surface> </Surface>
</KeyboardAwareScrollView> </KeyboardAwareScrollView>
</View> </View>