diff --git a/app/mobile/ios/Databag.xcodeproj/project.pbxproj b/app/mobile/ios/Databag.xcodeproj/project.pbxproj
index d6343f17..e18215b7 100644
--- a/app/mobile/ios/Databag.xcodeproj/project.pbxproj
+++ b/app/mobile/ios/Databag.xcodeproj/project.pbxproj
@@ -567,7 +567,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "c++14";
+ CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
@@ -639,7 +639,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "c++14";
+ CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
diff --git a/app/mobile/ios/Podfile.lock b/app/mobile/ios/Podfile.lock
index da67571b..1e6d904b 100644
--- a/app/mobile/ios/Podfile.lock
+++ b/app/mobile/ios/Podfile.lock
@@ -428,6 +428,15 @@ PODS:
- RNFBApp
- RNGestureHandler (2.9.0):
- React-Core
+ - RNImageCropPicker (0.39.0):
+ - React-Core
+ - React-RCTImage
+ - RNImageCropPicker/QBImagePickerController (= 0.39.0)
+ - TOCropViewController
+ - RNImageCropPicker/QBImagePickerController (0.39.0):
+ - React-Core
+ - React-RCTImage
+ - TOCropViewController
- RNReanimated (2.14.4):
- DoubleConversion
- FBLazyVector
@@ -460,6 +469,7 @@ PODS:
- React-RCTImage
- RNVectorIcons (9.2.0):
- React-Core
+ - TOCropViewController (2.6.1)
- Yoga (1.14.0)
DEPENDENCIES:
@@ -506,6 +516,7 @@ DEPENDENCIES:
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
+ - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
@@ -525,6 +536,7 @@ SPEC REPOS:
- libevent
- nanopb
- PromisesObjC
+ - TOCropViewController
EXTERNAL SOURCES:
boost:
@@ -609,6 +621,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-firebase/messaging"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
+ RNImageCropPicker:
+ :path: "../node_modules/react-native-image-crop-picker"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNScreens:
@@ -672,9 +686,11 @@ SPEC CHECKSUMS:
RNFBApp: 4f8ea53443d52c7db793234d2398a357fc6cfbf1
RNFBMessaging: c686471358d20d54f716a8b7b7f10f8944c966ec
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
+ RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332
RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128
RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
+ TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
Yoga: 5ed1699acbba8863755998a4245daa200ff3817b
PODFILE CHECKSUM: 8a4cbf61c865c6e404d389f26395fa04926b8f43
diff --git a/app/mobile/package.json b/app/mobile/package.json
index 5612b57f..ae430929 100644
--- a/app/mobile/package.json
+++ b/app/mobile/package.json
@@ -20,11 +20,13 @@
"@react-navigation/stack": "^6.3.14",
"axios": "^1.3.3",
"crypto-js": "^4.1.1",
+ "moment": "^2.29.4",
"react": "18.2.0",
"react-native": "0.71.3",
"react-native-base64": "^0.2.1",
"react-native-device-info": "^10.4.0",
"react-native-gesture-handler": "^2.9.0",
+ "react-native-image-crop-picker": "^0.39.0",
"react-native-reanimated": "^2.14.4",
"react-native-rsa-native": "^2.0.5",
"react-native-safe-area-context": "^4.5.0",
diff --git a/app/mobile/src/context/useAppContext.hook.js b/app/mobile/src/context/useAppContext.hook.js
index e33510bf..c101612a 100644
--- a/app/mobile/src/context/useAppContext.hook.js
+++ b/app/mobile/src/context/useAppContext.hook.js
@@ -16,7 +16,7 @@ import messaging from '@react-native-firebase/messaging';
export function useAppContext() {
const [state, setState] = useState({
session: null,
- status: 'disconnected',
+ status: null,
loggingOut: false,
adminToken: null,
version: getVersion(),
diff --git a/app/mobile/src/session/Session.jsx b/app/mobile/src/session/Session.jsx
index 2b73a19f..14d05d5a 100644
--- a/app/mobile/src/session/Session.jsx
+++ b/app/mobile/src/session/Session.jsx
@@ -1,4 +1,4 @@
-import { View, TouchableOpacity, StatusBar, Text, Image } from 'react-native';
+import { View, ScrollView, TouchableOpacity, StatusBar, Text, Image } from 'react-native';
import { useState, useEffect, useContext } from 'react';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
@@ -9,7 +9,7 @@ import Ionicons from 'react-native-vector-icons/AntDesign';
import { useSession } from './useSession.hook';
import { styles } from './Session.styled';
import Colors from 'constants/Colors';
-import { Profile } from './profile/Profile';
+import { Profile, ProfileHeader, ProfileBody } from './profile/Profile';
import { CardsTitle, CardsBody, Cards } from './cards/Cards';
import { RegistryTitle, RegistryBody, Registry } from './registry/Registry';
import { Contact, ContactTitle } from './contact/Contact';
@@ -83,7 +83,9 @@ export function Session() {
const ProfileStackScreen = () => {
return (
(screenParams)}>
-
+ }}>
+ {(props) => }
+
);
}
@@ -268,8 +270,9 @@ export function Session() {
{ state.firstRun == false && (
{ state.tabbed === false && (
- }>
+ (
+
+ )}>
{(props) => }
diff --git a/app/mobile/src/session/profile/Profile.jsx b/app/mobile/src/session/profile/Profile.jsx
index 65b347d2..57634b4a 100644
--- a/app/mobile/src/session/profile/Profile.jsx
+++ b/app/mobile/src/session/profile/Profile.jsx
@@ -1,6 +1,590 @@
-import { Text } from 'react-native';
+import { ActivityIndicator, KeyboardAvoidingView, Modal, View, Switch, Text, TextInput, TouchableOpacity, Alert } from 'react-native';
+import Ionicons from 'react-native-vector-icons/AntDesign';
+import ImagePicker from 'react-native-image-crop-picker'
+import { Colors } from 'constants/Colors';
+import { useProfile } from './useProfile.hook';
+import { Logo } from 'utils/Logo';
+import { styles } from './Profile.styled';
+import { BlockedTopics } from './blockedTopics/BlockedTopics';
+import { BlockedContacts } from './blockedContacts/BlockedContacts';
+import { BlockedMessages } from './blockedMessages/BlockedMessages';
-export function Profile({ navigation }) {
- return Profile;
+export function ProfileHeader() {
+ const { state, actions } = useProfile();
+
+ return (
+ { `${state.handle}@${state.node}` }
+ )
+}
+
+export function ProfileBody() {
+ const { state, actions } = useProfile();
+
+ const logout = async () => {
+ Alert.alert(
+ "Logging Out",
+ "Confirm?",
+ [
+ { text: "Cancel",
+ onPress: () => {},
+ },
+ { text: "Logout", onPress: () => {
+ actions.logout();
+ }}
+ ]
+ );
+ }
+
+ const remove = async () => {
+ try {
+ await actions.remove();
+ }
+ catch (err) {
+ console.log(err);
+ Alert.alert(
+ 'Failed to Delete Account',
+ 'Please try again.'
+ )
+ }
+ }
+
+ const onGallery = 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 });
+ await actions.setProfileImage(crop.data);
+ }
+ catch (err) {
+ console.log(err);
+ }
+ }
+
+ const setNotifications = async (notify) => {
+ try {
+ await actions.setNotifications(notify);
+ }
+ catch (err) {
+ console.log(err);
+ Alert.alert(
+ 'Account Update Failed',
+ 'Please try again.',
+ );
+ }
+ }
+
+ const setVisible = async (visible) => {
+ try {
+ await actions.setVisible(visible);
+ }
+ catch (err) {
+ console.log(err);
+ Alert.alert(
+ 'Account Update Failed',
+ 'Please try again.'
+ );
+ }
+ }
+
+ const saveSeal = async () => {
+ try {
+ await actions.saveSeal();
+ actions.hideSealEdit();
+ }
+ catch (err) {
+ console.log(err);
+ Alert.alert(
+ 'Failed to Update Topic Sealing',
+ 'Please try again.',
+ )
+ }
+ }
+
+ const saveDetails = async () => {
+ try {
+ await actions.saveDetails();
+ actions.hideDetailEdit();
+ }
+ catch (err) {
+ console.log(err);
+ Alert.alert(
+ 'Failed to Save Details',
+ 'Please try again.'
+ )
+ }
+ }
+
+ const saveLogin = async () => {
+ try {
+ await actions.saveLogin();
+ actions.hideLoginEdit();
+ }
+ catch (err) {
+ console.log(err);
+ Alert.alert(
+ 'Failed to Change Login',
+ 'Please try again.'
+ )
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ { state.disconnected && (
+ Disconnected
+ )}
+
+
+
+
+ { state.name && (
+ { state.name }
+ )}
+ { !state.name && (
+ Name
+ )}
+
+
+
+
+
+
+ { state.location && (
+ { state.location }
+ )}
+ { !state.location && (
+ Location
+ )}
+
+
+
+
+
+ { state.description && (
+ { state.description }
+ )}
+ { !state.description && (
+ Description
+ )}
+
+
+
+
+ setVisible(!state.searchable)} activeOpacity={1}>
+ Visible in Registry
+
+
+
+
+ setNotifications(!state.pushEnabled)} activeOpacity={1}>
+ Enable Notifications
+
+
+
+ { state.sealable && (
+
+
+ Sealed Topics
+
+ )}
+
+
+
+ Change Login
+
+
+
+
+ Logout
+
+
+
+
+ Delete Account
+
+
+ Manage Blocked:
+
+
+ Contacts
+
+
+ Topics
+
+
+ Messages
+
+
+
+
+
+
+ Deleting Your Account
+
+
+
+
+
+ Cancel
+
+ { state.confirmDelete === 'delete' && (
+
+ Delete
+
+ )}
+ { state.confirmDelete !== 'delete' && (
+
+ Delete
+
+ )}
+
+
+
+
+
+
+
+ Blocked Contacts:
+
+
+
+
+
+ Close
+
+
+
+
+
+
+
+
+ Blocked Topics:
+
+
+
+
+
+ Close
+
+
+
+
+
+
+
+
+ Blocked Messages:
+
+
+
+
+
+ Close
+
+
+
+
+
+
+
+
+ Edit Details:
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Save
+
+
+
+
+
+
+
+
+ Sealed Topics:
+
+ actions.setSealable(!state.sealable)} activeOpacity={1}>
+ Enable Sealed Topics
+
+
+
+ { state.sealMode === 'unlocking' && (
+ <>
+ { !state.showSealUnlock && (
+
+
+
+
+
+
+ )}
+ { state.showSealUnlock && (
+
+
+
+
+
+
+ )}
+ >
+ )}
+ { (state.sealMode === 'updating' || state.sealMode === 'enabling') && (
+ <>
+ { !state.showSealPassword && (
+
+
+
+
+
+
+ )}
+ { state.showSealPassword && (
+
+
+
+
+
+
+ )}
+ { !state.showSealConfirm && (
+
+
+
+
+
+
+ )}
+ { state.showSealConfirm && (
+
+
+
+
+
+
+ )}
+ saving can take a minute
+ >
+ )}
+ { state.sealMode === 'disabling' && (
+
+
+
+
+ )}
+ { state.sealMode === 'unlocked' && (
+
+
+
+
+
+ )}
+
+
+ Cancel
+
+ { state.canSaveSeal && (
+ <>
+ { state.sealMode !== 'unlocking' && state.sealMode !== 'unlocked' && (
+
+ Save
+
+ )}
+ { state.sealMode === 'unlocked' && (
+
+ Forget
+
+ )}
+ { state.sealMode === 'unlocking' && (
+
+ Unlock
+
+ )}
+ >
+ )}
+ { !state.canSaveSeal && (
+ <>
+ { state.sealMode !== 'unlocking' && (
+
+ Save
+
+ )}
+ { state.sealMode === 'unlocking' && (
+
+ Unlock
+
+ )}
+ >
+ )}
+
+
+
+
+
+
+
+
+ Change Login:
+
+
+ { state.checked && state.available && (
+
+ )}
+ { state.checked && !state.available && (
+
+ )}
+
+ { !state.showPassword && (
+
+
+
+
+
+
+ )}
+ { state.showPassword && (
+
+
+
+
+
+
+ )}
+ { !state.showConfirm && (
+
+
+
+
+
+
+ )}
+ { state.showConfirm && (
+
+
+
+
+
+
+ )}
+
+
+ Cancel
+
+ { (state.checked && state.available && state.editConfirm === state.editPassword && state.editPassword) && (
+
+ Save
+
+ )}
+ { !(state.checked && state.available && state.editConfirm === state.editPassword && state.editPassword) && (
+
+ Save
+
+ )}
+
+
+
+
+
+ );
+}
+
+export function Profile() {
+ return (
+
+
+
+
+ );
}
diff --git a/app/mobile/src/session/profile/Profile.styled.js b/app/mobile/src/session/profile/Profile.styled.js
new file mode 100644
index 00000000..f7956c8b
--- /dev/null
+++ b/app/mobile/src/session/profile/Profile.styled.js
@@ -0,0 +1,241 @@
+
+import { StyleSheet } from 'react-native';
+import { Colors } from 'constants/Colors';
+
+export const styles = StyleSheet.create({
+ body: {
+ display: 'flex',
+ flexGrow: 1,
+ },
+ button: {
+ paddingRight: 16,
+ },
+ headerText: {
+ fontSize: 18,
+ overflow: 'hidden',
+ textAlign: 'center',
+ },
+ logo: {
+ marginTop: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ display: 'flex',
+ },
+ alert: {
+ height: 16,
+ width: '100%',
+ alignItems: 'center',
+ },
+ alertText: {
+ color: Colors.alert,
+ },
+ logout: {
+ marginTop: 16,
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ },
+ logoutText: {
+ marginLeft: 8,
+ color: Colors.primary,
+ },
+ delete: {
+ marginTop: 16,
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ },
+ deleteText: {
+ marginLeft: 8,
+ color: Colors.alert,
+ },
+ modalWrapper: {
+ display: 'flex',
+ width: '100%',
+ height: '100%',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: 'rgba(52, 52, 52, 0.8)'
+ },
+ modalContainer: {
+ backgroundColor: Colors.formBackground,
+ padding: 16,
+ width: '80%',
+ maxWidth: 400,
+ },
+ modalHeader: {
+ fontSize: 18,
+ paddingBottom: 16,
+ },
+ modalList: {
+ width: '100%',
+ borderWidth: 1,
+ borderColor: Colors.lightgrey,
+ borderRadius: 2,
+ },
+ modalControls: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ },
+ cancel: {
+ borderWidth: 1,
+ borderColor: Colors.lightgrey,
+ borderRadius: 4,
+ padding: 8,
+ marginRight: 8,
+ width: 72,
+ display: 'flex',
+ alignItems: 'center',
+ },
+ unconfirmed: {
+ backgroundColor: Colors.lightgrey,
+ borderRadius: 4,
+ padding: 8,
+ width: 72,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ remove: {
+ backgroundColor: Colors.error,
+ borderRadius: 4,
+ padding: 8,
+ width: 72,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ removeText: {
+ color: Colors.white,
+ },
+ inputField: {
+ width: '100%',
+ borderWidth: 1,
+ borderColor: Colors.lightgrey,
+ borderRadius: 4,
+ padding: 8,
+ marginBottom: 8,
+ maxHeight: 92,
+ display: 'flex',
+ flexDirection: 'row',
+ },
+ gallery: {
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+ padding: 8,
+ backgroundColor: Colors.lightgrey,
+ borderBottomRightRadius: 8,
+ borderTopLeftRadius: 8,
+ },
+ detail: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ color: Colors.text,
+ paddingLeft: 32,
+ paddingRight: 32,
+ marginTop: 16,
+ marginBottom: 16,
+ },
+ attribute: {
+ display: 'flex',
+ flexDirection: 'row',
+ paddingBottom: 8,
+ },
+ nonametext: {
+ fontSize: 18,
+ paddingRight: 8,
+ fontWeight: 'bold',
+ color: Colors.grey,
+ },
+ nametext: {
+ fontSize: 18,
+ paddingRight: 8,
+ fontWeight: 'bold',
+ },
+ locationtext: {
+ fontSize: 16,
+ paddingLeft: 8,
+ color: Colors.text,
+ },
+ nolocationtext: {
+ fontSize: 16,
+ paddingLeft: 8,
+ color: Colors.grey,
+ },
+ descriptiontext: {
+ fontSize: 16,
+ paddingLeft: 8,
+ color: Colors.text,
+ },
+ nodescriptiontext: {
+ fontSize: 16,
+ paddingLeft: 8,
+ color: Colors.grey,
+ },
+ save: {
+ padding: 8,
+ borderRadius: 4,
+ backgroundColor: Colors.primary,
+ width: 72,
+ display: 'flex',
+ alignItems: 'center',
+ },
+ saveText: {
+ color: Colors.white,
+ },
+ blocked: {
+ alignSelf: 'center',
+ borderColor: Colors.lightgrey,
+ borderWidth: 1,
+ borderRadius: 4,
+ padding: 8,
+ marginBottom: 8,
+ display: 'flex',
+ flexDirection: 'row',
+ },
+ blockedLabel: {
+ marginTop: 24,
+ alignSelf: 'center',
+ color: Colors.grey,
+ },
+ enable: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ },
+ enableText: {
+ color: Colors.primary,
+ },
+ enableSwitch: {
+ transform: [{ scaleX: .6 }, { scaleY: .6 }],
+ },
+ link: {
+ marginLeft: 8,
+ marginRight: 8,
+ },
+ linkText: {
+ color: Colors.primary,
+ },
+ close: {
+ borderWidth: 1,
+ borderColor: Colors.lightgrey,
+ borderRadius: 4,
+ padding: 8,
+ marginTop: 8,
+ width: 72,
+ display: 'flex',
+ alignItems: 'center',
+ },
+});
+
diff --git a/app/mobile/src/session/profile/blockedContacts/BlockedContacts.jsx b/app/mobile/src/session/profile/blockedContacts/BlockedContacts.jsx
new file mode 100644
index 00000000..e8c0df4b
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedContacts/BlockedContacts.jsx
@@ -0,0 +1,48 @@
+import { FlatList, View, Alert, TouchableOpacity, Text } from 'react-native';
+import { styles } from './BlockedContacts.styled';
+import { useBlockedContacts } from './useBlockedContacts.hook';
+import { Logo } from 'utils/Logo';
+
+export function BlockedContacts() {
+
+ const { state, actions } = useBlockedContacts();
+
+ const unblock = (cardId) => {
+ Alert.alert(
+ 'Unblocking Contact',
+ 'Confirm?',
+ [
+ { text: "Cancel", onPress: () => {}, },
+ { text: "Unblock", onPress: () => actions.unblock(cardId) },
+ ],
+ );
+ };
+
+ const BlockedItem = ({ item }) => {
+ return (
+ unblock(item.cardId)}>
+
+
+ { item.name }
+ { item.handle }
+
+
+ )
+ }
+
+ return (
+
+ { state.cards.length === 0 && (
+ No Blocked Contacts
+ )}
+ { state.cards.length !== 0 && (
+ }
+ keyExtractor={item => item.cardId}
+ />
+ )}
+
+ );
+}
+
diff --git a/app/mobile/src/session/profile/blockedContacts/BlockedContacts.styled.js b/app/mobile/src/session/profile/blockedContacts/BlockedContacts.styled.js
new file mode 100644
index 00000000..18a5c33d
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedContacts/BlockedContacts.styled.js
@@ -0,0 +1,43 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'constants/Colors';
+
+export const styles = StyleSheet.create({
+ container: {
+ backgroundColor: Colors.white,
+ display: 'flex',
+ width: '100%',
+ justifyContent: 'center',
+ fontSize: 14,
+ height: 200,
+ },
+ default: {
+ textAlign: 'center',
+ color: Colors.grey,
+ },
+ item: {
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'row',
+ height: 48,
+ paddingLeft: 16,
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ borderColor: Colors.itemDivider,
+ },
+ detail: {
+ paddingLeft: 12,
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ flexGrow: 1,
+ flexShrink: 1,
+ },
+ name: {
+ color: Colors.text,
+ fontSize: 14,
+ },
+ handle: {
+ color: Colors.text,
+ fontSize: 12,
+ },
+});
diff --git a/app/mobile/src/session/profile/blockedContacts/useBlockedContacts.hook.js b/app/mobile/src/session/profile/blockedContacts/useBlockedContacts.hook.js
new file mode 100644
index 00000000..e51091d9
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedContacts/useBlockedContacts.hook.js
@@ -0,0 +1,53 @@
+import { useState, useEffect, useContext } from 'react';
+import { CardContext } from 'context/CardContext';
+
+export function useBlockedContacts() {
+
+ const [state, setState] = useState({
+ cards: [],
+ });
+
+ const card = useContext(CardContext);
+
+ const updateState = (value) => {
+ setState((s) => ({ ...s, ...value }));
+ }
+
+ const setCardItem = (item) => {
+ const { profile } = item;
+ return {
+ cardId: item.cardId,
+ name: profile?.name,
+ handle: `${profile?.handle}@${profile?.node}`,
+ blocked: item.blocked,
+ logo: profile?.imageSet ? card.actions.getCardImageUrl(item.cardId) : 'avatar',
+ }
+ };
+
+ useEffect(() => {
+ const cards = Array.from(card.state.cards.values());
+ const items = cards.map(setCardItem);
+ const filtered = items.filter(item => {
+ return item.blocked;
+ });
+ filtered.sort((a, b) => {
+ if (a.name === b.name) {
+ return 0;
+ }
+ if (!a.name || (a.name < b.name)) {
+ return -1;
+ }
+ return 1;
+ });
+ updateState({ cards: filtered });
+ }, [card]);
+
+ const actions = {
+ unblock: async (cardId) => {
+ await card.actions.clearCardBlocked(cardId);
+ }
+ };
+
+ return { state, actions };
+}
+
diff --git a/app/mobile/src/session/profile/blockedMessages/BlockedMessages.jsx b/app/mobile/src/session/profile/blockedMessages/BlockedMessages.jsx
new file mode 100644
index 00000000..4160eaf4
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedMessages/BlockedMessages.jsx
@@ -0,0 +1,47 @@
+import { FlatList, View, Alert, TouchableOpacity, Text } from 'react-native';
+import { styles } from './BlockedMessages.styled';
+import { useBlockedMessages } from './useBlockedMessages.hook';
+import { Logo } from 'utils/Logo';
+
+export function BlockedMessages() {
+
+ const { state, actions } = useBlockedMessages();
+
+ const unblock = (cardId, channelId, topicId) => {
+ Alert.alert(
+ 'Unblocking Message',
+ 'Confirm?',
+ [
+ { text: "Cancel", onPress: () => {}, },
+ { text: "Unblock", onPress: () => actions.unblock(cardId, channelId, topicId) },
+ ],
+ );
+ };
+
+ const BlockedItem = ({ item }) => {
+ return (
+ unblock(item.cardId, item.channelId, item.topicId)}>
+
+ { item.name }
+ { item.timestamp }
+
+
+ )
+ }
+
+ return (
+
+ { state.messages.length === 0 && (
+ No Blocked Messages
+ )}
+ { state.messages.length !== 0 && (
+ }
+ keyExtractor={item => item.id}
+ />
+ )}
+
+ );
+}
+
diff --git a/app/mobile/src/session/profile/blockedMessages/BlockedMessages.styled.js b/app/mobile/src/session/profile/blockedMessages/BlockedMessages.styled.js
new file mode 100644
index 00000000..c29efdcb
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedMessages/BlockedMessages.styled.js
@@ -0,0 +1,46 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'constants/Colors';
+
+export const styles = StyleSheet.create({
+ container: {
+ backgroundColor: Colors.white,
+ display: 'flex',
+ width: '100%',
+ justifyContent: 'center',
+ fontSize: 14,
+ height: 200,
+ },
+ default: {
+ textAlign: 'center',
+ color: Colors.grey,
+ },
+ item: {
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'row',
+ height: 32,
+ paddingLeft: 16,
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ borderColor: Colors.itemDivider,
+ },
+ detail: {
+ paddingLeft: 12,
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ width: '100%',
+ },
+ name: {
+ color: Colors.text,
+ fontSize: 14,
+ flexGrow: 1,
+ flexShrink: 1,
+ minWidth: 0,
+ },
+ created: {
+ color: Colors.text,
+ fontSize: 12,
+ paddingRight: 16,
+ },
+});
diff --git a/app/mobile/src/session/profile/blockedMessages/useBlockedMessages.hook.js b/app/mobile/src/session/profile/blockedMessages/useBlockedMessages.hook.js
new file mode 100644
index 00000000..c8438acd
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedMessages/useBlockedMessages.hook.js
@@ -0,0 +1,96 @@
+import { useState, useEffect, useContext } from 'react';
+import { StoreContext } from 'context/StoreContext';
+import { ChannelContext } from 'context/ChannelContext';
+import { CardContext } from 'context/CardContext';
+import { ProfileContext } from 'context/ProfileContext';
+import { ConversationContext } from 'context/ConversationContext';
+import moment from 'moment';
+
+export function useBlockedMessages() {
+
+ const [state, setState] = useState({
+ messages: []
+ });
+
+ const store = useContext(StoreContext);
+ const card = useContext(CardContext);
+ const channel = useContext(ChannelContext);
+ const profile = useContext(ProfileContext);
+ const conversation = useContext(ConversationContext);
+
+ const updateState = (value) => {
+ setState((s) => ({ ...s, ...value }));
+ }
+
+ const setItem = (item) => {
+ let name, nameSet
+ if (item.detail.guid === profile.state.identity.guid) {
+ const identity = profile.state.identity;
+ if (identity.name) {
+ name = identity.name;
+ }
+ else {
+ name = `${identity.handle}@${identity.node}`;
+ }
+ nameSet = true;
+ }
+ else {
+ const contact = card.actions.getByGuid(item.detail.guid);
+ if (contact) {
+ if (contact?.profile?.name) {
+ name = contact.profile.name;
+ }
+ else {
+ name = `${contact.profile.handle}@${contact.profile.node}`;
+ }
+ nameSet = true;
+ }
+ else {
+ name = 'unknown';
+ nameSet = false;
+ }
+ }
+
+ let timestamp;
+ const date = new Date(item.detail.created * 1000);
+ const now = new Date();
+ const offset = now.getTime() - date.getTime();
+ if(offset < 86400000) {
+ timestamp = moment(date).format('h:mma');
+ }
+ else if (offset < 31449600000) {
+ timestamp = moment(date).format('M/DD');
+ }
+ else {
+ timestamp = moment(date).format('M/DD/YYYY');
+ }
+
+ const { cardId, channelId, topicId } = item;
+ return { name, nameSet, timestamp, cardId, channelId, topicId, id: `${cardId}:${channelId}:${topicId}` };
+ };
+
+ const loadBlocked = async () => {
+ //TODO
+ }
+
+ useEffect(() => {
+ loadBlocked();
+ }, []);
+
+ const actions = {
+ unblock: async (cardId, channelId, topicId) => {
+ const id = `${cardId}:${channelId}:${topicId}`;
+ if (cardId) {
+ card.actions.clearChannelTopicBlocked(cardId, channelId, topicId);
+ }
+ else {
+ channel.actions.clearTopicBlocked(channelId, topicId);
+ }
+ conversation.actions.unblockTopic(cardId, channelId, topicId);
+ updateState({ messages: state.messages.filter(item => item.id !== id) });
+ }
+ };
+
+ return { state, actions };
+}
+
diff --git a/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx
new file mode 100644
index 00000000..cb788d41
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx
@@ -0,0 +1,47 @@
+import { FlatList, View, Alert, TouchableOpacity, Text } from 'react-native';
+import { styles } from './BlockedTopics.styled';
+import { useBlockedTopics } from './useBlockedTopics.hook';
+import { Logo } from 'utils/Logo';
+
+export function BlockedTopics() {
+
+ const { state, actions } = useBlockedTopics();
+
+ const unblock = (cardId, channelId) => {
+ Alert.alert(
+ 'Unblocking Contact',
+ 'Confirm?',
+ [
+ { text: "Cancel", onPress: () => {}, },
+ { text: "Unblock", onPress: () => actions.unblock(cardId, channelId) },
+ ],
+ );
+ };
+
+ const BlockedItem = ({ item }) => {
+ return (
+ unblock(item.cardId, item.channelId)}>
+
+ { item.name }
+ { item.created }
+
+
+ )
+ }
+
+ return (
+
+ { state.channels.length === 0 && (
+ No Blocked Topics
+ )}
+ { state.channels.length !== 0 && (
+ }
+ keyExtractor={item => item.id}
+ />
+ )}
+
+ );
+}
+
diff --git a/app/mobile/src/session/profile/blockedTopics/BlockedTopics.styled.js b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.styled.js
new file mode 100644
index 00000000..c29efdcb
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.styled.js
@@ -0,0 +1,46 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'constants/Colors';
+
+export const styles = StyleSheet.create({
+ container: {
+ backgroundColor: Colors.white,
+ display: 'flex',
+ width: '100%',
+ justifyContent: 'center',
+ fontSize: 14,
+ height: 200,
+ },
+ default: {
+ textAlign: 'center',
+ color: Colors.grey,
+ },
+ item: {
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'row',
+ height: 32,
+ paddingLeft: 16,
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ borderColor: Colors.itemDivider,
+ },
+ detail: {
+ paddingLeft: 12,
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ width: '100%',
+ },
+ name: {
+ color: Colors.text,
+ fontSize: 14,
+ flexGrow: 1,
+ flexShrink: 1,
+ minWidth: 0,
+ },
+ created: {
+ color: Colors.text,
+ fontSize: 12,
+ paddingRight: 16,
+ },
+});
diff --git a/app/mobile/src/session/profile/blockedTopics/useBlockedTopics.hook.js b/app/mobile/src/session/profile/blockedTopics/useBlockedTopics.hook.js
new file mode 100644
index 00000000..5ee87bef
--- /dev/null
+++ b/app/mobile/src/session/profile/blockedTopics/useBlockedTopics.hook.js
@@ -0,0 +1,122 @@
+import { useState, useEffect, useContext } from 'react';
+import { CardContext } from 'context/CardContext';
+import { ChannelContext } from 'context/ChannelContext';
+import { ProfileContext } from 'context/ProfileContext';
+import moment from 'moment';
+
+export function useBlockedTopics() {
+
+ const [state, setState] = useState({
+ channels: []
+ });
+
+ const profile = useContext(ProfileContext);
+ const card = useContext(CardContext);
+ const channel = useContext(ChannelContext);
+
+ const updateState = (value) => {
+ setState((s) => ({ ...s, ...value }));
+ }
+
+ const getCard = (guid) => {
+ let contact = null
+ card.state.cards.forEach((card, cardId, map) => {
+ if (card?.profile?.guid === guid) {
+ contact = card;
+ }
+ });
+ return contact;
+ }
+
+ const setChannelItem = (item) => {
+ let timestamp;
+ const date = new Date(item.detail.created * 1000);
+ const now = new Date();
+ const offset = now.getTime() - date.getTime();
+ if(offset < 86400000) {
+ timestamp = moment(date).format('h:mma');
+ }
+ else if (offset < 31449600000) {
+ timestamp = moment(date).format('M/DD');
+ }
+ else {
+ timestamp = moment(date).format('M/DD/YYYY');
+ }
+
+ let contacts = [];
+ if (item.cardId) {
+ contacts.push(card.state.cards.get(item.cardId));
+ }
+ if (item?.detail?.members) {
+ const profileGuid = profile.state.identity.guid;
+ item.detail.members.forEach(guid => {
+ if (profileGuid !== guid) {
+ const contact = getCard(guid);
+ contacts.push(contact);
+ }
+ })
+ }
+
+ let subject;
+ if (item?.detail?.data) {
+ try {
+ topic = JSON.parse(item?.detail?.data).subject;
+ subject = topic;
+ }
+ catch (err) {
+ console.log(err);
+ }
+ }
+ if (!subject) {
+ if (contacts.length) {
+ let names = [];
+ for (let contact of contacts) {
+ if (contact?.profile?.name) {
+ names.push(contact.profile.name);
+ }
+ else if (contact?.profile?.handle) {
+ names.push(contact?.profile?.handle);
+ }
+ }
+ subject = names.join(', ');
+ }
+ else {
+ subject = "Notes";
+ }
+ }
+
+ return {
+ id: `${item.cardId}:${item.channelId}`,
+ cardId: item.cardId,
+ channelId: item.channelId,
+ name: subject,
+ blocked: item.blocked,
+ created: timestamp,
+ }
+ };
+
+ useEffect(() => {
+ let merged = [];
+ card.state.cards.forEach((card, cardId, map) => {
+ merged.push(...Array.from(card.channels.values()));
+ });
+ merged.push(...Array.from(channel.state.channels.values()));
+ const items = merged.map(setChannelItem);
+ const filtered = items.filter(item => item.blocked);
+ updateState({ channels: filtered });
+ }, [card, channel]);
+
+ const actions = {
+ unblock: async (cardId, channelId) => {
+ if (cardId) {
+ await card.actions.clearChannelBlocked(cardId, channelId);
+ }
+ else {
+ await channel.actions.clearBlocked(channelId);
+ }
+ }
+ };
+
+ return { state, actions };
+}
+
diff --git a/app/mobile/src/session/profile/useProfile.hook.js b/app/mobile/src/session/profile/useProfile.hook.js
new file mode 100644
index 00000000..2f6e1d58
--- /dev/null
+++ b/app/mobile/src/session/profile/useProfile.hook.js
@@ -0,0 +1,118 @@
+import { useState, useEffect, useRef, useContext } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { ProfileContext } from 'context/ProfileContext';
+import { AppContext } from 'context/AppContext';
+
+export function useProfile() {
+
+ const [state, setState] = useState({
+ name: null,
+ handle: null,
+ editHandle: null,
+ location: null,
+ editLocation: null,
+ description: null,
+ editDescritpion: null,
+ node: null,
+ showDelete: false,
+ editDetails: false,
+ editLogin: false,
+ editSeal: false,
+ confirmDelete: null,
+ blockedChannels: false,
+ blockedCards: false,
+ blockedMessages: false,
+ logginOut: false,
+ disconnected: false,
+ });
+
+ const app = useContext(AppContext);
+ const profile = useContext(ProfileContext);
+ const navigate = useNavigate();
+
+ const updateState = (value) => {
+ setState((s) => ({ ...s, ...value }));
+ }
+
+ useEffect(() => {
+ const { name, handle, node, location, description, image } = profile.state.identity;
+ const imageSource = image ? profile.state.imageUrl : 'avatar';
+ updateState({ name, handle, node, location, description, imageSource, editHandle: handle,
+ editName: name, editLocation: location, editDescription: description });
+ }, [profile]);
+
+ useEffect(() => {
+ const { loggingOut, status } = app.state;
+ updateState({ loggingOut, disconnected: status === 'disconnected' });
+ }, [app.state]);
+
+ const actions = {
+ logout: async () => {
+ await app.actions.logout();
+ navigate('/');
+ },
+ remove: async () => {
+ await app.actions.remove();
+ updateState({ showDelete: false });
+ navigate('/');
+ },
+ showDelete: () => {
+ updateState({ showDelete: true, confirmDelete: null });
+ },
+ hideDelete: () => {
+ updateState({ showDelete: false });
+ },
+ setConfirmDelete: (confirmDelete) => {
+ updateState({ confirmDelete });
+ },
+ showEditDetails: () => {
+ updateState({ editDetails: true });
+ },
+ hideEditDetails: () => {
+ updateState({ editDetails: false });
+ },
+ showEditLogin: () => {
+ updateState({ editLogin: true });
+ },
+ hideEditLogin: () => {
+ updateState({ editLogin: false });
+ },
+ showEditSeal: () => {
+ updateState({ editSeal: true });
+ },
+ hideEditSeal: () => {
+ updateState({ editSeal: false });
+ },
+ showBlockedChannels: () => {
+ updateState({ blockedChannels: true });
+ },
+ hideBlockedChannels: () => {
+ updateState({ blockedChannels: false });
+ },
+ showBlockedCards: () => {
+ updateState({ blockedCards: true });
+ },
+ hideBlockedCards: () => {
+ updateState({ blockedCards: false });
+ },
+ showBlockedMessages: () => {
+ updateState({ blockedMessages: true });
+ },
+ hideBlockedMessages: () => {
+ updateState({ blockedMessages: false });
+ },
+ setEditName: (editName) => {
+ updateState({ editName });
+ },
+ setEditLocation: (editLocation) => {
+ updateState({ editLocation });
+ },
+ setEditDescription: (editDescription) => {
+ updateState({ editDescription });
+ },
+ };
+
+ return { state, actions };
+}
+
+
diff --git a/app/mobile/yarn.lock b/app/mobile/yarn.lock
index 96399a94..9f56ffac 100644
--- a/app/mobile/yarn.lock
+++ b/app/mobile/yarn.lock
@@ -5597,6 +5597,11 @@ mkdirp@^0.5.1:
dependencies:
minimist "^1.2.6"
+moment@^2.29.4:
+ version "2.29.4"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
+ integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
@@ -6228,6 +6233,11 @@ react-native-gradle-plugin@^0.71.15:
resolved "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.15.tgz"
integrity sha512-7S3pAuPaQJlhax6EZ4JMsDNpj05TfuzX9gPgWLrFfAIWIFLuJ6aDQYAZy2TEI9QJALPoWrj8LWaqP/DGYh14pw==
+react-native-image-crop-picker@^0.39.0:
+ version "0.39.0"
+ resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.39.0.tgz#9cb8e8ffb0e8ab06f7b3227cadf077169e225eba"
+ integrity sha512-4aANbQMrmU6zN/4b0rVBA7SbaZ3aa5JESm3Xk751sINybZMt1yz/9h95LkO7U0pbslHDo3ofXjG75PmQRP6a/w==
+
react-native-reanimated@^2.14.4:
version "2.14.4"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.14.4.tgz#3fa3da4e7b99f5dfb28f86bcf24d9d1024d38836"