video call wip
This commit is contained in:
		
							parent
							
								
									2e31bfdb03
								
							
						
					
					
						commit
						4b5cfa6516
					
				
							
								
								
									
										37
									
								
								EnotaryoPH/EnotaryoPH.Data/Constants/NotaryoEvent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								EnotaryoPH/EnotaryoPH.Data/Constants/NotaryoEvent.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| namespace EnotaryoPH.Data.Constants | ||||
| { | ||||
|     public enum NotaryoEvent | ||||
|     { | ||||
|         Unknown = 0, | ||||
|         PrincipalRegistered = 1, | ||||
|         SignatoryRegistered = 2, | ||||
|         WitnessRegistered = 3, | ||||
|         LawyerRegistered = 4, | ||||
|         LawyerFingerprintScanned = 5, | ||||
| 
 | ||||
|         IdentificationDocumentUploaded = 10, | ||||
| 
 | ||||
|         SelfiePassed = 15, | ||||
|         SelfieFailed = 16, | ||||
| 
 | ||||
|         DocumentUploaded = 20, | ||||
| 
 | ||||
|         LawyerSelected = 30, | ||||
| 
 | ||||
|         TransactionSubmitted = 40, | ||||
|         TransactionApproved = 41, | ||||
|         TransactionRejected = 42, | ||||
| 
 | ||||
|         VideoConferenceStarted = 50, | ||||
|         VideoRecordingStarted = 51, | ||||
|         VideoRecordingStopped = 52, | ||||
| 
 | ||||
|         SignatoryApproved = 61, | ||||
|         WitnessApproved = 62, | ||||
| 
 | ||||
|         PaymentReceived = 70, | ||||
|         PaymentFailed = 71, | ||||
| 
 | ||||
|         TransactionCompleted = 100 | ||||
|     } | ||||
| } | ||||
| @ -6,6 +6,7 @@ | ||||
|         EmailSent = 1, | ||||
|         Registered = 2, | ||||
|         FaceMatch = 3, | ||||
|         Completed = 10 | ||||
|         Approved = 4, | ||||
|         Rejected = 5, | ||||
|     } | ||||
| } | ||||
| @ -7,6 +7,8 @@ | ||||
|         DocumentUploaded = 2, | ||||
|         Submitted = 3, | ||||
|         Accepted = 4, | ||||
|         Approved = 5, | ||||
|         Rejected = 6, | ||||
|         Completed = 100 | ||||
|     } | ||||
| } | ||||
| @ -9,7 +9,7 @@ namespace EnotaryoPH.Data.Entities | ||||
|         public int EventLogID { get; set; } | ||||
| 
 | ||||
|         [Column("StreamID")] | ||||
|         public int StreamID { get; set; } | ||||
|         public string StreamID { get; set; } | ||||
| 
 | ||||
|         [Column("LogType")] | ||||
|         public string LogType { get; set; } | ||||
|  | ||||
| @ -43,6 +43,9 @@ namespace EnotaryoPH.Data.Entities | ||||
|         [Column("Status")] | ||||
|         public string? Status { get; set; } | ||||
| 
 | ||||
|         [ForeignKey("TransactionID")] | ||||
|         public Transaction Transaction { get; set; } | ||||
| 
 | ||||
|         [Column("TransactionID")] | ||||
|         public int TransactionID { get; set; } | ||||
| 
 | ||||
|  | ||||
| @ -29,6 +29,8 @@ namespace EnotaryoPH.Data.Entities | ||||
|         [Column("PrincipalID")] | ||||
|         public int PrincipalID { get; set; } | ||||
| 
 | ||||
|         public LawyerVideoConferenceSchedule Schedule { get; set; } | ||||
| 
 | ||||
|         [Column("Status")] | ||||
|         public string Status { get; set; } | ||||
| 
 | ||||
