mirror of
https://github.com/cheveguerra/whaticket-community.git
synced 2026-04-18 19:59:20 +00:00
feat: start internatinalization
This commit is contained in:
@@ -2,10 +2,11 @@ import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Formik, FieldArray } from "formik";
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { green } from "@material-ui/core/colors";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
|
||||
import DialogActions from "@material-ui/core/DialogActions";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||
@@ -13,9 +14,8 @@ import Typography from "@material-ui/core/Typography";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import { green } from "@material-ui/core/colors";
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { i18n } from "../../translate/i18n";
|
||||
|
||||
import api from "../../services/api";
|
||||
|
||||
@@ -121,14 +121,16 @@ const ContactModal = ({ open, onClose, contactId }) => {
|
||||
}) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{contactId ? "Editar contato" : "Adicionar contato"}
|
||||
{contactId
|
||||
? `${i18n.t("contactModal.title.edit")}`
|
||||
: `${i18n.t("contactModal.title.add")}`}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Dados do contato
|
||||
{i18n.t("contactModal.form.mainInfo")}
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Nome"
|
||||
label={i18n.t("contactModal.form.name")}
|
||||
name="name"
|
||||
value={values.name || ""}
|
||||
onChange={handleChange}
|
||||
@@ -138,18 +140,18 @@ const ContactModal = ({ open, onClose, contactId }) => {
|
||||
className={classes.textField}
|
||||
/>
|
||||
<TextField
|
||||
label="Número do Whatsapp"
|
||||
label={i18n.t("contactModal.form.number")}
|
||||
name="number"
|
||||
value={values.number || ""}
|
||||
onChange={handleChange}
|
||||
placeholder="Ex: 5513912344321"
|
||||
placeholder="5513912344321"
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
required
|
||||
/>
|
||||
<div>
|
||||
<TextField
|
||||
label="Email"
|
||||
label={i18n.t("contactModal.form.email")}
|
||||
name="email"
|
||||
value={values.email || ""}
|
||||
onChange={handleChange}
|
||||
@@ -163,7 +165,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
|
||||
style={{ marginBottom: 8, marginTop: 12 }}
|
||||
variant="subtitle1"
|
||||
>
|
||||
Informações adicionais
|
||||
{i18n.t("contactModal.form.extraInfo")}
|
||||
</Typography>
|
||||
|
||||
<FieldArray name="extraInfo">
|
||||
@@ -177,7 +179,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
|
||||
key={`${index}-info`}
|
||||
>
|
||||
<TextField
|
||||
label="Nome do campo"
|
||||
label={i18n.t("contactModal.form.extraName")}
|
||||
name={`extraInfo[${index}].name`}
|
||||
value={info.name || ""}
|
||||
onChange={handleChange}
|
||||
@@ -187,7 +189,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
|
||||
className={classes.textField}
|
||||
/>
|
||||
<TextField
|
||||
label="Valor"
|
||||
label={i18n.t("contactModal.form.extraValue")}
|
||||
name={`extraInfo[${index}].value`}
|
||||
value={info.value || ""}
|
||||
onChange={handleChange}
|
||||
@@ -211,7 +213,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
|
||||
color="primary"
|
||||
onClick={() => push({ name: "", value: "" })}
|
||||
>
|
||||
+ Adicionar atributo
|
||||
{`+ ${i18n.t("contactModal.buttons.addExtraInfo")}`}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -225,7 +227,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
|
||||
disabled={isSubmitting}
|
||||
variant="outlined"
|
||||
>
|
||||
Cancelar
|
||||
{i18n.t("contactModal.buttons.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -234,7 +236,9 @@ const ContactModal = ({ open, onClose, contactId }) => {
|
||||
variant="contained"
|
||||
className={classes.btnWrapper}
|
||||
>
|
||||
{contactId ? "Salvar" : "Adicionar"}
|
||||
{contactId
|
||||
? `${i18n.t("contactModal.buttons.okEdit")}`
|
||||
: `${i18n.t("contactModal.buttons.okAdd")}`}
|
||||
{isSubmitting && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
import React from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
import List from "@material-ui/core/List";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||
import ListItemText from "@material-ui/core/ListItemText";
|
||||
// import ListSubheader from "@material-ui/core/ListSubheader";
|
||||
import Collapse from "@material-ui/core/Collapse";
|
||||
|
||||
import DashboardIcon from "@material-ui/icons/Dashboard";
|
||||
import WhatsAppIcon from "@material-ui/icons/WhatsApp";
|
||||
import SyncAltIcon from "@material-ui/icons/SyncAlt";
|
||||
import ChatIcon from "@material-ui/icons/Chat";
|
||||
import LayersIcon from "@material-ui/icons/Layers";
|
||||
import ExpandLess from "@material-ui/icons/ExpandLess";
|
||||
import ExpandMore from "@material-ui/icons/ExpandMore";
|
||||
|
||||
import ContactPhoneIcon from "@material-ui/icons/ContactPhone";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
nested: {
|
||||
paddingLeft: theme.spacing(4),
|
||||
},
|
||||
}));
|
||||
import { i18n } from "../../translate/i18n";
|
||||
|
||||
function ListItemLink(props) {
|
||||
const { icon, primary, to, className } = props;
|
||||
@@ -46,76 +34,27 @@ function ListItemLink(props) {
|
||||
}
|
||||
|
||||
const MainListItems = () => {
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListItemLink to="/" primary="Dashboard" icon={<DashboardIcon />} />
|
||||
<ListItemLink
|
||||
to="/whats-auth"
|
||||
primary={i18n.t("mainDrawer.listItems.connection")}
|
||||
icon={<SyncAltIcon />}
|
||||
/>
|
||||
<ListItemLink
|
||||
to="/chat"
|
||||
primary={i18n.t("mainDrawer.listItems.tickets")}
|
||||
icon={<WhatsAppIcon />}
|
||||
/>
|
||||
|
||||
<ListItem button onClick={handleClick}>
|
||||
<ListItemIcon>
|
||||
<WhatsAppIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="WhatsApp" />
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
<ListItemLink
|
||||
className={classes.nested}
|
||||
to="/whats-auth"
|
||||
primary="Conexão"
|
||||
icon={<SyncAltIcon />}
|
||||
/>
|
||||
<ListItemLink
|
||||
className={classes.nested}
|
||||
to="/chat"
|
||||
primary="Chat"
|
||||
icon={<ChatIcon />}
|
||||
/>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItemLink
|
||||
to="/contacts"
|
||||
primary="Contatos"
|
||||
primary={i18n.t("mainDrawer.listItems.contacts")}
|
||||
icon={<ContactPhoneIcon />}
|
||||
/>
|
||||
<ListItem button disabled>
|
||||
<ListItemIcon>
|
||||
<LayersIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Integrações" />
|
||||
</ListItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// export const secondaryListItems = (
|
||||
// <div>
|
||||
// <ListSubheader inset>Saved reports</ListSubheader>
|
||||
// <ListItem button>
|
||||
// <ListItemIcon>
|
||||
// <AssignmentIcon />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText primary="Current month" />
|
||||
// </ListItem>
|
||||
// <ListItem button>
|
||||
// <ListItemIcon>
|
||||
// <AssignmentIcon />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText primary="Last quarter" />
|
||||
// </ListItem>
|
||||
// <ListItem button>
|
||||
// <ListItemIcon>
|
||||
// <AssignmentIcon />
|
||||
// </ListItemIcon>
|
||||
// <ListItemText primary="Year-end sale" />
|
||||
// </ListItem>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
export default MainListItems;
|
||||
|
||||
@@ -221,8 +221,6 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
let socket;
|
||||
|
||||
const MessagesList = () => {
|
||||
const { ticketId } = useParams();
|
||||
const history = useHistory();
|
||||
@@ -272,17 +270,9 @@ const MessagesList = () => {
|
||||
}, [pageNumber, ticketId, history]);
|
||||
|
||||
useEffect(() => {
|
||||
socket = openSocket(process.env.REACT_APP_BACKEND_URL);
|
||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
|
||||
socket.emit("joinChatBox", ticketId, () => {});
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
setPageNumber(1);
|
||||
setMessagesList([]);
|
||||
};
|
||||
}, [ticketId]);
|
||||
|
||||
useEffect(() => {
|
||||
socket.on("appMessage", data => {
|
||||
if (loading) return;
|
||||
|
||||
@@ -300,7 +290,13 @@ const MessagesList = () => {
|
||||
setContact(data.contact);
|
||||
}
|
||||
});
|
||||
}, [loading]);
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
setPageNumber(1);
|
||||
setMessagesList([]);
|
||||
};
|
||||
}, [ticketId, loading]);
|
||||
|
||||
const loadMore = () => {
|
||||
setPageNumber(prevPageNumber => prevPageNumber + 1);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { green } from "@material-ui/core/colors";
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
import { i18n } from "../../translate/i18n";
|
||||
import api from "../../services/api";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@@ -101,7 +102,9 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
|
||||
scroll="paper"
|
||||
>
|
||||
<form onSubmit={handleSaveTicket}>
|
||||
<DialogTitle id="form-dialog-title">Criar Ticket</DialogTitle>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{i18n.t("newTicketModal.title")}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Autocomplete
|
||||
id="asynchronous-demo"
|
||||
@@ -115,7 +118,7 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Digite para pesquisar o contato"
|
||||
label={i18n.t("newTicketModal.fieldLabel")}
|
||||
variant="outlined"
|
||||
required
|
||||
autoFocus
|
||||
@@ -143,7 +146,7 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
|
||||
disabled={loading}
|
||||
variant="outlined"
|
||||
>
|
||||
Cancelar
|
||||
{i18n.t("newTicketModal.buttons.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -152,7 +155,7 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
|
||||
variant="contained"
|
||||
className={classes.btnWrapper}
|
||||
>
|
||||
Salvar
|
||||
{i18n.t("newTicketModal.buttons.ok")}
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
|
||||
@@ -2,11 +2,13 @@ import React from "react";
|
||||
import QRCode from "qrcode.react";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
import { i18n } from "../../translate/i18n";
|
||||
|
||||
const Qrcode = ({ qrCode }) => {
|
||||
return (
|
||||
<div>
|
||||
<Typography color="primary" gutterBottom>
|
||||
Leia o QrCode para iniciar a sessão
|
||||
{i18n.t("qrCode.message")}
|
||||
</Typography>
|
||||
<QRCode value={qrCode} size={256} />
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import React from "react";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
import { i18n } from "../../translate/i18n";
|
||||
|
||||
const SessionInfo = ({ session }) => {
|
||||
console.log(session);
|
||||
return (
|
||||
<div>
|
||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
||||
{`Status: ${session.status}`}
|
||||
{`${i18n.t("sessionInfo.status")}${session.status}`}
|
||||
</Typography>
|
||||
<Typography component="p" variant="h6">
|
||||
{`Bateria: ${session.battery}%`}
|
||||
{`${i18n.t("sessionInfo.battery")}${session.battery}%`}
|
||||
</Typography>
|
||||
<Typography color="textSecondary">
|
||||
{`Carregando: ${session.plugged} `}
|
||||
{`${i18n.t("sessionInfo.charging")}${session.plugged} `}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,10 +23,9 @@ import NewTicketModal from "../NewTicketModal";
|
||||
import TicketsList from "../TicketsList";
|
||||
import TabPanel from "../TabPanel";
|
||||
|
||||
import { i18n } from "../../translate/i18n";
|
||||
import api from "../../services/api";
|
||||
|
||||
let socket;
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
ticketsWrapper: {
|
||||
position: "relative",
|
||||
@@ -209,15 +208,9 @@ const Tickets = () => {
|
||||
}, [searchParam, pageNumber, token, tab]);
|
||||
|
||||
useEffect(() => {
|
||||
socket = openSocket(process.env.REACT_APP_BACKEND_URL);
|
||||
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
|
||||
socket.emit("joinNotification");
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
socket.on("ticket", data => {
|
||||
if (loading) return;
|
||||
|
||||
@@ -232,7 +225,7 @@ const Tickets = () => {
|
||||
if (data.action === "delete") {
|
||||
deleteTicket(data);
|
||||
if (ticketId && data.ticketId === +ticketId) {
|
||||
toast.warn("O ticket que você estava foi deletado.");
|
||||
toast.warn(i18n.t("tickets.toasts.deleted"));
|
||||
history.push("/chat");
|
||||
}
|
||||
}
|
||||
@@ -253,6 +246,10 @@ const Tickets = () => {
|
||||
showDesktopNotification(data);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [history, ticketId, userId, loading]);
|
||||
|
||||
const loadMore = () => {
|
||||
@@ -262,16 +259,11 @@ const Tickets = () => {
|
||||
const updateTickets = ({ ticket }) => {
|
||||
setTickets(prevState => {
|
||||
const ticketIndex = prevState.findIndex(t => t.id === ticket.id);
|
||||
|
||||
if (prevState.length >= 20) {
|
||||
if (ticketIndex !== -1) {
|
||||
let aux = [...prevState];
|
||||
aux[ticketIndex] = ticket;
|
||||
aux.unshift(aux.splice(ticketIndex, 1)[0]);
|
||||
return aux;
|
||||
} else {
|
||||
return [ticket, ...prevState];
|
||||
}
|
||||
if (ticketIndex !== -1) {
|
||||
let aux = [...prevState];
|
||||
aux[ticketIndex] = ticket;
|
||||
aux.unshift(aux.splice(ticketIndex, 1)[0]);
|
||||
return aux;
|
||||
} else {
|
||||
return [ticket, ...prevState];
|
||||
}
|
||||
@@ -298,7 +290,10 @@ const Tickets = () => {
|
||||
icon: contact.profilePicUrl,
|
||||
tag: ticket.id,
|
||||
};
|
||||
let notification = new Notification(`Mensagem de ${contact.name}`, options);
|
||||
let notification = new Notification(
|
||||
`${i18n.t("tickets.notification.message")} ${contact.name}`,
|
||||
options
|
||||
);
|
||||
|
||||
notification.onclick = function (event) {
|
||||
event.preventDefault(); //
|
||||
@@ -396,19 +391,19 @@ const Tickets = () => {
|
||||
<Tab
|
||||
value={"open"}
|
||||
icon={<MoveToInboxIcon />}
|
||||
label="Inbox"
|
||||
label={i18n.t("tickets.tabs.open.title")}
|
||||
classes={{ root: classes.tab }}
|
||||
/>
|
||||
<Tab
|
||||
value={"closed"}
|
||||
icon={<CheckBoxIcon />}
|
||||
label="Resolvidos"
|
||||
label={i18n.t("tickets.tabs.closed.title")}
|
||||
classes={{ root: classes.tab }}
|
||||
/>
|
||||
<Tab
|
||||
value={"search"}
|
||||
icon={<SearchIcon />}
|
||||
label="Busca"
|
||||
label={i18n.t("tickets.tabs.search.title")}
|
||||
classes={{ root: classes.tab }}
|
||||
/>
|
||||
</Tabs>
|
||||
@@ -418,7 +413,7 @@ const Tickets = () => {
|
||||
<SearchIcon className={classes.searchIcon} />
|
||||
<InputBase
|
||||
className={classes.contactsSearchInput}
|
||||
placeholder="Pesquisar tickets e mensagens"
|
||||
placeholder={i18n.t("tickets.search.placeholder")}
|
||||
type="search"
|
||||
onChange={handleSearchContact}
|
||||
/>
|
||||
@@ -432,19 +427,19 @@ const Tickets = () => {
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div className={classes.ticketsListHeader}>
|
||||
Atendendo
|
||||
{i18n.t("tickets.tabs.open.assignedHeader")}
|
||||
<span className={classes.ticketsCount}>
|
||||
{countTickets("open", userId)}
|
||||
</span>
|
||||
<div className={classes.ticketsListActions}>
|
||||
<FormControlLabel
|
||||
label="Todos"
|
||||
label={i18n.t("tickets.buttons.showAll")}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showAllTickets}
|
||||
onChange={e => setShowAllTickets(prevState => !prevState)}
|
||||
onChange={() => setShowAllTickets(prevState => !prevState)}
|
||||
name="showAllTickets"
|
||||
color="primary"
|
||||
/>
|
||||
@@ -467,8 +462,10 @@ const Tickets = () => {
|
||||
showAllTickets={showAllTickets}
|
||||
ticketId={ticketId}
|
||||
handleAcepptTicket={handleAcepptTicket}
|
||||
noTicketsTitle="Pronto pra mais?"
|
||||
noTicketsMessage="Aceite um ticket da fila para começar."
|
||||
noTicketsTitle={i18n.t("tickets.tabs.open.openNoTicketsTitle")}
|
||||
noTicketsMessage={i18n.t(
|
||||
"tickets.tabs.open.openNoTicketsMessage"
|
||||
)}
|
||||
status="open"
|
||||
userId={userId}
|
||||
/>
|
||||
@@ -482,7 +479,7 @@ const Tickets = () => {
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div className={classes.ticketsListHeader}>
|
||||
Aguardando
|
||||
{i18n.t("tickets.tabs.open.pendingHeader")}
|
||||
<span className={classes.ticketsCount}>
|
||||
{countTickets("pending", null)}
|
||||
</span>
|
||||
@@ -495,8 +492,10 @@ const Tickets = () => {
|
||||
showAllTickets={showAllTickets}
|
||||
ticketId={ticketId}
|
||||
handleAcepptTicket={handleAcepptTicket}
|
||||
noTicketsTitle="Tudo resolvido!"
|
||||
noTicketsMessage="Nenhum ticket pendente."
|
||||
noTicketsTitle={i18n.t("tickets.tabs.open.pendingNoTicketsTitle")}
|
||||
noTicketsMessage={i18n.t(
|
||||
"tickets.tabs.open.pendingNoTicketsMessage"
|
||||
)}
|
||||
status="pending"
|
||||
userId={null}
|
||||
/>
|
||||
@@ -541,8 +540,8 @@ const Tickets = () => {
|
||||
showAllTickets={showAllTickets}
|
||||
ticketId={ticketId}
|
||||
handleAcepptTicket={handleAcepptTicket}
|
||||
noTicketsTitle="Nada encontrado!"
|
||||
noTicketsMessage="Tente buscar por outro termo."
|
||||
noTicketsTitle={i18n.t("tickets.tabs.search.noTicketsTitle")}
|
||||
noTicketsMessage={i18n.t("tickets.tabs.search.noTicketsMessage")}
|
||||
status="all"
|
||||
/>
|
||||
{loading && <TicketsSkeleton />}
|
||||
|
||||
@@ -12,6 +12,8 @@ import Divider from "@material-ui/core/Divider";
|
||||
import Badge from "@material-ui/core/Badge";
|
||||
import Button from "@material-ui/core/Button";
|
||||
|
||||
import { i18n } from "../../translate/i18n";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
ticket: {
|
||||
position: "relative",
|
||||
@@ -185,7 +187,7 @@ const TicketsList = ({
|
||||
className="hidden-button"
|
||||
onClick={e => handleAcepptTicket(ticket.id)}
|
||||
>
|
||||
Aceitar
|
||||
{i18n.t("ticketsList.buttons.accept")}
|
||||
</Button>
|
||||
) : null}
|
||||
</ListItem>
|
||||
|
||||
Reference in New Issue
Block a user