mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-12 11:15:08 +00:00
Merge branch 'development' into feature/room-combining
This commit is contained in:
@@ -11,10 +11,9 @@ namespace PepperDash.Essentials.Devices.Common.AudioCodec
|
||||
/// <summary>
|
||||
/// For rooms that have audio codec
|
||||
/// </summary>
|
||||
public interface IHasAudioCodec
|
||||
public interface IHasAudioCodec:IHasInCallFeedback
|
||||
{
|
||||
AudioCodecBase AudioCodec { get; }
|
||||
BoolFeedback InCallFeedback { get; }
|
||||
|
||||
///// <summary>
|
||||
///// Make this more specific
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
using PepperDash.Essentials.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.Codec
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a device that has Do Not Disturb mode capability
|
||||
/// </summary>
|
||||
public interface IHasDoNotDisturbMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Indictes whether Do Not Disturb mode is on (Activated)
|
||||
/// </summary>
|
||||
BoolFeedback DoNotDisturbModeIsOnFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Activates Do Not Disturb mode
|
||||
/// </summary>
|
||||
void ActivateDoNotDisturbMode();
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates Do Not Disturb mode
|
||||
/// </summary>
|
||||
void DeactivateDoNotDisturbMode();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles Do Not Disturb mode
|
||||
/// </summary>
|
||||
void ToggleDoNotDisturbMode();
|
||||
}
|
||||
|
||||
public interface IHasDoNotDisturbModeWithTimeout : IHasDoNotDisturbMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Activates Do Not Disturb mode with a timeout
|
||||
/// </summary>
|
||||
/// <param name="timeout"></param>
|
||||
void ActivateDoNotDisturbMode(int timeout);
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,19 @@ namespace PepperDash.Essentials.Devices.Common.Codec
|
||||
/// </summary>
|
||||
public interface IInvitableContact
|
||||
{
|
||||
bool IsInvitableContact { get; }
|
||||
}
|
||||
|
||||
public class InvitableDirectoryContact : DirectoryContact, IInvitableContact
|
||||
{
|
||||
[JsonProperty("isInvitableContact")]
|
||||
public bool IsInvitableContact
|
||||
{
|
||||
get
|
||||
{
|
||||
return this is IInvitableContact;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -209,8 +221,6 @@ namespace PepperDash.Essentials.Devices.Common.Codec
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
|
||||
|
||||
[JsonProperty("contactMethods")]
|
||||
public List<ContactMethod> ContactMethods { get; set; }
|
||||
|
||||
|
||||
@@ -235,6 +235,15 @@ namespace PepperDash.Essentials.Devices.Common.Codec
|
||||
{
|
||||
Calls = new List<Call>();
|
||||
}
|
||||
|
||||
#region Overrides of Object
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Format("{0}:{1}: {2}-{3}", Title, Agenda, StartTime, EndTime);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class Call
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
<Compile Include="Codec\eCodecCallStatus.cs" />
|
||||
<Compile Include="Codec\eMeetingPrivacy.cs" />
|
||||
<Compile Include="Codec\iCodecAudio.cs" />
|
||||
<Compile Include="Codec\IHasDoNotDisturb.cs" />
|
||||
<Compile Include="Codec\IHasExternalSourceSwitching.cs" />
|
||||
<Compile Include="ImageProcessors\TVOneCorio.cs" />
|
||||
<Compile Include="ImageProcessors\TVOneCorioPropertiesConfig.cs" />
|
||||
@@ -117,12 +118,17 @@
|
||||
<Compile Include="ImageProcessors\AnalogWay\AnalogWayLiveCorePropertiesConfig.cs" />
|
||||
<Compile Include="SoftCodec\BlueJeansPc.cs" />
|
||||
<Compile Include="VideoCodec\CiscoCodec\CiscoCamera.cs" />
|
||||
<Compile Include="VideoCodec\CiscoCodec\CiscoCodecJoinMap.cs" />
|
||||
<Compile Include="VideoCodec\CiscoCodec\RoomPresets.cs" />
|
||||
<Compile Include="Cameras\CameraControl.cs" />
|
||||
<Compile Include="Display\PanasonicThDisplay.cs" />
|
||||
<Compile Include="VideoCodec\Interfaces\IHasMeetingInfo.cs" />
|
||||
<Compile Include="VideoCodec\Interfaces\IHasParticipants.cs" />
|
||||
<Compile Include="VideoCodec\Interfaces\IHasPresentationOnlyMeeting.cs" />
|
||||
<Compile Include="VideoCodec\Interfaces\IHasSelfviewPosition.cs" />
|
||||
<Compile Include="VideoCodec\Interfaces\IHasSelfviewSize.cs" />
|
||||
<Compile Include="VideoCodec\Interfaces\IHasStandbyMode.cs" />
|
||||
<Compile Include="VideoCodec\Interfaces\IHasStartMeeting.cs" />
|
||||
<Compile Include="VideoCodec\Interfaces\iVideoCodecInfo.cs" />
|
||||
<Compile Include="Codec\iHasCallFavorites.cs" />
|
||||
<Compile Include="Codec\iHasCallHistory.cs" />
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using PepperDash.Essentials.Core;
|
||||
using PepperDash.Essentials.Core.Bridges.JoinMaps;
|
||||
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
{
|
||||
public class CiscoCodecJoinMap : VideoCodecControllerJoinMap
|
||||
{
|
||||
#region Digital
|
||||
|
||||
[JoinName("ActivateDoNotDisturbMode")]
|
||||
public JoinDataComplete ActivateDoNotDisturbMode = new JoinDataComplete(
|
||||
new JoinData
|
||||
{
|
||||
JoinNumber = 221,
|
||||
JoinSpan = 1
|
||||
},
|
||||
new JoinMetadata
|
||||
{
|
||||
Description = "Activates Do Not Disturb Mode. FB High if active.",
|
||||
JoinCapabilities = eJoinCapabilities.ToFromSIMPL,
|
||||
JoinType = eJoinType.Digital
|
||||
});
|
||||
|
||||
[JoinName("DeactivateDoNotDisturbMode")]
|
||||
public JoinDataComplete DeactivateDoNotDisturbMode = new JoinDataComplete(
|
||||
new JoinData
|
||||
{
|
||||
JoinNumber = 222,
|
||||
JoinSpan = 1
|
||||
},
|
||||
new JoinMetadata
|
||||
{
|
||||
Description = "Deactivates Do Not Disturb Mode. FB High if deactivated.",
|
||||
JoinCapabilities = eJoinCapabilities.ToFromSIMPL,
|
||||
JoinType = eJoinType.Digital
|
||||
});
|
||||
|
||||
[JoinName("ToggleDoNotDisturbMode")]
|
||||
public JoinDataComplete ToggleDoNotDisturbMode = new JoinDataComplete(
|
||||
new JoinData
|
||||
{
|
||||
JoinNumber = 223,
|
||||
JoinSpan = 1
|
||||
},
|
||||
new JoinMetadata
|
||||
{
|
||||
Description = "Toggles Do Not Disturb Mode.",
|
||||
JoinCapabilities = eJoinCapabilities.ToSIMPL,
|
||||
JoinType = eJoinType.Digital
|
||||
});
|
||||
|
||||
[JoinName("ActivateStandby")]
|
||||
public JoinDataComplete ActivateStandby = new JoinDataComplete(
|
||||
new JoinData
|
||||
{
|
||||
JoinNumber = 226,
|
||||
JoinSpan = 1
|
||||
},
|
||||
new JoinMetadata
|
||||
{
|
||||
Description = "Activates Standby Mode. FB High if active.",
|
||||
JoinCapabilities = eJoinCapabilities.ToFromSIMPL,
|
||||
JoinType = eJoinType.Digital
|
||||
});
|
||||
|
||||
[JoinName("DeactivateStandby")]
|
||||
public JoinDataComplete DeactivateStandby = new JoinDataComplete(
|
||||
new JoinData
|
||||
{
|
||||
JoinNumber = 227,
|
||||
JoinSpan = 1
|
||||
},
|
||||
new JoinMetadata
|
||||
{
|
||||
Description = "Deactivates Standby Mode. FB High if deactivated.",
|
||||
JoinCapabilities = eJoinCapabilities.ToFromSIMPL,
|
||||
JoinType = eJoinType.Digital
|
||||
});
|
||||
|
||||
[JoinName("ActivateHalfWakeMode")]
|
||||
public JoinDataComplete ActivateHalfWakeMode = new JoinDataComplete(
|
||||
new JoinData
|
||||
{
|
||||
JoinNumber = 228,
|
||||
JoinSpan = 1
|
||||
},
|
||||
new JoinMetadata
|
||||
{
|
||||
Description = "Activates Half Wake Mode. FB High if active.",
|
||||
JoinCapabilities = eJoinCapabilities.ToFromSIMPL,
|
||||
JoinType = eJoinType.Digital
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Analog
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Serials
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
public CiscoCodecJoinMap(uint joinStart)
|
||||
: base(joinStart, typeof(CiscoCodecJoinMap))
|
||||
{
|
||||
}
|
||||
|
||||
public CiscoCodecJoinMap(uint joinStart, Type type)
|
||||
: base(joinStart, type)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
|
||||
public class CiscoSparkCodec : VideoCodecBase, IHasCallHistory, IHasCallFavorites, IHasDirectoryHistoryStack,
|
||||
IHasScheduleAwareness, IOccupancyStatusProvider, IHasCodecLayouts, IHasCodecSelfView,
|
||||
ICommunicationMonitor, IRouting, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IHasExternalSourceSwitching, IHasBranding, IHasCameraOff, IHasCameraMute
|
||||
ICommunicationMonitor, IRouting, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets,
|
||||
IHasExternalSourceSwitching, IHasBranding, IHasCameraOff, IHasCameraMute, IHasDoNotDisturbMode,
|
||||
IHasHalfWakeMode
|
||||
{
|
||||
private bool _externalSourceChangeRequested;
|
||||
|
||||
@@ -326,6 +328,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
CameraIsMutedFeedback = CameraIsOffFeedback;
|
||||
SupportsCameraOff = true;
|
||||
|
||||
DoNotDisturbModeIsOnFeedback = new BoolFeedback(() => CodecStatus.Status.Conference.DoNotDisturb.BoolValue);
|
||||
HalfWakeModeIsOnFeedback = new BoolFeedback(() => CodecStatus.Status.Standby.State.Value == "Halfwake");
|
||||
|
||||
PresentationViewMaximizedFeedback = new BoolFeedback(() => CurrentPresentationView == "Maximized");
|
||||
|
||||
Communication = comm;
|
||||
@@ -422,7 +427,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
CodecStatus.Status.Audio.Volume.ValueChangedAction = VolumeLevelFeedback.FireUpdate;
|
||||
CodecStatus.Status.Audio.VolumeMute.ValueChangedAction = MuteFeedback.FireUpdate;
|
||||
CodecStatus.Status.Audio.Microphones.Mute.ValueChangedAction = PrivacyModeIsOnFeedback.FireUpdate;
|
||||
CodecStatus.Status.Standby.State.ValueChangedAction = StandbyIsOnFeedback.FireUpdate;
|
||||
CodecStatus.Status.Standby.State.ValueChangedAction = new Action(() =>
|
||||
{
|
||||
StandbyIsOnFeedback.FireUpdate();
|
||||
HalfWakeModeIsOnFeedback.FireUpdate();
|
||||
});
|
||||
CodecStatus.Status.RoomAnalytics.PeoplePresence.ValueChangedAction = RoomIsOccupiedFeedback.FireUpdate;
|
||||
CodecStatus.Status.RoomAnalytics.PeopleCount.Current.ValueChangedAction = PeopleCountFeedback.FireUpdate;
|
||||
CodecStatus.Status.Cameras.SpeakerTrack.Status.ValueChangedAction = CameraAutoModeIsOnFeedback.FireUpdate;
|
||||
@@ -432,6 +441,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
CodecStatus.Status.Video.Layout.LayoutFamily.Local.ValueChangedAction = ComputeLocalLayout;
|
||||
CodecStatus.Status.Conference.Presentation.Mode.ValueChangedAction = SharingContentIsOnFeedback.FireUpdate;
|
||||
CodecStatus.Status.Conference.Presentation.Mode.ValueChangedAction = FarEndIsSharingContentFeedback.FireUpdate;
|
||||
CodecStatus.Status.Conference.DoNotDisturb.ValueChangedAction = DoNotDisturbModeIsOnFeedback.FireUpdate;
|
||||
|
||||
try
|
||||
{
|
||||
CodecStatus.Status.Video.Input.MainVideoMute.ValueChangedAction = CameraIsOffFeedback.FireUpdate;
|
||||
@@ -584,6 +595,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
prefix + "/Status/Audio" + Delimiter +
|
||||
prefix + "/Status/Call" + Delimiter +
|
||||
prefix + "/Status/Conference/Presentation" + Delimiter +
|
||||
prefix + "/Status/Conference/DoNotDisturb" + Delimiter +
|
||||
prefix + "/Status/Cameras/SpeakerTrack" + Delimiter +
|
||||
prefix + "/Status/RoomAnalytics" + Delimiter +
|
||||
prefix + "/Status/RoomPreset" + Delimiter +
|
||||
@@ -592,10 +604,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
prefix + "/Status/Video/Layout" + Delimiter +
|
||||
prefix + "/Status/Video/Input/MainVideoMute" + Delimiter +
|
||||
prefix + "/Bookings" + Delimiter +
|
||||
prefix + "/Event/CallDisconnect" + Delimiter +
|
||||
prefix + "/Event/Bookings" + Delimiter +
|
||||
prefix + "/Event/CameraPresetListUpdated" + Delimiter +
|
||||
prefix + "/Event/UserInterface/Presentation/ExternalSource/Selected/SourceIdentifier" + Delimiter;
|
||||
prefix + "/Event/UserInterface/Presentation/ExternalSource/Selected/SourceIdentifier" + Delimiter +
|
||||
prefix + "/Event/CallDisconnect" + Delimiter; // Keep CallDisconnect last to detect when feedback registration completes correctly
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -752,6 +765,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the delimiter and send the command to the codec
|
||||
/// </summary>
|
||||
/// <param name="command"></param>
|
||||
public void SendText(string command)
|
||||
{
|
||||
if (CommDebuggingIsOn)
|
||||
@@ -1490,7 +1507,49 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
|
||||
public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge)
|
||||
{
|
||||
var joinMap = new CiscoCodecJoinMap(joinStart);
|
||||
|
||||
var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey);
|
||||
|
||||
if (customJoins != null)
|
||||
{
|
||||
joinMap.SetCustomJoinData(customJoins);
|
||||
}
|
||||
|
||||
if (bridge != null)
|
||||
{
|
||||
bridge.AddJoinMap(Key, joinMap);
|
||||
}
|
||||
|
||||
LinkVideoCodecToApi(this, trilist, joinStart, joinMapKey, bridge);
|
||||
|
||||
LinkCiscoCodecToApi(trilist, joinMap);
|
||||
}
|
||||
|
||||
public void LinkCiscoCodecToApi(BasicTriList trilist, CiscoCodecJoinMap joinMap)
|
||||
{
|
||||
var dndCodec = this as IHasDoNotDisturbMode;
|
||||
if (dndCodec != null)
|
||||
{
|
||||
dndCodec.DoNotDisturbModeIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.ActivateDoNotDisturbMode.JoinNumber]);
|
||||
dndCodec.DoNotDisturbModeIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.DeactivateDoNotDisturbMode.JoinNumber]);
|
||||
|
||||
trilist.SetSigFalseAction(joinMap.ActivateDoNotDisturbMode.JoinNumber, () => dndCodec.ActivateDoNotDisturbMode());
|
||||
trilist.SetSigFalseAction(joinMap.DeactivateDoNotDisturbMode.JoinNumber, () => dndCodec.DeactivateDoNotDisturbMode());
|
||||
trilist.SetSigFalseAction(joinMap.ToggleDoNotDisturbMode.JoinNumber, () => dndCodec.ToggleDoNotDisturbMode());
|
||||
}
|
||||
|
||||
var halfwakeCodec = this as IHasHalfWakeMode;
|
||||
if (halfwakeCodec != null)
|
||||
{
|
||||
halfwakeCodec.StandbyIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.ActivateStandby.JoinNumber]);
|
||||
halfwakeCodec.StandbyIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.DeactivateStandby.JoinNumber]);
|
||||
halfwakeCodec.HalfWakeModeIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.ActivateHalfWakeMode.JoinNumber]);
|
||||
|
||||
trilist.SetSigFalseAction(joinMap.ActivateStandby.JoinNumber, () => halfwakeCodec.StandbyActivate());
|
||||
trilist.SetSigFalseAction(joinMap.DeactivateStandby.JoinNumber, () => halfwakeCodec.StandbyDeactivate());
|
||||
trilist.SetSigFalseAction(joinMap.ActivateHalfWakeMode.JoinNumber, () => halfwakeCodec.HalfwakeActivate());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2074,6 +2133,45 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
else
|
||||
CameraMuteOn();
|
||||
}
|
||||
|
||||
#region IHasDoNotDisturbMode Members
|
||||
|
||||
public BoolFeedback DoNotDisturbModeIsOnFeedback { get; private set; }
|
||||
|
||||
public void ActivateDoNotDisturbMode()
|
||||
{
|
||||
SendText("xCommand Conference DoNotDisturb Activate");
|
||||
}
|
||||
|
||||
public void DeactivateDoNotDisturbMode()
|
||||
{
|
||||
SendText("xCommand Conference DoNotDisturb Deactivate");
|
||||
}
|
||||
|
||||
public void ToggleDoNotDisturbMode()
|
||||
{
|
||||
if (DoNotDisturbModeIsOnFeedback.BoolValue)
|
||||
{
|
||||
DeactivateDoNotDisturbMode();
|
||||
}
|
||||
else
|
||||
{
|
||||
ActivateDoNotDisturbMode();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IHasHalfWakeMode Members
|
||||
|
||||
public BoolFeedback HalfWakeModeIsOnFeedback { get; private set; }
|
||||
|
||||
public void HalfwakeActivate()
|
||||
{
|
||||
SendText("xCommand Standby Halfwake");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -440,9 +440,26 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
public CallId CallId { get; set; }
|
||||
}
|
||||
|
||||
public class DoNotDisturb
|
||||
public class DoNotDisturb : ValueProperty
|
||||
{
|
||||
public string Value { get; set; }
|
||||
string _Value;
|
||||
|
||||
public bool BoolValue { get; private set; }
|
||||
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Value = value;
|
||||
// If the incoming value is "On" it sets the BoolValue true, otherwise sets it false
|
||||
BoolValue = value == "On" || value == "Active";
|
||||
OnValueChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Mode
|
||||
@@ -600,6 +617,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
public Conference2()
|
||||
{
|
||||
Presentation = new Presentation();
|
||||
DoNotDisturb = new DoNotDisturb();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1380,12 +1398,16 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
|
||||
public class State : ValueProperty
|
||||
{
|
||||
string _value;
|
||||
|
||||
public bool BoolValue { get; private set; }
|
||||
|
||||
public string Value // Valid values are Standby/EnteringStandby/Halfwake/Off
|
||||
{
|
||||
get { return _value; }
|
||||
set
|
||||
{
|
||||
_value = value;
|
||||
// If the incoming value is "On" it sets the BoolValue true, otherwise sets it false
|
||||
BoolValue = value == "On" || value == "Standby";
|
||||
OnValueChanged();
|
||||
@@ -2091,6 +2113,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco
|
||||
Conference = new Conference2();
|
||||
SystemUnit = new SystemUnit();
|
||||
Video = new Video();
|
||||
Conference = new Conference2();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a device that provides meeting information (like a ZoomRoom)
|
||||
/// </summary>
|
||||
public interface IHasMeetingInfo
|
||||
{
|
||||
event EventHandler<MeetingInfoEventArgs> MeetingInfoChanged;
|
||||
|
||||
MeetingInfo MeetingInfo { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the information about a meeting in progress
|
||||
/// Currently used for Zoom meetings
|
||||
/// </summary>
|
||||
public class MeetingInfo
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; private set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; private set; }
|
||||
[JsonProperty("host")]
|
||||
public string Host { get; private set; }
|
||||
[JsonProperty("password")]
|
||||
public string Password { get; private set; }
|
||||
[JsonProperty("shareStatus")]
|
||||
public string ShareStatus { get; private set; }
|
||||
[JsonProperty("isHost")]
|
||||
public Boolean IsHost { get; private set; }
|
||||
[JsonProperty("isSharingMeeting")]
|
||||
public Boolean IsSharingMeeting { get; private set; }
|
||||
[JsonProperty("waitingForHost")]
|
||||
public Boolean WaitingForHost { get; private set; }
|
||||
|
||||
public MeetingInfo(string id, string name, string host, string password, string shareStatus, bool isHost, bool isSharingMeeting, bool waitingForHost)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Host = host;
|
||||
Password = password;
|
||||
ShareStatus = shareStatus;
|
||||
IsHost = isHost;
|
||||
IsSharingMeeting = isSharingMeeting;
|
||||
WaitingForHost = waitingForHost;
|
||||
}
|
||||
}
|
||||
|
||||
public class MeetingInfoEventArgs : EventArgs
|
||||
{
|
||||
public MeetingInfo Info { get; private set; }
|
||||
|
||||
public MeetingInfoEventArgs(MeetingInfo info)
|
||||
{
|
||||
Info = info;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,100 +1,112 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a device that has call participants
|
||||
/// </summary>
|
||||
public interface IHasParticipants
|
||||
{
|
||||
CodecParticipants Participants { get; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Describes a device that has call participants
|
||||
/// </summary>
|
||||
public interface IHasParticipants
|
||||
{
|
||||
CodecParticipants Participants { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the ability to mute and unmute a participant's video in a meeting
|
||||
/// </summary>
|
||||
public interface IHasParticipantVideoMute:IHasParticipants
|
||||
{
|
||||
void MuteVideoForParticipant(int userId);
|
||||
void UnmuteVideoForParticipant(int userId);
|
||||
void ToggleVideoForParticipant(int userId);
|
||||
}
|
||||
/// <summary>
|
||||
/// Describes the ability to mute and unmute a participant's video in a meeting
|
||||
/// </summary>
|
||||
public interface IHasParticipantVideoMute : IHasParticipants
|
||||
{
|
||||
void MuteVideoForParticipant(int userId);
|
||||
void UnmuteVideoForParticipant(int userId);
|
||||
void ToggleVideoForParticipant(int userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the ability to mute and unmute a participant's audio in a meeting
|
||||
/// </summary>
|
||||
public interface IHasParticipantAudioMute : IHasParticipantVideoMute
|
||||
{
|
||||
void MuteAudioForParticipant(int userId);
|
||||
void UnmuteAudioForParticipant(int userId);
|
||||
void ToggleAudioForParticipant(int userId);
|
||||
}
|
||||
/// <summary>
|
||||
/// Describes the ability to mute and unmute a participant's audio in a meeting
|
||||
/// </summary>
|
||||
public interface IHasParticipantAudioMute : IHasParticipantVideoMute
|
||||
{
|
||||
void MuteAudioForParticipant(int userId);
|
||||
void UnmuteAudioForParticipant(int userId);
|
||||
void ToggleAudioForParticipant(int userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the ability to pin and unpin a participant in a meeting
|
||||
/// </summary>
|
||||
public interface IHasParticipantPinUnpin : IHasParticipants
|
||||
{
|
||||
IntFeedback NumberOfScreensFeedback { get; }
|
||||
int ScreenIndexToPinUserTo { get; }
|
||||
/// <summary>
|
||||
/// Describes the ability to pin and unpin a participant in a meeting
|
||||
/// </summary>
|
||||
public interface IHasParticipantPinUnpin : IHasParticipants
|
||||
{
|
||||
IntFeedback NumberOfScreensFeedback { get; }
|
||||
int ScreenIndexToPinUserTo { get; }
|
||||
|
||||
void PinParticipant(int userId, int screenIndex);
|
||||
void UnPinParticipant(int userId);
|
||||
void ToggleParticipantPinState(int userId, int screenIndex);
|
||||
}
|
||||
void PinParticipant(int userId, int screenIndex);
|
||||
void UnPinParticipant(int userId);
|
||||
void ToggleParticipantPinState(int userId, int screenIndex);
|
||||
}
|
||||
|
||||
public class CodecParticipants
|
||||
{
|
||||
private List<Participant> _currentParticipants;
|
||||
|
||||
public List<Participant> CurrentParticipants {
|
||||
get { return _currentParticipants; }
|
||||
set
|
||||
public class CodecParticipants
|
||||
{
|
||||
private List<Participant> _currentParticipants;
|
||||
|
||||
public List<Participant> CurrentParticipants
|
||||
{
|
||||
get { return _currentParticipants; }
|
||||
set
|
||||
{
|
||||
_currentParticipants = value;
|
||||
OnParticipantsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Participant Host
|
||||
{
|
||||
get
|
||||
{
|
||||
_currentParticipants = value;
|
||||
OnParticipantsChanged();
|
||||
return _currentParticipants.FirstOrDefault(p => p.IsHost);
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs> ParticipantsListHasChanged;
|
||||
public event EventHandler<EventArgs> ParticipantsListHasChanged;
|
||||
|
||||
public CodecParticipants()
|
||||
{
|
||||
_currentParticipants = new List<Participant>();
|
||||
}
|
||||
public CodecParticipants()
|
||||
{
|
||||
_currentParticipants = new List<Participant>();
|
||||
}
|
||||
|
||||
public void OnParticipantsChanged()
|
||||
{
|
||||
var handler = ParticipantsListHasChanged;
|
||||
public void OnParticipantsChanged()
|
||||
{
|
||||
var handler = ParticipantsListHasChanged;
|
||||
|
||||
if (handler == null) return;
|
||||
if (handler == null) return;
|
||||
|
||||
handler(this, new EventArgs());
|
||||
}
|
||||
}
|
||||
handler(this, new EventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a call participant
|
||||
/// </summary>
|
||||
public class Participant
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public bool IsHost { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool CanMuteVideo { get; set; }
|
||||
public bool CanUnmuteVideo { get; set; }
|
||||
public bool VideoMuteFb { get; set; }
|
||||
public bool AudioMuteFb { get; set; }
|
||||
public bool HandIsRaisedFb { get; set; }
|
||||
public bool IsPinnedFb { get; set; }
|
||||
public int ScreenIndexIsPinnedToFb { get; set; }
|
||||
/// <summary>
|
||||
/// Represents a call participant
|
||||
/// </summary>
|
||||
public class Participant
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public bool IsHost { get; set; }
|
||||
public bool IsMyself { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool CanMuteVideo { get; set; }
|
||||
public bool CanUnmuteVideo { get; set; }
|
||||
public bool VideoMuteFb { get; set; }
|
||||
public bool AudioMuteFb { get; set; }
|
||||
public bool HandIsRaisedFb { get; set; }
|
||||
public bool IsPinnedFb { get; set; }
|
||||
public int ScreenIndexIsPinnedToFb { get; set; }
|
||||
|
||||
public Participant()
|
||||
{
|
||||
// Initialize to -1 (no screen)
|
||||
ScreenIndexIsPinnedToFb = -1;
|
||||
}
|
||||
}
|
||||
public Participant()
|
||||
{
|
||||
// Initialize to -1 (no screen)
|
||||
ScreenIndexIsPinnedToFb = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces
|
||||
{
|
||||
public interface IHasPresentationOnlyMeeting
|
||||
{
|
||||
void StartSharingOnlyMeeting();
|
||||
void StartSharingOnlyMeeting(eSharingMeetingMode mode);
|
||||
void StartSharingOnlyMeeting(eSharingMeetingMode mode, ushort duration);
|
||||
void StartSharingOnlyMeeting(eSharingMeetingMode mode, ushort duration, string password);
|
||||
void StartNormalMeetingFromSharingOnlyMeeting();
|
||||
}
|
||||
|
||||
public enum eSharingMeetingMode
|
||||
{
|
||||
None,
|
||||
Laptop,
|
||||
Ios,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
using PepperDash.Essentials.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a device that has Standby Mode capability
|
||||
/// </summary>
|
||||
public interface IHasStandbyMode
|
||||
{
|
||||
BoolFeedback StandbyIsOnFeedback { get; }
|
||||
|
||||
void StandbyActivate();
|
||||
|
||||
void StandbyDeactivate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a device that has Half Waek Mode capability
|
||||
/// </summary>
|
||||
public interface IHasHalfWakeMode : IHasStandbyMode
|
||||
{
|
||||
BoolFeedback HalfWakeModeIsOnFeedback { get; }
|
||||
|
||||
void HalfwakeActivate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the ability to start an ad-hoc meeting
|
||||
/// </summary>
|
||||
public interface IHasStartMeeting
|
||||
{
|
||||
/// <summary>
|
||||
/// The default meeting duration in minutes
|
||||
/// </summary>
|
||||
uint DefaultMeetingDurationMin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Start an ad-hoc meeting for the specified duration
|
||||
/// </summary>
|
||||
/// <param name="duration"></param>
|
||||
void StartMeeting(uint duration);
|
||||
|
||||
/// <summary>
|
||||
/// Leaves a meeting without ending it
|
||||
/// </summary>
|
||||
void LeaveMeeting();
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||
/// <summary>
|
||||
/// For rooms that have video codec
|
||||
/// </summary>
|
||||
public interface IHasVideoCodec
|
||||
public interface IHasVideoCodec:IHasInCallFeedback,IPrivacy
|
||||
{
|
||||
VideoCodecBase VideoCodec { get; }
|
||||
BoolFeedback InCallFeedback { get; }
|
||||
|
||||
///// <summary>
|
||||
///// Make this more specific
|
||||
@@ -26,11 +25,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||
/// </summary>
|
||||
IntFeedback CallTypeFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
BoolFeedback PrivacyModeIsOnFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// When something in the room is sharing with the far end or through other means
|
||||
/// </summary>
|
||||
|
||||
@@ -123,12 +123,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||
/// Creates the fake OSD source, and connects it's AudioVideo output to the CodecOsdIn input
|
||||
/// to enable routing
|
||||
/// </summary>
|
||||
void CreateOsdSource()
|
||||
private void CreateOsdSource()
|
||||
{
|
||||
OsdSource = new DummyRoutingInputsDevice(Key + "[osd]");
|
||||
DeviceManager.AddDevice(OsdSource);
|
||||
var tl = new TieLine(OsdSource.AudioVideoOutputPort, CodecOsdIn);
|
||||
TieLineCollection.Default.Add(tl);
|
||||
|
||||
//foreach(var input in Status.Video.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -580,6 +582,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||
|
||||
void SetupCameras()
|
||||
{
|
||||
SupportsCameraAutoMode = true;
|
||||
|
||||
SupportsCameraOff = false;
|
||||
|
||||
Cameras = new List<CameraBase>();
|
||||
|
||||
var internalCamera = new MockVCCamera(Key + "-camera1", "Near End", this);
|
||||
|
||||
@@ -25,7 +25,7 @@ using Feedback = PepperDash.Essentials.Core.Feedback;
|
||||
namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||
{
|
||||
public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs,
|
||||
IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced
|
||||
IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode
|
||||
{
|
||||
private const int XSigEncoding = 28591;
|
||||
protected const int MaxParticipants = 50;
|
||||
@@ -836,10 +836,10 @@ ScreenIndexIsPinnedTo: {8} (a{17})
|
||||
//serials
|
||||
tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, meeting.Organizer);
|
||||
tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, meeting.Title);
|
||||
tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, meeting.StartTime.ToShortDateString());
|
||||
tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, meeting.StartTime.ToShortTimeString());
|
||||
tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, meeting.EndTime.ToShortDateString());
|
||||
tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, meeting.EndTime.ToShortTimeString());
|
||||
tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, meeting.StartTime.ToString("t", Global.Culture));
|
||||
tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, meeting.StartTime.ToString("t", Global.Culture));
|
||||
tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, meeting.EndTime.ToString("t", Global.Culture));
|
||||
tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, meeting.EndTime.ToString("t", Global.Culture));
|
||||
tokenArray[stringIndex + 6] = new XSigSerialToken(stringIndex + 7, meeting.Id);
|
||||
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
public List<zStatus.AudioVideoInputOutputLineItem> AudioOuputs { get; set; }
|
||||
public List<zStatus.AudioVideoInputOutputLineItem> Cameras { get; set; }
|
||||
public zEvent.PhoneCallStatus PhoneCall { get; set; }
|
||||
public zEvent.NeedWaitForHost NeedWaitForHost { get; set; }
|
||||
|
||||
public ZoomRoomStatus()
|
||||
{
|
||||
@@ -76,6 +77,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
AudioOuputs = new List<zStatus.AudioVideoInputOutputLineItem>();
|
||||
Cameras = new List<zStatus.AudioVideoInputOutputLineItem>();
|
||||
PhoneCall = new zEvent.PhoneCallStatus();
|
||||
NeedWaitForHost = new zEvent.NeedWaitForHost();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,14 +238,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to be able to inplement IInvitableContact on DirectoryContact
|
||||
/// </summary>
|
||||
public class ZoomDirectoryContact : DirectoryContact, IInvitableContact
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class Phonebook
|
||||
{
|
||||
[JsonProperty("Contacts")]
|
||||
@@ -302,7 +296,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
|
||||
foreach (Contact c in zoomContacts)
|
||||
{
|
||||
var contact = new ZoomDirectoryContact { Name = c.ScreenName, ContactId = c.Jid };
|
||||
var contact = new InvitableDirectoryContact { Name = c.ScreenName, ContactId = c.Jid };
|
||||
|
||||
contact.ContactMethods.Add(new ContactMethod() { Number = c.Jid, Device = eContactMethodDevice.Video, CallType = eContactMethodCallType.Video, ContactMethodId = c.Jid });
|
||||
|
||||
@@ -439,6 +433,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
{
|
||||
private string _dispState;
|
||||
private string _password;
|
||||
private bool _isAirHostClientConnected;
|
||||
private bool _isSharingBlackMagic;
|
||||
private bool _isDirectPresentationConnected;
|
||||
|
||||
|
||||
public string directPresentationPairingCode { get; set; }
|
||||
/// <summary>
|
||||
@@ -460,11 +458,51 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool isAirHostClientConnected { get; set; }
|
||||
|
||||
public bool isAirHostClientConnected
|
||||
{
|
||||
get { return _isAirHostClientConnected; }
|
||||
set
|
||||
{
|
||||
if (value != _isAirHostClientConnected)
|
||||
{
|
||||
_isAirHostClientConnected = value;
|
||||
NotifyPropertyChanged("isAirHostClientConnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool isBlackMagicConnected { get; set; }
|
||||
public bool isBlackMagicDataAvailable { get; set; }
|
||||
public bool isDirectPresentationConnected { get; set; }
|
||||
public bool isSharingBlackMagic { get; set; }
|
||||
|
||||
public bool isDirectPresentationConnected
|
||||
{
|
||||
get { return _isDirectPresentationConnected; }
|
||||
set
|
||||
{
|
||||
if (value != _isDirectPresentationConnected)
|
||||
{
|
||||
_isDirectPresentationConnected = value;
|
||||
NotifyPropertyChanged("isDirectPresentationConnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool isSharingBlackMagic
|
||||
{
|
||||
get { return _isSharingBlackMagic; }
|
||||
set
|
||||
{
|
||||
if (value != _isSharingBlackMagic)
|
||||
{
|
||||
_isSharingBlackMagic = value;
|
||||
NotifyPropertyChanged("isSharingBlackMagic");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// IOS Airplay code
|
||||
/// </summary>
|
||||
@@ -720,6 +758,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
/// </summary>
|
||||
public class zEvent
|
||||
{
|
||||
public class StartLocalPresentMeeting
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
public class NeedWaitForHost
|
||||
{
|
||||
public bool Wait { get; set; }
|
||||
@@ -787,7 +829,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
private bool _paused;
|
||||
private eSharingState _state;
|
||||
|
||||
public bool IsSharing;
|
||||
public bool IsSharing { get; private set; }
|
||||
|
||||
[JsonProperty("paused")]
|
||||
public bool Paused
|
||||
@@ -927,6 +969,15 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
PhoneCallStatus_InCall,
|
||||
PhoneCallStatus_Init,
|
||||
}
|
||||
|
||||
public class MeetingNeedsPassword
|
||||
{
|
||||
[JsonProperty("needsPassword")]
|
||||
public bool NeedsPassword { get; set; }
|
||||
|
||||
[JsonProperty("wrongAndRetry")]
|
||||
public bool WrongAndRetry { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1426,6 +1477,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
public static List<Participant> GetGenericParticipantListFromParticipantsResult(
|
||||
List<ListParticipant> participants)
|
||||
{
|
||||
if (participants.Count == 0)
|
||||
{
|
||||
return new List<Participant>();
|
||||
}
|
||||
//return participants.Select(p => new Participant
|
||||
// {
|
||||
// UserId = p.UserId,
|
||||
@@ -1444,6 +1499,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
UserId = p.UserId,
|
||||
Name = p.UserName,
|
||||
IsHost = p.IsHost,
|
||||
IsMyself = p.IsMyself,
|
||||
CanMuteVideo = p.IsVideoCanMuteByHost,
|
||||
CanUnmuteVideo = p.IsVideoCanUnmuteByHost,
|
||||
AudioMuteFb = p.AudioStatusState == "AUDIO_MUTED",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,5 +25,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
||||
|
||||
//This layout will be selected when a call is connected and no content is being shared
|
||||
public string DefaultCallLayout { get; set; }
|
||||
|
||||
public int MinutesBeforeMeetingStart { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user