mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Phase 5 (batch 25a): de-dup telemetry fields + push web-client-active to core
Prep for moving the HP-status callback into the controller:
- Remove the duplicate FLast{SMeter,FwdW,SWR,SupplyV,SupplyA,PLLLock}
fields from MainForm; the controller (which already declared them) is
now the sole owner. The UI-only S-meter ballistics (FSMeterPeak/Min/Avg)
stay in MainForm.
- TWebServer gains an OnClientActiveChanged event (fired via a
SetClientActive setter at the three client connect/disconnect points);
MainForm mirrors it into FController.FWebClientActive. The lazy mirror
in ApplyMOX/ApplyTUN is removed, so the flag is always current — needed
for the HWPTT->SetMOX path that moves into the controller next.
Behaviour-preserving (the flag held the same value at MOX time before).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
239ff54942
commit
9d2324cc6c
+65
-64
@@ -145,18 +145,11 @@ type
|
||||
FDUCPendingI: array[0..239] of Integer;
|
||||
FDUCPendingQ: array[0..239] of Integer;
|
||||
FDUCPendingCount: Integer;
|
||||
FLastSMeter: Double;
|
||||
FSMeterPeak: Double; // верхняя граница светлой зоны
|
||||
FSMeterMin: Double; // нижняя граница светлой зоны
|
||||
FSMeterAvg: Double; // сглаженное среднее (EMA)
|
||||
// RX/seq телеметрия переехала в TRadioController (пишется из OnDDCIQ).
|
||||
FLastFwdW: Double;
|
||||
FLastSWR: Double;
|
||||
// Эти значения приходят с HP Status пакетами (50..200 раз/с). UI обновляется
|
||||
// из MeterTimerTick (10 Гц), чтобы цифры/полоски не дёргались.
|
||||
FLastSupplyV: Double;
|
||||
FLastSupplyA: Double;
|
||||
FLastPLLLock: Boolean;
|
||||
// RX/seq + HP-телеметрия (FController.FLastSMeter/FwdW/SWR/SupplyV/SupplyA/PLLLock)
|
||||
// живут в TRadioController.
|
||||
|
||||
// ---- Spectrum / Waterfall view ----
|
||||
FSpecView: TSpectrumView;
|
||||
@@ -478,6 +471,7 @@ type
|
||||
procedure WebOnWfNF(On_: Boolean);
|
||||
procedure WebOnRun(On_: Boolean);
|
||||
procedure WebOnMute(On_: Boolean);
|
||||
procedure WebOnClientActiveChanged(Active: Boolean);
|
||||
procedure WebOnCtun(On_: Boolean);
|
||||
procedure WebOnNR(Mode: Integer);
|
||||
procedure WebOnNB(Mode: Integer);
|
||||
@@ -996,7 +990,7 @@ begin
|
||||
RestoreWindowBounds;
|
||||
// S-метр позиционируем после рестора размера окна
|
||||
// (будет пересчитан в первом тике SpectrumTimerTick)
|
||||
FLastSMeter := -130;
|
||||
FController.FLastSMeter := -130;
|
||||
|
||||
// Загружаем настройки веб-сервера из JSON и создаём сервер
|
||||
FController.FSettings.LoadWebSettings(WebCfg);
|
||||
@@ -1034,6 +1028,9 @@ begin
|
||||
FWebServer.OnFMStep := WebOnFMStep;
|
||||
FWebServer.OnWebMic := WebOnMic;
|
||||
FWebServer.OnXvtrBand := WebOnXvtrBand;
|
||||
// Web-слой держит FController.FWebClientActive актуальным (выбор mic-source
|
||||
// при TX, в т.ч. для HWPTT из контроллера) — больше не зеркалим лениво.
|
||||
FWebServer.OnClientActiveChanged := WebOnClientActiveChanged;
|
||||
if FWebEnabled then FWebServer.Start;
|
||||
PushXvtrToWeb;
|
||||
FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0);
|
||||
@@ -1066,11 +1063,11 @@ begin
|
||||
FController.FLastSeqErrorDDC := -1;
|
||||
FController.FLastSeqErrorDelta:= 0;
|
||||
FController.FSeqOkStreak := 0;
|
||||
FLastFwdW := 0;
|
||||
FLastSWR := 1;
|
||||
FLastSupplyV:= -1;
|
||||
FLastSupplyA:= -1;
|
||||
FLastPLLLock:= False;
|
||||
FController.FLastFwdW := 0;
|
||||
FController.FLastSWR := 1;
|
||||
FController.FLastSupplyV:= -1;
|
||||
FController.FLastSupplyA:= -1;
|
||||
FController.FLastPLLLock:= False;
|
||||
FDeviceCount := 0;
|
||||
FDeviceDialog := TDeviceDialog.Create(Self);
|
||||
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
|
||||
@@ -2973,11 +2970,11 @@ begin
|
||||
|
||||
|
||||
// Сглаженное среднее (EMA)
|
||||
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FLastSMeter * AVG_ALPHA;
|
||||
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FController.FLastSMeter * AVG_ALPHA;
|
||||
|
||||
// Цели для Peak и Min
|
||||
PeakTarget := Max(FLastSMeter, FSMeterAvg + ZONE_DB);
|
||||
MinTarget := Min(FLastSMeter, FSMeterAvg - ZONE_DB);
|
||||
PeakTarget := Max(FController.FLastSMeter, FSMeterAvg + ZONE_DB);
|
||||
MinTarget := Min(FController.FLastSMeter, FSMeterAvg - ZONE_DB);
|
||||
|
||||
// Адаптивный alpha: чем дальше от цели — тем быстрее догоняем
|
||||
PeakDiff := Abs(PeakTarget - FSMeterPeak);
|
||||
@@ -2988,11 +2985,11 @@ begin
|
||||
FSMeterPeak := FSMeterPeak + PeakAlpha * (PeakTarget - FSMeterPeak);
|
||||
FSMeterMin := FSMeterMin + MinAlpha * (MinTarget - FSMeterMin);
|
||||
|
||||
FSpecView.LastSMeter := FLastSMeter;
|
||||
FSpecView.LastSMeter := FController.FLastSMeter;
|
||||
FSpecView.SMeterPeak := FSMeterPeak;
|
||||
FSpecView.SMeterMin := FSMeterMin;
|
||||
FSpecView.LastFwdW := FLastFwdW;
|
||||
FSpecView.LastSWR := FLastSWR;
|
||||
FSpecView.LastFwdW := FController.FLastFwdW;
|
||||
FSpecView.LastSWR := FController.FLastSWR;
|
||||
FSpecView.Transmitting := FController.FTransmitting;
|
||||
|
||||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||||
@@ -3080,7 +3077,7 @@ begin
|
||||
|
||||
if FController.FWDSPReady then
|
||||
begin
|
||||
FLastSMeter := FController.FDSPEngine.GetSMeterDBm;
|
||||
FController.FLastSMeter := FController.FDSPEngine.GetSMeterDBm;
|
||||
FController.FDSPEngine.SetSpectrumWidth(FSpectrumWidth);
|
||||
Inc(FAgcLineCounter);
|
||||
if FAgcLineCounter >= 6 then
|
||||
@@ -3095,7 +3092,7 @@ begin
|
||||
// Обновляем оверлей если видим
|
||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||||
begin
|
||||
FVfoOverlay.UpdateSMeter(FLastSMeter);
|
||||
FVfoOverlay.UpdateSMeter(FController.FLastSMeter);
|
||||
FVfoOverlay.UpdateVfo(FController.FVfoA);
|
||||
PositionVfoOverlay;
|
||||
end;
|
||||
@@ -3187,19 +3184,19 @@ begin
|
||||
end
|
||||
else
|
||||
begin
|
||||
if FLastPLLLock then
|
||||
if FController.FLastPLLLock then
|
||||
WebPLLText := 'PLL OK'
|
||||
else
|
||||
WebPLLText := 'PLL?';
|
||||
|
||||
if not BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then
|
||||
WebSupplyText := 'Supply n/a'
|
||||
else if FLastSupplyV >= 0 then
|
||||
else if FController.FLastSupplyV >= 0 then
|
||||
begin
|
||||
if FLastSupplyA >= 0 then
|
||||
WebSupplyText := Format('Supply %.1fV %.1fA', [FLastSupplyV, FLastSupplyA])
|
||||
if FController.FLastSupplyA >= 0 then
|
||||
WebSupplyText := Format('Supply %.1fV %.1fA', [FController.FLastSupplyV, FController.FLastSupplyA])
|
||||
else
|
||||
WebSupplyText := Format('Supply %.1fV', [FLastSupplyV]);
|
||||
WebSupplyText := Format('Supply %.1fV', [FController.FLastSupplyV]);
|
||||
end
|
||||
else
|
||||
WebSupplyText := 'Supply --';
|
||||
@@ -3227,7 +3224,7 @@ begin
|
||||
FWebServer.PushSpectrum(
|
||||
FSpectrumBuf, 1024,
|
||||
FWaterfallBuf,
|
||||
FLastSMeter,
|
||||
FController.FLastSMeter,
|
||||
FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop,
|
||||
FController.FSpanHz, FController.FVolume,
|
||||
FController.FWfAGCEnabled, FController.FWfNFEnabled,
|
||||
@@ -3239,7 +3236,7 @@ begin
|
||||
FController.FVfoB, FController.FActiveVfo,
|
||||
FController.FTransmitting, FController.FDrivePercent,
|
||||
FController.FAtten, FController.FTuning, FController.FDisplayDuplex,
|
||||
FLastFwdW, FLastSWR, FController.FPAMaxPower,
|
||||
FController.FLastFwdW, FController.FLastSWR, FController.FPAMaxPower,
|
||||
WebStatusText, WebBoardText, WebIPText, WebSupplyText,
|
||||
WebPLLText, WebRXText, WebTXText, WebSeqText);
|
||||
end;
|
||||
@@ -3322,7 +3319,7 @@ begin
|
||||
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
|
||||
FController.FNetwork.Device.BoardType);
|
||||
end;
|
||||
// FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick —
|
||||
// FController.FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick —
|
||||
// ExciterPwr это мощность TX, не уровень принятого сигнала.
|
||||
FwdW := ADCToWatts100(FwdPwr);
|
||||
// FController.FTransmitting — наш программный флаг (MOX/TUN/HW-PTT через ApplyMOX).
|
||||
@@ -3377,35 +3374,35 @@ begin
|
||||
// FwdW=0 / SWRV=1 — это «не на передаче»: снапим без decay,
|
||||
// чтобы метры моментально падали при отпускании PTT.
|
||||
if FwdW <= 0 then
|
||||
FLastFwdW := 0
|
||||
else if FwdW > FLastFwdW then
|
||||
FLastFwdW := FwdW // attack: instant
|
||||
FController.FLastFwdW := 0
|
||||
else if FwdW > FController.FLastFwdW then
|
||||
FController.FLastFwdW := FwdW // attack: instant
|
||||
else
|
||||
FLastFwdW := DECAY_FWD * FwdW + (1.0 - DECAY_FWD) * FLastFwdW;
|
||||
FController.FLastFwdW := DECAY_FWD * FwdW + (1.0 - DECAY_FWD) * FController.FLastFwdW;
|
||||
if SWRV <= 1.0 then
|
||||
FLastSWR := 1.0
|
||||
else if SWRV > FLastSWR then
|
||||
FLastSWR := SWRV // attack: instant
|
||||
FController.FLastSWR := 1.0
|
||||
else if SWRV > FController.FLastSWR then
|
||||
FController.FLastSWR := SWRV // attack: instant
|
||||
else
|
||||
FLastSWR := DECAY_SWR * SWRV + (1.0 - DECAY_SWR) * FLastSWR;
|
||||
FController.FLastSWR := DECAY_SWR * SWRV + (1.0 - DECAY_SWR) * FController.FLastSWR;
|
||||
if SupplyV >= 0 then
|
||||
begin
|
||||
if FLastSupplyV < 0 then
|
||||
FLastSupplyV := SupplyV // первое значение — без сглаживания
|
||||
if FController.FLastSupplyV < 0 then
|
||||
FController.FLastSupplyV := SupplyV // первое значение — без сглаживания
|
||||
else
|
||||
FLastSupplyV := ALPHA_SUPPLY * SupplyV + (1.0 - ALPHA_SUPPLY) * FLastSupplyV;
|
||||
FController.FLastSupplyV := ALPHA_SUPPLY * SupplyV + (1.0 - ALPHA_SUPPLY) * FController.FLastSupplyV;
|
||||
end else
|
||||
FLastSupplyV := -1.0;
|
||||
FController.FLastSupplyV := -1.0;
|
||||
|
||||
if SupplyA >= 0 then
|
||||
begin
|
||||
if FLastSupplyA < 0 then
|
||||
FLastSupplyA := SupplyA
|
||||
if FController.FLastSupplyA < 0 then
|
||||
FController.FLastSupplyA := SupplyA
|
||||
else
|
||||
FLastSupplyA := ALPHA_SUPPLY * SupplyA + (1.0 - ALPHA_SUPPLY) * FLastSupplyA;
|
||||
FController.FLastSupplyA := ALPHA_SUPPLY * SupplyA + (1.0 - ALPHA_SUPPLY) * FController.FLastSupplyA;
|
||||
end else
|
||||
FLastSupplyA := -1.0;
|
||||
FLastPLLLock := PLLLock;
|
||||
FController.FLastSupplyA := -1.0;
|
||||
FController.FLastPLLLock := PLLLock;
|
||||
|
||||
if (FSpecView <> nil) and (FSpecView.ADCOverloadVisible <> (ADCOverload <> 0)) then
|
||||
begin
|
||||
@@ -3444,8 +3441,8 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateTXMeters;
|
||||
// Вызывается из MeterTimerTick (10 Гц). Использует FLastFwdW/FLastSWR/
|
||||
// FLastSupplyV/FLastSupplyA обновляются на rate HP Status (~150/с)
|
||||
// Вызывается из MeterTimerTick (10 Гц). Использует FController.FLastFwdW/FController.FLastSWR/
|
||||
// FController.FLastSupplyV/FController.FLastSupplyA обновляются на rate HP Status (~150/с)
|
||||
// в DoUpdateStatus.
|
||||
begin
|
||||
if not FController.FRunning then
|
||||
@@ -3455,7 +3452,7 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if FLastPLLLock then
|
||||
if FController.FLastPLLLock then
|
||||
SetStatusText(7, 'PLL OK')
|
||||
else
|
||||
SetStatusText(7, 'PLL?');
|
||||
@@ -3466,14 +3463,14 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if FLastSupplyV >= 0 then
|
||||
if FController.FLastSupplyV >= 0 then
|
||||
begin
|
||||
if FLastSupplyA >= 0 then
|
||||
if FController.FLastSupplyA >= 0 then
|
||||
SetStatusText(3,
|
||||
Format('Supply %.1fV %.1fA', [FLastSupplyV, FLastSupplyA]))
|
||||
Format('Supply %.1fV %.1fA', [FController.FLastSupplyV, FController.FLastSupplyA]))
|
||||
else
|
||||
SetStatusText(3,
|
||||
Format('Supply %.1fV', [FLastSupplyV]));
|
||||
Format('Supply %.1fV', [FController.FLastSupplyV]));
|
||||
end else
|
||||
SetStatusText(3, 'Supply --');
|
||||
end;
|
||||
@@ -4470,9 +4467,7 @@ end;
|
||||
// ---------------------------------------------------------------------------
|
||||
procedure TMainForm.ApplyTUN(Active: Boolean);
|
||||
begin
|
||||
// Web-слой пока не владеет своим флагом — зеркалим состояние клиента в
|
||||
// контроллер перед командой (SetTune зовёт SetMOX, который читает его).
|
||||
FController.FWebClientActive := Assigned(FWebServer) and FWebServer.WebClientActive;
|
||||
// FWebClientActive держит web-слой актуальным (OnClientActiveChanged).
|
||||
FController.SetTune(Active); // ядро TUN (safety/тон/drive/MOX) + рендер через rfTuning
|
||||
end;
|
||||
|
||||
@@ -5209,9 +5204,8 @@ end;
|
||||
|
||||
procedure TMainForm.ApplyMOX(Active: Boolean);
|
||||
begin
|
||||
// Web-слой пока не владеет своим флагом — зеркалим текущее состояние клиента
|
||||
// в контроллер перед командой (определяет mic-source при TX-on).
|
||||
FController.FWebClientActive := Assigned(FWebServer) and FWebServer.WebClientActive;
|
||||
// FWebClientActive держит web-слой актуальным (OnClientActiveChanged) —
|
||||
// определяет mic-source при TX-on.
|
||||
FController.SetMOX(Active); // ядро TX (safety/PTT/DSP) + рендер через rfTransmitting
|
||||
end;
|
||||
|
||||
@@ -5350,7 +5344,7 @@ begin
|
||||
PanelLeft.Width := 0;
|
||||
FVfoOverlay.Width := 260;
|
||||
FVfoOverlay.Height := 136; // OVL_H_NORM — расширяется сам при открытии AGC-пикера
|
||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FLastSMeter);
|
||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
||||
FVfoOverlay.Visible := True;
|
||||
PositionVfoOverlay;
|
||||
@@ -5435,7 +5429,7 @@ begin
|
||||
SyncSpecViewFreq;
|
||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||||
begin
|
||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FLastSMeter);
|
||||
FVfoOverlay.SetState(FController.FMode, FController.FFilterBW, FController.FVfoA, FController.FLastSMeter);
|
||||
FVfoOverlay.SetDSPState(FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF, FController.FAGCMode);
|
||||
PositionVfoOverlay; // перепозиционируем при смене LSB↔USB
|
||||
end;
|
||||
@@ -5682,6 +5676,13 @@ begin
|
||||
FController.SetMute(FWebSyncBool); // идемпотентно; OnControllerState обновит UI
|
||||
end;
|
||||
|
||||
procedure TMainForm.WebOnClientActiveChanged(Active: Boolean);
|
||||
// Вызывается из потока web-сервера при connect/disconnect клиента. Зеркалим в
|
||||
// ядро — SetMOX (включая HWPTT-путь в контроллере) выбирает по этому mic-source.
|
||||
begin
|
||||
FController.FWebClientActive := Active;
|
||||
end;
|
||||
|
||||
// ── CTUN ──────────────────────────────────────────────────────────────────
|
||||
|
||||
procedure TMainForm.WebOnCtun(On_: Boolean);
|
||||
@@ -6788,7 +6789,7 @@ function TMainForm.CATGetSNB: Boolean; begin Result := FController.FSNB; e
|
||||
function TMainForm.CATGetANF: Boolean; begin Result := FController.FANF; end;
|
||||
function TMainForm.CATGetTX: Boolean; begin Result := FController.FTransmitting; end;
|
||||
function TMainForm.CATGetRunning: Boolean; begin Result := FController.FRunning; end;
|
||||
function TMainForm.CATGetSMeter: Double; begin Result := FLastSMeter; end;
|
||||
function TMainForm.CATGetSMeter: Double; begin Result := FController.FLastSMeter; end;
|
||||
function TMainForm.CATGetBand: Integer; begin Result := FController.FCurrentBand; end;
|
||||
|
||||
// --- Setters ----------------------------------------------------------------
|
||||
|
||||
+23
-5
@@ -151,6 +151,9 @@ type
|
||||
// XVTR-band: web клиент кликнул кнопку трансвертера (Idx 0..CFG_XVTR_COUNT-1).
|
||||
// Idx=-1 — выход в HF.
|
||||
TWebCmdXvtrBand = procedure(Idx: Integer) of object;
|
||||
// Активность web-клиента изменилась (подключился/отключился последний клиент).
|
||||
// Хозяин зеркалит это в ядро (FController.FWebClientActive) для выбора mic-source.
|
||||
TWebClientActiveEvent = procedure(Active: Boolean) of object;
|
||||
// Один XVTR-слот для статуса (для отображения в web-bandSel).
|
||||
TWebXvtrInfo = record
|
||||
Idx: Integer;
|
||||
@@ -294,6 +297,11 @@ type
|
||||
FSeqText: string;
|
||||
|
||||
FWebClientActive: Boolean;
|
||||
FOnClientActiveChanged: TWebClientActiveEvent;
|
||||
|
||||
// Обновляет FWebClientActive и при изменении уведомляет хозяина. Вызывать
|
||||
// под уже взятой блокировкой поля (FStateLock/FClientLock) — сам не лочит.
|
||||
procedure SetClientActive(Value: Boolean);
|
||||
|
||||
// ── Внутренние методы ──
|
||||
function LoadOpus: Boolean;
|
||||
@@ -346,6 +354,8 @@ type
|
||||
RXText, TXText, SeqText: string);
|
||||
|
||||
property WebClientActive: Boolean read FWebClientActive;
|
||||
property OnClientActiveChanged: TWebClientActiveEvent
|
||||
read FOnClientActiveChanged write FOnClientActiveChanged;
|
||||
property FreqMhzDigits: Integer read FFreqMhzDigits write FFreqMhzDigits;
|
||||
property FMStepIdx: Integer read FFMStepIdx write FFMStepIdx;
|
||||
|
||||
@@ -980,7 +990,7 @@ begin
|
||||
Client.State := wsOpen;
|
||||
|
||||
FStateLock.Enter;
|
||||
FWebClientActive := True;
|
||||
SetClientActive(True);
|
||||
FStateLock.Leave;
|
||||
|
||||
Client.SendText(BuildStateJson);
|
||||
@@ -1114,14 +1124,21 @@ begin
|
||||
end;
|
||||
|
||||
FStateLock.Enter;
|
||||
FWebClientActive := (FClientCount > 1);
|
||||
SetClientActive(FClientCount > 1);
|
||||
FStateLock.Leave;
|
||||
|
||||
RemoveClient(Client);
|
||||
end;
|
||||
|
||||
procedure TWebServer.SetClientActive(Value: Boolean);
|
||||
begin
|
||||
if FWebClientActive = Value then Exit;
|
||||
FWebClientActive := Value;
|
||||
if Assigned(FOnClientActiveChanged) then FOnClientActiveChanged(Value);
|
||||
end;
|
||||
|
||||
procedure TWebServer.RemoveClient(Client: TWsClient);
|
||||
var i, j: Integer;
|
||||
var i, j: Integer; NewActive: Boolean;
|
||||
begin
|
||||
FClientLock.Enter;
|
||||
try
|
||||
@@ -1134,10 +1151,11 @@ begin
|
||||
Dec(FClientCount);
|
||||
Break;
|
||||
end;
|
||||
FWebClientActive := False;
|
||||
NewActive := False;
|
||||
for i := 0 to FClientCount - 1 do
|
||||
if (FClients[i] <> nil) and (FClients[i].State = wsOpen) then
|
||||
begin FWebClientActive := True; Break; end;
|
||||
begin NewActive := True; Break; end;
|
||||
SetClientActive(NewActive);
|
||||
finally
|
||||
FClientLock.Leave;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user