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,
|
EmailSent = 1,
|
||||||
Registered = 2,
|
Registered = 2,
|
||||||
FaceMatch = 3,
|
FaceMatch = 3,
|
||||||
Completed = 10
|
Approved = 4,
|
||||||
|
Rejected = 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,8 @@
|
|||||||
DocumentUploaded = 2,
|
DocumentUploaded = 2,
|
||||||
Submitted = 3,
|
Submitted = 3,
|
||||||
Accepted = 4,
|
Accepted = 4,
|
||||||
|
Approved = 5,
|
||||||
|
Rejected = 6,
|
||||||
Completed = 100
|
Completed = 100
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ namespace EnotaryoPH.Data.Entities
|
|||||||
public int EventLogID { get; set; }
|
public int EventLogID { get; set; }
|
||||||
|
|
||||||
[Column("StreamID")]
|
[Column("StreamID")]
|
||||||
public int StreamID { get; set; }
|
public string StreamID { get; set; }
|
||||||
|
|
||||||
[Column("LogType")]
|
[Column("LogType")]
|
||||||
public string LogType { get; set; }
|
public string LogType { get; set; }
|
||||||
|
@ -43,6 +43,9 @@ namespace EnotaryoPH.Data.Entities
|
|||||||
[Column("Status")]
|
[Column("Status")]
|
||||||
public string? Status { get; set; }
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("TransactionID")]
|
||||||
|
public Transaction Transaction { get; set; }
|
||||||
|
|
||||||
[Column("TransactionID")]
|
[Column("TransactionID")]
|
||||||
public int TransactionID { get; set; }
|
public int TransactionID { get; set; }
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@ namespace EnotaryoPH.Data.Entities
|
|||||||
[Column("PrincipalID")]
|
[Column("PrincipalID")]
|
||||||
public int PrincipalID { get; set; }
|
public int PrincipalID { get; set; }
|
||||||
|
|
||||||
|
public LawyerVideoConferenceSchedule Schedule { get; set; }
|
||||||
|
|
||||||
[Column("Status")]
|
[Column("Status")]
|
||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
@ -5,49 +5,52 @@ namespace EnotaryoPH.Data.Entities
|
|||||||
[Table("Users")]
|
[Table("Users")]
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
[Column("UserID")]
|
[Column("BirthDate")]
|
||||||
public int UserID { get; set; }
|
public DateTime BirthDate { get; set; }
|
||||||
|
|
||||||
|
[Column("CreatedOn")]
|
||||||
|
public DateTime? CreatedOn { get; set; }
|
||||||
|
|
||||||
[Column("Email")]
|
[Column("Email")]
|
||||||
public string Email { get; set; }
|
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")]
|
[Column("PasswordHash")]
|
||||||
public string PasswordHash { get; set; }
|
public string PasswordHash { get; set; }
|
||||||
|
|
||||||
[Column("PhoneNumber")]
|
[Column("PhoneNumber")]
|
||||||
public string? PhoneNumber { get; set; }
|
public string? PhoneNumber { get; set; }
|
||||||
|
|
||||||
[Column("Firstname")]
|
[Column("Prefix")]
|
||||||
public string? Firstname { get; set; }
|
public string? Prefix { 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("Role")]
|
[Column("Role")]
|
||||||
public string? Role { get; set; }
|
public string? Role { get; set; }
|
||||||
|
|
||||||
[Column("BirthDate")]
|
|
||||||
public DateTime BirthDate { get; set; }
|
|
||||||
|
|
||||||
[Column("Middlename")]
|
|
||||||
public string? Middlename { get; set; }
|
|
||||||
|
|
||||||
[Column("Suffix")]
|
[Column("Suffix")]
|
||||||
public string? Suffix { get; set; }
|
public string? Suffix { get; set; }
|
||||||
|
|
||||||
[Column("Prefix")]
|
[Column("User_UID")]
|
||||||
public string? Prefix { get; set; }
|
public Guid? User_UID { get; set; }
|
||||||
|
|
||||||
public List<EventLog> EventLogs { get; set; }
|
[Column("UserID")]
|
||||||
|
public int UserID { get; set; }
|
||||||
public List<IdentificationDocument> IdentificationDocuments { get; set; }
|
|
||||||
|
|
||||||
public List<LawyerVideoConferenceParticipant> LawyerVideoConferenceParticipants { 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';
|
import { CallClient, LocalVideoStream, VideoStreamRenderer } from '@azure/communication-calling';
|
||||||
|
|
||||||
class VideoCall {
|
class VideoCall {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.callClient = null;
|
//this.isLocalVideoStartedChangedCallback = null;
|
||||||
this.callAgent = null;
|
//this.localVideoStreamsUpdatedCallback = null;
|
||||||
this.deviceManager = null;
|
//this.onCreateLocalVideoStreamCallback = null;
|
||||||
this.localVideoStream = null;
|
this.onGetServerCallIDCallback = null;
|
||||||
this.call = null;
|
//this.remoteParticipantStateChangedCallback = null;
|
||||||
this.videoElement = document.createElement('video');
|
//this.remoteParticipantsUpdatedCallback = null;
|
||||||
this.videoElement.setAttribute('autoplay', '');
|
//this.remoteVideoIsAvailableChangedCallback = null;
|
||||||
this.videoElement.setAttribute('muted', '');
|
|
||||||
this.stateChangedCallback = null;
|
this.stateChangedCallback = null;
|
||||||
this.remoteParticipantsUpdated = null;
|
this.callEndedCallback = null;
|
||||||
this.isLocalVideoStartedChanged = null;
|
this.participantsJoinedCallback = null;
|
||||||
this.localVideoStreamsUpdated = null;
|
this.callsUpdatedCallback = null;
|
||||||
this.idChanged = null;
|
|
||||||
this.onCreateLocalVideoStream = null;
|
this.callAdapter = null;
|
||||||
this.remoteParticipantStateChanged = null;
|
this.videoContainer = null;
|
||||||
this.remoteVideoIsAvailableChanged = null;
|
this.displayName = null;
|
||||||
this.onGetServerCallID = 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.stateChangedCallback = options.stateChangedCallback;
|
||||||
this.remoteParticipantsUpdated = options.remoteParticipantsUpdated;
|
/*this.remoteParticipantsUpdatedCallback = options.remoteParticipantsUpdated;*/
|
||||||
this.isLocalVideoStartedChanged = options.isLocalVideoStartedChanged;
|
/*this.isLocalVideoStartedChangedCallback = options.isLocalVideoStartedChanged;*/
|
||||||
this.localVideoStreamsUpdated = options.localVideoStreamsUpdated;
|
/*this.localVideoStreamsUpdatedCallback = options.localVideoStreamsUpdated;*/
|
||||||
this.idChanged = options.idChanged;
|
this.idChangedCallback = options.idChanged;
|
||||||
this.onCreateLocalVideoStream = options.onCreateLocalVideoStream;
|
//this.onCreateLocalVideoStreamCallback = options.onCreateLocalVideoStream;
|
||||||
this.remoteParticipantStateChanged = options.remoteParticipantStateChanged;
|
//this.remoteParticipantStateChangedCallback = options.remoteParticipantStateChanged;
|
||||||
this.remoteVideoIsAvailableChanged = options.remoteVideoIsAvailableChanged;
|
//this.remoteVideoIsAvailableChangedCallback = options.remoteVideoIsAvailableChanged;
|
||||||
this.onGetServerCallID = options.onGetServerCallID;
|
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.serverCallId;
|
||||||
this.callAgent = await this.callClient.createCallAgent(tokenCredential);
|
|
||||||
|
|
||||||
this.deviceManager = await this.callClient.getDeviceManager();
|
const callControls = {
|
||||||
await this.deviceManager.askDevicePermission({ audio: true, video: true });
|
// Hide all default buttons
|
||||||
const cameras = await this.deviceManager.getCameras();
|
cameraButton: true,
|
||||||
this.localVideoStream = new LocalVideoStream(cameras[0]);
|
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() {
|
// Hide the entire control bar if needed
|
||||||
if (this.call) {
|
onFetchCustomButtonProps: this.onFetchCustomButtonPropsCallbacks
|
||||||
this.call.stopVideo(this.localVideoStream);
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async joinRoom(roomId) {
|
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||||
// Assuming you have a room ID and call the appropriate ACS API to join a room
|
const callCompositeProps = {
|
||||||
// This is just a placeholder, replace with actual logic
|
callControls: callControls,
|
||||||
console.log('Joining room:', roomId);
|
formFactor: isMobile ? 'mobile' : 'desktop',
|
||||||
if (this.callAgent) {
|
onFetchParticipantMenuItems: this.onFetchParticipantMenuItemsCallback,
|
||||||
try {
|
options: {
|
||||||
const localVideoStream = await this.createLocalVideoStream();
|
callControls: callControls
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
async subscribeToCall(call) {
|
const adapterArgs = {
|
||||||
try {
|
userId: createIdentifierFromRawId(this.userID),
|
||||||
call.on('idChanged', () => {
|
//credential: new AzureCommunicationTokenCredential(this.token),
|
||||||
this.idChanged?.(call.id);
|
token: this.token,
|
||||||
});
|
displayName: this.displayName,
|
||||||
call.on('stateChanged', async () => {
|
locator: { roomId: this.roomID },
|
||||||
this.stateChangedCallback?.(call.state);
|
callAdapterOptions: {},
|
||||||
});
|
callCompositeOptions: callCompositeProps
|
||||||
call.on('isLocalVideoStartedChanged', () => {
|
};
|
||||||
this.isLocalVideoStartedChanged?.(call.isLocalVideoStarted);
|
|
||||||
});
|
|
||||||
call.on('localVideoStreamsUpdated', e => {
|
|
||||||
this.localVideoStreamsUpdated?.(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to the call's 'remoteParticipantsUpdated' event to be
|
this.callAdapter = await callComposite.loadCallComposite(
|
||||||
// notified when new participants are added to the call or removed from the call.
|
adapterArgs,
|
||||||
call.on('remoteParticipantsUpdated', e => {
|
this.videoContainer, // container element,
|
||||||
this.remoteParticipantsUpdated?.(e);
|
callCompositeProps
|
||||||
|
);
|
||||||
|
|
||||||
e.added.forEach(remoteParticipant => {
|
//this.callAdapter.callAgent.on("callsUpdated", function (e) {
|
||||||
this.subscribeToRemoteParticipant(remoteParticipant)
|
// 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.
|
this.callAdapter.on("callEnded", function (e) {
|
||||||
if (remoteVideoStream.isAvailable) {
|
self.callEndedCallback?.(e);
|
||||||
try {
|
});
|
||||||
await createView();
|
|
||||||
} catch (e) {
|
this.callAdapter.on("participantsJoined", function (e, f) {
|
||||||
console.error(e);
|
self.participantsJoinedCallback?.(e, f);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
this.callAdapter.on("onAddParticipant", function (e, f) {
|
||||||
|
self.onAddParticipantCallback?.(e, f);
|
||||||
|
});
|
||||||
|
|
||||||
|
//CallEnded
|
||||||
|
|
||||||
|
return this.callAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createLocalVideoStream() {
|
//stopLocalVideo() {
|
||||||
const camera = (await this.deviceManager.getCameras())[0];
|
// if (this.call) {
|
||||||
if (camera) {
|
// this.call.stopVideo(this.localVideoStream);
|
||||||
return new LocalVideoStream(camera);
|
// }
|
||||||
} else {
|
//}
|
||||||
console.error(`No camera device found on the system`);
|
|
||||||
}
|
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() {
|
leaveRoom() {
|
||||||
if (this.call) {
|
if (this.call) {
|
||||||
this.call.hangUp();
|
this.call.hangUp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async displayLocalVideoStream(lvs) {
|
//createIdentifierFromRawId(rawId) {
|
||||||
try {
|
// return createIdentifierFromRawId(rawId);
|
||||||
let localVideoStreamRenderer = new VideoStreamRenderer(lvs);
|
//}
|
||||||
const view = await localVideoStreamRenderer.createView();
|
|
||||||
this.onCreateLocalVideoStream?.(view.target);
|
//async displayLocalVideoStream(lvs) {
|
||||||
} catch (error) {
|
// try {
|
||||||
console.error(error);
|
// let localVideoStreamRenderer = new VideoStreamRenderer(lvs);
|
||||||
}
|
// const view = await localVideoStreamRenderer.createView();
|
||||||
}
|
// this.onCreateLocalVideoStream?.(view.target);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error(error);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VideoCall;
|
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 Plus = '+';
|
||||||
private const char Slash = '/';
|
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);
|
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
|
namespace EnotaryoPH.Web.Common.Services
|
||||||
{
|
{
|
||||||
public class NotificationService
|
public class NotificationService : INotificationService
|
||||||
{
|
{
|
||||||
private readonly IHubContext<NotificationHub> _hubContext;
|
private readonly IHubContext<NotificationHub> _hubContext;
|
||||||
|
|
||||||
public NotificationService(IHubContext<NotificationHub> hubContext) => _hubContext = 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.CallAutomation" Version="1.3.0" />
|
||||||
<PackageReference Include="Azure.Communication.Identity" Version="1.3.1" />
|
<PackageReference Include="Azure.Communication.Identity" Version="1.3.1" />
|
||||||
<PackageReference Include="Azure.Communication.Rooms" Version="1.1.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="CompreFace.NET.Sdk" Version="1.0.2" />
|
||||||
<PackageReference Include="Coravel" Version="6.0.2" />
|
<PackageReference Include="Coravel" Version="6.0.2" />
|
||||||
<PackageReference Include="Coravel.Mailer" Version="7.1.0" />
|
<PackageReference Include="Coravel.Mailer" Version="7.1.0" />
|
||||||
|
@ -8,3 +8,19 @@
|
|||||||
<h1 class="display-4">Welcome</h1>
|
<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>
|
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
|
||||||
</div>
|
</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;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
@ -7,12 +6,14 @@ namespace EnotaryoPH.Web.Pages
|
|||||||
public class IndexModel : PageModel
|
public class IndexModel : PageModel
|
||||||
{
|
{
|
||||||
private readonly ICurrentUserService _currentUserService;
|
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;
|
_currentUserService = currentUserService;
|
||||||
_notificationService = notificationService;
|
_notificationService = notificationService;
|
||||||
|
_eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult OnGet()
|
public IActionResult OnGet()
|
||||||
@ -34,5 +35,14 @@ namespace EnotaryoPH.Web.Pages
|
|||||||
|
|
||||||
return Page();
|
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 const string STATUS_READY = "Ready";
|
||||||
private readonly ICurrentUserService _currentUserService;
|
private readonly ICurrentUserService _currentUserService;
|
||||||
private readonly IConferenceSheduleService _conferenceSheduleService;
|
|
||||||
private readonly NotaryoDBContext _notaryoDBContext;
|
private readonly NotaryoDBContext _notaryoDBContext;
|
||||||
|
private readonly IVideoConferenceService _videoConferenceService;
|
||||||
private Transaction? _transaction;
|
private Transaction? _transaction;
|
||||||
|
|
||||||
public IndexModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IConferenceSheduleService conferenceSheduleService)
|
public IndexModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IVideoConferenceService videoConferenceService)
|
||||||
{
|
{
|
||||||
_notaryoDBContext = notaryoDBContext;
|
_notaryoDBContext = notaryoDBContext;
|
||||||
_currentUserService = currentUserService;
|
_currentUserService = currentUserService;
|
||||||
_conferenceSheduleService = conferenceSheduleService;
|
_videoConferenceService = videoConferenceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult OnGet()
|
public IActionResult OnGet()
|
||||||
@ -81,16 +81,12 @@ namespace EnotaryoPH.Web.Pages.Notary.TransactionStatus
|
|||||||
_transaction.Status = nameof(TransactionState.Accepted);
|
_transaction.Status = nameof(TransactionState.Accepted);
|
||||||
_transaction.LawyerID = lawyer.LawyerID;
|
_transaction.LawyerID = lawyer.LawyerID;
|
||||||
_notaryoDBContext.Update(_transaction);
|
_notaryoDBContext.Update(_transaction);
|
||||||
|
_notaryoDBContext.SaveChanges();
|
||||||
|
|
||||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
var canStart = _videoConferenceService.CanStart(Transaction_UID);
|
||||||
if (schedule_UID != Guid.Empty)
|
return canStart
|
||||||
{
|
? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}")
|
||||||
return _transaction.TransactionSignatories.TrueForAll(sig => sig.Status == nameof(SignatoryStatus.FaceMatch))
|
: Redirect($"/Participant/VideoCall/Waiting/{Transaction_UID}");
|
||||||
? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}")
|
|
||||||
: Redirect($"/Participant/VideoCall/Waiting/{Transaction_UID}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Redirect("/Notary/Dashboard");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ChangeStatusLabel(string status) => status switch { nameof(SignatoryStatus.FaceMatch) => STATUS_READY, _ => status };
|
private string ChangeStatusLabel(string status) => status switch { nameof(SignatoryStatus.FaceMatch) => STATUS_READY, _ => status };
|
||||||
|
@ -6,98 +6,305 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@section Head {
|
@section Head {
|
||||||
<style>
|
<style>
|
||||||
.video-container {
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
|
||||||
background: #000;
|
|
||||||
border: 2px solid #444;
|
|
||||||
|
|
||||||
width: 100%;
|
.modal-dialog-right {
|
||||||
overflow: hidden;
|
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 {
|
.modal-content-right {
|
||||||
position: absolute;
|
border-radius: 0;
|
||||||
top: 0;
|
height: 100vh;
|
||||||
left: 0;
|
width: 100%;
|
||||||
width: 100%;
|
background-color: #fff;
|
||||||
height: 100%;
|
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.3);
|
||||||
object-fit: cover; /* This will maintain the video's original aspect ratio and crop if necessary */
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.video-element {
|
.list-group {
|
||||||
position: absolute;
|
list-style: none;
|
||||||
width: 100%;
|
padding: 0;
|
||||||
height: 100%;
|
}
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participant-name {
|
.list-group-item {
|
||||||
position: absolute;
|
display: flex;
|
||||||
bottom: 10px;
|
align-items: center;
|
||||||
left: 10px;
|
gap: 10px;
|
||||||
color: white;
|
margin-bottom: 10px;
|
||||||
background: rgba(0,0,0,0.5);
|
padding: 10px;
|
||||||
padding: 2px 8px;
|
border-radius: 5px;
|
||||||
border-radius: 4px;
|
/* background-color: #f9f9f9; */
|
||||||
z-index: 1;
|
/* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.avatar {
|
||||||
position: fixed;
|
width: 40px;
|
||||||
bottom: 20px;
|
height: 40px;
|
||||||
left: 50%;
|
border-radius: 50%;
|
||||||
/* transform: translateX(-50%); */
|
background-color: #007bff;
|
||||||
background: rgba(0,0,0,0.8);
|
display: flex;
|
||||||
padding: 10px;
|
align-items: center;
|
||||||
border-radius: 20px;
|
justify-content: center;
|
||||||
}
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.participant-col {
|
.modal-header.draggable {
|
||||||
transition: all 0.3s ease;
|
cursor: move;
|
||||||
|
}
|
||||||
display:flex;
|
</style>
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
}
|
}
|
||||||
|
<div class="vh-100" id="videoGrid-container"></div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<input type="hidden" id="Participants" value="@(System.Web.HttpUtility.JavaScriptStringEncode(JsonSerializer.Serialize(Model.Participants)).Replace("\\", ""))" />
|
<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" asp-for="CommunicationUserToken" />
|
||||||
<input type="hidden" id="CommunicationUserId" value="@Model.CommunicationUserId" />
|
<input type="hidden" asp-for="CommunicationUserId" />
|
||||||
<input type="hidden" id="CommunicationRoomId" value="@Model.CommunicationRoomId" />
|
<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">
|
<form method="post" asp-page-handler="StartRecording">
|
||||||
<input type="hidden" asp-for="ServerCallID" />
|
<input type="hidden" asp-for="ServerCallID" />
|
||||||
<input type="hidden" asp-for="Transaction_UID" />
|
<input type="hidden" asp-for="Transaction_UID" />
|
||||||
</form>
|
</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 {
|
@section Scripts {
|
||||||
<script type="text/javascript" src="/dist/_jfa.js"></script>
|
<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>
|
<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
|
public class RoomModel : PageModel
|
||||||
{
|
{
|
||||||
private const int VideoConferenceExpirationInHours = 2;
|
|
||||||
private readonly IConferenceSheduleService _conferenceSheduleService;
|
|
||||||
private readonly ICurrentUserService _currentUserService;
|
private readonly ICurrentUserService _currentUserService;
|
||||||
private readonly NotaryoDBContext _dbContext;
|
private readonly NotaryoDBContext _dbContext;
|
||||||
private LawyerVideoConferenceSchedule _LawyerVideoConferenceSchedule;
|
private readonly IVideoConferenceService _videoConferenceService;
|
||||||
private Transaction _Transaction;
|
private Transaction _Transaction;
|
||||||
|
|
||||||
public RoomModel(ICurrentUserService currentUserService, NotaryoDBContext dbContext, IConferenceSheduleService conferenceSheduleService)
|
public RoomModel(ICurrentUserService currentUserService, NotaryoDBContext dbContext,
|
||||||
|
IVideoConferenceService videoConferenceService)
|
||||||
{
|
{
|
||||||
_currentUserService = currentUserService;
|
_currentUserService = currentUserService;
|
||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
_conferenceSheduleService = conferenceSheduleService;
|
_videoConferenceService = videoConferenceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> OnGetAsync()
|
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 currentUser = _dbContext.Users.Single(u => u.User_UID == _currentUserService.GetUser_UID());
|
||||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
var canStart = _videoConferenceService.CanStart(Transaction_UID);
|
||||||
if (schedule_UID == Guid.Empty)
|
if (!canStart)
|
||||||
{
|
{
|
||||||
if (_Transaction.PrincipalID == currentUser.UserID)
|
if (_Transaction.PrincipalID == currentUser.UserID)
|
||||||
{
|
{
|
||||||
@ -46,59 +45,54 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
|||||||
return Redirect("/");
|
return Redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadVideoConferenceSchedule(schedule_UID);
|
if (_videoConferenceService.HasExpired(Transaction_UID))
|
||||||
|
|
||||||
if ((DateTime.UtcNow - _LawyerVideoConferenceSchedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours)
|
|
||||||
{
|
{
|
||||||
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.";
|
TempData["Warning"] = "The video conference has expired.";
|
||||||
return Redirect("/");
|
return Redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var schedule_UID = await _videoConferenceService.StartAsync(Transaction_UID);
|
||||||
|
|
||||||
CommunicationUserToken = currentUser.Role == nameof(UserType.Notary)
|
CommunicationUserToken = currentUser.Role == nameof(UserType.Notary)
|
||||||
? _LawyerVideoConferenceSchedule.MeetingRoomTokenID
|
? _Transaction.Schedule.MeetingRoomTokenID
|
||||||
: _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomTokenID;
|
: GetParticipant(currentUser).MeetingRoomTokenID;
|
||||||
CommunicationRoomId = _LawyerVideoConferenceSchedule.MeetingRoomID;
|
CommunicationRoomId = _Transaction.Schedule.MeetingRoomID;
|
||||||
CommunicationUserId = currentUser.Role == nameof(UserType.Notary)
|
CommunicationUserId = currentUser.Role == nameof(UserType.Notary)
|
||||||
? _LawyerVideoConferenceSchedule.MeetingRoomUserID
|
? _Transaction.Schedule.MeetingRoomUserID
|
||||||
: _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).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();
|
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();
|
await _videoConferenceService.ApproveTransactionAsync(Transaction_UID);
|
||||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
return new JsonResult(true);
|
||||||
LoadVideoConferenceSchedule(schedule_UID);
|
|
||||||
await _conferenceSheduleService.StartRecordingAsync(Transaction_UID, ServerCallID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnPostStopRecordingAsync()
|
public async Task<JsonResult> OnPostStartRecordingAsync()
|
||||||
{
|
{
|
||||||
LoadTransaction();
|
LoadTransaction();
|
||||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
await _videoConferenceService.StartRecordingAsync(Transaction_UID, ServerCallID);
|
||||||
LoadVideoConferenceSchedule(schedule_UID);
|
return new JsonResult(true);
|
||||||
await _conferenceSheduleService.StopRecordingAsync(Transaction_UID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadTransaction() => _Transaction = _dbContext.Transactions
|
private void LoadTransaction() => _Transaction = _dbContext.Transactions
|
||||||
.Include(t => t.TransactionSignatories)
|
.Include(t => t.TransactionSignatories)
|
||||||
.Include(t => t.Principal)
|
.Include(t => t.Lawyer)
|
||||||
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
|
.ThenInclude(l => l.User)
|
||||||
|
.Include(t => t.Principal)
|
||||||
private void LoadVideoConferenceSchedule(Guid schedule_UID) => _LawyerVideoConferenceSchedule = _dbContext.LawyerVideoConferenceSchedules
|
.Include(t => t.Schedule)
|
||||||
.Include(sched => sched.LawyerVideoConferenceParticipants)
|
.ThenInclude(sch => sch.LawyerVideoConferenceParticipants)
|
||||||
.ThenInclude(p => p.Participant)
|
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
|
||||||
.Include(sched => sched.Lawyer)
|
|
||||||
.ThenInclude(l => l.User)
|
|
||||||
.FirstOrDefault(sched => sched.LawyerVideoConferenceSchedule_UID == schedule_UID);
|
|
||||||
|
|
||||||
public string CommunicationRoomId { get; private set; }
|
public string CommunicationRoomId { get; private set; }
|
||||||
|
|
||||||
@ -106,25 +100,33 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
|||||||
|
|
||||||
public string CommunicationUserToken { get; private set; }
|
public string CommunicationUserToken { get; private set; }
|
||||||
|
|
||||||
|
public string DisplayName { get; private set; }
|
||||||
|
|
||||||
|
public string ParticipantType { get; set; }
|
||||||
|
|
||||||
public List<RoomParticipantViewModel> Participants
|
public List<RoomParticipantViewModel> Participants
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0).ToDictionary(k => k.UserID, v => v.Type);
|
var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0)
|
||||||
var participants = _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.ConvertAll(p => new RoomParticipantViewModel
|
.ToDictionary(k => k.UserID, v => v.Type);
|
||||||
{
|
var participants = _Transaction.Schedule.LawyerVideoConferenceParticipants.ConvertAll(p =>
|
||||||
Id = p.LawyerVideoConferenceParticipant_UID.ToString(),
|
new RoomParticipantViewModel
|
||||||
DisplayName = $"{p.Participant.Firstname} {p.Participant.Lastname}".Trim().DefaultIfEmpty(p.Participant.Email),
|
{
|
||||||
RoomUserID = p.MeetingRoomUserID,
|
Id = p.LawyerVideoConferenceParticipant_UID.ToString(),
|
||||||
Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal))
|
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
|
participants.Add(new RoomParticipantViewModel
|
||||||
{
|
{
|
||||||
DisplayName = $"{host.Firstname} {host.Lastname}".Trim().DefaultIfEmpty(host.Email),
|
DisplayName = $"{host.Firstname} {host.Lastname}".Trim().DefaultIfEmpty(host.Email),
|
||||||
Id = Guid.Empty.ToString(),
|
Id = Guid.Empty.ToString(),
|
||||||
RoomUserID = _LawyerVideoConferenceSchedule.MeetingRoomUserID,
|
RoomUserID = _Transaction.Schedule.MeetingRoomUserID,
|
||||||
Type = nameof(UserType.Notary)
|
Type = nameof(UserType.Notary)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -132,10 +134,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[BindProperty(SupportsGet = true)]
|
[BindProperty(SupportsGet = true)] public string ServerCallID { get; set; }
|
||||||
public string ServerCallID { get; set; }
|
|
||||||
|
|
||||||
[BindProperty(SupportsGet = true)]
|
[BindProperty(SupportsGet = true)] public Guid Transaction_UID { get; set; }
|
||||||
public Guid Transaction_UID { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,114 +5,291 @@
|
|||||||
control_communicationRoomId = document.getElementById("CommunicationRoomId"),
|
control_communicationRoomId = document.getElementById("CommunicationRoomId"),
|
||||||
control_communicationUserId = document.getElementById("CommunicationUserId"),
|
control_communicationUserId = document.getElementById("CommunicationUserId"),
|
||||||
control_communicationUserToken = document.getElementById("CommunicationUserToken"),
|
control_communicationUserToken = document.getElementById("CommunicationUserToken"),
|
||||||
|
control_displayName = document.getElementById("DisplayName"),
|
||||||
control_participants = document.getElementById("Participants"),
|
control_participants = document.getElementById("Participants"),
|
||||||
control_reject = document.getElementById("Reject"),
|
control_reject = document.getElementById("Reject"),
|
||||||
control_templateVideo = document.getElementById("templateVideo"),
|
control_templateSidePane = document.getElementById("TemplateSidePane"),
|
||||||
control_videoGrid = document.getElementById("VideoGrid"),
|
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_videoGridContainer = document.getElementById("videoGrid-container"),
|
||||||
control_viewDocument = document.getElementById("ViewDocument"),
|
control_viewDocument = document.getElementById("ViewDocument"),
|
||||||
control_serverCallIID = document.getElementById("ServerCallID"),
|
control_serverCallIID = document.getElementById("ServerCallID"),
|
||||||
|
control_participantType = document.getElementById("ParticipantType"),
|
||||||
|
control_participantListGroup = document.getElementById("ParticipantListGroup"),
|
||||||
x = 1;
|
x = 1;
|
||||||
|
|
||||||
let participants = JSON.parse(control_participants.value);
|
let participants = JSON.parse(control_participants.value);
|
||||||
|
|
||||||
async function _initVideoCall() {
|
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;
|
const videoCall = jfa.communication.videocall;
|
||||||
let options = {
|
let options = {
|
||||||
onCreateLocalVideoStream: function (el) {
|
//onCreateLocalVideoStream: function (el) {
|
||||||
if (el) {
|
// if (el) {
|
||||||
el.style['transform'] = '';
|
// el.style['transform'] = '';
|
||||||
let notary = participants.find(p => p.RoomUserID == control_communicationUserId.value);
|
// let notary = participants.find(p => p.RoomUserID == control_communicationUserId.value);
|
||||||
document.getElementById(notary.Id).appendChild(el);
|
// 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) {
|
stateChangedCallback: function (state) {
|
||||||
let participant = participants.find(p => p.RoomUserID == e.participantId);
|
//console.log(state);
|
||||||
if (participant) {
|
|
||||||
e.el.querySelector('video').style['object-fit'] = 'cover';
|
|
||||||
document.getElementById(participant.Id).appendChild(e.el);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onGetServerCallID: function (serverCallId) {
|
onFetchParticipantMenuItemsCallback: function (participantUserId, userId, defaultMenuItems) {
|
||||||
let url = jfa.utilities.routing.getCurrentURLWithHandler("StartRecording");
|
const participantList = document.querySelector('[data-ui-id="people-pane-content"]');
|
||||||
url.searchParams.append("ServerCallID", serverCallId);
|
if (participantList) {
|
||||||
jfa.utilities.request.post(url, {})
|
participantList.innerHTML = '';
|
||||||
.then(resp => {
|
console.log(participants);
|
||||||
if (resp.ok === true) {
|
}
|
||||||
console.log("started recording");
|
defaultMenuItems = [];
|
||||||
}
|
},
|
||||||
})
|
onFetchCustomButtonPropsCallbacks: customButtons,
|
||||||
.catch(err => console.error(err));
|
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);
|
await videoCall.init(options);
|
||||||
videoCall.joinRoom(control_communicationRoomId.value);
|
await videoCall.joinRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _updateGrid() {
|
function approveTransaction(e) {
|
||||||
control_videoGrid.innerHTML = '';
|
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) => {
|
//function _updateGrid() {
|
||||||
const col = document.createElement('div');
|
// control_videoGrid.innerHTML = '';
|
||||||
col.className = 'participant-col';
|
|
||||||
const tmpl = control_templateVideo.cloneNode(true).content;
|
// participants.forEach((participant, index) => {
|
||||||
const vidcontainer = tmpl.querySelector(".video-container")
|
// const col = document.createElement('div');
|
||||||
if (vidcontainer) {
|
// col.className = 'participant-col';
|
||||||
vidcontainer.id = participant.Id;
|
// const tmpl = control_templateVideo.cloneNode(true).content;
|
||||||
vidcontainer.classList.add(participant.Id);
|
// const vidcontainer = tmpl.querySelector(".video-container")
|
||||||
vidcontainer.classList.add(participant.Type == 'Notary' ? 'local-video-container' : 'remote-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")
|
const initials = participant.DisplayName.split(' ').map(n => n.charAt(0)).join('');
|
||||||
if (participantName) {
|
let tmpl = control_templateParticipantItem.cloneNode(true).content;
|
||||||
participantName.textContent = participant.DisplayName;
|
tmpl.querySelector('.participant-item').dataset.participantUid = participant.RoomUserID;
|
||||||
}
|
tmpl.querySelector('.participant-avatar').textContent = initials;
|
||||||
col.appendChild(tmpl);
|
tmpl.querySelector('.participant-name').textContent = participant.DisplayName;
|
||||||
control_videoGrid.appendChild(col);
|
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() {
|
function _bindEvents() {
|
||||||
window.addEventListener('resize', _updateGrid);
|
control_participantListGroup.addEventListener('click', function (event) {
|
||||||
control_viewDocument.addEventListener("click", function () {
|
let target = event.target?.closest('.list-group-item');
|
||||||
alert('not yet implemented');
|
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");
|
control_draggableModal.addEventListener('shown.bs.modal', function (e) {
|
||||||
jfa.utilities.request.post(url, {})
|
makeDraggable(control_draggableModal);
|
||||||
.then(resp => {
|
|
||||||
if (resp.ok === true) {
|
|
||||||
console.log("stopped recording");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => console.error(err));
|
|
||||||
});
|
|
||||||
control_reject.addEventListener("click", function () {
|
|
||||||
alert('not yet implemented');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function _init() {
|
||||||
_bindEvents();
|
_bindEvents();
|
||||||
_updateGrid();
|
//_updateGrid();
|
||||||
await _initVideoCall();
|
await _initVideoCall();
|
||||||
|
debugger;
|
||||||
|
_createParticipantListItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _init();
|
await _init();
|
||||||
|
@ -9,32 +9,31 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
|||||||
{
|
{
|
||||||
private readonly NotaryoDBContext _dbContext;
|
private readonly NotaryoDBContext _dbContext;
|
||||||
private readonly ICurrentUserService _currentUserService;
|
private readonly ICurrentUserService _currentUserService;
|
||||||
private readonly IConferenceSheduleService _conferenceSheduleService;
|
private readonly IVideoConferenceService _videoConferenceService;
|
||||||
private Transaction _transactionEntity;
|
private Transaction _transactionEntity;
|
||||||
|
|
||||||
public WaitingModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IConferenceSheduleService conferenceSheduleService)
|
public WaitingModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IVideoConferenceService videoConferenceService)
|
||||||
{
|
{
|
||||||
_dbContext = notaryoDBContext;
|
_dbContext = notaryoDBContext;
|
||||||
_currentUserService = currentUserService;
|
_currentUserService = currentUserService;
|
||||||
_conferenceSheduleService = conferenceSheduleService;
|
_videoConferenceService = videoConferenceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> OnGetAsync()
|
public async Task<IActionResult> OnGetAsync()
|
||||||
{
|
{
|
||||||
_transactionEntity = _dbContext.Transactions
|
_transactionEntity = _dbContext.Transactions
|
||||||
.Include(t => t.TransactionSignatories)
|
.Include(t => t.TransactionSignatories)
|
||||||
.ThenInclude(ts => ts.User)
|
.ThenInclude(ts => ts.User)
|
||||||
.Include(t => t.Lawyer)
|
.Include(t => t.Lawyer)
|
||||||
.ThenInclude(l => l.User)
|
.ThenInclude(l => l.User)
|
||||||
.Include(t => t.Principal)
|
.Include(t => t.Principal)
|
||||||
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
|
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
|
||||||
if (_transactionEntity == null || _transactionEntity.Status.IsInList(TransactionState.Completed))
|
if (_transactionEntity == null || _transactionEntity.Status.IsInList(TransactionState.Completed))
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
if (_videoConferenceService.CanStart(Transaction_UID))
|
||||||
if (schedule_UID != Guid.Empty)
|
|
||||||
{
|
{
|
||||||
return Redirect($"/Participant/VideoCall/Room/{Transaction_UID}");
|
return Redirect($"/Participant/VideoCall/Room/{Transaction_UID}");
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,8 @@
|
|||||||
.withUrl("/notificationHub")
|
.withUrl("/notificationHub")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
connection.on("ReceiveUserNotification", (user_UID, message) => {
|
connection.on("ReceiveUserNotification", (userID, message) => {
|
||||||
|
alert(message);
|
||||||
receiveUserNotificationCallback?.(message);
|
receiveUserNotificationCallback?.(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ using System.Security.Principal;
|
|||||||
using Azure.Communication.CallAutomation;
|
using Azure.Communication.CallAutomation;
|
||||||
using Azure.Communication.Identity;
|
using Azure.Communication.Identity;
|
||||||
using Azure.Communication.Rooms;
|
using Azure.Communication.Rooms;
|
||||||
|
using Azure.Storage.Queues;
|
||||||
using Coravel;
|
using Coravel;
|
||||||
using EnotaryoPH.Data;
|
using EnotaryoPH.Data;
|
||||||
using EnotaryoPH.Web.Common.Hubs;
|
using EnotaryoPH.Web.Common.Hubs;
|
||||||
@ -35,7 +36,7 @@ namespace EnotaryoPH.Web
|
|||||||
razorBuilder.AddRazorRuntimeCompilation();
|
razorBuilder.AddRazorRuntimeCompilation();
|
||||||
#endif
|
#endif
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
builder.Services.AddSingleton<NotificationService>();
|
builder.Services.AddSingleton<INotificationService, NotificationService>();
|
||||||
|
|
||||||
var config = builder.Configuration;
|
var config = builder.Configuration;
|
||||||
builder.Services.AddTransient<IConfiguration, ConfigurationManager>(provider => config);
|
builder.Services.AddTransient<IConfiguration, ConfigurationManager>(provider => config);
|
||||||
@ -60,9 +61,12 @@ namespace EnotaryoPH.Web
|
|||||||
return new CompreFaceClient(host, port);
|
return new CompreFaceClient(host, port);
|
||||||
});
|
});
|
||||||
builder.Services.AddQueue();
|
builder.Services.AddQueue();
|
||||||
|
builder.Services.AddScheduler();
|
||||||
builder.Services.AddMailer(config);
|
builder.Services.AddMailer(config);
|
||||||
builder.Services.AddTransient<SignatoryInvitationInvocable>();
|
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 =>
|
builder.Services.AddScoped<RoomsClient>(provider =>
|
||||||
{
|
{
|
||||||
var connectionString = config.GetConnectionString("AzureCommunication");
|
var connectionString = config.GetConnectionString("AzureCommunication");
|
||||||
@ -84,9 +88,20 @@ namespace EnotaryoPH.Web
|
|||||||
? throw new InvalidConfigurationException("AzureCommunication", string.Empty)
|
? throw new InvalidConfigurationException("AzureCommunication", string.Empty)
|
||||||
: new CallAutomationClient(connectionString);
|
: 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();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.Services.UseScheduler(scheduler => scheduler
|
||||||
|
.Schedule<CheckRecordingAvailabilityInvocable>()
|
||||||
|
.EveryTenSeconds());
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
"context": "browser",
|
"context": "browser",
|
||||||
"outputFormat": "global",
|
"outputFormat": "global",
|
||||||
"scopeHoist": false,
|
"scopeHoist": false,
|
||||||
"optimize": true,
|
"optimize": false,
|
||||||
"sourceMap": false
|
"sourceMap": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": "> 0.5%",
|
"browserslist": "> 0.5%",
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user