From edcffa6a6781f242d858d1e869d9431d3694478f Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Tue, 11 Oct 2022 12:18:08 -0700 Subject: [PATCH] support blocking and unblocking topics --- app/mobile/src/context/useCardContext.hook.js | 23 ++++ .../src/context/useChannelContext.hook.js | 19 +++ .../context/useConversationContext.hook.js | 11 ++ .../src/context/useStoreContext.hook.js | 13 +- .../src/session/channels/useChannels.hook.js | 8 +- app/mobile/src/session/details/Details.jsx | 77 ++++++++--- .../src/session/details/Details.styled.js | 2 + .../src/session/details/useDetails.hook.js | 3 + .../profile/blockedTopics/BlockedTopics.jsx | 45 ++++++- .../blockedTopics/BlockedTopics.styled.js | 46 +++++++ .../blockedTopics/useBlockedTopics.hook.js | 122 ++++++++++++++++++ .../src/session/profile/useProfile.hook.js | 6 +- 12 files changed, 348 insertions(+), 27 deletions(-) create mode 100644 app/mobile/src/session/profile/blockedTopics/BlockedTopics.styled.js create mode 100644 app/mobile/src/session/profile/blockedTopics/useBlockedTopics.hook.js diff --git a/app/mobile/src/context/useCardContext.hook.js b/app/mobile/src/context/useCardContext.hook.js index f0552319..00c696db 100644 --- a/app/mobile/src/context/useCardContext.hook.js +++ b/app/mobile/src/context/useCardContext.hook.js @@ -190,6 +190,17 @@ export function useCardContext() { } } } + const setCardChannelBlocked = (cardId, channelId, blocked) => { + let card = cards.current.get(cardId); + if (card) { + let channel = card.channels.get(channelId); + if (channel) { + channel.blocked = blocked; + card.channels.set(channelId, channel); + cards.current.set(cardId, card); + } + } + } const clearCardChannel = (cardId, channelId) => { let card = cards.current.get(cardId); if (card) { @@ -438,6 +449,18 @@ export function useCardContext() { setCardChannelSyncRevision(cardId, channelId, revision); updateState({ cards: cards.current }); }, + setChannelBlocked: async (cardId, channelId) => { + const { guid } = session.current; + await store.actions.setCardChannelItemBlocked(guid, cardId, channelId); + setCardChannelBlocked(cardId, channelId, true); + updateState({ cards: cards.current }); + }, + clearChannelBlocked: async (cardId, channelId) => { + const { guid } = session.current; + await store.actions.clearCardChannelItemBlocked(guid, cardId, channelId); + setCardChannelBlocked(cardId, channelId, false); + updateState({ cards: cards.current }); + }, getChannelTopicItems: async (cardId, channelId) => { const { guid } = session.current; return await store.actions.getCardChannelTopicItems(guid, cardId, channelId); diff --git a/app/mobile/src/context/useChannelContext.hook.js b/app/mobile/src/context/useChannelContext.hook.js index 40c53b25..65fb2a5b 100644 --- a/app/mobile/src/context/useChannelContext.hook.js +++ b/app/mobile/src/context/useChannelContext.hook.js @@ -83,6 +83,13 @@ export function useChannelContext() { channels.current.set(channelId, channel); } } + const setChannelBlocked = (channelId, blocked) => { + let channel = channels.current.get(channelId); + if (channel) { + channel.blocked = blocked; + channels.current.set(channelId, channel); + } + } const sync = async () => { @@ -182,6 +189,18 @@ export function useChannelContext() { setChannelSyncRevision(channelId, revision); updateState({ channels: channels.current }); }, + setBlocked: async (channelId) => { + const { guid } = session.current; + await store.actions.setChannelItemBlocked(guid, channelId); + setChannelBlocked(channelId, 1); + updateState({ channels: channels.current }); + }, + clearBlocked: async (channelId) => { + const { guid } = session.current; + await store.actions.clearChannelItemBlocked(guid, channelId); + setChannelBlocked(channelId, 0); + updateState({ channels: channels.current }); + }, getTopicItems: async (channelId) => { const { guid } = session.current; return await store.actions.getChannelTopicItems(guid, channelId); diff --git a/app/mobile/src/context/useConversationContext.hook.js b/app/mobile/src/context/useConversationContext.hook.js index 0ce26f01..15c2df6f 100644 --- a/app/mobile/src/context/useConversationContext.hook.js +++ b/app/mobile/src/context/useConversationContext.hook.js @@ -375,6 +375,17 @@ export function useConversationContext() { await channel.actions.clearCard(channelId, id); } }, + setBlocked: async () => { + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + if (cardId) { + await card.actions.setChannelBlocked(cardId, channelId); + } + else { + await channel.actions.setBlocked(channelId); + } + } + }, } return { state, actions } diff --git a/app/mobile/src/context/useStoreContext.hook.js b/app/mobile/src/context/useStoreContext.hook.js index da6be906..78f90cb1 100644 --- a/app/mobile/src/context/useStoreContext.hook.js +++ b/app/mobile/src/context/useStoreContext.hook.js @@ -1,7 +1,7 @@ import { useEffect, useState, useRef, useContext } from 'react'; import SQLite from "react-native-sqlite-storage"; -const DATABAG_DB = 'databag_v038.db'; +const DATABAG_DB = 'databag_v039.db'; export function useStoreContext() { const [state, setState] = useState({}); @@ -12,7 +12,7 @@ export function useStoreContext() { } const initSession = async (guid) => { - await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_${guid} (channel_id text, revision integer, detail_revision integer, topic_revision integer, sync_revision integer, detail text, summary text, offsync integer, read_revision integer, unique(channel_id))`); + await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_${guid} (channel_id text, revision integer, detail_revision integer, topic_revision integer, blocked integer, sync_revision integer, detail text, summary text, offsync integer, read_revision integer, unique(channel_id))`); await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_topic_${guid} (channel_id text, topic_id text, revision integer, detail_revision integer, detail text, unique(channel_id, topic_id))`); await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_${guid} (card_id text, revision integer, detail_revision integer, profile_revision integer, detail text, profile text, notified_view integer, notified_article integer, notified_profile integer, notified_channel integer, offsync integer, blocked integer, unique(card_id))`); await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_channel_${guid} (card_id text, channel_id text, revision integer, detail_revision integer, topic_revision integer, sync_revision integer, detail text, summary text, offsync integer, blocked integer, read_revision integer, unique(card_id, channel_id))`); @@ -189,6 +189,12 @@ export function useStoreContext() { setChannelItemSyncRevision: async (guid, channelId, revision) => { await db.current.executeSql(`UPDATE channel_${guid} set sync_revision=? where channel_id=?`, [revision, channelId]); }, + setChannelItemBlocked: async (guid, channelId) => { + await db.current.executeSql(`UPDATE channel_${guid} set blocked=? where channel_id=?`, [1, channelId]); + }, + clearChannelItemBlocked: async (guid, channelId) => { + await db.current.executeSql(`UPDATE channel_${guid} set blocked=? where channel_id=?`, [0, channelId]); + }, setChannelItemDetail: async (guid, channelId, revision, detail) => { await db.current.executeSql(`UPDATE channel_${guid} set detail_revision=?, detail=? where channel_id=?`, [revision, encodeObject(detail), channelId]); }, @@ -207,7 +213,7 @@ export function useStoreContext() { }; }, getChannelItems: async (guid) => { - const values = await getAppValues(db.current, `SELECT channel_id, read_revision, revision, sync_revision, detail_revision, topic_revision, detail, summary FROM channel_${guid}`, []); + const values = await getAppValues(db.current, `SELECT channel_id, read_revision, revision, sync_revision, blocked, detail_revision, topic_revision, detail, summary FROM channel_${guid}`, []); return values.map(channel => ({ channelId: channel.channel_id, revision: channel.revision, @@ -215,6 +221,7 @@ export function useStoreContext() { detailRevision: channel.detail_revision, topicRevision: channel.topic_revision, syncRevision: channel.sync_revision, + blocked: channel.blocked, detail: decodeObject(channel.detail), summary: decodeObject(channel.summary), })); diff --git a/app/mobile/src/session/channels/useChannels.hook.js b/app/mobile/src/session/channels/useChannels.hook.js index 13d1d85b..1bb4950d 100644 --- a/app/mobile/src/session/channels/useChannels.hook.js +++ b/app/mobile/src/session/channels/useChannels.hook.js @@ -126,7 +126,7 @@ export function useChannels() { const timestamp = item?.summary?.lastTopic?.created; - return { cardId: item.cardId, channelId: item.channelId, contacts, logo, subject, message, updated, revision: item.revision, timestamp }; + return { cardId: item.cardId, channelId: item.channelId, contacts, logo, subject, message, updated, revision: item.revision, timestamp, blocked: item.blocked === 1 }; } useEffect(() => { @@ -137,10 +137,14 @@ export function useChannels() { } }); merged.push(...Array.from(channel.state.channels.values())); - + const items = merged.map(setChannelEntry); const filtered = items.filter(item => { + if (item.blocked === true) { + return false; + } + if (!state.filter) { return true; } diff --git a/app/mobile/src/session/details/Details.jsx b/app/mobile/src/session/details/Details.jsx index a8af618a..d90202e4 100644 --- a/app/mobile/src/session/details/Details.jsx +++ b/app/mobile/src/session/details/Details.jsx @@ -28,18 +28,58 @@ export function DetailsBody({ channel, clearConversation }) { } } - const remove = async () => { - try { - await actions.remove(); - clearConversation(); - } - catch (err) { - console.log(err); - Alert.alert( - 'Failed to Delete Topic', - '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.' + ) + } + }, + } + ] + ); } return ( @@ -64,16 +104,19 @@ export function DetailsBody({ channel, clearConversation }) { Delete Topic )} - { !state.hostId && ( - - Edit Membership - - )} { state.hostId && ( Leave Topic )} + + Block Topic + + { !state.hostId && ( + + Edit Membership + + )} diff --git a/app/mobile/src/session/details/Details.styled.js b/app/mobile/src/session/details/Details.styled.js index 6d8bd143..b55daa7e 100644 --- a/app/mobile/src/session/details/Details.styled.js +++ b/app/mobile/src/session/details/Details.styled.js @@ -36,6 +36,8 @@ export const styles = StyleSheet.create({ fontSize: 18, flexShrink: 1, minWidth: 0, + color: Colors.text, + paddingRight: 4, }, created: { fontSize: 16, diff --git a/app/mobile/src/session/details/useDetails.hook.js b/app/mobile/src/session/details/useDetails.hook.js index 71ba7ad6..1d3ab2e2 100644 --- a/app/mobile/src/session/details/useDetails.hook.js +++ b/app/mobile/src/session/details/useDetails.hook.js @@ -58,6 +58,9 @@ export function useDetails() { remove: async () => { await conversation.actions.remove(); }, + block: async() => { + await conversation.actions.setBlocked(); + }, }; return { state, actions }; diff --git a/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx index b28b34ab..cb788d41 100644 --- a/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx +++ b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx @@ -1,6 +1,47 @@ -import { Text } from 'react-native'; +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() { - return TOPICS + + 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..ff817e48 --- /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.profile.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 index 32521b26..9e29199b 100644 --- a/app/mobile/src/session/profile/useProfile.hook.js +++ b/app/mobile/src/session/profile/useProfile.hook.js @@ -69,9 +69,9 @@ export function useProfile() { app.actions.logout(); navigate('/'); }, - setVisible: async (visible) => { - updateState({ visible }); - await account.actions.setSearchable(visible); + setVisible: async (searchable) => { + updateState({ searchable }); + await account.actions.setSearchable(searchable); }, setProfileImage: async (data) => { await profile.actions.setProfileImage(data);