Files
nirvana/packages/web/recoil/main.tsx
T

448 lines
13 KiB
TypeScript

import { User as FirebaseUser } from "@firebase/auth";
import Conversation, {
ConversationMember,
AudioClip,
Link,
ConversationMemberState,
} from "@nirvana/common/models/conversation";
import * as NirvanaUser from "@nirvana/common/models/user";
import { conversationService, userService } from "@nirvana/common/services";
import { atom, DefaultValue, selector, selectorFamily } from "recoil";
import {
today,
yesterday,
Days7Ago,
earlierThisMonth as earlyMonthDate,
} from "@nirvana/common/helpers/dateTime";
export enum RecoilActions {
TEST = "TEST",
CURR_PAGE_PATH = "CURR_PAGE_PATH",
ALL_COMPLETE_CONVERSATIONS = "ALL_COMPLETE_CONVERSATIONS",
ALL_USERS_CONVERSATION_RELATIONSHIPS = "ALL_USERS_CONVERSATION_RELATIONSHIPS",
USER_CONVO_ASSOCIATION_SELECTOR = "USER_CONVO_ASSOCIATION_SELECTOR",
ALL_RELEVANT_CONVERSATIONS = "ALL_RELEVANT_CONVERSATIONS",
SORTED_CONVERSATIONS = "SORTED_CONVERSATIONS",
LIVE_ROOMS = "LIVE_ROOMS",
LATER_CONVERSATIONS_SELECTOR = "LATER_CONVERSATIONS_SELECTOR",
PRIORITY_CONVERSATIONS_SELECTOR = "PRIORITY_CONVERSATIONS_SELECTOR",
DONE_CONVERSATIONS_SELECTOR = "DONE_CONVERSATIONS_SELECTOR",
RELATIVE_TIME_SECTIONS_CONVOS = "RELATIVE_TIME_SECTIONS_CONVOS",
CHECK_INCOMING_MESSAGE = "CHECK_INCOMING_MESSAGE",
ALL_RELEVANT_CONTACTS = "ALL_RELEVANT_CONTACTS",
RELEVANT_CONTACTS_SELECTOR_CACHE = "RELEVANT_CONTACTS_SELECTOR_CACHE",
USER_DATA = "USER_DATA",
SELECTED_CONVERSATION_ATOM = "SELECTED_CONVERSATION_ATOM",
COUNT_DEFAULT_CONVOS_SELECTOR = "COUNT_DEFAULT_CONVOS_SELECTOR",
COUNT_NAVIGATION_ITEMS = "COUNT_NAVIGATION_ITEMS",
IS_RECORDING_ATOM = "IS_RECORDING_ATOM",
HAS_MIC_PERMISSIONS = "HAS_MIC_PERMISSIONS",
AUDIO_QUEUE_ATOM = "AUDIO_QUEUE_ATOM",
AUDIO_QUEUE_CURRENT_ITEM_SELECTOR = "AUDIO_QUEUE_CURRENT_ITEM_SELECTOR",
}
export const nirvanaUserDataAtom = atom<NirvanaUser.default | null>({
key: RecoilActions.ALL_COMPLETE_CONVERSATIONS,
default: null,
});
// testing if we can import data in server side, but since it's next js we need firebase calls in client side
// import { conversationService } from "@nirvana/common/services";
import { earlierThisMonth } from "@nirvana/common/helpers/dateTime";
// const getTest = (async () => await conversationService.test())();
// export const test = atom({
// key: RecoilActions.TEST, // unique ID (with respect to other atoms/selectors)
// default: getTest, // default value (aka initial value)
// });
// approach: fill in all "bottom" level atoms, and then build the tree later with selectors
// convo id -> userMember object
export const allUsersConversationsAtom = atom<Map<string, ConversationMember>>({
key: RecoilActions.ALL_USERS_CONVERSATION_RELATIONSHIPS,
default: new Map<string, ConversationMember>(),
});
export const userConvoAssociationSelector = selectorFamily({
key: RecoilActions.USER_CONVO_ASSOCIATION_SELECTOR,
get:
(convoId: string) =>
({ get }) => {
const userConvoMap = get(allUsersConversationsAtom);
const userConvoAssoc = userConvoMap.get(convoId);
return userConvoAssoc;
},
});
export const allRelevantConversationsAtom = atom({
key: RecoilActions.ALL_RELEVANT_CONVERSATIONS,
default: new Map<string, Conversation>(),
effects: [
// ({ onSet }) => {
// onSet((newMap) => {
// //go through and make sure that our relevant user's cache is up to date
// // const allUsersInConvos = newMap.values()
// });
// },
],
});
export const sortedRoomSelector = selector<Conversation[]>({
key: RecoilActions.SORTED_CONVERSATIONS,
get: ({ get }) => {
const relConvos = get(allRelevantConversationsAtom);
const convosArr: Conversation[] = Array.from(relConvos.values());
convosArr.sort((a, b) => {
if (!a.lastActivityDate) {
return -1;
}
if (!b.lastActivityDate) {
return 1;
}
if (a.lastActivityDate.toDate() < b.lastActivityDate.toDate()) {
return 1;
}
return -1;
});
return convosArr;
},
});
// selectors for convos: inbox/default, live, later, done, priority
// get all rooms with active members in the room
export const liveRoomsSelector = selector<Conversation[]>({
key: RecoilActions.LIVE_ROOMS,
get: ({ get }) => {
const sortedConvos = get(sortedRoomSelector);
const liveRooms = sortedConvos.filter(
(conv) => conv.membersInLiveRoom?.length > 0
);
return liveRooms;
},
});
export const laterConvosSelector = selector<Conversation[]>({
key: RecoilActions.LATER_CONVERSATIONS_SELECTOR,
get: ({ get }) => {
const sortedConvos = get(sortedRoomSelector);
const convoUserMap = get(allUsersConversationsAtom);
const laterConvos: Conversation[] = sortedConvos.filter((conv) => {
// get the userconvoAssoc and see the state
if (convoUserMap.get(conv.id)?.state == ConversationMemberState.later) {
return true;
}
return false;
});
return laterConvos;
},
});
export const priorityConvosSelector = selector<Conversation[]>({
key: RecoilActions.PRIORITY_CONVERSATIONS_SELECTOR,
get: ({ get }) => {
const sortedConvos = get(sortedRoomSelector);
const convoUserMap = get(allUsersConversationsAtom);
const priorityConvos: Conversation[] = sortedConvos.filter((conv) => {
// get the userconvoAssoc and see the state
if (
convoUserMap.get(conv.id)?.state == ConversationMemberState.priority
) {
return true;
}
return false;
});
// todo: sort by createdDate, not lastActivityDate
priorityConvos.sort((a: Conversation, b: Conversation) => {
if (a.createdDate.toDate() > b.createdDate.toDate()) {
return -1;
}
return 0;
});
return priorityConvos;
},
});
export const doneConvosSelector = selector<Conversation[]>({
key: RecoilActions.DONE_CONVERSATIONS_SELECTOR,
get: ({ get }) => {
const sortedConvos = get(sortedRoomSelector);
const convoUserMap = get(allUsersConversationsAtom);
const doneConvos: Conversation[] = sortedConvos.filter((conv) => {
if (!convoUserMap.has(conv.id)) {
return false;
}
// if convo lastActivityDate > lastInteractionDate
// move to default/inbox convos
const userConvoAssoc = convoUserMap.get(conv.id);
// get the userconvoAssoc and see the state
if (userConvoAssoc?.state == ConversationMemberState.done) {
if (
userConvoAssoc?.lastInteractionDate &&
conv.lastActivityDate > userConvoAssoc?.lastInteractionDate
) {
conversationService.updateUserConvoRelationship(
userConvoAssoc.id,
conv.id,
ConversationMemberState.default,
false
);
return false;
}
return true;
}
return false;
});
return doneConvos;
},
});
export const countDefaultConvosSelector = selector<number>({
key: RecoilActions.COUNT_DEFAULT_CONVOS_SELECTOR,
get: ({ get }) => {
const sortedConvos = get(sortedRoomSelector);
const userMemberMap = get(allUsersConversationsAtom);
const defaultConvos = sortedConvos.filter((convo) => {
return (
userMemberMap.get(convo.id)?.state == ConversationMemberState.default &&
!convo.membersInLiveRoom?.length
);
});
return defaultConvos.length;
},
});
export interface NavigationContentCount {
liveCount: number;
doneCount: number;
defaultCount: number;
laterCount: number;
drawerCount: number;
}
export const countNavigationContentSelector = selector<NavigationContentCount>({
key: RecoilActions.COUNT_NAVIGATION_ITEMS,
get: ({ get }) => {
const laterConvos = get(laterConvosSelector);
const doneConvos = get(doneConvosSelector);
const defaultCount = get(countDefaultConvosSelector);
const liveRooms = get(liveRoomsSelector);
const navCountObject: NavigationContentCount = {
liveCount: liveRooms.length,
defaultCount,
laterCount: laterConvos.length,
doneCount: doneConvos.length,
drawerCount: 0,
};
return navCountObject;
},
});
export enum RelativeTimeConvosSections {
today = "TODAY",
last7Days = "LAST 7 DAYS",
earlierThisMonth = "EARLIER THIS MONTH",
oldJunk = "OLD JUNK",
}
// map from the section title to the list of conversations, all sorted desc lastActivity date
export const RelativeTimeSeparatedConvosSelector = selector<
Map<RelativeTimeConvosSections, Conversation[]>
>({
key: RecoilActions.RELATIVE_TIME_SECTIONS_CONVOS,
get: ({ get }) => {
const sortedConvos = get(sortedRoomSelector);
// find the convos where the user convo membership state is "inbox" or default
const userMemberMap = get(allUsersConversationsAtom);
// inbox/default Convos: already sorted
const defaultConvos = sortedConvos.filter((convo) => {
return (
userMemberMap.get(convo.id)?.state == ConversationMemberState.default &&
!convo.membersInLiveRoom?.length
);
});
// go through these and organize in the different sections
const organizedMapRelConvos = new Map<
RelativeTimeConvosSections,
Conversation[]
>(new Map());
// putting in sections in map so we can just add later
organizedMapRelConvos.set(
RelativeTimeConvosSections.today,
[] as Conversation[]
);
organizedMapRelConvos.set(
RelativeTimeConvosSections.last7Days,
[] as Conversation[]
);
organizedMapRelConvos.set(
RelativeTimeConvosSections.earlierThisMonth,
[] as Conversation[]
);
organizedMapRelConvos.set(
RelativeTimeConvosSections.oldJunk,
[] as Conversation[]
);
defaultConvos.map((convo) => {
let sectionForConvo = RelativeTimeConvosSections.today;
// put it in the junk with this
if (!convo.lastActivityDate) {
sectionForConvo = RelativeTimeConvosSections.oldJunk;
} else if (convo.lastActivityDate.toDate() > yesterday) {
sectionForConvo = RelativeTimeConvosSections.today;
} else if (convo.lastActivityDate.toDate() > Days7Ago) {
sectionForConvo = RelativeTimeConvosSections.last7Days;
} else if (convo.lastActivityDate.toDate() > earlyMonthDate) {
sectionForConvo = RelativeTimeConvosSections.earlierThisMonth;
} else {
sectionForConvo = RelativeTimeConvosSections.oldJunk;
}
const currValInSection = organizedMapRelConvos.get(sectionForConvo);
// already set, but just making sure
if (currValInSection) {
organizedMapRelConvos.set(sectionForConvo, [
...currValInSection,
convo,
]);
}
});
return organizedMapRelConvos;
},
});
export const checkIncomingMessageSelector = selectorFamily({
key: RecoilActions.CHECK_INCOMING_MESSAGE,
get:
(convoId: string) =>
({ get }) => {
const currConvosMap: Map<string, Conversation> = get(
allRelevantConversationsAtom
);
const currUserConvoMembersMap = get(allUsersConversationsAtom);
// true if our last interaction date is < lastActivityDate
const currConvo = currConvosMap.get(convoId);
const currUserConvoAssoc = currUserConvoMembersMap.get(convoId);
if (!currConvo || !currUserConvoAssoc) {
return false;
}
if (!currUserConvoAssoc?.lastInteractionDate) {
return true;
}
if (!currConvo?.lastActivityDate) {
return false;
}
if (
currConvo.lastActivityDate.toDate() >
currUserConvoAssoc.lastInteractionDate?.toDate()
) {
return true;
}
return false;
// todo: if it's incoming and it's in done/archive, then move it into the inbox by updating userConvoMember relationship
},
});
// map cache of all relevant users
// get all of the users in all of the conversations, once through a simple service call
export const allRelevantContactsAtom = atom<Map<string, NirvanaUser.default>>({
key: RecoilActions.ALL_RELEVANT_CONTACTS,
default: new Map<string, NirvanaUser.default>(),
});
// todo: useful selector where a component can pass in a list of users
// and return their full information...either get from cache if there or fetch from database if not and update cache
// RECORDING, SHORTCUTS, PLAYING, MIC, HEADPHONES, etc.
export const isRecordingAtom = atom<boolean>({
key: RecoilActions.IS_RECORDING_ATOM,
default: false,
});
export const selectedConvoAtom = atom<string | null>({
// convoId
key: RecoilActions.SELECTED_CONVERSATION_ATOM,
default: null,
});
export const hasMicPermissionsAtom = atom<boolean>({
key: RecoilActions.HAS_MIC_PERMISSIONS,
default: false,
});
export const audioQueueAtom = atom<AudioClip[]>({
key: RecoilActions.AUDIO_QUEUE_ATOM,
default: [] as AudioClip[],
});
export const audioQueueCurrentClipSelector = selector<AudioClip | null>({
key: RecoilActions.AUDIO_QUEUE_CURRENT_ITEM_SELECTOR,
get: ({ get }) => {
const currentQueue = get(audioQueueAtom);
if (currentQueue[0]) {
return currentQueue[0];
}
return null;
},
});