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"