cleaning, prolly broke a lot but whatever
This commit is contained in:
@@ -5,15 +5,15 @@ export default class Conversation {
|
|||||||
id: string = uuid();
|
id: string = uuid();
|
||||||
|
|
||||||
type: ConversationType;
|
type: ConversationType;
|
||||||
name: string; // engineering, general, arjun and jacob...
|
name?: string; // engineering, general, arjun, jacob and rachel...
|
||||||
|
|
||||||
createdDate: Timestamp = Timestamp.now();
|
createdDate: Timestamp = Timestamp.now();
|
||||||
createdByUserId: string;
|
createdByUserId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
_name: string,
|
|
||||||
_createdByUserId: string,
|
_createdByUserId: string,
|
||||||
_type: ConversationType
|
_type: ConversationType,
|
||||||
|
_name?: string
|
||||||
) {
|
) {
|
||||||
this.name = _name;
|
this.name = _name;
|
||||||
this.createdByUserId = _createdByUserId;
|
this.createdByUserId = _createdByUserId;
|
||||||
@@ -22,6 +22,6 @@ export default class Conversation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ConversationType {
|
export enum ConversationType {
|
||||||
personal = "personal",
|
personal = "personal", // no conversation name then
|
||||||
group = "group",
|
group = "group", // must have conversation name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
// I suggest you add those two rules:
|
// I suggest you add those two rules:
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
"prefer-const": "error",
|
"prefer-const": "error",
|
||||||
"react-hooks/rules-of-hooks": "error",
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import moment from "moment";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useState } from "react";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { FaCheck, FaPlay } from "react-icons/fa";
|
|
||||||
import { useAuth } from "../contexts/authContext";
|
|
||||||
import { useKeyboardContext } from "../contexts/keyboardContext";
|
|
||||||
import { useTeamDashboardContext } from "../contexts/teamDashboardContext";
|
|
||||||
import Announcement, { AnnouncementState } from "../models/announcement";
|
|
||||||
import { User } from "../models/user";
|
|
||||||
import { AnnouncementService } from "../services/announcementService";
|
|
||||||
import SkeletonLoader from "./Loading/skeletonLoader";
|
|
||||||
|
|
||||||
interface IAnnouncementCardProps {
|
|
||||||
announcement: Announcement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const announcementService = new AnnouncementService();
|
|
||||||
|
|
||||||
export default function AnnouncementCard(props: IAnnouncementCardProps) {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const { teamUsersMap, user } = useTeamDashboardContext();
|
|
||||||
const { handleAddAudioToQueue } = useKeyboardContext();
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
function playAnnouncement() {
|
|
||||||
handleAddAudioToQueue([props.announcement.audioDataUrl], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveAnnouncement() {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await announcementService.updateAnnouncementState(
|
|
||||||
props.announcement.id,
|
|
||||||
AnnouncementState.resolved
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("unable to resolve announcement");
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteAnnouncement() {
|
|
||||||
try {
|
|
||||||
await announcementService.updateAnnouncementState(
|
|
||||||
props.announcement.id,
|
|
||||||
AnnouncementState.deleted
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("unable to resolve announcement");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var announcementUser: User = null;
|
|
||||||
if (props.announcement.createdByUserId in teamUsersMap) {
|
|
||||||
announcementUser = teamUsersMap[props.announcement.createdByUserId];
|
|
||||||
} else if (currUser.uid == props.announcement.createdByUserId) {
|
|
||||||
announcementUser = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relativeCreatedDate = moment(
|
|
||||||
props.announcement.createdDate.toDate()
|
|
||||||
).fromNow();
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <SkeletonLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="flex flex-row p-3 bg-gray-300 bg-opacity-25 rounded-lg items-center shrink-0">
|
|
||||||
<span className="relative mr-2 grid items-center justify-items-center">
|
|
||||||
<span className="bg-gray-200 bg-opacity-20 rounded-full shadow-md absolute w-full h-full"></span>
|
|
||||||
|
|
||||||
<Image
|
|
||||||
src={"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-22.svg"}
|
|
||||||
alt="profile"
|
|
||||||
width={50}
|
|
||||||
height={50}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col items-baseline mr-5">
|
|
||||||
<span className="text-md font-bold text-white">
|
|
||||||
{announcementUser?.nickName}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-200">{relativeCreatedDate}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{props.announcement.state == AnnouncementState.active && (
|
|
||||||
<button
|
|
||||||
onClick={resolveAnnouncement}
|
|
||||||
className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40"
|
|
||||||
>
|
|
||||||
<FaCheck className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={playAnnouncement}
|
|
||||||
className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40"
|
|
||||||
>
|
|
||||||
<FaPlay className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import { FaAngleDown, FaCheck, FaMicrophoneAlt, FaPlay } from "react-icons/fa";
|
|
||||||
import { UserStatus } from "../../models/user";
|
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
collection,
|
|
||||||
getFirestore,
|
|
||||||
onSnapshot,
|
|
||||||
query,
|
|
||||||
where,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import { Collections } from "../../services/collections";
|
|
||||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
|
||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
import Announcement, { AnnouncementState } from "../../models/announcement";
|
|
||||||
import { getTime } from "../../helpers/dateTime";
|
|
||||||
import AnnouncementCard from "../AnnouncementCard";
|
|
||||||
import { Dropdown, Menu, Radio, Tooltip } from "antd";
|
|
||||||
import { useKeyboardContext } from "../../contexts/keyboardContext";
|
|
||||||
|
|
||||||
// for query
|
|
||||||
const yesterday = new Date();
|
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
|
||||||
|
|
||||||
const db = getFirestore();
|
|
||||||
|
|
||||||
export default function Announcements() {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const { team } = useTeamDashboardContext();
|
|
||||||
const { isRecordingAnnouncement } = useKeyboardContext();
|
|
||||||
|
|
||||||
const [annMap, setAnnMap] = useState<Map<string, Announcement>>(
|
|
||||||
new Map<string, Announcement>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// SECTION: LISTENER for Announcements
|
|
||||||
useEffect(() => {
|
|
||||||
/**
|
|
||||||
* QUERY:
|
|
||||||
* all announcements for the team in the past 24 hours for now
|
|
||||||
* order createdDate desc
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const q = query(
|
|
||||||
collection(db, Collections.announcements),
|
|
||||||
where("teamId", "==", team.id),
|
|
||||||
where("createdDate", ">", yesterday)
|
|
||||||
);
|
|
||||||
|
|
||||||
// return unsubscribe
|
|
||||||
return onSnapshot(q, (snapshot) => {
|
|
||||||
snapshot.docChanges().forEach((change) => {
|
|
||||||
let updatedOrNewAnn = change.doc.data() as Announcement;
|
|
||||||
updatedOrNewAnn.id = change.doc.id;
|
|
||||||
|
|
||||||
if (change.type === "added" || change.type === "modified") {
|
|
||||||
// update rooms map
|
|
||||||
setAnnMap((prevMap) => {
|
|
||||||
return new Map(prevMap.set(updatedOrNewAnn.id, updatedOrNewAnn));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (change.type === "removed") {
|
|
||||||
// going to more so be resolved or deleted
|
|
||||||
console.log("Removed annoncement: ", updatedOrNewAnn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const allAnn = Array.from(annMap.values());
|
|
||||||
// sort the announcements
|
|
||||||
allAnn.sort(function (a, b) {
|
|
||||||
return getTime(b.createdDate.toDate()) - getTime(a.createdDate.toDate());
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeAnn = allAnn.filter(
|
|
||||||
(ann) => ann.state == AnnouncementState.active
|
|
||||||
);
|
|
||||||
const resolvedAnn = allAnn.filter(
|
|
||||||
(ann) => ann.state == AnnouncementState.resolved
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedTabPane, setSelectedTabPane] = useState<string>(
|
|
||||||
AnnouncementState.active
|
|
||||||
);
|
|
||||||
|
|
||||||
function getTabContent() {
|
|
||||||
switch (selectedTabPane) {
|
|
||||||
case AnnouncementState.active:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{activeAnn.map((ann) => {
|
|
||||||
return <AnnouncementCard key={ann.id} announcement={ann} />;
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case AnnouncementState.resolved:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{resolvedAnn.map((ann) => {
|
|
||||||
return <AnnouncementCard key={ann.id} announcement={ann} />;
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimePeriodFilterMenu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key={1} disabled>
|
|
||||||
Past Week (Coming Soon)
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key={2} disabled>
|
|
||||||
This Month (Coming Soon)
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md">
|
|
||||||
<span className="flex flex-row justify-start items-center pb-5">
|
|
||||||
<span className="flex flex-col mr-auto">
|
|
||||||
<span className="text-white mr-auto">
|
|
||||||
ANNOUNCEMENTS
|
|
||||||
<Tooltip title={"Press and hold A to send an announcement."}>
|
|
||||||
<button
|
|
||||||
className={`right-1 rounded-lg py-1 px-2 ml-1
|
|
||||||
shadow-md text-center text-white text-sm font-bold ${
|
|
||||||
isRecordingAnnouncement
|
|
||||||
? "bg-orange-500 text-white"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
A
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
<span className="text-gray-300 text-xs">
|
|
||||||
updates, pep talks, blockers, reminders
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* tab pane */}
|
|
||||||
|
|
||||||
<Radio.Group
|
|
||||||
value={selectedTabPane}
|
|
||||||
onChange={(e) => setSelectedTabPane(e.target.value)}
|
|
||||||
>
|
|
||||||
<Radio.Button value={AnnouncementState.active}>
|
|
||||||
{AnnouncementState.active}{" "}
|
|
||||||
<span className="text-xs text-orange-500">
|
|
||||||
{activeAnn.length > 0 ? activeAnn.length : ""}
|
|
||||||
</span>
|
|
||||||
</Radio.Button>
|
|
||||||
<Radio.Button value={AnnouncementState.resolved}>
|
|
||||||
{AnnouncementState.resolved}{" "}
|
|
||||||
<span className="text-xs text-orange-500">
|
|
||||||
{activeAnn.length > 0 ? activeAnn.length : ""}
|
|
||||||
</span>
|
|
||||||
</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
|
|
||||||
<Dropdown overlay={TimePeriodFilterMenu}>
|
|
||||||
<span className="ml-2 text-sm text-gray-300 flex flex-row items-center uppercase hover:cursor-pointer">
|
|
||||||
24 HRS <FaAngleDown />
|
|
||||||
</span>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
<Tooltip title={"Press and hold A to record an announcement"}>
|
|
||||||
<button className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
|
|
||||||
<FaMicrophoneAlt className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
{/* <button className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
|
|
||||||
<FaPlay className="text-lg text-white" />
|
|
||||||
</button> */}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="flex flex-row overflow-auto whitespace-nowrap space-x-5 items-center pb-5">
|
|
||||||
{getTabContent()}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
import Image from "next/image";
|
|
||||||
import {
|
|
||||||
FaMicrophoneAlt,
|
|
||||||
FaHeadphonesAlt,
|
|
||||||
FaTh,
|
|
||||||
FaBell,
|
|
||||||
FaSearch,
|
|
||||||
FaBuilding,
|
|
||||||
FaCalendarDay,
|
|
||||||
FaClock,
|
|
||||||
FaPeopleCarry,
|
|
||||||
FaAngleDown,
|
|
||||||
FaCheck,
|
|
||||||
FaDatabase,
|
|
||||||
FaMicrophoneAltSlash,
|
|
||||||
FaVolumeMute,
|
|
||||||
FaLayerGroup,
|
|
||||||
FaUser,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
|
||||||
import router from "next/router";
|
|
||||||
import Moment from "react-moment";
|
|
||||||
import { User } from "../../models/user";
|
|
||||||
import { generateGreetings } from "../../helpers/dateTime";
|
|
||||||
import { Dropdown, Menu, Tooltip } from "antd";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { TeamMemberRole, TeamMemberStatus } from "../../models/teamMember";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import SubMenu from "antd/lib/menu/SubMenu";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import TeamService from "../../services/teamService";
|
|
||||||
import { Team } from "../../models/team";
|
|
||||||
import Loading from "../Loading";
|
|
||||||
import { useKeyboardContext } from "../../contexts/keyboardContext";
|
|
||||||
import UserStatusBubble from "../UserStatusBubble";
|
|
||||||
|
|
||||||
const teamService = new TeamService();
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const { currUser, logOut } = useAuth();
|
|
||||||
const { team, user, userTeamMember } = useTeamDashboardContext();
|
|
||||||
const router = useRouter();
|
|
||||||
const { teamid } = router.query;
|
|
||||||
const {
|
|
||||||
hasRecPermit,
|
|
||||||
|
|
||||||
audioInputDeviceId,
|
|
||||||
audioOutputDeviceId,
|
|
||||||
selectAudioOutput,
|
|
||||||
selectAudioInput,
|
|
||||||
inputDevices,
|
|
||||||
outputDevices,
|
|
||||||
|
|
||||||
isMuted,
|
|
||||||
isSilenceMode,
|
|
||||||
muteOrUnmute,
|
|
||||||
silenceOrLivenMode,
|
|
||||||
} = useKeyboardContext();
|
|
||||||
|
|
||||||
const [usersTeams, setUserTeams] = useState<Team[]>(null);
|
|
||||||
|
|
||||||
// get team data for the team dropdown
|
|
||||||
useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
try {
|
|
||||||
// get all the teams that this user is part of
|
|
||||||
const newTeams: Team[] =
|
|
||||||
await teamService.getActiveOrInvitedTeamsbyUser(
|
|
||||||
currUser.uid,
|
|
||||||
currUser.email
|
|
||||||
);
|
|
||||||
|
|
||||||
setUserTeams(newTeams);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("problem getting teams of this user");
|
|
||||||
toast.error("problem on load");
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleSignOut() {
|
|
||||||
console.log("clicked log out button");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await logOut();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
toast.error("Unable to log out!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAdminRoute() {
|
|
||||||
if (userTeamMember.role == TeamMemberRole.admin) {
|
|
||||||
router.push("/teams/" + teamid + "/admin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error("You are not a team admin!");
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchBar = (
|
|
||||||
<div className="pt-2 flex flex-row relative items-center">
|
|
||||||
<button type="submit" className="absolute left-0 top-0 mt-5 ml-5">
|
|
||||||
<FaSearch className="text-white" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className=" bg-white bg-opacity-40 h-10 px-5 pl-10 pr-16 rounded-lg text-white text-sm focus:outline-none"
|
|
||||||
type="search"
|
|
||||||
name="search"
|
|
||||||
placeholder="Search"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="absolute right-1 rounded-lg py-1 px-2 ml-1
|
|
||||||
shadow-md text-center text-white text-sm font-bold"
|
|
||||||
>
|
|
||||||
CTRL + K
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const TeamsMenu = (
|
|
||||||
<Menu key={2} title="teams">
|
|
||||||
<Menu.Item
|
|
||||||
onClick={() => window.open("/teams", "_self")}
|
|
||||||
key={"team hub"}
|
|
||||||
icon={<FaLayerGroup />}
|
|
||||||
>
|
|
||||||
<button>Team Hub</button>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item onClick={handleAdminRoute} key={"admin"} icon={<FaDatabase />}>
|
|
||||||
<button>Admin</button>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Divider />
|
|
||||||
{/* all current teams that this person is a part of */}
|
|
||||||
{usersTeams &&
|
|
||||||
usersTeams.map((uteam, i) => {
|
|
||||||
return (
|
|
||||||
<Menu.Item
|
|
||||||
key={i + 5}
|
|
||||||
icon={team.id == uteam.id ? <FaCheck /> : <></>}
|
|
||||||
onClick={() => {
|
|
||||||
console.log("going to " + uteam.id);
|
|
||||||
window.location.href = "/teams/" + uteam.id;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button>{uteam.name}</button>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
const UserMenu = (
|
|
||||||
<Menu title="user menu">
|
|
||||||
<Menu.Item
|
|
||||||
key={2}
|
|
||||||
onClick={() => router.push("/teams/profile")}
|
|
||||||
icon={<FaUser />}
|
|
||||||
>
|
|
||||||
<button>Profile</button>
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Item
|
|
||||||
onClick={() => window.open("/teams", "_self")}
|
|
||||||
key={"team hub"}
|
|
||||||
icon={<FaLayerGroup />}
|
|
||||||
>
|
|
||||||
<button>Team Hub</button>
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Divider />
|
|
||||||
|
|
||||||
<Menu.Item danger key={3} onClick={handleSignOut}>
|
|
||||||
<button>Sign Out</button>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSilenceOrLivenMode = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
silenceOrLivenMode();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMuteUnmute = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
muteOrUnmute();
|
|
||||||
};
|
|
||||||
|
|
||||||
const audioInputDropMenu = (
|
|
||||||
<Menu title="audio input">
|
|
||||||
<Menu.Item danger key={"muting input"} onClick={handleMuteUnmute}>
|
|
||||||
<button>{isMuted ? "Unmute" : "Mute"}</button>
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Divider />
|
|
||||||
|
|
||||||
{/* device */}
|
|
||||||
{inputDevices.map((device, i) => {
|
|
||||||
return (
|
|
||||||
<Menu.Item
|
|
||||||
key={device.deviceId}
|
|
||||||
icon={device.deviceId == audioInputDeviceId ? <FaCheck /> : <></>}
|
|
||||||
onClick={() => selectAudioInput(device.deviceId)}
|
|
||||||
>
|
|
||||||
<button>{device.label}</button>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
const audioOutputDropMenu = (
|
|
||||||
<Menu title="audio ouput">
|
|
||||||
<Menu.Item
|
|
||||||
danger
|
|
||||||
key={"silence output"}
|
|
||||||
onClick={handleSilenceOrLivenMode}
|
|
||||||
>
|
|
||||||
<button>{isSilenceMode ? "Unsilence" : "Silence"}</button>
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Divider />
|
|
||||||
|
|
||||||
{/* devices */}
|
|
||||||
{outputDevices.map((device, i) => {
|
|
||||||
return (
|
|
||||||
<Menu.Item
|
|
||||||
key={device.deviceId}
|
|
||||||
icon={device.deviceId == audioOutputDeviceId ? <FaCheck /> : <></>}
|
|
||||||
onClick={() => selectAudioOutput(device.deviceId)}
|
|
||||||
>
|
|
||||||
<button>{device.label}</button>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
function renderAudioInput() {
|
|
||||||
if (!hasRecPermit) {
|
|
||||||
return (
|
|
||||||
<Tooltip title="enable mic permissions">
|
|
||||||
<span>
|
|
||||||
<FaMicrophoneAltSlash className="text-xl text-orange-400 ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMuted) {
|
|
||||||
return (
|
|
||||||
<span onClick={handleMuteUnmute}>
|
|
||||||
<FaMicrophoneAltSlash className="text-xl text-orange-400 ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown overlay={audioInputDropMenu}>
|
|
||||||
<span onClick={handleMuteUnmute}>
|
|
||||||
<FaMicrophoneAlt className="text-xl text-gray-200 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
|
||||||
</span>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAudioOutput() {
|
|
||||||
if (isSilenceMode) {
|
|
||||||
return (
|
|
||||||
<span onClick={handleSilenceOrLivenMode}>
|
|
||||||
<FaVolumeMute className="text-xl text-orange-400 ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown overlay={audioOutputDropMenu}>
|
|
||||||
<span onClick={handleSilenceOrLivenMode}>
|
|
||||||
<FaHeadphonesAlt className="text-xl text-gray-200 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
|
||||||
</span>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const periodOfDay = generateGreetings();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="flex-1 flex flex-row items-center justify-between py-5">
|
|
||||||
{/* welcome message */}
|
|
||||||
<span className="flex flex-col items-baseline">
|
|
||||||
<span className="font-bold text-xl text-white capitalize ">
|
|
||||||
👋Hey {user.firstName}, {periodOfDay}!
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Date and time */}
|
|
||||||
<span className="flex flex-row space-x-2">
|
|
||||||
<span className="text-sky-700 bg-sky-200 p-1 rounded-md text-xs font-bold mt-2 flex flex-row items-center space-x-1">
|
|
||||||
<FaCalendarDay />
|
|
||||||
<Moment date={today} format="ddd, MMM DD" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-emerald-700 bg-emerald-200 p-1 rounded-md text-xs font-bold mt-2 flex flex-row items-center space-x-1">
|
|
||||||
<FaClock />
|
|
||||||
<Moment format="h:mm a" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* header control section */}
|
|
||||||
<span className="flex-row items-center space-x-5 hidden md:flex ml-auto">
|
|
||||||
{/* search bar */}
|
|
||||||
|
|
||||||
<FaBell className=" shrink-0 text-lg text-gray-200 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
|
||||||
|
|
||||||
{renderAudioOutput()}
|
|
||||||
|
|
||||||
{renderAudioInput()}
|
|
||||||
|
|
||||||
{/* teams menu */}
|
|
||||||
|
|
||||||
<Dropdown overlay={TeamsMenu} trigger={["click"]}>
|
|
||||||
<button
|
|
||||||
className="text-gray-200 flex flex-row items-center"
|
|
||||||
onClick={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{team.name} <FaAngleDown />
|
|
||||||
</button>
|
|
||||||
</Dropdown>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* avatar */}
|
|
||||||
<Dropdown
|
|
||||||
overlay={UserMenu}
|
|
||||||
trigger={["click"]}
|
|
||||||
className="ml-2 shrink-0"
|
|
||||||
>
|
|
||||||
<span className="relative flex hover:cursor-pointer">
|
|
||||||
<span className="bg-gray-200 bg-opacity-30 rounded-full shadow-md absolute w-full h-full"></span>
|
|
||||||
<UserStatusBubble status={user.userStatus} />
|
|
||||||
<img src={user.avatarUrl} alt="asdf" className="rounded-full w-12" />
|
|
||||||
</span>
|
|
||||||
</Dropdown>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import { BsThreeDots } from "react-icons/bs";
|
|
||||||
import { FaAngleDown, FaCode, FaFileImage, FaPlus } from "react-icons/fa";
|
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { Dropdown, Menu, Radio, Tooltip } from "antd";
|
|
||||||
import {
|
|
||||||
ShowModalType,
|
|
||||||
useKeyboardContext,
|
|
||||||
} from "../../contexts/keyboardContext";
|
|
||||||
import CreateOrUpdateLink from "../Modals/CreateOrUpdateLink";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import LinkCard from "../LinkCard";
|
|
||||||
import { Collections } from "../../services/collections";
|
|
||||||
import {
|
|
||||||
collection,
|
|
||||||
getFirestore,
|
|
||||||
onSnapshot,
|
|
||||||
orderBy,
|
|
||||||
query,
|
|
||||||
where,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
|
||||||
import Link, { LinkState } from "../../models/link";
|
|
||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
|
|
||||||
const lastWeek = new Date();
|
|
||||||
lastWeek.setDate(lastWeek.getDate() - 7);
|
|
||||||
|
|
||||||
const db = getFirestore();
|
|
||||||
|
|
||||||
export default function Links() {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const { handleModalType } = useKeyboardContext();
|
|
||||||
|
|
||||||
const { team } = useTeamDashboardContext();
|
|
||||||
|
|
||||||
const [linksMap, setLinksMap] = useState<Map<string, Link>>(
|
|
||||||
new Map<string, Link>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// SECTION: LISTENER for LINKS
|
|
||||||
useEffect(() => {
|
|
||||||
/**
|
|
||||||
* QUERY:
|
|
||||||
* all links for the team in the past 7 days
|
|
||||||
* order createdDate desc
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const q = query(
|
|
||||||
collection(db, Collections.links),
|
|
||||||
where("teamId", "==", team.id),
|
|
||||||
where("createdDate", ">", lastWeek),
|
|
||||||
orderBy("createdDate", "desc")
|
|
||||||
);
|
|
||||||
|
|
||||||
// return unsubscribe
|
|
||||||
return onSnapshot(q, (snapshot) => {
|
|
||||||
snapshot.docChanges().forEach((change) => {
|
|
||||||
let updatedOrNewLink = change.doc.data() as Link;
|
|
||||||
updatedOrNewLink.id = change.doc.id;
|
|
||||||
|
|
||||||
if (change.type === "added" || change.type === "modified") {
|
|
||||||
setLinksMap((prevMap) => {
|
|
||||||
return new Map(prevMap.set(updatedOrNewLink.id, updatedOrNewLink));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (change.type === "removed") {
|
|
||||||
console.log("Removed link: ", updatedOrNewLink);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const TimePeriodFilterMenu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key={1} disabled>
|
|
||||||
Past 24 Hours (Coming Soon)
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key={2} disabled>
|
|
||||||
This Month (Coming Soon)
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedTabPane, setSelectedTabPane] = useState<string>(
|
|
||||||
LinkTypeFilter.me
|
|
||||||
);
|
|
||||||
|
|
||||||
const allLinks = Array.from(linksMap.values());
|
|
||||||
|
|
||||||
const meLinks = allLinks.filter((link) => {
|
|
||||||
// if archived or deleted, don't show
|
|
||||||
if (link.state != LinkState.active) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if I am the sender, then also show, but only if it's me sending to a specific person
|
|
||||||
if (link.createdByUserId == currUser.uid && link.recipients?.length > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if I am a receiver
|
|
||||||
if (link.recipients?.includes(currUser.uid)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const teamLinks = allLinks.filter((link) => {
|
|
||||||
// not archived and does not have a recipients list
|
|
||||||
if (link.state == LinkState.active && !link.recipients) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const archivedLinks = allLinks.filter(
|
|
||||||
(link) => link.state == LinkState.archived
|
|
||||||
);
|
|
||||||
|
|
||||||
function getFilteredContent() {
|
|
||||||
switch (selectedTabPane) {
|
|
||||||
case LinkTypeFilter.me:
|
|
||||||
return meLinks;
|
|
||||||
case LinkTypeFilter.team:
|
|
||||||
return teamLinks;
|
|
||||||
case LinkTypeFilter.archived:
|
|
||||||
return archivedLinks;
|
|
||||||
default:
|
|
||||||
return allLinks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md">
|
|
||||||
{/* header row */}
|
|
||||||
<Tooltip title={"archive unneeded links, keep yourself focused on today"}>
|
|
||||||
{/* modal */}
|
|
||||||
<CreateOrUpdateLink />
|
|
||||||
|
|
||||||
<span className="flex flex-row justify-start pb-5 items-center">
|
|
||||||
<span className="flex flex-col mr-20">
|
|
||||||
<span className="text-white mr-auto">
|
|
||||||
DRAWER
|
|
||||||
<button
|
|
||||||
onClick={() => handleModalType(ShowModalType.createLink)}
|
|
||||||
className="right-1 rounded-lg py-1 px-2 ml-1
|
|
||||||
shadow-md text-center text-white text-sm font-bold"
|
|
||||||
>
|
|
||||||
CTRL + V
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-gray-300 text-xs">
|
|
||||||
links: jira tickets, drive files/folders, powerpoints...
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{meLinks.length > 6 || teamLinks.length > 6 ? (
|
|
||||||
<span className="text-gray-300 text-xs">
|
|
||||||
Make sure to{" "}
|
|
||||||
<span className="text-orange-500">
|
|
||||||
{"clear out your drawer and your team's"}
|
|
||||||
</span>{" "}
|
|
||||||
so that you are focused on this week.
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="ml-auto">
|
|
||||||
<Radio.Group
|
|
||||||
value={selectedTabPane}
|
|
||||||
onChange={(e) => setSelectedTabPane(e.target.value)}
|
|
||||||
>
|
|
||||||
<Radio.Button value={LinkTypeFilter.me}>
|
|
||||||
{LinkTypeFilter.me}{" "}
|
|
||||||
<span className="text-xs text-orange-500">
|
|
||||||
{meLinks.length > 0 ? meLinks.length : ""}
|
|
||||||
</span>
|
|
||||||
</Radio.Button>
|
|
||||||
<Radio.Button value={LinkTypeFilter.team}>
|
|
||||||
{LinkTypeFilter.team}{" "}
|
|
||||||
<span className="text-xs text-orange-500">
|
|
||||||
{teamLinks.length}
|
|
||||||
</span>
|
|
||||||
</Radio.Button>
|
|
||||||
<Tooltip title={"coming soon"}>
|
|
||||||
<Radio.Button disabled value={LinkTypeFilter.favorites}>
|
|
||||||
{LinkTypeFilter.favorites}{" "}
|
|
||||||
{/* <span className="text-xs text-orange-500">
|
|
||||||
{liveRooms.length > 0 ? fa.length : ""}
|
|
||||||
</span> */}
|
|
||||||
</Radio.Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Radio.Button value={LinkTypeFilter.archived}>
|
|
||||||
{LinkTypeFilter.archived}{" "}
|
|
||||||
<span className="text-xs text-orange-500">
|
|
||||||
{archivedLinks.length > 0 ? archivedLinks.length : ""}
|
|
||||||
</span>
|
|
||||||
</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dropdown overlay={TimePeriodFilterMenu}>
|
|
||||||
<span className="ml-2 text-sm text-gray-300 flex flex-row items-center uppercase hover:cursor-pointer">
|
|
||||||
Week <FaAngleDown />
|
|
||||||
</span>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
"screenshots: paste screenshot into the paste.pics website and bring back the link here and just paste it"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => window.open("https://paste.pics/", "_blank")}
|
|
||||||
className="bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40 ml-2"
|
|
||||||
>
|
|
||||||
<FaFileImage className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
"code blocks: copy the link and paste it in here when you are done"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => window.open("https://www.codepile.net/", "_blank")}
|
|
||||||
className="bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40 ml-2"
|
|
||||||
>
|
|
||||||
<FaCode className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => handleModalType(ShowModalType.createLink)}
|
|
||||||
className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40"
|
|
||||||
>
|
|
||||||
<FaPlus className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{/* table of links */}
|
|
||||||
<div className="flex flex-row space-x-2 overflow-x-auto py-2">
|
|
||||||
{getFilteredContent().map((link) => (
|
|
||||||
<LinkCard key={link.id} link={link} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum LinkTypeFilter {
|
|
||||||
team = "team",
|
|
||||||
me = "me",
|
|
||||||
favorites = "favorites",
|
|
||||||
archived = "archived",
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
import {
|
|
||||||
ClientConfig,
|
|
||||||
IAgoraRTC,
|
|
||||||
IAgoraRTCClient,
|
|
||||||
IMicrophoneAudioTrack,
|
|
||||||
} from "agora-rtc-sdk-ng";
|
|
||||||
import { Tooltip } from "antd";
|
|
||||||
import {
|
|
||||||
collection,
|
|
||||||
getFirestore,
|
|
||||||
onSnapshot,
|
|
||||||
query,
|
|
||||||
where,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
|
||||||
import OfficeRoom from "../../models/officeRoom";
|
|
||||||
import { appId } from "../../services/agoraService";
|
|
||||||
import { Collections } from "../../services/collections";
|
|
||||||
import OfficeRoomService from "../../services/officeRoomService";
|
|
||||||
import OfficeCard from "../OfficeCard";
|
|
||||||
|
|
||||||
const db = getFirestore();
|
|
||||||
|
|
||||||
const officeRoomService = new OfficeRoomService();
|
|
||||||
|
|
||||||
const config: ClientConfig = {
|
|
||||||
mode: "rtc",
|
|
||||||
codec: "vp8",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Office() {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const { team } = useTeamDashboardContext();
|
|
||||||
const [agoraRtc, setAgoraRtc] = useState<IAgoraRTC>(null);
|
|
||||||
const [agoraRtcClient, setAgoraRtcClient] = useState<IAgoraRTCClient>(null);
|
|
||||||
const [localAudioTrack, setLocalAudioTrack] =
|
|
||||||
useState<IMicrophoneAudioTrack>(null);
|
|
||||||
|
|
||||||
const [officeRoomsMap, setOfficeRoomsMap] = useState<Map<string, OfficeRoom>>(
|
|
||||||
new Map<string, OfficeRoom>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// get all offices for this team : realtime
|
|
||||||
useEffect(() => {
|
|
||||||
/**
|
|
||||||
* QUERY:
|
|
||||||
* all offices for this team
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const q = query(
|
|
||||||
collection(db, Collections.officeRooms),
|
|
||||||
where("teamId", "==", team.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
// return unsubscribe
|
|
||||||
return onSnapshot(q, (snapshot) => {
|
|
||||||
if (snapshot.empty) {
|
|
||||||
// seed data and create the initial rooms
|
|
||||||
console.log("seeding office rooms for this team");
|
|
||||||
officeRoomService.createInitialOfficeRooms(currUser.uid, team.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot.docChanges().forEach((change) => {
|
|
||||||
let updatedOfficeRoom = change.doc.data() as OfficeRoom;
|
|
||||||
updatedOfficeRoom.id = change.doc.id;
|
|
||||||
|
|
||||||
if (change.type === "added" || change.type === "modified") {
|
|
||||||
// update office rooms map on change of the room
|
|
||||||
setOfficeRoomsMap((prevMap) => {
|
|
||||||
return new Map(
|
|
||||||
prevMap.set(updatedOfficeRoom.id, updatedOfficeRoom)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (change.type === "removed") {
|
|
||||||
console.log("Removed office room: ", updatedOfficeRoom);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// set up agora stuff
|
|
||||||
useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
// dynamic import as the server side import doesn't work
|
|
||||||
const AgoraRTC = (await import("agora-rtc-sdk-ng")).default;
|
|
||||||
const agoraClient = AgoraRTC.createClient(config);
|
|
||||||
|
|
||||||
setAgoraRtcClient(agoraClient);
|
|
||||||
setAgoraRtc(AgoraRTC);
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleJoinChannel(channelName: string, agoraToken: string) {
|
|
||||||
const localTrack: IMicrophoneAudioTrack =
|
|
||||||
await agoraRtc.createMicrophoneAudioTrack();
|
|
||||||
|
|
||||||
setLocalAudioTrack(localTrack);
|
|
||||||
|
|
||||||
let init = async (chanName: string) => {
|
|
||||||
agoraRtcClient.on("user-published", async (user, mediaType) => {
|
|
||||||
await agoraRtcClient.subscribe(user, mediaType);
|
|
||||||
console.log("subscribe success");
|
|
||||||
|
|
||||||
if (mediaType === "audio") {
|
|
||||||
user.audioTrack?.play();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
agoraRtcClient.on("user-unpublished", async (user, type) => {
|
|
||||||
console.log("unpublished", user, type);
|
|
||||||
if (type === "audio") {
|
|
||||||
user.audioTrack?.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
await agoraRtcClient.unsubscribe(user);
|
|
||||||
});
|
|
||||||
|
|
||||||
agoraRtcClient.on("user-left", (user) => {
|
|
||||||
console.log("user left", user);
|
|
||||||
});
|
|
||||||
|
|
||||||
await agoraRtcClient.join(appId, chanName, agoraToken, null);
|
|
||||||
if (localTrack) await agoraRtcClient.publish(localTrack);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (localTrack) {
|
|
||||||
console.log("init ready");
|
|
||||||
init(channelName);
|
|
||||||
} else {
|
|
||||||
toast.error("Not ready for joining call");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLeaveChannel() {
|
|
||||||
// destroy local track
|
|
||||||
localAudioTrack?.close();
|
|
||||||
|
|
||||||
// leave all channels
|
|
||||||
await agoraRtcClient.leave();
|
|
||||||
}
|
|
||||||
|
|
||||||
const allOfficeRooms = Array.from(officeRoomsMap.values());
|
|
||||||
|
|
||||||
allOfficeRooms.sort((a, b) => {
|
|
||||||
if (a.name < b.name) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.name > b.name) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// if there are none, then show user button to create initial office rooms and then create them
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md w-96 shrink-0 flex-1 overflow-auto">
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
"Tell teammates to chat in the 'kitchen' or other places in the office to resolve matters on the spot."
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex flex-row justify-start items-center pb-5">
|
|
||||||
<span className="flex flex-col">
|
|
||||||
<span className="text-white uppercase">Office</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{/* all office rooms */}
|
|
||||||
<span className="flex flex-col overflow-auto pr-2 space-y-2">
|
|
||||||
{allOfficeRooms.map((officeRoom) => (
|
|
||||||
<OfficeCard
|
|
||||||
key={officeRoom.id}
|
|
||||||
officeRoom={officeRoom}
|
|
||||||
handleJoinChannel={handleJoinChannel}
|
|
||||||
handleLeaveChannel={handleLeaveChannel}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{/* <OfficeCard /> */}
|
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { FaAngleDown, FaBell, FaClock, FaPlus, FaLink } from "react-icons/fa";
|
|
||||||
import { IoTimer } from "react-icons/io5";
|
|
||||||
import { BsThreeDots } from "react-icons/bs";
|
|
||||||
import Image from "next/image";
|
|
||||||
import CreateOrUpdateRoom from "../Modals/CreateOrUpdateRoom";
|
|
||||||
import {
|
|
||||||
ShowModalType,
|
|
||||||
useKeyboardContext,
|
|
||||||
} from "../../contexts/keyboardContext";
|
|
||||||
import {
|
|
||||||
collection,
|
|
||||||
getFirestore,
|
|
||||||
onSnapshot,
|
|
||||||
orderBy,
|
|
||||||
query,
|
|
||||||
where,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import { Collections } from "../../services/collections";
|
|
||||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
|
||||||
import Room, { RoomStatus, RoomType } from "../../models/room";
|
|
||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
import { Divider, Dropdown, Menu, Radio, Tooltip } from "antd";
|
|
||||||
import RoomCard from "../RoomCard";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import RoomTypeTag from "../RoomTypeTag";
|
|
||||||
import moment from "moment";
|
|
||||||
import { getTime } from "../../helpers/dateTime";
|
|
||||||
|
|
||||||
enum RoomTypeFilter {
|
|
||||||
team = "team",
|
|
||||||
me = "me",
|
|
||||||
archived = "archived",
|
|
||||||
}
|
|
||||||
|
|
||||||
enum RoomTimeFilter {
|
|
||||||
today = "today",
|
|
||||||
week = "week",
|
|
||||||
month = "month",
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = getFirestore();
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
const lastWeek = new Date();
|
|
||||||
lastWeek.setDate(lastWeek.getDate() - 7);
|
|
||||||
const nextWeek = new Date();
|
|
||||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
|
||||||
|
|
||||||
export default function DashboardRoom() {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const { handleModalType, showModalType } = useKeyboardContext();
|
|
||||||
const { team } = useTeamDashboardContext();
|
|
||||||
|
|
||||||
const [roomsMap, setRoomsMap] = useState<Map<string, Room>>(
|
|
||||||
new Map<string, Room>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// get recurring rooms data realtime: ALL of them
|
|
||||||
useEffect(() => {
|
|
||||||
/**
|
|
||||||
* QUERY:
|
|
||||||
* all recurring rooms, as the created date can be a year ago
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const q = query(
|
|
||||||
collection(db, Collections.rooms),
|
|
||||||
where("teamId", "==", team.id),
|
|
||||||
where("type", "==", RoomType.recurring)
|
|
||||||
);
|
|
||||||
|
|
||||||
// return unsubscribe
|
|
||||||
return onSnapshot(q, (snapshot) => {
|
|
||||||
snapshot.docChanges().forEach(handleDocChange);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// get scheduled rooms data realtime: the ones with the date range
|
|
||||||
useEffect(() => {
|
|
||||||
/**
|
|
||||||
* QUERY:
|
|
||||||
* all scheduled rooms in the past 7 days or the next 7 days
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const q = query(
|
|
||||||
collection(db, Collections.rooms),
|
|
||||||
where("teamId", "==", team.id),
|
|
||||||
where("type", "==", RoomType.scheduled),
|
|
||||||
where("scheduledDateTime", ">", lastWeek),
|
|
||||||
where("scheduledDateTime", "<", nextWeek),
|
|
||||||
orderBy("scheduledDateTime", "asc")
|
|
||||||
);
|
|
||||||
|
|
||||||
// return unsubscribe
|
|
||||||
return onSnapshot(q, (snapshot) => {
|
|
||||||
snapshot.docChanges().forEach(handleDocChange);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// get now rooms data realtime
|
|
||||||
useEffect(() => {
|
|
||||||
/**
|
|
||||||
* QUERY:
|
|
||||||
* all live/now rooms going on right now
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const q = query(
|
|
||||||
collection(db, Collections.rooms),
|
|
||||||
where("teamId", "==", team.id),
|
|
||||||
where("type", "==", RoomType.now),
|
|
||||||
where("createdDate", ">", lastWeek),
|
|
||||||
orderBy("createdDate", "asc")
|
|
||||||
);
|
|
||||||
|
|
||||||
// return unsubscribe
|
|
||||||
return onSnapshot(q, (snapshot) => {
|
|
||||||
snapshot.docChanges().forEach(handleDocChange);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function handleDocChange(change) {
|
|
||||||
let updatedRoom = change.doc.data() as Room;
|
|
||||||
updatedRoom.id = change.doc.id;
|
|
||||||
|
|
||||||
if (change.type === "added" || change.type === "modified") {
|
|
||||||
// update rooms map
|
|
||||||
setRoomsMap((prevMap) => {
|
|
||||||
return new Map(prevMap.set(updatedRoom.id, updatedRoom));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (change.type === "removed") {
|
|
||||||
console.log("Removed room: ", updatedRoom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// on CTRL + Q, new tab to google meet
|
|
||||||
// on CTRL + V, show modal to create meeting with the link
|
|
||||||
|
|
||||||
const [selectedTabPane, setSelectedTabPane] = useState<string>(
|
|
||||||
RoomTypeFilter.me
|
|
||||||
);
|
|
||||||
|
|
||||||
// sort this mega array
|
|
||||||
const allRooms = Array.from(roomsMap.values());
|
|
||||||
|
|
||||||
const teamRooms = allRooms.filter(
|
|
||||||
(room) => room.status != RoomStatus.archived
|
|
||||||
);
|
|
||||||
const meRooms = allRooms.filter(
|
|
||||||
(room) =>
|
|
||||||
room.members.includes(currUser.uid) && room.status != RoomStatus.archived
|
|
||||||
);
|
|
||||||
|
|
||||||
const archivedRooms = allRooms.filter(
|
|
||||||
(room) => room.status == RoomStatus.archived
|
|
||||||
);
|
|
||||||
|
|
||||||
// data for different meRooms
|
|
||||||
const recurring = meRooms.filter(
|
|
||||||
(room) => room.type == RoomType.recurring && room.status == RoomStatus.empty
|
|
||||||
);
|
|
||||||
const now = meRooms.filter(
|
|
||||||
(room) =>
|
|
||||||
(room.type == RoomType.now && room.status != RoomStatus.archived) ||
|
|
||||||
room.status == RoomStatus.live
|
|
||||||
);
|
|
||||||
const scheduled = meRooms.filter(
|
|
||||||
(room) => room.type == RoomType.scheduled && room.status == RoomStatus.empty
|
|
||||||
);
|
|
||||||
|
|
||||||
// sorting array of scheduled rooms
|
|
||||||
scheduled.sort(function (a, b) {
|
|
||||||
// Turn your strings into dates, and then subtract them
|
|
||||||
// to get a value that is either negative, positive, or zero.
|
|
||||||
if (!b.scheduledDateTime) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
getTime(a.scheduledDateTime.toDate()) -
|
|
||||||
getTime(b.scheduledDateTime.toDate())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
var relativeTimeNextMeeting = "";
|
|
||||||
|
|
||||||
// go through scheduled and find the first one coming up
|
|
||||||
if (scheduled && scheduled.length > 0) {
|
|
||||||
const firstOneInFuture = scheduled.find(
|
|
||||||
(room) => moment(room.scheduledDateTime.toDate()).diff(today) > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (firstOneInFuture) {
|
|
||||||
const meetingDatetime: Date = firstOneInFuture.scheduledDateTime.toDate();
|
|
||||||
relativeTimeNextMeeting = moment(meetingDatetime).fromNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoomContent() {
|
|
||||||
// return data based on the selected filters
|
|
||||||
switch (selectedTabPane) {
|
|
||||||
case RoomTypeFilter.team:
|
|
||||||
// render sections for different types
|
|
||||||
|
|
||||||
const recurringTeam = teamRooms.filter(
|
|
||||||
(room) =>
|
|
||||||
room.type == RoomType.recurring && room.status == RoomStatus.empty
|
|
||||||
);
|
|
||||||
|
|
||||||
const nowTeam = teamRooms.filter(
|
|
||||||
(room) =>
|
|
||||||
(room.type == RoomType.now && room.status != RoomStatus.archived) ||
|
|
||||||
room.status == RoomStatus.live
|
|
||||||
);
|
|
||||||
|
|
||||||
const scheduledTeam = teamRooms.filter(
|
|
||||||
(room) =>
|
|
||||||
room.type == RoomType.scheduled && room.status == RoomStatus.empty
|
|
||||||
);
|
|
||||||
|
|
||||||
// sorting array of scheduled rooms
|
|
||||||
scheduledTeam.sort(function (a, b) {
|
|
||||||
// Turn your strings into dates, and then subtract them
|
|
||||||
// to get a value that is either negative, positive, or zero.
|
|
||||||
if (!b.scheduledDateTime) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
getTime(a.scheduledDateTime.toDate()) -
|
|
||||||
getTime(b.scheduledDateTime.toDate())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-row flex-wrap">
|
|
||||||
{nowTeam.map((room) => (
|
|
||||||
<RoomCard
|
|
||||||
key={room.id}
|
|
||||||
room={room}
|
|
||||||
updateRoomHandler={handleUpdateRoom}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{scheduledTeam.map((room) => (
|
|
||||||
<RoomCard
|
|
||||||
key={room.id}
|
|
||||||
room={room}
|
|
||||||
updateRoomHandler={handleUpdateRoom}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{recurringTeam.map((room) => (
|
|
||||||
<RoomCard
|
|
||||||
key={room.id}
|
|
||||||
room={room}
|
|
||||||
updateRoomHandler={handleUpdateRoom}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
case RoomTypeFilter.me:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-row flex-wrap">
|
|
||||||
{now.map((room) => (
|
|
||||||
<RoomCard
|
|
||||||
key={room.id}
|
|
||||||
room={room}
|
|
||||||
updateRoomHandler={handleUpdateRoom}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{scheduled.map((room) => (
|
|
||||||
<RoomCard
|
|
||||||
key={room.id}
|
|
||||||
room={room}
|
|
||||||
updateRoomHandler={handleUpdateRoom}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{recurring.map((room) => (
|
|
||||||
<RoomCard
|
|
||||||
key={room.id}
|
|
||||||
room={room}
|
|
||||||
updateRoomHandler={handleUpdateRoom}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case RoomTypeFilter.archived:
|
|
||||||
return (
|
|
||||||
<span className="flex flex-col space-y-5 px-10">
|
|
||||||
{archivedRooms.map((room) => {
|
|
||||||
// if the room is
|
|
||||||
return (
|
|
||||||
<RoomCard
|
|
||||||
key={room.id}
|
|
||||||
room={room}
|
|
||||||
updateRoomHandler={handleUpdateRoom}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return allRooms.map((room) => {
|
|
||||||
// if the room is
|
|
||||||
return (
|
|
||||||
<RoomCard
|
|
||||||
key={room.id}
|
|
||||||
room={room}
|
|
||||||
updateRoomHandler={handleUpdateRoom}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimePeriodFilterMenu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key={1} disabled>
|
|
||||||
Past 24 Hours (Coming Soon)
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key={2} disabled>
|
|
||||||
This Month (Coming Soon)
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedUpdateRoom, setSelectedUpdateRoom] = useState<Room>(null);
|
|
||||||
|
|
||||||
async function handleUpdateRoom(roomId: string) {
|
|
||||||
console.log("going to update room");
|
|
||||||
|
|
||||||
// get the room details and pass it into the modal
|
|
||||||
if (!roomsMap.has(roomId)) {
|
|
||||||
toast.error("not a valid room to edit");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass this to the modal for use
|
|
||||||
setSelectedUpdateRoom(roomsMap.get(roomId));
|
|
||||||
|
|
||||||
// call the keyboard context to show the modal
|
|
||||||
handleModalType(ShowModalType.createRoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCloseModal() {
|
|
||||||
console.log("closing modal");
|
|
||||||
|
|
||||||
// to make sure that the next time the props changes, the modal has the updated room object
|
|
||||||
setSelectedUpdateRoom(null);
|
|
||||||
|
|
||||||
// close the modal
|
|
||||||
handleModalType(ShowModalType.na);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md flex-1 overflow-auto">
|
|
||||||
{/* modal for creating room */}
|
|
||||||
<CreateOrUpdateRoom
|
|
||||||
show={showModalType == ShowModalType.createRoom}
|
|
||||||
updateRoom={selectedUpdateRoom}
|
|
||||||
handleClose={handleCloseModal}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* header */}
|
|
||||||
<Tooltip title={" archive rooms to keep your team focused"}>
|
|
||||||
<span className="flex flex-row justify-end space-x-3 pb-5 items-center">
|
|
||||||
<span className="flex flex-col mr-auto">
|
|
||||||
<span className="text-white ">
|
|
||||||
ROOMS
|
|
||||||
<Tooltip
|
|
||||||
title={"Press q to create an instant room if you have GSuite."}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="right-1 rounded-lg py-1 px-2 ml-1
|
|
||||||
shadow-md text-center text-white text-sm font-bold"
|
|
||||||
>
|
|
||||||
Q
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
{relativeTimeNextMeeting ? (
|
|
||||||
<span className="text-gray-300 text-xs">
|
|
||||||
{"You have your next scheduled one "}{" "}
|
|
||||||
<span className="text-orange-500">
|
|
||||||
{relativeTimeNextMeeting}
|
|
||||||
{"."}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Radio.Group
|
|
||||||
value={selectedTabPane}
|
|
||||||
onChange={(e) => setSelectedTabPane(e.target.value)}
|
|
||||||
>
|
|
||||||
<Radio.Button value={RoomTypeFilter.me}>
|
|
||||||
{RoomTypeFilter.me}{" "}
|
|
||||||
<span className="text-xs text-orange-500">
|
|
||||||
{meRooms.length > 0 ? meRooms.length : ""}
|
|
||||||
</span>
|
|
||||||
</Radio.Button>
|
|
||||||
<Radio.Button value={RoomTypeFilter.team}>
|
|
||||||
{RoomTypeFilter.team}{" "}
|
|
||||||
<span className="text-xs text-orange-500">
|
|
||||||
{teamRooms.length}
|
|
||||||
</span>
|
|
||||||
</Radio.Button>
|
|
||||||
<Radio.Button value={RoomTypeFilter.archived}>
|
|
||||||
{RoomTypeFilter.archived}{" "}
|
|
||||||
<span className="text-xs text-orange-500">
|
|
||||||
{archivedRooms.length > 0 ? archivedRooms.length : ""}
|
|
||||||
</span>
|
|
||||||
</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
|
|
||||||
<Dropdown overlay={TimePeriodFilterMenu}>
|
|
||||||
<span className="text-sm text-gray-300 flex flex-row items-center uppercase hover:cursor-pointer">
|
|
||||||
Week <FaAngleDown />
|
|
||||||
</span>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedUpdateRoom(null);
|
|
||||||
handleModalType(ShowModalType.createRoom);
|
|
||||||
}}
|
|
||||||
className="bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40"
|
|
||||||
>
|
|
||||||
<FaPlus className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* <BsThreeDots className="text-xl text-white" /> */}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{/* all rooms */}
|
|
||||||
<span className="overflow-auto">{getRoomContent()}</span>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import { FaPlus, FaBackward, FaArrowCircleDown } from "react-icons/fa";
|
|
||||||
import { BsThreeDots } from "react-icons/bs";
|
|
||||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { TeamMemberRole, TeamMemberStatus } from "../../models/teamMember";
|
|
||||||
import { getFirestore } from "firebase/firestore";
|
|
||||||
import { Tooltip } from "antd";
|
|
||||||
import {
|
|
||||||
ShowModalType,
|
|
||||||
useKeyboardContext,
|
|
||||||
} from "../../contexts/keyboardContext";
|
|
||||||
import UserStatusBubble, { UserPulse } from "../UserStatusBubble";
|
|
||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
import PowerPlayer from "../Modals/PowerPlayer";
|
|
||||||
|
|
||||||
const maxNumberOfKeyboardMappings: number = 9;
|
|
||||||
|
|
||||||
export default function TeamVoiceLine() {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const {
|
|
||||||
addTeamShortcutBinding,
|
|
||||||
selectTeamMember,
|
|
||||||
selectedTeammate,
|
|
||||||
isRecording,
|
|
||||||
handleModalType,
|
|
||||||
} = useKeyboardContext();
|
|
||||||
const router = useRouter();
|
|
||||||
const { teamid } = router.query;
|
|
||||||
const { userTeamMember, teamUsers, messagesByTeamMate } =
|
|
||||||
useTeamDashboardContext();
|
|
||||||
|
|
||||||
// todo use a global is loading
|
|
||||||
const [loading, setLoading] = useState<Boolean>(false);
|
|
||||||
|
|
||||||
// set up shortcuts for each teammate
|
|
||||||
useEffect(() => {
|
|
||||||
// create shortcuts
|
|
||||||
if (!teamUsers) {
|
|
||||||
console.log("no team members to map to shortcuts");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
teamUsers.forEach((tmUser, i) => {
|
|
||||||
// make sure we don't map a user if they are past the max allowed
|
|
||||||
if (i < maxNumberOfKeyboardMappings) {
|
|
||||||
let shortcut: number = i + 49; // 49 is what number 1 is on the keyboard
|
|
||||||
addTeamShortcutBinding(shortcut, tmUser.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [teamUsers]);
|
|
||||||
|
|
||||||
async function handleAdminRoute() {
|
|
||||||
if (userTeamMember.role == TeamMemberRole.admin) {
|
|
||||||
router.push("/teams/" + teamid + "/admin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error("You are not a team admin!");
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// todo move all variable stuff like shortcut mappings here
|
|
||||||
}, [teamUsers]);
|
|
||||||
|
|
||||||
function renderTeamMemberList() {
|
|
||||||
// show loading skeleton if not yet got friend info
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="w-full h-24 flex animate-pulse flex-row items-center justify-center space-x-5">
|
|
||||||
<div className="w-12 bg-gray-300 h-12 rounded-full "></div>
|
|
||||||
<div className="flex flex-col space-y-3">
|
|
||||||
<div className="w-60 bg-gray-300 h-6 rounded-md "></div>
|
|
||||||
<div className="w-40 bg-gray-300 h-6 rounded-md "></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if not teammates, stale state message to tell admin to add people
|
|
||||||
if (!teamUsers) {
|
|
||||||
return <span className="text-gray-300">Please add team members.</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return teamUsers.map((tmember, i) => {
|
|
||||||
var isMessageIncoming: boolean = false;
|
|
||||||
if (
|
|
||||||
messagesByTeamMate.has(tmember.id) &&
|
|
||||||
messagesByTeamMate.get(tmember.id)[0]?.receiverUserId == currUser.uid
|
|
||||||
) {
|
|
||||||
isMessageIncoming = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var customSelectClasses: string = "";
|
|
||||||
if (tmember.id == selectedTeammate) {
|
|
||||||
customSelectClasses = "bg-white scale-100 z-20 ";
|
|
||||||
} else if (isMessageIncoming) {
|
|
||||||
customSelectClasses = "bg-orange-500 bg-opacity-20";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
onClick={() => selectTeamMember(tmember.id)}
|
|
||||||
key={i}
|
|
||||||
className={`rounded flex flex-row items-center py-2 px-2 justify-items-start
|
|
||||||
ease-in-out duration-300 hover:cursor-pointer ${customSelectClasses} hover:bg-gray-200 hover:bg-opacity-20`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`relative flex mr-2 transition-all duration-500 ${
|
|
||||||
tmember.id == selectedTeammate ? "scale-110" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="bg-gray-200 bg-opacity-30 rounded-full shadow-md absolute w-full h-full"></span>
|
|
||||||
|
|
||||||
<UserStatusBubble status={tmember.userStatus} />
|
|
||||||
|
|
||||||
<img
|
|
||||||
src={tmember.avatarUrl}
|
|
||||||
alt="asdf"
|
|
||||||
className="rounded-full w-12"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col">
|
|
||||||
{tmember.id == selectedTeammate ? (
|
|
||||||
<>
|
|
||||||
<span className="flex flex-row items-center space-x-2">
|
|
||||||
<span className="text-md text-black font-bold">
|
|
||||||
{tmember.nickName}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className={"text-xs span-sans text-gray-500"}>
|
|
||||||
{tmember.teamRole}{" "}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="flex flex-row items-center space-x-2">
|
|
||||||
<span className="text-sm text-white font-bold">
|
|
||||||
{tmember.nickName}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-white shadow-xl font-bold px-3 py-1 rounded">
|
|
||||||
{/* keyboard shortcuts start from 1 */}
|
|
||||||
{i + 1}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className={"text-xs span-sans text-gray-300"}>
|
|
||||||
{tmember.teamRole}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{tmember.id == selectedTeammate ? (
|
|
||||||
<>
|
|
||||||
<Tooltip title="press and hold R to send audio message">
|
|
||||||
{isRecording ? (
|
|
||||||
<button className="ml-auto shadow-lg w-10 h-10 border-orange-400 bg-orange-400 bg-opacity-80 p-2 rounded hover:bg-opacity-100">
|
|
||||||
<span className="text-sm text-white font-bold">R</span>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button className="ml-auto shadow-lg w-10 h-10 border-orange-400 bg-opacity-80 p-2 rounded hover:bg-opacity-100">
|
|
||||||
<span className="text-sm text-orange-500 font-bold">R</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="press space to play latest convo chunk">
|
|
||||||
<button className="ml-2 h-10 shadow-lg border-green-400 bg-opacity-80 p-2 rounded hover:bg-opacity-100">
|
|
||||||
<span className="text-sm text-green-500 font-bold">
|
|
||||||
SPACE
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<BsThreeDots className="text-black ml-2 hover:cursor-pointer" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="flex flex-row items-center ml-auto ">
|
|
||||||
{isMessageIncoming ? (
|
|
||||||
<FaArrowCircleDown className="text-orange-500" />
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
|
|
||||||
<UserPulse status={tmember.userStatus} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const [showPowerPlayer, setShowPowerPlayer] = useState<boolean>(false);
|
|
||||||
|
|
||||||
function handleCloseModal() {
|
|
||||||
handleModalType(ShowModalType.na);
|
|
||||||
setShowPowerPlayer(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleShowModal() {
|
|
||||||
handleModalType(ShowModalType.powerPlayer); // disables controls
|
|
||||||
|
|
||||||
setShowPowerPlayer(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md w-96 shrink-0 max-h-[35rem]">
|
|
||||||
<span className="flex flex-row justify-start items-center pb-5">
|
|
||||||
<span className="flex flex-col">
|
|
||||||
<span className="text-white">TEAM</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Tooltip title="power playback mode">
|
|
||||||
<button
|
|
||||||
onClick={handleShowModal}
|
|
||||||
className="bg-gray-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40"
|
|
||||||
>
|
|
||||||
<FaBackward className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="add members">
|
|
||||||
<button
|
|
||||||
onClick={handleAdminRoute}
|
|
||||||
className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40"
|
|
||||||
>
|
|
||||||
<FaPlus className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<PowerPlayer show={showPowerPlayer} handleCloseModal={handleCloseModal} />
|
|
||||||
|
|
||||||
{/* list of team members */}
|
|
||||||
<div className="flex flex-col overflow-y-auto space-y-2">
|
|
||||||
{renderTeamMemberList()}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+2
-54
@@ -1,7 +1,6 @@
|
|||||||
import { Select, Tag } from "antd";
|
import { Select, Tag } from "antd";
|
||||||
import Avatar from "antd/lib/avatar/avatar";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { FaPaperPlane, FaRegTimesCircle } from "react-icons/fa";
|
import SimpleUserDetailsRow from "../UserDetails/SimpleUserDetailsRow";
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{ value: "gold" },
|
{ value: "gold" },
|
||||||
@@ -45,58 +44,7 @@ export default function CreateConversation() {
|
|||||||
{/* list of selected people who are nirvana users */}
|
{/* list of selected people who are nirvana users */}
|
||||||
|
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="flex flex-row items-center py-1 border-t">
|
<SimpleUserDetailsRow />
|
||||||
<Avatar shape="square" size={"small"}>
|
|
||||||
A
|
|
||||||
</Avatar>
|
|
||||||
|
|
||||||
<span className="flex flex-col ml-2">
|
|
||||||
<span className="text-slate-400 text-xs">
|
|
||||||
{"[email protected]"}
|
|
||||||
</span>
|
|
||||||
<span className="text-orange-500 text-xs">
|
|
||||||
Not a valid Nirvana user.
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button className="p-2 rounded-full hover:cursor-pointer text-sky-500 ml-auto">
|
|
||||||
<FaPaperPlane className="ml-auto text-lg" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row items-center py-1 border-t">
|
|
||||||
<Avatar
|
|
||||||
src={"https://joeschmoe.io/api/v1/random"}
|
|
||||||
shape="square"
|
|
||||||
size={"small"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className="flex flex-col ml-2">
|
|
||||||
<span className="text-teal-500 font-bold">{"Joe Smoe"}</span>
|
|
||||||
<span className="text-slate-400 text-xs">
|
|
||||||
{"[email protected]"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto">
|
|
||||||
<FaRegTimesCircle className="ml-auto text-lg" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span className="flex flex-row items-center py-1 border-t">
|
|
||||||
<Avatar
|
|
||||||
src={"https://joeschmoe.io/api/v1/2"}
|
|
||||||
shape="square"
|
|
||||||
size={"small"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className="flex flex-col ml-2">
|
|
||||||
<span className="text-teal-500 font-bold">{"Elon Musk"}</span>
|
|
||||||
<span className="text-slate-400 text-xs">{"[email protected]"}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto">
|
|
||||||
<FaRegTimesCircle className="ml-auto text-lg" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="flex flex-col mt-10">
|
<span className="flex flex-col mt-10">
|
||||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FaAngleRight, FaGripHorizontal } from "react-icons/fa";
|
import { FaAngleRight, FaGripHorizontal } from "react-icons/fa";
|
||||||
import { useAuth } from "../../contexts/authContext";
|
import { useAuth } from "../../contexts/authContext";
|
||||||
import MainLogo from "../MainLogo";
|
import MainLogo from "../Logo/MainLogo";
|
||||||
|
|
||||||
enum LandingPageNavigation {
|
enum LandingPageNavigation {
|
||||||
product = "/",
|
product = "/",
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
import {
|
|
||||||
FaAngleDoubleRight,
|
|
||||||
FaArchive,
|
|
||||||
FaExternalLinkAlt,
|
|
||||||
FaFilePdf,
|
|
||||||
FaTrash,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import Link, { LinkState, LinkType } from "../models/link";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { BsThreeDots } from "react-icons/bs";
|
|
||||||
import LinkIcon from "./LinkIcon";
|
|
||||||
import { useTeamDashboardContext } from "../contexts/teamDashboardContext";
|
|
||||||
import { useAuth } from "../contexts/authContext";
|
|
||||||
import { User } from "../models/user";
|
|
||||||
import { Avatar, Dropdown, Menu, Tooltip } from "antd";
|
|
||||||
import { useState } from "react";
|
|
||||||
import SkeletonLoader from "./Loading/skeletonLoader";
|
|
||||||
import moment from "moment";
|
|
||||||
import { LinkService } from "../services/linkService";
|
|
||||||
|
|
||||||
interface ILinkCardProps {
|
|
||||||
link: Link;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkService = new LinkService();
|
|
||||||
|
|
||||||
export default function LinkCard(props: ILinkCardProps) {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const { teamUsersMap, user } = useTeamDashboardContext();
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
var sender: User;
|
|
||||||
|
|
||||||
if (props.link.createdByUserId == currUser.uid) {
|
|
||||||
sender = user;
|
|
||||||
} else {
|
|
||||||
sender = teamUsersMap[props.link.createdByUserId];
|
|
||||||
}
|
|
||||||
|
|
||||||
var receivers: User[] = props.link.recipients?.reduce((results, userId) => {
|
|
||||||
if (userId in teamUsersMap) {
|
|
||||||
results.push(teamUsersMap[userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if I am the receiver still add me to the receivers
|
|
||||||
if (userId == currUser.uid) {
|
|
||||||
results.push(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}, [] as User[]);
|
|
||||||
|
|
||||||
async function handleDeleteLink() {
|
|
||||||
setLoading(true);
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"Are you sure you want to delete link? It will disappear for all recipients."
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.log("deleting link");
|
|
||||||
|
|
||||||
await linkService.updateLinkState(props.link.id, LinkState.deleted);
|
|
||||||
|
|
||||||
try {
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleArchivingLink() {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"Are you sure you want to archive link? It will be in the archive tab."
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await linkService.updateLinkState(props.link.id, LinkState.archived);
|
|
||||||
try {
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <SkeletonLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LinkOptionsMenu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item
|
|
||||||
key={2}
|
|
||||||
danger
|
|
||||||
onClick={handleArchivingLink}
|
|
||||||
icon={<FaArchive />}
|
|
||||||
>
|
|
||||||
<button>Archive Link</button>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
key={3}
|
|
||||||
danger
|
|
||||||
onClick={handleArchivingLink}
|
|
||||||
icon={<FaTrash />}
|
|
||||||
>
|
|
||||||
<button>Delete Link</button>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
const relativeDateTime: string = moment(
|
|
||||||
props.link.createdDate.toDate()
|
|
||||||
).fromNow();
|
|
||||||
|
|
||||||
var receiversNames = "team";
|
|
||||||
if (receivers) {
|
|
||||||
receiversNames = receivers.map((receiver) => receiver.firstName).join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="flex flex-col rounded-lg w-72 max-h-60 overflow-clip shrink-0">
|
|
||||||
{/* attmnt header */}
|
|
||||||
<Tooltip title={props.link.link}>
|
|
||||||
<span
|
|
||||||
onClick={() => window.open(props.link.link, "_blank")}
|
|
||||||
className="flex flex-row bg-gray-300 bg-opacity-25 py-5 px-3 items-center justify-start hover:cursor-pointer h-full"
|
|
||||||
>
|
|
||||||
<LinkIcon
|
|
||||||
className="text-4xl mr-2 shrink-0"
|
|
||||||
linkType={props.link.type}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className="flex flex-col items-baseline mr-10 space-y-1">
|
|
||||||
<span className="text-md font-bold text-white text-ellipsis overflow-hidden">
|
|
||||||
{props.link.name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={`text-gray-200 text-xs mb-auto text-ellipsis whitespace-pre-line max-h-10 overflow-hidden`}
|
|
||||||
>
|
|
||||||
{props.link.description}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* attachment actions */}
|
|
||||||
<button className="bg-gray-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40">
|
|
||||||
<FaExternalLinkAlt className="text-sm text-white" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{/* attmnt footer */}
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
sender.nickName + " -> " + receiversNames + ": " + relativeDateTime
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex flex-row items-center bg-gray-400 bg-opacity-30 p-3">
|
|
||||||
<Avatar src={sender.avatarUrl} className="shadow-xl" />
|
|
||||||
|
|
||||||
<FaAngleDoubleRight className="text-orange-500 text-2xl mx-2" />
|
|
||||||
|
|
||||||
<Avatar.Group>
|
|
||||||
{receivers ? (
|
|
||||||
receivers.map((receiverUser, i) => {
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
key={receiverUser.id}
|
|
||||||
src={receiverUser.avatarUrl}
|
|
||||||
className="shadow-xl"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-white bg-emerald-400 p-1 rounded-md font-bold flex flex-row space-x-2 items-center">
|
|
||||||
<span>team</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Avatar.Group>
|
|
||||||
|
|
||||||
<span className="ml-auto">
|
|
||||||
<Dropdown
|
|
||||||
className="ml-auto"
|
|
||||||
overlay={LinkOptionsMenu}
|
|
||||||
trigger={["click"]}
|
|
||||||
>
|
|
||||||
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />
|
|
||||||
</Dropdown>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+2
-2
@@ -11,8 +11,8 @@ import {
|
|||||||
FaMicrophoneAlt,
|
FaMicrophoneAlt,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { UserStatus } from "../../models/user";
|
import { UserStatus } from "../../models/user";
|
||||||
import MainLogo from "../MainLogo";
|
import MainLogo from "../Logo/MainLogo";
|
||||||
import UserStatusBubble from "../UserStatusBubble";
|
import UserStatusBubble from "../UserDetails/UserStatusBubble";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import { Avatar, Tooltip } from "antd";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { FaPhoneSlash, FaPlus, FaWalking } from "react-icons/fa";
|
|
||||||
import { HiSpeakerphone } from "react-icons/hi";
|
|
||||||
import { useAuth } from "../contexts/authContext";
|
|
||||||
import { useTeamDashboardContext } from "../contexts/teamDashboardContext";
|
|
||||||
import OfficeRoom, { OfficeRoomState } from "../models/officeRoom";
|
|
||||||
import { User } from "../models/user";
|
|
||||||
import OfficeRoomService from "../services/officeRoomService";
|
|
||||||
import { VscDebugDisconnect } from "react-icons/vsc";
|
|
||||||
import AgoraService from "../services/agoraService";
|
|
||||||
|
|
||||||
interface IOfficeCard {
|
|
||||||
officeRoom: OfficeRoom;
|
|
||||||
handleJoinChannel: Function;
|
|
||||||
handleLeaveChannel: Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
const officeRoomService = new OfficeRoomService();
|
|
||||||
const agoraService = new AgoraService();
|
|
||||||
|
|
||||||
export default function OfficeCard(props: IOfficeCard) {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const { teamUsersMap, user } = useTeamDashboardContext();
|
|
||||||
|
|
||||||
var allMembersInRoom = props.officeRoom.members?.reduce((results, userId) => {
|
|
||||||
if (userId in teamUsersMap) {
|
|
||||||
results.push(teamUsersMap[userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId == currUser.uid) {
|
|
||||||
results.push(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}, [] as User[]);
|
|
||||||
|
|
||||||
async function handleJoinOfficeRoom() {
|
|
||||||
toast.loading("Connecting");
|
|
||||||
|
|
||||||
// make sure user is not in another room
|
|
||||||
// get agora token from cloud function
|
|
||||||
// join channel for agora
|
|
||||||
|
|
||||||
if (props.officeRoom.members.includes(currUser.uid)) {
|
|
||||||
toast.error("You are already in this office room!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// agora token from CF
|
|
||||||
const agoraToken = await agoraService.getAgoraToken(props.officeRoom.id);
|
|
||||||
|
|
||||||
// // function to do it all
|
|
||||||
// await officeRoomService.joinOfficeRoom(props.officeRoom, currUser.uid);
|
|
||||||
|
|
||||||
// handle joining agora channel
|
|
||||||
await props.handleJoinChannel(props.officeRoom.id, agoraToken);
|
|
||||||
|
|
||||||
// update firestore to add ourselves in the office room
|
|
||||||
const newMembers = [...props.officeRoom.members, currUser.uid];
|
|
||||||
await officeRoomService.updateMembersInOfficeRoom(
|
|
||||||
props.officeRoom.id,
|
|
||||||
newMembers
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error("problem joining office room");
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.dismiss();
|
|
||||||
toast.success("joined office room");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLeaveOfficeRoom() {
|
|
||||||
toast.loading("leaving");
|
|
||||||
|
|
||||||
if (!props.officeRoom.members.includes(currUser.uid)) {
|
|
||||||
toast.error("You are not in this office room!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// leave agora channel
|
|
||||||
await props.handleLeaveChannel();
|
|
||||||
|
|
||||||
// leave from firestore database
|
|
||||||
const newMembersInRoom = props.officeRoom.members.filter(
|
|
||||||
(memberId) => memberId != currUser.uid
|
|
||||||
);
|
|
||||||
|
|
||||||
await officeRoomService.updateMembersInOfficeRoom(
|
|
||||||
props.officeRoom.id,
|
|
||||||
newMembersInRoom
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error("problem leaving office room");
|
|
||||||
}
|
|
||||||
toast.dismiss();
|
|
||||||
toast.success("left office room");
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if user is in the office room
|
|
||||||
var isUserInRoom: boolean = false;
|
|
||||||
if (props.officeRoom.members?.includes(currUser.uid)) {
|
|
||||||
isUserInRoom = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`group flex flex-row justify-start items-center transition-all duration-500 hover:bg-gray-200 hover:bg-opacity-25
|
|
||||||
py-3 px-3 min-h-[3rem] rounded-lg ${
|
|
||||||
isUserInRoom ? "bg-orange-500 bg-opacity-25" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{renderOfficePulse(props.officeRoom.state)}
|
|
||||||
|
|
||||||
{/* office location name */}
|
|
||||||
<span className="text-gray-200 text-md font-semibold ml-2">
|
|
||||||
{props.officeRoom.name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* all members in room */}
|
|
||||||
<span className="ml-auto items-center flex">
|
|
||||||
<Avatar.Group>
|
|
||||||
{allMembersInRoom.map((member) => {
|
|
||||||
return (
|
|
||||||
<Tooltip key={member.id} title={member.nickName}>
|
|
||||||
<Avatar
|
|
||||||
src={member.avatarUrl}
|
|
||||||
style={{ backgroundColor: "teal", verticalAlign: "middle" }}
|
|
||||||
className="shadow-xl hover:z-20 hover:cursor-pointer"
|
|
||||||
>
|
|
||||||
{member.nickName[0]}
|
|
||||||
</Avatar>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Avatar.Group>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{isUserInRoom ? (
|
|
||||||
<button
|
|
||||||
onClick={handleLeaveOfficeRoom}
|
|
||||||
className="ml-2 bg-orange-500 bg-opacity-40 p-2 rounded"
|
|
||||||
>
|
|
||||||
<VscDebugDisconnect className="text-orange-500 text-xl" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={"Join"}>
|
|
||||||
<button
|
|
||||||
onClick={handleJoinOfficeRoom}
|
|
||||||
className="ml-2 bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40 group-hover:flex hidden"
|
|
||||||
>
|
|
||||||
<FaWalking className="text-lg text-white" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderOfficePulse(officeRoomState: OfficeRoomState) {
|
|
||||||
switch (officeRoomState) {
|
|
||||||
case OfficeRoomState.active:
|
|
||||||
return (
|
|
||||||
<span className="h-4 w-4 rounded-full bg-green-500 animate-pulse shadow-lg"></span>
|
|
||||||
);
|
|
||||||
case OfficeRoomState.idle:
|
|
||||||
return (
|
|
||||||
<span className="h-4 w-4 rounded-full bg-gray-400 animate-pulse shadow-lg"></span>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<span className="h-4 w-4 rounded-full bg-gray-400 animate-pulse shadow-lg"></span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
import { BsThreeDots } from "react-icons/bs";
|
|
||||||
import {
|
|
||||||
FaArchive,
|
|
||||||
FaBell,
|
|
||||||
FaCalendarDay,
|
|
||||||
FaClock,
|
|
||||||
FaLink,
|
|
||||||
FaPlus,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import { IoTimer } from "react-icons/io5";
|
|
||||||
import Room, { RoomStatus, RoomType } from "../models/room";
|
|
||||||
import RoomTypeTag from "./RoomTypeTag";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useAuth } from "../contexts/authContext";
|
|
||||||
import { Avatar, Divider, Dropdown, Menu, Popover, Tooltip } from "antd";
|
|
||||||
import { UserOutlined, AntDesignOutlined } from "@ant-design/icons";
|
|
||||||
import { useTeamDashboardContext } from "../contexts/teamDashboardContext";
|
|
||||||
import { User, UserStatus } from "../models/user";
|
|
||||||
import RoomService from "../services/roomService";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { useState } from "react";
|
|
||||||
import SkeletonLoader from "./Loading/skeletonLoader";
|
|
||||||
import { useKeyboardContext } from "../contexts/keyboardContext";
|
|
||||||
import UserService from "../services/userService";
|
|
||||||
import Moment from "react-moment";
|
|
||||||
import moment from "moment";
|
|
||||||
|
|
||||||
interface IRoomCardProps {
|
|
||||||
room: Room;
|
|
||||||
updateRoomHandler: Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomService = new RoomService();
|
|
||||||
const userService = new UserService();
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
export default function RoomCard(props: IRoomCardProps) {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const { teamUsersMap, user } = useTeamDashboardContext();
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
var isUserInRoom: boolean;
|
|
||||||
if (props.room.membersInRoom?.includes(currUser.uid)) {
|
|
||||||
isUserInRoom = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// first array of all members invited
|
|
||||||
var allMembersInvited = props.room.members?.reduce((results, userId) => {
|
|
||||||
if (userId in teamUsersMap) {
|
|
||||||
results.push(teamUsersMap[userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId == currUser.uid) {
|
|
||||||
results.push(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}, [] as User[]);
|
|
||||||
|
|
||||||
// second array of all members in room
|
|
||||||
var allMembersInRoom = props.room.membersInRoom?.reduce((results, userId) => {
|
|
||||||
if (userId in teamUsersMap) {
|
|
||||||
results.push(teamUsersMap[userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId == currUser.uid) {
|
|
||||||
results.push(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}, [] as User[]);
|
|
||||||
|
|
||||||
// third array all members invited and not in the array of users in room
|
|
||||||
const allMembersInvitedButNotInRoom = allMembersInvited.filter(
|
|
||||||
(invitedMember) => {
|
|
||||||
if (allMembersInRoom.some((user) => user.id === invitedMember.id)) {
|
|
||||||
/* vendors contains the element we're looking for */
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// show members who are in the invite list but not already in the room
|
|
||||||
const membersInvited = () => {
|
|
||||||
if (allMembersInvited?.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listOfNames = allMembersInvited
|
|
||||||
.map((user) => user.nickName)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip title={listOfNames}>
|
|
||||||
<span className="inline-flex flex-row-reverse items-center shrink-0 mr-1">
|
|
||||||
<Avatar.Group>
|
|
||||||
{allMembersInvitedButNotInRoom.map((user, i) => {
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
key={user.id}
|
|
||||||
src={user.avatarUrl}
|
|
||||||
className="shadow-xl"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Avatar.Group>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const membersInRoom = () => {
|
|
||||||
if (allMembersInRoom?.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listOfNames = allMembersInRoom
|
|
||||||
.map((user) => user.nickName)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip title={listOfNames}>
|
|
||||||
<span className="inline-flex flex-row-reverse items-center shrink-0 mr-auto">
|
|
||||||
<Avatar.Group>
|
|
||||||
{allMembersInRoom.map((user, i) => {
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
key={user.id}
|
|
||||||
src={user.avatarUrl}
|
|
||||||
className="shadow-xl"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Avatar.Group>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleLeavingRoom() {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// filter current membersInRoom and return new array
|
|
||||||
const newMembersInRoom = props.room.membersInRoom.filter(
|
|
||||||
(memberId) => memberId != currUser.uid
|
|
||||||
);
|
|
||||||
|
|
||||||
// update the room with room id to have a new array of userIds
|
|
||||||
await roomService.updateMembersInRoom(props.room.id, newMembersInRoom);
|
|
||||||
|
|
||||||
// change curr user status to free now
|
|
||||||
await userService.updateUserStatus(currUser.uid, UserStatus.online);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
toast.error("unable to leave room...something went wrong");
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleJoiningRoom() {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// update members in room
|
|
||||||
const newMembersInRoom = [...props.room.membersInRoom, currUser.uid];
|
|
||||||
|
|
||||||
// update the room with room id to have a new array of userIds
|
|
||||||
await roomService.updateMembersInRoom(props.room.id, newMembersInRoom);
|
|
||||||
|
|
||||||
// change curr user status to free now
|
|
||||||
await userService.updateUserStatus(currUser.uid, UserStatus.busy);
|
|
||||||
|
|
||||||
// then window.open to room's link
|
|
||||||
window.open(props.room.link, "_blank");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
toast.error("unable to leave room...something went wrong");
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleArchivingRoom() {
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"Are you sure you want to archive room? It will be added to the archived tab."
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.log("archiving room");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newRoom: Room = { ...props.room };
|
|
||||||
|
|
||||||
newRoom.status = RoomStatus.archived;
|
|
||||||
newRoom.membersInRoom = [];
|
|
||||||
await roomService.updateRoom(newRoom);
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <SkeletonLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUpdateRoom() {
|
|
||||||
props.updateRoomHandler(props.room.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const RoomOptionsMenu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key={1}>
|
|
||||||
<button onClick={handleUpdateRoom}>Edit Room Details</button>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key={2} danger onClick={handleArchivingRoom}>
|
|
||||||
Archive Room
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
const AgendaContent = (
|
|
||||||
<div>
|
|
||||||
<span className="text-sm whitespace-pre-line">
|
|
||||||
{props.room.description}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// handle specifics for a scheduled room
|
|
||||||
var dateTimeScheduled = null;
|
|
||||||
var isPastRoom = false; // signal user to archive this room if it's past
|
|
||||||
if (props.room.type == RoomType.scheduled) {
|
|
||||||
dateTimeScheduled = props.room.scheduledDateTime.toDate();
|
|
||||||
|
|
||||||
// check if the moment date scheduled is future or past
|
|
||||||
|
|
||||||
if (moment(today).diff(moment(dateTimeScheduled)) > 0) {
|
|
||||||
isPastRoom = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTopRightCardInfo() {
|
|
||||||
if (props.room.type == RoomType.scheduled && props.room.scheduledDateTime) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className="text-sky-700 bg-sky-200 p-1 rounded-md text-xs font-bold flex flex-row items-center space-x-1">
|
|
||||||
<FaCalendarDay />
|
|
||||||
<Moment date={dateTimeScheduled} format="ddd Do" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-emerald-700 bg-emerald-200 p-1 rounded-md text-xs font-bold mt-2 flex flex-row items-center space-x-1">
|
|
||||||
<FaClock />
|
|
||||||
<Moment date={dateTimeScheduled} format="h:mm a" />
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.room.status == RoomStatus.archived) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={`flex flex-row bg-gray-300 bg-opacity-25 px-5 items-center justify-start rounded-lg`}
|
|
||||||
>
|
|
||||||
{membersInvited()}
|
|
||||||
|
|
||||||
{/* header */}
|
|
||||||
<span className="flex flex-row justify-between items-start space-x-1 p-5 h-full">
|
|
||||||
{/* meeting details */}
|
|
||||||
<span className="flex flex-col items-baseline justify-start max-w-xs h-full">
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
isUserInRoom ? " text-gray-500" : "text-white"
|
|
||||||
} font-semibold`}
|
|
||||||
>
|
|
||||||
{props.room.name}
|
|
||||||
</span>
|
|
||||||
<Popover content={AgendaContent} title="Agenda">
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
isUserInRoom ? "text-gray-400" : "text-gray-200"
|
|
||||||
} text-xs mb-auto text-ellipsis whitespace-pre-line max-h-[3rem] overflow-hidden`}
|
|
||||||
>
|
|
||||||
{props.room.description}
|
|
||||||
</span>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
{/* all invited members who are not in the room already */}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<RoomTypeTag
|
|
||||||
roomStatus={props.room.status}
|
|
||||||
roomType={props.room.type}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* room status and link(s) */}
|
|
||||||
<span className="ml-auto flex flex-col items-end justify-between h-full w-fit">
|
|
||||||
{renderTopRightCardInfo()}
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
isUserInRoom ? "text-gray-400" : "text-gray-200"
|
|
||||||
} text-xs text-right mb-auto`}
|
|
||||||
>
|
|
||||||
{props.room.approximateDateTime}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* room attachments */}
|
|
||||||
<span className="flex flex-row space-x-2">
|
|
||||||
{props.room.attachments && props.room.attachments.length > 0 ? (
|
|
||||||
<button
|
|
||||||
onClick={() => window.open(props.room.attachments[0], "_blank")}
|
|
||||||
className={`${
|
|
||||||
isUserInRoom
|
|
||||||
? " bg-gray-400 bg-opacity-25 text-gray-400"
|
|
||||||
: "bg-gray-300 bg-opacity-25 text-white"
|
|
||||||
} bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40`}
|
|
||||||
>
|
|
||||||
<FaLink className="text-sm" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* footer */}
|
|
||||||
<span className="flex flex-row justify-end items-center p-3 rounded-lg">
|
|
||||||
<button
|
|
||||||
onClick={handleJoiningRoom}
|
|
||||||
className="text-sm font-semibold py-1 px-4 rounded bg-gray-200 text-green-500"
|
|
||||||
>
|
|
||||||
Join
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`flex flex-col ${
|
|
||||||
isUserInRoom ? " bg-white bg-opacity-80" : "bg-gray-300 bg-opacity-25"
|
|
||||||
} rounded-lg justify-between md:w-96 w-72 m-2 shrink-0 h-[14rem] shadow-md overflow-ellipsis`}
|
|
||||||
>
|
|
||||||
{/* header */}
|
|
||||||
<span className="flex flex-row justify-between items-start space-x-1 p-5 h-full">
|
|
||||||
{/* meeting details */}
|
|
||||||
<span className="flex flex-col items-baseline justify-start max-w-[12rem] h-full">
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
isUserInRoom ? " text-gray-500" : "text-white"
|
|
||||||
} font-semibold`}
|
|
||||||
>
|
|
||||||
{props.room.name}
|
|
||||||
</span>
|
|
||||||
<Popover content={AgendaContent} title="Agenda">
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
isUserInRoom ? "text-gray-400" : "text-gray-200"
|
|
||||||
} text-xs mb-auto text-ellipsis whitespace-pre-line max-h-[3rem] truncate`}
|
|
||||||
>
|
|
||||||
{props.room.description}
|
|
||||||
</span>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
{/* badges and tags */}
|
|
||||||
{/* <span className="flex flex-row flex-wrap space-x-2">
|
|
||||||
<span className="text-xs my-3 text-white bg-cyan-400 p-1 rounded-md font-bold flex flex-row space-x-2 items-center">
|
|
||||||
<span>scrum</span>
|
|
||||||
</span>
|
|
||||||
</span> */}
|
|
||||||
|
|
||||||
{/* all invited members who are not in the room already */}
|
|
||||||
{membersInvited()}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* room status and link(s) */}
|
|
||||||
<span className="flex flex-col items-end justify-between h-full w-fit">
|
|
||||||
<RoomTypeTag
|
|
||||||
roomStatus={props.room.status}
|
|
||||||
roomType={props.room.type}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{renderTopRightCardInfo()}
|
|
||||||
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
isUserInRoom ? "text-gray-400" : "text-gray-200"
|
|
||||||
} text-xs text-right mb-auto`}
|
|
||||||
>
|
|
||||||
{props.room.approximateDateTime}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* room attachments */}
|
|
||||||
<span className="flex flex-row space-x-2">
|
|
||||||
{props.room.attachments && props.room.attachments.length > 0 ? (
|
|
||||||
<button
|
|
||||||
onClick={() => window.open(props.room.attachments[0], "_blank")}
|
|
||||||
className={`${
|
|
||||||
isUserInRoom
|
|
||||||
? " bg-gray-400 bg-opacity-25 text-gray-400"
|
|
||||||
: "bg-gray-300 bg-opacity-25 text-white"
|
|
||||||
} bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40`}
|
|
||||||
>
|
|
||||||
<FaLink className="text-sm" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={handleUpdateRoom}
|
|
||||||
className={`${
|
|
||||||
isUserInRoom
|
|
||||||
? " bg-gray-400 bg-opacity-25 text-gray-400"
|
|
||||||
: "bg-gray-300 bg-opacity-25 text-white"
|
|
||||||
} bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40`}
|
|
||||||
>
|
|
||||||
<FaPlus className="text-sm" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* footer */}
|
|
||||||
<span className="flex flex-row justify-end items-center bg-gray-400 bg-opacity-30 p-3 rounded-lg">
|
|
||||||
{membersInRoom()}
|
|
||||||
|
|
||||||
{/* tell them to archive if it's past */}
|
|
||||||
{isPastRoom ? (
|
|
||||||
<Tooltip title={"please archive this meeting, it is over"}>
|
|
||||||
<button
|
|
||||||
onClick={handleArchivingRoom}
|
|
||||||
className="flex flex-row items-center space-x-2 text-sm mx-2 text-orange-500 font-semibold py-1 px-4 bg-gray-200 rounded"
|
|
||||||
>
|
|
||||||
<FaArchive /> <span>Archive</span>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isUserInRoom ? (
|
|
||||||
<button
|
|
||||||
onClick={handleLeavingRoom}
|
|
||||||
className="text-sm text-orange-500 font-semibold py-1 px-4 bg-gray-200 rounded"
|
|
||||||
>
|
|
||||||
👋 Leave
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={handleJoiningRoom}
|
|
||||||
className="text-sm font-semibold py-1 px-4 rounded bg-gray-200 text-green-500"
|
|
||||||
>
|
|
||||||
Join
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dropdown overlay={RoomOptionsMenu} trigger={["click"]}>
|
|
||||||
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />
|
|
||||||
</Dropdown>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { FaClock } from "react-icons/fa";
|
|
||||||
import { IoTimer } from "react-icons/io5";
|
|
||||||
import { RoomStatus, RoomType } from "../models/room";
|
|
||||||
|
|
||||||
interface RoomTypeTagProps {
|
|
||||||
roomType: RoomType;
|
|
||||||
roomStatus: RoomStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RoomTypeTag(props: RoomTypeTagProps) {
|
|
||||||
const universalClassNames =
|
|
||||||
"text-xs font-bold p-1 rounded-md flex flex-row space-x-2 items-center shadow-lg";
|
|
||||||
|
|
||||||
// show live even if it's some other type because people are in it
|
|
||||||
if (props.roomStatus == RoomStatus.live) {
|
|
||||||
return (
|
|
||||||
<span className={"bg-red-500 text-white " + universalClassNames}>
|
|
||||||
<FaClock />
|
|
||||||
<span>live</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else if (props.roomStatus == RoomStatus.archived) {
|
|
||||||
if (props.roomType == RoomType.now) {
|
|
||||||
return (
|
|
||||||
// <span className={"text-gray-700 bg-gray-200 " + universalClassNames}>
|
|
||||||
// <span>archived</span>
|
|
||||||
// </span>
|
|
||||||
|
|
||||||
<span className={"text-gray-700 bg-gray-200 " + universalClassNames}>
|
|
||||||
<FaClock />
|
|
||||||
<span>now room</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else if (props.roomType == RoomType.scheduled) {
|
|
||||||
return (
|
|
||||||
<span className={"text-blue-700 bg-blue-200 " + universalClassNames}>
|
|
||||||
<FaClock />
|
|
||||||
<span>scheduled room</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else if (props.roomType == RoomType.recurring) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={"text-yellow-700 bg-yellow-200 " + universalClassNames}
|
|
||||||
>
|
|
||||||
<IoTimer />
|
|
||||||
<span>recurring room</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it's just empty and not empty, then go ahead and show the right type
|
|
||||||
switch (props.roomType) {
|
|
||||||
case RoomType.now:
|
|
||||||
return (
|
|
||||||
<span className={"bg-red-500 text-white " + universalClassNames}>
|
|
||||||
<FaClock />
|
|
||||||
<span>live</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
case RoomType.scheduled:
|
|
||||||
return (
|
|
||||||
<></>
|
|
||||||
// <span className={"text-blue-700 bg-blue-200 " + universalClassNames}>
|
|
||||||
// <FaClock />
|
|
||||||
// <span>scheduled</span>
|
|
||||||
// </span>
|
|
||||||
);
|
|
||||||
|
|
||||||
case RoomType.recurring:
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={"text-yellow-700 bg-yellow-200 " + universalClassNames}
|
|
||||||
>
|
|
||||||
<IoTimer />
|
|
||||||
<span>recurring</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import Room, { RoomType } from "../models/room";
|
|
||||||
import RoomCard from "./RoomCard";
|
|
||||||
|
|
||||||
interface IRoomsData {
|
|
||||||
rooms: Room[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// different rows for the types
|
|
||||||
export function RoomsRowsByType(props: IRoomsData) {
|
|
||||||
// got the top level content/rooms
|
|
||||||
// now let's drill down and filter
|
|
||||||
const recurringRooms = props.rooms.filter(
|
|
||||||
(room) => room.type == RoomType.recurring
|
|
||||||
);
|
|
||||||
|
|
||||||
const scheduledRooms = props.rooms.filter(
|
|
||||||
(room) => room.type == RoomType.scheduled
|
|
||||||
);
|
|
||||||
|
|
||||||
const nowRooms = props.rooms.filter(
|
|
||||||
(room) => room.type == RoomType.scheduled
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
// return props.rooms.map((room) => {
|
|
||||||
// // if the room is
|
|
||||||
// return (
|
|
||||||
// <RoomCard
|
|
||||||
// key={room.id}
|
|
||||||
// room={room}
|
|
||||||
// updateRoomHandler={handleUpdateRoom}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
// simple table for the archived rooms
|
|
||||||
export function ArchivedRoomsTable(props: IRoomsData) {}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Avatar } from "antd";
|
||||||
|
import { FaPaperPlane, FaRegTimesCircle } from "react-icons/fa";
|
||||||
|
import User from "@nirvana/common/models/user";
|
||||||
|
|
||||||
|
export default function SelectedUserRow(props: { user: User }) {
|
||||||
|
console.log(props);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="flex flex-row items-center py-1 border-t">
|
||||||
|
<Avatar shape="square" size={"small"}>
|
||||||
|
A
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<span className="flex flex-col ml-2">
|
||||||
|
<span className="text-slate-400 text-xs">
|
||||||
|
{"[email protected]"}
|
||||||
|
</span>
|
||||||
|
<span className="text-orange-500 text-xs">
|
||||||
|
Not a valid Nirvana user.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button className="p-2 rounded-full hover:cursor-pointer text-sky-500 ml-auto">
|
||||||
|
<FaPaperPlane className="ml-auto text-lg" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row items-center py-1 border-t">
|
||||||
|
<Avatar
|
||||||
|
src={"https://joeschmoe.io/api/v1/random"}
|
||||||
|
shape="square"
|
||||||
|
size={"small"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="flex flex-col ml-2">
|
||||||
|
<span className="text-teal-500 font-bold">{"Joe Smoe"}</span>
|
||||||
|
<span className="text-slate-400 text-xs">{"[email protected]"}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto">
|
||||||
|
<FaRegTimesCircle className="ml-auto text-lg" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-row items-center py-1 border-t">
|
||||||
|
<Avatar
|
||||||
|
src={"https://joeschmoe.io/api/v1/2"}
|
||||||
|
shape="square"
|
||||||
|
size={"small"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="flex flex-col ml-2">
|
||||||
|
<span className="text-teal-500 font-bold">{"Elon Musk"}</span>
|
||||||
|
<span className="text-slate-400 text-xs">{"[email protected]"}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto">
|
||||||
|
<FaRegTimesCircle className="ml-auto text-lg" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { IoPulseOutline, IoRemoveOutline } from "react-icons/io5";
|
import { IoPulseOutline, IoRemoveOutline } from "react-icons/io5";
|
||||||
import { UserStatus } from "../models/user";
|
import { UserStatus } from "../../models/user";
|
||||||
|
|
||||||
interface UserStatusPropsInterface {
|
interface UserStatusPropsInterface {
|
||||||
status: UserStatus;
|
status: UserStatus;
|
||||||
@@ -1,646 +0,0 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { KeyCode } from "../globals/keycode";
|
|
||||||
import MicRecorder from "mic-recorder-to-mp3";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
import AudioPlayer from "react-h5-audio-player";
|
|
||||||
import "react-h5-audio-player/lib/styles.css";
|
|
||||||
import CloudStorageService from "../services/cloudStorageService";
|
|
||||||
import { Message } from "../models/message";
|
|
||||||
import { useAuth } from "./authContext";
|
|
||||||
import { SendService } from "../services/sendService";
|
|
||||||
import { useTeamDashboardContext } from "./teamDashboardContext";
|
|
||||||
import isValidHttpUrl from "../helpers/urlHelper";
|
|
||||||
import { GlobalHotKeys, KeyMap } from "react-hotkeys";
|
|
||||||
import Announcement from "../models/announcement";
|
|
||||||
|
|
||||||
interface KeyboardContextInterface {
|
|
||||||
selectedTeammate: string; // can only have one selected
|
|
||||||
selectTeamMember: Function;
|
|
||||||
|
|
||||||
teamShortcutMappings: {};
|
|
||||||
addTeamShortcutBinding: Function;
|
|
||||||
|
|
||||||
isRecording: Boolean; // can only record if someone is selected or maybe for an announcement
|
|
||||||
isRecordingAnnouncement: boolean;
|
|
||||||
|
|
||||||
isMuted: Boolean;
|
|
||||||
isSilenceMode: Boolean; // won't automatically listen to notifications or sounds
|
|
||||||
muteOrUnmute: Function;
|
|
||||||
silenceOrLivenMode: Function;
|
|
||||||
|
|
||||||
hasRecPermit: Boolean; // permission to record or not
|
|
||||||
|
|
||||||
audioInputDeviceId: string;
|
|
||||||
audioOutputDeviceId: string;
|
|
||||||
|
|
||||||
selectAudioOutput: Function;
|
|
||||||
selectAudioInput: Function;
|
|
||||||
|
|
||||||
inputDevices: MediaDeviceInfo[];
|
|
||||||
outputDevices: MediaDeviceInfo[];
|
|
||||||
|
|
||||||
ctrlDown: boolean;
|
|
||||||
|
|
||||||
showModalType: ShowModalType;
|
|
||||||
handleModalType: Function;
|
|
||||||
pastedLink: string;
|
|
||||||
|
|
||||||
handleAddAudioToQueue: Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ShowModalType {
|
|
||||||
createLink = "link",
|
|
||||||
createRoom = "room",
|
|
||||||
powerPlayer = "powerPlayer",
|
|
||||||
na = "none",
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeyboardContext = React.createContext<KeyboardContextInterface | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
function stopBothVideoAndAudio(stream) {
|
|
||||||
stream.getTracks().forEach(function (track) {
|
|
||||||
if (track.readyState == "live") {
|
|
||||||
track.stop();
|
|
||||||
|
|
||||||
console.log("stopped playing anything");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudStorageService = new CloudStorageService();
|
|
||||||
const sendService = new SendService();
|
|
||||||
|
|
||||||
export default function KeyboardContextProvider({ children }) {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
|
|
||||||
// SECTION: set up for shortcuts and recording and such
|
|
||||||
const [selectedTeammate, setSelectedTeamMember] = useState<string>(null); // id of selected teammate
|
|
||||||
const [isRecording, setIsRecording] = useState<Boolean>(false);
|
|
||||||
const [isRecordingAnnouncement, setIsRecordingAnnouncement] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
const [hasRecPermit, setHasRecPermit] = useState<Boolean>(false);
|
|
||||||
|
|
||||||
const [ctrlDown, setCtrlDown] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [teamShortcutMappings, setTeamShortcutMappings] = useState<{}>({});
|
|
||||||
const [audioInputDeviceId, setAudioInputDevice] = useState<string>(null); // device id
|
|
||||||
const [audioOutputDeviceId, setAudioOutputDevice] = useState<string>(null); // device id
|
|
||||||
|
|
||||||
const [inputDevices, setInputDevices] = useState<MediaDeviceInfo[]>([]);
|
|
||||||
const [outputDevices, setOutputDevices] = useState<MediaDeviceInfo[]>([]);
|
|
||||||
|
|
||||||
const [isMuted, setIsMuted] = useState<Boolean>(false);
|
|
||||||
const [isSilenceMode, setIseSilenceMode] = useState<Boolean>(false);
|
|
||||||
|
|
||||||
const [pastedLink, setPastedLink] = useState<string>(null);
|
|
||||||
const [showModalType, setShowModalType] = useState<ShowModalType>(
|
|
||||||
ShowModalType.na
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleModalType = (modalType: ShowModalType) => {
|
|
||||||
handleResetView();
|
|
||||||
setShowModalType(modalType);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [recorder, setRecorder] = useState<MicRecorder>(
|
|
||||||
new MicRecorder({ bitRate: 128 })
|
|
||||||
);
|
|
||||||
|
|
||||||
const [audioQueue, setAudioQueue] = useState<string[]>([]); // queue for the player to keep playing
|
|
||||||
|
|
||||||
// playing incoming messages
|
|
||||||
const { allMessages, messagesByTeamMate, team } = useTeamDashboardContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!allMessages.length) {
|
|
||||||
toast("no message to play");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only play if it's incoming, as I listened to my message before sending it
|
|
||||||
if (allMessages[0].receiverUserId == currUser.uid) {
|
|
||||||
console.log("playing message");
|
|
||||||
console.log(allMessages[0]);
|
|
||||||
|
|
||||||
// select the user
|
|
||||||
setSelectedTeamMember(allMessages[0].senderUserId);
|
|
||||||
|
|
||||||
// start player on the bottom
|
|
||||||
// autoplay message
|
|
||||||
// add to the queue
|
|
||||||
handleAddAudioToQueue([allMessages[0].audioDataUrl]);
|
|
||||||
}
|
|
||||||
}, [allMessages]);
|
|
||||||
|
|
||||||
// manage audio queue
|
|
||||||
useEffect(() => {
|
|
||||||
// every time queue changes,
|
|
||||||
// setPlayerSrc as the next item if there is any to play it
|
|
||||||
|
|
||||||
if (audioQueue && audioQueue.length > 0) {
|
|
||||||
toast("Adding Message to Queue");
|
|
||||||
|
|
||||||
// todo: edge case, if it's the same link again, then still play it somehow
|
|
||||||
if (playerSrc == audioQueue[0]) {
|
|
||||||
// play empty one and then add the next audio file
|
|
||||||
setPlayerSrc("");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setPlayerSrc(audioQueue[0]);
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
setPlayerSrc(audioQueue[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!audioQueue || audioQueue.length == 0) {
|
|
||||||
setPlayerSrc(null);
|
|
||||||
}
|
|
||||||
}, [audioQueue]);
|
|
||||||
|
|
||||||
// show toast when recording
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRecordingAnnouncement || isRecording) {
|
|
||||||
toast.loading("Recording...");
|
|
||||||
} else {
|
|
||||||
toast.dismiss();
|
|
||||||
}
|
|
||||||
}, [isRecording, isRecordingAnnouncement]);
|
|
||||||
|
|
||||||
const value: KeyboardContextInterface = {
|
|
||||||
selectedTeammate,
|
|
||||||
selectTeamMember,
|
|
||||||
addTeamShortcutBinding,
|
|
||||||
isRecording,
|
|
||||||
isRecordingAnnouncement,
|
|
||||||
teamShortcutMappings,
|
|
||||||
|
|
||||||
isMuted,
|
|
||||||
isSilenceMode,
|
|
||||||
muteOrUnmute,
|
|
||||||
silenceOrLivenMode,
|
|
||||||
|
|
||||||
hasRecPermit,
|
|
||||||
|
|
||||||
audioInputDeviceId,
|
|
||||||
audioOutputDeviceId,
|
|
||||||
selectAudioOutput,
|
|
||||||
selectAudioInput,
|
|
||||||
|
|
||||||
inputDevices,
|
|
||||||
outputDevices,
|
|
||||||
|
|
||||||
ctrlDown,
|
|
||||||
|
|
||||||
pastedLink,
|
|
||||||
handleModalType,
|
|
||||||
showModalType,
|
|
||||||
|
|
||||||
handleAddAudioToQueue,
|
|
||||||
};
|
|
||||||
|
|
||||||
function muteOrUnmute() {
|
|
||||||
toast.success(isMuted ? "Unmuted" : "Muted");
|
|
||||||
|
|
||||||
setIsMuted((prevVal) => !prevVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
function silenceOrLivenMode() {
|
|
||||||
toast.success(isSilenceMode ? "Unsilenced" : "Auto listen mode disabled");
|
|
||||||
|
|
||||||
setIseSilenceMode((prevVal) => !prevVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectAudioOutput(deviceId: string) {
|
|
||||||
toast.success("Changed output device");
|
|
||||||
setAudioOutputDevice(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectAudioInput(deviceId: string) {
|
|
||||||
toast.success("Changed input device");
|
|
||||||
setAudioInputDevice(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up audio
|
|
||||||
useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
try {
|
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
|
|
||||||
const inputDevices: MediaDeviceInfo[] = devices.filter(
|
|
||||||
(d) => d.kind == "audioinput"
|
|
||||||
);
|
|
||||||
|
|
||||||
setAudioInputDevice(inputDevices ? inputDevices[0].deviceId : null);
|
|
||||||
setInputDevices(inputDevices);
|
|
||||||
|
|
||||||
const outputDevices: MediaDeviceInfo[] = devices.filter(
|
|
||||||
(d) => d.kind == "audiooutput"
|
|
||||||
);
|
|
||||||
|
|
||||||
setAudioOutputDevice(outputDevices ? outputDevices[0].deviceId : null);
|
|
||||||
setOutputDevices(outputDevices);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
toast.error("Problem in setting up audio devices");
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [hasRecPermit]);
|
|
||||||
|
|
||||||
// first load just force check if permissions enabled
|
|
||||||
// through fake stream
|
|
||||||
// todo: only call this when starting to record and do it with a usecallback
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
// won't work in https!!!
|
|
||||||
navigator.mediaDevices
|
|
||||||
.getUserMedia({ audio: { deviceId: audioInputDeviceId } })
|
|
||||||
.then((stream) => {
|
|
||||||
// stop playing anything
|
|
||||||
stopBothVideoAndAudio(stream);
|
|
||||||
|
|
||||||
console.log("Permission Granted");
|
|
||||||
setHasRecPermit(true);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log("Permission Denied");
|
|
||||||
toast.error(
|
|
||||||
"Please make sure that you have connected a microphone and given permissions."
|
|
||||||
);
|
|
||||||
setHasRecPermit(false);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
toast.error("something went wrong");
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// set recording device
|
|
||||||
useEffect(() => {
|
|
||||||
setRecorder(
|
|
||||||
new MicRecorder({ bitRate: 128, deviceId: audioInputDeviceId })
|
|
||||||
);
|
|
||||||
}, [audioInputDeviceId]); // change it everytime we change the input device
|
|
||||||
|
|
||||||
// SECTION: recording
|
|
||||||
async function startRecording() {
|
|
||||||
recorder
|
|
||||||
.start()
|
|
||||||
.then(() => {
|
|
||||||
// toast.success("started recording");
|
|
||||||
console.log("recording started");
|
|
||||||
})
|
|
||||||
.catch((e) =>
|
|
||||||
toast.error("there was a problem in starting your recording")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopRecording(): Promise<File> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
recorder
|
|
||||||
.stop()
|
|
||||||
.getMp3()
|
|
||||||
.then(([buffer, blob]) => {
|
|
||||||
const blobURL = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const file = new File(buffer, uuidv4() + ".mp3", {
|
|
||||||
type: blob.type,
|
|
||||||
lastModified: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const player = new Audio(URL.createObjectURL(file));
|
|
||||||
player.onended = onEndedPlaying;
|
|
||||||
player.play();
|
|
||||||
|
|
||||||
resolve(file);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
reject(error.message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEndedPlaying(e) {
|
|
||||||
// toast.success("finished playing");
|
|
||||||
|
|
||||||
// remove from queue and the queue manager will handle the rest
|
|
||||||
setAudioQueue((prevQueue) => {
|
|
||||||
let newQueue: string[] = [...prevQueue];
|
|
||||||
newQueue.shift();
|
|
||||||
return newQueue;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyUp = useCallback(
|
|
||||||
(event) => {
|
|
||||||
// if was recording and released R, then stop recording and send message
|
|
||||||
if (event.keyCode == KeyCode.R && selectedTeammate && isRecording) {
|
|
||||||
console.log("stopped recording");
|
|
||||||
setIsRecording(false);
|
|
||||||
|
|
||||||
const currReceiverUserId = selectedTeammate;
|
|
||||||
|
|
||||||
stopRecording()
|
|
||||||
.then((file) => {
|
|
||||||
console.log(file);
|
|
||||||
|
|
||||||
// upload to cloud storage
|
|
||||||
return cloudStorageService.uploadMessageAudioFile(file);
|
|
||||||
})
|
|
||||||
.then((downloadUrl) => {
|
|
||||||
console.log("file is stored: " + downloadUrl);
|
|
||||||
|
|
||||||
// send message to firestore
|
|
||||||
const message = new Message();
|
|
||||||
message.audioDataUrl = downloadUrl;
|
|
||||||
message.senderUserId = currUser.uid;
|
|
||||||
message.receiverUserId = currReceiverUserId;
|
|
||||||
|
|
||||||
return sendService.sendMessage(message);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("clip sent");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
toast.error("Problem in sending clip");
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("sending message to " + selectedTeammate);
|
|
||||||
|
|
||||||
handleResetView();
|
|
||||||
} else if (event.keyCode == KeyCode.Ctrl) {
|
|
||||||
setCtrlDown(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isRecording, selectedTeammate]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyboardShortcut = useCallback(
|
|
||||||
(event) => {
|
|
||||||
// no need to monitor repeats
|
|
||||||
if (event.repeat) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle for when in modals, don't want any of this crap
|
|
||||||
if (showModalType != ShowModalType.na) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// recording
|
|
||||||
if (event.keyCode == KeyCode.R && !ctrlDown) {
|
|
||||||
if (!audioInputDeviceId) {
|
|
||||||
toast.error("No microphone selected");
|
|
||||||
} else if (!hasRecPermit) {
|
|
||||||
toast.error(
|
|
||||||
"You did not allow microphone permissions in your browser!"
|
|
||||||
);
|
|
||||||
} else if (!selectedTeammate) {
|
|
||||||
toast.error("Please select a team member or announcements first");
|
|
||||||
} else if (isMuted) {
|
|
||||||
toast.error("You are muted!");
|
|
||||||
} else {
|
|
||||||
// alright now you are good to go
|
|
||||||
console.log("started recording");
|
|
||||||
setIsRecording(true);
|
|
||||||
startRecording();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if we have a valid user for such a shortcut, then go ahead...otherwise
|
|
||||||
else if (teamShortcutMappings[event.keyCode]) {
|
|
||||||
setSelectedTeamMember(teamShortcutMappings[event.keyCode]);
|
|
||||||
} else if (event.keyCode == KeyCode.Escape) {
|
|
||||||
handleResetView();
|
|
||||||
} else if (event.keyCode == KeyCode.Space) {
|
|
||||||
// listen to last message in convo
|
|
||||||
if (isSilenceMode) {
|
|
||||||
toast.error("you are in silence mode, please disable it first");
|
|
||||||
} else if (!selectedTeammate) {
|
|
||||||
toast.error("select a team member first to play");
|
|
||||||
} else {
|
|
||||||
// alright now you are good to play last message chunk in conversation with selected user
|
|
||||||
|
|
||||||
const arrayMessagesforUser: Message[] =
|
|
||||||
messagesByTeamMate.get(selectedTeammate);
|
|
||||||
if (
|
|
||||||
messagesByTeamMate.has(selectedTeammate) &&
|
|
||||||
arrayMessagesforUser &&
|
|
||||||
arrayMessagesforUser.length > 0
|
|
||||||
) {
|
|
||||||
var convoChunk: string[] = [];
|
|
||||||
console.log("playing from this cache");
|
|
||||||
|
|
||||||
// wait for x changes, and then break adding to the queue...I want to hear the past loop of conversation maybe
|
|
||||||
const maxConvoChanges: number = 1;
|
|
||||||
var convoChangeCount: number = 0;
|
|
||||||
|
|
||||||
var currTalkerId = arrayMessagesforUser[0].senderUserId;
|
|
||||||
for (const audioMessage of arrayMessagesforUser) {
|
|
||||||
if (audioMessage.senderUserId != currTalkerId) {
|
|
||||||
convoChangeCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// heard enough of this convo
|
|
||||||
if (convoChangeCount == maxConvoChanges) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
convoChunk.push(audioMessage.audioDataUrl);
|
|
||||||
currTalkerId = audioMessage.senderUserId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// reverse the chunk so that I listen to the messages in order
|
|
||||||
convoChunk.reverse();
|
|
||||||
|
|
||||||
// add convo chunk to the queue player
|
|
||||||
handleAddAudioToQueue(convoChunk);
|
|
||||||
} else {
|
|
||||||
toast("nothing to play");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (event.keyCode == KeyCode.Ctrl) {
|
|
||||||
handleResetView();
|
|
||||||
|
|
||||||
setCtrlDown(true);
|
|
||||||
} else if (event.keyCode == KeyCode.Q && ctrlDown) {
|
|
||||||
toast("navigating to create google meet");
|
|
||||||
|
|
||||||
window.open(new URL("https://meet.google.com/"), "_blank");
|
|
||||||
} else if (event.keyCode == KeyCode.V && ctrlDown) {
|
|
||||||
toast("pasting link");
|
|
||||||
|
|
||||||
navigator.clipboard
|
|
||||||
.readText()
|
|
||||||
.then((text) => {
|
|
||||||
console.log("Pasted content: ", text);
|
|
||||||
|
|
||||||
// check that the link is valid
|
|
||||||
if (!isValidHttpUrl(text)) {
|
|
||||||
toast.error("Not a valid link");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set it as we should show modal regardless now
|
|
||||||
setPastedLink(text);
|
|
||||||
|
|
||||||
// see which modal to go to based on room vs. attachment
|
|
||||||
// https://meet.google.com/soc-ebwc-rkt
|
|
||||||
if (text.includes("meet.google.com")) {
|
|
||||||
setShowModalType(ShowModalType.createRoom);
|
|
||||||
} else {
|
|
||||||
setShowModalType(ShowModalType.createLink);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("Failed to read clipboard contents: ", err);
|
|
||||||
toast.error("Please enable permissions for clipboard");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// toast("Invalid keyboard shortcut.");
|
|
||||||
console.log("no shortcut logged");
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo play message if pressing space
|
|
||||||
|
|
||||||
// todo if we press the same shortcut twice, deactive selected user
|
|
||||||
},
|
|
||||||
[
|
|
||||||
selectedTeammate,
|
|
||||||
audioInputDeviceId,
|
|
||||||
hasRecPermit,
|
|
||||||
isMuted,
|
|
||||||
teamShortcutMappings,
|
|
||||||
ctrlDown,
|
|
||||||
showModalType,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleResetView() {
|
|
||||||
setSelectedTeamMember(null);
|
|
||||||
setShowModalType(ShowModalType.na);
|
|
||||||
|
|
||||||
setAudioQueue([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: shortcut handlers need to be updated as the function has to have the fresh state
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("updating event listeners");
|
|
||||||
document.addEventListener("keydown", handleKeyboardShortcut);
|
|
||||||
document.addEventListener("keyup", handleKeyUp);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", handleKeyboardShortcut);
|
|
||||||
document.removeEventListener("keyup", handleKeyUp);
|
|
||||||
};
|
|
||||||
}, [handleKeyboardShortcut, handleKeyUp]);
|
|
||||||
|
|
||||||
function addTeamShortcutBinding(keyCode: number, userId: string) {
|
|
||||||
setTeamShortcutMappings((prevMap) => ({ ...prevMap, [keyCode]: userId }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTeamMember(userId: string) {
|
|
||||||
setSelectedTeamMember(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [playerSrc, setPlayerSrc] = useState<string>(null);
|
|
||||||
|
|
||||||
function handleAddAudioToQueue(
|
|
||||||
urls: string[],
|
|
||||||
clearEverythingElse: boolean = false
|
|
||||||
) {
|
|
||||||
if (clearEverythingElse) {
|
|
||||||
setAudioQueue(urls);
|
|
||||||
} else {
|
|
||||||
setAudioQueue((prevQueue) => [...prevQueue, ...urls]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startAnnouncement() {
|
|
||||||
setIsRecordingAnnouncement(true);
|
|
||||||
|
|
||||||
startRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendAnnouncement() {
|
|
||||||
setIsRecordingAnnouncement(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const file = await stopRecording();
|
|
||||||
|
|
||||||
// confirm with user that he wants to make the announcement
|
|
||||||
// before sending and all
|
|
||||||
// show this 2 seconds after playing entire message
|
|
||||||
setTimeout(async () => {
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"Send this announcement to team? OR just cancel and create a new one to restate something."
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const downloadUrl = await cloudStorageService.uploadMessageAudioFile(
|
|
||||||
file
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("file is stored: " + downloadUrl);
|
|
||||||
|
|
||||||
// create announcement object and send to firestore
|
|
||||||
const newAnnounce = new Announcement(
|
|
||||||
downloadUrl,
|
|
||||||
team.id,
|
|
||||||
currUser.uid
|
|
||||||
);
|
|
||||||
|
|
||||||
await sendService.sendAnnouncement(newAnnounce);
|
|
||||||
|
|
||||||
toast.success("clip sent");
|
|
||||||
} else {
|
|
||||||
toast.error("cancelled announcement");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("problem in sending");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyMap: KeyMap = {
|
|
||||||
RECORD_ANNOUNCEMENT: "a",
|
|
||||||
STOP_RECORDING_ANNOUNCEMENT: {
|
|
||||||
name: "Stop recording",
|
|
||||||
sequence: "a",
|
|
||||||
action: "keyup",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const handlers = {
|
|
||||||
RECORD_ANNOUNCEMENT: startAnnouncement,
|
|
||||||
STOP_RECORDING_ANNOUNCEMENT: sendAnnouncement,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<KeyboardContext.Provider value={value}>
|
|
||||||
<GlobalHotKeys keyMap={keyMap} handlers={handlers} />
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{/* player for audio messages */}
|
|
||||||
{playerSrc && (
|
|
||||||
<AudioPlayer
|
|
||||||
autoPlay
|
|
||||||
src={playerSrc}
|
|
||||||
onPlay={(e) => console.log("onPlay")}
|
|
||||||
showSkipControls={true}
|
|
||||||
onEnded={onEndedPlaying}
|
|
||||||
className="w-screen flex flex-row"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</KeyboardContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useKeyboardContext() {
|
|
||||||
return useContext(KeyboardContext);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Timestamp } from "firebase/firestore";
|
|
||||||
|
|
||||||
export default class Announcement {
|
|
||||||
id: string;
|
|
||||||
teamId: string;
|
|
||||||
audioDataUrl: string; // link to cloud storage file
|
|
||||||
|
|
||||||
state: AnnouncementState = AnnouncementState.active;
|
|
||||||
|
|
||||||
createdByUserId: string;
|
|
||||||
createdDate: Timestamp;
|
|
||||||
lastUpdatedDate: Timestamp;
|
|
||||||
|
|
||||||
constructor(_audioUrl: string, _teamId: string, _createdByUserId: string) {
|
|
||||||
this.audioDataUrl = _audioUrl;
|
|
||||||
this.createdByUserId = _createdByUserId;
|
|
||||||
this.teamId = _teamId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AnnouncementState {
|
|
||||||
active = "active",
|
|
||||||
resolved = "resolved",
|
|
||||||
deleted = "deleted",
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { Timestamp } from "firebase/firestore";
|
|
||||||
|
|
||||||
export default class Link {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
link: string; //url for file
|
|
||||||
|
|
||||||
state: LinkState = LinkState.active;
|
|
||||||
type: LinkType;
|
|
||||||
|
|
||||||
teamId: string;
|
|
||||||
// if it's not a teamAttachment, then have a list of members who it's for
|
|
||||||
recipients: string[]; // userIds
|
|
||||||
|
|
||||||
createdByUserId: string;
|
|
||||||
createdDate: Timestamp;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
_name: string,
|
|
||||||
_description: string,
|
|
||||||
_link: string,
|
|
||||||
_teamId: string,
|
|
||||||
recipientsArr: string[],
|
|
||||||
_createdByUserId: string
|
|
||||||
) {
|
|
||||||
this.name = _name;
|
|
||||||
this.description = _description;
|
|
||||||
this.link = _link;
|
|
||||||
this.teamId = _teamId;
|
|
||||||
this.recipients = recipientsArr;
|
|
||||||
this.createdByUserId = _createdByUserId;
|
|
||||||
|
|
||||||
if (!recipientsArr || recipientsArr?.length == 0) {
|
|
||||||
this.recipients = null;
|
|
||||||
} else {
|
|
||||||
this.recipients = recipientsArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.type = Link.getLinkType(_link);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getLinkType(url: string): LinkType {
|
|
||||||
if (url.includes(LinkType.github)) {
|
|
||||||
return LinkType.github;
|
|
||||||
} else if (url.includes(LinkType.atlassian)) {
|
|
||||||
return LinkType.atlassian;
|
|
||||||
} else if (
|
|
||||||
url.includes(LinkType.googleDrive) ||
|
|
||||||
url.includes("docs.google")
|
|
||||||
) {
|
|
||||||
return LinkType.googleDrive;
|
|
||||||
} else if (
|
|
||||||
url.includes(".png") ||
|
|
||||||
url.includes(".jpg") ||
|
|
||||||
url.includes(".svg") ||
|
|
||||||
url.includes(".gif") ||
|
|
||||||
url.includes(LinkType.pastePics)
|
|
||||||
) {
|
|
||||||
return LinkType.image;
|
|
||||||
} else if (url.includes(LinkType.pdf)) {
|
|
||||||
return LinkType.pdf;
|
|
||||||
} else if (url.includes(LinkType.codePile)) {
|
|
||||||
return LinkType.codePile;
|
|
||||||
} else {
|
|
||||||
return LinkType.default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LinkState {
|
|
||||||
active = "active",
|
|
||||||
archived = "archived",
|
|
||||||
deleted = "deleted",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LinkType {
|
|
||||||
default = "default",
|
|
||||||
github = "github",
|
|
||||||
atlassian = "atlassian",
|
|
||||||
googleDrive = "drive.google",
|
|
||||||
onedrive = "onedrive",
|
|
||||||
image = "image",
|
|
||||||
pdf = "pdf",
|
|
||||||
codePile = "codepile",
|
|
||||||
pastePics = "paste.pics",
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Timestamp } from "firebase/firestore";
|
|
||||||
|
|
||||||
export class Message {
|
|
||||||
id: string;
|
|
||||||
audioDataUrl: string;
|
|
||||||
|
|
||||||
senderUserId: string;
|
|
||||||
receiverUserId: string;
|
|
||||||
|
|
||||||
senderReceiver: string[]; // composite to make querying easier in the future
|
|
||||||
|
|
||||||
createdDate: Timestamp;
|
|
||||||
|
|
||||||
// firstListenDate: Timestamp;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Timestamp } from "firebase/firestore";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
export default class OfficeRoom {
|
|
||||||
id: string = uuidv4();
|
|
||||||
|
|
||||||
teamId: string;
|
|
||||||
|
|
||||||
name: string; // entrance, kitchen, etc.
|
|
||||||
|
|
||||||
createdDate: Timestamp;
|
|
||||||
createdByUserId: string;
|
|
||||||
|
|
||||||
lastUpdatedDate: Timestamp;
|
|
||||||
|
|
||||||
members: string[] = []; // id's of users in the office room
|
|
||||||
|
|
||||||
state: OfficeRoomState;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
_name: string,
|
|
||||||
_teamId: string,
|
|
||||||
_createdBy: string,
|
|
||||||
_state: OfficeRoomState = OfficeRoomState.idle
|
|
||||||
) {
|
|
||||||
this.name = _name;
|
|
||||||
this.teamId = _teamId;
|
|
||||||
this.createdByUserId = _createdBy;
|
|
||||||
this.state = _state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum OfficeRoomState {
|
|
||||||
active = "active",
|
|
||||||
idle = "idle",
|
|
||||||
archived = "archived",
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Timestamp } from "firebase/firestore";
|
|
||||||
|
|
||||||
export default class Room {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
link: string; // google meet link for now
|
|
||||||
|
|
||||||
members: string[] = []; //userIds of "mandatory"/invited people including the person who created it
|
|
||||||
|
|
||||||
membersInRoom: string[] = [];
|
|
||||||
|
|
||||||
attachments: string[] = []; // the links themselves (NOT Ids)...there will be duplicate entries in the attachments table which will be created
|
|
||||||
|
|
||||||
type: RoomType;
|
|
||||||
status: RoomStatus = RoomStatus.empty;
|
|
||||||
|
|
||||||
approximateDateTime: string; // vaguely say when the meeting should be...give user pointers
|
|
||||||
scheduledDateTime: Timestamp;
|
|
||||||
|
|
||||||
// scheduledJsDateTime(): Date {
|
|
||||||
// return this.scheduledDateTime.toDate();
|
|
||||||
// }
|
|
||||||
|
|
||||||
createdDate: Timestamp;
|
|
||||||
|
|
||||||
// createdJsDate(): Date {
|
|
||||||
// return this.createdDate.toDate();
|
|
||||||
// }
|
|
||||||
|
|
||||||
createdByUserId: string;
|
|
||||||
|
|
||||||
teamId: string;
|
|
||||||
|
|
||||||
lastUpdatedDate: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RoomType {
|
|
||||||
now = "now",
|
|
||||||
scheduled = "scheduled", // one time sort of standard meeting
|
|
||||||
recurring = "recurring", //daily standup
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RoomStatus {
|
|
||||||
live = "live",
|
|
||||||
empty = "empty",
|
|
||||||
archived = "archived", // user marks it over
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Timestamp } from "firebase/firestore";
|
|
||||||
|
|
||||||
export class Team {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
status: TeamStatus;
|
|
||||||
|
|
||||||
allowedUserCount: number = 2;
|
|
||||||
// subscriptionPlan: TeamSubscriptionPlan = TeamSubscriptionPlan.basic
|
|
||||||
|
|
||||||
companySite: string;
|
|
||||||
|
|
||||||
createdByUserId: string;
|
|
||||||
createdDate: Timestamp;
|
|
||||||
|
|
||||||
lastUpdatedDate: Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TeamStatus {
|
|
||||||
created = "created",
|
|
||||||
deactivated = "deactivated",
|
|
||||||
deleted = "deleted",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TeamSubscriptionPlan {
|
|
||||||
free = "free",
|
|
||||||
basic = "basic",
|
|
||||||
pro = "pro",
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Timestamp } from "firebase/firestore";
|
|
||||||
|
|
||||||
export class TeamMember {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
teamId: string;
|
|
||||||
|
|
||||||
inviteEmailAddress: string;
|
|
||||||
invitedByUserId: string;
|
|
||||||
|
|
||||||
role: TeamMemberRole
|
|
||||||
status: TeamMemberStatus
|
|
||||||
|
|
||||||
createdDate: Timestamp
|
|
||||||
lastUpdatedDate: Timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TeamMemberRole {
|
|
||||||
admin = "admin"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TeamMemberStatus {
|
|
||||||
invited = "invited",
|
|
||||||
activated = "activated",
|
|
||||||
deleted = "deleted"
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ import Announcements from "../components/demo/Announcements";
|
|||||||
import Rooms from "../components/demo/Rooms";
|
import Rooms from "../components/demo/Rooms";
|
||||||
import VoiceLineConceptDemo from "../components/demo/VoiceLineConceptDemo";
|
import VoiceLineConceptDemo from "../components/demo/VoiceLineConceptDemo";
|
||||||
|
|
||||||
import MainLogo from "../components/MainLogo";
|
import MainLogo from "../components/Logo/MainLogo";
|
||||||
import { Divider, Tooltip, Image as AntDImage } from "antd";
|
import { Divider, Tooltip, Image as AntDImage } from "antd";
|
||||||
import LangingPageLayout from "../components/Layouts/LandingPageLayout";
|
import LangingPageLayout from "../components/Layouts/LandingPageLayout";
|
||||||
import LandingPageActionBar from "../components/demo/LandingPageActionBar";
|
import LandingPageActionBar from "../components/demo/LandingPageActionBar";
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import Conversations from "../../components/v2/Conversations";
|
import Conversations from "../../components/MainTabsOrPages/Conversations";
|
||||||
import Done from "../../components/v2/Done";
|
import Done from "../../components/MainTabsOrPages/Done";
|
||||||
import Drawer from "../../components/v2/Drawer";
|
import Drawer from "../../components/MainTabsOrPages/Drawer";
|
||||||
import Header from "../../components/v2/Header";
|
import Header from "../../components/MainTabsOrPages/Header";
|
||||||
import Later from "../../components/v2/Later";
|
import Later from "../../components/MainTabsOrPages/Later";
|
||||||
import Sidebar from "../../components/v2/Sidebar";
|
import Sidebar from "../../components/MainTabsOrPages/Sidebar";
|
||||||
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
||||||
import CreateConversation from "../../components/v2/CreateConversation";
|
import CreateConversation from "../../components/FullPageExperiences/CreateConversation.tsx";
|
||||||
import { FaArrowLeft } from "react-icons/fa";
|
import { FaArrowLeft } from "react-icons/fa";
|
||||||
import KeyboardShortcutHandler from "../../components/v2/KeyboardShortcutHandler";
|
import KeyboardShortcutHandler from "../../components/MainTabsOrPages/KeyboardShortcutHandler";
|
||||||
import SearchResults from "../../components/v2/SearchResults";
|
import SearchResults from "../../components/FullPageExperiences/SearchResults";
|
||||||
import { QueryRoutes, Routes } from "@nirvana/common/helpers/routes";
|
import { QueryRoutes, Routes } from "@nirvana/common/helpers/routes";
|
||||||
|
|
||||||
export default function Me() {
|
export default function Me() {
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Header from "../../components/Dashboard/Header";
|
|
||||||
import Rooms from "../../components/Dashboard/Rooms";
|
|
||||||
import TeamVoiceLine from "../../components/Dashboard/TeamVoiceLine";
|
|
||||||
import BackgroundLayout from "../../components/Layouts/BackgroundLayout";
|
|
||||||
import Loading from "../../components/Loading";
|
|
||||||
import AudioContextProvider from "../../contexts/keyboardContext";
|
|
||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
import {
|
|
||||||
TeamDashboardContextProvider,
|
|
||||||
useTeamDashboardContext,
|
|
||||||
} from "../../contexts/teamDashboardContext";
|
|
||||||
import { Team } from "../../models/team";
|
|
||||||
import { TeamMemberStatus } from "../../models/teamMember";
|
|
||||||
import { UserStatus } from "../../models/user";
|
|
||||||
import TeamService from "../../services/teamService";
|
|
||||||
import UserService from "../../services/userService";
|
|
||||||
import Announcements from "../../components/Dashboard/Announcements";
|
|
||||||
import Links from "../../components/Dashboard/Links";
|
|
||||||
import Office from "../../components/Dashboard/Office";
|
|
||||||
import ShortcutHelpModal from "../../components/Modals/ShortcutHelpModal";
|
|
||||||
|
|
||||||
// User has switched back to the tab
|
|
||||||
const onFocus = () => {
|
|
||||||
console.log("Tab is in focus");
|
|
||||||
};
|
|
||||||
|
|
||||||
// User has switched away from the tab (AKA tab is hidden)
|
|
||||||
const onBlur = () => {
|
|
||||||
console.log("Tab is blurred");
|
|
||||||
};
|
|
||||||
|
|
||||||
const alertUserAboutClosing = (ev) => {
|
|
||||||
// change user status to offline
|
|
||||||
|
|
||||||
ev.preventDefault();
|
|
||||||
return (ev.returnValue = "are you sure?");
|
|
||||||
};
|
|
||||||
|
|
||||||
const userService = new UserService();
|
|
||||||
|
|
||||||
function TeamDashboard() {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const router = useRouter();
|
|
||||||
const teamDashboardContext = useTeamDashboardContext();
|
|
||||||
|
|
||||||
// user goes offline
|
|
||||||
const handleTabClosing = () => {
|
|
||||||
// change status of user
|
|
||||||
userService.updateUserStatus(currUser.uid, UserStatus.offline);
|
|
||||||
};
|
|
||||||
|
|
||||||
// shows browser alert to warn user of exiting
|
|
||||||
const alertUserAboutClosing = (event: any) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.returnValue = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// set the document title based on the team
|
|
||||||
// todo: update based on who sent you a message
|
|
||||||
document.title = teamDashboardContext.team.name;
|
|
||||||
|
|
||||||
window.addEventListener("focus", onFocus);
|
|
||||||
window.addEventListener("blur", onBlur);
|
|
||||||
// Calls onFocus when the window first loads
|
|
||||||
onFocus();
|
|
||||||
|
|
||||||
// set user to online until he closes tab/window
|
|
||||||
userService.updateUserStatus(currUser.uid, UserStatus.online);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", alertUserAboutClosing);
|
|
||||||
window.addEventListener("unload", handleTabClosing);
|
|
||||||
return () => {
|
|
||||||
// Specify how to clean up after this effect:
|
|
||||||
window.removeEventListener("focus", onFocus);
|
|
||||||
window.removeEventListener("blur", onBlur);
|
|
||||||
window.removeEventListener("beforeunload", alertUserAboutClosing);
|
|
||||||
window.removeEventListener("unload", handleTabClosing);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShortcutHelpModal />
|
|
||||||
|
|
||||||
<div className="container mx-auto py-10 lg:px-10 flex flex-col space-y-5">
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<div className="flex flex-row items-start lg:space-x-5 h-[56rem]">
|
|
||||||
<div className="hidden lg:flex flex-col max-w-sm space-y-5 h-full">
|
|
||||||
<Office />
|
|
||||||
|
|
||||||
<TeamVoiceLine />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col space-y-5 flex-1 h-full">
|
|
||||||
<Announcements />
|
|
||||||
|
|
||||||
<Rooms />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Links />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TeamDashboardWrapper() {
|
|
||||||
return (
|
|
||||||
<TeamDashboardContextProvider>
|
|
||||||
<AudioContextProvider>
|
|
||||||
<BackgroundLayout>
|
|
||||||
<TeamDashboard />
|
|
||||||
</BackgroundLayout>
|
|
||||||
</AudioContextProvider>
|
|
||||||
</TeamDashboardContextProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|
||||||
// basic check if this team exists
|
|
||||||
|
|
||||||
// todo check if authenticated
|
|
||||||
|
|
||||||
// todo check if this user is in this team
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
import { Divider } from "antd";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Loading from "../../../components/Loading";
|
|
||||||
import {
|
|
||||||
TeamDashboardContextProvider,
|
|
||||||
useTeamDashboardContext,
|
|
||||||
} from "../../../contexts/teamDashboardContext";
|
|
||||||
import {
|
|
||||||
TeamMember,
|
|
||||||
TeamMemberRole,
|
|
||||||
TeamMemberStatus,
|
|
||||||
} from "../../../models/teamMember";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { Team, TeamStatus } from "../../../models/team";
|
|
||||||
|
|
||||||
import {
|
|
||||||
FaMoneyBillWave,
|
|
||||||
FaTrash,
|
|
||||||
FaPaperPlane,
|
|
||||||
FaArrowLeft,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import TeamService from "../../../services/teamService";
|
|
||||||
import Router from "next/router";
|
|
||||||
import Moment from "react-moment";
|
|
||||||
|
|
||||||
export default function TeamAdminWrapper() {
|
|
||||||
return (
|
|
||||||
<TeamDashboardContextProvider>
|
|
||||||
<TeamAdmin />
|
|
||||||
</TeamDashboardContextProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialSite: string = "https://";
|
|
||||||
|
|
||||||
function renderTeamMemberStatus(status: TeamMemberStatus) {
|
|
||||||
switch (status) {
|
|
||||||
case TeamMemberStatus.invited:
|
|
||||||
return (
|
|
||||||
<span className="text-gray-700 ml-1 bg-gray-200 p-1 rounded-md text-xs font-bold mt-2">
|
|
||||||
invited
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case TeamMemberStatus.activated:
|
|
||||||
return (
|
|
||||||
<span className="text-sky-700 ml-1 bg-sky-200 p-1 rounded-md text-xs font-bold mt-2">
|
|
||||||
active
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case TeamMemberStatus.deleted:
|
|
||||||
return (
|
|
||||||
<span className="text-red-700 ml-1 bg-red-200 p-1 rounded-md text-xs font-bold mt-2">
|
|
||||||
deleted
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const teamService = new TeamService();
|
|
||||||
|
|
||||||
function TeamAdmin() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { teamid } = router.query;
|
|
||||||
const { team, userTeamMember, teamMembers, user } = useTeamDashboardContext();
|
|
||||||
|
|
||||||
const [error, setError] = useState<string>("");
|
|
||||||
const [teamName, setTeamName] = useState<string>(team.name);
|
|
||||||
const [companySite, setcompanySite] = useState<string>(team.companySite);
|
|
||||||
const [inviteEmail, setInviteEmail] = useState<string>("");
|
|
||||||
const [loading, setLoading] = useState<Boolean>(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
// have checks for team and part of team or not in context
|
|
||||||
try {
|
|
||||||
// if not admin, I shouldn't be here
|
|
||||||
if (userTeamMember.role != TeamMemberRole.admin) {
|
|
||||||
toast.error("You are not allowed here!");
|
|
||||||
router.push("/teams");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("something went wrong in admin team page");
|
|
||||||
router.push("/teams");
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleSubmitTeamUpdate(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (!teamName) {
|
|
||||||
setError("You must input a team name!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updatedTeam = new Team();
|
|
||||||
updatedTeam.id = team.id;
|
|
||||||
updatedTeam.name = teamName;
|
|
||||||
updatedTeam.createdByUserId = user.id;
|
|
||||||
updatedTeam.status = TeamStatus.created;
|
|
||||||
|
|
||||||
if (companySite != initialSite) {
|
|
||||||
// make sure to put null in database for the team
|
|
||||||
updatedTeam.companySite = companySite;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update team name and company site if it changed
|
|
||||||
await teamService.updateTeam(updatedTeam);
|
|
||||||
|
|
||||||
// take user back to team dashboard
|
|
||||||
router.push("/teams/" + team.id);
|
|
||||||
} catch (error) {
|
|
||||||
setError(error.message);
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmitNewTeamMember(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (!inviteEmail) {
|
|
||||||
setError("You must input a valid email!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newInviteTeamMember = new TeamMember();
|
|
||||||
newInviteTeamMember.inviteEmailAddress = inviteEmail;
|
|
||||||
newInviteTeamMember.invitedByUserId = user.id;
|
|
||||||
newInviteTeamMember.status = TeamMemberStatus.invited;
|
|
||||||
newInviteTeamMember.teamId = team.id;
|
|
||||||
|
|
||||||
await teamService.createTeamInvite(newInviteTeamMember);
|
|
||||||
|
|
||||||
Router.reload();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("something went wrong");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDeleteTeamMember(e, teamMemberId: string) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await teamService.updateTeamMemberStatus(
|
|
||||||
teamMemberId,
|
|
||||||
TeamMemberStatus.deleted
|
|
||||||
);
|
|
||||||
|
|
||||||
Router.reload();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("unable to delete team member");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleInviteTeamMember(e, teamMemberId: string) {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await teamService.updateTeamMemberStatus(
|
|
||||||
teamMemberId,
|
|
||||||
TeamMemberStatus.invited
|
|
||||||
);
|
|
||||||
|
|
||||||
Router.reload();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("unable to delete team member");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get count of all active and invited users...
|
|
||||||
const activeOrInvited = teamMembers.filter(
|
|
||||||
(tmember) =>
|
|
||||||
tmember.status == TeamMemberStatus.invited ||
|
|
||||||
tmember.status == TeamMemberStatus.activated
|
|
||||||
);
|
|
||||||
const teamSpotsRemaining =
|
|
||||||
(team.allowedUserCount || 0) - activeOrInvited.length - 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto max-w-screen-sm m-10 bg-white p-10 rounded-lg shadow-md">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmitTeamUpdate}
|
|
||||||
className="flex flex-col space-y-5"
|
|
||||||
>
|
|
||||||
{/* header */}
|
|
||||||
<div className="text-lg flex flex-row items-center space-x-2">
|
|
||||||
<FaArrowLeft
|
|
||||||
className="hover:cursor-pointer"
|
|
||||||
onClick={() => router.push("/teams/" + teamid)}
|
|
||||||
/>{" "}
|
|
||||||
<span>Manage Team</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <span className="text-red-300 text-md">{error}</span>}
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<span className="flex flex-col items-start">
|
|
||||||
<span className="text-md">Team Name</span>
|
|
||||||
<span className="text-gray-300 text-xs mb-2">
|
|
||||||
Everyone in your team will see this.
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
placeholder="ex. AirBnB"
|
|
||||||
className="w-full rounded-lg bg-gray-50 p-3"
|
|
||||||
value={teamName}
|
|
||||||
onChange={(e) => setTeamName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col items-start">
|
|
||||||
<span className="text-md">Company Site</span>
|
|
||||||
<span className="text-gray-300 text-xs mb-2">optional</span>
|
|
||||||
<input
|
|
||||||
placeholder="ex. https://"
|
|
||||||
className="w-full rounded-lg bg-gray-50 p-3"
|
|
||||||
value={companySite}
|
|
||||||
onChange={(e) => setcompanySite(e.target.value)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<span className="flex flex-row justify-end space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => router.push("/teams/" + team.id)}
|
|
||||||
className="bg-gray-100 py-2 px-5 rounded text-gray-400"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="text-sm text-white font-semibold py-2 px-5 bg-teal-500 rounded"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form
|
|
||||||
className="flex flex-col space-y-5"
|
|
||||||
onSubmit={handleSubmitNewTeamMember}
|
|
||||||
>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<span className="flex flex-row justify-start items-start">
|
|
||||||
<FaMoneyBillWave className="text-5xl text-green-500" />
|
|
||||||
|
|
||||||
<span className="flex flex-col justify-start ml-2">
|
|
||||||
<span className="text-md ">Billing</span>
|
|
||||||
<span className="text-md text-gray-500 mr-20">
|
|
||||||
Please email{" "}
|
|
||||||
<button className="underline decoration-teal-500 text-brown-500">
|
|
||||||
usenirvana@gmail.com
|
|
||||||
</button>{" "}
|
|
||||||
to open more spots for your team.
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="ml-auto text-center text-orange-700 bg-orange-200 p-1 rounded-md text-sm font-bold mt-2">
|
|
||||||
{teamSpotsRemaining} spots remaining
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<span className="text-md">Team</span>
|
|
||||||
|
|
||||||
{/* current user */}
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<img src={user.avatarUrl} alt="asdf" className="rounded-full w-12" />
|
|
||||||
<span className="flex flex-col items-start ml-2">
|
|
||||||
<span className="text-md text-gray-500">
|
|
||||||
{user.firstName + " " + user.lastName}
|
|
||||||
|
|
||||||
<span className="text-green-700 ml-1 bg-green-200 p-1 rounded-md text-xs font-bold mt-2">
|
|
||||||
admin
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{renderTeamMemberStatus(userTeamMember.status)}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-200">{user.emailAddress}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* mapping all team members */}
|
|
||||||
{teamMembers.map((tmember, i) => {
|
|
||||||
return (
|
|
||||||
<div key={i} className="flex flex-row items-center">
|
|
||||||
<div className="rounded-full w-12 animate-pulse bg-gray-200 h-12" />
|
|
||||||
|
|
||||||
<span className="flex flex-col items-start ml-2">
|
|
||||||
<span className="text-md text-gray-500">
|
|
||||||
{tmember.inviteEmailAddress}
|
|
||||||
|
|
||||||
{renderTeamMemberStatus(tmember.status)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* todo: date joined */}
|
|
||||||
{/* <Moment date={tmember} className="text-xs text-gray-200" /> */}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{tmember.status == TeamMemberStatus.deleted &&
|
|
||||||
teamSpotsRemaining > 0 ? (
|
|
||||||
<button
|
|
||||||
onClick={(e) => handleInviteTeamMember(e, tmember.id)}
|
|
||||||
className="ml-auto bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40"
|
|
||||||
>
|
|
||||||
<FaPaperPlane className="text-sm text-teal-500 " />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{tmember.status == TeamMemberStatus.invited ||
|
|
||||||
tmember.status == TeamMemberStatus.activated ? (
|
|
||||||
<button
|
|
||||||
onClick={(e) => handleDeleteTeamMember(e, tmember.id)}
|
|
||||||
className="bg-orange-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40"
|
|
||||||
>
|
|
||||||
<FaTrash className="text-sm text-orange-500 " />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* input for adding people */}
|
|
||||||
{teamSpotsRemaining > 0 ? (
|
|
||||||
<>
|
|
||||||
<span className="flex flex-col items-start">
|
|
||||||
<span className="text-md">Add Members</span>
|
|
||||||
<span className="text-red-300 text-md mb-2">
|
|
||||||
IMPORTANT: Tell them to sign up at{" "}
|
|
||||||
<span className="underline decoration-teal-500">
|
|
||||||
usenirvana.com{" "}
|
|
||||||
</span>{" "}
|
|
||||||
using this same email address.
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
placeholder="ex. [email protected]"
|
|
||||||
className="w-full rounded-lg bg-gray-50 p-3"
|
|
||||||
value={inviteEmail}
|
|
||||||
onChange={(e) => setInviteEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-row justify-end space-x-2">
|
|
||||||
{inviteEmail ? (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="text-sm text-white font-semibold py-2 px-5 bg-teal-500 rounded"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="text-sm text-white font-semibold py-2 px-5 bg-gray-200 rounded"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import { Divider } from "antd";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
import { Team, TeamStatus } from "../../models/team";
|
|
||||||
import { User, UserStatus } from "../../models/user";
|
|
||||||
import TeamService from "../../services/teamService";
|
|
||||||
|
|
||||||
const initialSite: string = "https://";
|
|
||||||
|
|
||||||
const teamService = new TeamService();
|
|
||||||
|
|
||||||
export default function CreateTeam() {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const [error, setError] = useState<string>("");
|
|
||||||
const [loading, setLoading] = useState<Boolean>(true);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
try {
|
|
||||||
// if not authenticated, take user to the login
|
|
||||||
if (!currUser) {
|
|
||||||
console.log(
|
|
||||||
"not authenticated...routing from dashboard to teams home"
|
|
||||||
);
|
|
||||||
router.push("/teams/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo if user is in a team already, notify them to make sure
|
|
||||||
// buttt most likely will already be routed to the correct place from the router...don't do the router's job
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
router.push("/teams/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (!teamName) {
|
|
||||||
setError("You must input a team name!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const team = new Team();
|
|
||||||
team.name = teamName;
|
|
||||||
team.createdByUserId = currUser.uid;
|
|
||||||
team.status = TeamStatus.created;
|
|
||||||
team.allowedUserCount = 2;
|
|
||||||
|
|
||||||
if (companySite != initialSite) {
|
|
||||||
// make sure to put null in database for the team
|
|
||||||
team.companySite = companySite;
|
|
||||||
}
|
|
||||||
|
|
||||||
// provision a team with state of initiated
|
|
||||||
const createdTeamId = await teamService.createTeam(team);
|
|
||||||
router.push("/teams/" + createdTeamId);
|
|
||||||
} catch (error) {
|
|
||||||
setError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [teamName, setTeamName] = useState<string>("");
|
|
||||||
const [companySite, setcompanySite] = useState<string>(initialSite);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className="container mx-auto flex flex-col max-w-md m-10 bg-white p-10 rounded-lg shadow-md space-y-5"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
{/* header */}
|
|
||||||
<div className="text-lg">🙌Create a Team</div>
|
|
||||||
|
|
||||||
{error && <span className="text-red-300 text-md">{error}</span>}
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<span className="flex flex-col items-start">
|
|
||||||
<span className="text-md">Team Name</span>
|
|
||||||
<span className="text-gray-300 text-xs mb-2">
|
|
||||||
Everyone in your team will see this. You can always change this later.
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
placeholder="ex. AirBnB"
|
|
||||||
className="w-full rounded-lg bg-gray-50 p-3"
|
|
||||||
value={teamName}
|
|
||||||
onChange={(e) => setTeamName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col items-start">
|
|
||||||
<span className="text-md">Company Site</span>
|
|
||||||
<span className="text-gray-300 text-xs mb-2">optional</span>
|
|
||||||
<input
|
|
||||||
placeholder="ex. https://"
|
|
||||||
className="w-full rounded-lg bg-gray-50 p-3"
|
|
||||||
value={companySite}
|
|
||||||
onChange={(e) => setcompanySite(e.target.value)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<span className="flex flex-row justify-end space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
router.push("/teams");
|
|
||||||
}}
|
|
||||||
className="bg-gray-100 py-2 px-5 rounded text-gray-400"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
{loading ? (
|
|
||||||
<div
|
|
||||||
className="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full text-blue-600"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span className="text-black hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="text-sm text-white font-semibold py-2 px-5 bg-teal-500 rounded"
|
|
||||||
>
|
|
||||||
{"Continue ->"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import { useAuth } from "../../contexts/authContext";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import UserService from "../../services/userService";
|
|
||||||
import { User } from "../../models/user";
|
|
||||||
import { GetServerSidePropsContext } from "next";
|
|
||||||
import firebaseAdmin from "firebase-admin";
|
|
||||||
import {
|
|
||||||
FaPeopleCarry,
|
|
||||||
FaArrowRight,
|
|
||||||
FaBullhorn,
|
|
||||||
FaPlus,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import BackgroundLayout from "../../components/Layouts/BackgroundLayout";
|
|
||||||
import TeamService from "../../services/teamService";
|
|
||||||
import Loading from "../../components/Loading";
|
|
||||||
import { TeamMemberStatus } from "../../models/teamMember";
|
|
||||||
import { Divider, Tooltip } from "antd";
|
|
||||||
import { Team } from "../../models/team";
|
|
||||||
import moment from "moment";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* figure out where to take the user based on everything
|
|
||||||
*/
|
|
||||||
|
|
||||||
const userService: UserService = new UserService();
|
|
||||||
const teamService: TeamService = new TeamService();
|
|
||||||
|
|
||||||
function RouteHandler() {
|
|
||||||
const { currUser } = useAuth();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState<Boolean>(true);
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
const [teams, setTeams] = useState<Team[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
try {
|
|
||||||
// if not authenticated, take user to the login
|
|
||||||
if (!currUser) {
|
|
||||||
console.log(
|
|
||||||
"not authenticated...routing from dashboard to teams home"
|
|
||||||
);
|
|
||||||
router.push("/teams/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get user
|
|
||||||
const returnedUser: User | null = await userService.getUser(
|
|
||||||
currUser.uid
|
|
||||||
);
|
|
||||||
|
|
||||||
// if user has no profile, then go to create profile
|
|
||||||
if (
|
|
||||||
!returnedUser ||
|
|
||||||
!returnedUser.firstName ||
|
|
||||||
!returnedUser.lastName ||
|
|
||||||
!returnedUser.nickName
|
|
||||||
) {
|
|
||||||
console.log("no profile for the user, routing him/her there");
|
|
||||||
router.push("/teams/profile");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setUser(returnedUser);
|
|
||||||
|
|
||||||
// get all teams that this user id is active in
|
|
||||||
// get all of the teams that this user's email is invited to
|
|
||||||
const teams: Team[] = await teamService.getActiveOrInvitedTeamsbyUser(
|
|
||||||
currUser.uid,
|
|
||||||
returnedUser.emailAddress
|
|
||||||
);
|
|
||||||
|
|
||||||
setTeams(teams);
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
router.push("/teams/login");
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loading || !user) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not in a team yet, and have not created a team yet
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto flex flex-col max-w-screen-md m-10 bg-white p-10 rounded-lg shadow-md space-y-5">
|
|
||||||
{/* header */}
|
|
||||||
<div className="flex flex-row items-center justify-between">
|
|
||||||
<span className="flex flex-col justify-start">
|
|
||||||
<div className="text-lg">👋Hey, {user.firstName}</div>
|
|
||||||
<span className="text-gray-300 text-md">
|
|
||||||
Let's get you started.
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Tooltip title={"Click to Edit Profile"}>
|
|
||||||
<button onClick={() => router.push("/teams/profile")}>
|
|
||||||
<img
|
|
||||||
src={user ? user.avatarUrl : currUser.photoURL}
|
|
||||||
alt="avatar"
|
|
||||||
className="rounded-full w-12 shadow-lg"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<span className="text-gray-400">Your Teams</span>
|
|
||||||
<Tooltip title={"Add members and get started immediately!"}>
|
|
||||||
<button
|
|
||||||
onClick={() => window.open("/teams/create", "_self")}
|
|
||||||
className="ml-auto rounded text-xs font-semibold bg-gray-200 p-2 text-teal-600 shadow-lg flex flex-row items-center space-x-2"
|
|
||||||
>
|
|
||||||
<FaPlus />
|
|
||||||
<span>Create</span>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* all teams part of */}
|
|
||||||
<div className="flex flex-row flex-wrap">
|
|
||||||
{teams.map((team) => {
|
|
||||||
console.log(team);
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={team.id}
|
|
||||||
className="group hover:bg-teal-600 hover:bg-opacity-10 transition-all
|
|
||||||
p-5 rounded bg-gray-200 bg-opacity-20 flex flex-row justify-between items-center w-[20rem] m-2"
|
|
||||||
>
|
|
||||||
<span className="flex flex-col mr-10 items-start">
|
|
||||||
<span className="flex flex-row items-center space-x-2">
|
|
||||||
<span className="text-lg text-teal-600 group-hover:font-semibold transition-all">
|
|
||||||
{team.name}
|
|
||||||
</span>
|
|
||||||
<Tooltip title={"Number of pro spots purchased"}>
|
|
||||||
<span className="bg-teal-600 bg-opacity-50 text-xs rounded-full w-5 h-5 flex items-center justify-evenly text-white shrink-0">
|
|
||||||
{team.allowedUserCount || 2}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{team.companySite && (
|
|
||||||
<span
|
|
||||||
onClick={() => window.open(team.companySite, "_blank")}
|
|
||||||
className="text-xs hover:cursor-pointer font-semibold text-gray-500"
|
|
||||||
>
|
|
||||||
{team.companySite}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className="text-xs text-gray-300">
|
|
||||||
{"created: " + moment(team.createdDate.toDate()).fromNow()}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Tooltip title={"Go to " + team.name}>
|
|
||||||
<span
|
|
||||||
onClick={() => window.open("/teams/" + team.id, "_blank")}
|
|
||||||
className="ml-auto hover:cursor-pointer transition-all bg-gray-500 bg-opacity-25 p-2 rounded group-hover:bg-opacity-40 group-hover:bg-teal-500"
|
|
||||||
>
|
|
||||||
<FaArrowRight className="text-sm text-white" />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{teams.length == 0 ? (
|
|
||||||
<button className="group flex flex-row py-5 px-3 items-center border border-dashed rounded">
|
|
||||||
<FaBullhorn className="text-5xl text-orange-300" />
|
|
||||||
|
|
||||||
<span className="flex flex-col items-start ml-2 mr-10">
|
|
||||||
<span className="text-gray-500">Remind Your Manager</span>
|
|
||||||
<span className="text-gray-300 text-xs text-left">
|
|
||||||
Your account email is {user.emailAddress}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="text-gray-300 ml-auto text-md mt-10">
|
|
||||||
or learn more about{" "}
|
|
||||||
<button
|
|
||||||
onClick={() => window.open("/", "_self")}
|
|
||||||
className="underline font-satisfy text-xl text-teal-500 decoration-teal-500"
|
|
||||||
>
|
|
||||||
nirvana
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|
||||||
// const cookies = nookies.get(context);
|
|
||||||
|
|
||||||
// var user: User | null = null
|
|
||||||
|
|
||||||
// if (cookies.token) {
|
|
||||||
// try {
|
|
||||||
// const headers: HeadersInit = {
|
|
||||||
// 'Content-Type': 'application/json',
|
|
||||||
// Authorization: JSON.stringify({ token: cookies.token })
|
|
||||||
// };
|
|
||||||
// const result = await unfetch('/api/auth/validateToken', { headers });
|
|
||||||
// console.log(result)
|
|
||||||
|
|
||||||
// // get user data
|
|
||||||
// const userService: UserService = new UserService()
|
|
||||||
|
|
||||||
// // the user is authenticated!
|
|
||||||
|
|
||||||
// // FETCH STUFF HERE!! 🚀
|
|
||||||
|
|
||||||
// // user = await userService.getUser("asdf")
|
|
||||||
// } catch (e) {
|
|
||||||
// // let exceptions fail silently
|
|
||||||
// // could be invalid token, just let client-side deal with that
|
|
||||||
|
|
||||||
// console.log(e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Pass data to the page via props
|
|
||||||
// return { props: {
|
|
||||||
// user
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return { props: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RouteHandler;
|
|
||||||
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { FaArrowLeft } from "react-icons/fa";
|
import { FaArrowLeft } from "react-icons/fa";
|
||||||
import { FcGoogle } from "react-icons/fc";
|
import { FcGoogle } from "react-icons/fc";
|
||||||
import MainLogo from "../../components/MainLogo";
|
import MainLogo from "../../components/Logo/MainLogo";
|
||||||
import { useAuth } from "../../contexts/authContext";
|
import { useAuth } from "../../contexts/authContext";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Firestore } from "firebase/firestore";
|
|
||||||
|
|
||||||
export default interface IService {
|
|
||||||
db: Firestore;
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import AgoraRTC, {
|
|
||||||
ClientConfig,
|
|
||||||
IAgoraRTCRemoteUser,
|
|
||||||
ICameraVideoTrack,
|
|
||||||
IMicrophoneAudioTrack,
|
|
||||||
} from "agora-rtc-sdk-ng";
|
|
||||||
// import {
|
|
||||||
// AgoraVideoPlayer,
|
|
||||||
// createClient,
|
|
||||||
// createMicrophoneAndCameraTracks,
|
|
||||||
// createMicrophoneAudioTrack,
|
|
||||||
// } from "agora-rtc-react";
|
|
||||||
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
const config: ClientConfig = {
|
|
||||||
mode: "rtc",
|
|
||||||
codec: "vp8",
|
|
||||||
};
|
|
||||||
|
|
||||||
// const useClient = createClient(config);
|
|
||||||
// const useMicrophoneTracks = createMicrophoneAudioTrack();
|
|
||||||
|
|
||||||
// TODO: move this to env file
|
|
||||||
const appId: string = "c8dfd65deb5c4741bd564085627139d0"; //ENTER APP ID HERE
|
|
||||||
|
|
||||||
export default class AgoraService {
|
|
||||||
private functions = getFunctions();
|
|
||||||
// private rtcClient = useClient();
|
|
||||||
|
|
||||||
async getAgoraToken(channelName: string): Promise<string> {
|
|
||||||
const cfResult = httpsCallable(this.functions, "agoraToken");
|
|
||||||
|
|
||||||
return await cfResult({ channelName })
|
|
||||||
.then((result: { data: { token: string } }) => {
|
|
||||||
const data = result.data;
|
|
||||||
|
|
||||||
if (!data.token) {
|
|
||||||
throw new Error("No token retrieved from agora");
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.token;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
// Getting the Error details.
|
|
||||||
const code = error.code;
|
|
||||||
const message = error.message;
|
|
||||||
const details = error.details;
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// async handleJoinChannel(channelName: string, agoraToken: string) {
|
|
||||||
// const localTrack: IMicrophoneAudioTrack =
|
|
||||||
// await AgoraRTC.createMicrophoneAudioTrack();
|
|
||||||
|
|
||||||
// let init = async (name: string) => {
|
|
||||||
// this.rtcClient.on("user-published", async (user, mediaType) => {
|
|
||||||
// await this.rtcClient.subscribe(user, mediaType);
|
|
||||||
// console.log("subscribe success");
|
|
||||||
|
|
||||||
// if (mediaType === "audio") {
|
|
||||||
// user.audioTrack?.play();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// this.rtcClient.on("user-unpublished", async (user, type) => {
|
|
||||||
// console.log("unpublished", user, type);
|
|
||||||
// if (type === "audio") {
|
|
||||||
// user.audioTrack?.stop();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await this.rtcClient.unsubscribe(user);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// this.rtcClient.on("user-left", (user) => {
|
|
||||||
// console.log("user left", user);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// await this.rtcClient.join(appId, name, agoraToken, null);
|
|
||||||
// if (localTrack) await this.rtcClient.publish(localTrack);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (localTrack) {
|
|
||||||
// console.log("init ready");
|
|
||||||
// init(channelName);
|
|
||||||
// } else {
|
|
||||||
// toast.error("Not ready for joining call");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
export { appId };
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {
|
|
||||||
addDoc,
|
|
||||||
collection,
|
|
||||||
doc,
|
|
||||||
Firestore,
|
|
||||||
getFirestore,
|
|
||||||
serverTimestamp,
|
|
||||||
setDoc,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import { AnnouncementState } from "../models/announcement";
|
|
||||||
import { Collections } from "./collections";
|
|
||||||
|
|
||||||
export class AnnouncementService {
|
|
||||||
private db: Firestore = getFirestore();
|
|
||||||
|
|
||||||
async updateAnnouncementState(
|
|
||||||
announcementId: string,
|
|
||||||
newState: AnnouncementState
|
|
||||||
) {
|
|
||||||
const docRef = doc(this.db, Collections.announcements, announcementId);
|
|
||||||
await setDoc(
|
|
||||||
docRef,
|
|
||||||
{ state: newState, lastUpdatedDate: serverTimestamp() },
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import {
|
|
||||||
getDownloadURL,
|
|
||||||
getStorage,
|
|
||||||
ref,
|
|
||||||
uploadBytesResumable,
|
|
||||||
} from "firebase/storage";
|
|
||||||
|
|
||||||
export default class CloudStorageService {
|
|
||||||
private storage = getStorage();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return the
|
|
||||||
*/
|
|
||||||
async uploadMessageAudioFile(audioFile: File): Promise<string> {
|
|
||||||
// Create the file metadata
|
|
||||||
/** @type {any} */
|
|
||||||
const metadata = {
|
|
||||||
contentType: "audio/mpeg",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Upload file and metadata to the object 'images/mountains.jpg'
|
|
||||||
const storageRef = ref(this.storage, "messages/" + audioFile.name);
|
|
||||||
const uploadTask = uploadBytesResumable(storageRef, audioFile, metadata);
|
|
||||||
|
|
||||||
// Listen for state changes, errors, and completion of the upload.
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
uploadTask.on(
|
|
||||||
"state_changed",
|
|
||||||
(snapshot) => {
|
|
||||||
// Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
|
|
||||||
const progress =
|
|
||||||
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
|
|
||||||
console.log("Upload is " + progress + "% done");
|
|
||||||
switch (snapshot.state) {
|
|
||||||
case "paused":
|
|
||||||
console.log("Upload is paused");
|
|
||||||
break;
|
|
||||||
case "running":
|
|
||||||
console.log("Upload is running");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
// A full list of error codes is available at
|
|
||||||
// https://firebase.google.com/docs/storage/web/handle-errors
|
|
||||||
|
|
||||||
reject(new Error(error.message));
|
|
||||||
|
|
||||||
switch (error.code) {
|
|
||||||
case "storage/unauthorized":
|
|
||||||
// User doesn't have permission to access the object
|
|
||||||
break;
|
|
||||||
case "storage/canceled":
|
|
||||||
// User canceled the upload
|
|
||||||
break;
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
case "storage/unknown":
|
|
||||||
// Unknown error occurred, inspect error.serverResponse
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// Upload completed successfully, now we can get the download URL
|
|
||||||
return getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
|
|
||||||
console.log("File available at", downloadURL);
|
|
||||||
|
|
||||||
return resolve(downloadURL);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export enum Collections {
|
|
||||||
users = "users",
|
|
||||||
teams = "teams",
|
|
||||||
teamMembers = "teamMembers",
|
|
||||||
teamSubscriptions = "teamSubscriptions",
|
|
||||||
audioMessages = "audioMessages",
|
|
||||||
|
|
||||||
rooms = "rooms",
|
|
||||||
announcements = "announcements",
|
|
||||||
links = "links",
|
|
||||||
|
|
||||||
officeRooms = "officeRooms",
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import firebaseAdmin from "firebase-admin/app";
|
|
||||||
|
|
||||||
import serviceAccount from "../nirvana-for-business-firebase-adminsdk";
|
|
||||||
|
|
||||||
const adminApp = firebaseAdmin.initializeApp({
|
|
||||||
credential: credential.cert({
|
|
||||||
privateKey: serviceAccount.private_key,
|
|
||||||
clientEmail: serviceAccount.client_email,
|
|
||||||
projectId: serviceAccount.project_id,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("initialized firebase admin");
|
|
||||||
|
|
||||||
export { adminApp };
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import {
|
|
||||||
addDoc,
|
|
||||||
collection,
|
|
||||||
doc,
|
|
||||||
Firestore,
|
|
||||||
getFirestore,
|
|
||||||
serverTimestamp,
|
|
||||||
setDoc,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import Link, { LinkState } from "../models/link";
|
|
||||||
import { Collections } from "./collections";
|
|
||||||
|
|
||||||
export class LinkService {
|
|
||||||
private db: Firestore = getFirestore();
|
|
||||||
|
|
||||||
async updateLinkState(linkId: string, newState: LinkState) {
|
|
||||||
const docRef = doc(this.db, Collections.links, linkId);
|
|
||||||
await setDoc(
|
|
||||||
docRef,
|
|
||||||
{ state: newState, lastUpdatedDate: serverTimestamp() },
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import {
|
|
||||||
addDoc,
|
|
||||||
collection,
|
|
||||||
doc,
|
|
||||||
FieldValue,
|
|
||||||
Firestore,
|
|
||||||
getFirestore,
|
|
||||||
serverTimestamp,
|
|
||||||
setDoc,
|
|
||||||
writeBatch,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import OfficeRoom, { OfficeRoomState } from "../models/officeRoom";
|
|
||||||
// import AgoraService from "./agoraService";
|
|
||||||
import { Collections } from "./collections";
|
|
||||||
|
|
||||||
export default class OfficeRoomService {
|
|
||||||
private db: Firestore = getFirestore();
|
|
||||||
private batch = writeBatch(this.db);
|
|
||||||
|
|
||||||
// private agoraService = new AgoraService();
|
|
||||||
|
|
||||||
async createInitialOfficeRooms(createdByUserId: string, teamId: string) {
|
|
||||||
const entrance = new OfficeRoom("Entrance", teamId, createdByUserId);
|
|
||||||
const kitchen = new OfficeRoom("Kitchen", teamId, createdByUserId);
|
|
||||||
const hallway = new OfficeRoom("Hallway", teamId, createdByUserId);
|
|
||||||
const corner = new OfficeRoom("Corner", teamId, createdByUserId);
|
|
||||||
const main = new OfficeRoom("Team Hub", teamId, createdByUserId);
|
|
||||||
const handsOnDeck = new OfficeRoom(
|
|
||||||
"All Hands On Deck",
|
|
||||||
teamId,
|
|
||||||
createdByUserId
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialORs: OfficeRoom[] = [
|
|
||||||
entrance,
|
|
||||||
kitchen,
|
|
||||||
hallway,
|
|
||||||
corner,
|
|
||||||
main,
|
|
||||||
handsOnDeck,
|
|
||||||
];
|
|
||||||
|
|
||||||
initialORs.forEach((oR) => {
|
|
||||||
const oRRef = doc(this.db, Collections.officeRooms, oR.id);
|
|
||||||
this.batch.set(oRRef, { ...oR, createdDate: serverTimestamp() });
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.batch.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
async joinOfficeRoom(officeRoom: OfficeRoom, userId: string) {
|
|
||||||
// get agoraToken
|
|
||||||
// const agoraToken = await this.agoraService.getAgoraToken();
|
|
||||||
|
|
||||||
// join channel
|
|
||||||
// await this.agoraService.handleJoinChannel(officeRoom.id, agoraToken);
|
|
||||||
|
|
||||||
// update members list in firestore
|
|
||||||
const newMembers = [...officeRoom.members, userId];
|
|
||||||
await this.updateMembersInOfficeRoom(officeRoom.id, newMembers);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateMembersInOfficeRoom(
|
|
||||||
officeRoomId: string,
|
|
||||||
newMembersInRoom: string[]
|
|
||||||
) {
|
|
||||||
// if the room is going to be empty, then change status accordingly
|
|
||||||
var state: OfficeRoomState = OfficeRoomState.active;
|
|
||||||
if (newMembersInRoom.length == 0) {
|
|
||||||
state = OfficeRoomState.idle;
|
|
||||||
}
|
|
||||||
|
|
||||||
const docRef = doc(this.db, Collections.officeRooms, officeRoomId);
|
|
||||||
await setDoc(
|
|
||||||
docRef,
|
|
||||||
{
|
|
||||||
state,
|
|
||||||
members: newMembersInRoom,
|
|
||||||
lastUpdatedDate: serverTimestamp(),
|
|
||||||
},
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// async updateRoom(room: Room) {
|
|
||||||
// const docRef = doc(this.db, Collections.rooms, room.id);
|
|
||||||
// await setDoc(
|
|
||||||
// docRef,
|
|
||||||
// { ...room, lastUpdatedDate: serverTimestamp() },
|
|
||||||
// { merge: true }
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import {
|
|
||||||
addDoc,
|
|
||||||
collection,
|
|
||||||
doc,
|
|
||||||
Firestore,
|
|
||||||
getFirestore,
|
|
||||||
serverTimestamp,
|
|
||||||
setDoc,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import Room, { RoomStatus } from "../models/room";
|
|
||||||
import { Collections } from "./collections";
|
|
||||||
|
|
||||||
export default class RoomService {
|
|
||||||
private db: Firestore = getFirestore();
|
|
||||||
|
|
||||||
async createOrUpdateRoom(room: Room) {
|
|
||||||
if (room.id) {
|
|
||||||
//update
|
|
||||||
await this.updateRoom(room);
|
|
||||||
} else {
|
|
||||||
//create
|
|
||||||
const roomDocRef = await addDoc(collection(this.db, Collections.rooms), {
|
|
||||||
...room,
|
|
||||||
createdDate: serverTimestamp(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateMembersInRoom(roomId: string, newMembersInRoom: string[]) {
|
|
||||||
// if the room is going to be empty, then change status accordingly
|
|
||||||
var status: RoomStatus = RoomStatus.live;
|
|
||||||
if (newMembersInRoom.length == 0) {
|
|
||||||
status = RoomStatus.empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
const docRef = doc(this.db, Collections.rooms, roomId);
|
|
||||||
await setDoc(
|
|
||||||
docRef,
|
|
||||||
{
|
|
||||||
status,
|
|
||||||
membersInRoom: newMembersInRoom,
|
|
||||||
lastUpdatedDate: serverTimestamp(),
|
|
||||||
},
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateRoom(room: Room) {
|
|
||||||
const docRef = doc(this.db, Collections.rooms, room.id);
|
|
||||||
await setDoc(
|
|
||||||
docRef,
|
|
||||||
{ ...room, lastUpdatedDate: serverTimestamp() },
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import {
|
|
||||||
addDoc,
|
|
||||||
collection,
|
|
||||||
Firestore,
|
|
||||||
getFirestore,
|
|
||||||
serverTimestamp,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import Announcement from "../models/announcement";
|
|
||||||
import Link from "../models/link";
|
|
||||||
import { Message } from "../models/message";
|
|
||||||
import { Collections } from "./collections";
|
|
||||||
|
|
||||||
export class SendService {
|
|
||||||
private db: Firestore = getFirestore();
|
|
||||||
|
|
||||||
async sendMessage(message: Message) {
|
|
||||||
// do quick stuff to create the composite for easier future querying
|
|
||||||
message.senderReceiver = [message.senderUserId, message.receiverUserId];
|
|
||||||
|
|
||||||
const teamDocRef = await addDoc(
|
|
||||||
collection(this.db, Collections.audioMessages),
|
|
||||||
{
|
|
||||||
...message,
|
|
||||||
createdDate: serverTimestamp(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendLink(link: Link) {
|
|
||||||
await addDoc(collection(this.db, Collections.links), {
|
|
||||||
...link,
|
|
||||||
createdDate: serverTimestamp(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendAnnouncement(announcement: Announcement) {
|
|
||||||
await addDoc(collection(this.db, Collections.announcements), {
|
|
||||||
...announcement,
|
|
||||||
createdDate: serverTimestamp(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
import {
|
|
||||||
addDoc,
|
|
||||||
collection,
|
|
||||||
doc,
|
|
||||||
DocumentReference,
|
|
||||||
Firestore,
|
|
||||||
getDoc,
|
|
||||||
getDocs,
|
|
||||||
getFirestore,
|
|
||||||
orderBy,
|
|
||||||
query,
|
|
||||||
serverTimestamp,
|
|
||||||
setDoc,
|
|
||||||
where,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import { Team } from "../models/team";
|
|
||||||
import {
|
|
||||||
TeamMember,
|
|
||||||
TeamMemberRole,
|
|
||||||
TeamMemberStatus,
|
|
||||||
} from "../models/teamMember";
|
|
||||||
import { Collections } from "./collections";
|
|
||||||
import IService from "./IService";
|
|
||||||
|
|
||||||
export default class TeamService implements IService {
|
|
||||||
db: Firestore = getFirestore();
|
|
||||||
|
|
||||||
async createTeam(team: Team): Promise<string> {
|
|
||||||
// create team
|
|
||||||
const teamDocRef = await addDoc(collection(this.db, Collections.teams), {
|
|
||||||
...team,
|
|
||||||
createdDate: serverTimestamp(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const teamMember = new TeamMember();
|
|
||||||
teamMember.role = TeamMemberRole.admin;
|
|
||||||
teamMember.teamId = teamDocRef.id;
|
|
||||||
teamMember.userId = team.createdByUserId;
|
|
||||||
teamMember.status = TeamMemberStatus.activated;
|
|
||||||
|
|
||||||
// create team member as admin who created the team
|
|
||||||
const teamMemberRef = await addDoc(
|
|
||||||
collection(this.db, Collections.teamMembers),
|
|
||||||
{
|
|
||||||
...teamMember,
|
|
||||||
createdDate: serverTimestamp(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("created team");
|
|
||||||
|
|
||||||
return teamDocRef.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTeam(team: Team) {
|
|
||||||
const docRef = doc(this.db, Collections.teams, team.id);
|
|
||||||
await setDoc(
|
|
||||||
docRef,
|
|
||||||
{ ...team, lastUpdatedDate: serverTimestamp() },
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTeam(teamId: string): Promise<Team | null> {
|
|
||||||
const docRef = doc(this.db, Collections.teams, teamId);
|
|
||||||
const docSnap = await getDoc(docRef);
|
|
||||||
|
|
||||||
if (docSnap.exists()) {
|
|
||||||
console.log("got team data");
|
|
||||||
let team: Team = docSnap.data() as Team;
|
|
||||||
team.id = docSnap.id;
|
|
||||||
return team;
|
|
||||||
} else {
|
|
||||||
// doc.data() will be undefined in this case
|
|
||||||
console.log("team not found!");
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTeamMemberByUserId(
|
|
||||||
teamId: string,
|
|
||||||
userId: string
|
|
||||||
): Promise<TeamMember | null> {
|
|
||||||
const q = query(
|
|
||||||
collection(this.db, Collections.teamMembers),
|
|
||||||
where("teamId", "==", teamId),
|
|
||||||
where("userId", "==", userId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const querySnapshot = await getDocs(q);
|
|
||||||
|
|
||||||
if (querySnapshot.size > 1) {
|
|
||||||
console.log(
|
|
||||||
"there are multiple teammembers for this user...error in teamservice"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var teamMember: TeamMember | null = null;
|
|
||||||
querySnapshot.forEach((doc) => {
|
|
||||||
// doc.data() is never undefined for query doc snapshots
|
|
||||||
// get the first one and just return...shouldn't be more
|
|
||||||
console.log("got teammember data");
|
|
||||||
teamMember = doc.data() as TeamMember;
|
|
||||||
|
|
||||||
teamMember.id = doc.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
return teamMember;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTeamMemberByEmailInvite(
|
|
||||||
teamId: string,
|
|
||||||
emailAddress: string
|
|
||||||
): Promise<TeamMember | null> {
|
|
||||||
const q = query(
|
|
||||||
collection(this.db, Collections.teamMembers),
|
|
||||||
where("teamId", "==", teamId),
|
|
||||||
where("inviteEmailAddress", "==", emailAddress)
|
|
||||||
);
|
|
||||||
|
|
||||||
const querySnapshot = await getDocs(q);
|
|
||||||
|
|
||||||
if (querySnapshot.size > 1) {
|
|
||||||
console.log(
|
|
||||||
"there are multiple teammembers for this user...error in teamservice"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var teamMember: TeamMember | null = null;
|
|
||||||
querySnapshot.forEach((doc) => {
|
|
||||||
// doc.data() is never undefined for query doc snapshots
|
|
||||||
// get the first one and just return...shouldn't be more
|
|
||||||
console.log("got teammember data");
|
|
||||||
teamMember = doc.data() as TeamMember;
|
|
||||||
|
|
||||||
teamMember.id = doc.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
return teamMember;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTeamMembersByEmailInvite(
|
|
||||||
emailAddress: string
|
|
||||||
): Promise<TeamMember[]> {
|
|
||||||
const q = query(
|
|
||||||
collection(this.db, Collections.teamMembers),
|
|
||||||
where("inviteEmailAddress", "==", emailAddress)
|
|
||||||
);
|
|
||||||
|
|
||||||
const querySnapshot = await getDocs(q);
|
|
||||||
|
|
||||||
var teamMembers: TeamMember[] = [];
|
|
||||||
querySnapshot.forEach((doc) => {
|
|
||||||
// doc.data() is never undefined for query doc snapshots
|
|
||||||
// get the first one and just return...shouldn't be more
|
|
||||||
console.log("got teammember data");
|
|
||||||
let teamMember: TeamMember = doc.data() as TeamMember;
|
|
||||||
|
|
||||||
teamMember.id = doc.id;
|
|
||||||
|
|
||||||
teamMembers.push(teamMember);
|
|
||||||
});
|
|
||||||
|
|
||||||
return teamMembers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTeamMembersByUserId(userId: string): Promise<TeamMember[]> {
|
|
||||||
const q = query(
|
|
||||||
collection(this.db, Collections.teamMembers),
|
|
||||||
where("userId", "==", userId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const querySnapshot = await getDocs(q);
|
|
||||||
|
|
||||||
if (querySnapshot.size > 1) {
|
|
||||||
console.log("this user is part of multiple teams");
|
|
||||||
}
|
|
||||||
|
|
||||||
var teamMembers: TeamMember[] = [];
|
|
||||||
|
|
||||||
querySnapshot.forEach((doc) => {
|
|
||||||
// doc.data() is never undefined for query doc snapshots
|
|
||||||
// get the first one and just return...shouldn't be more
|
|
||||||
console.log("got teammember data");
|
|
||||||
let teamMember: TeamMember = doc.data() as TeamMember;
|
|
||||||
teamMember.id = doc.id;
|
|
||||||
|
|
||||||
teamMembers.push(teamMember);
|
|
||||||
});
|
|
||||||
|
|
||||||
return teamMembers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTeamMembersByTeamId(teamId: string): Promise<TeamMember[]> {
|
|
||||||
const q = query(
|
|
||||||
collection(this.db, Collections.teamMembers),
|
|
||||||
where("teamId", "==", teamId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const querySnapshot = await getDocs(q);
|
|
||||||
|
|
||||||
var teamMembers: TeamMember[] = [];
|
|
||||||
|
|
||||||
querySnapshot.forEach((doc) => {
|
|
||||||
// doc.data() is never undefined for query doc snapshots
|
|
||||||
// get the first one and just return...shouldn't be more
|
|
||||||
let teamMember: TeamMember = doc.data() as TeamMember;
|
|
||||||
teamMember.id = doc.id;
|
|
||||||
|
|
||||||
teamMembers.push(teamMember);
|
|
||||||
});
|
|
||||||
|
|
||||||
return teamMembers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTeamMember(teamMember: TeamMember) {
|
|
||||||
const docRef = doc(this.db, Collections.teamMembers, teamMember.id);
|
|
||||||
await setDoc(
|
|
||||||
docRef,
|
|
||||||
{ ...teamMember, lastUpdatedDate: serverTimestamp() },
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTeamInvite(teamMember: TeamMember) {
|
|
||||||
if (teamMember.status == TeamMemberStatus.activated) {
|
|
||||||
console.log('cannot invite this user, already active.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if a person was already invited, then update that record, otherwise create a new one
|
|
||||||
const existingTeamMember = await this.getTeamMemberByEmailInvite(
|
|
||||||
teamMember.teamId,
|
|
||||||
teamMember.inviteEmailAddress
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existingTeamMember) {
|
|
||||||
const teamMemberRef = await addDoc(
|
|
||||||
collection(this.db, Collections.teamMembers),
|
|
||||||
{
|
|
||||||
...teamMember,
|
|
||||||
createdDate: serverTimestamp(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// just update the current user to active since we are activating again
|
|
||||||
await this.updateTeamMemberStatus(
|
|
||||||
existingTeamMember.id,
|
|
||||||
TeamMemberStatus.invited
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTeamMemberStatus(teamMemberId: string, status: TeamMemberStatus) {
|
|
||||||
const docRef = doc(this.db, Collections.teamMembers, teamMemberId);
|
|
||||||
await setDoc(
|
|
||||||
docRef,
|
|
||||||
{ status, lastUpdatedDate: serverTimestamp() },
|
|
||||||
{ merge: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getActiveOrInvitedTeamsbyUser(
|
|
||||||
userId: string,
|
|
||||||
email: string
|
|
||||||
): Promise<Team[]> {
|
|
||||||
// get all of the teammember entries for the user
|
|
||||||
const userTeamMembers = await this.getTeamMembersByUserId(userId);
|
|
||||||
const userTeamMembersByEmail = await this.getTeamMembersByEmailInvite(
|
|
||||||
email
|
|
||||||
);
|
|
||||||
|
|
||||||
// traverse through and get the teams for each
|
|
||||||
var teams: Promise<Team>[];
|
|
||||||
if (userTeamMembers) {
|
|
||||||
teams = userTeamMembers.map(async (tm) => {
|
|
||||||
if (tm.status != TeamMemberStatus.deleted) {
|
|
||||||
let team = await this.getTeam(tm.teamId);
|
|
||||||
return team;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var invitedToTeams: Promise<Team>[] = [];
|
|
||||||
invitedToTeams = userTeamMembersByEmail
|
|
||||||
.filter((tm) => tm.status == TeamMemberStatus.invited)
|
|
||||||
.map(async (tm) => {
|
|
||||||
let team = await this.getTeam(tm.teamId);
|
|
||||||
return team;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all([...teams, ...invitedToTeams]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user