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: {
height: '100%',
width: '33%',
minWidth: 325,
backgroundColor: 'yellow',
maxWidth: 300,
},
right: {
height: '100%',
display: 'flex',
flexGrow: 1,
},
channels: {
flexGrow: 1,
},
});

View File

@ -1,14 +1,15 @@
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 {BottomNavigation, Button, Text} from 'react-native-paper';
import {DisplayContext} from '../context/DisplayContext';
import {BottomNavigation, Surface, Menu, Button, Text} from 'react-native-paper';
import {Settings} from '../settings/Settings';
import {Channels} from '../channels/Channels';
import {Contacts} from '../contacts/Contacts';
import {Registry} from '../registry/Registry';
import {Profile} from '../profile/Profile';
import {Details} from '../details/Details';
import {Identity} from '../identity/Identity';
import {useSession} from './useSession.hook';
import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native';
import {createDrawerNavigator} from '@react-navigation/drawer';
@ -24,8 +25,8 @@ const ProfileDrawer = createDrawerNavigator();
const DetailsDrawer = createDrawerNavigator();
export function Session() {
const {state, actions} = useSession();
const scheme = useColorScheme();
const display = useContext(DisplayContext);
const [index, setIndex] = useState(0);
const [routes] = useState([
{
@ -47,7 +48,7 @@ export function Session() {
unfocusedIcon: 'cog-outline',
},
]);
const sessionNav = {settings: null};
const sessionNav = {strings: state.strings};
const renderScene = BottomNavigation.SceneMap({
channels: ChannelsRoute,
@ -57,7 +58,7 @@ export function Session() {
return (
<View style={styles.screen}>
{display.state.layout !== 'large' && (
{state.layout !== 'large' && (
<BottomNavigation
labeled={false}
navigationState={{index, routes}}
@ -65,7 +66,7 @@ export function Session() {
renderScene={renderScene}
/>
)}
{display.state.layout === 'large' && (
{state.layout === 'large' && (
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
<DetailsScreen nav={sessionNav} />
</NavigationContainer>
@ -169,14 +170,20 @@ function SettingsScreen({nav}) {
}
function HomeScreen({nav}) {
const [menu, setMenu] = useState(false);
return (
<SafeAreaView style={styles.frame}>
<View style={styles.frame}>
<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 style={styles.right}>
<Text>CONVERSATION</Text>
</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,
backgroundColor: 'transparent',
},
modalControls: {
display: 'flex',
flexDirection: 'row',
gap: 16,
paddingTop: 16,
justifyContent: 'flex-end',
},
header: {
fontSize: 22,
textAlign: 'center',
@ -83,7 +90,7 @@ export const styles = StyleSheet.create({
},
editDetails: {
position: 'absolute',
bottom: -12,
bottom: -11,
right: 12,
zIndex: 3,
},

View File

@ -11,8 +11,9 @@ export function Settings() {
const { state, actions } = useSettings();
const [alert, setAlert] = useState(false);
const [details, setDetails] = useState(false);
const [savingDetails, setSavingDetails] = useState(false);
const SelectImage = async () => {
const selectImage = async () => {
try {
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 });
@ -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 (
<>
<ScrollView bounces={false}>
<SafeAreaView style={styles.settings}>
<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 }} />
<View style={styles.editBar}>
<TouchableOpacity onPress={selectImage}>
<Surface style={styles.editBorder} elevation={0}>
<Text style={styles.editLogo}>{state.strings.edit}</Text>
</Surface>
</TouchableOpacity>
</View>
</TouchableOpacity>
</View>
<View style={styles.divider}>
<Divider style={styles.line} bold={true} />
@ -55,7 +74,7 @@ export function Settings() {
<Text style={styles.nameUnset}>{state.strings.name}</Text>
)}
{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.icon}>
@ -91,9 +110,11 @@ export function Settings() {
</SafeAreaView>
</ScrollView>
<Modal
animationType="fade"
transparent={true}
visible={alert}
onDismiss={() => setAlert(false)}
contentContainerStyle={styles.modal}>
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => setAlert(false)}>
<View style={styles.modal}>
<BlurView
style={styles.blur}
@ -101,16 +122,18 @@ export function Settings() {
blurAmount={2}
reducedTransparencyFallbackColor="dark"
/>
<Surface elevation={1} mode="flat" style={styles.content}>
<Text variant="titleLarge">{state.strings.error}</Text>
<Text variant="titleSmall">{state.strings.tryAgain}</Text>
<Button
mode="text"
style={styles.close}
onPress={() => setAlert(false)}>
{state.strings.close}
</Button>
</Surface>
<View style={styles.content}>
<Surface elevation={1} mode="flat" style={styles.surface}>
<Text variant="titleLarge">{state.strings.error}</Text>
<Text variant="titleSmall">{state.strings.tryAgain}</Text>
<Button
mode="text"
style={styles.close}
onPress={() => setAlert(false)}>
{state.strings.close}
</Button>
</Surface>
</View>
</View>
</Modal>
<Modal
@ -163,6 +186,11 @@ export function Settings() {
left={<TextInput.Icon style={styles.inputIcon} icon="book-open-outline" />}
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>
</KeyboardAwareScrollView>
</View>