mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-05-01 06:35:41 +08:00
Added updates by VATSIM data file for voice capabilities
This commit is contained in:
@@ -44,6 +44,7 @@ namespace BlackCore
|
|||||||
|
|
||||||
// AutoConnection: this should also avoid race conditions by updating the bookings
|
// AutoConnection: this should also avoid race conditions by updating the bookings
|
||||||
this->connect(this->m_vatsimBookingReader, &CVatsimBookingReader::dataRead, this, &CAirspaceMonitor::ps_receivedBookings);
|
this->connect(this->m_vatsimBookingReader, &CVatsimBookingReader::dataRead, this, &CAirspaceMonitor::ps_receivedBookings);
|
||||||
|
this->connect(this->m_vatsimDataFileReader, &CVatsimDataFileReader::dataRead, this, &CAirspaceMonitor::ps_receivedDataFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
CFlightPlan CAirspaceMonitor::loadFlightPlanFromNetwork(const CCallsign &callsign)
|
CFlightPlan CAirspaceMonitor::loadFlightPlanFromNetwork(const CCallsign &callsign)
|
||||||
@@ -237,7 +238,9 @@ namespace BlackCore
|
|||||||
vm = CIndexVariantMap({CAircraft::IndexPilot, CUser::IndexRealName}, realname);
|
vm = CIndexVariantMap({CAircraft::IndexPilot, CUser::IndexRealName}, realname);
|
||||||
this->m_aircraftsInRange.applyIf(&CAircraft::getCallsign, callsign, vm);
|
this->m_aircraftsInRange.applyIf(&CAircraft::getCallsign, callsign, vm);
|
||||||
|
|
||||||
|
// Client
|
||||||
vm = CIndexVariantMap({CClient::IndexUser, CUser::IndexRealName}, realname);
|
vm = CIndexVariantMap({CClient::IndexUser, CUser::IndexRealName}, realname);
|
||||||
|
this->addVoiceCapabilitiesFromDataFile(vm, callsign);
|
||||||
this->m_otherClients.applyIf(&CClient::getCallsign, callsign, vm);
|
this->m_otherClients.applyIf(&CClient::getCallsign, callsign, vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +252,7 @@ namespace BlackCore
|
|||||||
capabilities.addValue(CClient::FsdWithInterimPositions, (flags & INetwork::SupportsInterimPosUpdates));
|
capabilities.addValue(CClient::FsdWithInterimPositions, (flags & INetwork::SupportsInterimPosUpdates));
|
||||||
capabilities.addValue(CClient::FsdWithModelDescription, (flags & INetwork::SupportsModelDescriptions));
|
capabilities.addValue(CClient::FsdWithModelDescription, (flags & INetwork::SupportsModelDescriptions));
|
||||||
CIndexVariantMap vm(CClient::IndexCapabilities, capabilities.toQVariant());
|
CIndexVariantMap vm(CClient::IndexCapabilities, capabilities.toQVariant());
|
||||||
|
this->addVoiceCapabilitiesFromDataFile(vm, callsign);
|
||||||
this->m_otherClients.applyIf(&CClient::getCallsign, callsign, vm);
|
this->m_otherClients.applyIf(&CClient::getCallsign, callsign, vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +297,7 @@ namespace BlackCore
|
|||||||
if (this->m_atcStationsBooked.contains(&CAtcStation::getCallsign, callsignTower)) { emit this->changedAtcStationsBooked(); }
|
if (this->m_atcStationsBooked.contains(&CAtcStation::getCallsign, callsignTower)) { emit this->changedAtcStationsBooked(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAirspaceMonitor::ps_flightplanReceived(const CCallsign &callsign, const CFlightPlan &flightPlan)
|
void CAirspaceMonitor::ps_flightPlanReceived(const CCallsign &callsign, const CFlightPlan &flightPlan)
|
||||||
{
|
{
|
||||||
CFlightPlan plan(flightPlan);
|
CFlightPlan plan(flightPlan);
|
||||||
plan.setWhenLastSentOrLoaded(QDateTime::currentDateTimeUtc());
|
plan.setWhenLastSentOrLoaded(QDateTime::currentDateTimeUtc());
|
||||||
@@ -318,6 +322,17 @@ namespace BlackCore
|
|||||||
this->m_network->sendFsipirCustomPacket(recipientCallsign, icao.getAirlineDesignator(), icao.getAircraftDesignator(), icao.getAircraftCombinedType(), modelString);
|
this->m_network->sendFsipirCustomPacket(recipientCallsign, icao.getAirlineDesignator(), icao.getAircraftDesignator(), icao.getAircraftCombinedType(), modelString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CAirspaceMonitor::addVoiceCapabilitiesFromDataFile(CIndexVariantMap &vm, const CCallsign &callsign)
|
||||||
|
{
|
||||||
|
Q_ASSERT(this->m_vatsimDataFileReader);
|
||||||
|
if (callsign.isEmpty()) return;
|
||||||
|
CVoiceCapabilities vc = this->m_vatsimDataFileReader->getVoiceCapabilityForCallsign(callsign);
|
||||||
|
if (!vc.isUnknown())
|
||||||
|
{
|
||||||
|
vm.addValue(CClient::IndexVoiceCapabilities, vc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CAirspaceMonitor::ps_receivedBookings(const CAtcStationList &bookedStations)
|
void CAirspaceMonitor::ps_receivedBookings(const CAtcStationList &bookedStations)
|
||||||
{
|
{
|
||||||
Q_ASSERT(BlackCore::isCurrentThreadCreatingThread(this));
|
Q_ASSERT(BlackCore::isCurrentThreadCreatingThread(this));
|
||||||
@@ -326,7 +341,7 @@ namespace BlackCore
|
|||||||
{
|
{
|
||||||
// complete by VATSIM data file data
|
// complete by VATSIM data file data
|
||||||
this->m_vatsimDataFileReader->getAtcStations().updateFromVatsimDataFileStation(bookedStation);
|
this->m_vatsimDataFileReader->getAtcStations().updateFromVatsimDataFileStation(bookedStation);
|
||||||
// exchange booking and online data
|
// exchange booking and online data, both sides are updated
|
||||||
this->m_atcStationsOnline.mergeWithBooking(bookedStation);
|
this->m_atcStationsOnline.mergeWithBooking(bookedStation);
|
||||||
// into list
|
// into list
|
||||||
this->m_atcStationsBooked.push_back(bookedStation);
|
this->m_atcStationsBooked.push_back(bookedStation);
|
||||||
@@ -334,6 +349,20 @@ namespace BlackCore
|
|||||||
emit this->changedAtcStationsBooked(); // all booked stations reloaded
|
emit this->changedAtcStationsBooked(); // all booked stations reloaded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CAirspaceMonitor::ps_receivedDataFile()
|
||||||
|
{
|
||||||
|
Q_ASSERT(BlackCore::isCurrentThreadCreatingThread(this));
|
||||||
|
for (auto i = this->m_otherClients.begin(); i != this->m_otherClients.end(); ++i)
|
||||||
|
{
|
||||||
|
CClient client = (*i);
|
||||||
|
if (client.hasSpecifiedVoiceCapabilities()) continue;
|
||||||
|
CVoiceCapabilities vc = this->m_vatsimDataFileReader->getVoiceCapabilityForCallsign(client.getCallsign());
|
||||||
|
if (vc.isUnknown()) continue;
|
||||||
|
client.setVoiceCapabilities(vc);
|
||||||
|
(*i) = client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CAirspaceMonitor::ps_atcPositionUpdate(const CCallsign &callsign, const BlackMisc::PhysicalQuantities::CFrequency &frequency, const CCoordinateGeodetic &position, const BlackMisc::PhysicalQuantities::CLength &range)
|
void CAirspaceMonitor::ps_atcPositionUpdate(const CCallsign &callsign, const BlackMisc::PhysicalQuantities::CFrequency &frequency, const CCoordinateGeodetic &position, const BlackMisc::PhysicalQuantities::CLength &range)
|
||||||
{
|
{
|
||||||
Q_ASSERT(BlackCore::isCurrentThreadCreatingThread(this));
|
Q_ASSERT(BlackCore::isCurrentThreadCreatingThread(this));
|
||||||
@@ -422,7 +451,9 @@ namespace BlackCore
|
|||||||
this->m_atcStationsBooked.applyIf(&CAtcStation::getCallsign, callsign, vm);
|
this->m_atcStationsBooked.applyIf(&CAtcStation::getCallsign, callsign, vm);
|
||||||
emit this->changedAtcStationsBooked(); // single ATIS received
|
emit this->changedAtcStationsBooked(); // single ATIS received
|
||||||
}
|
}
|
||||||
vm = CIndexVariantMap(CClient::IndexVoiceCapabilities, CVoiceCapabilities(CVoiceCapabilities::Voice).toQVariant());
|
|
||||||
|
// receiving voice room means ATC has voice
|
||||||
|
vm = CIndexVariantMap(CClient::IndexVoiceCapabilities, CVoiceCapabilities::fromVoiceCapabilities(CVoiceCapabilities::Voice).toQVariant());
|
||||||
this->m_otherClients.applyIf(&CClient::getCallsign, callsign, vm);
|
this->m_otherClients.applyIf(&CClient::getCallsign, callsign, vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ namespace BlackCore
|
|||||||
void sendFsipirCustomPacket(const BlackMisc::Aviation::CCallsign &recipientCallsign) const;
|
void sendFsipirCustomPacket(const BlackMisc::Aviation::CCallsign &recipientCallsign) const;
|
||||||
QStringList createFsipiCustomPacketData() const;
|
QStringList createFsipiCustomPacketData() const;
|
||||||
|
|
||||||
|
//! Helper method, add voice capabilites if available
|
||||||
|
void addVoiceCapabilitiesFromDataFile(BlackMisc::CIndexVariantMap &vm, const BlackMisc::Aviation::CCallsign &callsign);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void ps_realNameReplyReceived(const BlackMisc::Aviation::CCallsign &callsign, const QString &realname);
|
void ps_realNameReplyReceived(const BlackMisc::Aviation::CCallsign &callsign, const QString &realname);
|
||||||
void ps_capabilitiesReplyReceived(const BlackMisc::Aviation::CCallsign &callsign, quint32 flags);
|
void ps_capabilitiesReplyReceived(const BlackMisc::Aviation::CCallsign &callsign, quint32 flags);
|
||||||
|
|||||||
@@ -87,6 +87,19 @@ namespace BlackCore
|
|||||||
return aircraft.getIcaoInfo();
|
return aircraft.getIcaoInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CVoiceCapabilities CVatsimDataFileReader::getVoiceCapabilityForCallsign(const CCallsign &callsign)
|
||||||
|
{
|
||||||
|
QReadLocker rl(&this->m_lock);
|
||||||
|
if (this->m_voiceCapabilities.contains(callsign))
|
||||||
|
{
|
||||||
|
return m_voiceCapabilities[callsign];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return CVoiceCapabilities::fromVoiceCapabilities(CVoiceCapabilities::Unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CVatsimDataFileReader::updateWithVatsimDataFileData(CAircraft &aircraftToBeUdpated) const
|
void CVatsimDataFileReader::updateWithVatsimDataFileData(CAircraft &aircraftToBeUdpated) const
|
||||||
{
|
{
|
||||||
this->getAircrafts().updateWithVatsimDataFileData(aircraftToBeUdpated);
|
this->getAircrafts().updateWithVatsimDataFileData(aircraftToBeUdpated);
|
||||||
@@ -158,9 +171,10 @@ namespace BlackCore
|
|||||||
if (lines.isEmpty()) return;
|
if (lines.isEmpty()) return;
|
||||||
|
|
||||||
// build on local vars for thread safety
|
// build on local vars for thread safety
|
||||||
CServerList voiceServers;
|
CServerList voiceServers;
|
||||||
CAtcStationList atcStations;
|
CAtcStationList atcStations;
|
||||||
CAircraftList aircrafts;
|
CAircraftList aircrafts;
|
||||||
|
QMap<CCallsign, CVoiceCapabilities> voiceCapabilities;
|
||||||
QDateTime updateTimestampFromFile;
|
QDateTime updateTimestampFromFile;
|
||||||
|
|
||||||
QStringList clientSectionAttributes;
|
QStringList clientSectionAttributes;
|
||||||
@@ -213,8 +227,9 @@ namespace BlackCore
|
|||||||
case SectionClients:
|
case SectionClients:
|
||||||
{
|
{
|
||||||
QMap<QString, QString> clientPartsMap = clientPartsToMap(currentLine, clientSectionAttributes);
|
QMap<QString, QString> clientPartsMap = clientPartsToMap(currentLine, clientSectionAttributes);
|
||||||
BlackMisc::Network::CUser user(clientPartsMap["cid"], clientPartsMap["realname"], CCallsign(clientPartsMap["callsign"]));
|
CCallsign callsign = CCallsign(clientPartsMap["callsign"]);
|
||||||
if (!user.hasValidCallsign()) continue;
|
if (callsign.isEmpty()) continue;
|
||||||
|
BlackMisc::Network::CUser user(clientPartsMap["cid"], clientPartsMap["realname"], callsign);
|
||||||
const QString clientType = clientPartsMap["clienttype"].toLower();
|
const QString clientType = clientPartsMap["clienttype"].toLower();
|
||||||
if (clientType.isEmpty()) break; // sometimes type is empty
|
if (clientType.isEmpty()) break; // sometimes type is empty
|
||||||
double lat = clientPartsMap["latitude"].toDouble();
|
double lat = clientPartsMap["latitude"].toDouble();
|
||||||
@@ -223,7 +238,19 @@ namespace BlackCore
|
|||||||
CFrequency frequency = CFrequency(clientPartsMap["frequency"].toDouble(), CFrequencyUnit::MHz());
|
CFrequency frequency = CFrequency(clientPartsMap["frequency"].toDouble(), CFrequencyUnit::MHz());
|
||||||
CCoordinateGeodetic position(lat, lng, -1);
|
CCoordinateGeodetic position(lat, lng, -1);
|
||||||
CAltitude altitude(alt, CAltitude::MeanSeaLevel, CLengthUnit::ft());
|
CAltitude altitude(alt, CAltitude::MeanSeaLevel, CLengthUnit::ft());
|
||||||
|
QString flightPlanRemarks = clientPartsMap["planned_remarks"];
|
||||||
|
|
||||||
|
// Voice capabilities
|
||||||
|
if (!flightPlanRemarks.isEmpty())
|
||||||
|
{
|
||||||
|
CVoiceCapabilities vc(flightPlanRemarks);
|
||||||
|
if (!vc.isUnknown())
|
||||||
|
{
|
||||||
|
voiceCapabilities.insert(callsign, vc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set as per ATC/pilot
|
||||||
if (clientType.startsWith('p'))
|
if (clientType.startsWith('p'))
|
||||||
{
|
{
|
||||||
// Pilot section
|
// Pilot section
|
||||||
@@ -300,6 +327,7 @@ namespace BlackCore
|
|||||||
this->m_aircrafts = aircrafts;
|
this->m_aircrafts = aircrafts;
|
||||||
this->m_atcStations = atcStations;
|
this->m_atcStations = atcStations;
|
||||||
this->m_voiceServers = voiceServers;
|
this->m_voiceServers = voiceServers;
|
||||||
|
this->m_voiceCapabilities = voiceCapabilities;
|
||||||
}
|
}
|
||||||
} // read success
|
} // read success
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "blackmisc/avaircraftlist.h"
|
#include "blackmisc/avaircraftlist.h"
|
||||||
#include "blackmisc/nwserverlist.h"
|
#include "blackmisc/nwserverlist.h"
|
||||||
#include "blackmisc/nwuserlist.h"
|
#include "blackmisc/nwuserlist.h"
|
||||||
|
#include "blackmisc/nwvoicecapabilities.h"
|
||||||
#include "blackmisc/avcallsignlist.h"
|
#include "blackmisc/avcallsignlist.h"
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
@@ -80,6 +81,10 @@ namespace BlackCore
|
|||||||
//! \threadsafe
|
//! \threadsafe
|
||||||
BlackMisc::Aviation::CAircraftIcao getIcaoInfo(const BlackMisc::Aviation::CCallsign &callsign);
|
BlackMisc::Aviation::CAircraftIcao getIcaoInfo(const BlackMisc::Aviation::CCallsign &callsign);
|
||||||
|
|
||||||
|
//! Voice capability for callsign
|
||||||
|
//! \threadsafe
|
||||||
|
BlackMisc::Network::CVoiceCapabilities getVoiceCapabilityForCallsign(const BlackMisc::Aviation::CCallsign &callsign);
|
||||||
|
|
||||||
//! Update aircraft with VATSIM aircraft data from data file
|
//! Update aircraft with VATSIM aircraft data from data file
|
||||||
//! \threadsafe
|
//! \threadsafe
|
||||||
void updateWithVatsimDataFileData(BlackMisc::Aviation::CAircraft &aircraftToBeUdpated) const;
|
void updateWithVatsimDataFileData(BlackMisc::Aviation::CAircraft &aircraftToBeUdpated) const;
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ namespace BlackMisc
|
|||||||
//! Get voice capabilities
|
//! Get voice capabilities
|
||||||
const CVoiceCapabilities &getVoiceCapabilities() const { return m_voiceCapabilities;}
|
const CVoiceCapabilities &getVoiceCapabilities() const { return m_voiceCapabilities;}
|
||||||
|
|
||||||
|
//! Has known voice capabilities?
|
||||||
|
bool hasSpecifiedVoiceCapabilities() const { return !m_voiceCapabilities.isUnknown();}
|
||||||
|
|
||||||
//! Set voice capabilities
|
//! Set voice capabilities
|
||||||
void setVoiceCapabilities(const CVoiceCapabilities &voiceCapabilities) { m_voiceCapabilities = voiceCapabilities;}
|
void setVoiceCapabilities(const CVoiceCapabilities &voiceCapabilities) { m_voiceCapabilities = voiceCapabilities;}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,20 @@ namespace BlackMisc
|
|||||||
this->setCapabilities(Voice);
|
this->setCapabilities(Voice);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (r.contains("/t/"))
|
||||||
|
{
|
||||||
|
this->setCapabilities(TextOnly);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.contains("/r/"))
|
||||||
|
{
|
||||||
|
this->setCapabilities(VoiceReceivingOnly);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->setCapabilities(Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -191,6 +205,39 @@ namespace BlackMisc
|
|||||||
return TupleConverter<CVoiceCapabilities>::jsonMembers();
|
return TupleConverter<CVoiceCapabilities>::jsonMembers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* From enum
|
||||||
|
*/
|
||||||
|
const CVoiceCapabilities &CVoiceCapabilities::fromVoiceCapabilities(CVoiceCapabilities::VoiceCapabilities capabilities)
|
||||||
|
{
|
||||||
|
static const CVoiceCapabilities u(CVoiceCapabilities::Unknown);
|
||||||
|
static const CVoiceCapabilities to(CVoiceCapabilities::TextOnly);
|
||||||
|
static const CVoiceCapabilities v(CVoiceCapabilities::Voice);
|
||||||
|
static const CVoiceCapabilities vro(CVoiceCapabilities::VoiceReceivingOnly);
|
||||||
|
|
||||||
|
switch (capabilities)
|
||||||
|
{
|
||||||
|
case CVoiceCapabilities::TextOnly:
|
||||||
|
return to;
|
||||||
|
case CVoiceCapabilities::Voice:
|
||||||
|
return v;
|
||||||
|
case CVoiceCapabilities::VoiceReceivingOnly:
|
||||||
|
return vro;
|
||||||
|
case CVoiceCapabilities::Unknown:
|
||||||
|
default:
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* All
|
||||||
|
*/
|
||||||
|
const QList<CVoiceCapabilities> &CVoiceCapabilities::allCapabilities()
|
||||||
|
{
|
||||||
|
static const QList<CVoiceCapabilities> all({fromVoiceCapabilities(Unknown), fromVoiceCapabilities(Voice), fromVoiceCapabilities(VoiceReceivingOnly), fromVoiceCapabilities(TextOnly)});
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Register metadata
|
* Register metadata
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ namespace BlackMisc
|
|||||||
//! Set capabilites
|
//! Set capabilites
|
||||||
void setCapabilities(VoiceCapabilities capabilites) { m_voiceCapabilities = static_cast<int>(capabilites); }
|
void setCapabilities(VoiceCapabilities capabilites) { m_voiceCapabilities = static_cast<int>(capabilites); }
|
||||||
|
|
||||||
|
//! Is capability known
|
||||||
|
bool isUnknown() const { return m_voiceCapabilities == Unknown; }
|
||||||
|
|
||||||
//! \copydoc CValueObject::toIcon()
|
//! \copydoc CValueObject::toIcon()
|
||||||
virtual CIcon toIcon() const override;
|
virtual CIcon toIcon() const override;
|
||||||
|
|
||||||
@@ -81,6 +84,18 @@ namespace BlackMisc
|
|||||||
//! Members
|
//! Members
|
||||||
static const QStringList &jsonMembers();
|
static const QStringList &jsonMembers();
|
||||||
|
|
||||||
|
//! From enum
|
||||||
|
static const CVoiceCapabilities &fromVoiceCapabilities(VoiceCapabilities capabilities);
|
||||||
|
|
||||||
|
//! From flight plan remarks
|
||||||
|
static CVoiceCapabilities fromFlightPlanRemarks(const QString &remarks)
|
||||||
|
{
|
||||||
|
return CVoiceCapabilities(remarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
//! All capabilities as list
|
||||||
|
static const QList<CVoiceCapabilities> &allCapabilities();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
//! \copydoc CValueObject::convertToQString
|
//! \copydoc CValueObject::convertToQString
|
||||||
virtual QString convertToQString(bool i18n = false) const override;
|
virtual QString convertToQString(bool i18n = false) const override;
|
||||||
@@ -105,7 +120,7 @@ namespace BlackMisc
|
|||||||
int m_voiceCapabilities = Unknown;
|
int m_voiceCapabilities = Unknown;
|
||||||
|
|
||||||
//! Capabilites from flight plans remarks such as "/V/"
|
//! Capabilites from flight plans remarks such as "/V/"
|
||||||
void fromFlightPlanRemarks(const QString &flightPlanRemarks);
|
void setFromFlightPlanRemarks(const QString &flightPlanRemarks);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
Reference in New Issue
Block a user