feat: Adding more helpers, wip
This commit is contained in:
41
Sources/ComposableSubscriber/Effect+fail.swift
Normal file
41
Sources/ComposableSubscriber/Effect+fail.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import ComposableArchitecture
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
extension Effect {
|
||||||
|
|
||||||
|
/// An effect that throws a runtime warning and optionally logs an error message.
|
||||||
|
@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
|
||||||
|
public static func fail(
|
||||||
|
_ message: String,
|
||||||
|
logger: Logger? = nil
|
||||||
|
) -> Self {
|
||||||
|
XCTFail("\(message)")
|
||||||
|
logger?.error("\(message)")
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An effect that throws a runtime warning and optionally logs an error message.
|
||||||
|
@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
|
||||||
|
public static func fail(
|
||||||
|
prefix: String = "Failed error:",
|
||||||
|
error: any Error,
|
||||||
|
logger: Logger
|
||||||
|
) -> Self {
|
||||||
|
return .fail(prefix: prefix, error: error, log: { logger.error("\($0)") })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An effect that throws a runtime warning and optionally logs an error message.
|
||||||
|
public static func fail(
|
||||||
|
prefix: String = "Failed error:",
|
||||||
|
error: any Error,
|
||||||
|
log: ((String) -> Void)? = nil
|
||||||
|
) -> Self {
|
||||||
|
let message = "\(prefix) \(error.localizedDescription)"
|
||||||
|
XCTFail("\(message)")
|
||||||
|
log?("\(message)")
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
51
Sources/ComposableSubscriber/Effect+receive.swift
Normal file
51
Sources/ComposableSubscriber/Effect+receive.swift
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import ComposableArchitecture
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
public protocol ReceiveAction<ReceiveAction> {
|
||||||
|
associatedtype ReceiveAction
|
||||||
|
static func receive(_ result: TaskResult<ReceiveAction>) -> Self
|
||||||
|
|
||||||
|
var result: TaskResult<ReceiveAction>? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReceiveAction {
|
||||||
|
|
||||||
|
public var result: TaskResult<ReceiveAction>? {
|
||||||
|
AnyCasePath(unsafe: Self.receive).extract(from: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Effect where Action: ReceiveAction {
|
||||||
|
|
||||||
|
public static func receive(
|
||||||
|
_ operation: @escaping () async throws -> Action.ReceiveAction
|
||||||
|
) -> Self {
|
||||||
|
.run { send in
|
||||||
|
await send(.receive(
|
||||||
|
TaskResult { try await operation() }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func receive<T>(
|
||||||
|
_ operation: @escaping () async throws -> T,
|
||||||
|
transform: @escaping (T) -> Action.ReceiveAction
|
||||||
|
) -> Self {
|
||||||
|
.run { send in
|
||||||
|
await send(.receive(
|
||||||
|
TaskResult { try await operation() }
|
||||||
|
.map(transform)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func receive<T>(
|
||||||
|
_ toReceiveAction: CaseKeyPath<Action.ReceiveAction, T>,
|
||||||
|
_ operation: @escaping () async throws -> T
|
||||||
|
) -> Self {
|
||||||
|
return .receive(operation) {
|
||||||
|
AnyCasePath(toReceiveAction).embed($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,18 +1,36 @@
|
|||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
import OSLog
|
||||||
|
|
||||||
extension Reducer {
|
extension Reducer {
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func onReceive<V>(
|
||||||
|
action toReceiveAction: CaseKeyPath<Action, V>,
|
||||||
|
set setAction: @escaping (inout State, V) -> Effect<Action>
|
||||||
|
) -> _ReceiveReducer<Self, V> {
|
||||||
|
.init(
|
||||||
|
parent: self,
|
||||||
|
receiveAction: { AnyCasePath(toReceiveAction).extract(from: $0) },
|
||||||
|
setAction: setAction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public func onReceive<V>(
|
public func onReceive<V>(
|
||||||
action toReceiveAction: CaseKeyPath<Action, V>,
|
action toReceiveAction: CaseKeyPath<Action, V>,
|
||||||
set setAction: @escaping (inout State, V) -> Void
|
set setAction: @escaping (inout State, V) -> Void
|
||||||
) -> _ReceiveReducer<Self, V> {
|
) -> _ReceiveReducer<Self, V> {
|
||||||
.init(
|
.init(
|
||||||
parent: self,
|
parent: self,
|
||||||
receiveAction: AnyCasePath(toReceiveAction),
|
receiveAction: { AnyCasePath(toReceiveAction).extract(from: $0) },
|
||||||
setAction: setAction
|
setAction: { state, value in
|
||||||
|
setAction(&state, value)
|
||||||
|
return .none
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public func onReceive<V>(
|
public func onReceive<V>(
|
||||||
action toReceiveAction: CaseKeyPath<Action, V>,
|
action toReceiveAction: CaseKeyPath<Action, V>,
|
||||||
set toStateKeyPath: WritableKeyPath<State, V>
|
set toStateKeyPath: WritableKeyPath<State, V>
|
||||||
@@ -20,6 +38,7 @@ extension Reducer {
|
|||||||
self.onReceive(action: toReceiveAction, set: toStateKeyPath.callAsFunction(root:value:))
|
self.onReceive(action: toReceiveAction, set: toStateKeyPath.callAsFunction(root:value:))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public func onReceive<V>(
|
public func onReceive<V>(
|
||||||
action toReceiveAction: CaseKeyPath<Action, V>,
|
action toReceiveAction: CaseKeyPath<Action, V>,
|
||||||
set toStateKeyPath: WritableKeyPath<State, V?>
|
set toStateKeyPath: WritableKeyPath<State, V?>
|
||||||
@@ -27,80 +46,98 @@ extension Reducer {
|
|||||||
self.onReceive(action: toReceiveAction, set: toStateKeyPath.callAsFunction(root:value:))
|
self.onReceive(action: toReceiveAction, set: toStateKeyPath.callAsFunction(root:value:))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public func onReceive<V>(
|
public func onReceive<V>(
|
||||||
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
||||||
onFail: OnFailAction<State>? = nil,
|
onFail: OnFailAction<State, Action>? = nil,
|
||||||
onSuccess setAction: @escaping (inout State, V) -> Void
|
onSuccess setAction: @escaping (inout State, V) -> Void
|
||||||
) -> _ReceiveReducer<Self, TaskResult<V>> {
|
) -> _ReceiveReducer<Self, TaskResult<V>> {
|
||||||
self.onReceive(action: toReceiveAction) { state, result in
|
self.onReceive(action: toReceiveAction) { state, result in
|
||||||
switch result {
|
switch result {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
if let onFail {
|
if let onFail {
|
||||||
onFail(state: &state, error: error)
|
return onFail(state: &state, error: error)
|
||||||
}
|
}
|
||||||
|
return .none
|
||||||
case let .success(value):
|
case let .success(value):
|
||||||
setAction(&state, value)
|
setAction(&state, value)
|
||||||
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public func onReceive<V>(
|
public func onReceive<V>(
|
||||||
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
||||||
set toStateKeyPath: WritableKeyPath<State, V>,
|
set toStateKeyPath: WritableKeyPath<State, V>,
|
||||||
onFail: OnFailAction<State>? = nil
|
onFail: OnFailAction<State, Action>? = nil
|
||||||
) -> _ReceiveReducer<Self, TaskResult<V>> {
|
) -> _ReceiveReducer<Self, TaskResult<V>> {
|
||||||
self.onReceive(action: toReceiveAction) { state, result in
|
self.onReceive(action: toReceiveAction) { state, result in
|
||||||
switch result {
|
switch result {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
if let onFail {
|
if let onFail {
|
||||||
onFail(state: &state, error: error)
|
return onFail(state: &state, error: error)
|
||||||
}
|
}
|
||||||
|
return .none
|
||||||
case let .success(value):
|
case let .success(value):
|
||||||
toStateKeyPath(root: &state, value: value)
|
toStateKeyPath(root: &state, value: value)
|
||||||
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public func onReceive<V>(
|
public func onReceive<V>(
|
||||||
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
||||||
set toStateKeyPath: WritableKeyPath<State, V?>,
|
set toStateKeyPath: WritableKeyPath<State, V?>,
|
||||||
onFail: OnFailAction<State>? = nil
|
onFail: OnFailAction<State, Action>? = nil
|
||||||
) -> _ReceiveReducer<Self, TaskResult<V>> {
|
) -> _ReceiveReducer<Self, TaskResult<V>> {
|
||||||
self.onReceive(action: toReceiveAction) { state, result in
|
self.onReceive(action: toReceiveAction) { state, result in
|
||||||
switch result {
|
switch result {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
if let onFail {
|
if let onFail {
|
||||||
onFail(state: &state, error: error)
|
return onFail(state: &state, error: error)
|
||||||
}
|
}
|
||||||
|
return .none
|
||||||
case let .success(value):
|
case let .success(value):
|
||||||
toStateKeyPath(root: &state, value: value)
|
toStateKeyPath(root: &state, value: value)
|
||||||
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum OnFailAction<State> {
|
public enum OnFailAction<State, Action> {
|
||||||
case xctFail(prefix: String? = nil)
|
case fail(prefix: String? = nil, log: ((String) -> Void)? = nil)
|
||||||
case handle((inout State, Error) -> Void)
|
case handle((inout State, Error) -> Void)
|
||||||
|
|
||||||
|
@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
|
||||||
|
@inlinable
|
||||||
|
static func fail(prefix: String? = nil, logger: Logger) -> Self {
|
||||||
|
.fail(prefix: prefix, log: { logger.error("\($0)") })
|
||||||
|
}
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
func callAsFunction(state: inout State, error: Error) {
|
func callAsFunction(state: inout State, error: Error) -> Effect<Action> {
|
||||||
switch self {
|
switch self {
|
||||||
case let .xctFail(prefix):
|
case let .fail(prefix, log):
|
||||||
if let prefix {
|
if let prefix {
|
||||||
XCTFail("\(prefix): \(error)")
|
return .fail(prefix: prefix, error: error, log: log)
|
||||||
} else {
|
} else {
|
||||||
XCTFail("\(error)")
|
return .fail(error: error, log: log)
|
||||||
}
|
}
|
||||||
case let .handle(handler):
|
case let .handle(handler):
|
||||||
handler(&state, error)
|
handler(&state, error)
|
||||||
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WritableKeyPath {
|
extension WritableKeyPath {
|
||||||
|
|
||||||
public func callAsFunction(root: inout Root, value: Value) {
|
@usableFromInline
|
||||||
|
func callAsFunction(root: inout Root, value: Value) {
|
||||||
root[keyPath: self] = value
|
root[keyPath: self] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,18 +149,133 @@ public struct _ReceiveReducer<Parent: Reducer, Value>: Reducer {
|
|||||||
let parent: Parent
|
let parent: Parent
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
let receiveAction: AnyCasePath<Parent.Action, Value>
|
let receiveAction: (Parent.Action) -> Value?
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
let setAction: (inout Parent.State, Value) -> Void
|
let setAction: (inout Parent.State, Value) -> Effect<Parent.Action>
|
||||||
|
|
||||||
public func reduce(into state: inout Parent.State, action: Parent.Action) -> Effect<Parent.Action> {
|
@usableFromInline
|
||||||
let baseEffects = parent.reduce(into: &state, action: action)
|
init(
|
||||||
|
parent: Parent,
|
||||||
if let value = receiveAction.extract(from: action) {
|
receiveAction: @escaping (Parent.Action) -> Value?,
|
||||||
setAction(&state, value)
|
setAction: @escaping (inout Parent.State, Value) -> Effect<Parent.Action>
|
||||||
|
) {
|
||||||
|
self.parent = parent
|
||||||
|
self.receiveAction = receiveAction
|
||||||
|
self.setAction = setAction
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseEffects
|
@inlinable
|
||||||
|
public func reduce(
|
||||||
|
into state: inout Parent.State,
|
||||||
|
action: Parent.Action
|
||||||
|
) -> Effect<Parent.Action> {
|
||||||
|
let baseEffects = parent.reduce(into: &state, action: action)
|
||||||
|
var setEffects = Effect<Action>.none
|
||||||
|
|
||||||
|
if let value = receiveAction(action) {
|
||||||
|
setEffects = setAction(&state, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .merge(baseEffects, setEffects)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct _OnRecieveReducer<Parent: Reducer, TriggerAction, Value>: Reducer {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let parent: Parent
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let triggerAction: (Parent.Action) -> TriggerAction?
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let toReceiveAction: (TaskResult<Value>) -> Parent.Action
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let resultHandler: () async throws -> Value
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(
|
||||||
|
parent: Parent,
|
||||||
|
triggerAction: @escaping (Parent.Action) -> TriggerAction?,
|
||||||
|
toReceiveAction: @escaping (TaskResult<Value>) -> Parent.Action,
|
||||||
|
resultHandler: @escaping () -> Value
|
||||||
|
) {
|
||||||
|
self.parent = parent
|
||||||
|
self.triggerAction = triggerAction
|
||||||
|
self.toReceiveAction = toReceiveAction
|
||||||
|
self.resultHandler = resultHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func reduce(
|
||||||
|
into state: inout Parent.State,
|
||||||
|
action: Parent.Action) -> Effect<Parent.Action>
|
||||||
|
{
|
||||||
|
let baseEffects = parent.reduce(into: &state, action: action)
|
||||||
|
|
||||||
|
guard triggerAction(action) != nil else {
|
||||||
|
return baseEffects
|
||||||
|
}
|
||||||
|
|
||||||
|
return .merge(
|
||||||
|
baseEffects,
|
||||||
|
.run { send in
|
||||||
|
await send(toReceiveAction(
|
||||||
|
TaskResult { try await resultHandler() }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ReceiveReducer<State, Action: ReceiveAction>: Reducer {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let toResult: (Action) -> TaskResult<Action.ReceiveAction>?
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let onFail: OnFailAction<State, Action>?
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let onSuccess: (inout State, Action.ReceiveAction) -> Effect<Action>
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
init(
|
||||||
|
internal toResult: @escaping (Action) -> TaskResult<Action.ReceiveAction>?,
|
||||||
|
onFail: OnFailAction<State, Action>?,
|
||||||
|
onSuccess: @escaping(inout State, Action.ReceiveAction) -> Effect<Action>
|
||||||
|
) {
|
||||||
|
self.toResult = toResult
|
||||||
|
self.onFail = onFail
|
||||||
|
self.onSuccess = onSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(
|
||||||
|
onFail: OnFailAction<State, Action>? = nil,
|
||||||
|
onSuccess: @escaping (inout State, Action.ReceiveAction) -> Effect<Action>
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
internal: {
|
||||||
|
AnyCasePath(unsafe: Action.receive).extract(from: $0)
|
||||||
|
},
|
||||||
|
onFail: onFail,
|
||||||
|
onSuccess: onSuccess
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||||
|
guard let result = toResult(action) else { return .none }
|
||||||
|
switch result {
|
||||||
|
case let .failure(error):
|
||||||
|
guard let onFail else { return .none }
|
||||||
|
return onFail(state: &state, error: error)
|
||||||
|
case let .success(value):
|
||||||
|
return onSuccess(&state, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ import XCTest
|
|||||||
struct NumberClient {
|
struct NumberClient {
|
||||||
var numberStreamWithoutArg: @Sendable () async -> AsyncStream<Int> = { .never }
|
var numberStreamWithoutArg: @Sendable () async -> AsyncStream<Int> = { .never }
|
||||||
var numberStreamWithArg: @Sendable (Int) async -> AsyncStream<Int> = { _ in .never }
|
var numberStreamWithArg: @Sendable (Int) async -> AsyncStream<Int> = { _ in .never }
|
||||||
|
var currentNumber: @Sendable () async throws -> Int
|
||||||
|
|
||||||
|
func currentNumber(fail: Bool = false) async throws -> Int {
|
||||||
|
if fail {
|
||||||
|
struct CurrentNumberError: Error { }
|
||||||
|
throw CurrentNumberError()
|
||||||
|
}
|
||||||
|
return try await currentNumber()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NumberClient: TestDependencyKey {
|
extension NumberClient: TestDependencyKey {
|
||||||
@@ -23,7 +33,8 @@ extension NumberClient: TestDependencyKey {
|
|||||||
continuation.yield(number)
|
continuation.yield(number)
|
||||||
continuation.finish()
|
continuation.finish()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
currentNumber: { 69420 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +102,50 @@ struct ReducerWithTransform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Reducer
|
||||||
|
struct ReducerWithReceiveAction {
|
||||||
|
typealias State = NumberState
|
||||||
|
|
||||||
|
enum Action: ReceiveAction {
|
||||||
|
|
||||||
|
case receive(TaskResult<ReceiveAction>)
|
||||||
|
case task
|
||||||
|
|
||||||
|
@CasePathable
|
||||||
|
enum ReceiveAction {
|
||||||
|
case currentNumber(Int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dependency(\.numberClient) var numberClient
|
||||||
|
|
||||||
|
public var body: some Reducer<State, Action> {
|
||||||
|
ReceiveReducer(onFail: .fail()) { state, action in
|
||||||
|
switch action {
|
||||||
|
case let .currentNumber(number):
|
||||||
|
state.currentNumber = number
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Reduce<State, Action> { state, action in
|
||||||
|
switch action {
|
||||||
|
|
||||||
|
case .receive:
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .task:
|
||||||
|
return .receive(\.currentNumber) {
|
||||||
|
try await numberClient.currentNumber()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class swift_composable_subscriberTests: XCTestCase {
|
final class TCAExtrasTests: XCTestCase {
|
||||||
|
|
||||||
func testSubscribeWithArg() async throws {
|
func testSubscribeWithArg() async throws {
|
||||||
let store = TestStore(
|
let store = TestStore(
|
||||||
@@ -128,4 +181,21 @@ final class swift_composable_subscriberTests: XCTestCase {
|
|||||||
await store.finish()
|
await store.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testReceiveAction() async throws {
|
||||||
|
let store = TestStore(
|
||||||
|
initialState: ReducerWithReceiveAction.State(number: 19),
|
||||||
|
reducer: ReducerWithReceiveAction.init
|
||||||
|
) {
|
||||||
|
$0.numberClient = .live
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = await store.send(.task)
|
||||||
|
await store.receive(\.receive) {
|
||||||
|
$0.currentNumber = 69420
|
||||||
|
}
|
||||||
|
|
||||||
|
await task.cancel()
|
||||||
|
await store.finish()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user