mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 08:57:54 +00:00
Fix scrubber position
This commit is contained in:
1 parent
dcd5a54350
commit
326fe2c497
12 files changed
+141
-205
No files matched your search
@@ -1,78 +1,81 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { loadingBarData, loadingBarIsShowing } from "~/store/loadingBar";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
|
||||
interface LoadingBarProps{
|
||||
interface LoadingBarProps {
|
||||
id?: string;
|
||||
wrapperClassName?:string;
|
||||
wrapperClassName?: string;
|
||||
innerWrapperClassName?: string;
|
||||
progressBackgroundClassName?: string;
|
||||
progressClassName?: string;
|
||||
label?: string;
|
||||
progress?: number;
|
||||
isShowing?: boolean;
|
||||
variant?: string;
|
||||
message?: string;
|
||||
useFakeTimer?: number;
|
||||
}
|
||||
export const LoadingBar = ({
|
||||
wrapperClassName : propsWrapperClassName,
|
||||
wrapperClassName: propsWrapperClassName,
|
||||
innerWrapperClassName: propsInnerWrapperClassName,
|
||||
progressBackgroundClassName: propsProgressBackgroundClassName,
|
||||
progressClassName : propsProgressClassName,
|
||||
label,
|
||||
progress: propsProgress = 0,
|
||||
isShowing = true,
|
||||
variant = 'primary',
|
||||
message,
|
||||
useFakeTimer,
|
||||
progressClassName: propsProgressClassName,
|
||||
variant = "primary",
|
||||
...rest
|
||||
}: LoadingBarProps) => {
|
||||
const [progress, setProgress] = useState<number>(propsProgress);
|
||||
useSignals();
|
||||
const [progress, setProgress] = useState<number>(
|
||||
loadingBarData.value.progress,
|
||||
);
|
||||
const [loop, setLoop] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
// this takes the real progress coming from parent
|
||||
useEffect(()=>{
|
||||
setProgress(propsProgress);
|
||||
}, [propsProgress]);
|
||||
|
||||
// this sets a fake progress, it goes to 96%
|
||||
// in the amount of miliseconds provided by the flag
|
||||
useEffect(()=>{
|
||||
let loop: NodeJS.Timeout;
|
||||
if(useFakeTimer){
|
||||
if(useFakeTimer >= 30000){
|
||||
// this math produce 96 cuts so the progress bar updates more
|
||||
loop = setInterval(function step(){
|
||||
setProgress((curr)=>{
|
||||
if(curr<96){
|
||||
return curr + 3
|
||||
}else{
|
||||
clearInterval(loop);
|
||||
return curr;
|
||||
}
|
||||
})
|
||||
},Math.round(useFakeTimer/96)*3);
|
||||
}else{
|
||||
// this math produce less cuts if useFakeTimer predicts
|
||||
// a shorter load time, shorter thatn 30s
|
||||
const progressPerInterval = useFakeTimer/500
|
||||
loop = loop = setInterval(function step(){
|
||||
setProgress((curr)=>{
|
||||
if(curr + progressPerInterval < 96){
|
||||
return curr + progressPerInterval
|
||||
}else if (curr< 96 && curr + progressPerInterval >= 96){
|
||||
return 96;
|
||||
}else{
|
||||
clearInterval(loop);
|
||||
return curr;
|
||||
}
|
||||
})
|
||||
}, 300);
|
||||
}
|
||||
const useFakeTimer = useRef(0);
|
||||
if (useFakeTimer.current !== loadingBarData.value.useFakeTimer) {
|
||||
useFakeTimer.current = loadingBarData.value.useFakeTimer;
|
||||
if (loop) {
|
||||
clearInterval(loop);
|
||||
}
|
||||
return () => clearInterval(loop);
|
||||
}, []);
|
||||
if (loadingBarData.value.useFakeTimer > 0) {
|
||||
if (loadingBarData.value.useFakeTimer >= 30000) {
|
||||
// this math produce 96 cuts so the progress bar updates more
|
||||
setLoop(
|
||||
setInterval(
|
||||
function step() {
|
||||
setProgress((curr) => {
|
||||
if (curr < 96) {
|
||||
return curr + 3;
|
||||
} else {
|
||||
clearInterval(loop!);
|
||||
return curr;
|
||||
}
|
||||
});
|
||||
},
|
||||
Math.round(loadingBarData.value.useFakeTimer / 96) * 3,
|
||||
),
|
||||
);
|
||||
}
|
||||
// this math produce less cuts if useFakeTimer predicts
|
||||
// a shorter load time, shorter than 30s
|
||||
const progressPerInterval = loadingBarData.value.useFakeTimer / 500;
|
||||
setLoop(
|
||||
setInterval(function step() {
|
||||
setProgress((curr) => {
|
||||
if (curr + progressPerInterval < 96) {
|
||||
return curr + progressPerInterval;
|
||||
} else if (curr < 96 && curr + progressPerInterval >= 96) {
|
||||
return 96;
|
||||
} else {
|
||||
clearInterval(loop!);
|
||||
return curr;
|
||||
}
|
||||
});
|
||||
}, 300),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const oldProgress = useRef(loadingBarData.value.progress);
|
||||
if (oldProgress.current !== loadingBarData.value.progress) {
|
||||
oldProgress.current = loadingBarData.value.progress;
|
||||
setProgress(loadingBarData.value.progress);
|
||||
}
|
||||
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
@@ -89,43 +92,44 @@ export const LoadingBar = ({
|
||||
const wrapperClassName = twMerge(
|
||||
"w-full h-full relative bg-ui-background",
|
||||
propsWrapperClassName,
|
||||
)
|
||||
);
|
||||
const innerWrapperClassName = twMerge(
|
||||
"w-full h-full p-4 gap-4 m-auto flex flex-col justify-center items-center",
|
||||
propsInnerWrapperClassName,
|
||||
);
|
||||
const progressBackgroundClassName = twMerge(
|
||||
"w-full bg-gray-500 rounded-full h-2.5",
|
||||
propsProgressBackgroundClassName
|
||||
propsProgressBackgroundClassName,
|
||||
);
|
||||
const progressClassName = twMerge(
|
||||
"h-2.5 rounded-full transition-all duration-1000",
|
||||
getVariantClassNames(variant),
|
||||
propsProgressClassName
|
||||
propsProgressClassName,
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition
|
||||
className={wrapperClassName}
|
||||
show={isShowing}
|
||||
show={loadingBarIsShowing.value}
|
||||
enter="transition-opacity duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-1000"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
{...rest}
|
||||
>
|
||||
{...rest}>
|
||||
<div className={innerWrapperClassName}>
|
||||
{label && <label>{label}</label>}
|
||||
{loadingBarData.value.label && (
|
||||
<label>{loadingBarData.value.label}</label>
|
||||
)}
|
||||
<div className={progressBackgroundClassName}>
|
||||
<div
|
||||
className={progressClassName}
|
||||
style={{width: progress + '%'}}
|
||||
<div
|
||||
className={progressClassName}
|
||||
style={{ width: progress + "%" }}
|
||||
/>
|
||||
</div>
|
||||
{message && <p>{message}</p>}
|
||||
{loadingBarData.value.message && <p>{loadingBarData.value.message}</p>}
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { LoadingBar, LoadingDots } from "~/components";
|
||||
import { LoadingDots } from "~/components";
|
||||
import { SidePanel } from "~/modules/SidePanel";
|
||||
|
||||
import { Controls3D } from "./comps/Controls3D";
|
||||
@@ -109,15 +109,6 @@ export const PageEditor = ({ setPage }: Props) => {
|
||||
type="bricks"
|
||||
message={appUiState.showEditorLoader.message}
|
||||
/>
|
||||
<LoadingBar
|
||||
id="editor-loading-bar"
|
||||
wrapperClassName="absolute top-0 left-0"
|
||||
innerWrapperClassName="max-w-screen-sm"
|
||||
isShowing={appUiState.showEditorLoadingBar.isShowing}
|
||||
message={appUiState.showEditorLoadingBar.message}
|
||||
label={appUiState.showEditorLoadingBar.label}
|
||||
progress={appUiState.showEditorLoadingBar.progress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Side panel */}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TrackProvider } from "~/pages/PageEnigma/contexts/TrackContext/TrackPro
|
||||
import { DragComponent } from "~/pages/PageEnigma/comps/DragComponent/DragComponent";
|
||||
import { EngineProvider } from "~/contexts/EngineProvider";
|
||||
import { useInferenceJobManager } from "~/hooks";
|
||||
import { LoadingBar } from "~/components";
|
||||
|
||||
export const PageEnigma = () => {
|
||||
useInferenceJobManager();
|
||||
return (
|
||||
@@ -11,6 +13,11 @@ export const PageEnigma = () => {
|
||||
<PageEnigmaComponent />
|
||||
<DragComponent />
|
||||
</EngineProvider>
|
||||
<LoadingBar
|
||||
id="editor-loading-bar"
|
||||
wrapperClassName="absolute top-0 left-0"
|
||||
innerWrapperClassName="max-w-screen-sm"
|
||||
/>
|
||||
</TrackProvider>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
faArrowsRotate,
|
||||
faFilm,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, LoadingBar } from "~/components";
|
||||
import { Button } from "~/components";
|
||||
import { StyleSelection } from "~/pages/PageEnigma/comps/StyleSelection";
|
||||
import { TimerGrid } from "~/pages/PageEnigma/comps/Timeline/TimerGrid";
|
||||
import { Scrubber } from "~/pages/PageEnigma/comps/Timeline/Scrubber";
|
||||
@@ -17,20 +17,22 @@ import {
|
||||
currentTime,
|
||||
filmLength,
|
||||
scale,
|
||||
timelineHeight,
|
||||
timelineScrollX,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import { pageWidth } from "~/store";
|
||||
import { RowHeaders } from "~/pages/PageEnigma/comps/Timeline/RowHeaders";
|
||||
|
||||
interface Props {
|
||||
setPage: (page: string) => void;
|
||||
}
|
||||
import { AppUiContext } from "~/contexts/AppUiContext";
|
||||
|
||||
export const PageStyling = ({ setPage }: Props) => {
|
||||
useSignals();
|
||||
const editorEngine = useContext(EngineContext);
|
||||
const [appUiState] = useContext(AppUiContext);
|
||||
|
||||
const generateFrame = async () => {
|
||||
await editorEngine?.generateFrame();
|
||||
@@ -52,6 +54,7 @@ export const PageStyling = ({ setPage }: Props) => {
|
||||
const onTimelineClick = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
console.log("click", event.clientX, event.pageX, timelineScrollX.value);
|
||||
const newTime = Math.round(
|
||||
(event.clientX + timelineScrollX.value - 92) / 4 / scale.value,
|
||||
);
|
||||
@@ -62,7 +65,7 @@ export const PageStyling = ({ setPage }: Props) => {
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
data: { currentTime: Math.round(newTime) },
|
||||
data: { currentTime: newTime },
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -97,30 +100,28 @@ export const PageStyling = ({ setPage }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed bottom-0 left-0 w-full">
|
||||
<div className="relative flex h-[80px] w-full gap-5 border-t border-t-action-600 bg-ui-panel">
|
||||
<div className="w-screen overflow-x-auto" onScroll={onScroll}>
|
||||
<div
|
||||
className="fixed bottom-0 left-0 w-full border-t border-t-action-600 bg-ui-panel"
|
||||
onClick={onTimelineClick}>
|
||||
<TimerGrid />
|
||||
<div className="flex w-full ">
|
||||
<div className="block h-[30px] w-[88px]" />
|
||||
<div
|
||||
className="h-[30px] overflow-x-auto overflow-y-hidden"
|
||||
onScroll={onScroll}
|
||||
style={{
|
||||
width: pageWidth.value,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: filmLength.value * 60 * 4 * scale.value,
|
||||
width: filmLength.value * 60 * 4 * scale.value + 90,
|
||||
height: 60,
|
||||
}}
|
||||
onClick={onTimelineClick}>
|
||||
<TimerGrid />
|
||||
}}>
|
||||
<Scrubber />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingBar
|
||||
id="editor-loading-bar"
|
||||
wrapperClassName="absolute top-0 left-0"
|
||||
innerWrapperClassName="max-w-screen-sm"
|
||||
isShowing={appUiState.showEditorLoadingBar.isShowing}
|
||||
message={appUiState.showEditorLoadingBar.message}
|
||||
label={appUiState.showEditorLoadingBar.label}
|
||||
progress={appUiState.showEditorLoadingBar.progress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,11 +14,10 @@ export const Scrubber = () => {
|
||||
const { onPointerDown, time } = useMouseEventsScrubber();
|
||||
const displayTime = time === -1 ? currentTime.value : time;
|
||||
|
||||
if (displayTime * 4 * scale.value + 84 - timelineScrollX.value < 84) {
|
||||
if (displayTime * 4 * scale.value - timelineScrollX.value < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(currentTime.value, displayTime, timelineScrollX.value);
|
||||
return (
|
||||
<div
|
||||
className="absolute flex cursor-ew-resize flex-col items-center text-brand-primary"
|
||||
|
||||
@@ -92,8 +92,7 @@ export const Timeline = () => {
|
||||
style={{
|
||||
width: 88,
|
||||
height: timelineHeight.value - 54,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<RowHeaders />
|
||||
</div>
|
||||
<div
|
||||
@@ -102,11 +101,9 @@ export const Timeline = () => {
|
||||
style={{
|
||||
width: pageWidth.value - 90,
|
||||
height: timelineHeight.value - 54,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<div
|
||||
style={{ width: filmLength.value * 60 * 4 * scale.value + 32 }}
|
||||
>
|
||||
style={{ width: filmLength.value * 60 * 4 * scale.value + 32 }}>
|
||||
<Characters />
|
||||
<div className="pb-4 pr-8">
|
||||
<Camera />
|
||||
|
||||
@@ -36,6 +36,7 @@ export const useMouseEventsScrubber = () => {
|
||||
(event.clientX - clientX) / 4 / scale.value + currentTime.value,
|
||||
);
|
||||
if (isActive) {
|
||||
console.log("1111");
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (delta < 0 || delta > max) {
|
||||
|
||||
@@ -31,7 +31,7 @@ import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
|
||||
import { AssetType, MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { LoadingBar } from "~/components";
|
||||
import { loadingBarData, loadingBarIsShowing } from "~/store/loadingBar";
|
||||
|
||||
// Main editor class that will call everything else all you need to call is " initialize() ".
|
||||
class Editor {
|
||||
@@ -362,18 +362,11 @@ class Editor {
|
||||
this.loadScene(sceneToken);
|
||||
}
|
||||
|
||||
this.dispatchAppUiState({
|
||||
type: APPUI_ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR,
|
||||
payload: {
|
||||
showEditorLoadingBar: {
|
||||
progress: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.dispatchAppUiState({
|
||||
type: APPUI_ACTION_TYPES.HIDE_EDITOR_LOADINGBAR,
|
||||
});
|
||||
loadingBarData.value = {
|
||||
...loadingBarData.value,
|
||||
progress: 100,
|
||||
};
|
||||
loadingBarIsShowing.value = false;
|
||||
}
|
||||
|
||||
// Token comes in from the front end to load the scene from the site.
|
||||
@@ -565,27 +558,20 @@ class Editor {
|
||||
}
|
||||
|
||||
async showLoading() {
|
||||
this.dispatchAppUiState({
|
||||
type: APPUI_ACTION_TYPES.SHOW_EDITOR_LOADINGBAR,
|
||||
});
|
||||
loadingBarIsShowing.value = true;
|
||||
}
|
||||
|
||||
async updateLoad(progress: number, message: string) {
|
||||
this.dispatchAppUiState({
|
||||
type: APPUI_ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR,
|
||||
payload: {
|
||||
showEditorLoadingBar: {
|
||||
progress: progress,
|
||||
message: message,
|
||||
},
|
||||
},
|
||||
});
|
||||
loadingBarData.value = {
|
||||
...loadingBarData.value,
|
||||
progress: progress,
|
||||
message: message,
|
||||
};
|
||||
}
|
||||
|
||||
async endLoading() {
|
||||
this.dispatchAppUiState({
|
||||
type: APPUI_ACTION_TYPES.HIDE_EDITOR_LOADINGBAR,
|
||||
});
|
||||
console.log("stop loading");
|
||||
loadingBarIsShowing.value = false;
|
||||
}
|
||||
|
||||
async _test_demo() {
|
||||
|
||||
@@ -64,33 +64,6 @@ export function reducer(state: State, action: Action): State {
|
||||
},
|
||||
};
|
||||
}
|
||||
case ACTION_TYPES.SHOW_EDITOR_LOADINGBAR: {
|
||||
return {
|
||||
...state,
|
||||
showEditorLoadingBar: {
|
||||
...action.payload?.showEditorLoadingBar,
|
||||
isShowing: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
case ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR: {
|
||||
return {
|
||||
...state,
|
||||
showEditorLoadingBar: {
|
||||
...state.showEditorLoadingBar,
|
||||
...action.payload?.showEditorLoadingBar,
|
||||
},
|
||||
};
|
||||
}
|
||||
case ACTION_TYPES.HIDE_EDITOR_LOADINGBAR: {
|
||||
return {
|
||||
...state,
|
||||
showEditorLoadingBar: {
|
||||
...state.showEditorLoadingBar,
|
||||
isShowing: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
case ACTION_TYPES.ON_TIMELINE_RESIZE: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -26,17 +26,10 @@ export type State = {
|
||||
isShowing: boolean;
|
||||
message?: string;
|
||||
};
|
||||
showEditorLoadingBar: {
|
||||
isShowing: boolean;
|
||||
label?: string;
|
||||
message?: string;
|
||||
progress?: number;
|
||||
useFakeTimer?: number;
|
||||
};
|
||||
controlPanel: ControlPanel;
|
||||
diagloueTts: {
|
||||
isOpen: boolean;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export enum ACTION_TYPES {
|
||||
@@ -44,9 +37,6 @@ export enum ACTION_TYPES {
|
||||
ON_CHANGE_VIEW_MODE = "on_change_view_mode",
|
||||
SHOW_EDITOR_LOADER = "show_editor_loader",
|
||||
HIDE_EDITOR_LOADER = "hide_editor_loader",
|
||||
SHOW_EDITOR_LOADINGBAR = "show_editor_loadingbar",
|
||||
UPDATE_EDITOR_LOADINGBAR = "update_editor_loadingbar",
|
||||
HIDE_EDITOR_LOADINGBAR = "hide_editor_loadingbar",
|
||||
SHOW_CONTROLPANELS_SCENEOBJECT = "show_controlpanels_sceneobject",
|
||||
UPDATE_CONTROLPANELS_SCENEOBJECT = "update_controlpanels_sceneobject",
|
||||
HIDE_CONTROLPANELS_SCENEOBJECT = "hide_controlpanels_sceneobject",
|
||||
@@ -75,28 +65,6 @@ export type Action =
|
||||
};
|
||||
};
|
||||
}
|
||||
| { type: ACTION_TYPES.HIDE_EDITOR_LOADINGBAR }
|
||||
| {
|
||||
type: ACTION_TYPES.SHOW_EDITOR_LOADINGBAR;
|
||||
payload?: {
|
||||
showEditorLoadingBar: {
|
||||
label?: string;
|
||||
message?: string;
|
||||
progress?: number;
|
||||
useFakeTimer?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR;
|
||||
payload: {
|
||||
showEditorLoadingBar: {
|
||||
label?: string;
|
||||
message?: string;
|
||||
progress?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.ON_TIMELINE_RESIZE;
|
||||
payload: {
|
||||
|
||||
@@ -8,11 +8,6 @@ export const initialStateValues: State = {
|
||||
isShowing: false,
|
||||
message: "Loading Editor Engine 🦊",
|
||||
},
|
||||
showEditorLoadingBar: {
|
||||
isShowing: true,
|
||||
progress: 5,
|
||||
message: "Loading Editor Engine 🦊",
|
||||
},
|
||||
controlPanel: {
|
||||
isShowing: false,
|
||||
currentSceneObject: {
|
||||
@@ -27,5 +22,5 @@ export const initialStateValues: State = {
|
||||
},
|
||||
},
|
||||
},
|
||||
diagloueTts:{isOpen:false}
|
||||
diagloueTts: { isOpen: false },
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { signal } from "@preact/signals-core";
|
||||
|
||||
export const loadingBarIsShowing = signal(true);
|
||||
export const loadingBarData = signal<{
|
||||
label: string;
|
||||
message: string;
|
||||
progress: number;
|
||||
useFakeTimer: number;
|
||||
}>({
|
||||
label: "",
|
||||
progress: 5,
|
||||
message: "Loading Editor Engine 🦊",
|
||||
useFakeTimer: 0,
|
||||
});
|
||||
Reference in new issue
Block a user