diff --git a/package-lock.json b/package-lock.json index 37e549055c72..a493c63f819e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@internxt/css-config": "^1.0.2", "@internxt/eslint-config-internxt": "^2.0.0", "@internxt/lib": "^1.2.0", - "@internxt/sdk": "=1.11.8", + "@internxt/sdk": "=1.11.9", "@internxt/ui": "0.0.25", "@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz", "@jitsi/js-utils": "2.2.1", @@ -3454,9 +3454,9 @@ "integrity": "sha512-Vepa2uEBPuvtqAPDhA5mgGYl6byZMaRWH0z67vPXoOb4cIfv++S8xXGaOmAUVBktwbhCE4lWuTgT6BN2N19INQ==" }, "node_modules/@internxt/sdk": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.11.8.tgz", - "integrity": "sha512-KOKB71uY/9F/rHDs1nxCO+xLMVREgqv5EBTDtUsPhV+sDqvnnv931uXkOp4maDEFtSzjJ0ITEJa3NJFRgyQ5jQ==", + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.11.9.tgz", + "integrity": "sha512-ZgPptKnmH0mCO2sf5OJdqeQmbi9sgN+YUJyGBMycD0Cug+OHHIDmwazZWmSuyYegOaiaYlT47fuN6xu6wNESMQ==", "license": "MIT", "dependencies": { "axios": "1.11.0", diff --git a/package.json b/package.json index 6f1906409fc7..bfbd66b5def5 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@internxt/css-config": "^1.0.2", "@internxt/eslint-config-internxt": "^2.0.0", "@internxt/lib": "^1.2.0", - "@internxt/sdk": "=1.11.8", + "@internxt/sdk": "=1.11.9", "@internxt/ui": "0.0.25", "@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz", "@jitsi/js-utils": "2.2.1", diff --git a/react/features/base/connection/actions.any.ts b/react/features/base/connection/actions.any.ts index 33b6f0376dd6..7ee841158ced 100644 --- a/react/features/base/connection/actions.any.ts +++ b/react/features/base/connection/actions.any.ts @@ -12,6 +12,7 @@ import { } from '../util/uri'; import { setJoinRoomError } from "../meet/general/store/errors/actions"; +import { LocalStorageManager } from "../meet/LocalStorageManager"; import MeetingService from "../meet/services/meeting.service"; import { CONNECTION_DISCONNECTED, @@ -238,10 +239,16 @@ export function _connectInternal({ if (room !== NEW_MEETING_URL) try { + let userUUID: string | undefined; + + if (isAnonymous) { + userUUID = LocalStorageManager.instance.getOrCreateAnonymousUUID(); + } const { token: jwt, appId } = await MeetingService.instance.joinCall(room, { name: displayName ?? name ?? "", lastname: lastname ?? "", anonymous: !!isAnonymous, + anonymousId: userUUID, }); const newOptions = get8x8Options(options, appId, room); diff --git a/react/features/base/connection/actions.web.ts b/react/features/base/connection/actions.web.ts index 84e8c7fd4eb7..71535fce797e 100644 --- a/react/features/base/connection/actions.web.ts +++ b/react/features/base/connection/actions.web.ts @@ -10,6 +10,7 @@ import { stopLocalVideoRecording } from '../../recording/actions.any'; import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager.web'; import { setJWT } from '../jwt/actions'; +import { LocalStorageManager } from "../meet/LocalStorageManager"; import MeetingService from "../meet/services/meeting.service"; import { _connectInternal } from "./actions.any"; @@ -27,8 +28,9 @@ export function connect(id?: string, password?: string) { const state = getState(); const { jwt } = state["features/base/jwt"]; const { iAmRecorder, iAmSipGateway } = state["features/base/config"]; - const { user } = state["features/user"]; - + // TODO: CHECK WHY USER REDUCER IS NULL IN THIS POINT, initializers are not executing as expected + // const { user } = state["features/user"]; + const user = LocalStorageManager.instance.getUser(); if (!iAmRecorder && !iAmSipGateway && isVpaasMeeting(state)) { return dispatch(getCustomerDetails()) .then(() => { @@ -44,7 +46,7 @@ export function connect(id?: string, password?: string) { password, name: user?.name, lastname: user?.lastname, - isAnonymous: !!user, + isAnonymous: !user, }) ) ); @@ -67,7 +69,7 @@ export function connect(id?: string, password?: string) { password, name: user?.name, lastname: user?.lastname, - isAnonymous: !!user, + isAnonymous: !user, }) ); }; diff --git a/react/features/base/meet/LocalStorageManager.ts b/react/features/base/meet/LocalStorageManager.ts index d5128493ff88..f169a0942ab8 100644 --- a/react/features/base/meet/LocalStorageManager.ts +++ b/react/features/base/meet/LocalStorageManager.ts @@ -1,4 +1,5 @@ import { UserSubscription } from "@internxt/sdk/dist/drive/payments/types/types"; +import { v4 } from "uuid"; import { User } from "./general/store/user/types"; /** @@ -19,6 +20,7 @@ export class LocalStorageManager { MNEMONIC: "xMnemonic", USER: "xUser", SUBSCRIPTION: "xSubscription", + ANONYMOUS_USER_UUID: "xAnonymousUserUUID", }; private constructor() {} @@ -152,8 +154,8 @@ export class LocalStorageManager { /** * Gets the user information */ - public getUser(): T | null | undefined { - return this.get(LocalStorageManager.KEYS.USER); + public getUser(): User | null | undefined { + return this.get(LocalStorageManager.KEYS.USER); } /** @@ -184,6 +186,42 @@ export class LocalStorageManager { this.remove(LocalStorageManager.KEYS.SUBSCRIPTION); } + /** + * Generates and stores a UUID for anonymous users + * @returns The generated or existing UUID + */ + public getOrCreateAnonymousUUID(): string { + let uuid = this.get(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID); + + if (!uuid) { + uuid = v4(); + this.set(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID, uuid); + } + + return uuid; + } + + /** + * Gets the anonymous user UUID + */ + public getAnonymousUUID(): string | null | undefined { + return this.get(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID); + } + + /** + * Sets the anonymous user UUID + */ + public setAnonymousUUID(uuid: string): void { + this.set(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID, uuid); + } + + /** + * Removes the anonymous user UUID + */ + public removeAnonymousUUID(): void { + this.remove(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID); + } + /** * Saves the session credentials * @param token Token @@ -218,6 +256,7 @@ export class LocalStorageManager { this.remove(LocalStorageManager.KEYS.MNEMONIC); this.remove(LocalStorageManager.KEYS.USER); this.remove(LocalStorageManager.KEYS.SUBSCRIPTION); + this.remove(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID); } public clearStorage(): void { diff --git a/react/features/base/meet/general/store/user/actions.ts b/react/features/base/meet/general/store/user/actions.ts index e86d378da69c..fe32971be35f 100644 --- a/react/features/base/meet/general/store/user/actions.ts +++ b/react/features/base/meet/general/store/user/actions.ts @@ -60,7 +60,7 @@ export function clearUser(): { export const initializeUser = (): ThunkAction => { return (dispatch) => { const localStorageManager = LocalStorageManager.instance; - const user = localStorageManager.getUser(); + const user = localStorageManager.getUser(); if (user) { dispatch(setUser(user)); diff --git a/react/features/base/meet/middlewares/meeting.middleware.ts b/react/features/base/meet/middlewares/meeting.middleware.ts index 387aea5f854f..f9cfb505251a 100644 --- a/react/features/base/meet/middlewares/meeting.middleware.ts +++ b/react/features/base/meet/middlewares/meeting.middleware.ts @@ -1,4 +1,3 @@ -import { UserSettings } from "@internxt/sdk/dist/shared/types/userSettings"; import { AnyAction, Dispatch, Middleware } from "redux"; import MiddlewareRegistry from "../../redux/MiddlewareRegistry"; @@ -131,7 +130,7 @@ export const refreshUserData = async (dispatch: Dispatch, force: bool const now = Date.now(); const lastRefreshTime = LocalStorageManager.instance.get(STORAGE_KEYS.LAST_USER_REFRESH, 0) ?? 0; const hasExpiredRefreshInterval = now - lastRefreshTime > USER_REFRESH_INTERVAL; - const currentUser = LocalStorageManager.instance.getUser(); + const currentUser = LocalStorageManager.instance.getUser(); const tokenNeedsRefresh = shouldRefreshToken(); if (!currentUser) { @@ -193,7 +192,7 @@ const shouldRefreshToken = (): boolean => { */ export const refreshUserAvatar = async (dispatch: Dispatch): Promise => { try { - const currentUser = LocalStorageManager.instance.getUser(); + const currentUser = LocalStorageManager.instance.getUser(); if (!currentUser?.avatar) { return; } diff --git a/react/features/base/meet/views/Conference/containers/VideoGalleryWrapper.tsx b/react/features/base/meet/views/Conference/containers/VideoGalleryWrapper.tsx index 229c78af9677..57b42007e3b3 100644 --- a/react/features/base/meet/views/Conference/containers/VideoGalleryWrapper.tsx +++ b/react/features/base/meet/views/Conference/containers/VideoGalleryWrapper.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from "react"; +import React from "react"; import { WithTranslation } from "react-i18next"; import { connect, useSelector } from "react-redux"; import { IReduxState } from "../../../../../app/types"; @@ -7,8 +7,7 @@ import { getCurrentConference } from "../../../../conference/functions"; import { translate } from "../../../../i18n/functions"; import { useAspectRatio } from "../../../general/hooks/useAspectRatio"; import { useE2EEActivation } from "../../../general/hooks/useE2EEActivation"; -import MeetingService from "../../../services/meeting.service"; -import { MeetingUser } from "../../../services/types/meeting.types"; +import { useParticipantAvatar } from "../../PreMeeting/hooks/useParticipantAvatar"; import VideoGallery from "../components/VideoGallery"; import VideoSpeaker from "../components/VideoSpeaker"; import { getParticipantsWithTracks } from "../utils"; @@ -28,52 +27,20 @@ const GalleryVideoWrapper = ({ videoMode, t, isE2EESupported, room }: GalleryVid const { containerStyle } = useAspectRatio(); useE2EEActivation(isE2EESupported); - const participants = useSelector((state: IReduxState) => getParticipantsWithTracks(state)); + useParticipantAvatar(); + const participants = useSelector(getParticipantsWithTracks); const flipX = useSelector((state: IReduxState) => state["features/base/settings"].localFlipX); const contStyle = videoMode === "gallery" ? containerStyle : {}; - const [meetingParticipants, setMeetingParticipants] = React.useState([]); - - useEffect(() => { - const fetchMeetingParticipants = async (): Promise => { - if (!room) return; - - try { - const meetingParticipantsData = await MeetingService.instance.getCurrentUsersInCall(room); - setMeetingParticipants(meetingParticipantsData); - } catch (error) { - console.error("Error fetching meeting participants:", error); - } - }; - - fetchMeetingParticipants(); - }, [room, participants?.length]); - - const participantsWithAvatar = useMemo(() => { - if (!participants || participants.length === 0) return []; - if (!meetingParticipants || meetingParticipants.length === 0) return participants; - - const avatarMap: Record = {}; - meetingParticipants.forEach((mp) => { - if (mp.userId) { - avatarMap[mp.userId] = mp.avatar; - } - }); - - return participants.map((participant) => ({ - ...participant, - avatarSource: avatarMap[participant.id], - })); - }, [participants, meetingParticipants]); return (
- +
- +
); @@ -87,7 +54,7 @@ function mapStateToProps(state: IReduxState, ownProps: OwnProps): MappedStatePro return { ...ownProps, isE2EESupported, - room + room, }; } diff --git a/react/features/base/meet/views/Conference/utils.ts b/react/features/base/meet/views/Conference/utils.ts index 1cdbb8a662f0..00d3bc5c0b46 100644 --- a/react/features/base/meet/views/Conference/utils.ts +++ b/react/features/base/meet/views/Conference/utils.ts @@ -38,6 +38,7 @@ export const getParticipantsWithTracks = (state: IReduxState) => { hidden: false, dominantSpeaker: participant.dominantSpeaker || false, raisedHand: hasRaisedHand(participant), + avatarSource: !!participant.loadableAvatarUrl ? participant.loadableAvatarUrl : participant.avatarURL, }; }) .filter((participant) => !participant.hidden); diff --git a/react/features/base/meet/views/PreMeeting/PreMeetingScreen.tsx b/react/features/base/meet/views/PreMeeting/PreMeetingScreen.tsx index 0d2d18223144..0e945072465e 100644 --- a/react/features/base/meet/views/PreMeeting/PreMeetingScreen.tsx +++ b/react/features/base/meet/views/PreMeeting/PreMeetingScreen.tsx @@ -20,11 +20,12 @@ import { updateSettings } from "../../../settings/actions"; import { getDisplayName } from "../../../settings/functions.web"; import { withPixelLineHeight } from "../../../styles/functions.web"; import MeetingButton from "../../general/containers/MeetingButton"; -import { logout } from "../../general/store/auth/actions"; +import { loginSuccess, logout } from "../../general/store/auth/actions"; import { setCreateRoomError } from "../../general/store/errors/actions"; import { useLocalStorage } from "../../LocalStorageManager"; import MeetingService from "../../services/meeting.service"; import { MeetingUser } from "../../services/types/meeting.types"; +import AuthModal from "../Home/containers/AuthModal"; import Header from "./components/Header"; import PreMeetingModal from "./components/PreMeetingModal"; import SecureMeetingMessage from "./components/SecureMeetingMessage"; @@ -202,6 +203,9 @@ const PreMeetingScreen = ({ const [isCreatingMeeting, setIsCreatingMeeting] = useState(false); const [meetingUsersData, setMeetingUsersData] = useState([]); const userData = useUserData(); + const [openLogin, setOpenLogin] = useState(true); + + const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); const storageManager = useLocalStorage(); const dispatch = useDispatch(); @@ -271,11 +275,6 @@ const PreMeetingScreen = ({ } }; - const handleRedirectToSignUp = () => { - // HARDCODED, MODIFY WHEN SIGN UP PAGE IS READY - window.location.href = "https://drive.internxt.com/new"; - }; - const updateNameInStorage = (name: string) => { try { const user = storageManager.getUser(); @@ -318,9 +317,15 @@ const PreMeetingScreen = ({ userData={userData} subscription={subscription} translate={t} - onLogin={handleRedirectToLogin} + onLogin={() => { + setOpenLogin(true); + setIsAuthModalOpen(true); + }} + onSignUp={() => { + setOpenLogin(false); + setIsAuthModalOpen(true); + }} onLogout={onLogout} - onSignUp={handleRedirectToSignUp} meetingButton={ isInNewMeeting ? ( - -
{isE2EESupported && }
+ setIsAuthModalOpen(false)} + onSignup={(credentials) => dispatch(loginSuccess(credentials))} + translate={t} + /> + ;
- {/* UNCOMMENT IN DEV MODE TO SEE OLD IMPLEMENTATION */} {/*
diff --git a/react/features/base/meet/views/PreMeeting/hooks/useParticipantAvatar.ts b/react/features/base/meet/views/PreMeeting/hooks/useParticipantAvatar.ts new file mode 100644 index 000000000000..f2138454c35b --- /dev/null +++ b/react/features/base/meet/views/PreMeeting/hooks/useParticipantAvatar.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { participantUpdated, setLoadableAvatarUrl } from "../../../../participants/actions"; +import { getLocalParticipant } from "../../../../participants/functions"; +import { useLocalStorage } from "../../../LocalStorageManager"; + +/** + * Custom hook to initialize and manage the local participant's avatar in a Jitsi conference. + * + * This hook automatically retrieves the user's avatar from localStorage and sets it for the + * local participant when they join a conference. It ensures the avatar is visible both locally + * and propagated to remote participants through Jitsi's signaling system. + * + * @description The hook performs the following operations: + * - Retrieves the user's avatar URL from localStorage + * - Sets the avatar for display in the local UI via setLoadableAvatarUrl + * - Updates the participant data with avatarURL for propagation to remote participants + * - Ensures the avatar is only set once per conference session + * + * @example + * ```typescript + * // Use in a conference component + * function ConferenceView() { + * useParticipantAvatar(); + * // ... rest of component + * } + * ``` + * + * @requires localStorage - Must have user data with avatar property stored + * @requires localParticipant - Must be called within a conference context + * + * @side-effects + * - Dispatches setLoadableAvatarUrl action to update UI + * - Dispatches participantUpdated action to sync with other participants + * - Automatically sends avatar command to remote participants via Jitsi + * + * @hook + * @since 1.0.0 + */ +export const useParticipantAvatar = () => { + const dispatch = useDispatch(); + const localStorage = useLocalStorage(); + const localParticipant = useSelector(getLocalParticipant); + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + if (localParticipant?.id && !initialized) { + const user = localStorage.getUser(); + + if (user?.avatar) { + dispatch(setLoadableAvatarUrl(localParticipant.id, user.avatar, true)); + dispatch( + participantUpdated({ + id: localParticipant.id, + loadableAvatarUrl: user.avatar, + avatarURL: user.avatar, + }) + ); + } + + setInitialized(true); + } + }, [localParticipant, initialized, dispatch, localStorage]); +};