fix: close STEP-085 V4 renderer efficiency
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -7,6 +7,8 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -59,6 +61,12 @@ public final class WeatherRuntime {
|
||||
private boolean particleVisible = true;
|
||||
private PlaybackState playbackState = PlaybackState.PAUSE;
|
||||
private TargetFps targetFps = TargetFps.AUTO;
|
||||
// V4 owns its window in Core, so it never transitions the legacy V3 state
|
||||
// to READY. This only owns Android's next-frame wake, not a second timeline.
|
||||
// This is a Host-command bookkeeping hint for scheduling only. Core still
|
||||
// decides whether a resolved V4 window may actually advance simulation.
|
||||
@Nullable private String v4ParticleBindingFieldKey;
|
||||
private final Set<String> submittedEncodedFieldKeys = new HashSet<>();
|
||||
private boolean disposed;
|
||||
private Handler cadenceHandler;
|
||||
private final Runnable cadenceWake = new Runnable() {
|
||||
@@ -91,7 +99,9 @@ public final class WeatherRuntime {
|
||||
if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) || !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldId) || !WeatherRuntimeV4Guards.hasValidEncodedContractUtf8Length(contractJson) || !hasValidEncodedContractProfileId(contractJson) || generation <= 0 || dataVersion < 0 || !WeatherRuntimeV4Guards.hasValidEncodedQbin(qbin)) throw new IllegalArgumentException("invalid encoded field argument");
|
||||
int code = map.submitRasterArraySourceEncodedField(config.sourceId, fieldKey, fieldId, generation, dataVersion, contractJson, qbin);
|
||||
if (code == 0) throw new IllegalStateException("SOURCE_MISSING");
|
||||
return EncodedFieldSubmitResult.fromCode(code);
|
||||
EncodedFieldSubmitResult result = EncodedFieldSubmitResult.fromCode(code);
|
||||
if (result.code > 0) { submittedEncodedFieldKeys.add(fieldKey); cancelCadenceWake(); scheduleCadenceWake(); }
|
||||
return result;
|
||||
}
|
||||
private static boolean hasValidEncodedContractProfileId(String contractJson) {
|
||||
if (contractJson == null) return false;
|
||||
@@ -101,15 +111,15 @@ public final class WeatherRuntime {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public CommandResult clearEncodedField(@NonNull String fieldKey) { requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.clearRasterArraySourceEncodedField(config.sourceId, fieldKey)); }
|
||||
public CommandResult clearAllEncodedFields() { requireAttached(); return encodedCommand(map.clearAllRasterArraySourceEncodedFields(config.sourceId)); }
|
||||
public CommandResult clearEncodedField(@NonNull String fieldKey) { requireAttached(); if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey)) return CommandResult.REJECTED; CommandResult result = encodedCommand(map.clearRasterArraySourceEncodedField(config.sourceId, fieldKey)); if (result == CommandResult.ACCEPTED) { submittedEncodedFieldKeys.remove(fieldKey); cancelCadenceWake(); scheduleCadenceWake(); } return result; }
|
||||
public CommandResult clearAllEncodedFields() { requireAttached(); CommandResult result = encodedCommand(map.clearAllRasterArraySourceEncodedFields(config.sourceId)); if (result == CommandResult.ACCEPTED) { submittedEncodedFieldKeys.clear(); cancelCadenceWake(); scheduleCadenceWake(); } return result; }
|
||||
/** V4 binding intent; unit and all mode selectors reach Core unchanged. */
|
||||
public CommandResult setEncodedColorBinding(@NonNull String fieldKey, int semanticID, int groupID, int valueClass, int expectedUnitID) {
|
||||
requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedColorBinding(config.sourceId, fieldKey, semanticID, groupID, valueClass, expectedUnitID));
|
||||
}
|
||||
/** ABI reserves all selector slots: inactive slots must be exactly (0, 0). */
|
||||
public CommandResult setEncodedParticleBinding(@NonNull String fieldKey, int mode, int uSemanticID, int uGroupID, int vSemanticID, int vGroupID, int directionSemanticID, int directionGroupID, int expectedUnitID) {
|
||||
requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedParticleBinding(config.sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, expectedUnitID));
|
||||
requireAttached(); if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey)) return CommandResult.REJECTED; CommandResult result = encodedCommand(map.setRasterArraySourceEncodedParticleBinding(config.sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, expectedUnitID)); if (result == CommandResult.ACCEPTED) { v4ParticleBindingFieldKey = fieldKey; cancelCadenceWake(); scheduleCadenceWake(); } return result;
|
||||
}
|
||||
public CommandResult setEncodedForecastTime(long timestamp) { requireAttached(); return timestamp < 0 ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedForecastTime(config.sourceId, timestamp)); }
|
||||
/** State and speed intentionally remain independent V4 Core commands. */
|
||||
@@ -153,6 +163,8 @@ public final class WeatherRuntime {
|
||||
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, weatherColorVisible);
|
||||
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, particleVisible);
|
||||
state = stateAfterEnable(state, color && particle);
|
||||
cancelCadenceWake();
|
||||
scheduleCadenceWake();
|
||||
return result(color && particle);
|
||||
}
|
||||
public CommandResult setWeatherColorVisible(boolean visible) { if (disposed) return CommandResult.REJECTED; weatherColorVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); cancelCadenceWake(); scheduleCadenceWake(); return result; }
|
||||
@@ -211,7 +223,10 @@ public final class WeatherRuntime {
|
||||
private static CommandResult encodedCommand(int code) { if (code == 0) throw new IllegalStateException("SOURCE_MISSING"); return code > 0 ? CommandResult.ACCEPTED : CommandResult.REJECTED; }
|
||||
public enum FieldValueSource { SCALAR(0f), VECTOR_MAGNITUDE(1f); final float code; FieldValueSource(float code) { this.code = code; } }
|
||||
private boolean shouldWakeWeather() {
|
||||
return !disposed && targetFps != TargetFps.AUTO && state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY));
|
||||
if (disposed || state == State.DISABLED || targetFps == TargetFps.AUTO) return false;
|
||||
final boolean v4ParticleFieldReady = v4ParticleBindingFieldKey != null && submittedEncodedFieldKeys.contains(v4ParticleBindingFieldKey);
|
||||
return (v4ParticleFieldReady && particleVisible) ||
|
||||
(state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY)));
|
||||
}
|
||||
private void scheduleCadenceWake() {
|
||||
if (!shouldWakeWeather()) return;
|
||||
|
||||
@@ -101,7 +101,13 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
MLNParticleFieldRuntimeTargetFps _targetFps;
|
||||
BOOL _weatherColorVisible;
|
||||
BOOL _particleVisible;
|
||||
NSString *_v4ParticleBindingFieldKey;
|
||||
NSMutableSet<NSString *> *_submittedEncodedFieldKeys;
|
||||
NSTimer *_v4CadenceTimer;
|
||||
}
|
||||
- (BOOL)isV4ParticleCadenceReady;
|
||||
- (void)refreshV4CadenceWake;
|
||||
- (void)runV4CadenceWake:(NSTimer *)timer;
|
||||
- (instancetype)initWithMapView:(MLNMapView *)mapView
|
||||
configuration:(MLNParticleFieldRuntimeConfiguration *)configuration
|
||||
error:(NSError **)error;
|
||||
@@ -120,6 +126,7 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
_targetFps = MLNParticleFieldRuntimeTargetFpsAuto;
|
||||
_weatherColorVisible = YES;
|
||||
_particleVisible = YES;
|
||||
_submittedEncodedFieldKeys = [NSMutableSet set];
|
||||
if (![self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return true; }]) return nil;
|
||||
}
|
||||
return self;
|
||||
@@ -131,10 +138,38 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
- (MLNParticleFieldRuntimeTargetFps)targetFps { return _targetFps; }
|
||||
|
||||
- (void)dispose {
|
||||
[_v4CadenceTimer invalidate];
|
||||
_v4CadenceTimer = nil;
|
||||
_disposed = YES;
|
||||
_mapView = nil;
|
||||
}
|
||||
|
||||
- (void)refreshV4CadenceWake {
|
||||
[_v4CadenceTimer invalidate];
|
||||
_v4CadenceTimer = nil;
|
||||
if (_disposed || ![self isV4ParticleCadenceReady] || !_particleVisible ||
|
||||
_targetFps == MLNParticleFieldRuntimeTargetFpsAuto) return;
|
||||
const NSTimeInterval interval = _targetFps == MLNParticleFieldRuntimeTargetFps30 ? 0.034 : 0.017;
|
||||
_v4CadenceTimer = [NSTimer scheduledTimerWithTimeInterval:interval
|
||||
target:self
|
||||
selector:@selector(runV4CadenceWake:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
|
||||
- (void)runV4CadenceWake:(NSTimer *)timer {
|
||||
if (timer != _v4CadenceTimer) return;
|
||||
_v4CadenceTimer = nil;
|
||||
if (_disposed || ![self isV4ParticleCadenceReady] || !_particleVisible) return;
|
||||
[_mapView triggerRepaint];
|
||||
[self refreshV4CadenceWake];
|
||||
}
|
||||
|
||||
- (BOOL)isV4ParticleCadenceReady {
|
||||
return _state != MLNParticleFieldRuntimeStateDisabled && _state != MLNParticleFieldRuntimeStateError && _v4ParticleBindingFieldKey &&
|
||||
[_submittedEncodedFieldKeys containsObject:_v4ParticleBindingFieldKey];
|
||||
}
|
||||
|
||||
- (BOOL)withCurrentSource:(NSError **)error action:(bool (^)(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &))action {
|
||||
if (_disposed) {
|
||||
setError(error, MLNParticleFieldRuntimeErrorDisposed, @"Particle field runtime has been disposed.");
|
||||
@@ -213,6 +248,10 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
result = static_cast<MLNParticleEncodedFieldSubmitResult>(coreResult);
|
||||
return true;
|
||||
}];
|
||||
if (completed && result > MLNParticleEncodedFieldSubmitResultNone) {
|
||||
[_submittedEncodedFieldKeys addObject:fieldKey];
|
||||
[self refreshV4CadenceWake];
|
||||
}
|
||||
return completed ? result : MLNParticleEncodedFieldSubmitResultNone;
|
||||
}
|
||||
|
||||
@@ -220,10 +259,11 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
if (!hasValidIdentifier(fieldKey) || semanticID == 0 || expectedValueClass < MLNParticleFieldValueClassScalar || expectedValueClass > MLNParticleFieldValueClassDirectionAngle) {
|
||||
setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid encoded color binding."); return NO;
|
||||
}
|
||||
return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) {
|
||||
const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) {
|
||||
source.setColorBinding({stringFromNSString(fieldKey), {semanticID, groupID}, static_cast<mbgl::particle::FieldValueClass>(expectedValueClass), expectedUnitID});
|
||||
return true;
|
||||
}];
|
||||
return completed;
|
||||
}
|
||||
|
||||
- (BOOL)setEncodedParticleBindingWithFieldKey:(NSString *)fieldKey mode:(MLNParticleEncodedParticleBindingMode)mode uSemanticID:(uint16_t)uSemanticID uGroupID:(uint16_t)uGroupID vSemanticID:(uint16_t)vSemanticID vGroupID:(uint16_t)vGroupID directionSemanticID:(uint16_t)directionSemanticID directionGroupID:(uint16_t)directionGroupID expectedUnitID:(uint16_t)expectedUnitID error:(NSError **)error {
|
||||
@@ -231,7 +271,7 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
const bool vector = mode == MLNParticleEncodedParticleBindingModeVectorComponents && uSemanticID != 0 && vSemanticID != 0 && directionSemanticID == 0 && directionGroupID == 0;
|
||||
const bool direction = mode == MLNParticleEncodedParticleBindingModeDirectionAngle && uSemanticID == 0 && uGroupID == 0 && vSemanticID == 0 && vGroupID == 0 && directionSemanticID != 0;
|
||||
if (!vector && !direction) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Inactive particle selectors must be exactly semanticID = 0 and groupID = 0."); return NO; }
|
||||
return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) {
|
||||
const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) {
|
||||
mbgl::particle::ParticleBinding binding;
|
||||
binding.fieldKey = stringFromNSString(fieldKey);
|
||||
binding.expectedUnitID = expectedUnitID;
|
||||
@@ -240,6 +280,8 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
source.setParticleBinding(std::move(binding));
|
||||
return true;
|
||||
}];
|
||||
if (completed) { _v4ParticleBindingFieldKey = [fieldKey copy]; [self refreshV4CadenceWake]; }
|
||||
return completed;
|
||||
}
|
||||
|
||||
- (BOOL)setEncodedForecastTime:(uint64_t)timestamp error:(NSError **)error {
|
||||
@@ -257,11 +299,15 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
|
||||
- (BOOL)clearEncodedFieldWithFieldKey:(NSString *)fieldKey error:(NSError **)error {
|
||||
if (!hasValidIdentifier(fieldKey)) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid encoded field key."); return NO; }
|
||||
return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return source.clearEncodedField(stringFromNSString(fieldKey)); }];
|
||||
const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { return source.clearEncodedField(stringFromNSString(fieldKey)); }];
|
||||
if (completed) { [_submittedEncodedFieldKeys removeObject:fieldKey]; [self refreshV4CadenceWake]; }
|
||||
return completed;
|
||||
}
|
||||
|
||||
- (BOOL)clearAllEncodedFieldsWithError:(NSError **)error {
|
||||
return [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { source.clearAllEncodedFields(); return true; }];
|
||||
const BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { source.clearAllEncodedFields(); return true; }];
|
||||
if (completed) { [_submittedEncodedFieldKeys removeAllObjects]; [self refreshV4CadenceWake]; }
|
||||
return completed;
|
||||
}
|
||||
|
||||
- (MLNParticleHostFieldSubmitResult)submitWindFieldWithMetadataJSON:(NSString *)metadataJSON payload:(NSData *)payload error:(NSError **)error {
|
||||
@@ -309,16 +355,17 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
else { weather.setVisibility(_weatherColorVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); particle.setVisibility(_particleVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); accepted = true; } return true;
|
||||
}];
|
||||
if (completed && accepted) _state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled; else if (completed) _state = MLNParticleFieldRuntimeStateError;
|
||||
[self refreshV4CadenceWake];
|
||||
return commandResult(completed && accepted);
|
||||
}
|
||||
|
||||
- (MLNParticleFieldRuntimeCommandResult)setWeatherColorVisible:(BOOL)visible error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; _weatherColorVisible = visible; return [self setVisibility:visible weather:YES error:error]; }
|
||||
- (MLNParticleFieldRuntimeCommandResult)setParticleVisible:(BOOL)visible error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; _particleVisible = visible; return [self setVisibility:visible weather:NO error:error]; }
|
||||
- (MLNParticleFieldRuntimeCommandResult)setParticleVisible:(BOOL)visible error:(NSError **)error { if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected; _particleVisible = visible; MLNParticleFieldRuntimeCommandResult result = [self setVisibility:visible weather:NO error:error]; [self refreshV4CadenceWake]; return result; }
|
||||
- (MLNParticleFieldRuntimeCommandResult)setVisibility:(BOOL)visible weather:(BOOL)weatherLayer error:(NSError **)error {
|
||||
BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &particle) { (weatherLayer ? static_cast<mbgl::style::Layer &>(weather) : static_cast<mbgl::style::Layer &>(particle)).setVisibility(visible && _state != MLNParticleFieldRuntimeStateDisabled ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); return true; }]; return commandResult(completed);
|
||||
}
|
||||
|
||||
- (MLNParticleFieldRuntimeCommandResult)setTargetFps:(MLNParticleFieldRuntimeTargetFps)target error:(NSError **)error { uint8_t value = target == MLNParticleFieldRuntimeTargetFps30 ? 30 : target == MLNParticleFieldRuntimeTargetFps60 ? 60 : target == MLNParticleFieldRuntimeTargetFpsAuto ? 0 : 255; if (value == 255) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid target FPS."); return MLNParticleFieldRuntimeCommandResultRejected; } __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.setRuntimeTargetFps(value); return true; }]; if (done && ok) _targetFps = target; return commandResult(done && ok); }
|
||||
- (MLNParticleFieldRuntimeCommandResult)setTargetFps:(MLNParticleFieldRuntimeTargetFps)target error:(NSError **)error { uint8_t value = target == MLNParticleFieldRuntimeTargetFps30 ? 30 : target == MLNParticleFieldRuntimeTargetFps60 ? 60 : target == MLNParticleFieldRuntimeTargetFpsAuto ? 0 : 255; if (value == 255) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"Invalid target FPS."); return MLNParticleFieldRuntimeCommandResultRejected; } __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.setRuntimeTargetFps(value); return true; }]; if (done && ok) { _targetFps = target; [self refreshV4CadenceWake]; } return commandResult(done && ok); }
|
||||
- (MLNParticleFieldRuntimeCommandResult)setForecastTime:(uint64_t)timestamp error:(NSError **)error { if (_state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.seekRuntimeTimelineToTimestamp(timestamp); return true; }]; return commandResult(done && ok); }
|
||||
- (MLNParticleFieldRuntimeCommandResult)setPlaybackState:(MLNParticleFieldRuntimePlaybackState)playback error:(NSError **)error { if (_state == MLNParticleFieldRuntimeStateDisabled || (playback != MLNParticleFieldRuntimePlaybackStatePlay && playback != MLNParticleFieldRuntimePlaybackStatePause)) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.controlRuntimeTimeline(playback == MLNParticleFieldRuntimePlaybackStatePlay ? 0 : 1); return true; }]; if (done && ok) _playbackState = playback; return commandResult(done && ok); }
|
||||
- (MLNParticleFieldRuntimeCommandResult)setPlaybackSpeed:(float)speed error:(NSError **)error { if (!validFloat(speed) || _state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultRejected; __block bool ok = false; BOOL done = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &, mbgl::style::RasterParticleLayer &) { ok = source.controlRuntimeTimeline(2, speed); return true; }]; return commandResult(done && ok); }
|
||||
|
||||
Reference in New Issue
Block a user