Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 56 additions & 18 deletions apps/web/src/components/Session/InSession.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import React, { useEffect, useState } from "react";
import { SessionChat } from "@/components/Session/SessionChat";
import { useNavigate, useSearchParams } from "react-router-dom";
import Controllers, { Controller } from "@/components/Session/Controllers";
import CallMembers from "@/components/Session/CallMembers";
import { useMediaCall } from "@/hooks/mediaCall";
import Controllers, { Controller } from "@/components/Session/Controllers";
import PostSessionDialogs from "@/components/Session/PostSessionDialogs";
import { SessionChat } from "@/components/Session/SessionChat";
import { RemoteMember } from "@/components/Session/types";
import { AlertType, AlertV2 } from "@litespace/ui/Alert";
import { useRender } from "@litespace/headless/common";
import { useOnError } from "@/hooks/error";
import { useMediaCall } from "@/hooks/mediaCall";
import dayjs from "@/lib/dayjs";
import { Device, MemberConnectionState } from "@/modules/MediaCall/types";
import { useFormatMessage } from "@litespace/ui/hooks/intl";
import { Button } from "@litespace/ui/Button";
import CallIncoming from "@litespace/assets/CallIncoming";
import Close2 from "@litespace/assets/Close2";
import { ISession, IUser, Wss } from "@litespace/types";
import { useSocket } from "@litespace/headless/socket";
import { useCreateReport } from "@litespace/headless/report";
import { useRender } from "@litespace/headless/common";
import { useUser } from "@litespace/headless/context/user";
import dayjs from "@/lib/dayjs";
import { useOnError } from "@/hooks/error";
import { useToast } from "@litespace/ui/Toast";
import { useReportLesson } from "@litespace/headless/lessons";
import { Web } from "@litespace/utils/routes";
import { useFindTutorRatings } from "@litespace/headless/rating";
import { useCreateReport } from "@litespace/headless/report";
import { useSocket } from "@litespace/headless/socket";
import { ISession, IUser, Wss } from "@litespace/types";
import { AlertType, AlertV2 } from "@litespace/ui/Alert";
import { Button } from "@litespace/ui/Button";
import { ConfirmationDialog } from "@litespace/ui/ConfirmationDialog";
import CallIncoming from "@litespace/assets/CallIncoming";
import { useFormatMessage } from "@litespace/ui/hooks/intl";
import { useToast } from "@litespace/ui/Toast";
import { MIN_LESSON_DURATION } from "@litespace/utils";
import { Web } from "@litespace/utils/routes";
import { first } from "lodash";
import React, { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";

const InSession: React.FC<{
sessionId: ISession.Id;
Expand Down Expand Up @@ -56,10 +59,33 @@ const InSession: React.FC<{
const [_, setParams] = useSearchParams();
const [newMessageIndicator, setNewMessageIndicator] = useState<number>(0);

const [postSession, setPostSession] = useState(false);

const [connState, setConnState] = useState<
MemberConnectionState | undefined
>();

const [tutorRateByUser, settutorRateByUser] = useState<number | undefined>(
undefined
);

const ratings = useFindTutorRatings(remoteMember.id, {});

// const tutorRateByUser = useMemo(() => {
// if (user?.role === IUser.Role.Student)
// return first(
// ratings.query.data?.list.filter((rate) => rate.userId === user?.id)
// )?.value;
// }, [ratings.query.data?.list, user?.id, user?.role]);

useEffect(() => {
const rateValue = first(
ratings.query.data?.list.filter((rate) => rate.userId === user?.id)
)?.value;

settutorRateByUser(rateValue);
}, [ratings.query.data?.list, user?.id]);

const connAlertRender = useRender();
const [connAlertData, setConnAlertData] = useState<{
title: string;
Expand Down Expand Up @@ -312,7 +338,7 @@ const InSession: React.FC<{
call.manager?.session.disconnect().then(
// this approach makes the lesson page more robust by avoiding
// akward scenarios. Like livekit not emitting the disconnect event.
() => document.location.reload()
() => setPostSession(true)
)
}
audio={controllers.audio}
Expand All @@ -339,6 +365,18 @@ const InSession: React.FC<{
open={reportLessonDialog.open}
close={reportLessonDialog.hide}
/>

{user?.role === IUser.Role.Student ? (
<PostSessionDialogs
postSession={postSession}
tutorName={remoteMember.name}
tutorId={remoteMember.id}
studentId={user?.id}
tutorRateByUser={tutorRateByUser}
isTutorManager={remoteMember.role === IUser.Role.TutorManager}
tutorImage={remoteMember.image}
/>
) : null}
</div>
);
};
Expand Down
196 changes: 196 additions & 0 deletions apps/web/src/components/Session/PostSessionDialogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { useOnError } from "@/hooks/error";
import { asSlotBoundries } from "@/lib/lesson";
import { useFindAvailabilitySlots } from "@litespace/headless/availabilitySlots";
import { QueryKey } from "@litespace/headless/constants";
import { useSubscription } from "@litespace/headless/context/subscription";
import { useCreateLesson, useFindLessons } from "@litespace/headless/lessons";
import { useInvalidateQuery } from "@litespace/headless/query";
import { useCreateRatingTutor } from "@litespace/headless/rating";
import { useSubscriptionWeekBoundaries } from "@litespace/headless/subscription";
import { IAvailabilitySlot, ILesson } from "@litespace/types";
import { Dialog } from "@litespace/ui/Dialog";
import { useFormatMessage } from "@litespace/ui/hooks/intl";
import { ManageLessonDialog, RateLesson } from "@litespace/ui/Lessons";
import { useToast } from "@litespace/ui/Toast";
import { dayjs, MAX_LESSON_DURATION, nullable } from "@litespace/utils";
import React, { useCallback, useMemo } from "react";

const PostSessionDialogs: React.FC<{
postSession: boolean;
tutorName: string | null;
tutorId: number;
studentId: number;
tutorRateByUser?: number;
isTutorManager: boolean;
tutorImage: string | null;
}> = ({
postSession,
tutorName,
tutorId,
studentId,
tutorRateByUser,
isTutorManager,
tutorImage,
}) => {
const intl = useFormatMessage();
const toast = useToast();
const invalidate = useInvalidateQuery();

const { info, remainingWeeklyMinutes, refetch } = useSubscription();

const lessons = useFindLessons({
canceled: false,
users: studentId ? [studentId] : [],
after: dayjs().toISOString(),
userOnly: true,
size: 1,
});

const hasBookedLessons = useMemo(() => {
return !!lessons.query.data && !!lessons.query.data.list.length;
}, [lessons]);

const asRemainingWeeklyMinutes = useMemo(() => {
if (info) return remainingWeeklyMinutes;
if (isTutorManager) return MAX_LESSON_DURATION;
return 0;
}, [info, remainingWeeklyMinutes, isTutorManager]);

const weekBoundaries = useSubscriptionWeekBoundaries(info);

const slotBoundries = useMemo(
() =>
asSlotBoundries({
start: weekBoundaries.start.toISOString(),
end: weekBoundaries.end.toISOString(),
}),
[weekBoundaries.start, weekBoundaries.end]
);

const tutorAvailabilitySlots = useFindAvailabilitySlots({
userIds: [tutorId],
purposes: [
IAvailabilitySlot.Purpose.General,
IAvailabilitySlot.Purpose.Lesson,
],
...slotBoundries,
});

const bookedSlots = useMemo(() => {
if (!tutorAvailabilitySlots.data?.subslots) return [];
// if (payload.type === "book")
return tutorAvailabilitySlots.data.subslots;
// const start = dayjs.utc(payload.start);
// const end = start.add(payload.duration, "minutes");
// return tutorAvailabilitySlots.data.subslots.filter(
// (slot) => !start.isSame(slot.start) || !end.isSame(slot.end)
// );
}, [tutorAvailabilitySlots.data?.subslots]);

const onSuccess = useCallback(() => {
toast.success({ title: intl("tutor.rate.create.success") });
}, [intl, toast]);

const onError = useOnError({
type: "mutation",
handler: () => {
toast.error({ title: intl("tutor.rate.create.error") });
},
});

const createRate = useCreateRatingTutor({ onSuccess, onError });

// const [isRated, setIsRated] = useState(!!tutorRateByUser);

console.log(tutorRateByUser);

const onCreateSuccess = useCallback(() => {
// if (payload.onSuccess) payload.onSuccess();
invalidate([QueryKey.FindAvailabilitySlots]);
invalidate([QueryKey.FindLesson]);
invalidate([QueryKey.FindInfiniteLessons]);
invalidate([QueryKey.FindTutors]);
// close();
}, [invalidate]);

const onCreateError = useOnError({
type: "mutation",
handler: ({ messageId }) => {
toast.error({
title: intl("book-lesson.error"),
description: intl(messageId),
});
},
});

const createLessonMutation = useCreateLesson({
onSuccess: onCreateSuccess,
onError: onCreateError,
});

const onSubmit = useCallback(
({
slotId,
start,
duration,
}: {
slotId: number;
start: string;
duration: ILesson.Duration;
}) => {
// if (payload.type === "book")
createLessonMutation
.mutateAsync({
tutorId,
slotId,
duration,
start,
})
.then(() => refetch());

// updateLessonMutation
// .mutateAsync({
// lessonId: payload.lessonId,
// slotId,
// duration,
// start,
// })
// .then(() => refetch());
},
[createLessonMutation, tutorId, refetch]
);

if (!postSession) return;
return (
<div>
<Dialog open={!tutorRateByUser}>
<RateLesson
close={() => {}}
onRate={({ value, feedback }) => {
createRate.mutate({ value, feedback, rateeId: tutorId });
}}
rateLoading={false}
tutorName={tutorName}
type="session"
initialRating={tutorRateByUser}
/>
</Dialog>
<ManageLessonDialog
remainingWeeklyMinutes={asRemainingWeeklyMinutes}
open={!!tutorRateByUser}
bookedSlots={bookedSlots}
close={() => {}}
hasBookedLessons={hasBookedLessons}
imageUrl={nullable(tutorImage)}
name={tutorName}
onSubmit={onSubmit}
retry={tutorAvailabilitySlots.refetch}
slots={tutorAvailabilitySlots.data?.slots.list || []}
subscribed={!!info}
tutorId={tutorId}
/>
</div>
);
};

export default PostSessionDialogs;
36 changes: 18 additions & 18 deletions apps/web/src/components/Session/PreSession.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { ISession, IUser, Wss } from "@litespace/types";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import Ready from "@/components/Session/Ready";
import { useSocket } from "@litespace/headless/socket";
import { Typography } from "@litespace/ui/Typography";
import { Button } from "@litespace/ui/Button";
import { useFormatMessage } from "@litespace/ui/hooks/intl";
import { RefreshCw } from "react-feather";
import { useUser } from "@litespace/headless/context/user";
import { useGetSessionToken } from "@litespace/headless/sessions";
import { sockets } from "@litespace/atlas";
import { env } from "@/lib/env";
import { useMediaCall } from "@/hooks/mediaCall";
import InSession from "@/components/Session/InSession";
import { CallError, Device } from "@/modules/MediaCall/types";
import Preview from "@/components/Session/Preview";
import Controllers, { Controller } from "@/components/Session/Controllers";
import { Dialogs, DialogTypes } from "@/components/Session/Dialogs";
import InSession from "@/components/Session/InSession";
import Preview from "@/components/Session/Preview";
import Ready from "@/components/Session/Ready";
import { RemoteMember } from "@/components/Session/types";
import { useToast } from "@litespace/ui/Toast";
import { useMediaCall } from "@/hooks/mediaCall";
import { env } from "@/lib/env";
import { CallError, Device } from "@/modules/MediaCall/types";
import Microphone from "@litespace/assets/Microphone";
import { sockets } from "@litespace/atlas";
import { useRender } from "@litespace/headless/common";
import { useUser } from "@litespace/headless/context/user";
import { useGetSessionToken } from "@litespace/headless/sessions";
import { useSocket } from "@litespace/headless/socket";
import { ISession, IUser, Wss } from "@litespace/types";
import { Button } from "@litespace/ui/Button";
import { ConfirmationDialog } from "@litespace/ui/ConfirmationDialog";
import Microphone from "@litespace/assets/Microphone";
import { useFormatMessage } from "@litespace/ui/hooks/intl";
import { useToast } from "@litespace/ui/Toast";
import { Typography } from "@litespace/ui/Typography";
import { VideoPlayer } from "@litespace/ui/VideoPlayer";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { RefreshCw } from "react-feather";

const PreSession: React.FC<{
type: ISession.Type;
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/Lessons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { PastLessonsTable } from "@/components/Lessons/PastLessonsTable";
export { PastLessonsSummary } from "@/components/Lessons/PastLessonsSummary";
export { UpcomingLessonsSummary } from "@/components/Lessons/UpcomingLessonsSummary";
export { CancelLesson } from "@/components/Lessons/CancelLesson/CancelLesson";
export { RateLesson } from "@/components/Lessons/RateLesson";
2 changes: 2 additions & 0 deletions packages/ui/src/locales/ar-eg.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@
"tutor.recommended": "معلم موصي به",
"tutor.rate.delete.error": "عفوًا! حدث خطأ أثناء إزالة تقييمك",
"tutor.rate-count": "({value} تقييم)",
"tutor.rate.create.success": "تم إضافة التقييم بنجاح",
"tutor.rate.create.error": "حدث خطأ أثناء إضافة التقييم",
"tutor.achievements": "{lessonCount} حصة مع {studentCount} طالب",
"tutor.book": "حجز حصة",
"tutor.rating.rate": "رأيك في {tutor}",
Expand Down