video call wip

This commit is contained in:
jojo aquino 2025-04-02 22:08:17 +01:00
parent 2e31bfdb03
commit 4b5cfa6516
37 changed files with 1560 additions and 645 deletions

View 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
}
}

View File

@ -6,6 +6,7 @@
EmailSent = 1, EmailSent = 1,
Registered = 2, Registered = 2,
FaceMatch = 3, FaceMatch = 3,
Completed = 10 Approved = 4,
Rejected = 5,
} }
} }

View File

@ -7,6 +7,8 @@
DocumentUploaded = 2, DocumentUploaded = 2,
Submitted = 3, Submitted = 3,
Accepted = 4, Accepted = 4,
Approved = 5,
Rejected = 6,
Completed = 100 Completed = 100
} }
} }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }
} }
} }

View File

@ -1,206 +1,281 @@
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',
// Hide the entire control bar if needed
onFetchCustomButtonProps: this.onFetchCustomButtonPropsCallbacks
};
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const callCompositeProps = {
callControls: callControls,
formFactor: isMobile ? 'mobile' : 'desktop',
onFetchParticipantMenuItems: this.onFetchParticipantMenuItemsCallback,
options: {
callControls: callControls
} }
};
stopLocalVideo() { const adapterArgs = {
if (this.call) { userId: createIdentifierFromRawId(this.userID),
this.call.stopVideo(this.localVideoStream); //credential: new AzureCommunicationTokenCredential(this.token),
} token: this.token,
} displayName: this.displayName,
locator: { roomId: this.roomID },
callAdapterOptions: {},
callCompositeOptions: callCompositeProps
};
async joinRoom(roomId) { this.callAdapter = await callComposite.loadCallComposite(
// Assuming you have a room ID and call the appropriate ACS API to join a room adapterArgs,
// This is just a placeholder, replace with actual logic this.videoContainer, // container element,
console.log('Joining room:', roomId); callCompositeProps
if (this.callAgent) { );
try {
const localVideoStream = await this.createLocalVideoStream();
const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
this.callAgent.on('callsUpdated', e => { //this.callAdapter.callAgent.on("callsUpdated", function (e) {
e.added.forEach((addedCall) => { // e.added.forEach((addedCall) => {
addedCall.on('stateChanged', () => { // addedCall.on('stateChanged', (state) => this.stateChanged(addedCall));
if (addedCall.state === 'Connected') { // });
addedCall.info.getServerCallId().then(result => { //});
this.onGetServerCallID?.(result);
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 => { }).catch(err => {
console.log(err); console.log(err);
}); });
} }
}); });
});
this.callAdapter.on("callEnded", function (e) {
self.callEndedCallback?.(e);
}); });
const roomCallLocator = { roomId: roomId }; this.callAdapter.on("participantsJoined", function (e, f) {
let call = this.callAgent.join(roomCallLocator, { videoOptions }); self.participantsJoinedCallback?.(e, f);
this.subscribeToCall(call); });
} catch (error) {
console.error(error); this.callAdapter.on("onAddParticipant", function (e, f) {
} self.onAddParticipantCallback?.(e, f);
} });
//CallEnded
return this.callAdapter;
} }
async subscribeToCall(call) { //stopLocalVideo() {
try { // if (this.call) {
call.on('idChanged', () => { // this.call.stopVideo(this.localVideoStream);
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 async joinRoom() {
// notified when new participants are added to the call or removed from the call. await this.callAdapter?.joinCall({
call.on('remoteParticipantsUpdated', e => { microphoneOn: true,
this.remoteParticipantsUpdated?.(e); cameraOn: true
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 async stopCall(forEveryone = false) {
remoteVideoStream.on('isAvailableChanged', async () => { await this.callAdapter?.leaveCall(forEveryone);
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() { //async subscribeToCall(call) {
const camera = (await this.deviceManager.getCameras())[0]; // try {
if (camera) { // call.on('idChanged', () => {
return new LocalVideoStream(camera); // this.idChanged?.(call.id);
} else { // });
console.error(`No camera device found on the system`); // 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) {
@ -208,15 +283,19 @@ class VideoCall {
} }
} }
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;

View File

@ -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; }
}
}

View File

@ -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() ?? [];
}
}

View File

@ -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;
}
}
}

View File

@ -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);

View File

@ -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);
}
}
}

View 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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,7 @@
namespace EnotaryoPH.Web.Common.Jobs.Models
{
public class RecordingStorageInfo
{
public List<RecordingChunk> RecordingChunks { get; set; }
}
}

View File

@ -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;
}
}
}

View 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();
}
}
}

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
} }
} }

View File

@ -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);
}
}
}

View File

@ -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" />

View File

@ -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>
}

View File

@ -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("/");
}
} }
} }

View File

