From 41a5d8f5fe2777fe94d0b106fc88f93eff6e4837 Mon Sep 17 00:00:00 2001 From: duogenesis <136373989+duogenesis@users.noreply.github.com> Date: Sat, 28 Oct 2023 17:56:05 +1100 Subject: [PATCH] Move blocked and hidden messages to the archive (#110) --- components/conversation-screen.tsx | 4 +- components/inbox-item.tsx | 8 +- components/inbox-tab.tsx | 43 +++++++--- components/speech-bubble.tsx | 4 +- components/tab-bar.tsx | 19 +++-- util/util.tsx | 14 ++- xmpp/xmpp.tsx | 132 ++++++++++++++++------------- 7 files changed, 137 insertions(+), 87 deletions(-) diff --git a/components/conversation-screen.tsx b/components/conversation-screen.tsx index fb51075c..9665d391 100644 --- a/components/conversation-screen.tsx +++ b/components/conversation-screen.tsx @@ -53,7 +53,7 @@ const ConversationScreen = ({navigation, route}) => { const personId: number = route?.params?.personId; const name: string = route?.params?.name; const imageUuid: number = route?.params?.imageUuid; - const isDeletedUser: boolean = route?.params?.isDeletedUser; + const isAvailableUser: boolean = route?.params?.isAvailableUser ?? true; const listRef = useRef(null) @@ -320,7 +320,7 @@ const ConversationScreen = ({navigation, route}) => { {lastMessageStatus === 'blocked' ? name + ' is unavailable right now. Try messaging someone else!' : '' } {lastMessageStatus === 'not unique' ? `Someone already sent that intro! Try sending ${name} a different message...` : '' } - {!messageFetchTimeout && !isDeletedUser && + {!messageFetchTimeout && isAvailableUser && } diff --git a/components/inbox-item.tsx b/components/inbox-item.tsx index c16aabdc..250b3e85 100644 --- a/components/inbox-item.tsx +++ b/components/inbox-item.tsx @@ -21,7 +21,7 @@ const InboxItem = ({ matchPercentage, lastMessage, lastMessageTimestamp, - isDeletedUser, + isAvailableUser, }: { wasRead: boolean name: string @@ -30,7 +30,7 @@ const InboxItem = ({ matchPercentage: number lastMessage: string lastMessageTimestamp: Date - isDeletedUser: boolean + isAvailableUser: boolean }) => { const navigation = useNavigation(); @@ -60,8 +60,8 @@ const InboxItem = ({ const onPress = useCallback(() => navigation.navigate( 'Conversation Screen', - { personId, name, imageUuid, isDeletedUser } - ), [personId, name, imageUuid, isDeletedUser]); + { personId, name, imageUuid, isAvailableUser } + ), [personId, name, imageUuid, isAvailableUser]); return ( { const [showArchive, setShowArchive] = useState(false); const listRef = useRef(undefined); + const _inboxStats = inbox ? inboxStats(inbox) : null; + + const numUnreadChats = (() => { + if (!_inboxStats) { + return 0; + } + + return showArchive ? + _inboxStats.chats.numUnreadUnavailable : + _inboxStats.chats.numUnreadAvailable; + })(); + + const numUnreadIntros = (() => { + if (!_inboxStats) { + return 0; + } + + return showArchive ? + _inboxStats.intros.numUnreadUnavailable : + _inboxStats.intros.numUnreadAvailable; + })(); + const buttonOpacity = useRef(new Animated.Value(0)).current; const fadeOut = useCallback(() => { @@ -116,20 +138,19 @@ const InboxTab_ = ({navigation}) => { const a = section.conversations[0]; - const pageSize = 10; const page = [...section.conversations] - .filter((c) => c.isDeletedUser === showArchive) + .filter((c) => (!c.isAvailableUser || c.wasArchivedByMe) === showArchive) .sort((a, b) => { if (sectionName === 'intros' && sortByIndex === 1) { return compareArrays( - [!b.isDeletedUser, b.matchPercentage, +b.lastMessageTimestamp], - [!a.isDeletedUser, a.matchPercentage, +a.lastMessageTimestamp], + [b.matchPercentage, +b.lastMessageTimestamp], + [a.matchPercentage, +a.lastMessageTimestamp], ); } else { return compareArrays( - [!b.isDeletedUser, +b.lastMessageTimestamp, b.matchPercentage], - [!a.isDeletedUser, +a.lastMessageTimestamp, a.matchPercentage], + [+b.lastMessageTimestamp, b.matchPercentage], + [+a.lastMessageTimestamp, a.matchPercentage], ); } }) @@ -157,8 +178,8 @@ const InboxTab_ = ({navigation}) => { <> { }} /> - {!isTooManyTapped && sectionIndex === 0 && inbox && inbox.intros.numUnread >= 5 && + {!isTooManyTapped && sectionIndex === 0 && !showArchive && numUnreadIntros >= 3 && { matchPercentage={x.item.matchPercentage} lastMessage={x.item.lastMessage} lastMessageTimestamp={x.item.lastMessageTimestamp} - isDeletedUser={x.item.isDeletedUser} + isAvailableUser={x.item.isAvailableUser} /> ), []); diff --git a/components/speech-bubble.tsx b/components/speech-bubble.tsx index caa2c87a..7cbda81d 100644 --- a/components/speech-bubble.tsx +++ b/components/speech-bubble.tsx @@ -7,7 +7,7 @@ import { View, } from 'react-native'; import { DefaultText } from './default-text'; -import { friendlyTimestamp } from '../util/util'; +import { longFriendlyTimestamp } from '../util/util'; type State = 'Read' | 'Delivered'; @@ -66,7 +66,7 @@ const SpeechBubble = (props: Props) => { color: '#666', }} > - {friendlyTimestamp(props.timestamp)} + {longFriendlyTimestamp(props.timestamp)} } {props.state && diff --git a/components/tab-bar.tsx b/components/tab-bar.tsx index 406ad1cb..dee07eb9 100644 --- a/components/tab-bar.tsx +++ b/components/tab-bar.tsx @@ -12,7 +12,7 @@ import { DefaultText } from './default-text'; import Ionicons from '@expo/vector-icons/Ionicons'; import { StackActions } from '@react-navigation/native'; import { QAndADevice } from './q-and-a-device'; -import { Inbox, observeInbox } from '../xmpp/xmpp'; +import { Inbox, inboxStats, observeInbox } from '../xmpp/xmpp'; const displayedTabs: Set = new Set([ "Q&A", @@ -24,13 +24,16 @@ const displayedTabs: Set = new Set([ const TabBar = ({state, descriptors, navigation}) => { const [inboxHasUnread, setInboxHasUnread] = useState(false); - const onChangeInbox = useCallback( - (inbox: Inbox) => setInboxHasUnread( - inbox?.numUnread !== undefined && - inbox?.numUnread > 0 - ), - [] - ); + const onChangeInbox = useCallback((inbox: Inbox | null) => { + if (!inbox) { + setInboxHasUnread(false); + return; + } + + const numUnreadAvailable = inboxStats(inbox).numUnreadAvailable; + + setInboxHasUnread(numUnreadAvailable > 0); + }, []); observeInbox(onChangeInbox); return ( diff --git a/util/util.tsx b/util/util.tsx index 83a9665c..cd775424 100644 --- a/util/util.tsx +++ b/util/util.tsx @@ -35,7 +35,7 @@ const friendlyTimestamp = (date: Date): string => { return format(date, 'h:mm aaa') } else if (isThisWeek(date)) { // Format as 'eeee' (day of the week) - return format(date, 'eeee') + return format(date, 'eee') } else if (isThisYear(date)) { // Format as 'd MMM' (date and month) return format(date, 'd MMM') @@ -45,6 +45,17 @@ const friendlyTimestamp = (date: Date): string => { } }; +const longFriendlyTimestamp = (date: Date): string => { + // Format as 'hh:mm' + const timeOfDay = format(date, 'h:mm aaa'); + + if (isToday(date)) { + return timeOfDay; + } else { + return friendlyTimestamp(date) + ', ' + timeOfDay + } +}; + const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); const deleteFromArray = (array: T[], element: T): T[] => { @@ -68,5 +79,6 @@ export { deleteFromArray, friendlyTimestamp, isMobile, + longFriendlyTimestamp, withTimeout, }; diff --git a/xmpp/xmpp.tsx b/xmpp/xmpp.tsx index fb2b4bc9..18daa7fb 100644 --- a/xmpp/xmpp.tsx +++ b/xmpp/xmpp.tsx @@ -12,7 +12,7 @@ import { signedInUser } from '../App'; import { getRandomString } from '../random/string'; import { deviceId } from '../kv-storage/device-id'; -import { api } from '../api/api'; +import { japi } from '../api/api'; import { deleteFromArray, withTimeout, delay } from '../util/util'; // TODO: Catch more exceptions. If a network request fails, that shouldn't crash the app. @@ -43,7 +43,8 @@ type Conversation = { lastMessage: string lastMessageRead: boolean lastMessageTimestamp: Date - isDeletedUser: boolean + isAvailableUser: boolean + wasArchivedByMe: boolean }; type ConversationsMap = { [key: string]: Conversation }; @@ -52,21 +53,72 @@ type MarkDisplayedMap = { [key: string]: number }; type Conversations = { conversations: Conversation[] conversationsMap: ConversationsMap - numUnread: number }; type Inbox = { chats: Conversations intros: Conversations - numUnread: number }; -const emptyInbox = (): Inbox => ({ +const inboxStats = (inbox: Inbox): { chats: { - conversations: [], conversationsMap: {}, numUnread: 0 }, + numUnreadAvailable: number + numUnreadUnavailable: number + } intros: { - conversations: [], conversationsMap: {}, numUnread: 0 }, - numUnread: 0, + numUnreadAvailable: number + numUnreadUnavailable: number + } + numUnreadAvailable: number + numUnreadUnavailable: number +} => { + const unreadAvailable = (sum: number, c: Conversation) => + sum + ( c.isAvailableUser && !c.lastMessageRead ? 1 : 0); + + const unreadUnavailable = (sum: number, c: Conversation) => + sum + (!c.isAvailableUser && !c.lastMessageRead ? 1 : 0); + + const chats = { + numUnreadAvailable: inbox.chats.conversations.reduce( + unreadAvailable, + 0 + ), + numUnreadUnavailable: inbox.chats.conversations.reduce( + unreadUnavailable, + 0 + ), + }; + + const intros = { + numUnreadAvailable: inbox.intros.conversations.reduce( + unreadAvailable, + 0 + ), + numUnreadUnavailable: inbox.intros.conversations.reduce( + unreadUnavailable, + 0 + ), + }; + + const numUnreadAvailable = ( + chats .numUnreadAvailable + + intros.numUnreadAvailable); + + const numUnreadUnavailable = ( + chats .numUnreadUnavailable + + intros.numUnreadUnavailable); + + return { + chats, + intros, + numUnreadAvailable, + numUnreadUnavailable, + }; +}; + +const emptyInbox = (): Inbox => ({ + chats: { conversations: [], conversationsMap: {} }, + intros: { conversations: [], conversationsMap: {} }, }); let _xmpp: Client | undefined; @@ -109,11 +161,10 @@ const populateConversationList = async ( ): Promise => { const personIds: number[] = conversationList.map(c => c.personId); - const query = personIds.map(id => `prospect-person-id=${id}`).join('&'); // TODO: Better error handling const response = conversationList.length === 0 ? [] : - (await api('get', `/inbox-info?${query}`)).json; + (await japi('post', '/inbox-info', {person_ids: personIds})).json; const personIdToInfo = response.reduce((obj, item) => { obj[item.person_id] = item; @@ -121,10 +172,11 @@ const populateConversationList = async ( }, {}); conversationList.forEach((c: Conversation) => { - c.name = personIdToInfo[c.personId]?.name ?? 'Deleted account'; + c.name = personIdToInfo[c.personId]?.name ?? 'Unavailable User'; c.matchPercentage = personIdToInfo[c.personId]?.match_percentage ?? 0; c.imageUuid = personIdToInfo[c.personId]?.image_uuid ?? null; - c.isDeletedUser = personIdToInfo[c.personId] === undefined; + c.isAvailableUser = personIdToInfo[c.personId] !== undefined; + c.wasArchivedByMe = personIdToInfo[c.personId]?.was_archived_by_me ?? false; }); }; @@ -157,21 +209,13 @@ const setInboxSent = (recipientPersonId: number, message: string) => { const introsConversation = i.intros.conversationsMap[recipientPersonId] as Conversation | undefined; - i.chats.numUnread -= ( - chatsConversation ?.lastMessageRead ?? true) ? 0 : 1; - i.intros.numUnread -= ( - introsConversation?.lastMessageRead ?? true) ? 0 : 1; - - i.numUnread = ( - i.chats.numUnread + - i.intros.numUnread); - const updatedConversation: Conversation = { personId: recipientPersonId, name: '', matchPercentage: 0, imageUuid: null, - isDeletedUser: false, + isAvailableUser: true, + wasArchivedByMe: false, ...chatsConversation, ...introsConversation, lastMessage: message, @@ -217,28 +261,13 @@ const setInboxRecieved = async ( const introsConversation = inbox.intros.conversationsMap[fromPersonId] as Conversation | undefined; - inbox.chats.numUnread += ( - // The received message is the continuation of a 'chats' conversation - // whose last message was read - chatsConversation && chatsConversation.lastMessageRead - ) ? 1 : 0; - - inbox.intros.numUnread += ( - // The received message is the continuation of an 'intro' conversation - // whose last message was read - introsConversation && introsConversation.lastMessageRead || - // The received message is new - !introsConversation && !chatsConversation - ) ? 1 : 0; - - inbox.numUnread = inbox.chats.numUnread + inbox.intros.numUnread; - const updatedConversation: Conversation = { personId: fromPersonId, name: '', matchPercentage: 0, imageUuid: null, - isDeletedUser: false, + isAvailableUser: true, + wasArchivedByMe: false, ...chatsConversation, ...introsConversation, lastMessage: message, @@ -271,18 +300,6 @@ const setInboxDisplayed = async (fromPersonId: number) => { const introsConversation = inbox.intros.conversationsMap[fromPersonId] as Conversation | undefined; - inbox.chats.numUnread -= - (chatsConversation?.lastMessageRead ?? true) ? - 0 : - 1; - - inbox.intros.numUnread -= - (introsConversation?.lastMessageRead ?? true) ? - 0 : - 1; - - inbox.numUnread = inbox.chats.numUnread + inbox.intros.numUnread; - const updatedConversation = { ...chatsConversation, ...introsConversation, @@ -785,7 +802,8 @@ const _fetchBox = async ( const lastMessage = bodyText.toString(); const lastMessageRead = numUnread.toString() === '0'; const lastMessageTimestamp = new Date(timestamp.toString()); - const isDeletedUser = false; + const isAvailableUser = true; + const wasArchivedByMe = false; const conversation: Conversation = { personId, @@ -795,7 +813,8 @@ const _fetchBox = async ( lastMessage, lastMessageRead, lastMessageTimestamp, - isDeletedUser, + isAvailableUser, + wasArchivedByMe, }; conversationList.push(conversation); @@ -815,10 +834,6 @@ const _fetchBox = async ( const conversations: Conversations = { conversations: conversationList, conversationsMap: conversationListToMap(conversationList), - numUnread: conversationList.reduce( - (acc, conversation) => acc + (conversation.lastMessageRead ? 0 : 1), - 0 - ), }; await populateConversationList(conversations.conversations); @@ -844,12 +859,10 @@ const fetchBox = async (box: string): Promise => { const refreshInbox = async (): Promise => { const chats = await fetchBox('chats'); if (!chats) return; const intros = await fetchBox('inbox'); if (!intros) return; - const numUnread = chats.numUnread + intros.numUnread; setInbox((inbox: Inbox) => ({ chats, intros, - numUnread, })); }; @@ -868,6 +881,7 @@ export { Message, MessageStatus, fetchConversation, + inboxStats, login, logout, observeInbox,