mirror of
https://github.com/wassname/talk.git
synced 2026-09-11 12:51:33 +08:00
[CORL-155] User Suspending and Banning (#2247)
* feat: suspending, banning, now propogation * feat: adapting to `now` * feat: support auth for suspension/banned * feat: added trace-id to requests * feat: new mutation api with hooks support * feat: added user status filtering, current field * feat: Implement filter by status, adapt to new USER_STATUS type, add lookup helper <3 * fix: typo * fix: tests * chore: rename banned status to ban status * test: feature test + lots of test helper improvements e.g. types * fix: add translation to ban user modal * fix: translation * fix: test
This commit is contained in:
@@ -14,6 +14,14 @@ interface Props {
|
||||
localeBundles: TalkContext["localeBundles"];
|
||||
}
|
||||
|
||||
interface InjectedProps {
|
||||
getMessage: GetMessage;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
localeBundles: TalkContext["localeBundles"];
|
||||
}
|
||||
|
||||
/**
|
||||
* withGetMessage provides a property `getMessage: (id: string) => string`
|
||||
* that'll provide a translated string associated with `id`.
|
||||
@@ -22,13 +30,17 @@ const withGetMessage: DefaultingInferableComponentEnhancer<
|
||||
InjectedProps
|
||||
> = hoistStatics<InjectedProps>(
|
||||
<T extends InjectedProps>(BaseComponent: React.ComponentType<T>) => {
|
||||
// TODO: (cvle) This is a workaround for a typescript bug
|
||||
// https://github.com/Microsoft/TypeScript/issues/30762
|
||||
const Workaround = BaseComponent as React.ComponentType<InjectedProps>;
|
||||
|
||||
class WithGetMessage extends React.Component<Props> {
|
||||
private getMessage = (id: string, defaultTo?: string) => {
|
||||
return getMessage(this.props.localeBundles, id, defaultTo);
|
||||
};
|
||||
public render() {
|
||||
const { localeBundles: _, ...rest } = this.props;
|
||||
return <BaseComponent {...rest} getMessage={this.getMessage} />;
|
||||
return <Workaround {...rest} getMessage={this.getMessage} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ const withInView: DefaultingInferableComponentEnhancer<
|
||||
InjectedProps
|
||||
> = hoistStatics<InjectedProps>(
|
||||
<T extends InjectedProps>(BaseComponent: React.ComponentType<T>) => {
|
||||
// TODO: (cvle) This is a workaround for a typescript bug
|
||||
// https://github.com/Microsoft/TypeScript/issues/30762
|
||||
const Workaround = BaseComponent as React.ComponentType<InjectedProps>;
|
||||
|
||||
class WithInView extends React.Component<Props, State> {
|
||||
private unobserve: (() => void) | null = null;
|
||||
|
||||
@@ -62,7 +66,7 @@ const withInView: DefaultingInferableComponentEnhancer<
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<BaseComponent
|
||||
<Workaround
|
||||
{...this.props}
|
||||
inView={this.state.inView}
|
||||
intersectionRef={this.changeRef}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { commitMutation } from "react-relay";
|
||||
import { Environment, MutationConfig, OperationBase } from "relay-runtime";
|
||||
|
||||
import { Omit } from "talk-framework/types";
|
||||
import { DeepPartial, Omit } from "talk-framework/types";
|
||||
|
||||
import extractPayload from "./extractPayload";
|
||||
|
||||
@@ -11,8 +11,8 @@ import extractPayload from "./extractPayload";
|
||||
*/
|
||||
export type MutationPromiseConfig<T extends OperationBase> = Omit<
|
||||
MutationConfig<T>,
|
||||
"onCompleted" | "onError"
|
||||
>;
|
||||
"onCompleted" | "onError" | "optimisticResponse"
|
||||
> & { optimisticResponse?: DeepPartial<T["response"]> };
|
||||
|
||||
/**
|
||||
* Normalizes response and error from `commitMutationPromise`.
|
||||
|
||||
@@ -6,24 +6,9 @@ import {
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
import { Environment } from "relay-runtime";
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { TalkContext, withContext } from "../bootstrap";
|
||||
|
||||
export type MutationInput<
|
||||
T extends { variables: { input: { clientMutationId: string } } }
|
||||
> = Omit<T["variables"]["input"], "clientMutationId">;
|
||||
|
||||
export type MutationResponse<
|
||||
T extends { response: { [P in U]: any } },
|
||||
U extends string
|
||||
> = Exclude<T["response"][U], null>;
|
||||
|
||||
export type MutationResponsePromise<
|
||||
T extends { response: { [P in U]: any } },
|
||||
U extends string
|
||||
> = Promise<MutationResponse<T, U>>;
|
||||
|
||||
/**
|
||||
* createMutationContainer creates a HOC that
|
||||
* injects a property with the name specified in `propName`
|
||||
|
||||
@@ -5,12 +5,16 @@ export { default as withLocalStateContainer } from "./withLocalStateContainer";
|
||||
export * from "./withLocalStateContainer";
|
||||
export { default as QueryRenderer } from "./QueryRenderer";
|
||||
export * from "./QueryRenderer";
|
||||
export { default as createMutationContainer } from "./createMutationContainer";
|
||||
export {
|
||||
default as createMutationContainer,
|
||||
createMutation,
|
||||
useMutation,
|
||||
withMutation,
|
||||
MutationInput,
|
||||
MutationResponse,
|
||||
MutationResponsePromise,
|
||||
} from "./createMutationContainer";
|
||||
MutationProp,
|
||||
} from "./mutation";
|
||||
export { default as createFetchContainer } from "./createFetchContainer";
|
||||
export { default as createAndRetain } from "./createAndRetain";
|
||||
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
|
||||
@@ -26,3 +30,4 @@ export { initLocalBaseState, setAccessTokenInLocalState } from "./localState";
|
||||
export { default as fetchQuery } from "./fetchQuery";
|
||||
export { default as useRefetch } from "./useRefetch";
|
||||
export { default as useLoadMore } from "./useLoadMore";
|
||||
export { default as lookup } from "./lookup";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Environment, RelayInMemoryRecordSource } from "relay-runtime";
|
||||
|
||||
/**
|
||||
* RecordSourceProxy has the same shape as the underlying Schema Type, but
|
||||
* makes all fields optional and readonly.
|
||||
*/
|
||||
type RecordSourceProxy<T> = T extends object
|
||||
? {
|
||||
readonly [P in keyof T]?: T[P] extends Array<infer U>
|
||||
? ReadonlyArray<RecordSourceProxy<U>>
|
||||
: T[P] extends ReadonlyArray<infer V>
|
||||
? ReadonlyArray<RecordSourceProxy<V>>
|
||||
: RecordSourceProxy<T[P]>
|
||||
}
|
||||
: T;
|
||||
|
||||
/**
|
||||
* createProxy returns a proxy for `recordSource`, that automatically
|
||||
* resolves references to other record sources.
|
||||
*/
|
||||
const createProxy = <T = any>(
|
||||
environment: Environment,
|
||||
recordSource: RelayInMemoryRecordSource
|
||||
) => {
|
||||
const proxy: ProxyHandler<any> = {
|
||||
get(_, prop) {
|
||||
if ((recordSource as any)[prop].__ref) {
|
||||
return lookup(environment, (recordSource as any)[prop].__ref);
|
||||
}
|
||||
return (recordSource as any)[prop];
|
||||
},
|
||||
};
|
||||
return new Proxy({}, proxy) as RecordSourceProxy<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lookup the Relay Cache with given object id. Returns a `RecordSourceProxy``
|
||||
* for easy traversing through the Relay Cache.
|
||||
*/
|
||||
export default function lookup<T = any>(environment: Environment, id: string) {
|
||||
const recordSource = environment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(id);
|
||||
if (!recordSource) {
|
||||
return null;
|
||||
}
|
||||
return createProxy<T>(environment, recordSource);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
compose,
|
||||
hoistStatics,
|
||||
InferableComponentEnhancer,
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { TalkContext, useTalkContext, withContext } from "../bootstrap";
|
||||
|
||||
export interface Mutation<N, I, R> {
|
||||
name: N;
|
||||
commit: (environment: Environment, input: I, context: TalkContext) => R;
|
||||
}
|
||||
|
||||
export type MutationInput<
|
||||
T extends { variables: { input: { clientMutationId: string } } }
|
||||
> = Omit<T["variables"]["input"], "clientMutationId">;
|
||||
|
||||
export type MutationResponse<
|
||||
T extends { response: { [P in U]: any } },
|
||||
U extends string | number | symbol
|
||||
> = Exclude<T["response"][U], null>;
|
||||
|
||||
export type MutationResponsePromise<
|
||||
T extends { response: { [P in U]: any } },
|
||||
U extends string | number | symbol
|
||||
> = Promise<MutationResponse<T, U>>;
|
||||
|
||||
export type MutationProp<
|
||||
T extends Mutation<any, any, any>
|
||||
> = T extends Mutation<any, infer I, infer R>
|
||||
? Parameters<T["commit"]>[1] extends undefined
|
||||
? () => R
|
||||
: keyof Parameters<T["commit"]>[1] extends never ? () => R : (input: I) => R
|
||||
: never;
|
||||
|
||||
type RemoveClientMutationID<T> = T extends Promise<infer U>
|
||||
? Promise<
|
||||
U extends { clientMutationId: any } ? Omit<U, "clientMutationId"> : U
|
||||
>
|
||||
: T extends { clientMutationId: any } ? Omit<T, "clientMutationId"> : T;
|
||||
|
||||
export function createMutation<N extends string, I, R>(
|
||||
name: N,
|
||||
commit: (environment: Environment, input: I, context: TalkContext) => R
|
||||
// (cvle) We remove `clientMutationId` from the response, so we don't use it inside our app.
|
||||
// It is a Relay implementation detail that is pending for removal.
|
||||
// https://github.com/facebook/relay/pull/2349
|
||||
): Mutation<N, I, RemoveClientMutationID<R>> {
|
||||
return {
|
||||
name,
|
||||
commit,
|
||||
} as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* useMutation is a React Hook that
|
||||
* returns a callback to call the mutation.
|
||||
*/
|
||||
export function useMutation<I, R>(
|
||||
mutation: Mutation<any, I, R>
|
||||
): MutationProp<typeof mutation> {
|
||||
const context = useTalkContext();
|
||||
return useCallback<MutationProp<typeof mutation>>(
|
||||
((input: I) => {
|
||||
context.eventEmitter.emit(`mutation.${mutation.name}`, input);
|
||||
return mutation.commit(context.relayEnvironment, input, context);
|
||||
}) as any,
|
||||
[context]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* withMutation creates a HOC that injects the mutation as
|
||||
* a property.
|
||||
*/
|
||||
export function withMutation<N extends string, I, R>(
|
||||
mutation: Mutation<N, I, R>
|
||||
): InferableComponentEnhancer<{ [P in N]: MutationProp<typeof mutation> }> {
|
||||
return compose(
|
||||
withContext(context => ({ context })),
|
||||
hoistStatics((BaseComponent: React.ComponentType<any>) => {
|
||||
class WithMutation extends React.Component<{
|
||||
context: TalkContext;
|
||||
}> {
|
||||
public static displayName = wrapDisplayName(
|
||||
BaseComponent,
|
||||
"withMutation"
|
||||
);
|
||||
|
||||
private commit = (input: I) => {
|
||||
this.props.context.eventEmitter.emit(
|
||||
`mutation.${mutation.name}`,
|
||||
input
|
||||
);
|
||||
return mutation.commit(
|
||||
this.props.context.relayEnvironment,
|
||||
input,
|
||||
this.props.context
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { context: _, ...rest } = this.props;
|
||||
const inject = {
|
||||
[mutation.name]: this.commit,
|
||||
};
|
||||
return <BaseComponent {...rest} {...inject} />;
|
||||
}
|
||||
}
|
||||
return WithMutation as React.ComponentType<any>;
|
||||
})
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user