Convert Flipper plugin "Fresco" to TypeScript
Summary: Moves the Fresco plugin to TypeScript, including fixing any typing/nullable warnings following this. Note that parameters of event handlers previously using the SyntheticInputEvent flow type now accept a parameter of type `any`. This is done since the InputEvent api is not covered by ts bindings, as the interface is deemed experimental and not fully covered by browsers (see https://fettblog.eu/typescript-react/events/#wheres-inputevent for more info) Reviewed By: passy Differential Revision: D18201893 fbshipit-source-id: 41d1e5fc1ceaa8f8453c0f5929e754b7c32c0eb8
This commit is contained in:
committed by
Facebook Github Bot
parent
7ca230a9c6
commit
d0ab63297f
@@ -7,15 +7,15 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ImageId, ImageData} from './api.js';
|
import {ImageId, ImageData} from './api.js';
|
||||||
|
|
||||||
export type ImagesMap = {[imageId: ImageId]: ImageData};
|
export type ImagesMap = {[imageId in ImageId]: ImageData};
|
||||||
|
|
||||||
const maxInflightRequests = 10;
|
const maxInflightRequests = 10;
|
||||||
|
|
||||||
export default class ImagePool {
|
export default class ImagePool {
|
||||||
cache: ImagesMap = {};
|
cache: ImagesMap = {};
|
||||||
requested: {[imageId: ImageId]: boolean} = {};
|
requested: {[imageId in ImageId]: boolean} = {};
|
||||||
queued: Array<ImageId> = [];
|
queued: Array<ImageId> = [];
|
||||||
inFlightRequests: number = 0;
|
inFlightRequests: number = 0;
|
||||||
fetchImage: (imageId: ImageId) => void;
|
fetchImage: (imageId: ImageId) => void;
|
||||||
@@ -59,7 +59,8 @@ export default class ImagePool {
|
|||||||
delete this.requested[image.imageId];
|
delete this.requested[image.imageId];
|
||||||
|
|
||||||
if (this.queued.length > 0) {
|
if (this.queued.length > 0) {
|
||||||
this.fetchImage(this.queued.pop());
|
const popped = this.queued.pop() as string;
|
||||||
|
this.fetchImage(popped);
|
||||||
} else {
|
} else {
|
||||||
this.inFlightRequests--;
|
this.inFlightRequests--;
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ImageId, ImageData, ImagesList} from './api.js';
|
import {CacheInfo, ImageId, ImageData, ImagesList} from './api';
|
||||||
import type {ImageEventWithId} from './index.js';
|
import {ImageEventWithId} from './index';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Toolbar,
|
Toolbar,
|
||||||
@@ -24,10 +24,10 @@ import {
|
|||||||
ToggleButton,
|
ToggleButton,
|
||||||
Text,
|
Text,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import MultipleSelect from './MultipleSelect.js';
|
import MultipleSelect from './MultipleSelect';
|
||||||
import type {ImagesMap} from './ImagePool.js';
|
import {ImagesMap} from './ImagePool';
|
||||||
import {clipboard} from 'electron';
|
import {clipboard} from 'electron';
|
||||||
import {PureComponent} from 'react';
|
import React, {ChangeEvent, KeyboardEvent, PureComponent} from 'react';
|
||||||
|
|
||||||
function formatMB(bytes: number) {
|
function formatMB(bytes: number) {
|
||||||
return Math.floor(bytes / (1024 * 1024)) + 'MB';
|
return Math.floor(bytes / (1024 * 1024)) + 'MB';
|
||||||
@@ -37,11 +37,11 @@ function formatKB(bytes: number) {
|
|||||||
return Math.floor(bytes / 1024) + 'KB';
|
return Math.floor(bytes / 1024) + 'KB';
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToggleProps = {|
|
type ToggleProps = {
|
||||||
label: string,
|
label: string;
|
||||||
onClick?: (newValue: boolean) => void,
|
onClick?: (newValue: boolean) => void;
|
||||||
toggled: boolean,
|
toggled: boolean;
|
||||||
|};
|
};
|
||||||
|
|
||||||
const ToolbarToggleButton = styled(ToggleButton)(_props => ({
|
const ToolbarToggleButton = styled(ToggleButton)(_props => ({
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
@@ -68,31 +68,31 @@ function Toggle(props: ToggleProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ImagesCacheOverviewProps = {
|
type ImagesCacheOverviewProps = {
|
||||||
onColdStartChange: (checked: boolean) => void,
|
onColdStartChange: (checked: boolean) => void;
|
||||||
coldStartFilter: boolean,
|
coldStartFilter: boolean;
|
||||||
allSurfacesOption: string,
|
allSurfacesOption: string;
|
||||||
surfaceOptions: Set<string>,
|
surfaceOptions: Set<string>;
|
||||||
selectedSurfaces: Set<string>,
|
selectedSurfaces: Set<string>;
|
||||||
onChangeSurface: (key: Set<string>) => void,
|
onChangeSurface: (key: Set<string>) => void;
|
||||||
images: ImagesList,
|
images: ImagesList;
|
||||||
onClear: (type: string) => void,
|
onClear: (type: string) => void;
|
||||||
onTrimMemory: () => void,
|
onTrimMemory: () => void;
|
||||||
onRefresh: () => void,
|
onRefresh: () => void;
|
||||||
onEnableDebugOverlay: (enabled: boolean) => void,
|
onEnableDebugOverlay: (enabled: boolean) => void;
|
||||||
isDebugOverlayEnabled: boolean,
|
isDebugOverlayEnabled: boolean;
|
||||||
onEnableAutoRefresh: (enabled: boolean) => void,
|
onEnableAutoRefresh: (enabled: boolean) => void;
|
||||||
isAutoRefreshEnabled: boolean,
|
isAutoRefreshEnabled: boolean;
|
||||||
onImageSelected: (selectedImage: ImageId) => void,
|
onImageSelected: (selectedImage: ImageId) => void;
|
||||||
imagesMap: ImagesMap,
|
imagesMap: ImagesMap;
|
||||||
events: Array<ImageEventWithId>,
|
events: Array<ImageEventWithId>;
|
||||||
onTrackLeaks: (enabled: boolean) => void,
|
onTrackLeaks: (enabled: boolean) => void;
|
||||||
isLeakTrackingEnabled: boolean,
|
isLeakTrackingEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ImagesCacheOverviewState = {|
|
type ImagesCacheOverviewState = {
|
||||||
selectedImage: ?ImageId,
|
selectedImage: ImageId | null;
|
||||||
size: number,
|
size: number;
|
||||||
|};
|
};
|
||||||
|
|
||||||
const StyledSelect = styled(Select)(props => ({
|
const StyledSelect = styled(Select)(props => ({
|
||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
@@ -103,8 +103,13 @@ const StyledSelect = styled(Select)(props => ({
|
|||||||
|
|
||||||
export default class ImagesCacheOverview extends PureComponent<
|
export default class ImagesCacheOverview extends PureComponent<
|
||||||
ImagesCacheOverviewProps,
|
ImagesCacheOverviewProps,
|
||||||
ImagesCacheOverviewState,
|
ImagesCacheOverviewState
|
||||||
> {
|
> {
|
||||||
|
state = {
|
||||||
|
selectedImage: null,
|
||||||
|
size: 150,
|
||||||
|
};
|
||||||
|
|
||||||
static Container = styled(FlexColumn)({
|
static Container = styled(FlexColumn)({
|
||||||
backgroundColor: colors.white,
|
backgroundColor: colors.white,
|
||||||
});
|
});
|
||||||
@@ -121,17 +126,12 @@ export default class ImagesCacheOverview extends PureComponent<
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
state = {
|
|
||||||
selectedImage: undefined,
|
|
||||||
size: 150,
|
|
||||||
};
|
|
||||||
|
|
||||||
onImageSelected = (selectedImage: ImageId) => {
|
onImageSelected = (selectedImage: ImageId) => {
|
||||||
this.setState({selectedImage});
|
this.setState({selectedImage});
|
||||||
this.props.onImageSelected(selectedImage);
|
this.props.onImageSelected(selectedImage);
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyDown = (e: SyntheticKeyboardEvent<*>) => {
|
onKeyDown = (e: KeyboardEvent) => {
|
||||||
const selectedImage = this.state.selectedImage;
|
const selectedImage = this.state.selectedImage;
|
||||||
const imagesMap = this.props.imagesMap;
|
const imagesMap = this.props.imagesMap;
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ export default class ImagesCacheOverview extends PureComponent<
|
|||||||
this.props.onEnableAutoRefresh(!this.props.isAutoRefreshEnabled);
|
this.props.onEnableAutoRefresh(!this.props.isAutoRefreshEnabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeSize = (e: SyntheticInputEvent<HTMLInputElement>) =>
|
onChangeSize = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
this.setState({size: parseInt(e.target.value, 10)});
|
this.setState({size: parseInt(e.target.value, 10)});
|
||||||
|
|
||||||
onSurfaceOptionsChange = (selectedItem: string, checked: boolean) => {
|
onSurfaceOptionsChange = (selectedItem: string, checked: boolean) => {
|
||||||
@@ -199,7 +199,7 @@ export default class ImagesCacheOverview extends PureComponent<
|
|||||||
<ImagesCacheOverview.Container
|
<ImagesCacheOverview.Container
|
||||||
grow={true}
|
grow={true}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
tabIndex="0">
|
tabIndex={0}>
|
||||||
<Toolbar position="top">
|
<Toolbar position="top">
|
||||||
<Button icon="trash" onClick={this.props.onTrimMemory}>
|
<Button icon="trash" onClick={this.props.onTrimMemory}>
|
||||||
Trim Memory
|
Trim Memory
|
||||||
@@ -242,18 +242,19 @@ export default class ImagesCacheOverview extends PureComponent<
|
|||||||
</Toolbar>
|
</Toolbar>
|
||||||
{!hasImages ? (
|
{!hasImages ? (
|
||||||
<ImagesCacheOverview.Empty>
|
<ImagesCacheOverview.Empty>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator size={50} />
|
||||||
</ImagesCacheOverview.Empty>
|
</ImagesCacheOverview.Empty>
|
||||||
) : (
|
) : (
|
||||||
<ImagesCacheOverview.Content>
|
<ImagesCacheOverview.Content>
|
||||||
{this.props.images.map((data, index) => {
|
{this.props.images.map((data: CacheInfo, index: number) => {
|
||||||
const maxSize = data.maxSizeBytes;
|
const maxSize = data.maxSizeBytes;
|
||||||
const subtitle = maxSize
|
const subtitle = maxSize
|
||||||
? formatMB(data.sizeBytes) + ' / ' + formatMB(maxSize)
|
? formatMB(data.sizeBytes) + ' / ' + formatMB(maxSize)
|
||||||
: formatMB(data.sizeBytes);
|
: formatMB(data.sizeBytes);
|
||||||
const onClear = data.clearKey
|
const onClear =
|
||||||
? () => this.props.onClear(data.clearKey)
|
data.clearKey !== undefined
|
||||||
: null;
|
? () => this.props.onClear(data.clearKey as string)
|
||||||
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<ImageGrid
|
<ImageGrid
|
||||||
key={index}
|
key={index}
|
||||||
@@ -277,15 +278,15 @@ export default class ImagesCacheOverview extends PureComponent<
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ImageGrid extends PureComponent<{
|
class ImageGrid extends PureComponent<{
|
||||||
title: string,
|
title: string;
|
||||||
subtitle: string,
|
subtitle: string;
|
||||||
images: Array<ImageId>,
|
images: Array<ImageId>;
|
||||||
selectedImage: ?ImageId,
|
selectedImage: ImageId | null;
|
||||||
onImageSelected: (image: ImageId) => void,
|
onImageSelected: (image: ImageId) => void;
|
||||||
onClear: ?() => void,
|
onClear: (() => void) | undefined;
|
||||||
imagesMap: ImagesMap,
|
imagesMap: ImagesMap;
|
||||||
size: number,
|
size: number;
|
||||||
events: Array<ImageEventWithId>,
|
events: Array<ImageEventWithId>;
|
||||||
}> {
|
}> {
|
||||||
static Content = styled('div')({
|
static Content = styled('div')({
|
||||||
paddingLeft: 15,
|
paddingLeft: 15,
|
||||||
@@ -325,9 +326,9 @@ class ImageGrid extends PureComponent<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ImageGridHeader extends PureComponent<{
|
class ImageGridHeader extends PureComponent<{
|
||||||
title: string,
|
title: string;
|
||||||
subtitle: string,
|
subtitle: string;
|
||||||
onClear: ?() => void,
|
onClear: (() => void) | undefined;
|
||||||
}> {
|
}> {
|
||||||
static Container = styled(FlexRow)({
|
static Container = styled(FlexRow)({
|
||||||
color: colors.dark70,
|
color: colors.dark70,
|
||||||
@@ -384,20 +385,20 @@ class ImageGridHeader extends PureComponent<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ImageItem extends PureComponent<{
|
class ImageItem extends PureComponent<{
|
||||||
imageId: ImageId,
|
imageId: ImageId;
|
||||||
image: ?ImageData,
|
image: ImageData;
|
||||||
selected: boolean,
|
selected: boolean;
|
||||||
onSelected: (image: ImageId) => void,
|
onSelected: (image: ImageId) => void;
|
||||||
size: number,
|
size: number;
|
||||||
numberOfRequests: number,
|
numberOfRequests: number;
|
||||||
}> {
|
}> {
|
||||||
static Container = styled(FlexBox)(({size}) => ({
|
static Container = styled(FlexBox)((props: {size: number}) => ({
|
||||||
float: 'left',
|
float: 'left',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
height: size,
|
height: props.size,
|
||||||
width: size,
|
width: props.size,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
marginRight: 15,
|
marginRight: 15,
|
||||||
marginBottom: 15,
|
marginBottom: 15,
|
||||||
@@ -415,7 +416,7 @@ class ImageItem extends PureComponent<{
|
|||||||
padding: '0 0',
|
padding: '0 0',
|
||||||
});
|
});
|
||||||
|
|
||||||
static SelectedHighlight = styled('div')(props => ({
|
static SelectedHighlight = styled('div')((props: {selected: boolean}) => ({
|
||||||
borderColor: colors.highlight,
|
borderColor: colors.highlight,
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
borderWidth: props.selected ? 3 : 0,
|
borderWidth: props.selected ? 3 : 0,
|
||||||
@@ -428,23 +429,25 @@ class ImageItem extends PureComponent<{
|
|||||||
top: 0,
|
top: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
static HoverOverlay = styled(FlexColumn)(props => ({
|
static HoverOverlay = styled(FlexColumn)(
|
||||||
alignItems: 'center',
|
(props: {selected: boolean; size: number}) => ({
|
||||||
backgroundColor: colors.whiteAlpha80,
|
alignItems: 'center',
|
||||||
bottom: props.selected ? 4 : 0,
|
backgroundColor: colors.whiteAlpha80,
|
||||||
fontSize: props.size > 100 ? 16 : 11,
|
bottom: props.selected ? 4 : 0,
|
||||||
justifyContent: 'center',
|
fontSize: props.size > 100 ? 16 : 11,
|
||||||
left: props.selected ? 4 : 0,
|
justifyContent: 'center',
|
||||||
opacity: 0,
|
left: props.selected ? 4 : 0,
|
||||||
position: 'absolute',
|
opacity: 0,
|
||||||
right: props.selected ? 4 : 0,
|
position: 'absolute',
|
||||||
top: props.selected ? 4 : 0,
|
right: props.selected ? 4 : 0,
|
||||||
overflow: 'hidden',
|
top: props.selected ? 4 : 0,
|
||||||
transition: '.1s opacity',
|
overflow: 'hidden',
|
||||||
'&:hover': {
|
transition: '.1s opacity',
|
||||||
opacity: 1,
|
'&:hover': {
|
||||||
},
|
opacity: 1,
|
||||||
}));
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
static MemoryLabel = styled('span')({
|
static MemoryLabel = styled('span')({
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ImageData} from './api.js';
|
import {ImageData} from './api';
|
||||||
import type {ImageEventWithId} from './index.js';
|
import {ImageEventWithId} from './index';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
DataDescription,
|
DataDescription,
|
||||||
@@ -20,10 +20,11 @@ import {
|
|||||||
colors,
|
colors,
|
||||||
styled,
|
styled,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
type ImagesSidebarProps = {
|
type ImagesSidebarProps = {
|
||||||
image: ?ImageData,
|
image: ImageData;
|
||||||
events: Array<ImageEventWithId>,
|
events: Array<ImageEventWithId>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ImagesSidebarState = {};
|
type ImagesSidebarState = {};
|
||||||
@@ -38,7 +39,7 @@ const WordBreakFlexColumn = styled(FlexColumn)({
|
|||||||
|
|
||||||
export default class ImagesSidebar extends Component<
|
export default class ImagesSidebar extends Component<
|
||||||
ImagesSidebarProps,
|
ImagesSidebarProps,
|
||||||
ImagesSidebarState,
|
ImagesSidebarState
|
||||||
> {
|
> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
@@ -81,7 +82,7 @@ export default class ImagesSidebar extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EventDetails extends Component<{
|
class EventDetails extends Component<{
|
||||||
event: ImageEventWithId,
|
event: ImageEventWithId;
|
||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const {event} = this.props;
|
const {event} = this.props;
|
||||||
@@ -158,9 +159,9 @@ class EventDetails extends Component<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RequestHeader extends Component<{
|
class RequestHeader extends Component<{
|
||||||
event: ImageEventWithId,
|
event: ImageEventWithId;
|
||||||
}> {
|
}> {
|
||||||
dateString = timestamp => {
|
dateString = (timestamp: number) => {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
return `${date.toTimeString().split(' ')[0]}.${(
|
return `${date.toTimeString().split(' ')[0]}.${(
|
||||||
'000' + date.getMilliseconds()
|
'000' + date.getMilliseconds()
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Block, Button, colors, FlexColumn, styled, Glyph} from 'flipper';
|
import {Block, Button, colors, FlexColumn, styled, Glyph} from 'flipper';
|
||||||
import React, {Component} from 'react';
|
import React, {ChangeEvent, Component} from 'react';
|
||||||
|
|
||||||
const Container = styled(Block)({
|
const Container = styled(Block)({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -54,26 +54,26 @@ const StyledGlyph = styled(Glyph)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
visibleList: boolean,
|
visibleList: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class MultipleSelect extends Component<
|
export default class MultipleSelect extends Component<
|
||||||
{
|
{
|
||||||
selected: Set<string>,
|
selected: Set<string>;
|
||||||
|
|
||||||
options: Set<string>,
|
options: Set<string>;
|
||||||
|
|
||||||
onChange: (selectedItem: string, checked: boolean) => void,
|
onChange: (selectedItem: string, checked: boolean) => void;
|
||||||
|
|
||||||
label: string,
|
label: string;
|
||||||
},
|
},
|
||||||
State,
|
State
|
||||||
> {
|
> {
|
||||||
state = {
|
state = {
|
||||||
visibleList: false,
|
visibleList: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOnChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const {
|
const {
|
||||||
target: {value, checked},
|
target: {value, checked},
|
||||||
} = event;
|
} = event;
|
||||||
@@ -7,23 +7,26 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import FrescoPlugin from '../index.js';
|
import FrescoPlugin from '../index';
|
||||||
import type {PersistedState, ImageEventWithId} from '../index.js';
|
import {PersistedState, ImageEventWithId} from '../index';
|
||||||
import type {AndroidCloseableReferenceLeakEvent} from '../api.js';
|
import {AndroidCloseableReferenceLeakEvent} from '../api';
|
||||||
import type {MetricType} from 'flipper';
|
import {MetricType} from 'flipper';
|
||||||
import type {Notification} from '../../../plugin.tsx';
|
import {Notification} from '../../../plugin';
|
||||||
|
import {ImagesMap} from '../ImagePool';
|
||||||
|
|
||||||
|
type ScanDisplayTime = {[scan_number: number]: number};
|
||||||
|
|
||||||
function mockPersistedState(
|
function mockPersistedState(
|
||||||
imageSizes: Array<{
|
imageSizes: Array<{
|
||||||
width: number,
|
width: number;
|
||||||
height: number,
|
height: number;
|
||||||
}> = [],
|
}> = [],
|
||||||
viewport: {
|
viewport: {
|
||||||
width: number,
|
width: number;
|
||||||
height: number,
|
height: number;
|
||||||
} = {width: 150, height: 150},
|
} = {width: 150, height: 150},
|
||||||
): PersistedState {
|
): PersistedState {
|
||||||
const scanDisplayTime = {};
|
const scanDisplayTime: ScanDisplayTime = {};
|
||||||
scanDisplayTime[1] = 3;
|
scanDisplayTime[1] = 3;
|
||||||
const events: Array<ImageEventWithId> = [
|
const events: Array<ImageEventWithId> = [
|
||||||
{
|
{
|
||||||
@@ -38,16 +41,19 @@ function mockPersistedState(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const imagesMap = imageSizes.reduce((acc, val, index) => {
|
const imagesMap = imageSizes.reduce(
|
||||||
acc[index] = {
|
(acc, val, index) => {
|
||||||
imageId: String(index),
|
acc[index] = {
|
||||||
width: val.width,
|
imageId: String(index),
|
||||||
height: val.height,
|
width: val.width,
|
||||||
sizeBytes: 10,
|
height: val.height,
|
||||||
data: undefined,
|
sizeBytes: 10,
|
||||||
};
|
data: 'undefined',
|
||||||
return acc;
|
};
|
||||||
}, {});
|
return acc;
|
||||||
|
},
|
||||||
|
{} as ImagesMap,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
surfaceList: new Set(),
|
surfaceList: new Set(),
|
||||||
@@ -184,9 +190,8 @@ test('the metric reducer with the no viewPort data in events', () => {
|
|||||||
const metrics = metricsReducer(persistedState);
|
const metrics = metricsReducer(persistedState);
|
||||||
return expect(metrics).resolves.toMatchObject({WASTED_BYTES: 0});
|
return expect(metrics).resolves.toMatchObject({WASTED_BYTES: 0});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the metric reducer with the multiple events', () => {
|
test('the metric reducer with the multiple events', () => {
|
||||||
const scanDisplayTime = {};
|
const scanDisplayTime: ScanDisplayTime = {};
|
||||||
scanDisplayTime[1] = 3;
|
scanDisplayTime[1] = 3;
|
||||||
const events: Array<ImageEventWithId> = [
|
const events: Array<ImageEventWithId> = [
|
||||||
{
|
{
|
||||||
@@ -228,22 +233,27 @@ test('the metric reducer with the multiple events', () => {
|
|||||||
height: 300,
|
height: 300,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const imagesMap = imageSizes.reduce((acc, val, index) => {
|
const imagesMap = imageSizes.reduce(
|
||||||
acc[index] = {
|
(acc, val, index) => {
|
||||||
imageId: String(index),
|
acc[index] = {
|
||||||
width: val.width,
|
imageId: String(index),
|
||||||
height: val.height,
|
width: val.width,
|
||||||
sizeBytes: 10,
|
height: val.height,
|
||||||
data: undefined,
|
sizeBytes: 10,
|
||||||
};
|
data: 'undefined',
|
||||||
return acc;
|
};
|
||||||
}, {});
|
return acc;
|
||||||
|
},
|
||||||
|
{} as ImagesMap,
|
||||||
|
);
|
||||||
const persistedState = {
|
const persistedState = {
|
||||||
surfaceList: new Set(),
|
surfaceList: new Set<string>(),
|
||||||
images: [],
|
images: [],
|
||||||
nextEventId: 0,
|
nextEventId: 0,
|
||||||
events,
|
events,
|
||||||
imagesMap,
|
imagesMap,
|
||||||
|
closeableReferenceLeaks: [],
|
||||||
|
isLeakTrackingEnabled: true,
|
||||||
};
|
};
|
||||||
const metricsReducer = FrescoPlugin.metricsReducer;
|
const metricsReducer = FrescoPlugin.metricsReducer;
|
||||||
expect(metricsReducer).toBeDefined();
|
expect(metricsReducer).toBeDefined();
|
||||||
@@ -255,7 +265,7 @@ test('the metric reducer with the multiple events', () => {
|
|||||||
test('closeable reference metrics on empty state', () => {
|
test('closeable reference metrics on empty state', () => {
|
||||||
const metricsReducer: (
|
const metricsReducer: (
|
||||||
persistedState: PersistedState,
|
persistedState: PersistedState,
|
||||||
) => Promise<MetricType> = (FrescoPlugin.metricsReducer: any);
|
) => Promise<MetricType> = FrescoPlugin.metricsReducer;
|
||||||
const persistedState = mockPersistedState();
|
const persistedState = mockPersistedState();
|
||||||
const metrics = metricsReducer(persistedState);
|
const metrics = metricsReducer(persistedState);
|
||||||
return expect(metrics).resolves.toMatchObject({CLOSEABLE_REFERENCE_LEAKS: 0});
|
return expect(metrics).resolves.toMatchObject({CLOSEABLE_REFERENCE_LEAKS: 0});
|
||||||
@@ -264,7 +274,7 @@ test('closeable reference metrics on empty state', () => {
|
|||||||
test('closeable reference metrics on input', () => {
|
test('closeable reference metrics on input', () => {
|
||||||
const metricsReducer: (
|
const metricsReducer: (
|
||||||
persistedState: PersistedState,
|
persistedState: PersistedState,
|
||||||
) => Promise<MetricType> = (FrescoPlugin.metricsReducer: any);
|
) => Promise<MetricType> = FrescoPlugin.metricsReducer;
|
||||||
const closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent> = [
|
const closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent> = [
|
||||||
{
|
{
|
||||||
identityHashCode: 'deadbeef',
|
identityHashCode: 'deadbeef',
|
||||||
@@ -288,7 +298,7 @@ test('closeable reference metrics on input', () => {
|
|||||||
test('notifications for leaks', () => {
|
test('notifications for leaks', () => {
|
||||||
const notificationReducer: (
|
const notificationReducer: (
|
||||||
persistedState: PersistedState,
|
persistedState: PersistedState,
|
||||||
) => Array<Notification> = (FrescoPlugin.getActiveNotifications: any);
|
) => Array<Notification> = FrescoPlugin.getActiveNotifications;
|
||||||
const closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent> = [
|
const closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent> = [
|
||||||
{
|
{
|
||||||
identityHashCode: 'deadbeef',
|
identityHashCode: 'deadbeef',
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @format
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type ImageId = string;
|
|
||||||
|
|
||||||
// Listing images
|
|
||||||
|
|
||||||
export type CacheInfo = {|
|
|
||||||
cacheType: string,
|
|
||||||
clearKey?: null, // set this if this cache level supports clear(<key>)
|
|
||||||
sizeBytes: number,
|
|
||||||
maxSizeBytes?: number,
|
|
||||||
imageIds: Array<ImageId>,
|
|
||||||
|};
|
|
||||||
|
|
||||||
export type ImagesList = Array<CacheInfo>;
|
|
||||||
|
|
||||||
// The iOS Flipper api does not support a top-level array, so we wrap it in an object
|
|
||||||
export type ImagesListResponse = {|
|
|
||||||
levels: ImagesList,
|
|
||||||
|};
|
|
||||||
|
|
||||||
// listImages() -> ImagesListResponse
|
|
||||||
|
|
||||||
// Getting details on a specific image
|
|
||||||
|
|
||||||
export type ImageBytes = string;
|
|
||||||
|
|
||||||
export type ImageData = {|
|
|
||||||
imageId: ImageId,
|
|
||||||
uri?: string,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
sizeBytes: number,
|
|
||||||
data: ImageBytes,
|
|
||||||
surface?: string,
|
|
||||||
|};
|
|
||||||
|
|
||||||
// getImage({imageId: string}) -> ImageData
|
|
||||||
|
|
||||||
// Subscribing to image events (requests and prefetches)
|
|
||||||
|
|
||||||
export type Timestamp = number;
|
|
||||||
|
|
||||||
export type ViewportData = {|
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
scanDisplayTime: {[scan_number: number]: Timestamp},
|
|
||||||
|};
|
|
||||||
|
|
||||||
export type ImageEvent = {
|
|
||||||
imageIds: Array<ImageId>,
|
|
||||||
attribution: Array<string>,
|
|
||||||
startTime: Timestamp,
|
|
||||||
endTime: Timestamp,
|
|
||||||
source: string,
|
|
||||||
coldStart: boolean,
|
|
||||||
viewport?: ViewportData, // not set for prefetches
|
|
||||||
};
|
|
||||||
|
|
||||||
// Misc
|
|
||||||
|
|
||||||
export type FrescoDebugOverlayEvent = {|
|
|
||||||
enabled: boolean,
|
|
||||||
|};
|
|
||||||
|
|
||||||
export type AndroidCloseableReferenceLeakEvent = {|
|
|
||||||
identityHashCode: string,
|
|
||||||
className: string,
|
|
||||||
stacktrace: ?string,
|
|
||||||
|};
|
|
||||||
77
src/plugins/fresco/api.tsx
Normal file
77
src/plugins/fresco/api.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ImageId = string;
|
||||||
|
|
||||||
|
// Listing images
|
||||||
|
|
||||||
|
export type CacheInfo = {
|
||||||
|
cacheType: string;
|
||||||
|
clearKey?: string; // set this if this cache level supports clear(<key>)
|
||||||
|
sizeBytes: number;
|
||||||
|
maxSizeBytes?: number;
|
||||||
|
imageIds: Array<ImageId>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImagesList = Array<CacheInfo>;
|
||||||
|
|
||||||
|
// The iOS Flipper api does not support a top-level array, so we wrap it in an object
|
||||||
|
export type ImagesListResponse = {
|
||||||
|
levels: ImagesList;
|
||||||
|
};
|
||||||
|
|
||||||
|
// listImages() -> ImagesListResponse
|
||||||
|
|
||||||
|
// Getting details on a specific image
|
||||||
|
|
||||||
|
export type ImageBytes = string;
|
||||||
|
|
||||||
|
export type ImageData = {
|
||||||
|
imageId: ImageId;
|
||||||
|
uri?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
sizeBytes: number;
|
||||||
|
data: ImageBytes;
|
||||||
|
surface?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// getImage({imageId: string}) -> ImageData
|
||||||
|
|
||||||
|
// Subscribing to image events (requests and prefetches)
|
||||||
|
|
||||||
|
export type Timestamp = number;
|
||||||
|
|
||||||
|
export type ViewportData = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
scanDisplayTime: {[scan_number: number]: Timestamp};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageEvent = {
|
||||||
|
imageIds: Array<ImageId>;
|
||||||
|
attribution: Array<string>;
|
||||||
|
startTime: Timestamp;
|
||||||
|
endTime: Timestamp;
|
||||||
|
source: string;
|
||||||
|
coldStart: boolean;
|
||||||
|
viewport?: ViewportData; // not set for prefetches
|
||||||
|
};
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
|
||||||
|
export type FrescoDebugOverlayEvent = {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AndroidCloseableReferenceLeakEvent = {
|
||||||
|
identityHashCode: string;
|
||||||
|
className: string;
|
||||||
|
stacktrace: string | null;
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
ImageId,
|
ImageId,
|
||||||
ImageData,
|
ImageData,
|
||||||
ImagesList,
|
ImagesList,
|
||||||
@@ -16,12 +16,12 @@ import type {
|
|||||||
FrescoDebugOverlayEvent,
|
FrescoDebugOverlayEvent,
|
||||||
AndroidCloseableReferenceLeakEvent,
|
AndroidCloseableReferenceLeakEvent,
|
||||||
CacheInfo,
|
CacheInfo,
|
||||||
} from './api.js';
|
} from './api';
|
||||||
import {Fragment} from 'react';
|
import {Fragment} from 'react';
|
||||||
import type {ImagesMap} from './ImagePool.js';
|
import {ImagesMap} from './ImagePool';
|
||||||
import type {MetricType, MiddlewareAPI} from 'flipper';
|
import {MetricType, MiddlewareAPI} from 'flipper';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImagesCacheOverview from './ImagesCacheOverview.js';
|
import ImagesCacheOverview from './ImagesCacheOverview';
|
||||||
import {
|
import {
|
||||||
FlipperPlugin,
|
FlipperPlugin,
|
||||||
FlexRow,
|
FlexRow,
|
||||||
@@ -31,29 +31,29 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
isProduction,
|
isProduction,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import ImagesSidebar from './ImagesSidebar.js';
|
import ImagesSidebar from './ImagesSidebar';
|
||||||
import ImagePool from './ImagePool.js';
|
import ImagePool from './ImagePool';
|
||||||
import type {Notification} from '../../plugin.tsx';
|
import {Notification, BaseAction} from '../../plugin';
|
||||||
|
|
||||||
export type ImageEventWithId = ImageEvent & {eventId: number};
|
export type ImageEventWithId = ImageEvent & {eventId: number};
|
||||||
|
|
||||||
export type PersistedState = {
|
export type PersistedState = {
|
||||||
surfaceList: Set<string>,
|
surfaceList: Set<string>;
|
||||||
images: ImagesList,
|
images: ImagesList;
|
||||||
events: Array<ImageEventWithId>,
|
events: Array<ImageEventWithId>;
|
||||||
imagesMap: ImagesMap,
|
imagesMap: ImagesMap;
|
||||||
closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent>,
|
closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent>;
|
||||||
isLeakTrackingEnabled: boolean,
|
isLeakTrackingEnabled: boolean;
|
||||||
nextEventId: number,
|
nextEventId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PluginState = {
|
type PluginState = {
|
||||||
selectedSurfaces: Set<string>,
|
selectedSurfaces: Set<string>;
|
||||||
selectedImage: ?ImageId,
|
selectedImage: ImageId | null;
|
||||||
isDebugOverlayEnabled: boolean,
|
isDebugOverlayEnabled: boolean;
|
||||||
isAutoRefreshEnabled: boolean,
|
isAutoRefreshEnabled: boolean;
|
||||||
images: ImagesList,
|
images: ImagesList;
|
||||||
coldStartFilter: boolean,
|
coldStartFilter: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmptySidebar = styled(FlexRow)({
|
const EmptySidebar = styled(FlexRow)({
|
||||||
@@ -70,23 +70,23 @@ export const InlineFlexRow = styled(FlexRow)({
|
|||||||
|
|
||||||
const surfaceDefaultText = 'SELECT ALL SURFACES';
|
const surfaceDefaultText = 'SELECT ALL SURFACES';
|
||||||
|
|
||||||
const debugLog = (...args) => {
|
const debugLog = (...args: any[]) => {
|
||||||
if (!isProduction()) {
|
if (!isProduction()) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(...args);
|
console.log(...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type ImagesMetaData = {|
|
type ImagesMetaData = {
|
||||||
levels: ImagesListResponse,
|
levels: ImagesListResponse;
|
||||||
events: Array<ImageEventWithId>,
|
events: Array<ImageEventWithId>;
|
||||||
imageDataList: Array<ImageData>,
|
imageDataList: Array<ImageData>;
|
||||||
|};
|
};
|
||||||
|
|
||||||
export default class FlipperImagesPlugin extends FlipperPlugin<
|
export default class FlipperImagesPlugin extends FlipperPlugin<
|
||||||
PluginState,
|
PluginState,
|
||||||
*,
|
BaseAction,
|
||||||
PersistedState,
|
PersistedState
|
||||||
> {
|
> {
|
||||||
static defaultPersistedState: PersistedState = {
|
static defaultPersistedState: PersistedState = {
|
||||||
images: [],
|
images: [],
|
||||||
@@ -99,10 +99,10 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
};
|
};
|
||||||
|
|
||||||
static exportPersistedState = (
|
static exportPersistedState = (
|
||||||
callClient: (string, ?Object) => Promise<Object>,
|
callClient: (method: string, params?: any) => Promise<any>,
|
||||||
persistedState: ?PersistedState,
|
persistedState: PersistedState,
|
||||||
store: ?MiddlewareAPI,
|
store?: MiddlewareAPI,
|
||||||
): Promise<?PersistedState> => {
|
): Promise<PersistedState> => {
|
||||||
const defaultPromise = Promise.resolve(persistedState);
|
const defaultPromise = Promise.resolve(persistedState);
|
||||||
if (!persistedState) {
|
if (!persistedState) {
|
||||||
persistedState = FlipperImagesPlugin.defaultPersistedState;
|
persistedState = FlipperImagesPlugin.defaultPersistedState;
|
||||||
@@ -149,9 +149,9 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
acc.add(id);
|
acc.add(id);
|
||||||
});
|
});
|
||||||
return acc;
|
return acc;
|
||||||
}, new Set());
|
}, new Set<string>());
|
||||||
const imageDataList: Array<ImageData> = [];
|
const imageDataList: Array<ImageData> = [];
|
||||||
for (const id: string of idSet) {
|
for (const id of idSet) {
|
||||||
try {
|
try {
|
||||||
const imageData: ImageData = await callClient('getImage', {
|
const imageData: ImageData = await callClient('getImage', {
|
||||||
imageId: id,
|
imageId: id,
|
||||||
@@ -173,10 +173,10 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
static persistedStateReducer = (
|
static persistedStateReducer = (
|
||||||
persistedState: PersistedState,
|
persistedState: PersistedState,
|
||||||
method: string,
|
method: string,
|
||||||
data: Object,
|
data: AndroidCloseableReferenceLeakEvent | ImageEvent,
|
||||||
): PersistedState => {
|
): PersistedState => {
|
||||||
if (method == 'closeable_reference_leak_event') {
|
if (method == 'closeable_reference_leak_event') {
|
||||||
const event: AndroidCloseableReferenceLeakEvent = data;
|
const event: AndroidCloseableReferenceLeakEvent = data as AndroidCloseableReferenceLeakEvent;
|
||||||
return {
|
return {
|
||||||
...persistedState,
|
...persistedState,
|
||||||
closeableReferenceLeaks: persistedState.closeableReferenceLeaks.concat(
|
closeableReferenceLeaks: persistedState.closeableReferenceLeaks.concat(
|
||||||
@@ -184,8 +184,7 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (method == 'events') {
|
} else if (method == 'events') {
|
||||||
const event: ImageEvent = data;
|
const event: ImageEvent = data as ImageEvent;
|
||||||
|
|
||||||
debugLog('Received events', event);
|
debugLog('Received events', event);
|
||||||
const {surfaceList} = persistedState;
|
const {surfaceList} = persistedState;
|
||||||
const {attribution} = event;
|
const {attribution} = event;
|
||||||
@@ -267,11 +266,7 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
category: 'closeablereference_leak',
|
category: 'closeablereference_leak',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
state: PluginState;
|
state: PluginState = {
|
||||||
imagePool: ImagePool;
|
|
||||||
nextEventId: number = 1;
|
|
||||||
|
|
||||||
state = {
|
|
||||||
selectedSurfaces: new Set([surfaceDefaultText]),
|
selectedSurfaces: new Set([surfaceDefaultText]),
|
||||||
selectedImage: null,
|
selectedImage: null,
|
||||||
isDebugOverlayEnabled: false,
|
isDebugOverlayEnabled: false,
|
||||||
@@ -279,6 +274,8 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
images: [],
|
images: [],
|
||||||
coldStartFilter: false,
|
coldStartFilter: false,
|
||||||
};
|
};
|
||||||
|
imagePool: ImagePool | undefined;
|
||||||
|
nextEventId: number = 1;
|
||||||
|
|
||||||
filterImages = (
|
filterImages = (
|
||||||
images: ImagesList,
|
images: ImagesList,
|
||||||
@@ -340,7 +337,7 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
}
|
}
|
||||||
|
|
||||||
teardown() {
|
teardown() {
|
||||||
this.imagePool.clear();
|
this.imagePool ? this.imagePool.clear() : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateImagesOnUI = (
|
updateImagesOnUI = (
|
||||||
@@ -365,7 +362,7 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
debugLog('Requesting images list (reason=' + reason + ')');
|
debugLog('Requesting images list (reason=' + reason + ')');
|
||||||
this.client.call('listImages').then((response: ImagesListResponse) => {
|
this.client.call('listImages').then((response: ImagesListResponse) => {
|
||||||
response.levels.forEach(data =>
|
response.levels.forEach(data =>
|
||||||
this.imagePool.fetchImages(data.imageIds),
|
this.imagePool ? this.imagePool.fetchImages(data.imageIds) : undefined,
|
||||||
);
|
);
|
||||||
this.props.setPersistedState({images: response.levels});
|
this.props.setPersistedState({images: response.levels});
|
||||||
this.updateImagesOnUI(
|
this.updateImagesOnUI(
|
||||||
@@ -409,7 +406,7 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
|
|||||||
debugLog('<- getImage requested for ' + imageId);
|
debugLog('<- getImage requested for ' + imageId);
|
||||||
this.client.call('getImage', {imageId}).then((image: ImageData) => {
|
this.client.call('getImage', {imageId}).then((image: ImageData) => {
|
||||||
debugLog('-> getImage ' + imageId + ' returned');
|
debugLog('-> getImage ' + imageId + ' returned');
|
||||||
this.imagePool._fetchCompleted(image);
|
this.imagePool ? this.imagePool._fetchCompleted(image) : undefined;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Fresco",
|
"name": "Fresco",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.tsx",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": ["flipper-plugin"],
|
"keywords": ["flipper-plugin"],
|
||||||
"title": "Images",
|
"title": "Images",
|
||||||
|
|||||||
Reference in New Issue
Block a user