diff --git a/app/mobile/src/context/useConversationContext.hook.js b/app/mobile/src/context/useConversationContext.hook.js index fc64d1e1..607cf9ca 100644 --- a/app/mobile/src/context/useConversationContext.hook.js +++ b/app/mobile/src/context/useConversationContext.hook.js @@ -14,6 +14,7 @@ export function useConversationContext() { topics: new Map(), card: null, channel: null, + notification: null, }); const card = useContext(CardContext); const channel = useContext(ChannelContext); @@ -48,7 +49,16 @@ export function useConversationContext() { reset.current = false; loaded.current = false; topics.current = new Map(); - updateState({ offsync: false, channel: null, card: null, topics: topics.current }); + + let notification; + try { + notification = await getNotifications(); + } + catch(err) { + console.log(err); + } + + updateState({ offsync: false, channel: null, card: null, topics: topics.current, notification }); } if (conversation) { @@ -167,23 +177,15 @@ export function useConversationContext() { await channel.actions.removeChannel(channelId); } }, - getNotifications: async () => { + setNotifications: async (notification) => { const { cardId, channelId } = conversationId.current || {}; if (cardId) { - await card.actions.getChannelNotifications(cardId, channelId); + await card.actions.setChannelNotifications(cardId, channelId, notification); } else if (channelId) { - await channel.actions.getNotifications(channelId); - } - }, - setNotifications: async (notify) => { - const { cardId, channelId } = conversationId.current || {}; - if (cardId) { - await card.actions.setChannelNotifications(cardId, channelId); - } - else if (channelId) { - await channel.actions.setNotifications(channelId, notify); + await channel.actions.setNotifications(channelId, notification); } + updateState({ notification }); }, setChannelCard: async (id) => { const { cardId, channelId } = conversationId.current || {}; @@ -381,6 +383,16 @@ export function useConversationContext() { return await channel.actions.getTopic(channelId, topicId); } + const getNotifications = async (notification) => { + const { cardId, channelId } = conversationId.current || {}; + if (cardId) { + return await card.actions.getChannelNotifications(cardId, channelId); + } + else if (channelId) { + return await channel.actions.getNotifications(channelId); + } + } + const mapTopicEntry = (entry) => { return { topicId: entry.id, diff --git a/app/mobile/src/session/Session.jsx b/app/mobile/src/session/Session.jsx index 6d02c994..75c321dc 100644 --- a/app/mobile/src/session/Session.jsx +++ b/app/mobile/src/session/Session.jsx @@ -13,7 +13,7 @@ import { Profile, ProfileHeader, ProfileBody } from './profile/Profile'; import { CardsHeader, CardsBody, Cards } from './cards/Cards'; import { RegistryHeader, RegistryBody, Registry } from './registry/Registry'; import { ContactHeader, ContactBody, Contact } from './contact/Contact'; -import { Details, DetailsHeader, DetailsBody } from './details/Details'; +import { Details } from './details/Details'; import { Conversation, ConversationHeader, ConversationBody } from './conversation/Conversation'; import { Welcome } from './welcome/Welcome'; import { Channels } from './channels/Channels'; @@ -70,8 +70,10 @@ export function Session() { {(props) => openDetails(props.navigation)} closeConversation={closeConversation} /> } - }}> - {(props) => clearConversation(props.navigation)} />} + ( + Details + )}}> + {(props) =>
clearConversation(props.navigation)} />} diff --git a/app/mobile/src/session/Session.styled.js b/app/mobile/src/session/Session.styled.js index 4f22549c..0cd3ebb7 100644 --- a/app/mobile/src/session/Session.styled.js +++ b/app/mobile/src/session/Session.styled.js @@ -134,4 +134,8 @@ export const styles = StyleSheet.create({ profileLabel: { paddingLeft: 8, }, + headertext: { + fontSize: 18, + color: Colors.tetx, + }, }); diff --git a/app/mobile/src/session/details/Details.jsx b/app/mobile/src/session/details/Details.jsx index b42e4ddb..b237a318 100644 --- a/app/mobile/src/session/details/Details.jsx +++ b/app/mobile/src/session/details/Details.jsx @@ -1,14 +1,246 @@ -import { Text } from 'react-native'; - -export function DetailsHeader() { - return DetailsHeader -} - -export function DetailsBody({ channel, clearConversation }) { - return DetailsBody -} +import { KeyboardAvoidingView, FlatList, Alert, Modal, View, Text, Switch, TouchableOpacity, TextInput } from 'react-native'; +import { styles } from './Details.styled'; +import { useDetails } from './useDetails.hook'; +import { Logo } from 'utils/Logo'; +import AntIcons from 'react-native-vector-icons/AntDesign'; +import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import Colors from 'constants/Colors'; export function Details({ channel, clearConversation }) { - return Details + + const { state, actions } = useDetails(); + + const saveSubject = async () => { + try { + await actions.saveSubject(); + actions.hideEditSubject(); + } + catch (err) { + console.log(err); + Alert.alert( + 'Failed to Save Subject', + 'Please try again.' + ) + } + } + + const setNotifications = async (notify) => { + try { + await actions.setNotifications(notify); + } + catch (err) { + console.log(err); + Alert.alert( + 'Failed to Update Notifications', + 'Please try again.', + ) + } + } + + const remove = () => { + Alert.alert( + "Removing Topic", + "Confirm?", + [ + { text: "Cancel", + onPress: () => {}, + }, + { text: "Remove", + onPress: async () => { + try { + await actions.remove(); + clearConversation(); + } + catch (err) { + console.log(err); + Alert.alert( + 'Failed to Delete Topic', + 'Please try again.' + ) + } + }, + } + ] + ); + } + + const block = () => { + Alert.alert( + "Blocking Topic", + "Confirm?", + [ + { text: "Cancel", + onPress: () => {}, + }, + { text: "Block", + onPress: async () => { + try { + await actions.block(); + clearConversation(); + } + catch (err) { + console.log(err); + Alert.alert( + 'Failed to Block Topic', + 'Please try again.' + ) + } + }, + } + ] + ); + } + + + const report = () => { + Alert.alert( + "Report Topic", + "Confirm?", + [ + { text: "Cancel", + onPress: () => {}, + }, + { text: "Report", + onPress: async () => { + try { + await actions.report(); + } + catch (err) { + console.log(err); + Alert.alert( + 'Failed to Report Topic', + 'Please try again.' + ) + } + }, + } + ] + ); + } + + return ( + + + + + + { state.locked && !state.unlocked && ( + + )} + { state.locked && state.unlocked && ( + + )} + { state.subject } + { !state.hostId && (!state.locked || state.sealable) && ( + + + + )} + + { state.created } + { state.hostId ? 'guest' : 'host' } + + + + + { !state.hostId && ( + + Delete Topic + + )} + { state.hostId && ( + + Leave Topic + + )} + + Block Topic + + { state.hostId && ( + + Report Topic + + )} + { !state.hostId && !state.locked && ( + + Edit Membership + + )} + + + setNotifications(!state.notification)} activeOpacity={1}> + Enable Notifications + + + + + + + + Members: + { state.count - state.members.length > 0 && ( + (+ {state.count - state.contacts.length} unknown) + )} + + + } + keyExtractor={item => item.cardId} + /> + + + + + Edit Subject: + + + + + + Cancel + + + Save + + + + + + + + + + Channel Members: + MEMBER } + keyExtractor={item => item.cardId} + /> + + + Done + + + + + + + + ) } + diff --git a/app/mobile/src/session/details/Details.styled.js b/app/mobile/src/session/details/Details.styled.js new file mode 100644 index 00000000..81248d30 --- /dev/null +++ b/app/mobile/src/session/details/Details.styled.js @@ -0,0 +1,181 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + body: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + details: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + paddingTop: 16, + flexShrink: 1, + minWidth: 0, + }, + info: { + paddingLeft: 8, + display: 'flex', + flexDirection: 'column', + minWidth: 0, + flexShrink: 1, + }, + subject: { + fontSize: 18, + display: 'flex', + flexDirection: 'row', + paddingRight: 8, + color: Colors.text, + alignItems: 'center', + minWidth: 0, + flexShrink: 1, + }, + subjectIcon: { + paddingRight: 4 + }, + subjectText: { + fontSize: 18, + flexShrink: 1, + minWidth: 0, + color: Colors.text, + paddingRight: 4, + }, + created: { + fontSize: 16, + color: Colors.text, + }, + mode: { + fontSize: 16, + color: Colors.text, + }, + title: { + fontSize: 20, + }, + controls: { + paddingTop: 16, + }, + button: { + width: 128, + backgroundColor: Colors.primary, + borderRadius: 4, + margin: 8, + }, + buttonText: { + width: '100%', + textAlign: 'center', + color: Colors.white, + padding: 4, + }, + members: { + paddingBottom: 4, + paddingTop: 24, + width: '100%', + borderBottomWidth: 1, + borderColor: Colors.divider, + display: 'flex', + flexDirection: 'row', + }, + membersLabel: { + paddingLeft: 16, + }, + unknown: { + color: Colors.grey, + paddingLeft: 8, + }, + cards: { + width: '100%', + }, + save: { + padding: 8, + borderRadius: 4, + backgroundColor: Colors.primary, + width: 72, + display: 'flex', + alignItems: 'center', + }, + link: { + marginTop: 16, + }, + linkText: { + color: Colors.primary, + }, + saveText: { + color: Colors.white, + }, + cancel: { + borderWidth: 1, + borderColor: Colors.lightgrey, + borderRadius: 4, + padding: 8, + marginRight: 8, + width: 72, + display: 'flex', + alignItems: 'center', + }, + inputField: { + width: '100%', + borderWidth: 1, + borderColor: Colors.lightgrey, + borderRadius: 4, + padding: 8, + marginBottom: 8, + maxHeight: 92, + display: 'flex', + flexDirection: 'row', + }, + input: { + fontSize: 14, + flexGrow: 1, + }, + editControls: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + }, + editWrapper: { + display: 'flex', + width: '100%', + height: '100%', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(52, 52, 52, 0.8)' + }, + editContainer: { + backgroundColor: Colors.formBackground, + padding: 16, + width: '80%', + maxWidth: 400, + }, + editHeader: { + fontSize: 18, + paddingBottom: 16, + }, + editMembers: { + width: '100%', + borderWidth: 1, + borderColor: Colors.lightgrey, + borderRadius: 4, + marginBottom: 8, + height: 250, + }, + notify: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + paddingTop: 16, + }, + notifyText: { + fontSize: 16, + color: Colors.text, + }, + track: { + false: Colors.grey, + true: Colors.background, + }, + switch: { + transform: [{ scaleX: .7 }, { scaleY: .7 }], + }, +}) diff --git a/app/mobile/src/session/details/useDetails.hook.js b/app/mobile/src/session/details/useDetails.hook.js new file mode 100644 index 00000000..42cba054 --- /dev/null +++ b/app/mobile/src/session/details/useDetails.hook.js @@ -0,0 +1,136 @@ +import { useState, useEffect, useRef, useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ConversationContext } from 'context/ConversationContext'; +import { CardContext } from 'context/CardContext'; +import { AccountContext } from 'context/AccountContext'; +import { ProfileContext } from 'context/ProfileContext'; +import { getChannelSubjectLogo } from 'context/channelUtil'; +import { getChannelSeals, isUnsealed } from 'context/sealUtil'; +import moment from 'moment'; + +export function useDetails() { + + const [state, setState] = useState({ + subject: null, + created: null, + logo: null, + hostId: null, + connected: [], + members: [], + editSubject: false, + editMembers: false, + subjectUpdate: null, + pushEnabled: false, + locked: false, + unlocked: false, + count: 0, + }); + + const card = useContext(CardContext); + const account = useContext(AccountContext); + const conversation = useContext(ConversationContext); + const profile = useContext(ProfileContext); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + let locked; + let unlocked; + const { channel, notification } = conversation.state; + if (channel.detail.dataType === 'sealed') { + locked = true; + try { + const { sealKey } = account.state; + const seals = getChannelSeals(channel.detail.data); + unlocked = isUnsealed(seals, sealKey); + } + catch(err) { + console.log(err); + unlocked = false; + } + } + else { + locked = false; + unlocked = false; + } + updateState({ locked, unlocked, notification }); + }, [account.state, conversation.state]); + + useEffect(() => { + const connected = []; + card.state.cards.forEach(contact => { + if (contact?.card?.detail?.status === 'connected') { + connected.push(contact.card); + } + }); + updateState({ connected }); + }, [card.state]); + + useEffect(() => { + const cardId = conversation.state.card?.cardId; + const profileGuid = profile.state.identity?.guid; + const channel = conversation.state.channel; + const cards = card.state.cards; + const cardImageUrl = card.actions.getCardImageUrl; + const { logo, subject } = getChannelSubjectLogo(cardId, profileGuid, channel, cards, cardImageUrl); + const timestamp = conversation.state.channel?.detail?.created; + + let created; + const date = new Date(item.detail.created * 1000); + const now = new Date(); + const offset = now.getTime() - date.getTime(); + if(offset < 86400000) { + created = moment(date).format('h:mma'); + } + else if (offset < 31449600000) { + created = moment(date).format('M/DD'); + } + else { + created = moment(date).format('M/DD/YYYY'); + } + + updateState({ logo, subject, created }); + }, [conversation]); + + const actions = { + showEditMembers: () => { + updateState({ editMembers: true }); + }, + hideEditMembers: () => { + updateState({ editMembers: false }); + }, + showEditSubject: () => { + updateState({ editSubject: true }); + }, + hideEditSubject: () => { + updateState({ editSubject: false }); + }, + setSubjectUpdate: (subjectUpdate) => { + updateState({ subjectUpdate }); + }, + saveSubject: async () => { + if (state.locked) { + await conversation.actions.setSealedSubject(state.subjectUpdate, account.state.sealKey); + } + else { + await conversation.actions.setSubject(state.subjectUpdate); + } + }, + remove: async () => { + await conversation.actions.removeChannel(); + }, + block: async() => { + await conversation.actions.setChannelFlag(); + }, + report: async() => { + await conversation.actions.addChannelAlert(); + }, + setNotifications: async (notification) => { + await conversation.actions.setNotifications(notification); + }, + }; + + return { state, actions }; +}