diff --git a/apps/dashboard/src/components/UserDetails/Content.tsx b/apps/dashboard/src/components/UserDetails/Content.tsx index 07bffb719..790488b11 100644 --- a/apps/dashboard/src/components/UserDetails/Content.tsx +++ b/apps/dashboard/src/components/UserDetails/Content.tsx @@ -16,7 +16,7 @@ import { rolesMap } from "@/components/utils/user"; import { useFormatMessage } from "@litespace/luna/hooks/intl"; const Content: React.FC<{ - user?: IUser.Self; + user?: IUser.PopulatedSelf; tutor?: ITutor.Self; tutorStats?: ITutor.FindTutorStatsApiResponse | null; loading?: boolean; diff --git a/apps/dashboard/src/components/Users/List.tsx b/apps/dashboard/src/components/Users/List.tsx index 62b5bc0b6..60343e99d 100644 --- a/apps/dashboard/src/components/Users/List.tsx +++ b/apps/dashboard/src/components/Users/List.tsx @@ -23,7 +23,7 @@ const List: React.FC<{ page: number; }> = ({ query, ...props }) => { const intl = useFormatMessage(); - const columnHelper = createColumnHelper(); + const columnHelper = createColumnHelper(); const columns = useMemo( () => [ columnHelper.accessor("id", { diff --git a/apps/dashboard/src/pages/UserDetails.tsx b/apps/dashboard/src/pages/UserDetails.tsx index 4f64be4bf..059cdc565 100644 --- a/apps/dashboard/src/pages/UserDetails.tsx +++ b/apps/dashboard/src/pages/UserDetails.tsx @@ -30,7 +30,7 @@ const UserDetails = () => { return value; }, [params.id]); - const query: UseQueryResult = useFindUserById(id); + const query: UseQueryResult = useFindUserById(id); const role = useMemo(() => { if (!query.data) return null; diff --git a/packages/headless/src/users.ts b/packages/headless/src/users.ts index 72072e113..0c8a13601 100644 --- a/packages/headless/src/users.ts +++ b/packages/headless/src/users.ts @@ -7,7 +7,7 @@ import { QueryKey } from "./constants"; export function useUsers( filter?: Omit -): UsePaginateResult { +): UsePaginateResult { const atlas = useAtlas(); const findUsers = useCallback( @@ -21,7 +21,7 @@ export function useUsers( export function useFindUserById( id: string | number -): UseQueryResult { +): UseQueryResult { const atlas = useAtlas(); const findUserById = useCallback( diff --git a/packages/models/migrations/1716146586880_setup.js b/packages/models/migrations/1716146586880_setup.js index 87ea38865..c86fe25dd 100644 --- a/packages/models/migrations/1716146586880_setup.js +++ b/packages/models/migrations/1716146586880_setup.js @@ -58,7 +58,6 @@ exports.up = (pgm) => { role: { type: "user_role", default: null }, birth_year: { type: "INT", default: null }, gender: { type: "user_gender", default: null }, - //online: { type: "BOOLEAN", notNull: true, default: false }, verified: { type: "BOOLEAN", notNull: true, default: false }, phone_number: { type: "VARCHAR(15)", default: null }, city: { type: "SMALLINT", default: null }, diff --git a/packages/models/src/cache/index.ts b/packages/models/src/cache/index.ts index 1b9b4c695..b19e70df7 100644 --- a/packages/models/src/cache/index.ts +++ b/packages/models/src/cache/index.ts @@ -4,7 +4,7 @@ import { Rules } from "@/cache/rules"; import { RedisClient } from "@/cache/base"; import { Peer } from "@/cache/peer"; import { Call } from "@/cache/call"; -import { OnlineStatus } from "./onlineStatus"; +import { OnlineStatus } from "@/cache/onlineStatus"; export class Cache { public tutors: Tutors; diff --git a/packages/models/src/cache/onlineStatus.ts b/packages/models/src/cache/onlineStatus.ts index fbe5de41a..ffeffae75 100644 --- a/packages/models/src/cache/onlineStatus.ts +++ b/packages/models/src/cache/onlineStatus.ts @@ -19,6 +19,12 @@ export class OnlineStatus extends CacheBase { return await this.client.hExists(this.key, this.asField(userId)); } + async isOnlineBatch(userIds: number[]): Promise { + return await Promise.all( + userIds.map(async id => this.client.hExists(this.key, this.asField(id))) + ); + } + private asField(userId: number): string { return userId.toString(); } diff --git a/packages/models/src/rooms.ts b/packages/models/src/rooms.ts index 6fc37b1f1..e1ccd163c 100644 --- a/packages/models/src/rooms.ts +++ b/packages/models/src/rooms.ts @@ -100,7 +100,6 @@ export class Rooms { name: users.column("name"), image: users.column("image"), role: users.column("role"), - //online: users.column("online"), TODO: to be removed pinned: this.column.members("pinned"), muted: this.column.members("muted"), createdAt: users.column("created_at"), @@ -258,10 +257,11 @@ export class Rooms { }; } - async asPopulatedMember(row: IRoom.PopulatedMemberRow): Promise { + asPopulatedMember(row: IRoom.PopulatedMemberRow): IRoom.PopulatedMember { return merge(omit(row, "createdAt", "updatedAt"), { createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), + online: false // TODO: substitute this workaround }); } } diff --git a/packages/models/src/tutors.ts b/packages/models/src/tutors.ts index 79e869b79..2e4b561d9 100644 --- a/packages/models/src/tutors.ts +++ b/packages/models/src/tutors.ts @@ -20,7 +20,6 @@ const fullTutorFields: FullTutorFieldsMap = { password: users.column("password"), birthYear: users.column("birth_year"), gender: users.column("gender"), - //online: users.column("online"), TODO: to be removed verified: users.column("verified"), creditScore: users.column("credit_score"), city: users.column("city"), diff --git a/packages/models/src/users.ts b/packages/models/src/users.ts index 03d3dba9f..15235d02e 100644 --- a/packages/models/src/users.ts +++ b/packages/models/src/users.ts @@ -16,7 +16,6 @@ export class Users { "role", "birth_year", "gender", - //"online", TODO: to be removed "verified", "city", "credit_score", @@ -60,7 +59,6 @@ export class Users { email: payload.email, image: payload.image, gender: payload.gender, - //online: payload.online, TODO: to be removed name: payload.name, verified: payload.verified, password: payload.password, @@ -120,7 +118,6 @@ export class Users { role, verified, gender, - //online, TODO: to be removed page, size, orderBy, diff --git a/packages/models/tests/users.test.ts b/packages/models/tests/users.test.ts index 82f6f708e..eb1f0207a 100644 --- a/packages/models/tests/users.test.ts +++ b/packages/models/tests/users.test.ts @@ -49,7 +49,6 @@ describe("Users", () => { birthYear: 2003, gender: IUser.Gender.Female, verified: true, - //online: true, TODO: to be removed creditScore: 2382001, phoneNumber: "01018303125", city: 2, @@ -63,7 +62,6 @@ describe("Users", () => { expect(updated.birthYear).to.be.eq(2003); expect(updated.gender).to.be.eq(IUser.Gender.Female); expect(updated.verified).to.be.eq(true); - //expect(updated.online).to.be.eq(true); TODO: to be removed expect(updated.creditScore).to.be.eq(2382001); expect(updated.phoneNumber).to.be.eq("01018303125"); expect(updated.city).to.be.eq(2); diff --git a/packages/types/src/room.ts b/packages/types/src/room.ts index ffbb1927f..507fa6f33 100644 --- a/packages/types/src/room.ts +++ b/packages/types/src/room.ts @@ -37,7 +37,6 @@ export type PopulatedMemberRow = { name: IUser.Row["name"]; image: IUser.Row["image"]; role: IUser.Row["role"]; - //online: IUser.Row["online"]; TODO: to be removed createdAt: IUser.Row["created_at"]; updatedAt: IUser.Row["updated_at"]; }; @@ -51,7 +50,7 @@ export type PopulatedMember = { name: IUser.Self["name"]; image: IUser.Self["image"]; role: IUser.Self["role"]; - //online: IUser.Self["online"]; TODO: to be removed + online: boolean; createdAt: IUser.Self["createdAt"]; updatedAt: IUser.Self["updatedAt"]; }; @@ -76,7 +75,7 @@ export type FindUserRoomsApiRecord = { id: number; name: string | null; image: string | null; - //online: boolean; TODO: to be removed + online: boolean; role: IUser.Role; lastSeen: string; }; diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts index 53be82774..e3f814dff 100644 --- a/packages/types/src/user.ts +++ b/packages/types/src/user.ts @@ -57,7 +57,6 @@ export type Self = { birthYear: number | null; gender: Gender | null; role: Role; - //online: boolean; TODO: to be removed verified: boolean; creditScore: number; city: City | null; @@ -66,6 +65,8 @@ export type Self = { updatedAt: string; }; +export type PopulatedSelf = Self & { online: boolean }; + export type Row = { id: number; email: string; @@ -75,7 +76,6 @@ export type Row = { birth_year: number | null; gender: Gender | null; role: Role; - //online: boolean; TODO: to be removed verified: boolean; credit_score: number; city: City | null; @@ -106,7 +106,6 @@ export type UpdatePayload = { birthYear?: number; gender?: Gender; verified?: boolean; - //online?: boolean; TODO: to be removed creditScore?: number; phoneNumber?: string; city?: City; @@ -150,7 +149,7 @@ export type LoginApiResponse = { export type RegisterApiResponse = LoginApiResponse; -export type FindUsersApiResponse = Paginated; +export type FindUsersApiResponse = Paginated; export type ResetPasswordApiResponse = LoginApiResponse; diff --git a/services/server/src/handlers/user.ts b/services/server/src/handlers/user.ts index f78e40d26..3fb1e4221 100644 --- a/services/server/src/handlers/user.ts +++ b/services/server/src/handlers/user.ts @@ -1,5 +1,5 @@ import { tutors, users, knex, lessons } from "@litespace/models"; -import { ILesson, ITutor, IUser, Wss } from "@litespace/types"; +import { ILesson, ITutor, IUser, Paginated, Wss } from "@litespace/types"; import { apierror, exists, @@ -59,7 +59,7 @@ import { sendBackgroundMessage } from "@/workers"; import { WorkerMessageType } from "@/workers/messages"; import { isValidPassword } from "@litespace/sol/verification"; import { selectTutorRuleEvents } from "@/lib/events"; -import { Gender } from "@litespace/types/dist/esm/user"; +import { Gender, PopulatedSelf } from "@litespace/types/dist/esm/user"; import { isTutor, isTutorManager } from "@litespace/auth/dist/authorization"; const createUserPayload = zod.object({ @@ -316,7 +316,17 @@ async function findUsers(req: Request, res: Response, next: NextFunction) { const query: IUser.FindUsersApiQuery = findUsersQuery.parse(req.query); const result = await users.find(query); - const response: IUser.FindUsersApiResponse = result; + + const isOnlineList = await cache.onlineStatus.isOnlineBatch(result.list.map(user => user.id)); + const resList = result.list.map((user, i) => ({ + ...user, + online: isOnlineList[i], + })); + + const response: IUser.FindUsersApiResponse = { + list: resList, + total: result.total + }; res.status(200).json(response); } diff --git a/services/server/src/lib/chat.ts b/services/server/src/lib/chat.ts index 67582b3f3..1953ce326 100644 --- a/services/server/src/lib/chat.ts +++ b/services/server/src/lib/chat.ts @@ -39,6 +39,7 @@ export async function asFindUserRoomsApiRecord({ id: otherMember.id, name: otherMember.name, image: otherMember.image, + online: otherMember.online, role: otherMember.role, lastSeen: otherMember.updatedAt, }, diff --git a/services/server/src/lib/tutor.ts b/services/server/src/lib/tutor.ts index 04bb2af59..0dd42c841 100644 --- a/services/server/src/lib/tutor.ts +++ b/services/server/src/lib/tutor.ts @@ -68,10 +68,10 @@ export async function constructTutorsCache(date: Dayjs): Promise { } ); - const onlineUsers = await cache.onlineStatus.getAll(); + const isOnlineList = await cache.onlineStatus.isOnlineBatch(onboardedTutors.map(t => t.id)); // restruct tutors list to match ITutor.Cache[] - const cacheTutors: ITutor.Cache[] = onboardedTutors.map((tutor) => { + const cacheTutors: ITutor.Cache[] = onboardedTutors.map((tutor, i) => { const filteredTopics = tutorsTopics ?.filter((item) => item.userId === tutor.id) .map((item) => item.name.ar); @@ -84,7 +84,7 @@ export async function constructTutorsCache(date: Dayjs): Promise { bio: tutor.bio, about: tutor.about, gender: tutor.gender, - online: onlineUsers[tutor.id] ? true : false, + online: isOnlineList[i] ? true : false, notice: tutor.notice, topics: filteredTopics, avgRating: @@ -171,7 +171,7 @@ export function orderTutors({ * - lesson count */ async function findTutorCacheMeta(tutorId: number) { - const [tutorTopics, avgRatings, studentCount, lessonCount] = + const [tutorTopics, avgRatings, studentCount, lessonCount, online] = await Promise.all([ topics.findUserTopics({ users: [tutorId] }), ratings.findAvgRatings([tutorId]), @@ -181,6 +181,7 @@ async function findTutorCacheMeta(tutorId: number) { canceled: false, ratified: true, }), + cache.onlineStatus.isOnline(tutorId), ]); return { @@ -188,6 +189,7 @@ async function findTutorCacheMeta(tutorId: number) { avgRating: first(avgRatings)?.avg || 0, studentCount, lessonCount, + online, }; } @@ -201,6 +203,7 @@ export async function joinTutorCache( avgRating: cacheData.avgRating, studentCount: cacheData.studentCount, lessonCount: cacheData.lessonCount, + online: cacheData.online, } : await findTutorCacheMeta(tutor.id); @@ -213,7 +216,6 @@ export async function joinTutorCache( about: tutor.about, gender: tutor.gender, notice: tutor.notice, - online: await cache.onlineStatus.isOnline(tutor.id), ...meta, }; } diff --git a/services/server/src/wss/handlers/connection.ts b/services/server/src/wss/handlers/connection.ts index 477c72e31..3d9f656ad 100644 --- a/services/server/src/wss/handlers/connection.ts +++ b/services/server/src/wss/handlers/connection.ts @@ -24,7 +24,7 @@ export class Connection extends WssHandler { if (isGhost(user)) return; await cache.onlineStatus.addUser(user.id); - this.announceStatus(user.id, true); + this.announceStatus({ userId: user.id, online: true }); await this.joinRooms(); if (isAdmin(this.user)) this.emitServerStats(); @@ -38,7 +38,7 @@ export class Connection extends WssHandler { if (isGhost(user)) return; await cache.onlineStatus.removeUser(user.id); - this.announceStatus(user.id, false); + this.announceStatus({ userId: user.id, online: false }); await this.deregisterPeer(); await this.removeUserFromCalls(); @@ -46,13 +46,20 @@ export class Connection extends WssHandler { if (error instanceof Error) stdout.error(error.message); } - private async announceStatus(userId: number, status: boolean) { + private async announceStatus({ + userId, + online, + }: { + userId: number, + online: boolean, + }) { const userRooms = await rooms.findMemberFullRoomIds(userId); - for (const room of userRooms) { - this.broadcast(Wss.ServerEvent.UserStatusChanged, room.toString(), { - online: status, - }); + this.broadcast( + Wss.ServerEvent.UserStatusChanged, + room.toString(), + { online } + ); } }