@ -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,18 +81,14 @@ 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
{
return _transaction.TransactionSignatories.TrueForAll(sig => sig.Status == nameof(SignatoryStatus.FaceMatch))
? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}") ? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}")
: Redirect($"/Participant/VideoCall/Waiting/{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 };
public bool IsTaken => string.Equals(_transaction?.Status, nameof(TransactionState.Accepted), StringComparison.OrdinalIgnoreCase); public bool IsTaken => string.Equals(_transaction?.Status, nameof(TransactionState.Accepted), StringComparison.OrdinalIgnoreCase);

View File

@ -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;
}
.video-container video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover; /* This will maintain the video's original aspect ratio and crop if necessary */
}
.video-element {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
.participant-name {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0,0,0,0.5);
padding: 2px 8px;
border-radius: 4px;
z-index: 1;
}
.controls {
position: fixed; position: fixed;
bottom: 20px; top: 0;
left: 50%; bottom: 0;
/* transform: translateX(-50%); */ left: auto;
background: rgba(0,0,0,0.8); right: 0;
padding: 10px; margin: 0;
border-radius: 20px; padding: 0;
width: 350px; /* Adjust the width as needed */
height: 100%;
} }
.participant-col { .modal-content-right {
transition: all 0.3s ease; border-radius: 0;
height: 100vh;
width: 100%;
background-color: #fff;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.3);
}
display:flex; .list-group {
justify-content: center; list-style: none;
padding: 0;
}
.list-group-item {
display: flex;
align-items: center; align-items: center;
gap: 10px;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
/* background-color: #f9f9f9; */
/* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
} }
</style>
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #007bff;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
text-transform: uppercase;
}
.modal-header.draggable {
cursor: move;
}
</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>
} }

View File

@ -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);
private void LoadVideoConferenceSchedule(Guid schedule_UID) => _LawyerVideoConferenceSchedule = _dbContext.LawyerVideoConferenceSchedules
.Include(sched => sched.LawyerVideoConferenceParticipants)
.ThenInclude(p => p.Participant)
.Include(sched => sched.Lawyer)
.ThenInclude(l => l.User) .ThenInclude(l => l.User)
.FirstOrDefault(sched => sched.LawyerVideoConferenceSchedule_UID == schedule_UID); .Include(t => t.Principal)
.Include(t => t.Schedule)
.ThenInclude(sch => sch.LawyerVideoConferenceParticipants)
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
public string CommunicationRoomId { get; private set; } 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 =>
new RoomParticipantViewModel
{ {
Id = p.LawyerVideoConferenceParticipant_UID.ToString(), Id = p.LawyerVideoConferenceParticipant_UID.ToString(),
DisplayName = $"{p.Participant.Firstname} {p.Participant.Lastname}".Trim().DefaultIfEmpty(p.Participant.Email), DisplayName =
$"{p.Participant.Firstname} {p.Participant.Lastname}".Trim()
.DefaultIfEmpty(p.Participant.Email),
RoomUserID = p.MeetingRoomUserID, RoomUserID = p.MeetingRoomUserID,
Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal)) 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; }
} }
} }

View File

@ -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) {
participantList.innerHTML = '';
console.log(participants);
}
defaultMenuItems = [];
},
onFetchCustomButtonPropsCallbacks: customButtons,
onAddParticipantCallback: function (e, f) {
//debugger;
console.log(e, f);
},
participantsJoinedCallback: function (e, f) {
//debugger;
console.log(e, f);
},
callEndedCallback: function (a, b, c) {
},
token: control_communicationUserToken.value,
roomID: control_communicationRoomId.value,
displayName: control_displayName.value,
userID: control_communicationUserId.value,
videoContainer: control_videoGridContainer
};
await videoCall.init(options);
await videoCall.joinRoom();
}
function approveTransaction(e) {
jfa.communication.videocall.stopCall(true);
let url = jfa.utilities.routing.getCurrentURLWithHandler("Approve");
jfa.utilities.request.post(url, {}) jfa.utilities.request.post(url, {})
.then(resp => { .then(resp => {
if (resp.ok === true) { if (resp.ok === true) {
console.log("started recording"); debugger;
} }
}) })
.catch(err => console.error(err)); .catch(err => console.error(err));
} }
};
await videoCall.init(userAccessToken, options);
videoCall.joinRoom(control_communicationRoomId.value);
}
function _updateGrid() { //function _updateGrid() {
control_videoGrid.innerHTML = ''; // control_videoGrid.innerHTML = '';
participants.forEach((participant, index) => { // participants.forEach((participant, index) => {
const col = document.createElement('div'); // const col = document.createElement('div');
col.className = 'participant-col'; // col.className = 'participant-col';
const tmpl = control_templateVideo.cloneNode(true).content; // const tmpl = control_templateVideo.cloneNode(true).content;
const vidcontainer = tmpl.querySelector(".video-container") // const vidcontainer = tmpl.querySelector(".video-container")
if (vidcontainer) { // if (vidcontainer) {
vidcontainer.id = participant.Id; // vidcontainer.id = participant.Id;
vidcontainer.classList.add(participant.Id); // vidcontainer.classList.add(participant.Id);
vidcontainer.classList.add(participant.Type == 'Notary' ? 'local-video-container' : 'remote-video-container'); // 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) {
});
control_approve.addEventListener("click", function () { const sidebarModal = bootstrap.Modal.getOrCreateInstance(control_rightSidebarModal);
let url = jfa.utilities.routing.getCurrentURLWithHandler("StopRecording"); sidebarModal.hide();
jfa.utilities.request.post(url, {})
.then(resp => { const draggableModal = bootstrap.Modal.getOrCreateInstance(control_draggableModal);
if (resp.ok === true) { draggableModal.show();
console.log("stopped recording");
} }
})
.catch(err => console.error(err));
}); });
control_reject.addEventListener("click", function () {
alert('not yet implemented'); control_draggableModal.addEventListener('shown.bs.modal', function (e) {
makeDraggable(control_draggableModal);
}); });
} }
function makeDraggable(modal) {
let isDragging = false;
let offsetX, offsetY;
const header = modal.querySelector('.modal-header');
header.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - modal.offsetLeft;
offsetY = e.clientY - modal.offsetTop;
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
modal.style.left = `${e.clientX - offsetX}px`;
modal.style.top = `${e.clientY - offsetY}px`;
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
async function _init() { async function _init() {
_bindEvents(); _bindEvents();
_updateGrid(); //_updateGrid();
await _initVideoCall(); await _initVideoCall();
debugger;
_createParticipantListItems();
} }
await _init(); await _init();

View File

@ -9,14 +9,14 @@ 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()
@ -33,8 +33,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
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}");
} }

View File

@ -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);
}); });

View File

@ -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())
{ {

View File

@ -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