|  | ||||
| @ -5,49 +5,52 @@ namespace EnotaryoPH.Data.Entities | ||||
|     [Table("Users")] | ||||
|     public class User | ||||
|     { | ||||
|         [Column("UserID")] | ||||
|         public int UserID { get; set; } | ||||
|         [Column("BirthDate")] | ||||
|         public DateTime BirthDate { get; set; } | ||||
| 
 | ||||
|         [Column("CreatedOn")] | ||||
|         public DateTime? CreatedOn { get; set; } | ||||
| 
 | ||||
|         [Column("Email")] | ||||
|         public string Email { get; set; } | ||||
| 
 | ||||
|         public List<EventLog> EventLogs { get; set; } | ||||
| 
 | ||||
|         [Column("Firstname")] | ||||
|         public string? Firstname { get; set; } | ||||
| 
 | ||||
|         [NotMapped] | ||||
|         public string Fullname => $"{Firstname} {Lastname}".Trim(); | ||||
| 
 | ||||
|         public List<IdentificationDocument> IdentificationDocuments { get; set; } | ||||
| 
 | ||||
|         [Column("Lastname")] | ||||
|         public string? Lastname { get; set; } | ||||
| 
 | ||||
|         public List<LawyerVideoConferenceParticipant> LawyerVideoConferenceParticipants { get; set; } | ||||
| 
 | ||||
|         [Column("Middlename")] | ||||
|         public string? Middlename { get; set; } | ||||
| 
 | ||||
|         [Column("PasswordHash")] | ||||
|         public string PasswordHash { get; set; } | ||||
| 
 | ||||
|         [Column("PhoneNumber")] | ||||
|         public string? PhoneNumber { get; set; } | ||||
| 
 | ||||
|         [Column("Firstname")] | ||||
|         public string? Firstname { get; set; } | ||||
| 
 | ||||
|         [Column("Lastname")] | ||||
|         public string? Lastname { get; set; } | ||||
| 
 | ||||
|         [Column("CreatedOn")] | ||||
|         public DateTime? CreatedOn { get; set; } | ||||
| 
 | ||||
|         [Column("User_UID")] | ||||
|         public Guid? User_UID { get; set; } | ||||
|         [Column("Prefix")] | ||||
|         public string? Prefix { get; set; } | ||||
| 
 | ||||
|         [Column("Role")] | ||||
|         public string? Role { get; set; } | ||||
| 
 | ||||
|         [Column("BirthDate")] | ||||
|         public DateTime BirthDate { get; set; } | ||||
| 
 | ||||
|         [Column("Middlename")] | ||||
|         public string? Middlename { get; set; } | ||||
| 
 | ||||
|         [Column("Suffix")] | ||||
|         public string? Suffix { get; set; } | ||||
| 
 | ||||
|         [Column("Prefix")] | ||||
|         public string? Prefix { get; set; } | ||||
|         [Column("User_UID")] | ||||
|         public Guid? User_UID { get; set; } | ||||
| 
 | ||||
|         public List<EventLog> EventLogs { get; set; } | ||||
| 
 | ||||
|         public List<IdentificationDocument> IdentificationDocuments { get; set; } | ||||
| 
 | ||||
|         public List<LawyerVideoConferenceParticipant> LawyerVideoConferenceParticipants { get; set; } | ||||
|         [Column("UserID")] | ||||
|         public int UserID { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -1,222 +1,301 @@ | ||||
| import { AzureCommunicationTokenCredential } from '@azure/communication-common'; | ||||
| import { AzureCommunicationTokenCredential, createIdentifierFromRawId } from '@azure/communication-common'; | ||||
| import { CallClient, LocalVideoStream, VideoStreamRenderer } from '@azure/communication-calling'; | ||||
| 
 | ||||
| class VideoCall { | ||||
|   constructor() { | ||||
|     this.callClient = null; | ||||
|     this.callAgent = null; | ||||
|     this.deviceManager = null; | ||||
|     this.localVideoStream = null; | ||||
|     this.call = null; | ||||
|     this.videoElement = document.createElement('video'); | ||||
|     this.videoElement.setAttribute('autoplay', ''); | ||||
|     this.videoElement.setAttribute('muted', ''); | ||||
|     //this.isLocalVideoStartedChangedCallback = null;
 | ||||
|     //this.localVideoStreamsUpdatedCallback = null;
 | ||||
|     //this.onCreateLocalVideoStreamCallback = null;
 | ||||
|     this.onGetServerCallIDCallback = null; | ||||
|     //this.remoteParticipantStateChangedCallback = null;
 | ||||
|     //this.remoteParticipantsUpdatedCallback = null;
 | ||||
|     //this.remoteVideoIsAvailableChangedCallback = null;
 | ||||
|     this.stateChangedCallback = null; | ||||
|     this.remoteParticipantsUpdated = null; | ||||
|     this.isLocalVideoStartedChanged = null; | ||||
|     this.localVideoStreamsUpdated = null; | ||||
|     this.idChanged = null; | ||||
|     this.onCreateLocalVideoStream = null; | ||||
|     this.remoteParticipantStateChanged = null; | ||||
|     this.remoteVideoIsAvailableChanged = null; | ||||
|     this.onGetServerCallID = null; | ||||
|     this.callEndedCallback = null; | ||||
|     this.participantsJoinedCallback = null; | ||||
|     this.callsUpdatedCallback = null; | ||||
| 
 | ||||
|     this.callAdapter = null; | ||||
|     this.videoContainer = null; | ||||
|     this.displayName = null; | ||||
|     this.roomID = null; | ||||
|     this.token = null; | ||||
|     this.userID = null; | ||||
| 
 | ||||
|     this.onFetchParticipantMenuItemsCallback = null; | ||||
|     this.onFetchCustomButtonPropsCallbacks = []; | ||||
|     this.onAddParticipantCallback = null; | ||||
|   } | ||||
| 
 | ||||
|   async init(userAccessToken, options) { | ||||
|   async init(options) { | ||||
|     let self = this; | ||||
|     this.stateChangedCallback = options.stateChangedCallback; | ||||
|     this.remoteParticipantsUpdated = options.remoteParticipantsUpdated; | ||||
|     this.isLocalVideoStartedChanged = options.isLocalVideoStartedChanged; | ||||
|     this.localVideoStreamsUpdated = options.localVideoStreamsUpdated; | ||||
|     this.idChanged = options.idChanged; | ||||
|     this.onCreateLocalVideoStream = options.onCreateLocalVideoStream; | ||||
|     this.remoteParticipantStateChanged = options.remoteParticipantStateChanged; | ||||
|     this.remoteVideoIsAvailableChanged = options.remoteVideoIsAvailableChanged; | ||||
|     this.onGetServerCallID = options.onGetServerCallID; | ||||
|     /*this.remoteParticipantsUpdatedCallback = options.remoteParticipantsUpdated;*/ | ||||
|     /*this.isLocalVideoStartedChangedCallback = options.isLocalVideoStartedChanged;*/ | ||||
|     /*this.localVideoStreamsUpdatedCallback = options.localVideoStreamsUpdated;*/ | ||||
|     this.idChangedCallback = options.idChanged; | ||||
|     //this.onCreateLocalVideoStreamCallback = options.onCreateLocalVideoStream;
 | ||||
|     //this.remoteParticipantStateChangedCallback = options.remoteParticipantStateChanged;
 | ||||
|     //this.remoteVideoIsAvailableChangedCallback = options.remoteVideoIsAvailableChanged;
 | ||||
|     this.onGetServerCallIDCallback = options.onGetServerCallIDCallback; | ||||
|     this.callEndedCallback = options.callEndedCallback; | ||||
|     this.participantsJoinedCallback = options.participantsJoinedCallback; | ||||
|     this.onFetchParticipantMenuItemsCallback = options.onFetchParticipantMenuItemsCallback; | ||||
|     this.onFetchCustomButtonPropsCallbacks = options.onFetchCustomButtonPropsCallbacks || []; | ||||
|     this.onAddParticipantCallback = options.onAddParticipantCallback; | ||||
| 
 | ||||
|     const tokenCredential = new AzureCommunicationTokenCredential(userAccessToken); | ||||
|     this.callAdapter = options.callAdapter; | ||||
|     this.videoContainer = options.videoContainer; | ||||
|     this.displayName = options.displayName; | ||||
|     this.roomID = options.roomID; | ||||
|     this.token = options.token; | ||||
|     this.userID = options.userID; | ||||
| 
 | ||||
|     this.callClient = new CallClient(); | ||||
|     this.callAgent = await this.callClient.createCallAgent(tokenCredential); | ||||
|     this.serverCallId; | ||||
| 
 | ||||
|     this.deviceManager = await this.callClient.getDeviceManager(); | ||||
|     await this.deviceManager.askDevicePermission({ audio: true, video: true }); | ||||
|     const cameras = await this.deviceManager.getCameras(); | ||||
|     this.localVideoStream = new LocalVideoStream(cameras[0]); | ||||
|   } | ||||
|     const callControls = { | ||||
|       // Hide all default buttons
 | ||||
|       cameraButton: true, | ||||
|       endCallButton: false, | ||||
|       microphoneButton: false, | ||||
|       participantsButton: true, | ||||
|       screenShareButton: false, | ||||
|       devicesButton: false, | ||||
|       moreButton: false, | ||||
|       raiseHandButton: false, | ||||
|       reactionButton: false, | ||||
|       dtmfDialerButton: false, | ||||
|       holdButton: false, | ||||
|       peopleButton: false, | ||||
|       exitSpotlightButton: false, | ||||
|       captionsButton: false, | ||||
|       galleryControlsButton: false, | ||||
|       teamsMeetingPhoneCallButton: false, | ||||
|       displayType: 'compact', | ||||
| 
 | ||||
|   stopLocalVideo() { | ||||
|     if (this.call) { | ||||
|       this.call.stopVideo(this.localVideoStream); | ||||
|     } | ||||
|   } | ||||
|       // Hide the entire control bar if needed
 | ||||
|       onFetchCustomButtonProps: this.onFetchCustomButtonPropsCallbacks | ||||
|     }; | ||||
| 
 | ||||
|   async joinRoom(roomId) { | ||||
|     // Assuming you have a room ID and call the appropriate ACS API to join a room
 | ||||
|     // This is just a placeholder, replace with actual logic
 | ||||
|     console.log('Joining room:', roomId); | ||||
|     if (this.callAgent) { | ||||
|       try { | ||||
|         const localVideoStream = await this.createLocalVideoStream(); | ||||
|         const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined; | ||||
| 
 | ||||
|         this.callAgent.on('callsUpdated', e => { | ||||
|           e.added.forEach((addedCall) => { | ||||
|             addedCall.on('stateChanged', () => { | ||||
|               if (addedCall.state === 'Connected') { | ||||
|                 addedCall.info.getServerCallId().then(result => { | ||||
|                   this.onGetServerCallID?.(result); | ||||
|                 }).catch(err => { | ||||
|                   console.log(err); | ||||
|                 }); | ||||
|               } | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         const roomCallLocator = { roomId: roomId }; | ||||
|         let call = this.callAgent.join(roomCallLocator, { videoOptions }); | ||||
|         this.subscribeToCall(call); | ||||
|       } catch (error) { | ||||
|         console.error(error); | ||||
|     const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); | ||||
|     const callCompositeProps = { | ||||
|       callControls: callControls, | ||||
|       formFactor: isMobile ? 'mobile' : 'desktop', | ||||
|       onFetchParticipantMenuItems: this.onFetchParticipantMenuItemsCallback, | ||||
|       options: { | ||||
|         callControls: callControls | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|     }; | ||||
| 
 | ||||
|   async subscribeToCall(call) { | ||||
|     try { | ||||
|       call.on('idChanged', () => { | ||||
|         this.idChanged?.(call.id); | ||||
|       }); | ||||
|       call.on('stateChanged', async () => { | ||||
|         this.stateChangedCallback?.(call.state); | ||||
|       }); | ||||
|       call.on('isLocalVideoStartedChanged', () => { | ||||
|         this.isLocalVideoStartedChanged?.(call.isLocalVideoStarted); | ||||
|       }); | ||||
|       call.on('localVideoStreamsUpdated', e => { | ||||
|         this.localVideoStreamsUpdated?.(e); | ||||
|       }); | ||||
|     const adapterArgs = { | ||||
|       userId: createIdentifierFromRawId(this.userID), | ||||
|       //credential: new AzureCommunicationTokenCredential(this.token),
 | ||||
|       token: this.token, | ||||
|       displayName: this.displayName, | ||||
|       locator: { roomId: this.roomID }, | ||||
|       callAdapterOptions: {}, | ||||
|       callCompositeOptions: callCompositeProps | ||||
|     }; | ||||
| 
 | ||||
|       // Subscribe to the call's 'remoteParticipantsUpdated' event to be
 | ||||
|       // notified when new participants are added to the call or removed from the call.
 | ||||
|       call.on('remoteParticipantsUpdated', e => { | ||||
|         this.remoteParticipantsUpdated?.(e); | ||||
|     this.callAdapter = await callComposite.loadCallComposite( | ||||
|       adapterArgs, | ||||
|       this.videoContainer, // container element,
 | ||||
|       callCompositeProps | ||||
|     ); | ||||
| 
 | ||||
|         e.added.forEach(remoteParticipant => { | ||||
|           this.subscribeToRemoteParticipant(remoteParticipant) | ||||
|     //this.callAdapter.callAgent.on("callsUpdated", function (e) {
 | ||||
|     //  e.added.forEach((addedCall) => {
 | ||||
|     //    addedCall.on('stateChanged', (state) => this.stateChanged(addedCall));
 | ||||
|     //  });
 | ||||
|     //});
 | ||||
| 
 | ||||
|     this.callAdapter.on("callIdChanged", function (e) { | ||||
|     }); | ||||
| 
 | ||||
|     this.callAdapter.onStateChange(state => { | ||||
|       if (state.call?.info && !this.serverCallId) { | ||||
|         state.call.info.getServerCallId().then(result => { | ||||
|           this.serverCallId = result; | ||||
|           this.onGetServerCallIDCallback?.(result); | ||||
|         }).catch(err => { | ||||
|           console.log(err); | ||||
|         }); | ||||
|         // Unsubscribe from participants that are removed from the call
 | ||||
|         e.removed.forEach(remoteParticipant => { | ||||
|           console.log('Remote participant removed from the call.'); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       call.localVideoStreams.forEach(async (lvs) => { | ||||
|         this.localVideoStream = lvs; | ||||
|         await this.displayLocalVideoStream(lvs); | ||||
|       }); | ||||
| 
 | ||||
|       // Inspect the call's current remote participants and subscribe to them.
 | ||||
|       call.remoteParticipants.forEach(remoteParticipant => { | ||||
|         this.subscribeToRemoteParticipant(remoteParticipant); | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error(error); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   subscribeToRemoteParticipant(remoteParticipant) { | ||||
|     try { | ||||
|       // Inspect the initial remoteParticipant.state value.
 | ||||
|       console.log(`Remote participant state: ${remoteParticipant.state}`); | ||||
|       // Subscribe to remoteParticipant's 'stateChanged' event for value changes.
 | ||||
|       remoteParticipant.on('stateChanged', () => { | ||||
|         console.log(`Remote participant state changed: ${remoteParticipant.state}`, JSON.stringify(remoteParticipant)); | ||||
|         this.remoteParticipantStateChanged?.(remoteParticipant.state); | ||||
|       }); | ||||
| 
 | ||||
|       // Inspect the remoteParticipants's current videoStreams and subscribe to them.
 | ||||
|       remoteParticipant.videoStreams.forEach(remoteVideoStream => { | ||||
|         this.subscribeToRemoteVideoStream(remoteVideoStream); | ||||
|       }); | ||||
|       // Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be
 | ||||
|       // notified when the remoteParticipant adds new videoStreams and removes video streams.
 | ||||
|       remoteParticipant.on('videoStreamsUpdated', e => { | ||||
|         // Subscribe to new remote participant's video streams that were added.
 | ||||
|         e.added.forEach(remoteVideoStream => { | ||||
|           this.subscribeToRemoteVideoStream(remoteVideoStream); | ||||
|         }); | ||||
|         // Unsubscribe from remote participant's video streams that were removed.
 | ||||
|         e.removed.forEach(remoteVideoStream => { | ||||
|           console.log('Remote participant video stream was removed.'); | ||||
|         }) | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error(error); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async subscribeToRemoteVideoStream(remoteVideoStream) { | ||||
|     let renderer = new VideoStreamRenderer(remoteVideoStream); | ||||
|     let view; | ||||
| 
 | ||||
|     const createView = async () => { | ||||
|       // Create a renderer view for the remote video stream.
 | ||||
|       view = await renderer.createView(); | ||||
|       this.remoteVideoIsAvailableChanged?.({ | ||||
|         isAvailable: remoteVideoStream.isAvailable, | ||||
|         participantId: remoteVideoStream.tsParticipantId, | ||||
|         el: view.target | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     // Remote participant has switched video on/off
 | ||||
|     remoteVideoStream.on('isAvailableChanged', async () => { | ||||
|       try { | ||||
|         if (remoteVideoStream.isAvailable) { | ||||
|           await createView(); | ||||
|         } else { | ||||
|           view?.dispose(); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         console.error(e); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     // Remote participant has video on initially.
 | ||||
|     if (remoteVideoStream.isAvailable) { | ||||
|       try { | ||||
|         await createView(); | ||||
|       } catch (e) { | ||||
|         console.error(e); | ||||
|       } | ||||
|     } | ||||
|     this.callAdapter.on("callEnded", function (e) { | ||||
|       self.callEndedCallback?.(e); | ||||
|     }); | ||||
| 
 | ||||
|     this.callAdapter.on("participantsJoined", function (e, f) { | ||||
|       self.participantsJoinedCallback?.(e, f); | ||||
|     }); | ||||
| 
 | ||||
|     this.callAdapter.on("onAddParticipant", function (e, f) { | ||||
|       self.onAddParticipantCallback?.(e, f); | ||||
|     }); | ||||
| 
 | ||||
|     //CallEnded
 | ||||
| 
 | ||||
|     return this.callAdapter; | ||||
|   } | ||||
| 
 | ||||
|   async createLocalVideoStream() { | ||||
|     const camera = (await this.deviceManager.getCameras())[0]; | ||||
|     if (camera) { | ||||
|       return new LocalVideoStream(camera); | ||||
|     } else { | ||||
|       console.error(`No camera device found on the system`); | ||||
|     } | ||||
|   //stopLocalVideo() {
 | ||||
|   //  if (this.call) {
 | ||||
|   //    this.call.stopVideo(this.localVideoStream);
 | ||||
|   //  }
 | ||||
|   //}
 | ||||
| 
 | ||||
|   async joinRoom() { | ||||
|     await this.callAdapter?.joinCall({ | ||||
|       microphoneOn: true, | ||||
|       cameraOn: true | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async stopCall(forEveryone = false) { | ||||
|     await this.callAdapter?.leaveCall(forEveryone); | ||||
|   } | ||||
| 
 | ||||
|   //async subscribeToCall(call) {
 | ||||
|   //  try {
 | ||||
|   //    call.on('idChanged', () => {
 | ||||
|   //      this.idChanged?.(call.id);
 | ||||
|   //    });
 | ||||
|   //    call.on('stateChanged', async () => {
 | ||||
|   //      this.stateChangedCallback?.(call.state);
 | ||||
|   //    });
 | ||||
|   //    call.on('isLocalVideoStartedChanged', () => {
 | ||||
|   //      this.isLocalVideoStartedChanged?.(call.isLocalVideoStarted);
 | ||||
|   //    });
 | ||||
|   //    call.on('localVideoStreamsUpdated', e => {
 | ||||
|   //      this.localVideoStreamsUpdated?.(e);
 | ||||
|   //    });
 | ||||
| 
 | ||||
|   //    // Subscribe to the call's 'remoteParticipantsUpdated' event to be
 | ||||
|   //    // notified when new participants are added to the call or removed from the call.
 | ||||
|   //    call.on('remoteParticipantsUpdated', e => {
 | ||||
|   //      this.remoteParticipantsUpdated?.(e);
 | ||||
| 
 | ||||
|   //      e.added.forEach(remoteParticipant => {
 | ||||
|   //        this.subscribeToRemoteParticipant(remoteParticipant)
 | ||||
|   //      });
 | ||||
|   //      // Unsubscribe from participants that are removed from the call
 | ||||
|   //      e.removed.forEach(remoteParticipant => {
 | ||||
|   //        console.log('Remote participant removed from the call.');
 | ||||
|   //      });
 | ||||
|   //    });
 | ||||
| 
 | ||||
|   //    call.localVideoStreams.forEach(async (lvs) => {
 | ||||
|   //      this.localVideoStream = lvs;
 | ||||
|   //      await this.displayLocalVideoStream(lvs);
 | ||||
|   //    });
 | ||||
| 
 | ||||
|   //    // Inspect the call's current remote participants and subscribe to them.
 | ||||
|   //    call.remoteParticipants.forEach(remoteParticipant => {
 | ||||
|   //      this.subscribeToRemoteParticipant(remoteParticipant);
 | ||||
|   //    });
 | ||||
|   //  } catch (error) {
 | ||||
|   //    console.error(error);
 | ||||
|   //  }
 | ||||
|   //}
 | ||||
| 
 | ||||
|   //subscribeToRemoteParticipant(remoteParticipant) {
 | ||||
|   //  try {
 | ||||
|   //    // Inspect the initial remoteParticipant.state value.
 | ||||
|   //    console.log(`Remote participant state: ${remoteParticipant.state}`);
 | ||||
|   //    // Subscribe to remoteParticipant's 'stateChanged' event for value changes.
 | ||||
|   //    remoteParticipant.on('stateChanged', () => {
 | ||||
|   //      console.log(`Remote participant state changed: ${remoteParticipant.state}`, JSON.stringify(remoteParticipant));
 | ||||
|   //      this.remoteParticipantStateChanged?.(remoteParticipant.state);
 | ||||
|   //    });
 | ||||
| 
 | ||||
|   //    // Inspect the remoteParticipants's current videoStreams and subscribe to them.
 | ||||
|   //    remoteParticipant.videoStreams.forEach(remoteVideoStream => {
 | ||||
|   //      this.subscribeToRemoteVideoStream(remoteVideoStream);
 | ||||
|   //    });
 | ||||
|   //    // Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be
 | ||||
|   //    // notified when the remoteParticipant adds new videoStreams and removes video streams.
 | ||||
|   //    remoteParticipant.on('videoStreamsUpdated', e => {
 | ||||
|   //      // Subscribe to new remote participant's video streams that were added.
 | ||||
|   //      e.added.forEach(remoteVideoStream => {
 | ||||
|   //        this.subscribeToRemoteVideoStream(remoteVideoStream);
 | ||||
|   //      });
 | ||||
|   //      // Unsubscribe from remote participant's video streams that were removed.
 | ||||
|   //      e.removed.forEach(remoteVideoStream => {
 | ||||
|   //        console.log('Remote participant video stream was removed.');
 | ||||
|   //      })
 | ||||
|   //    });
 | ||||
|   //  } catch (error) {
 | ||||
|   //    console.error(error);
 | ||||
|   //  }
 | ||||
|   //}
 | ||||
| 
 | ||||
|   //async subscribeToRemoteVideoStream(remoteVideoStream) {
 | ||||
|   //  let renderer = new VideoStreamRenderer(remoteVideoStream);
 | ||||
|   //  let view;
 | ||||
| 
 | ||||
|   //  const createView = async () => {
 | ||||
|   //    // Create a renderer view for the remote video stream.
 | ||||
|   //    view = await renderer.createView();
 | ||||
|   //    this.remoteVideoIsAvailableChanged?.({
 | ||||
|   //      isAvailable: remoteVideoStream.isAvailable,
 | ||||
|   //      participantId: remoteVideoStream.tsParticipantId,
 | ||||
|   //      el: view.target
 | ||||
|   //    });
 | ||||
|   //  }
 | ||||
| 
 | ||||
|   //  // Remote participant has switched video on/off
 | ||||
|   //  remoteVideoStream.on('isAvailableChanged', async () => {
 | ||||
|   //    try {
 | ||||
|   //      if (remoteVideoStream.isAvailable) {
 | ||||
|   //        await createView();
 | ||||
|   //      } else {
 | ||||
|   //        view?.dispose();
 | ||||
|   //      }
 | ||||
|   //    } catch (e) {
 | ||||
|   //      console.error(e);
 | ||||
|   //    }
 | ||||
|   //  });
 | ||||
| 
 | ||||
|   //  // Remote participant has video on initially.
 | ||||
|   //  if (remoteVideoStream.isAvailable) {
 | ||||
|   //    try {
 | ||||
|   //      await createView();
 | ||||
|   //    } catch (e) {
 | ||||
|   //      console.error(e);
 | ||||
|   //    }
 | ||||
|   //  }
 | ||||
|   //}
 | ||||
| 
 | ||||
|   //async createLocalVideoStream() {
 | ||||
|   //  const camera = (await this.deviceManager.getCameras())[0];
 | ||||
|   //  if (camera) {
 | ||||
|   //    return new LocalVideoStream(camera);
 | ||||
|   //  } else {
 | ||||
|   //    console.error(`No camera device found on the system`);
 | ||||
|   //  }
 | ||||
|   //}
 | ||||
| 
 | ||||
|   leaveRoom() { | ||||
|     if (this.call) { | ||||
|       this.call.hangUp(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async displayLocalVideoStream(lvs) { | ||||
|     try { | ||||
|       let localVideoStreamRenderer = new VideoStreamRenderer(lvs); | ||||
|       const view = await localVideoStreamRenderer.createView(); | ||||
|       this.onCreateLocalVideoStream?.(view.target); | ||||
|     } catch (error) { | ||||
|       console.error(error); | ||||
|     } | ||||
|   } | ||||
|   //createIdentifierFromRawId(rawId) {
 | ||||
|   //  return createIdentifierFromRawId(rawId);
 | ||||
|   //}
 | ||||
| 
 | ||||
|   //async displayLocalVideoStream(lvs) {
 | ||||
|   //  try {
 | ||||
|   //    let localVideoStreamRenderer = new VideoStreamRenderer(lvs);
 | ||||
|   //    const view = await localVideoStreamRenderer.createView();
 | ||||
|   //    this.onCreateLocalVideoStream?.(view.target);
 | ||||
|   //  } catch (error) {
 | ||||
|   //    console.error(error);
 | ||||
|   //  }
 | ||||
|   //}
 | ||||
| } | ||||
| 
 | ||||
| export default VideoCall; | ||||
| @ -0,0 +1,39 @@ | ||||
| namespace EnotaryoPH.Web.Common.Exceptions | ||||
| { | ||||
|     public class NoDataException : Exception | ||||
|     { | ||||
|         public NoDataException(string typeName, object id) : base($"No data found for {typeName} with id = '{id}'.") | ||||
|         { | ||||
|             TypeName = typeName; | ||||
|             ID = id; | ||||
|             Key = string.Empty; | ||||
|         } | ||||
| 
 | ||||
|         public NoDataException(string typeName, object id, string key) : base($"No data found for {typeName} with {key} = '{id}'.") | ||||
|         { | ||||
|             TypeName = typeName; | ||||
|             ID = id; | ||||
|             Key = key; | ||||
|         } | ||||
| 
 | ||||
|         public static void ThrowIfNull(object data, string typeName, object id) | ||||
|         { | ||||
|             if (data == null) | ||||
|             { | ||||
|                 throw new NoDataException(typeName, id); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public static void ThrowIfNull(object data, string typeName, object id, string key) | ||||
|         { | ||||
|             if (data == null) | ||||
|             { | ||||
|                 throw new NoDataException(typeName, id, key); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public object ID { get; private set; } | ||||
|         public string Key { get; private set; } | ||||
|         public string TypeName { get; private set; } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| namespace EnotaryoPH.Web.Common.Extensions | ||||
| { | ||||
|     public static class IEnumerableExtensions | ||||
|     { | ||||
|         public static IEnumerable<T> ToSafeEnumerable<T>(this IEnumerable<T> enumerable) => enumerable ?? Enumerable.Empty<T>(); | ||||
| 
 | ||||
|         public static List<T> ToSafeList<T>(this IEnumerable<T> enumerable) => enumerable?.ToList() ?? []; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,11 @@ | ||||
| namespace EnotaryoPH.Web.Common.Extensions | ||||
| { | ||||
|     public static class ObjectExtensions | ||||
|     { | ||||
|         public static int ToInteger(this object obj) | ||||
|         { | ||||
|             int.TryParse(obj.ToString(), out var result); | ||||
|             return result; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -6,7 +6,7 @@ | ||||
|         private const char Plus = '+'; | ||||
|         private const char Slash = '/'; | ||||
| 
 | ||||
|         public static string DefaultIfEmpty(this string s, string defaultValue) => !string.IsNullOrWhiteSpace(s) ? s : (defaultValue ?? string.Empty); | ||||
|         public static string DefaultIfEmpty(this string s, string defaultValue) => !string.IsNullOrWhiteSpace(s) ? s : defaultValue ?? string.Empty; | ||||
| 
 | ||||
|         public static bool IsInList(this string s, params string[] list) => list.Contains(s, StringComparer.OrdinalIgnoreCase); | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,85 @@ | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using Azure.Communication.CallAutomation; | ||||
| using Azure.Storage.Queues; | ||||
| using Coravel.Invocable; | ||||
| using EnotaryoPH.Data; | ||||
| using EnotaryoPH.Data.Entities; | ||||
| using EnotaryoPH.Web.Common.Jobs.Models; | ||||
| 
 | ||||
| namespace EnotaryoPH.Web.Common.Jobs | ||||
| { | ||||
|     public class CheckRecordingAvailabilityInvocable : IInvocable | ||||
|     { | ||||
|         private readonly QueueClient _queueClient; | ||||
|         private readonly CallAutomationClient _callAutomationClient; | ||||
|         private readonly IConfiguration _configuration; | ||||
|         private readonly IServiceScopeFactory _serviceScopeFactory; | ||||
| 
 | ||||
|         public CheckRecordingAvailabilityInvocable(QueueClient queueClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IServiceScopeFactory serviceScopeFactory) | ||||
|         { | ||||
|             _queueClient = queueClient; | ||||
|             _callAutomationClient = callAutomationClient; | ||||
|             _configuration = configuration; | ||||
|             _serviceScopeFactory = serviceScopeFactory; | ||||
|         } | ||||
| 
 | ||||
|         public async Task Invoke() | ||||
|         { | ||||
|             var message = await _queueClient.ReceiveMessageAsync(); | ||||
|             if (message.Value == null) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
|             var base64EncodedData = message.Value.Body.ToString(); | ||||
|             var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); | ||||
|             var options = new JsonSerializerOptions | ||||
|             { | ||||
|                 PropertyNameCaseInsensitive = true | ||||
|             }; | ||||
|             var json = Encoding.UTF8.GetString(base64EncodedBytes); | ||||
|             var model = JsonSerializer.Deserialize<RecordingFileStatusUpdatedModel>(json, options); | ||||
|             if (model?.Data?.RecordingStorageInfo?.RecordingChunks?.Count > 0) | ||||
|             { | ||||
|                 var dbContext = _serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService<NotaryoDBContext>(); | ||||
|                 model.Data.RecordingStorageInfo.RecordingChunks.ForEach(async chunk => | ||||
|                 { | ||||
|                     var path = _configuration.GetValue<string>("VideoRecordingsLocation"); | ||||
|                     if (!Path.Exists(path)) | ||||
|                     { | ||||
|                         Directory.CreateDirectory(path); | ||||
|                     } | ||||
|                     var fileName = Path.Combine(path, $"{chunk.DocumentId}-{chunk.Index}.mp4"); | ||||
|                     if (File.Exists(fileName)) | ||||
|                     { | ||||
|                         File.Move(fileName, fileName.Replace(".mp4", $"_{DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm-dd")}.mp4")); | ||||
|                     } | ||||
| 
 | ||||
|                     using var memoryStream = new MemoryStream(); | ||||
|                     await _callAutomationClient | ||||
|                         .GetCallRecording().DownloadToAsync(new Uri(chunk.ContentLocation), fileName); | ||||
| 
 | ||||
|                     var schedule = dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.RecordingID == model.Data.RecordingId); | ||||
|                     if (schedule != null) | ||||
|                     { | ||||
|                         if (schedule.VideoRecording == null) | ||||
|                         { | ||||
|                             schedule.VideoRecording = new VideoRecording | ||||
|                             { | ||||
|                                 CreatedOn = DateTime.UtcNow, | ||||
|                                 LocationType = nameof(VideoRecordingLocationType.LocalFolder), | ||||
|                                 VideoConferenceScheduleID = schedule.LawyerVideoConferenceScheduleID, | ||||
|                                 VideoRecording_UID = Guid.CreateVersion7(DateTime.UtcNow) | ||||
|                             }; | ||||
|                         } | ||||
|                         schedule.VideoRecording.Path = fileName; | ||||
|                         schedule.VideoRecording.Metadata = JsonSerializer.Serialize(json); | ||||
|                         dbContext.UpdateOrCreate(schedule); | ||||
|                         dbContext.SaveChanges(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             await _queueClient.DeleteMessageAsync(message.Value.MessageId, message.Value.PopReceipt); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										12
									
								
								EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/Data.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/Data.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| namespace EnotaryoPH.Web.Common.Jobs.Models | ||||
| { | ||||
|     public class Data | ||||
|     { | ||||
|         public int RecordingDurationMs { get; set; } | ||||
|         public string RecordingId { get; set; } | ||||
|         public DateTime RecordingStartTime { get; set; } | ||||
|         public RecordingStorageInfo RecordingStorageInfo { get; set; } | ||||
|         public string SessionEndReason { get; set; } | ||||
|         public string StorageType { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| namespace EnotaryoPH.Web.Common.Jobs.Models | ||||
| { | ||||
|     public class RecordingChunk | ||||
|     { | ||||
|         public string ContentLocation { get; set; } | ||||
|         public string DeleteLocation { get; set; } | ||||
|         public string DocumentId { get; set; } | ||||
|         public string EndReason { get; set; } | ||||
|         public int Index { get; set; } | ||||
|         public string MetadataLocation { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,14 @@ | ||||
| namespace EnotaryoPH.Web.Common.Jobs.Models | ||||
| { | ||||
|     public class RecordingFileStatusUpdatedModel | ||||
|     { | ||||
|         public Data Data { get; set; } | ||||
|         public string DataVersion { get; set; } | ||||
|         public DateTime EventTime { get; set; } | ||||
|         public string EventType { get; set; } | ||||
|         public string Id { get; set; } | ||||
|         public string MetadataVersion { get; set; } | ||||
|         public string Subject { get; set; } | ||||
|         public string Topic { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| namespace EnotaryoPH.Web.Common.Jobs.Models | ||||
| { | ||||
|     public class RecordingStorageInfo | ||||
|     { | ||||
|         public List<RecordingChunk> RecordingChunks { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -1,175 +0,0 @@ | ||||
| using Azure; | ||||
| using Azure.Communication; | ||||
| using Azure.Communication.CallAutomation; | ||||
| using Azure.Communication.Identity; | ||||
| using Azure.Communication.Rooms; | ||||
| using EnotaryoPH.Data; | ||||
| using EnotaryoPH.Data.Entities; | ||||
| 
 | ||||
| namespace EnotaryoPH.Web.Common.Services | ||||
| { | ||||
|     public class ConferenceSheduleService : IConferenceSheduleService | ||||
|     { | ||||
|         private readonly NotaryoDBContext _dbContext; | ||||
|         private readonly CommunicationIdentityClient _communicationIdentityClient; | ||||
|         private readonly RoomsClient _roomsClient; | ||||
|         private readonly CallAutomationClient _callAutomationClient; | ||||
|         private readonly IConfiguration _configuration; | ||||
| 
 | ||||
|         public ConferenceSheduleService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration) | ||||
|         { | ||||
|             _dbContext = dbContext; | ||||
|             _communicationIdentityClient = communicationIdentityClient; | ||||
|             _roomsClient = roomsClient; | ||||
|             _callAutomationClient = callAutomationClient; | ||||
|             _configuration = configuration; | ||||
|         } | ||||
| 
 | ||||
|         public async Task<Guid> GetOrCreateScheduleIDAsync(Guid transaction_UID) | ||||
|         { | ||||
|             if (transaction_UID == Guid.Empty) | ||||
|             { | ||||
|                 return Guid.Empty; | ||||
|             } | ||||
| 
 | ||||
|             var transactionEntity = _dbContext.Transactions | ||||
|                 .Include(t => t.TransactionSignatories) | ||||
|                 .FirstOrDefault(t => t.Transaction_UID == transaction_UID); | ||||
|             if (transactionEntity == null) | ||||
|             { | ||||
|                 return Guid.Empty; | ||||
|             } | ||||
| 
 | ||||
|             var existingSchedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); | ||||
|             if (existingSchedule != null) | ||||
|             { | ||||
|                 return existingSchedule.LawyerVideoConferenceSchedule_UID; | ||||
|             } | ||||
| 
 | ||||
|             var schedule_UID = Guid.Empty; | ||||
|             var isReadyForVideoCall = transactionEntity.TransactionSignatories.All(signatory => signatory.Status == nameof(SignatoryStatus.FaceMatch)); | ||||
|             var isAcceptedByLawyer = transactionEntity.Status == nameof(TransactionState.Accepted) && transactionEntity.LawyerID > 0; | ||||
|             if (isReadyForVideoCall && isAcceptedByLawyer) | ||||
|             { | ||||
|                 var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); | ||||
|                 if (schedule == null) | ||||
|                 { | ||||
|                     var participants = transactionEntity.TransactionSignatories.ConvertAll(signatory => new LawyerVideoConferenceParticipant | ||||
|                     { | ||||
|                         CreatedOn = DateTime.UtcNow, | ||||
|                         Status = nameof(VideoConferenceStatus.New), | ||||
|                         LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), | ||||
|                         ParticipantID = signatory.UserID, | ||||
|                     }); | ||||
| 
 | ||||
|                     participants.Add(new LawyerVideoConferenceParticipant | ||||
|                     { | ||||
|                         CreatedOn = DateTime.UtcNow, | ||||
|                         Status = nameof(VideoConferenceStatus.New), | ||||
|                         LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), | ||||
|                         ParticipantID = transactionEntity.PrincipalID, | ||||
|                     }); | ||||
| 
 | ||||
|                     schedule = new LawyerVideoConferenceSchedule | ||||
|                     { | ||||
|                         LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(DateTime.UtcNow), | ||||
|                         CreatedOn = DateTime.UtcNow, | ||||
|                         LawyerID = transactionEntity.LawyerID.GetValueOrDefault(), | ||||
|                         TransactionID = transactionEntity.TransactionID, | ||||
|                         MeetingDate = DateTime.UtcNow, | ||||
|                         Status = nameof(VideoConferenceStatus.New), | ||||
|                     }; | ||||
| 
 | ||||
|                     var roomParticipants = new List<RoomParticipant>(); | ||||
|                     foreach (var participant in participants) | ||||
|                     { | ||||
|                         var attendee = await _communicationIdentityClient.CreateUserAsync(); | ||||
|                         participant.MeetingRoomTokenID = await GetTokenResponseAsync(attendee); | ||||
|                         participant.MeetingRoomUserID = attendee.Value.Id; | ||||
|                         roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee }); | ||||
|                     } | ||||
| 
 | ||||
|                     var presenter = await _communicationIdentityClient.CreateUserAsync(); | ||||
|                     schedule.MeetingRoomTokenID = await GetTokenResponseAsync(presenter); | ||||
|                     schedule.MeetingRoomUserID = presenter.Value.Id; | ||||
|                     roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter }); | ||||
| 
 | ||||
|                     CommunicationRoom room = await _roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants); | ||||
|                     schedule.MeetingRoomID = room.Id; | ||||
|                     schedule.LawyerVideoConferenceParticipants = participants.ToList(); | ||||
| 
 | ||||
|                     if (schedule.LawyerVideoConferenceScheduleID == 0) | ||||
|                     { | ||||
|                         _dbContext.Add(schedule); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         _dbContext.Update(schedule); | ||||
|                     } | ||||
|                     _dbContext.SaveChanges(); | ||||
|                     schedule_UID = schedule.LawyerVideoConferenceSchedule_UID; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return schedule_UID; | ||||
|         } | ||||
| 
 | ||||
|         public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID) | ||||
|         { | ||||
|             var transactionEntity = _dbContext.Transactions | ||||
|                 .FirstOrDefault(t => t.Transaction_UID == transaction_UID) ?? throw new ArgumentException("Transaction not found."); | ||||
| 
 | ||||
|             var existingSchedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID) ?? throw new ArgumentException("Schedule not found."); | ||||
|             if (string.IsNullOrEmpty(existingSchedule.ServerCallID) && string.IsNullOrEmpty(serverCallID)) | ||||
|             { | ||||
|                 throw new ArgumentException("ServerCallID is not set for this transaction."); | ||||
|             } | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(existingSchedule.RecordingID)) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(serverCallID)) | ||||
|             { | ||||
|                 existingSchedule.ServerCallID = serverCallID; | ||||
|             } | ||||
| 
 | ||||
|             CallLocator callLocator = new ServerCallLocator(existingSchedule.ServerCallID); | ||||
|             var uri = _configuration.GetValue<string>("UriRecordingBloblContainer") ?? string.Empty; | ||||
|             var recordingResult = await _callAutomationClient | ||||
|                 .GetCallRecording().StartAsync(new StartRecordingOptions(callLocator) | ||||
|                 { | ||||
|                     RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri)) | ||||
|                 }); | ||||
|             existingSchedule.RecordingID = recordingResult.Value.RecordingId; | ||||
|             _dbContext.Update(existingSchedule); | ||||
|             _dbContext.SaveChanges(); | ||||
|         } | ||||
| 
 | ||||
|         public async Task StopRecordingAsync(Guid transaction_UID) | ||||
|         { | ||||
|             var transactionEntity = _dbContext.Transactions | ||||
|                 .FirstOrDefault(t => t.Transaction_UID == transaction_UID) ?? throw new ArgumentException("Transaction not found."); | ||||
| 
 | ||||
|             var existingSchedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID) ?? throw new ArgumentException("Schedule not found."); | ||||
|             if (string.IsNullOrEmpty(existingSchedule.ServerCallID)) | ||||
|             { | ||||
|                 throw new ArgumentException("ServerCallID is not set for this transaction."); | ||||
|             } | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(existingSchedule.RecordingID)) | ||||
|             { | ||||
|                 throw new ArgumentException("Recording ID is not set for this transaction."); | ||||
|             } | ||||
| 
 | ||||
|             await _callAutomationClient.GetCallRecording().StopAsync(existingSchedule.RecordingID); | ||||
|         } | ||||
| 
 | ||||
|         private async Task<string> GetTokenResponseAsync(Response<CommunicationUserIdentifier> user) | ||||
|         { | ||||
|             var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP }); | ||||
|             return tokenResponse.Value.Token; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										63
									
								
								EnotaryoPH/EnotaryoPH.Web/Common/Services/EventService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								EnotaryoPH/EnotaryoPH.Web/Common/Services/EventService.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| using System.Text.Json; | ||||
| using EnotaryoPH.Data; | ||||
| using EnotaryoPH.Data.Entities; | ||||
| 
 | ||||
| namespace EnotaryoPH.Web.Common.Services | ||||
| { | ||||
|     public class EventService : IEventService | ||||
|     { | ||||
|         private readonly NotaryoDBContext _dBContext; | ||||
|         private readonly INotificationService _notificationService; | ||||
| 
 | ||||
|         public EventService(NotaryoDBContext dBContext, INotificationService notificationService) | ||||
|         { | ||||
|             _dBContext = dBContext; | ||||
|             _notificationService = notificationService; | ||||
|         } | ||||
| 
 | ||||
|         public Task LogAsync(NotaryoEvent notaryoEvent, object entityId) => LogAsync(notaryoEvent, entityId, null); | ||||
| 
 | ||||
|         public async Task LogAsync(NotaryoEvent notaryoEvent, List<object> entityIds, object payLoad) | ||||
|         { | ||||
|             foreach (var entityId in entityIds) | ||||
|             { | ||||
|                 await LogAsync(notaryoEvent, entityId, payLoad); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public async Task LogAsync(NotaryoEvent notaryoEvent, object entityId, object payLoad) | ||||
|         { | ||||
|             var logItem = new EventLog | ||||
|             { | ||||
|                 Description = $"Event: {notaryoEvent}, Entity: {entityId}", | ||||
|                 EventLog_UID = Guid.CreateVersion7(DateTime.UtcNow), | ||||
|                 LogDate = DateTime.UtcNow, | ||||
|                 LogType = notaryoEvent.ToString(), | ||||
|                 Payload = payLoad != null ? JsonSerializer.Serialize(payLoad) : null, | ||||
|                 StreamID = entityId.ToString(), | ||||
|             }; | ||||
|             if (notaryoEvent == NotaryoEvent.TransactionApproved) | ||||
|             { | ||||
|                 var transaction = _dBContext.Transactions.AsNoTracking() | ||||
|                     .Include(t => t.TransactionSignatories) | ||||
|                     .ThenInclude(ts => ts.User) | ||||
|                     .Include(t => t.Lawyer) | ||||
|                     .ThenInclude(l => l.User) | ||||
|                     .Include(t => t.Principal) | ||||
|                     .Include(t => t.TransactionDocument) | ||||
|                     .FirstOrDefault(t => t.TransactionID == entityId.ToInteger()); | ||||
|                 logItem.Description = $"Transaction {entityId} has been approved."; | ||||
|                 if (transaction != null) | ||||
|                 { | ||||
|                     var message = $"The document {transaction.TransactionDocument.DocumentType} has been approved."; | ||||
|                     var allUsers = transaction.TransactionSignatories.ConvertAll(ts => ts.User.Email); | ||||
|                     allUsers.Add(transaction.Lawyer.User.Email); | ||||
|                     allUsers.Add(transaction.Principal.Email); | ||||
|                     await _notificationService.NotifyUsersAsync(message, allUsers.ToArray()); | ||||
|                 } | ||||
|             } | ||||
|             _dBContext.Add(logItem); | ||||
|             _dBContext.SaveChanges(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,11 +0,0 @@ | ||||
| namespace EnotaryoPH.Web.Common.Services | ||||
| { | ||||
|     public interface IConferenceSheduleService | ||||
|     { | ||||
|         Task<Guid> GetOrCreateScheduleIDAsync(Guid transaction_UID); | ||||
| 
 | ||||
|         Task StartRecordingAsync(Guid transaction_UID, string serverCallID); | ||||
| 
 | ||||
|         Task StopRecordingAsync(Guid transaction_UID); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								EnotaryoPH/EnotaryoPH.Web/Common/Services/IEventService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								EnotaryoPH/EnotaryoPH.Web/Common/Services/IEventService.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| namespace EnotaryoPH.Web.Common.Services | ||||
| { | ||||
|     public interface IEventService | ||||
|     { | ||||
|         Task LogAsync(NotaryoEvent notaryoEvent, List<object> entityIds, object payLoad); | ||||
| 
 | ||||
|         Task LogAsync(NotaryoEvent notaryoEvent, object entityId); | ||||
| 
 | ||||
|         Task LogAsync(NotaryoEvent notaryoEvent, object entityId, object payLoad); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
|  | ||||
| namespace EnotaryoPH.Web.Common.Services | ||||
| { | ||||
|     public interface INotificationService | ||||
|     { | ||||
|         Task NotifyAllAsync(string message); | ||||
|         Task NotifyUserAsync(string message, string userID); | ||||
|         Task NotifyUsersAsync(string message, params string[] userIDs); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,17 @@ | ||||
| namespace EnotaryoPH.Web.Common.Services | ||||
| { | ||||
|     public interface IVideoConferenceService | ||||
|     { | ||||
|         Task ApproveTransactionAsync(Guid transaction_UID); | ||||
| 
 | ||||
|         bool CanStart(Guid transaction_UID); | ||||
| 
 | ||||
|         Guid GetUIDByTransactionUID(Guid transaction_UID); | ||||
| 
 | ||||
|         bool HasExpired(Guid transaction_UID); | ||||
| 
 | ||||
|         Task<Guid> StartAsync(Guid transaction_UID); | ||||
| 
 | ||||
|         Task StartRecordingAsync(Guid transaction_UID, string serverCallID); | ||||
|     } | ||||
| } | ||||
| @ -3,12 +3,16 @@ using Microsoft.AspNetCore.SignalR; | ||||
| 
 | ||||
| namespace EnotaryoPH.Web.Common.Services | ||||
| { | ||||
|     public class NotificationService | ||||
|     public class NotificationService : INotificationService | ||||
|     { | ||||
|         private readonly IHubContext<NotificationHub> _hubContext; | ||||
| 
 | ||||
|         public NotificationService(IHubContext<NotificationHub> hubContext) => _hubContext = hubContext; | ||||
| 
 | ||||
|         public async Task NotifyUserAsync(string user_UID, string message) => await _hubContext.Clients.All.SendAsync("ReceiveUserNotification", user_UID, message); | ||||
|         public async Task NotifyAllAsync(string message) => await _hubContext.Clients.All.SendAsync("ReceiveUserNotification", message); | ||||
| 
 | ||||
|         public async Task NotifyUserAsync(string message, string userID) => await NotifyUsersAsync(message, userID); | ||||
| 
 | ||||
|         public async Task NotifyUsersAsync(string message, params string[] userIDs) => await _hubContext.Clients.Users(userIDs).SendAsync("ReceiveUserNotification", message); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,256 @@ | ||||
| using Azure; | ||||
| using Azure.Communication; | ||||
| using Azure.Communication.CallAutomation; | ||||
| using Azure.Communication.Identity; | ||||
| using Azure.Communication.Rooms; | ||||
| using EnotaryoPH.Data; | ||||
| using EnotaryoPH.Data.Entities; | ||||
| 
 | ||||
| namespace EnotaryoPH.Web.Common.Services | ||||
| { | ||||
|     public class VideoConferenceService : IVideoConferenceService | ||||
|     { | ||||
|         private const int VideoConferenceExpirationInHours = 2; | ||||
| 
 | ||||
|         private readonly CallAutomationClient _callAutomationClient; | ||||
|         private readonly CommunicationIdentityClient _communicationIdentityClient; | ||||
|         private readonly IConfiguration _configuration; | ||||
|         private readonly NotaryoDBContext _dbContext; | ||||
|         private readonly IEventService _eventService; | ||||
|         private readonly RoomsClient _roomsClient; | ||||
| 
 | ||||
|         public VideoConferenceService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IEventService eventService) | ||||
|         { | ||||
|             _dbContext = dbContext; | ||||
|             _communicationIdentityClient = communicationIdentityClient; | ||||
|             _roomsClient = roomsClient; | ||||
|             _callAutomationClient = callAutomationClient; | ||||
|             _configuration = configuration; | ||||
|             _eventService = eventService; | ||||
|         } | ||||
| 
 | ||||
|         public async Task ApproveTransactionAsync(Guid transaction_UID) | ||||
|         { | ||||
|             var transaction = _dbContext.Transactions | ||||
|                 .Include(t => t.TransactionSignatories) | ||||
|                 .ThenInclude(ts => ts.User) | ||||
|                 .Include(t => t.Schedule) | ||||
|                 .Include(t => t.Lawyer) | ||||
|                 .ThenInclude(l => l.User) | ||||
|                 .Include(t => t.Principal) | ||||
|                 .FirstOrDefault(t => t.Transaction_UID == transaction_UID); | ||||
|             NoDataException.ThrowIfNull(transaction, nameof(Transaction), transaction_UID); | ||||
|             transaction.Status = nameof(TransactionState.Approved); | ||||
|             transaction.TransactionSignatories.ForEach(ts => ts.Status = nameof(SignatoryStatus.Approved)); | ||||
|             transaction.Schedule.Status = nameof(VideoConferenceStatus.Completed); | ||||
|             _dbContext.Update(transaction); | ||||
|             _dbContext.SaveChanges(); | ||||
| 
 | ||||
|             await Task.WhenAll( | ||||
|                 _eventService.LogAsync(NotaryoEvent.TransactionApproved, transaction_UID), | ||||
|                 StopRecordingAsync(transaction.Schedule) | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         public bool CanStart(Guid transaction_UID) | ||||
|         { | ||||
|             if (transaction_UID == Guid.Empty) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             var transactionEntity = _dbContext.Transactions | ||||
|                 .AsNoTracking() | ||||
|                 .Include(t => t.TransactionSignatories) | ||||
|                 .FirstOrDefault(t => t.Transaction_UID == transaction_UID); | ||||
|             if (transactionEntity == null) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             var isReadyForVideoCall = transactionEntity.TransactionSignatories.All(signatory => signatory.Status == nameof(SignatoryStatus.FaceMatch)); | ||||
|             var isAcceptedByLawyer = transactionEntity.Status == nameof(TransactionState.Accepted) && transactionEntity.LawyerID > 0; | ||||
|             return isReadyForVideoCall && isAcceptedByLawyer; | ||||
|         } | ||||
| 
 | ||||
|         public Guid GetUIDByTransactionUID(Guid transaction_UID) | ||||
|         { | ||||
|             var transactionEntity = _dbContext.Transactions | ||||
|                 .AsNoTracking() | ||||
|                 .FirstOrDefault(t => t.Transaction_UID == transaction_UID); | ||||
|             if (transactionEntity == null) | ||||
|             { | ||||
|                 return Guid.Empty; | ||||
|             } | ||||
| 
 | ||||
|             var schedule = _dbContext.LawyerVideoConferenceSchedules.AsNoTracking().FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); | ||||
|             return schedule == null ? Guid.Empty : schedule.LawyerVideoConferenceSchedule_UID; | ||||
|         } | ||||
| 
 | ||||
|         public bool HasExpired(Guid transaction_UID) | ||||
|         { | ||||
|             var transactionEntity = _dbContext.Transactions | ||||
|                 .Include(t => t.TransactionSignatories) | ||||
|                 .FirstOrDefault(t => t.Transaction_UID == transaction_UID); | ||||
|             if (transactionEntity == null) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|             var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); | ||||
|             if (schedule == null) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|             if (schedule.Status == nameof(VideoConferenceStatus.Expired)) | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
|             if ((DateTime.UtcNow - schedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours) | ||||
|             { | ||||
|                 if (!schedule.Status?.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed) ?? false) | ||||
|                 { | ||||
|                     schedule.Status = nameof(VideoConferenceStatus.Expired); | ||||
|                     _dbContext.Update(schedule); | ||||
|                     _dbContext.SaveChanges(); | ||||
|                 } | ||||
|                 return true; | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         public async Task<Guid> StartAsync(Guid transaction_UID) | ||||
|         { | ||||
|             if (!CanStart(transaction_UID)) | ||||
|             { | ||||
|                 throw new ArgumentException("Transaction is not ready for video conference."); | ||||
|             } | ||||
|             var schedule = GetOrCreateNewSchedule(transaction_UID); | ||||
|             if (schedule.Status == nameof(VideoConferenceStatus.New)) | ||||
|             { | ||||
|                 await CreateMeetingRoomAsync(schedule); | ||||
|                 _dbContext.UpdateOrCreate(schedule); | ||||
|                 _dbContext.SaveChanges(); | ||||
|             } | ||||
|             return schedule.LawyerVideoConferenceSchedule_UID; | ||||
|         } | ||||
| 
 | ||||
|         public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID) | ||||
|         { | ||||
|             var transactionEntity = _dbContext.Transactions | ||||
|                 .Include(t => t.TransactionSignatories) | ||||
|                 .Single(t => t.Transaction_UID == transaction_UID); | ||||
|             NoDataException.ThrowIfNull(transactionEntity, nameof(Transaction), transaction_UID); | ||||
| 
 | ||||
|             var schedule = _dbContext.LawyerVideoConferenceSchedules | ||||
|                 .FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); | ||||
|             NoDataException.ThrowIfNull(schedule, nameof(LawyerVideoConferenceSchedule), transactionEntity.TransactionID, FullName.Of(transactionEntity.TransactionID)); | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(schedule.RecordingID)) | ||||
|             { | ||||
|                 schedule.RecordingID = await StartRecordingAsync(serverCallID); | ||||
|                 schedule.ServerCallID = serverCallID; | ||||
|                 _dbContext.Update(schedule); | ||||
|                 _dbContext.SaveChanges(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private async Task CreateMeetingRoomAsync(LawyerVideoConferenceSchedule schedule) | ||||
|         { | ||||
|             var roomParticipants = new List<RoomParticipant>(); | ||||
|             foreach (var participant in schedule.LawyerVideoConferenceParticipants.ToSafeList()) | ||||
|             { | ||||
|                 var attendee = await _communicationIdentityClient.CreateUserAsync(); | ||||
|                 participant.MeetingRoomTokenID = await GetTokenResponseAsync(attendee); | ||||
|                 participant.MeetingRoomUserID = attendee.Value.Id; | ||||
|                 roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee }); | ||||
|             } | ||||
| 
 | ||||
|             var presenter = await _communicationIdentityClient.CreateUserAsync(); | ||||
|             schedule.MeetingRoomTokenID = await GetTokenResponseAsync(presenter); | ||||
|             schedule.MeetingRoomUserID = presenter.Value.Id; | ||||
|             roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter }); | ||||
| 
 | ||||
|             CommunicationRoom room = await _roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants); | ||||
|             schedule.MeetingRoomID = room.Id; | ||||
|             schedule.Status = nameof(VideoConferenceStatus.InProgress); | ||||
|         } | ||||
| 
 | ||||
|         private LawyerVideoConferenceSchedule GetOrCreateNewSchedule(Guid transaction_UID) | ||||
|         { | ||||
|             var transactionEntity = _dbContext.Transactions | ||||
|                 .Include(t => t.TransactionSignatories) | ||||
|                 .Single(t => t.Transaction_UID == transaction_UID); | ||||
| 
 | ||||
|             var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); | ||||
|             if (schedule != null) | ||||
|             { | ||||
|                 return schedule; | ||||
|             } | ||||
|             schedule = new LawyerVideoConferenceSchedule | ||||
|             { | ||||
|                 LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(DateTime.UtcNow), | ||||
|                 CreatedOn = DateTime.UtcNow, | ||||
|                 LawyerID = transactionEntity.LawyerID.GetValueOrDefault(), | ||||
|                 TransactionID = transactionEntity.TransactionID, | ||||
|                 Status = nameof(VideoConferenceStatus.New) | ||||
|             }; | ||||
| 
 | ||||
|             var participants = transactionEntity.TransactionSignatories.ConvertAll(signatory => new LawyerVideoConferenceParticipant | ||||
|             { | ||||
|                 CreatedOn = DateTime.UtcNow, | ||||
|                 Status = nameof(VideoConferenceStatus.New), | ||||
|                 LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), | ||||
|                 ParticipantID = signatory.UserID, | ||||
|             }); | ||||
|             participants.Add(new LawyerVideoConferenceParticipant | ||||
|             { | ||||
|                 CreatedOn = DateTime.UtcNow, | ||||
|                 Status = nameof(VideoConferenceStatus.New), | ||||
|                 LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), | ||||
|                 ParticipantID = transactionEntity.PrincipalID, | ||||
|             }); | ||||
| 
 | ||||
|             schedule.MeetingDate = DateTime.UtcNow; | ||||
|             schedule.LawyerVideoConferenceParticipants = participants.ToList(); | ||||
|             return schedule; | ||||
|         } | ||||
| 
 | ||||
|         private async Task<string> GetTokenResponseAsync(Response<CommunicationUserIdentifier> user) | ||||
|         { | ||||
|             var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP }); | ||||
|             return tokenResponse.Value.Token; | ||||
|         } | ||||
| 
 | ||||
|         private async Task<string> StartRecordingAsync(string serverCallID) | ||||
|         { | ||||
|             ArgumentException.ThrowIfNullOrWhiteSpace(serverCallID); | ||||
|             CallLocator callLocator = new ServerCallLocator(serverCallID); | ||||
|             var uri = _configuration.GetValue<string>("UriRecordingBlobContainer") ?? string.Empty; | ||||
|             var recordingResult = await _callAutomationClient | ||||
|                 .GetCallRecording().StartAsync(new StartRecordingOptions(callLocator) | ||||
|                 { | ||||
|                     RecordingContent = RecordingContent.AudioVideo, | ||||
|                     RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri)), | ||||
|                     RecordingFormat = RecordingFormat.Mp4 | ||||
|                 }); | ||||
|             return recordingResult.Value.RecordingId; | ||||
|         } | ||||
| 
 | ||||
|         private async Task StopRecordingAsync(LawyerVideoConferenceSchedule schedule) | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(schedule.ServerCallID)) | ||||
|             { | ||||
|                 Console.WriteLine("ServerCallID is not set for this transaction."); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(schedule.RecordingID)) | ||||
|             { | ||||
|                 Console.WriteLine("Recording ID is not set for this transaction."); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             await _callAutomationClient.GetCallRecording().StopAsync(schedule.RecordingID); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -11,6 +11,7 @@ | ||||
| 		<PackageReference Include="Azure.Communication.CallAutomation" Version="1.3.0" /> | ||||
| 		<PackageReference Include="Azure.Communication.Identity" Version="1.3.1" /> | ||||
| 		<PackageReference Include="Azure.Communication.Rooms" Version="1.1.1" /> | ||||
| 		<PackageReference Include="Azure.Storage.Queues" Version="12.22.0" /> | ||||
| 		<PackageReference Include="CompreFace.NET.Sdk" Version="1.0.2" /> | ||||
| 		<PackageReference Include="Coravel" Version="6.0.2" /> | ||||
| 		<PackageReference Include="Coravel.Mailer" Version="7.1.0" /> | ||||
|  | ||||
| @ -8,3 +8,19 @@ | ||||
|     <h1 class="display-4">Welcome</h1> | ||||
|     <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> | ||||
| </div> | ||||
| 
 | ||||
| <form method="post"> | ||||
|     <button onclick="onClickMe"> | ||||
|         click me | ||||
|     </button> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @section Scripts { | ||||
|     <script> | ||||
|         function onClickMe() { | ||||
|             alert('yoloooo'); | ||||
|         } | ||||
|     </script> | ||||
| } | ||||
| @ -1,4 +1,3 @@ | ||||
| using System.Text.Json; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | ||||
| 
 | ||||
| @ -7,12 +6,14 @@ namespace EnotaryoPH.Web.Pages | ||||
|     public class IndexModel : PageModel | ||||
|     { | ||||
|         private readonly ICurrentUserService _currentUserService; | ||||
|         private readonly NotificationService _notificationService; | ||||
|         private readonly INotificationService _notificationService; | ||||
|         private readonly IEventService _eventService; | ||||
| 
 | ||||
|         public IndexModel(ICurrentUserService currentUserService, NotificationService notificationService) | ||||
|         public IndexModel(ICurrentUserService currentUserService, INotificationService notificationService, IEventService eventService) | ||||
|         { | ||||
|             _currentUserService = currentUserService; | ||||
|             _notificationService = notificationService; | ||||
|             _eventService = eventService; | ||||
|         } | ||||
| 
 | ||||
|         public IActionResult OnGet() | ||||
| @ -34,5 +35,14 @@ namespace EnotaryoPH.Web.Pages | ||||
| 
 | ||||
|             return Page(); | ||||
|         } | ||||
| 
 | ||||
|         public async Task<IActionResult> OnPostAsync() | ||||
|         { | ||||
|             var message = new { Message = "the quick brown fox jumps over the lazy dog." }; | ||||
| 
 | ||||
|             await _eventService.LogAsync(NotaryoEvent.TransactionApproved, new Guid("0195dfd2-9048-77f1-9d9d-974828cb3e68")); | ||||
|             //await _notificationService.NotifyUserAsync("admin@enotaryo.ph", JsonSerializer.Serialize(message)); | ||||
|             return Redirect("/"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -9,15 +9,15 @@ namespace EnotaryoPH.Web.Pages.Notary.TransactionStatus | ||||
|     { | ||||
|         private const string STATUS_READY = "Ready"; | ||||
|         private readonly ICurrentUserService _currentUserService; | ||||
|         private readonly IConferenceSheduleService _conferenceSheduleService; | ||||
|         private readonly NotaryoDBContext _notaryoDBContext; | ||||
|         private readonly IVideoConferenceService _videoConferenceService; | ||||
|         private Transaction? _transaction; | ||||
| 
 | ||||
|         public IndexModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IConferenceSheduleService conferenceSheduleService) | ||||
|         public IndexModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IVideoConferenceService videoConferenceService) | ||||
|         { | ||||
|             _notaryoDBContext = notaryoDBContext; | ||||
|             _currentUserService = currentUserService; | ||||
|             _conferenceSheduleService = conferenceSheduleService; | ||||
|             _videoConferenceService = videoConferenceService; | ||||
|         } | ||||
| 
 | ||||
|         public IActionResult OnGet() | ||||
| @ -81,16 +81,12 @@ namespace EnotaryoPH.Web.Pages.Notary.TransactionStatus | ||||
|             _transaction.Status = nameof(TransactionState.Accepted); | ||||
|             _transaction.LawyerID = lawyer.LawyerID; | ||||
|             _notaryoDBContext.Update(_transaction); | ||||
|             _notaryoDBContext.SaveChanges(); | ||||
| 
 | ||||
|             var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); | ||||
|             if (schedule_UID != Guid.Empty) | ||||
|             { | ||||
|                 return _transaction.TransactionSignatories.TrueForAll(sig => sig.Status == nameof(SignatoryStatus.FaceMatch)) | ||||
|                     ? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}") | ||||
|                     : Redirect($"/Participant/VideoCall/Waiting/{Transaction_UID}"); | ||||
|             } | ||||
| 
 | ||||
|             return Redirect("/Notary/Dashboard"); | ||||
|             var canStart = _videoConferenceService.CanStart(Transaction_UID); | ||||
|             return canStart | ||||
|                 ? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}") | ||||
|                 : Redirect($"/Participant/VideoCall/Waiting/{Transaction_UID}"); | ||||
|         } | ||||
| 
 | ||||
|         private string ChangeStatusLabel(string status) => status switch { nameof(SignatoryStatus.FaceMatch) => STATUS_READY, _ => status }; | ||||
|  | ||||
| @ -6,98 +6,305 @@ | ||||
| } | ||||
| 
 | ||||
| @section Head { | ||||
|     <style> | ||||
|         .video-container { | ||||
|             position: relative; | ||||
|             padding-bottom: 56.25%; /* 16:9 aspect ratio */ | ||||
|             background: #000; | ||||
|             border: 2px solid #444; | ||||
| <style> | ||||
| 
 | ||||
|             width: 100%; | ||||
|             overflow: hidden; | ||||
|         } | ||||
| 		.modal-dialog-right { | ||||
| 			position: fixed; | ||||
| 			top: 0; | ||||
| 			bottom: 0; | ||||
| 			left: auto; | ||||
| 			right: 0; | ||||
| 			margin: 0; | ||||
| 			padding: 0; | ||||
| 			width: 350px; /* Adjust the width as needed */ | ||||
| 			height: 100%; | ||||
| 		} | ||||
| 
 | ||||
|         .video-container video { | ||||
|             position: absolute; | ||||
|             top: 0; | ||||
|             left: 0; | ||||
|             width: 100%; | ||||
|             height: 100%; | ||||
|             object-fit: cover; /* This will maintain the video's original aspect ratio and crop if necessary */ | ||||
|         } | ||||
| 		.modal-content-right { | ||||
| 			border-radius: 0; | ||||
| 			height: 100vh; | ||||
| 			width: 100%; | ||||
| 			background-color: #fff; | ||||
| 			box-shadow: -2px 0 5px rgba(0, 0, 0, 0.3); | ||||
| 		} | ||||
| 
 | ||||
|         .video-element { | ||||
|             position: absolute; | ||||
|             width: 100%; | ||||
|             height: 100%; | ||||
|             object-fit: cover; | ||||
|         } | ||||
| 		.list-group { | ||||
| 			list-style: none; | ||||
| 			padding: 0; | ||||
| 		} | ||||
| 
 | ||||
|         .participant-name { | ||||
|             position: absolute; | ||||
|             bottom: 10px; | ||||
|             left: 10px; | ||||
|             color: white; | ||||
|             background: rgba(0,0,0,0.5); | ||||
|             padding: 2px 8px; | ||||
|             border-radius: 4px; | ||||
|             z-index: 1; | ||||
|         } | ||||
| 		.list-group-item { | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 			gap: 10px; | ||||
| 			margin-bottom: 10px; | ||||
| 			padding: 10px; | ||||
| 			border-radius: 5px; | ||||
| /* 			background-color: #f9f9f9; */ | ||||
| /* 			box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */ | ||||
| 		} | ||||
| 
 | ||||
|         .controls { | ||||
|             position: fixed; | ||||
|             bottom: 20px; | ||||
|             left: 50%; | ||||
|             /* transform: translateX(-50%); */ | ||||
|             background: rgba(0,0,0,0.8); | ||||
|             padding: 10px; | ||||
|             border-radius: 20px; | ||||
|         } | ||||
| 		.avatar { | ||||
| 			width: 40px; | ||||
| 			height: 40px; | ||||
| 			border-radius: 50%; | ||||
| 			background-color: #007bff; | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 			justify-content: center; | ||||
| 			color: white; | ||||
| 			font-weight: bold; | ||||
| 			text-transform: uppercase; | ||||
| 		} | ||||
| 
 | ||||
|         .participant-col { | ||||
|             transition: all 0.3s ease; | ||||
| 
 | ||||
|             display:flex; | ||||
|             justify-content: center; | ||||
|             align-items: center; | ||||
|         } | ||||
|     </style> | ||||
| 		.modal-header.draggable { | ||||
| 			cursor: move; | ||||
| 		} | ||||
| </style> | ||||
| } | ||||
| 
 | ||||
| <div class="container-fluid py-3" id="videoGrid-container"> | ||||
|     <div class="d-flex mb-1"> | ||||
|         <div> | ||||
|             <span>32:04</span> | ||||
|         </div> | ||||
|         <div class="flex-fill"></div> | ||||
|         <div> | ||||
|             <a href="#" id="ViewDocument" class="btn btn-sm btn-secondary">View Document</a> | ||||
|             <a href="#" id="Approve" class="btn btn-sm btn-success">Approve</a> | ||||
|             <a href="#" id="Reject" class="btn btn-sm btn-danger">Reject</a> | ||||
|         </div> | ||||
| 
 | ||||
|     </div> | ||||
|     <div class="row g-2" id="VideoGrid"> | ||||
|     </div> | ||||
| </div> | ||||
| 
 | ||||
| <template id="templateVideo"> | ||||
|     <div class="video-container bg-light"> | ||||
|         <div class="participant-name">Participant Name</div> | ||||
|     </div> | ||||
| </template> | ||||
| <div class="vh-100" id="videoGrid-container"></div> | ||||
| 
 | ||||
| <input type="hidden" id="Participants" value="@(System.Web.HttpUtility.JavaScriptStringEncode(JsonSerializer.Serialize(Model.Participants)).Replace("\\", ""))" /> | ||||
| <input type="hidden" id="CommunicationUserToken" value="@Model.CommunicationUserToken" /> | ||||
| <input type="hidden" id="CommunicationUserId" value="@Model.CommunicationUserId" /> | ||||
| <input type="hidden" id="CommunicationRoomId" value="@Model.CommunicationRoomId" /> | ||||
| <input type="hidden" asp-for="CommunicationUserToken" /> | ||||
| <input type="hidden" asp-for="CommunicationUserId" /> | ||||
| <input type="hidden" asp-for="CommunicationRoomId" /> | ||||
| <input type="hidden" asp-for="DisplayName" /> | ||||
| <input type="hidden" asp-for="ParticipantType" /> | ||||
| 
 | ||||
| <form method="post" asp-page-handler="StartRecording"> | ||||
|     <input type="hidden" asp-for="ServerCallID" /> | ||||
|     <input type="hidden" asp-for="Transaction_UID" /> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <div class="modal fade right" id="RightSidebarModal" tabindex="-1" role="dialog" aria-labelledby="rightSidebarModalLabel" aria-hidden="true"> | ||||
| 	<div class="modal-dialog modal-dialog-right" role="document"> | ||||
| 		<div class="modal-content modal-content-right"> | ||||
| 			<!-- Modal Header --> | ||||
| 			<div class="modal-header"> | ||||
| 				<h5 class="modal-title" id="rightSidebarModalLabel">Participants</h5> | ||||
| 				<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="modal"></button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<!-- Modal Body --> | ||||
| 			<div class="modal-body"> | ||||
| 				<ul class="list-group" id="ParticipantListGroup"> | ||||
| 					<li class="list-group-item"> | ||||
| 						<div class="avatar">JD</div> | ||||
| 						John Doe | ||||
| 					</li> | ||||
| 					<li class="list-group-item"> | ||||
| 						<div class="avatar">SM</div> | ||||
| 						Sarah Miller | ||||
| 					</li> | ||||
| 					<li class="list-group-item"> | ||||
| 						<div class="avatar">AM</div> | ||||
| 						Alex Martin | ||||
| 					</li> | ||||
| 					<li class="list-group-item"> | ||||
| 						<div class="avatar">CW</div> | ||||
| 						Chris Wilson | ||||
| 					</li> | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 
 | ||||
| 
 | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| 
 | ||||
| <template id="TemplateParticipantItem"> | ||||
| 	<li class="list-group-item participant-item" data-participant-uid=""> | ||||
| 		<div class="avatar participant-avatar">JD</div> | ||||
| 		<span class="participant-name">John Doe</span> | ||||
| 	</li> | ||||
| </template> | ||||
| 
 | ||||
| 
 | ||||
| <div class="modal fade " id="DraggableModal" data-bs-backdrop="false"> | ||||
| 	<div class="modal-dialog modal-lg"> | ||||
| 		<div class="modal-content"> | ||||
| 
 | ||||
| 			<!-- Modal Header --> | ||||
| 			<div class="modal-header draggable"> | ||||
| 				<h4 class="modal-title">Signatory Name</h4> | ||||
| 				<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<!-- Modal Body --> | ||||
| 			<div class="modal-body"> | ||||
| 
 | ||||
| 				<!-- Nav tabs --> | ||||
| 				<ul class="nav nav-tabs"> | ||||
| 					<li class="nav-item"> | ||||
| 						<a class="nav-link active" data-bs-toggle="tab" href="#image1">Image 1</a> | ||||
| 					</li> | ||||
| 					<li class="nav-item"> | ||||
| 						<a class="nav-link" data-bs-toggle="tab" href="#image2">Image 2</a> | ||||
| 					</li> | ||||
| 				</ul> | ||||
| 
 | ||||
| 				<!-- Tab panes --> | ||||
| 				<div class="tab-content"> | ||||
| 					<div id="image1" class="container tab-pane active"> | ||||
| 						<br> | ||||
| 						<img src="https://placehold.co/600x400?text=Hello" alt="Image 1" class="img-fluid"> | ||||
| 					</div> | ||||
| 					<div id="image2" class="container tab-pane fade"> | ||||
| 						<br> | ||||
| 						<img src="https://placehold.co/600x400?text=World" alt="Image 2" class="img-fluid"> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 
 | ||||
| 			</div> | ||||
| 
 | ||||
| 			<!-- Modal Footer --> | ||||
| 			<div class="modal-footer justify-content-start"> | ||||
|                 <div class="flex-fill"> | ||||
|                     <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Approve</button> | ||||
|                     <button type="button" class="btn btn-danger" data-bs-dismiss="modal">Reject</button> | ||||
| 				</div> | ||||
| 				<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| 
 | ||||
| @* <template id="TemplateSidePane"> | ||||
| 	<div data-is-focusable="false" aria-modal="true" data-ui-id="SidePaneSignatories" class="ms-Stack css-366"> | ||||
| 		<div class="ms-Stack css-367"> | ||||
| 			<div role="heading" aria-label="People" aria-level="2" class="ms-StackItem css-368">Signatories</div> | ||||
| 			<div class="ms-StackItem css-157"> | ||||
| 				<button type="button" class="ms-Button ms-Button--commandBar root-369" aria-label="Close" data-is-focusable="true"> | ||||
| 					<span class="ms-Button-flexContainer flexContainer-193" data-automationid="splitbuttonprimary"> | ||||
| 						<i data-icon-name="cancel" aria-hidden="true" class="ms-Icon root-89 css-196 ms-Button-icon icon-370" style="font-family: FabricMDL2Icons;"></i> | ||||
| 					</span> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="ms-StackItem css-373"> | ||||
| 			<div class="ms-Stack css-374"> | ||||
| 				<div class="ms-StackItem css-375"> | ||||
| 					<div class="ms-Stack css-252"> | ||||
| 						<div data-ui-id="people-pane-content" class="ms-Stack css-376"> | ||||
| 							<div class="ms-Stack css-162"> | ||||
| 								<div aria-live="assertive" role="status" aria-atomic="true" class="ms-Stack css-241" /> | ||||
| 							</div> | ||||
| 							<div class="ms-StackItem css-377"> | ||||
| 								<div class="ms-Stack css-378"> | ||||
| 									<div class="ms-Stack css-379"> | ||||
| 										<div aria-label="In this call {numberOfPeople}" id="id__427" class="ms-StackItem css-380"> | ||||
| 											<h2 id="SignatoryCount">In this call (2)</h2> | ||||
| 										</div> | ||||
| 										<div class="ms-StackItem css-157"> | ||||
| 											<button type="button" data-ui-id="people-pane-header-more-button" class="ms-Button ms-Button--default ms-Button--hasMenu root-381" aria-label="More" data-is-focusable="true" aria-expanded="false" aria-haspopup="true"> | ||||
| 												<span class="ms-Button-flexContainer flexContainer-193" data-automationid="splitbuttonprimary"> | ||||
| 													<i data-icon-name="PeoplePaneMoreButton" aria-hidden="true" class="ms-Icon root-89 ms-Button-icon icon-382"> | ||||
| 														<svg fill="currentColor" class="___12fm75w f1w7gpdv fez10in fg4l7m0" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> | ||||
| 															<path d="M6.75 10a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0Zm5 0a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM15 11.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z" fill="currentColor" /> | ||||
| 														</svg> | ||||
| 													</i> | ||||
| 												</span> | ||||
| 											</button> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 									<div class="ms-Stack css-384"> | ||||
| 										<div id="SignatoryList" data-ui-id="participant-list" class="ms-Stack css-386" role="menu"> | ||||
| 											<div role="menuitem" id="id__436" aria-label="principal3@jfaquinojr.com, , Muted, , , , " aria-labelledby="id__427 id__436" aria-expanded="true" aria-disabled="true" aria-controls="id__437" data-is-focusable="false" data-ui-id="participant-item" class="css-392"> | ||||
| 												<div class="ms-Stack css-394"> | ||||
| 													<div data-ui-id="chat-composite-participant-custom-avatar" class="ms-Persona ms-Persona--size32 root-396"> | ||||
| 														<div role="presentation" class="ms-Persona-coin ms-Persona--size32 coin-261"> | ||||
| 															<div role="presentation" class="ms-Persona-imageArea imageArea-399"> | ||||
| 																<div class="ms-Persona-initials initials-402" aria-hidden="true"> | ||||
| 																	<span>P</span> | ||||
| 																</div> | ||||
| 															</div> | ||||
| 														</div> | ||||
| 													</div> | ||||
| 													<div style="padding: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"> | ||||
| 														<div class="ms-TooltipHost root-404" role="none"> | ||||
| 															<span aria-labelledby="text-tooltip428" class="css-405">principal3@jfaquinojr.com</span> | ||||
| 															<div hidden="" id="text-tooltip428" style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;">principal3@jfaquinojr.com</div> | ||||
| 														</div> | ||||
| 													</div> | ||||
| 													<div class="ms-Stack css-406"> | ||||
| 														<div class="ms-Stack css-407"> | ||||
| 															<i data-icon-name="ParticipantItemMicOff" role="img" aria-label="Muted" class="root-408"> | ||||
| 																<svg fill="currentColor" class="___12fm75w f1w7gpdv fez10in fg4l7m0" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> | ||||
| 																	<path d="M12 5v4.88l.9.9A3 3 0 0 0 13 10V5a3 3 0 0 0-6-.12l1 1V5a2 2 0 1 1 4 0ZM7 7.7 2.15 2.86a.5.5 0 1 1 .7-.7l15 15a.5.5 0 0 1-.7.7l-3.63-3.62a5.48 5.48 0 0 1-3.02 1.25v2.02a.5.5 0 0 1-1 0v-2.02a5.5 5.5 0 0 1-5-5.48.5.5 0 0 1 1 0 4.5 4.5 0 0 0 7.3 3.52l-1.06-1.07A3 3 0 0 1 7 10V7.7Zm4.02 4.02L8 8.71V10a2 2 0 0 0 3.02 1.72Zm3.78.96-.74-.74c.28-.59.44-1.25.44-1.94a.5.5 0 0 1 1 0c0 .97-.25 1.89-.7 2.68Z" fill="currentColor" /> | ||||
| 																</svg> | ||||
| 															</i> | ||||
| 														</div> | ||||
| 													</div> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 											<div role="menuitem" id="id__442" aria-label="Notary One, , , , , , " aria-labelledby="id__427 id__442" aria-expanded="false" aria-disabled="true" aria-controls="id__443" data-is-focusable="false" data-ui-id="participant-item" class="css-392"> | ||||
| 												<div class="ms-Stack css-394"> | ||||
| 													<div data-ui-id="chat-composite-participant-custom-avatar" class="ms-Persona ms-Persona--size32 root-396"> | ||||
| 														<div role="presentation" class="ms-Persona-coin ms-Persona--size32 coin-261"> | ||||
| 															<div role="presentation" class="ms-Persona-imageArea imageArea-399"> | ||||
| 																<div class="ms-Persona-initials initials-409" aria-hidden="true"> | ||||
| 																	<span>NO</span> | ||||
| 																</div> | ||||
| 															</div> | ||||
| 														</div> | ||||
| 													</div> | ||||
| 													<div style="padding: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"> | ||||
| 														<div class="ms-TooltipHost root-404" role="none"> | ||||
| 															<span aria-labelledby="text-tooltip428" class="css-405">Notary One</span> | ||||
| 															<div hidden="" id="text-tooltip428" style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;">Notary One</div> | ||||
| 														</div> | ||||
| 													</div> | ||||
| 													<span class="css-410">(you)</span> | ||||
| 													<div class="ms-Stack css-406" /> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </template> | ||||
| 
 | ||||
| <template id="TemplateSignatoryItem"> | ||||
| 	<div role="menuitem" class="css-392"> | ||||
| 		<div class="ms-Stack css-394"> | ||||
| 			<div class="ms-Persona ms-Persona--size32 root-396"> | ||||
| 				<div role="presentation" class="ms-Persona-coin ms-Persona--size32 coin-261"> | ||||
| 					<div role="presentation" class="ms-Persona-imageArea imageArea-399"> | ||||
| 						<div class="ms-Persona-initials initials-402" aria-hidden="true"> | ||||
| 							<span class="signatory-initials">X</span> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div style="padding: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"> | ||||
| 				<div class="ms-TooltipHost root-404" role="none"> | ||||
| 					<span aria-labelledby="text-tooltip428" class="css-405 signatory-name">principal3@jfaquinojr.com</span> | ||||
| 					<div hidden="" class="signatory-tooltip" style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;">principal3@jfaquinojr.com</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="ms-Stack css-406"> | ||||
| 				<div class="ms-Stack css-407"> | ||||
| 					<i data-icon-name="ParticipantItemMicOff" role="img" aria-label="Muted" class="root-408"> | ||||
| 						<svg fill="currentColor" class="___12fm75w f1w7gpdv fez10in fg4l7m0" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> | ||||
| 							<path d="M12 5v4.88l.9.9A3 3 0 0 0 13 10V5a3 3 0 0 0-6-.12l1 1V5a2 2 0 1 1 4 0ZM7 7.7 2.15 2.86a.5.5 0 1 1 .7-.7l15 15a.5.5 0 0 1-.7.7l-3.63-3.62a5.48 5.48 0 0 1-3.02 1.25v2.02a.5.5 0 0 1-1 0v-2.02a5.5 5.5 0 0 1-5-5.48.5.5 0 0 1 1 0 4.5 4.5 0 0 0 7.3 3.52l-1.06-1.07A3 3 0 0 1 7 10V7.7Zm4.02 4.02L8 8.71V10a2 2 0 0 0 3.02 1.72Zm3.78.96-.74-.74c.28-.59.44-1.25.44-1.94a.5.5 0 0 1 1 0c0 .97-.25 1.89-.7 2.68Z" fill="currentColor" /> | ||||
| 						</svg> | ||||
| 					</i> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </template> | ||||
|  *@ | ||||
| @section Scripts { | ||||
|     <script type="text/javascript" src="/dist/_jfa.js"></script> | ||||
|     <script type="text/javascript" src="/lib/azure-communication-service/callComposite.js"></script> | ||||
|     <script src="~/Pages/Participant/VideoCall/Room.cshtml.js" asp-append-version="true"></script> | ||||
| } | ||||
|  | ||||
| @ -7,18 +7,17 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall | ||||
| { | ||||
|     public class RoomModel : PageModel | ||||
|     { | ||||
|         private const int VideoConferenceExpirationInHours = 2; | ||||
|         private readonly IConferenceSheduleService _conferenceSheduleService; | ||||
|         private readonly ICurrentUserService _currentUserService; | ||||
|         private readonly NotaryoDBContext _dbContext; | ||||
|         private LawyerVideoConferenceSchedule _LawyerVideoConferenceSchedule; | ||||
|         private readonly IVideoConferenceService _videoConferenceService; | ||||
|         private Transaction _Transaction; | ||||
| 
 | ||||
|         public RoomModel(ICurrentUserService currentUserService, NotaryoDBContext dbContext, IConferenceSheduleService conferenceSheduleService) | ||||
|         public RoomModel(ICurrentUserService currentUserService, NotaryoDBContext dbContext, | ||||
|             IVideoConferenceService videoConferenceService) | ||||
|         { | ||||
|             _currentUserService = currentUserService; | ||||
|             _dbContext = dbContext; | ||||
|             _conferenceSheduleService = conferenceSheduleService; | ||||
|             _videoConferenceService = videoConferenceService; | ||||
|         } | ||||
| 
 | ||||
|         public async Task<IActionResult> OnGetAsync() | ||||
| @ -30,8 +29,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall | ||||
|             } | ||||
| 
 | ||||
|             var currentUser = _dbContext.Users.Single(u => u.User_UID == _currentUserService.GetUser_UID()); | ||||
|             var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); | ||||
|             if (schedule_UID == Guid.Empty) | ||||
|             var canStart = _videoConferenceService.CanStart(Transaction_UID); | ||||
|             if (!canStart) | ||||
|             { | ||||
|                 if (_Transaction.PrincipalID == currentUser.UserID) | ||||
|                 { | ||||
| @ -46,59 +45,54 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall | ||||
|                 return Redirect("/"); | ||||
|             } | ||||
| 
 | ||||
|             LoadVideoConferenceSchedule(schedule_UID); | ||||
| 
 | ||||
|             if ((DateTime.UtcNow - _LawyerVideoConferenceSchedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours) | ||||
|             if (_videoConferenceService.HasExpired(Transaction_UID)) | ||||
|             { | ||||
|                 if (!_LawyerVideoConferenceSchedule.Status.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed)) | ||||
|                 { | ||||
|                     _LawyerVideoConferenceSchedule.Status = nameof(VideoConferenceStatus.Expired); | ||||
|                     _dbContext.Update(_LawyerVideoConferenceSchedule); | ||||
|                     _dbContext.SaveChanges(); | ||||
|                 } | ||||
| 
 | ||||
|                 TempData["Warning"] = "The video conference has expired."; | ||||
|                 return Redirect("/"); | ||||
|             } | ||||
| 
 | ||||
|             var schedule_UID = await _videoConferenceService.StartAsync(Transaction_UID); | ||||
| 
 | ||||
|             CommunicationUserToken = currentUser.Role == nameof(UserType.Notary) | ||||
|                 ? _LawyerVideoConferenceSchedule.MeetingRoomTokenID | ||||
|                 : _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomTokenID; | ||||
|             CommunicationRoomId = _LawyerVideoConferenceSchedule.MeetingRoomID; | ||||
|                 ? _Transaction.Schedule.MeetingRoomTokenID | ||||
|                 : GetParticipant(currentUser).MeetingRoomTokenID; | ||||
|             CommunicationRoomId = _Transaction.Schedule.MeetingRoomID; | ||||
|             CommunicationUserId = currentUser.Role == nameof(UserType.Notary) | ||||
|                 ? _LawyerVideoConferenceSchedule.MeetingRoomUserID | ||||
|                 : _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomUserID; | ||||
|                 ? _Transaction.Schedule.MeetingRoomUserID | ||||
|                 : GetParticipant(currentUser).MeetingRoomUserID; | ||||
|             DisplayName = currentUser.Role == nameof(UserType.Notary) | ||||
|                 ? _Transaction.Lawyer.User.Fullname | ||||
|                 : GetParticipant(currentUser).Participant.Fullname.DefaultIfEmpty(currentUser.Email); | ||||
|             ParticipantType = currentUser.Role == nameof(UserType.Notary) | ||||
|                 ? nameof(UserType.Notary) | ||||
|                 : GetParticipant(currentUser).Participant.Role; | ||||
| 
 | ||||
|             return Page(); | ||||
|         } | ||||
| 
 | ||||
|         public async Task OnPostStartRecordingAsync() | ||||
|         private LawyerVideoConferenceParticipant GetParticipant(User currentUser) => _Transaction.Schedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID); | ||||
| 
 | ||||
|         public async Task<JsonResult> OnPostApproveAsync() | ||||
|         { | ||||
|             LoadTransaction(); | ||||
|             var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); | ||||
|             LoadVideoConferenceSchedule(schedule_UID); | ||||
|             await _conferenceSheduleService.StartRecordingAsync(Transaction_UID, ServerCallID); | ||||
|             await _videoConferenceService.ApproveTransactionAsync(Transaction_UID); | ||||
|             return new JsonResult(true); | ||||
|         } | ||||
| 
 | ||||
|         public async Task OnPostStopRecordingAsync() | ||||
|         public async Task<JsonResult> OnPostStartRecordingAsync() | ||||
|         { | ||||
|             LoadTransaction(); | ||||
|             var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); | ||||
|             LoadVideoConferenceSchedule(schedule_UID); | ||||
|             await _conferenceSheduleService.StopRecordingAsync(Transaction_UID); | ||||
|             await _videoConferenceService.StartRecordingAsync(Transaction_UID, ServerCallID); | ||||
|             return new JsonResult(true); | ||||
|         } | ||||
| 
 | ||||
|         private void LoadTransaction() => _Transaction = _dbContext.Transactions | ||||
|                                         .Include(t => t.TransactionSignatories) | ||||
|                         .Include(t => t.Principal) | ||||
|                         .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); | ||||
| 
 | ||||
|         private void LoadVideoConferenceSchedule(Guid schedule_UID) => _LawyerVideoConferenceSchedule = _dbContext.LawyerVideoConferenceSchedules | ||||
|                         .Include(sched => sched.LawyerVideoConferenceParticipants) | ||||
|                         .ThenInclude(p => p.Participant) | ||||
|                         .Include(sched => sched.Lawyer) | ||||
|                         .ThenInclude(l => l.User) | ||||
|                         .FirstOrDefault(sched => sched.LawyerVideoConferenceSchedule_UID == schedule_UID); | ||||
|             .Include(t => t.TransactionSignatories) | ||||
|             .Include(t => t.Lawyer) | ||||
|             .ThenInclude(l => l.User) | ||||
|             .Include(t => t.Principal) | ||||
|             .Include(t => t.Schedule) | ||||
|             .ThenInclude(sch => sch.LawyerVideoConferenceParticipants) | ||||
|             .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); | ||||
| 
 | ||||
|         public string CommunicationRoomId { get; private set; } | ||||
| 
 | ||||
| @ -106,25 +100,33 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall | ||||
| 
 | ||||
|         public string CommunicationUserToken { get; private set; } | ||||
| 
 | ||||
|         public string DisplayName { get; private set; } | ||||
| 
 | ||||
|         public string ParticipantType { get; set; } | ||||
| 
 | ||||
|         public List<RoomParticipantViewModel> Participants | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0).ToDictionary(k => k.UserID, v => v.Type); | ||||
|                 var participants = _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.ConvertAll(p => new RoomParticipantViewModel | ||||
|                 { | ||||
|                     Id = p.LawyerVideoConferenceParticipant_UID.ToString(), | ||||
|                     DisplayName = $"{p.Participant.Firstname} {p.Participant.Lastname}".Trim().DefaultIfEmpty(p.Participant.Email), | ||||
|                     RoomUserID = p.MeetingRoomUserID, | ||||
|                     Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal)) | ||||
|                 }); | ||||
|                 var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0) | ||||
|                     .ToDictionary(k => k.UserID, v => v.Type); | ||||
|                 var participants = _Transaction.Schedule.LawyerVideoConferenceParticipants.ConvertAll(p => | ||||
|                     new RoomParticipantViewModel | ||||
|                     { | ||||
|                         Id = p.LawyerVideoConferenceParticipant_UID.ToString(), | ||||
|                         DisplayName = | ||||
|                             $"{p.Participant.Firstname} {p.Participant.Lastname}".Trim() | ||||
|                                 .DefaultIfEmpty(p.Participant.Email), | ||||
|                         RoomUserID = p.MeetingRoomUserID, | ||||
|                         Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal)) | ||||
|                     }); | ||||
| 
 | ||||
|                 var host = _LawyerVideoConferenceSchedule.Lawyer.User; | ||||
|                 var host = _Transaction.Schedule.Lawyer.User; | ||||
|                 participants.Add(new RoomParticipantViewModel | ||||
|                 { | ||||
|                     DisplayName = $"{host.Firstname} {host.Lastname}".Trim().DefaultIfEmpty(host.Email), | ||||
|                     Id = Guid.Empty.ToString(), | ||||
|                     RoomUserID = _LawyerVideoConferenceSchedule.MeetingRoomUserID, | ||||
|                     RoomUserID = _Transaction.Schedule.MeetingRoomUserID, | ||||
|                     Type = nameof(UserType.Notary) | ||||
|                 }); | ||||
| 
 | ||||
| @ -132,10 +134,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         [BindProperty(SupportsGet = true)] | ||||
|         public string ServerCallID { get; set; } | ||||
|         [BindProperty(SupportsGet = true)] public string ServerCallID { get; set; } | ||||
| 
 | ||||
|         [BindProperty(SupportsGet = true)] | ||||
|         public Guid Transaction_UID { get; set; } | ||||
|         [BindProperty(SupportsGet = true)] public Guid Transaction_UID { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -5,114 +5,291 @@ | ||||
|     control_communicationRoomId = document.getElementById("CommunicationRoomId"), | ||||
|     control_communicationUserId = document.getElementById("CommunicationUserId"), | ||||
|     control_communicationUserToken = document.getElementById("CommunicationUserToken"), | ||||
|     control_displayName = document.getElementById("DisplayName"), | ||||
|     control_participants = document.getElementById("Participants"), | ||||
|     control_reject = document.getElementById("Reject"), | ||||
|     control_templateVideo = document.getElementById("templateVideo"), | ||||
|     control_videoGrid = document.getElementById("VideoGrid"), | ||||
|     control_templateSidePane = document.getElementById("TemplateSidePane"), | ||||
|     control_templateParticipantItem = document.getElementById("TemplateParticipantItem"), | ||||
|     control_draggableModal = document.getElementById("DraggableModal"), | ||||
|     control_rightSidebarModal = document.getElementById("RightSidebarModal"), | ||||
|     /*control_videoGrid = document.getElementById("VideoGrid"),*/ | ||||
|     control_videoGridContainer = document.getElementById("videoGrid-container"), | ||||
|     control_viewDocument = document.getElementById("ViewDocument"), | ||||
|     control_serverCallIID = document.getElementById("ServerCallID"), | ||||
|     control_participantType = document.getElementById("ParticipantType"), | ||||
|     control_participantListGroup = document.getElementById("ParticipantListGroup"), | ||||
|     x = 1; | ||||
| 
 | ||||
|   let participants = JSON.parse(control_participants.value); | ||||
| 
 | ||||
|   async function _initVideoCall() { | ||||
|     let userAccessToken = control_communicationUserToken.value; | ||||
|     let self = this; | ||||
|     let customButtons = []; | ||||
|     if (control_participantType.value == 'Notary') { | ||||
|       customButtons = [ | ||||
|         function (args) { | ||||
|           return { | ||||
|             placement: 'secondary', | ||||
|             // Icon registered by the composites.
 | ||||
|             iconName: 'OpenAttachment', | ||||
|             strings: { | ||||
|               label: 'View Document', | ||||
|               tooltipContent: 'View the document.' | ||||
|             }, | ||||
|             onItemClick: function () { | ||||
|               alert('Document Modal goes here.'); | ||||
|             } | ||||
|           }; | ||||
|         }, | ||||
|         function (args) { | ||||
|           return { | ||||
|             placement: 'secondary', | ||||
|             // Icon registered by the composites.
 | ||||
|             iconName: 'ControlButtonParticipantsContextualMenuItem', | ||||
|             strings: { | ||||
|               label: 'View Participants', | ||||
|               tooltipContent: 'View the participants.' | ||||
|             }, | ||||
|             onItemClick: function () { | ||||
|               const modal = bootstrap.Modal.getOrCreateInstance(control_rightSidebarModal); | ||||
|               modal.show(); | ||||
|             } | ||||
|           }; | ||||
|         }, | ||||
|         function (args, b, c) { | ||||
|           return { | ||||
|             placement: 'primary', | ||||
|             // Icon registered by the composites.
 | ||||
|             iconName: 'EditBoxSubmit', | ||||
|             strings: { | ||||
|               label: 'Approve', | ||||
|               tooltipContent: 'Approve the Transaction' | ||||
|             }, | ||||
|             onItemClick: approveTransaction | ||||
|           }; | ||||
|         }, | ||||
|         function (args, b, c) { | ||||
|           return { | ||||
|             placement: 'primary', | ||||
|             // Icon registered by the composites.
 | ||||
|             iconName: 'EditBoxCancel', | ||||
|             strings: { | ||||
|               label: 'Reject', | ||||
|               tooltipContent: 'Reject the Transaction' | ||||
|             }, | ||||
|             onItemClick: function () { | ||||
|               alert('Rejected!'); | ||||
|             } | ||||
|           }; | ||||
|         } | ||||
|       ]; | ||||
|     } else { | ||||
|       customButtons = [ | ||||
|         function (args) { | ||||
|           return { | ||||
|             placement: 'primary', | ||||
|             // Icon registered by the composites.
 | ||||
|             iconName: 'OpenAttachment', | ||||
|             strings: { | ||||
|               label: 'View Document', | ||||
|               tooltipContent: 'View the document.' | ||||
|             }, | ||||
|             onItemClick: function () { | ||||
|               alert('Document Modal goes here.'); | ||||
|             } | ||||
|           }; | ||||
|         }, | ||||
|         function (args) { | ||||
|           return { | ||||
|             placement: 'secondary', | ||||
|             // Icon registered by the composites.
 | ||||
|             iconName: 'OpenAttachment', | ||||
|             strings: { | ||||
|               label: 'View Document', | ||||
|               tooltipContent: 'View the document.' | ||||
|             }, | ||||
|             onItemClick: function () { | ||||
|               alert('Document Modal goes here.'); | ||||
|             } | ||||
|           }; | ||||
|         }, | ||||
|       ]; | ||||
|     } | ||||
| 
 | ||||
|     const videoCall = jfa.communication.videocall; | ||||
|     let options = { | ||||
|       onCreateLocalVideoStream: function (el) { | ||||
|         if (el) { | ||||
|           el.style['transform'] = ''; | ||||
|           let notary = participants.find(p => p.RoomUserID == control_communicationUserId.value); | ||||
|           document.getElementById(notary.Id).appendChild(el); | ||||
|         } | ||||
|       //onCreateLocalVideoStream: function (el) {
 | ||||
|       //  if (el) {
 | ||||
|       //    el.style['transform'] = '';
 | ||||
|       //    let notary = participants.find(p => p.RoomUserID == control_communicationUserId.value);
 | ||||
|       //    document.getElementById(notary.Id).appendChild(el);
 | ||||
|       //  }
 | ||||
|       //},
 | ||||
|       //remoteVideoIsAvailableChanged: function (e) {
 | ||||
|       //  let participant = participants.find(p => p.RoomUserID == e.participantId);
 | ||||
|       //  if (participant) {
 | ||||
|       //    e.el.querySelector('video').style['object-fit'] = 'cover';
 | ||||
|       //    document.getElementById(participant.Id).appendChild(e.el);
 | ||||
|       //  }
 | ||||
|       //},
 | ||||
|       onGetServerCallIDCallback: function (serverCallId) { | ||||
|         //  let url = jfa.utilities.routing.getCurrentURLWithHandler("StartRecording");
 | ||||
|         //  url.searchParams.append("ServerCallID", serverCallId);
 | ||||
|         //  jfa.utilities.request.post(url, {})
 | ||||
|         //    .then(resp => {
 | ||||
|         //      if (resp.ok === true) {
 | ||||
|         //        console.log("started recording");
 | ||||
|         //      }
 | ||||
|         //    })
 | ||||
|         //    .catch(err => console.error(err));
 | ||||
|         //debugger;
 | ||||
|       }, | ||||
|       remoteVideoIsAvailableChanged: function (e) { | ||||
|         let participant = participants.find(p => p.RoomUserID == e.participantId); | ||||
|         if (participant) { | ||||
|           e.el.querySelector('video').style['object-fit'] = 'cover'; | ||||
|           document.getElementById(participant.Id).appendChild(e.el); | ||||
|         } | ||||
|       stateChangedCallback: function (state) { | ||||
|         //console.log(state);
 | ||||
|       }, | ||||
|       onGetServerCallID: function (serverCallId) { | ||||
|         let url = jfa.utilities.routing.getCurrentURLWithHandler("StartRecording"); | ||||
|         url.searchParams.append("ServerCallID", serverCallId); | ||||
|         jfa.utilities.request.post(url, {}) | ||||
|           .then(resp => { | ||||
|             if (resp.ok === true) { | ||||
|               console.log("started recording"); | ||||
|             } | ||||
|           }) | ||||
|           .catch(err => console.error(err)); | ||||
|       } | ||||
|       onFetchParticipantMenuItemsCallback: function (participantUserId, userId, defaultMenuItems) { | ||||
|         const participantList = document.querySelector('[data-ui-id="people-pane-content"]'); | ||||
|         if (participantList) { | ||||
|           participantList.innerHTML = ''; | ||||
|           console.log(participants); | ||||
|         } | ||||
|         defaultMenuItems = []; | ||||
|       }, | ||||
|       onFetchCustomButtonPropsCallbacks: customButtons, | ||||
|       onAddParticipantCallback: function (e, f) { | ||||
|         //debugger;
 | ||||
|         console.log(e, f); | ||||
|       }, | ||||
|       participantsJoinedCallback: function (e, f) { | ||||
|         //debugger;
 | ||||
|         console.log(e, f); | ||||
|       }, | ||||
|       callEndedCallback: function (a, b, c) { | ||||
|       }, | ||||
|       token: control_communicationUserToken.value, | ||||
|       roomID: control_communicationRoomId.value, | ||||
|       displayName: control_displayName.value, | ||||
|       userID: control_communicationUserId.value, | ||||
|       videoContainer: control_videoGridContainer | ||||
|     }; | ||||
|     await videoCall.init(userAccessToken, options); | ||||
|     videoCall.joinRoom(control_communicationRoomId.value); | ||||
|     await videoCall.init(options); | ||||
|     await videoCall.joinRoom(); | ||||
|   } | ||||
| 
 | ||||
|   function _updateGrid() { | ||||
|     control_videoGrid.innerHTML = ''; | ||||
|   function approveTransaction(e) { | ||||
|     jfa.communication.videocall.stopCall(true); | ||||
|     let url = jfa.utilities.routing.getCurrentURLWithHandler("Approve"); | ||||
|     jfa.utilities.request.post(url, {}) | ||||
|       .then(resp => { | ||||
|         if (resp.ok === true) { | ||||
|           debugger; | ||||
|         } | ||||
|       }) | ||||
|       .catch(err => console.error(err)); | ||||
|   } | ||||
| 
 | ||||
|     participants.forEach((participant, index) => { | ||||
|       const col = document.createElement('div'); | ||||
|       col.className = 'participant-col'; | ||||
|       const tmpl = control_templateVideo.cloneNode(true).content; | ||||
|       const vidcontainer = tmpl.querySelector(".video-container") | ||||
|       if (vidcontainer) { | ||||
|         vidcontainer.id = participant.Id; | ||||
|         vidcontainer.classList.add(participant.Id); | ||||
|         vidcontainer.classList.add(participant.Type == 'Notary' ? 'local-video-container' : 'remote-video-container'); | ||||
|   //function _updateGrid() {
 | ||||
|   //  control_videoGrid.innerHTML = '';
 | ||||
| 
 | ||||
|   //  participants.forEach((participant, index) => {
 | ||||
|   //    const col = document.createElement('div');
 | ||||
|   //    col.className = 'participant-col';
 | ||||
|   //    const tmpl = control_templateVideo.cloneNode(true).content;
 | ||||
|   //    const vidcontainer = tmpl.querySelector(".video-container")
 | ||||
|   //    if (vidcontainer) {
 | ||||
|   //      vidcontainer.id = participant.Id;
 | ||||
|   //      vidcontainer.classList.add(participant.Id);
 | ||||
|   //      vidcontainer.classList.add(participant.Type == 'Notary' ? 'local-video-container' : 'remote-video-container');
 | ||||
|   //    }
 | ||||
|   //    let participantName = tmpl.querySelector(".participant-name")
 | ||||
|   //    if (participantName) {
 | ||||
|   //      participantName.textContent = participant.DisplayName;
 | ||||
|   //    }
 | ||||
|   //    col.appendChild(tmpl);
 | ||||
|   //    control_videoGrid.appendChild(col);
 | ||||
|   //  });
 | ||||
| 
 | ||||
|   //  // Dynamically adjust grid columns based on participant count
 | ||||
|   //  const count = participants.length;
 | ||||
|   //  const cols = count <= 2 ? 'col-12 col-sm-12 offset-md-1 col-md-10 offset-lg-0 col-lg-6' :
 | ||||
|   //    count <= 4 ? 'col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6' :
 | ||||
|   //      count <= 8 ? 'col-6 col-md-6 col-lg-4 col-xl-4' :
 | ||||
|   //        count <= 9 ? 'col-4' :
 | ||||
|   //          'col-6 col-sm-4 col-lg-3';
 | ||||
| 
 | ||||
|   //  document.querySelectorAll('.participant-col').forEach(el => {
 | ||||
|   //    el.className = `participant-col ${cols}`;
 | ||||
|   //  });
 | ||||
| 
 | ||||
|   //  const fluid = count <= 2 ? 'container-fluid' :
 | ||||
|   //    count <= 8 ? 'container-xxl' :
 | ||||
|   //      'container-xl'
 | ||||
|   //  control_videoGridContainer.className = `${fluid} py-3`;
 | ||||
|   //}
 | ||||
| 
 | ||||
|   function _createParticipantListItems() { | ||||
|     control_participantListGroup.innerHTML = ''; | ||||
|     participants.forEach(participant => { | ||||
|       if (participant.Type === 'Notary') { | ||||
|         return; | ||||
|       } | ||||
|       let participantName = tmpl.querySelector(".participant-name") | ||||
|       if (participantName) { | ||||
|         participantName.textContent = participant.DisplayName; | ||||
|       } | ||||
|       col.appendChild(tmpl); | ||||
|       control_videoGrid.appendChild(col); | ||||
|       const initials = participant.DisplayName.split(' ').map(n => n.charAt(0)).join(''); | ||||
|       let tmpl = control_templateParticipantItem.cloneNode(true).content; | ||||
|       tmpl.querySelector('.participant-item').dataset.participantUid = participant.RoomUserID; | ||||
|       tmpl.querySelector('.participant-avatar').textContent = initials; | ||||
|       tmpl.querySelector('.participant-name').textContent = participant.DisplayName; | ||||
|       control_participantListGroup.appendChild(tmpl); | ||||
|     }); | ||||
| 
 | ||||
|     // Dynamically adjust grid columns based on participant count
 | ||||
|     const count = participants.length; | ||||
|     const cols = count <= 2 ? 'col-12 col-sm-12 offset-md-1 col-md-10 offset-lg-0 col-lg-6' : | ||||
|       count <= 4 ? 'col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6' : | ||||
|         count <= 8 ? 'col-6 col-md-6 col-lg-4 col-xl-4' : | ||||
|           count <= 9 ? 'col-4' : | ||||
|             'col-6 col-sm-4 col-lg-3'; | ||||
| 
 | ||||
|     document.querySelectorAll('.participant-col').forEach(el => { | ||||
|       el.className = `participant-col ${cols}`; | ||||
|     }); | ||||
| 
 | ||||
|     const fluid = count <= 2 ? 'container-fluid' : | ||||
|       count <= 8 ? 'container-xxl' : | ||||
|         'container-xl' | ||||
|     control_videoGridContainer.className = `${fluid} py-3`; | ||||
|   } | ||||
| 
 | ||||
|   function _bindEvents() { | ||||
|     window.addEventListener('resize', _updateGrid); | ||||
|     control_viewDocument.addEventListener("click", function () { | ||||
|       alert('not yet implemented'); | ||||
|     control_participantListGroup.addEventListener('click', function (event) { | ||||
|       let target = event.target?.closest('.list-group-item'); | ||||
|       if (target) { | ||||
| 
 | ||||
|         const sidebarModal = bootstrap.Modal.getOrCreateInstance(control_rightSidebarModal); | ||||
|         sidebarModal.hide(); | ||||
| 
 | ||||
|         const draggableModal = bootstrap.Modal.getOrCreateInstance(control_draggableModal); | ||||
|         draggableModal.show(); | ||||
|       } | ||||
|     }); | ||||
|     control_approve.addEventListener("click", function () { | ||||
|       let url = jfa.utilities.routing.getCurrentURLWithHandler("StopRecording"); | ||||
|       jfa.utilities.request.post(url, {}) | ||||
|         .then(resp => { | ||||
|           if (resp.ok === true) { | ||||
|             console.log("stopped recording"); | ||||
|           } | ||||
|         }) | ||||
|         .catch(err => console.error(err)); | ||||
|     }); | ||||
|     control_reject.addEventListener("click", function () { | ||||
|       alert('not yet implemented'); | ||||
| 
 | ||||
|     control_draggableModal.addEventListener('shown.bs.modal', function (e) { | ||||
|       makeDraggable(control_draggableModal); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function makeDraggable(modal) { | ||||
|     let isDragging = false; | ||||
|     let offsetX, offsetY; | ||||
| 
 | ||||
|     const header = modal.querySelector('.modal-header'); | ||||
| 
 | ||||
|     header.addEventListener('mousedown', (e) => { | ||||
|       isDragging = true; | ||||
|       offsetX = e.clientX - modal.offsetLeft; | ||||
|       offsetY = e.clientY - modal.offsetTop; | ||||
|     }); | ||||
| 
 | ||||
|     document.addEventListener('mousemove', (e) => { | ||||
|       if (!isDragging) return; | ||||
|       modal.style.left = `${e.clientX - offsetX}px`; | ||||
|       modal.style.top = `${e.clientY - offsetY}px`; | ||||
|     }); | ||||
| 
 | ||||
|     document.addEventListener('mouseup', () => { | ||||
|       isDragging = false; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   async function _init() { | ||||
|     _bindEvents(); | ||||
|     _updateGrid(); | ||||
|     //_updateGrid();
 | ||||
|     await _initVideoCall(); | ||||
|     debugger; | ||||
|     _createParticipantListItems(); | ||||
|   } | ||||
| 
 | ||||
|   await _init(); | ||||
|  | ||||
| @ -9,32 +9,31 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall | ||||
|     { | ||||
|         private readonly NotaryoDBContext _dbContext; | ||||
|         private readonly ICurrentUserService _currentUserService; | ||||
|         private readonly IConferenceSheduleService _conferenceSheduleService; | ||||
|         private readonly IVideoConferenceService _videoConferenceService; | ||||
|         private Transaction _transactionEntity; | ||||
| 
 | ||||
|         public WaitingModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IConferenceSheduleService conferenceSheduleService) | ||||
|         public WaitingModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IVideoConferenceService videoConferenceService) | ||||
|         { | ||||
|             _dbContext = notaryoDBContext; | ||||
|             _currentUserService = currentUserService; | ||||
|             _conferenceSheduleService = conferenceSheduleService; | ||||
|             _videoConferenceService = videoConferenceService; | ||||
|         } | ||||
| 
 | ||||
|         public async Task<IActionResult> OnGetAsync() | ||||
|         { | ||||
|             _transactionEntity = _dbContext.Transactions | ||||
|                 .Include(t => t.TransactionSignatories) | ||||
|                 .ThenInclude(ts => ts.User) | ||||
|                 .Include(t => t.Lawyer) | ||||
|                 .ThenInclude(l => l.User) | ||||
|                 .Include(t => t.Principal) | ||||
|                 .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); | ||||
|                .Include(t => t.TransactionSignatories) | ||||
|                .ThenInclude(ts => ts.User) | ||||
|                .Include(t => t.Lawyer) | ||||
|                .ThenInclude(l => l.User) | ||||
|                .Include(t => t.Principal) | ||||
|                .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); | ||||
|             if (_transactionEntity == null || _transactionEntity.Status.IsInList(TransactionState.Completed)) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); | ||||
|             if (schedule_UID != Guid.Empty) | ||||
|             if (_videoConferenceService.CanStart(Transaction_UID)) | ||||
|             { | ||||
|                 return Redirect($"/Participant/VideoCall/Room/{Transaction_UID}"); | ||||
|             } | ||||
|  | ||||
| @ -71,7 +71,8 @@ | ||||
|                 .withUrl("/notificationHub") | ||||
|                 .build(); | ||||
| 
 | ||||
|             connection.on("ReceiveUserNotification", (user_UID, message) => { | ||||
|             connection.on("ReceiveUserNotification", (userID, message) => { | ||||
|                 alert(message); | ||||
|                 receiveUserNotificationCallback?.(message); | ||||
|             }); | ||||
| 
 | ||||
|  | ||||
| @ -2,6 +2,7 @@ using System.Security.Principal; | ||||
| using Azure.Communication.CallAutomation; | ||||
| using Azure.Communication.Identity; | ||||
| using Azure.Communication.Rooms; | ||||
| using Azure.Storage.Queues; | ||||
| using Coravel; | ||||
| using EnotaryoPH.Data; | ||||
| using EnotaryoPH.Web.Common.Hubs; | ||||
| @ -35,7 +36,7 @@ namespace EnotaryoPH.Web | ||||
|             razorBuilder.AddRazorRuntimeCompilation(); | ||||
| #endif | ||||
|             builder.Services.AddSignalR(); | ||||
|             builder.Services.AddSingleton<NotificationService>(); | ||||
|             builder.Services.AddSingleton<INotificationService, NotificationService>(); | ||||
| 
 | ||||
|             var config = builder.Configuration; | ||||
|             builder.Services.AddTransient<IConfiguration, ConfigurationManager>(provider => config); | ||||
| @ -60,9 +61,12 @@ namespace EnotaryoPH.Web | ||||
|                 return new CompreFaceClient(host, port); | ||||
|             }); | ||||
|             builder.Services.AddQueue(); | ||||
|             builder.Services.AddScheduler(); | ||||
|             builder.Services.AddMailer(config); | ||||
|             builder.Services.AddTransient<SignatoryInvitationInvocable>(); | ||||
|             builder.Services.AddTransient<IConferenceSheduleService, ConferenceSheduleService>(); | ||||
|             builder.Services.AddTransient<CheckRecordingAvailabilityInvocable>(); | ||||
|             builder.Services.AddTransient<IVideoConferenceService, VideoConferenceService>(); | ||||
|             builder.Services.AddTransient<IEventService, EventService>(); | ||||
|             builder.Services.AddScoped<RoomsClient>(provider => | ||||
|             { | ||||
|                 var connectionString = config.GetConnectionString("AzureCommunication"); | ||||
| @ -84,9 +88,20 @@ namespace EnotaryoPH.Web | ||||
|                     ? throw new InvalidConfigurationException("AzureCommunication", string.Empty) | ||||
|                     : new CallAutomationClient(connectionString); | ||||
|             }); | ||||
|             builder.Services.AddScoped<QueueClient>(provider => | ||||
|             { | ||||
|                 var connectionString = config.GetConnectionString("AzureStorage"); | ||||
|                 return string.IsNullOrEmpty(connectionString) | ||||
|                     ? throw new InvalidConfigurationException("AzureStorage", string.Empty) | ||||
|                     : new QueueClient(connectionString, "recording-ready"); | ||||
|             }); | ||||
| 
 | ||||
|             var app = builder.Build(); | ||||
| 
 | ||||
|             app.Services.UseScheduler(scheduler => scheduler | ||||
|                 .Schedule<CheckRecordingAvailabilityInvocable>() | ||||
|                 .EveryTenSeconds()); | ||||
| 
 | ||||
|             // Configure the HTTP request pipeline. | ||||
|             if (!app.Environment.IsDevelopment()) | ||||
|             { | ||||
|  | ||||
| @ -9,8 +9,8 @@ | ||||
|             "context": "browser", | ||||
|             "outputFormat": "global", | ||||
|             "scopeHoist": false, | ||||
|             "optimize": true, | ||||
|             "sourceMap": false | ||||
|             "optimize": false, | ||||
|             "sourceMap": true | ||||
|         } | ||||
|     }, | ||||
|     "browserslist": "> 0.5%", | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user