mirror of
https://github.com/balzack/databag.git
synced 2025-05-05 07:55:15 +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: {
|
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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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,
|
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,
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user