* creating centralized location to handle stale background state * adding global background threashold * creating auth context * disconnecting from liquid node during force auth * reset did get to homepage variable * fix crash if globalContactsInformation.myProfile is not set * resetting custody account settings * clearing keys information on reset * remove roostock swap listener on reset * resetting webview context on stale auth * adding auth reset to spark context * only load user info once per session * only open tables once per session * making sure to recheck login status to fix issue with initial load * adding large heap flag * fixing sql table open bug * making sure didLoadUserSettings runs on app open * fixing unqiueName being null bug * fixing race condition causing switch to naive spark version * misc changes
437 lines
13 KiB
JavaScript
437 lines
13 KiB
JavaScript
import React, {
|
|
createContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
useCallback,
|
|
} from 'react';
|
|
import {
|
|
addDataToCollection,
|
|
getDataFromCollection,
|
|
syncDatabasePayment,
|
|
} from '../db';
|
|
import {
|
|
decryptMessage,
|
|
encriptMessage,
|
|
} from '../app/functions/messaging/encodingAndDecodingMessages';
|
|
|
|
import {
|
|
CONTACTS_TRANSACTION_UPDATE_NAME,
|
|
contactsSQLEventEmitter,
|
|
deleteCachedMessages,
|
|
getCachedMessages,
|
|
queueSetCashedMessages,
|
|
} from '../app/functions/messaging/cachedMessages';
|
|
import { db } from '../db/initializeFirebase';
|
|
import { useKeysContext } from './keys';
|
|
import {
|
|
collection,
|
|
onSnapshot,
|
|
or,
|
|
orderBy,
|
|
query,
|
|
where,
|
|
} from '@react-native-firebase/firestore';
|
|
import { getCachedProfileImage } from '../app/functions/cachedImage';
|
|
|
|
// Create a context for the WebView ref
|
|
const GlobalContacts = createContext(null);
|
|
|
|
export const GlobalContactsList = ({ children }) => {
|
|
const { contactsPrivateKey, publicKey } = useKeysContext();
|
|
const [globalContactsInformation, setGlobalContactsInformation] = useState(
|
|
{},
|
|
);
|
|
const [contactsMessags, setContactsMessagses] = useState({});
|
|
const lookForNewMessages = useRef(false);
|
|
const unsubscribeMessagesRef = useRef(null);
|
|
const addedContacts = globalContactsInformation.addedContacts;
|
|
|
|
const toggleGlobalContactsInformation = useCallback(
|
|
(newData, writeToDB) => {
|
|
setGlobalContactsInformation(prev => {
|
|
const newContacts = { ...prev, ...newData };
|
|
if (writeToDB) {
|
|
addDataToCollection(
|
|
{ contacts: newContacts },
|
|
'blitzWalletUsers',
|
|
publicKey,
|
|
);
|
|
}
|
|
return newContacts;
|
|
});
|
|
},
|
|
[publicKey],
|
|
);
|
|
|
|
const decodedAddedContacts = useMemo(() => {
|
|
if (!publicKey || !addedContacts) return [];
|
|
return typeof addedContacts === 'string'
|
|
? [
|
|
...JSON.parse(
|
|
decryptMessage(contactsPrivateKey, publicKey, addedContacts),
|
|
),
|
|
]
|
|
: [];
|
|
}, [addedContacts, publicKey, contactsPrivateKey]);
|
|
|
|
const updatedCachedMessagesStateFunction = useCallback(async () => {
|
|
if (!Object.keys(globalContactsInformation).length || !contactsPrivateKey)
|
|
return;
|
|
const savedMessages = await getCachedMessages();
|
|
setContactsMessagses(savedMessages);
|
|
const unknownContacts = await Promise.all(
|
|
Object.keys(savedMessages)
|
|
.filter(key => key !== 'lastMessageTimestamp')
|
|
.filter(
|
|
contact =>
|
|
!decodedAddedContacts.find(
|
|
contactElement => contactElement.uuid === contact,
|
|
) && contact !== globalContactsInformation.myProfile.uuid,
|
|
)
|
|
.map(contact => getDataFromCollection('blitzWalletUsers', contact)),
|
|
);
|
|
|
|
const newContats = unknownContacts
|
|
.filter(
|
|
retrivedContact =>
|
|
retrivedContact &&
|
|
retrivedContact.uuid !== globalContactsInformation.myProfile.uuid,
|
|
)
|
|
.map(retrivedContact => ({
|
|
bio: retrivedContact.contacts.myProfile.bio || 'No bio',
|
|
isFavorite: false,
|
|
name: retrivedContact.contacts.myProfile.name,
|
|
receiveAddress: retrivedContact.contacts.myProfile.receiveAddress,
|
|
uniqueName: retrivedContact.contacts.myProfile.uniqueName,
|
|
uuid: retrivedContact.contacts.myProfile.uuid,
|
|
isAdded: false,
|
|
unlookedTransactions: 0,
|
|
}));
|
|
|
|
if (newContats.length > 0) {
|
|
await Promise.allSettled(
|
|
newContats.map(contact => getCachedProfileImage(contact.uuid)),
|
|
);
|
|
|
|
toggleGlobalContactsInformation(
|
|
{
|
|
myProfile: { ...globalContactsInformation.myProfile },
|
|
addedContacts: encriptMessage(
|
|
contactsPrivateKey,
|
|
globalContactsInformation.myProfile.uuid,
|
|
JSON.stringify(decodedAddedContacts.concat(newContats)),
|
|
),
|
|
},
|
|
true,
|
|
);
|
|
}
|
|
}, [globalContactsInformation, decodedAddedContacts, contactsPrivateKey]);
|
|
|
|
useEffect(() => {
|
|
async function handleUpdate(updateType) {
|
|
try {
|
|
console.log('Received contact transaction update type', updateType);
|
|
updatedCachedMessagesStateFunction();
|
|
} catch (err) {
|
|
console.log('error in contact messages update function', err);
|
|
}
|
|
}
|
|
contactsSQLEventEmitter.removeAllListeners(
|
|
CONTACTS_TRANSACTION_UPDATE_NAME,
|
|
);
|
|
contactsSQLEventEmitter.on(CONTACTS_TRANSACTION_UPDATE_NAME, handleUpdate);
|
|
|
|
return () => {
|
|
contactsSQLEventEmitter.removeAllListeners(
|
|
CONTACTS_TRANSACTION_UPDATE_NAME,
|
|
);
|
|
};
|
|
}, [updatedCachedMessagesStateFunction]);
|
|
|
|
useEffect(() => {
|
|
if (!Object.keys(globalContactsInformation).length) return;
|
|
const now = new Date().getTime();
|
|
|
|
// Unsubscribe from previous listeners before setting new ones
|
|
if (unsubscribeMessagesRef.current) {
|
|
unsubscribeMessagesRef.current();
|
|
}
|
|
|
|
const combinedMessageQuery = query(
|
|
collection(db, 'contactMessages'),
|
|
where('timestamp', '>', now),
|
|
or(
|
|
where('toPubKey', '==', globalContactsInformation.myProfile.uuid),
|
|
where('fromPubKey', '==', globalContactsInformation.myProfile.uuid),
|
|
),
|
|
orderBy('timestamp'),
|
|
);
|
|
|
|
unsubscribeMessagesRef.current = onSnapshot(
|
|
combinedMessageQuery,
|
|
snapshot => {
|
|
if (!snapshot?.docChanges()?.length) return;
|
|
let newMessages = [];
|
|
snapshot.docChanges().forEach(change => {
|
|
if (change.type === 'added') {
|
|
const newMessage = change.doc.data();
|
|
// Log whether it's sent or received
|
|
const isReceived =
|
|
newMessage.toPubKey === globalContactsInformation.myProfile.uuid;
|
|
console.log(
|
|
`${isReceived ? 'received' : 'sent'} a new message`,
|
|
newMessage,
|
|
);
|
|
if (typeof newMessage.message === 'string') {
|
|
const sendersPubkey =
|
|
newMessage.toPubKey === globalContactsInformation.myProfile.uuid
|
|
? newMessage.fromPubKey
|
|
: newMessage.toPubKey;
|
|
const decoded = decryptMessage(
|
|
contactsPrivateKey,
|
|
sendersPubkey,
|
|
newMessage.message,
|
|
);
|
|
|
|
if (!decoded) return;
|
|
let parsedMessage;
|
|
try {
|
|
parsedMessage = JSON.parse(decoded);
|
|
} catch (err) {
|
|
console.log('error parsing decoded message', err);
|
|
return;
|
|
}
|
|
newMessages.push({
|
|
...newMessage,
|
|
message: parsedMessage,
|
|
sendersPubkey,
|
|
isReceived,
|
|
});
|
|
} else newMessages.push(newMessage);
|
|
}
|
|
});
|
|
if (newMessages.length > 0) {
|
|
queueSetCashedMessages({
|
|
newMessagesList: newMessages,
|
|
myPubKey: globalContactsInformation.myProfile.uuid,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
return () => {
|
|
if (unsubscribeMessagesRef.current) {
|
|
unsubscribeMessagesRef.current();
|
|
}
|
|
};
|
|
}, [globalContactsInformation?.myProfile?.uuid, contactsPrivateKey]);
|
|
|
|
const addContact = useCallback(
|
|
async contact => {
|
|
try {
|
|
const newContact = {
|
|
name: contact.name || '',
|
|
nameLower: contact.nameLower || '',
|
|
bio: contact.bio,
|
|
unlookedTransactions: 0,
|
|
isLNURL: contact.isLNURL,
|
|
uniqueName: contact.uniqueName || '',
|
|
uuid: contact.uuid,
|
|
isAdded: true,
|
|
isFavorite: false,
|
|
profileImage: contact.profileImage,
|
|
receiveAddress: contact.receiveAddress,
|
|
transactions: [],
|
|
};
|
|
|
|
let newAddedContacts = JSON.parse(JSON.stringify(decodedAddedContacts));
|
|
|
|
const isContactInAddedContacts = newAddedContacts.filter(
|
|
addedContact => addedContact.uuid === newContact.uuid,
|
|
).length;
|
|
|
|
if (isContactInAddedContacts) {
|
|
newAddedContacts = newAddedContacts.map(addedContact => {
|
|
if (addedContact.uuid === newContact.uuid) {
|
|
return {
|
|
...addedContact,
|
|
name: newContact.name,
|
|
nameLower: newContact.nameLower,
|
|
bio: newContact.bio,
|
|
unlookedTransactions: 0,
|
|
isAdded: true,
|
|
};
|
|
} else return addedContact;
|
|
});
|
|
} else newAddedContacts.push(newContact);
|
|
|
|
toggleGlobalContactsInformation(
|
|
{
|
|
myProfile: {
|
|
...globalContactsInformation.myProfile,
|
|
didEditProfile: true,
|
|
},
|
|
addedContacts: encriptMessage(
|
|
contactsPrivateKey,
|
|
publicKey,
|
|
JSON.stringify(newAddedContacts),
|
|
),
|
|
},
|
|
true,
|
|
);
|
|
} catch (err) {
|
|
console.log('Error adding contact', err);
|
|
}
|
|
},
|
|
[
|
|
decodedAddedContacts,
|
|
contactsPrivateKey,
|
|
publicKey,
|
|
globalContactsInformation,
|
|
],
|
|
);
|
|
|
|
const deleteContact = useCallback(
|
|
async contact => {
|
|
try {
|
|
const newAddedContacts = decodedAddedContacts
|
|
.map(savedContacts => {
|
|
if (savedContacts.uuid === contact.uuid) {
|
|
return null;
|
|
} else return savedContacts;
|
|
})
|
|
.filter(contact => contact);
|
|
|
|
await deleteCachedMessages(contact.uuid);
|
|
|
|
toggleGlobalContactsInformation(
|
|
{
|
|
addedContacts: encriptMessage(
|
|
contactsPrivateKey,
|
|
publicKey,
|
|
JSON.stringify(newAddedContacts),
|
|
),
|
|
myProfile: { ...globalContactsInformation.myProfile },
|
|
},
|
|
true,
|
|
);
|
|
} catch (err) {
|
|
console.log('Error deleating contact', err);
|
|
}
|
|
},
|
|
[
|
|
decodedAddedContacts,
|
|
contactsPrivateKey,
|
|
publicKey,
|
|
globalContactsInformation,
|
|
],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!Object.keys(globalContactsInformation).length) return;
|
|
if (lookForNewMessages.current) return;
|
|
lookForNewMessages.current = true;
|
|
syncDatabasePayment(
|
|
globalContactsInformation.myProfile.uuid,
|
|
updatedCachedMessagesStateFunction,
|
|
contactsPrivateKey,
|
|
);
|
|
}, [
|
|
globalContactsInformation,
|
|
updatedCachedMessagesStateFunction,
|
|
contactsPrivateKey,
|
|
]);
|
|
|
|
const giftCardsList = useMemo(() => {
|
|
if (!contactsMessags) return [];
|
|
|
|
const actualContacts = Object.keys(contactsMessags);
|
|
const lastMessageTimestampIndex = actualContacts.indexOf(
|
|
'lastMessageTimestamp',
|
|
);
|
|
|
|
// Remove lastMessageTimestamp efficiently
|
|
if (lastMessageTimestampIndex > -1) {
|
|
actualContacts.splice(lastMessageTimestampIndex, 1);
|
|
}
|
|
|
|
if (actualContacts.length === 0) return [];
|
|
|
|
const giftCards = [];
|
|
|
|
// Process contacts efficiently
|
|
for (const contact of actualContacts) {
|
|
const contactData = contactsMessags[contact];
|
|
if (!contactData?.messages?.length) continue;
|
|
|
|
// Use for loop for better performance than filter + push
|
|
const messages = contactData.messages;
|
|
for (let i = 0; i < messages.length; i++) {
|
|
const message = messages[i];
|
|
if (message.message?.giftCardInfo && !message.message.didSend) {
|
|
giftCards.push(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort in-place for memory efficiency
|
|
giftCards.sort((a, b) => {
|
|
const timeA = a.serverTimestamp || a.timestamp;
|
|
const timeB = b.serverTimestamp || b.timestamp;
|
|
return timeB - timeA;
|
|
});
|
|
|
|
return giftCards;
|
|
}, [contactsMessags]);
|
|
|
|
const hasUnlookedTransactions = useMemo(() => {
|
|
return Object.keys(contactsMessags).some(contactUUID => {
|
|
if (
|
|
contactUUID === 'lastMessageTimestamp' ||
|
|
contactUUID === globalContactsInformation?.myProfile?.uuid
|
|
) {
|
|
return false;
|
|
}
|
|
const messages = contactsMessags[contactUUID]?.messages;
|
|
return messages?.some(message => !message.message.wasSeen) || false;
|
|
});
|
|
}, [contactsMessags, globalContactsInformation?.myProfile?.uuid]);
|
|
|
|
const contextValue = useMemo(
|
|
() => ({
|
|
decodedAddedContacts,
|
|
globalContactsInformation,
|
|
toggleGlobalContactsInformation,
|
|
contactsMessags,
|
|
updatedCachedMessagesStateFunction,
|
|
giftCardsList,
|
|
hasUnlookedTransactions,
|
|
deleteContact,
|
|
addContact,
|
|
}),
|
|
[
|
|
decodedAddedContacts,
|
|
globalContactsInformation,
|
|
toggleGlobalContactsInformation,
|
|
contactsMessags,
|
|
updatedCachedMessagesStateFunction,
|
|
giftCardsList,
|
|
hasUnlookedTransactions,
|
|
deleteContact,
|
|
addContact,
|
|
],
|
|
);
|
|
|
|
return (
|
|
<GlobalContacts.Provider value={contextValue}>
|
|
{children}
|
|
</GlobalContacts.Provider>
|
|
);
|
|
};
|
|
|
|
export const useGlobalContacts = () => {
|
|
return React.useContext(GlobalContacts);
|
|
};
|