More work on todo list
This commit is contained in:
		@@ -54,6 +54,11 @@ export type Todo = {
 | 
			
		||||
    value: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CreateTodo = {
 | 
			
		||||
    channel_id: string;
 | 
			
		||||
    value: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ChannelInfo = {
 | 
			
		||||
    id: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
@@ -209,17 +214,12 @@ export const patchGuildTodo = (guild: string) => ({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const postGuildTodo = (guild: string) => ({
 | 
			
		||||
    mutationFn: (reminder: Reminder) =>
 | 
			
		||||
        axios.post(`/dashboard/api/guild/${guild}/todos`, reminder).then((resp) => resp.data),
 | 
			
		||||
    mutationFn: (todo: CreateTodo) =>
 | 
			
		||||
        axios.post(`/dashboard/api/guild/${guild}/todos`, todo).then((resp) => resp.data),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const deleteGuildTodo = () => ({
 | 
			
		||||
    mutationFn: (todo: Todo) =>
 | 
			
		||||
        axios.delete(`/dashboard/api/todos`, {
 | 
			
		||||
            data: {
 | 
			
		||||
                id: todo.id,
 | 
			
		||||
            },
 | 
			
		||||
        }),
 | 
			
		||||
export const deleteGuildTodo = (guild: string) => ({
 | 
			
		||||
    mutationFn: (todoId: number) => axios.delete(`/dashboard/api/guild/${guild}/todos/${todoId}`),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const fetchUserReminders = () => ({
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import { createPortal, PropsWithChildren } from "preact/compat";
 | 
			
		||||
import { Import } from "../Import";
 | 
			
		||||
import { useGuild } from "../App/useGuild";
 | 
			
		||||
import { Link } from "wouter";
 | 
			
		||||
import { usePathname } from "wouter/use-browser-location";
 | 
			
		||||
 | 
			
		||||
import "./index.scss";
 | 
			
		||||
 | 
			
		||||
@@ -18,13 +19,14 @@ export const Guild = ({ children }: PropsWithChildren) => {
 | 
			
		||||
        return <GuildError />;
 | 
			
		||||
    } else {
 | 
			
		||||
        const importModal = createPortal(<Import />, document.getElementById("bottom-sidebar"));
 | 
			
		||||
        const path = usePathname();
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                {importModal}
 | 
			
		||||
                <div class="page-links">
 | 
			
		||||
                    <Link
 | 
			
		||||
                        class="button is-outlined is-success is-small"
 | 
			
		||||
                        class={`button is-outlined is-success is-small ${path.endsWith("reminders") && "is-focused"}`}
 | 
			
		||||
                        href={`/${guild}/reminders`}
 | 
			
		||||
                    >
 | 
			
		||||
                        <span>Reminders</span>
 | 
			
		||||
@@ -32,7 +34,10 @@ export const Guild = ({ children }: PropsWithChildren) => {
 | 
			
		||||
                            <i class="fa fa-chevron-right"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </Link>
 | 
			
		||||
                    <Link class="button is-outlined is-success is-small" href={`/${guild}/todos`}>
 | 
			
		||||
                    <Link
 | 
			
		||||
                        class={`button is-outlined is-success is-small ${path.endsWith("todos") && "is-focused"}`}
 | 
			
		||||
                        href={`/${guild}/todos`}
 | 
			
		||||
                    >
 | 
			
		||||
                        <span>Todo lists</span>
 | 
			
		||||
                        <span class="icon">
 | 
			
		||||
                            <i class="fa fa-chevron-right"></i>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,64 @@
 | 
			
		||||
import { useQuery } from "react-query";
 | 
			
		||||
import { fetchGuildChannels } from "../../api";
 | 
			
		||||
import { useMutation, useQuery, useQueryClient } from "react-query";
 | 
			
		||||
import { fetchGuildChannels, postGuildTodo } from "../../api";
 | 
			
		||||
import { useGuild } from "../App/useGuild";
 | 
			
		||||
import { useState } from "preact/hooks";
 | 
			
		||||
import { useFlash } from "../App/FlashContext";
 | 
			
		||||
import { ICON_FLASH_TIME } from "../../consts";
 | 
			
		||||
 | 
			
		||||
export const CreateTodo = () => {
 | 
			
		||||
    const guild = useGuild();
 | 
			
		||||
 | 
			
		||||
    const [recentlyCreated, setRecentlyCreated] = useState(false);
 | 
			
		||||
    const [newTodo, setNewTodo] = useState({ value: "", channel_id: null });
 | 
			
		||||
 | 
			
		||||
    const flash = useFlash();
 | 
			
		||||
 | 
			
		||||
    const queryClient = useQueryClient();
 | 
			
		||||
    const { isSuccess, data: channels } = useQuery(fetchGuildChannels(guild));
 | 
			
		||||
    const mutation = useMutation({
 | 
			
		||||
        ...postGuildTodo(guild),
 | 
			
		||||
        onSuccess: (data) => {
 | 
			
		||||
            if (data.error) {
 | 
			
		||||
                flash({
 | 
			
		||||
                    message: data.error,
 | 
			
		||||
                    type: "error",
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                flash({
 | 
			
		||||
                    message: "Todo created",
 | 
			
		||||
                    type: "success",
 | 
			
		||||
                });
 | 
			
		||||
                queryClient.invalidateQueries({
 | 
			
		||||
                    queryKey: ["GUILD_TODO"],
 | 
			
		||||
                });
 | 
			
		||||
                setRecentlyCreated(true);
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    setRecentlyCreated(false);
 | 
			
		||||
                }, ICON_FLASH_TIME);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    console.log(newTodo);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div class="todo">
 | 
			
		||||
            <textarea class="input todo-input" onInput={() => null} />
 | 
			
		||||
            <textarea
 | 
			
		||||
                class="input todo-input"
 | 
			
		||||
                onInput={(ev) => setNewTodo((todo) => ({ ...todo, value: ev.currentTarget.value }))}
 | 
			
		||||
            />
 | 
			
		||||
            <div class="control has-icons-left">
 | 
			
		||||
                <div class="select">
 | 
			
		||||
                    <select name="channel" class="channel-selector" onInput={() => {}}>
 | 
			
		||||
                    <select
 | 
			
		||||
                        name="channel"
 | 
			
		||||
                        class="channel-selector"
 | 
			
		||||
                        onInput={(ev) =>
 | 
			
		||||
                            setNewTodo((todo) => ({
 | 
			
		||||
                                ...todo,
 | 
			
		||||
                                channel_id: ev.currentTarget.value || null,
 | 
			
		||||
                            }))
 | 
			
		||||
                        }
 | 
			
		||||
                    >
 | 
			
		||||
                        <option value="">(None)</option>
 | 
			
		||||
                        {isSuccess && channels.map((c) => <option value={c.id}>{c.name}</option>)}
 | 
			
		||||
                    </select>
 | 
			
		||||
@@ -21,9 +67,21 @@ export const CreateTodo = () => {
 | 
			
		||||
                    <i class="fas fa-hashtag"></i>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <button onClick={() => null} class="button is-success save-btn">
 | 
			
		||||
            <button onClick={() => mutation.mutate(newTodo)} class="button is-success save-btn">
 | 
			
		||||
                <span class="icon">
 | 
			
		||||
                    <i class="fa fa-sparkles"></i>
 | 
			
		||||
                    {mutation.isLoading ? (
 | 
			
		||||
                        <span class="icon">
 | 
			
		||||
                            <i class="fas fa-spin fa-cog"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    ) : recentlyCreated ? (
 | 
			
		||||
                        <span class="icon">
 | 
			
		||||
                            <i class="fas fa-check"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <span class="icon">
 | 
			
		||||
                            <i class="fas fa-sparkles"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    )}
 | 
			
		||||
                </span>
 | 
			
		||||
            </button>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -29,9 +29,9 @@ pub mod string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub mod string_opt {
 | 
			
		||||
    use std::fmt::Display;
 | 
			
		||||
    use std::{fmt::Display, str::FromStr};
 | 
			
		||||
 | 
			
		||||
    use serde::{Deserializer, Serializer};
 | 
			
		||||
    use serde::{de, Deserialize, Deserializer, Serializer};
 | 
			
		||||
 | 
			
		||||
    pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
 | 
			
		||||
    where
 | 
			
		||||
@@ -44,6 +44,17 @@ pub mod string_opt {
 | 
			
		||||
            serializer.serialize_none()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
 | 
			
		||||
    where
 | 
			
		||||
        T: FromStr,
 | 
			
		||||
        T::Err: Display,
 | 
			
		||||
        D: Deserializer<'de>,
 | 
			
		||||
    {
 | 
			
		||||
        Option::<String>::deserialize(deserializer)?
 | 
			
		||||
            .map(|s| s.parse().map_err(de::Error::custom))
 | 
			
		||||
            .transpose()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
use std::{env, path::Path};
 | 
			
		||||
@@ -185,10 +196,10 @@ pub async fn initialize(
 | 
			
		||||
                routes::dashboard::api::guild::create_guild_reminder,
 | 
			
		||||
                routes::dashboard::api::guild::get_reminders,
 | 
			
		||||
                routes::dashboard::api::guild::edit_reminder,
 | 
			
		||||
                routes::dashboard::api::guild::create_todo,
 | 
			
		||||
                routes::dashboard::api::guild::get_todo,
 | 
			
		||||
                routes::dashboard::api::guild::update_todo,
 | 
			
		||||
                routes::dashboard::api::guild::delete_todo,
 | 
			
		||||
                routes::dashboard::api::guild::todos::create_todo,
 | 
			
		||||
                routes::dashboard::api::guild::todos::get_todo,
 | 
			
		||||
                routes::dashboard::api::guild::todos::update_todo,
 | 
			
		||||
                routes::dashboard::api::guild::todos::delete_todo,
 | 
			
		||||
                routes::dashboard::export::export_reminders,
 | 
			
		||||
                routes::dashboard::export::export_reminder_templates,
 | 
			
		||||
                routes::dashboard::export::export_todos,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ mod emojis;
 | 
			
		||||
mod reminders;
 | 
			
		||||
mod roles;
 | 
			
		||||
mod templates;
 | 
			
		||||
mod todos;
 | 
			
		||||
pub mod todos;
 | 
			
		||||
 | 
			
		||||
use std::env;
 | 
			
		||||
 | 
			
		||||
@@ -17,7 +17,6 @@ use serenity::{
 | 
			
		||||
    model::id::{GuildId, RoleId},
 | 
			
		||||
};
 | 
			
		||||
pub use templates::*;
 | 
			
		||||
pub use todos::{create_todo, delete_todo, get_todo, update_todo};
 | 
			
		||||
 | 
			
		||||
use crate::web::{check_authorization, routes::JsonResult};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,26 +7,38 @@ use rocket::{
 | 
			
		||||
    State,
 | 
			
		||||
};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use serenity::prelude::Context;
 | 
			
		||||
use serenity::{
 | 
			
		||||
    all::{ChannelId, GuildId},
 | 
			
		||||
    prelude::Context,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::web::{
 | 
			
		||||
    check_authorization, guards::transaction::Transaction, routes::JsonResult, string_opt,
 | 
			
		||||
    check_authorization,
 | 
			
		||||
    guards::transaction::Transaction,
 | 
			
		||||
    routes::{dashboard::check_channel_matches_guild, JsonResult},
 | 
			
		||||
    string_opt,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
pub struct CreateTodo {
 | 
			
		||||
    #[serde(with = "string_opt")]
 | 
			
		||||
    channel_id: Option<u64>,
 | 
			
		||||
    value: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
struct GetTodo {
 | 
			
		||||
pub struct GetTodo {
 | 
			
		||||
    id: u32,
 | 
			
		||||
    #[serde(with = "string_opt")]
 | 
			
		||||
    channel_id: Option<u64>,
 | 
			
		||||
    value: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
pub struct UpdateTodo {
 | 
			
		||||
    value: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[post("/api/guild/<id>/todos", data = "<todo>")]
 | 
			
		||||
pub async fn create_todo(
 | 
			
		||||
    id: u64,
 | 
			
		||||
@@ -37,6 +49,67 @@ pub async fn create_todo(
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    let guild_id = GuildId::new(id);
 | 
			
		||||
    if todo.value.len() > 2000 {
 | 
			
		||||
        return json_err!("Value too long");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match todo.channel_id {
 | 
			
		||||
        Some(channel_id) => {
 | 
			
		||||
            if !check_channel_matches_guild(ctx, ChannelId::new(channel_id), guild_id) {
 | 
			
		||||
                warn!("Channel {} not found for guild {}", channel_id, guild_id);
 | 
			
		||||
 | 
			
		||||
                return json_err!("Channel not found");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            sqlx::query!(
 | 
			
		||||
                "
 | 
			
		||||
                INSERT INTO todos (guild_id, channel_id, value)
 | 
			
		||||
                VALUES (
 | 
			
		||||
                    (SELECT id FROM guilds WHERE guild = ?),
 | 
			
		||||
                    (SELECT id FROM channels WHERE channel = ?),
 | 
			
		||||
                    ?
 | 
			
		||||
                )
 | 
			
		||||
                ",
 | 
			
		||||
                id,
 | 
			
		||||
                channel_id,
 | 
			
		||||
                todo.value
 | 
			
		||||
            )
 | 
			
		||||
            .execute(transaction.executor())
 | 
			
		||||
            .await
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                warn!("Error creating todo: {:?}", e);
 | 
			
		||||
                json!({"errors": vec!["Unknown error"]})
 | 
			
		||||
            })?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        None => {
 | 
			
		||||
            sqlx::query!(
 | 
			
		||||
                "
 | 
			
		||||
                INSERT INTO todos (guild_id, channel_id, value)
 | 
			
		||||
                VALUES (
 | 
			
		||||
                    (SELECT id FROM guilds WHERE guild = ?),
 | 
			
		||||
                    NULL,
 | 
			
		||||
                    ?
 | 
			
		||||
                )
 | 
			
		||||
                ",
 | 
			
		||||
                id,
 | 
			
		||||
                todo.value
 | 
			
		||||
            )
 | 
			
		||||
            .execute(transaction.executor())
 | 
			
		||||
            .await
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                warn!("Error creating todo: {:?}", e);
 | 
			
		||||
                json!({"errors": vec!["Unknown error"]})
 | 
			
		||||
            })?;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Err(e) = transaction.commit().await {
 | 
			
		||||
        warn!("Couldn't commit transaction: {:?}", e);
 | 
			
		||||
        return json_err!("Couldn't commit transaction.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(json!({}))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -75,26 +148,67 @@ pub async fn get_todo(
 | 
			
		||||
    Ok(json!(todos))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[patch("/api/guild/<id>/todos")]
 | 
			
		||||
#[patch("/api/guild/<guild_id>/todos/<todo_id>", data = "<todo>")]
 | 
			
		||||
pub async fn update_todo(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    guild_id: u64,
 | 
			
		||||
    todo_id: u64,
 | 
			
		||||
    todo: Json<UpdateTodo>,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    mut transaction: Transaction<'_>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), guild_id).await?;
 | 
			
		||||
 | 
			
		||||
    if todo.value.len() > 2000 {
 | 
			
		||||
        return json_err!("Value too long");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sqlx::query!(
 | 
			
		||||
        "
 | 
			
		||||
        UPDATE todos
 | 
			
		||||
        SET value = ?
 | 
			
		||||
        WHERE guild_id = ?
 | 
			
		||||
            AND id = ?
 | 
			
		||||
        ",
 | 
			
		||||
        todo.value,
 | 
			
		||||
        guild_id,
 | 
			
		||||
        todo_id,
 | 
			
		||||
    )
 | 
			
		||||
    .execute(transaction.executor())
 | 
			
		||||
    .await
 | 
			
		||||
    .map_err(|e| {
 | 
			
		||||
        warn!("Error updating todo: {:?}", e);
 | 
			
		||||
        json!({"errors": vec!["Unknown error"]})
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    Ok(json!({}))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[delete("/api/guild/<id>/todos")]
 | 
			
		||||
#[delete("/api/guild/<guild_id>/todos/<todo_id>")]
 | 
			
		||||
pub async fn delete_todo(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    guild_id: u64,
 | 
			
		||||
    todo_id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    mut transaction: Transaction<'_>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), guild_id).await?;
 | 
			
		||||
 | 
			
		||||
    sqlx::query!(
 | 
			
		||||
        "
 | 
			
		||||
        DELETE FROM todos
 | 
			
		||||
        WHERE guild_id = ?
 | 
			
		||||
            AND id = ?
 | 
			
		||||
        ",
 | 
			
		||||
        guild_id,
 | 
			
		||||
        todo_id,
 | 
			
		||||
    )
 | 
			
		||||
    .execute(transaction.executor())
 | 
			
		||||
    .await
 | 
			
		||||
    .map_err(|e| {
 | 
			
		||||
        warn!("Error deleting todo: {:?}", e);
 | 
			
		||||
        json!({"errors": vec!["Unknown error"]})
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    Ok(json!({}))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -388,23 +388,14 @@ pub(crate) async fn create_reminder(
 | 
			
		||||
        _ => {}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        // validate channel
 | 
			
		||||
        let channel = ChannelId::new(reminder.channel).to_channel_cached(&ctx.cache);
 | 
			
		||||
        let channel_exists = channel.is_some();
 | 
			
		||||
 | 
			
		||||
        let channel_matches_guild =
 | 
			
		||||
            channel.map_or(false, |c| c.guild(&ctx.cache).map_or(false, |c| c.id == guild_id));
 | 
			
		||||
 | 
			
		||||
        if !channel_matches_guild || !channel_exists {
 | 
			
		||||
    if !check_channel_matches_guild(ctx, ChannelId::new(reminder.channel), guild_id) {
 | 
			
		||||
        warn!(
 | 
			
		||||
                "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})",
 | 
			
		||||
                reminder.channel, guild_id, channel_exists
 | 
			
		||||
            "Error in `create_reminder`: channel {} not found for guild {}",
 | 
			
		||||
            reminder.channel, guild_id
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return Err(json!({"error": "Channel not found"}));
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let channel =
 | 
			
		||||
        create_database_channel(&ctx, ChannelId::new(reminder.channel), transaction).await;
 | 
			
		||||
@@ -601,6 +592,21 @@ pub(crate) async fn create_reminder(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn check_channel_matches_guild(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> bool {
 | 
			
		||||
    // validate channel
 | 
			
		||||
    let channel = channel_id.to_channel_cached(&ctx.cache);
 | 
			
		||||
    let channel_exists = channel.is_some();
 | 
			
		||||
 | 
			
		||||
    if !channel_exists {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let channel_matches_guild =
 | 
			
		||||
        channel.map_or(false, |c| c.guild(&ctx.cache).map_or(false, |g| g.id == guild_id));
 | 
			
		||||
 | 
			
		||||
    channel_matches_guild
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn create_database_channel(
 | 
			
		||||
    ctx: impl CacheHttp,
 | 
			
		||||
    channel: ChannelId,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user