mirror of
https://github.com/balzack/databag.git
synced 2025-04-24 10:35:23 +00:00
rendering identity component
This commit is contained in:
parent
533336d21e
commit
2ef4d337d2
48
app/client/mobile/src/identity/Identity.styled.ts
Normal file
48
app/client/mobile/src/identity/Identity.styled.ts
Normal 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,
|
||||
},
|
||||
});
|
36
app/client/mobile/src/identity/Identity.tsx
Normal file
36
app/client/mobile/src/identity/Identity.tsx
Normal 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>
|
||||
)
|
||||
}
|
49
app/client/mobile/src/identity/useIdentity.hook.ts
Normal file
49
app/client/mobile/src/identity/useIdentity.hook.ts
Normal 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 }
|
||||
}
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
34
app/client/mobile/src/session/useSession.hook.ts
Normal file
34
app/client/mobile/src/session/useSession.hook.ts
Normal 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 }
|
||||
}
|
@ -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,
|
||||
},
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user