adding contact profile screen

This commit is contained in:
balzack 2024-10-22 22:08:23 -07:00
parent 192fcd1b6c
commit 32aaa05200
7 changed files with 512 additions and 19 deletions

View File

@ -86,7 +86,7 @@ const databagColors = {
inverseOnSurface: 'rgb(46, 49, 46)',
inversePrimary: 'rgb(0, 108, 71)',
elevation: {
level0: 'rgb(25, 28, 26)',
level0: 'rgb(0, 0, 0)',
level1: 'rgb(29, 38, 33)',
level2: 'rgb(32, 43, 37)',
level3: 'rgb(35, 49, 41)',

View File

@ -2,6 +2,14 @@ import {NativeModules, Platform} from 'react-native';
const Strings = [
{
unknownStatus: 'Unsaved Contact',
savedStatus: 'Saved Contact',
pendingStatus: 'Unknown Contact Request',
connectingStatus: 'Connection Requested',
requestedStatus: 'Connection Requested by Contact',
connectedStatus: 'Connected Contact',
offsyncStatus: 'Offsync Contact',
// settings screen
languageCode: 'en',
visibleRegistry: 'Visible in Registry',
@ -219,6 +227,14 @@ const Strings = [
'Are you sure you want to disable multi-factor authentication',
},
{
unknownStatus: 'Contact Inconnu',
savedStatus: 'Contact Enregistré',
pendingStatus: 'Demande de Contact Inconnue',
connectingStatus: 'Demande de Connexion Envoyée',
requestedStatus: 'Demande de Connexion par le Contact',
connectedStatus: 'Contact Connecté',
offsyncStatus: 'Contact Désynchronisé',
languageCode: 'fr',
visibleRegistry: 'Visible dans le Registre',
edit: 'Modifier',
@ -434,6 +450,14 @@ const Strings = [
"Êtes-vous sûr de vouloir désactiver l'authentification multi-facteurs",
},
{
unknownStatus: 'Contacto Desconocido',
savedStatus: 'Contacto Guardado',
pendingStatus: 'Solicitud de Contacto Desconocido',
connectingStatus: 'Solicitud de Conexión Enviada',
requestedStatus: 'Solicitud de Conexión por el Contacto',
connectedStatus: 'Contacto Conectado',
offsyncStatus: 'Contacto Fuera de Sincronización',
languageCode: 'es',
visibleRegistry: 'Visible en el Registro',
edit: 'Editar',
@ -648,6 +672,14 @@ const Strings = [
'¿Estás seguro de que quieres desactivar la autenticación de dos factores?',
},
{
unknownStatus: 'Unbekannter Kontakt',
savedStatus: 'Gespeicherter Kontakt',
pendingStatus: 'Unbekannte Kontaktanfrage',
connectingStatus: 'Verbindungsanfrage Gesendet',
requestedStatus: 'Verbindungsanfrage vom Kontakt',
connectedStatus: 'Verbunden Kontakt',
offsyncStatus: 'Unsynchronisierter Kontakt',
languageCode: 'de',
visibleRegistry: 'Sichtbar in der Registrierung',
edit: 'Bearbeiten',
@ -863,6 +895,14 @@ const Strings = [
'Sind Sie sicher, dass Sie die Zwei-Faktor-Authentifizierung deaktivieren möchten?',
},
{
unknownStatus: 'Contato Desconhecido',
savedStatus: 'Contato Salvo',
pendingStatus: 'Solicitação de Contato Desconhecido',
connectingStatus: 'Solicitação de Conexão Enviada',
requestedStatus: 'Solicitação de Conexão pelo Contato',
connectedStatus: 'Contato Conectado',
offsyncStatus: 'Contato Fora de Sincronização',
languageCode: 'pt',
visibleRegistry: 'Visível no Registro',
edit: 'Editar',
@ -1063,6 +1103,14 @@ const Strings = [
'Tem certeza de que deseja desativar a autenticação de dois fatores?',
},
{
unknownStatus: 'Неизвестный Контакт',
savedStatus: 'Сохранённый Контакт',
pendingStatus: 'Запрос Неизвестного Контакта',
connectingStatus: 'Запрос на Подключение Отправлен',
requestedStatus: 'Запрос на Подключение от Контакта',
connectedStatus: 'Подключённый Контакт',
offsyncStatus: 'Несинхронизированный Контакт',
languageCode: 'ru',
visibleRegistry: 'Видимый в реестре',
edit: 'Редактировать',

View File

@ -0,0 +1,181 @@
import {StyleSheet} from 'react-native';
import {Colors} from '../constants/Colors';
export const styles = StyleSheet.create({
profile: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
height: '100%',
},
body: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
height: '100%',
paddingLeft: 16,
paddingRight: 16,
},
header: {
position: 'relative',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingRight: 8,
paddingLeft: 8,
width: '100%',
zIndex: 1,
},
headerLabel: {
flexShrink: 1,
fontSize: 22,
textAlign: 'center',
textWrap: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
paddingTop: 8,
flexGrow: 1,
verticalAlign: 'baseline',
lineHeight: 22,
},
spaceHolder: {
width: 64,
},
back: {
flexShrink: 0,
marginRight: 0,
marginLeft: 0,
backgroundColor: 'transparent',
},
image: {
position: 'relative',
width: '90%',
maxWidth: 250,
marginTop: 16,
marginBottom: 8,
},
logo: {
aspectRatio: 1,
resizeMode: 'contain',
borderRadius: 8,
width: null,
height: null,
borderWidth: 1,
},
line: {
marginTop: 16,
marginBottom: 16,
height: 2,
width: '100%',
},
attributes: {
display: 'flex',
flexDirection: 'column',
gap: 8,
width: '100%',
paddingTop: 12,
},
attribute: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingLeft: 32,
paddingRight: 32,
},
icon: {
flexShrink: 0,
width: 32,
display: 'flex',
justifyContent: 'flex-begin',
height: '100%',
backgroundColor: 'transparent',
},
label: {
fontSize: 16,
},
labelUnset: {
fontSize: 16,
fontStyle: 'italic',
flexGrow: 1,
},
labelSet: {
fontSize: 16,
flexGrow: 1,
},
nameSet: {
fontSize: 24,
width: '100%',
paddingLeft: 32,
paddingRight: 72,
},
nameUnset: {
fontSize: 24,
width: '100%',
paddingLeft: 32,
paddingRight: 32,
fontStyle: 'italic',
},
status: {
flexShrink: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
unknownStatus: {
color: Colors.unsaved,
fontSize: 14,
},
savedStatus: {
color: Colors.confirmed,
fontSize: 14,
},
pendingStatus: {
color: Colors.pending,
fontSize: 14,
},
requestedStatus: {
color: Colors.requested,
fontSize: 14,
},
connectingStatus: {
color: Colors.connecting,
fontSize: 14,
},
connectedStatus: {
color: Colors.connected,
fontSize: 14,
},
offsyncStatus: {
color: Colors.offsync,
fontSize: 14,
},
modal: {
display: 'flex',
width: '100%',
height: '100%',
alignItems: 'center',
},
blur: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
},
content: {
display: 'flex',
justifyContent: 'center',
height: '100%',
gap: 8,
},
close: {
paddingTop: 8,
},
surface: {
padding: 16,
},
});

View File

@ -1,8 +1,9 @@
import React from 'react';
import { IconButton } from 'react-native-paper';
import { SafeAreaView, View } from 'react-native';
import React, {useState} from 'react';
import { Button, Surface, Icon, Text, IconButton, Divider } from 'react-native-paper';
import { Modal, Image, SafeAreaView, View } from 'react-native';
import { styles } from './Profile.styled';
import { useProfile } from './useProfile.hook';
import {BlurView} from '@react-native-community/blur';
export type ContactParams = {
guid: string;
@ -17,14 +18,107 @@ export type ContactParams = {
offsync?: boolean;
}
export function Profile({ close }) {
export function Profile({ close, params }) {
const [ alert, setAlert ] = useState(false);
const { state, actions } = useProfile(params);
return (
<View style={styles.profile}>
<SafeAreaView style={styles.header}>
{ close && (
<IconButton style={styles.close} compact="true" mode="contained" icon="arrow-left" size={24} onPress={close} />
)}
<View style={styles.spaceHolder}>
{ close && (
<IconButton style={styles.back} compact="true" mode="contained" icon="arrow-left" size={24} onPress={close} />
)}
</View>
<Text
style={styles.headerLabel}
adjustsFontSizeToFit={true}
numberOfLines={1}>{`${state.handle}${
state.node ? '/' + state.node : ''
}`}</Text>
<View style={styles.spaceHolder}></View>
</SafeAreaView>
<View style={styles.image}>
<Image
style={styles.logo}
resizeMode={'contain'}
source={{uri: state.imageUrl}}
/>
</View>
<View style={styles.body}>
<Divider style={styles.line} bold={true} />
<View style={styles.attributes}>
{!state.name && (
<Text style={styles.nameUnset}>{state.strings.name}</Text>
)}
{state.name && (
<Text
style={styles.nameSet}
adjustsFontSizeToFit={true}
numberOfLines={1}>
{state.name}
</Text>
)}
<View style={styles.attribute}>
<View style={styles.icon}>
<Icon size={24} source="map-marker-outline" />
</View>
{!state.location && (
<Text style={styles.labelUnset}>{state.strings.location}</Text>
)}
{state.location && (
<Text style={styles.labelSet}>{state.location}</Text>
)}
</View>
<View style={styles.attribute}>
<View style={styles.icon}>
<Icon size={24} source="book-open-outline" />
</View>
{!state.description && (
<Text style={styles.labelUnset}>
{state.strings.description}
</Text>
)}
{state.description && (
<Text style={styles.labelSet}>{state.description}</Text>
)}
</View>
</View>
<Divider style={styles.line} bold={true} />
<View style={styles.status}>
<Text style={styles[state.statusLabel]}>{ state.strings[state.statusLabel] }</Text>
</View>
</View>
<Modal
animationType="fade"
transparent={true}
visible={alert}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => setAlert(false)}>
<View style={styles.modal}>
<BlurView
style={styles.blur}
blurType="dark"
blurAmount={2}
reducedTransparencyFallbackColor="dark"
/>
<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>
</View>
)
}

View File

@ -0,0 +1,146 @@
import { useState, useContext, useEffect } from 'react'
import { AppContext } from '../context/AppContext'
import { DisplayContext } from '../context/DisplayContext';
import { ContextType } from '../context/ContextType'
import { Card } from 'databag-client-sdk'
import { ContactParams } from './Profile';
export function useProfile(params: ProfileParams) {
const app = useContext(AppContext) as ContextType
const display = useContext(DisplayContext) as ContextType
const [state, setState] = useState({
strings: display.state.strings,
cards: [] as Card[],
guid: '',
name: '',
handle: '',
node: '',
location: '',
description: '',
imageUrl: null as string | null,
cardId: null as string | null,
status: '',
offsync: false,
statusLabel: '',
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateState = (value: any) => {
setState((s) => ({ ...s, ...value }))
}
useEffect(() => {
const guid = params.guid;
const handle = params.handle ? params.handle : '';
const node = params.node ? params.node : '';
const name = params.name ? params.name : '';
const location = params.location ? params.location : '';
const description = params.description ? params.description : '';
const imageUrl = params.imageUrl ? params.imageUrl : null;
const cardId = params.cardId ? params.cardId : null;
const status = params.status ? params.status : '';
const offsync = params.offsync ? params.offsync : false;
updateState({ guid, handle, node, name, location, description, imageUrl, cardId, status, offsync });
}, [params]);
const getStatusLabel = (card?: Card) => {
if (card) {
const { status, offsync } = card;
if (status === 'confirmed') {
return 'savedStatus'
}
if (status === 'pending') {
return 'pendingStatus'
}
if (status === 'requested') {
return 'requestedStatus'
}
if (status === 'connecting') {
return 'connectingStatus'
}
if (status === 'connected' && !offsync) {
return 'connectedStatus'
}
if (status === 'connected' && offsync) {
return 'offsyncStatus'
}
}
return 'unknownStatus'
}
useEffect(() => {
const card = state.cards.find(card => card.guid === state.guid);
const statusLabel = getStatusLabel(card);
if (card) {
const { handle, node, name, location, description, imageUrl, cardId, status, offsync } = card;
updateState({ handle, node, name, location, description, imageUrl, cardId, status, offsync, statusLabel });
} else {
updateState({ cardId: null, status: '', offsync: false, statusLabel });
}
}, [state.cards, state.guid]);
useEffect(() => {
const contact = app.state.session?.getContact();
const setCards = (cards: Card[]) => {
updateState({ cards });
};
contact.addCardListener(setCards);
return () => {
contact.removeCardListener(setCards);
}
}, [])
const actions = {
save: async () => {
const contact = app.state.session?.getContact();
await contact.addCard(state.node, state.guid);
},
remove: async () => {
const contact = app.state.session?.getContact();
await contact.removeCard(state.cardId);
},
connect: async () => {
const contact = app.state.session?.getContact();
await contact.connectCard(state.cardId);
},
disconnect: async () => {
const contact = app.state.session?.getContact();
await contact.disconnectCard(state.cardId);
},
ignore: async () => {
const contact = app.state.session?.getContact();
await contact.ignoreCard(state.cardId);
},
deny: async () => {
const contact = app.state.session?.getContact();
await contact.denyCard(state.cardId);
},
confirm: async () => {
const contact = app.state.session?.getContact();
await contact.confirmCard(state.cardId);
},
cancel: async () => {
const contact = app.state.session?.getContact();
await contact.disconnectCard(state.cardId);
},
accept: async () => {
const contact = app.state.session?.getContact();
await contact.connectCard(state.cardId);
},
resync: async () => {
const contact = app.state.session?.getContact();
await contact.resyncCard(state.cardId);
},
block: async () => {
const contact = app.state.session?.getContact();
await contact.setBlockedCard(state.cardId, true);
},
report: async () => {
const contact = app.state.session?.getContact();
await contact.flagCard(state.cardId);
},
}
return { state, actions }
}

View File

@ -53,7 +53,9 @@ export function Session() {
<ContactTab scheme={scheme} />
</View>
<View style={{...styles.body, display: tab === 'settings' ? 'flex' : 'none'}}>
<Settings showLogout={true} />
<Surface elevation={0}>
<Settings showLogout={true} />
</Surface>
</View>
<View style={styles.tabs}>
{ tab === 'content' && (
@ -104,17 +106,29 @@ function ContentTab({ scheme }: { scheme: string }) {
}
function ContactTab({ scheme }: { scheme: string }) {
const [contactParams, setContactParams] = useState({ guid: '' } as ContactParams);
const openContact = (params: ContactParams, nav) => {
setContactParams(params);
nav.navigate('profile');
}
return (
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
<ContactStack.Navigator initialRouteName="contacts" screenOptions={{ headerShown: false }}>
<ContactStack.Screen name="contacts" options={{ headerBackTitleVisible: false }}>
{(props) => <Contacts openRegistry={()=>{props.navigation.navigate('registry')}} openContact={(params: ContactParams)=>{props.navigation.navigate('profile')}} />}
{(props) => <Contacts openRegistry={()=>{props.navigation.navigate('registry')}} openContact={(params: ContactParams)=>{
setContactParams(params);
props.navigation.navigate('profile')
}} />}
</ContactStack.Screen>
<ContactStack.Screen name="registry" options={{ headerBackTitleVisible: false, ...TransitionPresets.ScaleFromCenterAndroid }}>
{(props) => <Registry close={props.navigation.goBack} openContact={(params: ContactParams)=>{props.navigation.navigate('profile')}} />}
{(props) => <Registry close={props.navigation.goBack} openContact={(params: ContactParams)=>{
setContactParams(params);
props.navigation.navigate('profile')
}} />}
</ContactStack.Screen>
<ContactStack.Screen name="profile" options={{ headerBackTitleVisible: false, ...TransitionPresets.ScaleFromCenterAndroid }}>
{(props) => <Profile close={props.navigation.goBack} />}
{(props) => <Profile close={props.navigation.goBack} params={contactParams} />}
</ContactStack.Screen>
</ContactStack.Navigator>
</NavigationContainer>
@ -141,10 +155,16 @@ function DetailsScreen({nav}) {
}
function ProfileScreen({nav}) {
const [contactParams, setContactParams] = useState({ guid: '' } as ContactParams);
const openContact = (params: ContactParams, open: ()=>{}) => {
setContactParams(params);
open();
}
return (
<ProfileDrawer.Navigator
id="ProfileDrawer"
drawerContent={Profile}
drawerContent={() => (<Profile params={contactParams} />)}
screenOptions={{
drawerStyle: {width: 300},
drawerPosition: 'right',
@ -153,7 +173,7 @@ function ProfileScreen({nav}) {
}}>
<ProfileDrawer.Screen name="registry">
{({navigation}) => (
<RegistryScreen nav={{...nav, profile: navigation}} />
<RegistryScreen nav={{...nav, profile: navigation, openContact}} />
)}
</ProfileDrawer.Screen>
</ProfileDrawer.Navigator>
@ -166,7 +186,7 @@ function RegistryScreen({nav}) {
id="RegistryDrawer"
drawerContent={() => (
<Surface elevation={1}>
<Registry openContact={(params: ContactParams)=>{nav.profile.openDrawer()}} />
<Registry openContact={(params: ContactParams)=>{nav.openContact(params, nav.profile.openDrawer)}} />
</Surface>
)}
screenOptions={{
@ -190,7 +210,7 @@ function ContactsScreen({nav}) {
id="ContactsDrawer"
drawerContent={() => (
<Surface elevation={1}>
<Contacts openRegistry={nav.registry.openDrawer} openContact={(params: ContactParams)=>{nav.profile.openDrawer()}} />
<Contacts openRegistry={nav.registry.openDrawer} openContact={(params: ContactParams)=>{nav.openContact(params, nav.profile.openDrawer)}} />
</Surface>
)}
screenOptions={{
@ -212,7 +232,11 @@ function SettingsScreen({nav}) {
return (
<SettingsDrawer.Navigator
id="SettingsDrawer"
drawerContent={() => (<Settings />)}
drawerContent={() => (
<Surface elevation={1}>
<Settings />
</Surface>
)}
screenOptions={{
drawerStyle: {width: '50%'},
drawerPosition: 'right',

View File

@ -326,7 +326,7 @@ export function Settings({showLogout}: {showLogout: boolean}) {
};
return (
<Surface elevation={0}>
<View>
<ScrollView bounces={false} showsVerticalScrollIndicator={false} style={{ width: '100%', height: '100%' }}>
<View style={styles.settings}>
<Text
@ -1427,6 +1427,6 @@ export function Settings({showLogout}: {showLogout: boolean}) {
</KeyboardAwareScrollView>
</View>
</Modal>
</Surface>
</View>
);
}