mirror of
https://github.com/usatiuk/y.git
synced 2025-10-28 18:37:47 +01:00
messages!
This commit is contained in:
@@ -11,6 +11,7 @@ import { Login } from "./Login";
|
|||||||
import { Signup } from "./Signup";
|
import { Signup } from "./Signup";
|
||||||
import { Home } from "./Home";
|
import { Home } from "./Home";
|
||||||
import {
|
import {
|
||||||
|
chatAction,
|
||||||
homeAction,
|
homeAction,
|
||||||
loginAction,
|
loginAction,
|
||||||
newChatAction,
|
newChatAction,
|
||||||
@@ -68,6 +69,7 @@ const router = createBrowserRouter([
|
|||||||
path: "messages/chat/:id",
|
path: "messages/chat/:id",
|
||||||
element: <Chat />,
|
element: <Chat />,
|
||||||
loader: chatLoader,
|
loader: chatLoader,
|
||||||
|
action: chatAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "users",
|
path: "users",
|
||||||
|
|||||||
60
client/src/Chat.scss
Normal file
60
client/src/Chat.scss
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
@import "./common";
|
||||||
|
|
||||||
|
.chat {
|
||||||
|
width: 20rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 2rem 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
border-bottom: solid 1px #A0A0A0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :first-child {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatMessages {
|
||||||
|
display: flex;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 0.3rem;
|
||||||
|
flex-grow: 0;
|
||||||
|
@include border-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
//margin: 2rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
|
||||||
|
@include border-shadow;
|
||||||
|
border: solid gray 1px;
|
||||||
|
|
||||||
|
@include post-editor;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,59 @@
|
|||||||
import { useLoaderData } from "react-router-dom";
|
import { useFetcher, useLoaderData, useRevalidator } from "react-router-dom";
|
||||||
import { chatLoader, LoaderToType } from "./loaders";
|
import { chatLoader, LoaderToType } from "./loaders";
|
||||||
import { isError } from "./api/dto";
|
import { isError } from "./api/dto";
|
||||||
|
|
||||||
|
import "./Chat.scss";
|
||||||
|
import "./PostForm.scss";
|
||||||
|
import { Message } from "./Message";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function Chat() {
|
export function Chat() {
|
||||||
const loaderData = useLoaderData() as LoaderToType<typeof chatLoader>;
|
const loaderData = useLoaderData() as LoaderToType<typeof chatLoader>;
|
||||||
if (!loaderData || isError(loaderData)) {
|
|
||||||
|
if (!loaderData) {
|
||||||
return <div>error</div>;
|
return <div>error</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={"chat"}>{loaderData.name}</div>;
|
const chat = loaderData.chat;
|
||||||
|
const messages = loaderData.messages;
|
||||||
|
if (!chat || isError(chat) || !messages || isError(messages)) {
|
||||||
|
return <div>error</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revalidator = useRevalidator();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => revalidator.revalidate(), 1000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const busy = fetcher.state === "submitting";
|
||||||
|
|
||||||
|
const sortedMessages = messages.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"chat"}>
|
||||||
|
<div className={"chatHeader"}>{chat.name}</div>
|
||||||
|
<fetcher.Form className={"messageForm postForm"} method="post">
|
||||||
|
<textarea placeholder={"Write something!"} name="text" />
|
||||||
|
<input hidden={true} value={chat.id} name={"chatId"} />
|
||||||
|
<button
|
||||||
|
name="intent"
|
||||||
|
value="addMessage"
|
||||||
|
type="submit"
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
Write
|
||||||
|
</button>
|
||||||
|
</fetcher.Form>
|
||||||
|
<div className={"messages"}>
|
||||||
|
{sortedMessages.map((m) => (
|
||||||
|
<Message key={m.id} message={m} chat={chat} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
29
client/src/ChatCreate.scss
Normal file
29
client/src/ChatCreate.scss
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.chatsNew {
|
||||||
|
width: 20rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 2rem 0;
|
||||||
|
|
||||||
|
.errors {
|
||||||
|
margin: 2rem 0;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0.3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,41 @@
|
|||||||
import { Form, useLoaderData, useNavigation } from "react-router-dom";
|
import {
|
||||||
|
Form,
|
||||||
|
useActionData,
|
||||||
|
useLoaderData,
|
||||||
|
useNavigation,
|
||||||
|
} from "react-router-dom";
|
||||||
import { LoaderToType, newChatLoader } from "./loaders";
|
import { LoaderToType, newChatLoader } from "./loaders";
|
||||||
import { isError } from "./api/dto";
|
import { isError } from "./api/dto";
|
||||||
import { getTokenUserUuid } from "./api/utils";
|
import { getTokenUserUuid } from "./api/utils";
|
||||||
|
|
||||||
|
import "./ChatCreate.scss";
|
||||||
|
import { ActionToType, newChatAction } from "./actions";
|
||||||
|
|
||||||
export function ChatCreate() {
|
export function ChatCreate() {
|
||||||
const loaderData = useLoaderData() as LoaderToType<typeof newChatLoader>;
|
const loaderData = useLoaderData() as LoaderToType<typeof newChatLoader>;
|
||||||
if (!loaderData || isError(loaderData)) {
|
if (!loaderData || isError(loaderData)) {
|
||||||
return <div>error</div>;
|
return <div>error</div>;
|
||||||
}
|
}
|
||||||
|
let errors: JSX.Element[] = [];
|
||||||
|
|
||||||
|
const actionData = useActionData() as ActionToType<typeof newChatAction>;
|
||||||
|
|
||||||
|
if (isError(actionData)) {
|
||||||
|
errors = actionData.errors.map((e) => {
|
||||||
|
return <a>{e}</a>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const busy = navigation.state === "submitting";
|
const busy = navigation.state === "submitting";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"chatsNew"}>
|
<div className={"chatsNew"}>
|
||||||
|
<div className={"errors"}>{errors}</div>
|
||||||
<Form method="post">
|
<Form method="post">
|
||||||
<label htmlFor="fname">name:</label>
|
<label htmlFor="fname">Chat name:</label>
|
||||||
<input type="text" name="name" />
|
<input type="text" name="name" />
|
||||||
|
<label htmlFor="members">Members:</label>
|
||||||
<select multiple name={"members"}>
|
<select multiple name={"members"}>
|
||||||
{loaderData
|
{loaderData
|
||||||
.filter((p) => p.uuid != getTokenUserUuid())
|
.filter((p) => p.uuid != getTokenUserUuid())
|
||||||
|
|||||||
@@ -1,3 +1,46 @@
|
|||||||
|
@import "./common";
|
||||||
|
|
||||||
.chats {
|
.chats {
|
||||||
|
width: 20rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 2rem 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatsHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
border-bottom: solid 1px #A0A0A0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :first-child {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatsList {
|
||||||
|
display: flex;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 0.3rem;
|
||||||
|
flex-grow: 0;
|
||||||
|
@include border-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export function Chats() {
|
|||||||
return (
|
return (
|
||||||
<div className={"chats"}>
|
<div className={"chats"}>
|
||||||
<div className={"chatsHeader"}>
|
<div className={"chatsHeader"}>
|
||||||
|
<span>Your chats</span>
|
||||||
<Link to={"../messages/chats/new"}>create</Link>
|
<Link to={"../messages/chats/new"}>create</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={"chatsList"}>
|
<div className={"chatsList"}>
|
||||||
|
|||||||
79
client/src/Message.scss
Normal file
79
client/src/Message.scss
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
@import "./common";
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 7px;
|
||||||
|
resize: none;
|
||||||
|
margin: 1rem 0;
|
||||||
|
min-height: 3rem;
|
||||||
|
|
||||||
|
@include border-shadow;
|
||||||
|
|
||||||
|
&.messageEditing {
|
||||||
|
padding: 0;
|
||||||
|
min-height: 6rem;
|
||||||
|
border: 1px solid #D0D0D0;
|
||||||
|
|
||||||
|
@include post-editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
margin-bottom: -0.3rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #A0A0A0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex: auto;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex-grow: 1;
|
||||||
|
align-self: start;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-self: end;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
client/src/Message.tsx
Normal file
50
client/src/Message.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import "./Message.scss";
|
||||||
|
import { TChatTo, TMessageTo } from "./api/dto";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export function Message({
|
||||||
|
message,
|
||||||
|
chat,
|
||||||
|
}: {
|
||||||
|
message: TMessageTo;
|
||||||
|
chat: TChatTo;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={"message"}>
|
||||||
|
<span className={"text"}>{message.contents}</span>
|
||||||
|
<div className={"footer"}>
|
||||||
|
<div className={"info"}>
|
||||||
|
<span className={"createdDate"}>
|
||||||
|
{new Date(message.createdAt * 1000).toUTCString()}
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
className={"authorLink"}
|
||||||
|
to={"/home/profile/" + message.authorUsername}
|
||||||
|
>
|
||||||
|
by {message.authorUsername}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{/*{actions && (*/}
|
||||||
|
{/* <div className={"actions"}>*/}
|
||||||
|
{/* {<button onClick={() => setEditing(true)}>edit</button>}*/}
|
||||||
|
{/* <Form method={"delete"}>*/}
|
||||||
|
{/* <input*/}
|
||||||
|
{/* hidden={true}*/}
|
||||||
|
{/* name={"postToDeleteId"}*/}
|
||||||
|
{/* value={id}*/}
|
||||||
|
{/* />*/}
|
||||||
|
{/* <button*/}
|
||||||
|
{/* name="intent"*/}
|
||||||
|
{/* value="deletePost"*/}
|
||||||
|
{/* type={"submit"}*/}
|
||||||
|
{/* disabled={busy}*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* delete*/}
|
||||||
|
{/* </button>*/}
|
||||||
|
{/* </Form>*/}
|
||||||
|
{/* </div>*/}
|
||||||
|
{/*)}*/}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function Messages() {
|
|
||||||
return <a>Messages</a>;
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import { isError } from "./api/dto";
|
|||||||
import { deleteToken, getTokenUserUuid, setToken } from "./api/utils";
|
import { deleteToken, getTokenUserUuid, setToken } from "./api/utils";
|
||||||
import { createPost, deletePost, updatePost } from "./api/Post";
|
import { createPost, deletePost, updatePost } from "./api/Post";
|
||||||
import { createChat } from "./api/Chat";
|
import { createChat } from "./api/Chat";
|
||||||
|
import { addMessagesToChat } from "./api/Message";
|
||||||
|
|
||||||
export type ActionToType<T extends (...args: any) => any> =
|
export type ActionToType<T extends (...args: any) => any> =
|
||||||
| Exclude<Awaited<ReturnType<T>>, Response>
|
| Exclude<Awaited<ReturnType<T>>, Response>
|
||||||
@@ -89,8 +90,19 @@ export async function userListAction({ request }: ActionFunctionArgs) {
|
|||||||
|
|
||||||
export async function newChatAction({ request }: ActionFunctionArgs) {
|
export async function newChatAction({ request }: ActionFunctionArgs) {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
return await createChat(formData.get("name")!.toString(), [
|
const ret = await createChat(formData.get("name")!.toString(), [
|
||||||
...formData.getAll("members")!.map((p) => p.toString()),
|
...formData.getAll("members")!.map((p) => p.toString()),
|
||||||
getTokenUserUuid()!,
|
getTokenUserUuid()!,
|
||||||
]);
|
]);
|
||||||
|
if (ret && !isError(ret)) {
|
||||||
|
return redirect("/home/messages/chat/" + ret.id);
|
||||||
|
} else return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function chatAction({ request }: ActionFunctionArgs) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
return await addMessagesToChat(
|
||||||
|
Number(formData.get("chatId")!.toString()),
|
||||||
|
formData.get("text")!.toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
22
client/src/api/Message.ts
Normal file
22
client/src/api/Message.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
MessagesToResp,
|
||||||
|
MessageToResp,
|
||||||
|
TMessagesToResp,
|
||||||
|
TMessageToResp,
|
||||||
|
} from "./dto";
|
||||||
|
import { fetchJSONAuth } from "./utils";
|
||||||
|
|
||||||
|
export async function getMessagesByChat(
|
||||||
|
chatId: number,
|
||||||
|
): Promise<TMessagesToResp> {
|
||||||
|
return fetchJSONAuth("/message/by-chat/" + chatId, "GET", MessagesToResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addMessagesToChat(
|
||||||
|
chatId: number,
|
||||||
|
messageContents: string,
|
||||||
|
): Promise<TMessageToResp> {
|
||||||
|
return fetchJSONAuth("/message/by-chat/" + chatId, "POST", MessageToResp, {
|
||||||
|
contents: messageContents,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -79,19 +79,23 @@ export const MessageTo = z.object({
|
|||||||
id: number(),
|
id: number(),
|
||||||
chatId: z.number(),
|
chatId: z.number(),
|
||||||
authorUuid: z.string(),
|
authorUuid: z.string(),
|
||||||
|
authorUsername: z.string(),
|
||||||
contents: z.string(),
|
contents: z.string(),
|
||||||
|
createdAt: z.number(),
|
||||||
});
|
});
|
||||||
export type TMessageTo = z.infer<typeof MessageTo>;
|
export type TMessageTo = z.infer<typeof MessageTo>;
|
||||||
|
|
||||||
export const MessageToResp = CreateAPIResponse(MessageTo);
|
export const MessageToResp = CreateAPIResponse(MessageTo);
|
||||||
export type TMessageToResp = z.infer<typeof MessageToResp>;
|
export type TMessageToResp = z.infer<typeof MessageToResp>;
|
||||||
|
|
||||||
|
export const MessagesToResp = CreateAPIResponse(z.array(MessageTo));
|
||||||
|
export type TMessagesToResp = z.infer<typeof MessagesToResp>;
|
||||||
|
|
||||||
export const ChatTo = z.object({
|
export const ChatTo = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
creatorUuid: z.string(),
|
creatorUuid: z.string(),
|
||||||
members: z.array(PersonTo),
|
memberCount: z.number(),
|
||||||
messages: z.array(MessageTo),
|
|
||||||
});
|
});
|
||||||
export type TChatTo = z.infer<typeof ChatTo>;
|
export type TChatTo = z.infer<typeof ChatTo>;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
getPostsByFollowees,
|
getPostsByFollowees,
|
||||||
} from "./api/Post";
|
} from "./api/Post";
|
||||||
import { getChat, getMyChats } from "./api/Chat";
|
import { getChat, getMyChats } from "./api/Chat";
|
||||||
|
import { getMessagesByChat } from "./api/Message";
|
||||||
|
|
||||||
export type LoaderToType<T extends (...args: any) => any> =
|
export type LoaderToType<T extends (...args: any) => any> =
|
||||||
| Exclude<Awaited<ReturnType<T>>, Response>
|
| Exclude<Awaited<ReturnType<T>>, Response>
|
||||||
@@ -86,5 +87,8 @@ export async function newChatLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function chatLoader({ params }: { params: { id?: number } }) {
|
export async function chatLoader({ params }: { params: { id?: number } }) {
|
||||||
return getChat(params.id!);
|
return {
|
||||||
|
chat: await getChat(params.id!),
|
||||||
|
messages: await getMessagesByChat(params.id!),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ public class ChatController {
|
|||||||
if (Arrays.stream(chatCreateTo.memberUuids()).noneMatch(n -> Objects.equals(n, principal.getName())))
|
if (Arrays.stream(chatCreateTo.memberUuids()).noneMatch(n -> Objects.equals(n, principal.getName())))
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Creator of chat must be its member");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Creator of chat must be its member");
|
||||||
|
|
||||||
|
if (chatCreateTo.memberUuids().length <= 1)
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Chat must have members other than its creator");
|
||||||
|
|
||||||
chat.setCreator(entityManager.getReference(Person.class, principal.getName()));
|
chat.setCreator(entityManager.getReference(Person.class, principal.getName()));
|
||||||
chat.setMembers(Arrays.stream(chatCreateTo.memberUuids()).map(
|
chat.setMembers(Arrays.stream(chatCreateTo.memberUuids()).map(
|
||||||
p -> entityManager.getReference(Person.class, p)
|
p -> entityManager.getReference(Person.class, p)
|
||||||
|
|||||||
@@ -1,10 +1,55 @@
|
|||||||
package com.usatiuk.tjv.y.server.controller;
|
package com.usatiuk.tjv.y.server.controller;
|
||||||
|
|
||||||
|
import com.usatiuk.tjv.y.server.dto.MessageCreateTo;
|
||||||
|
import com.usatiuk.tjv.y.server.dto.MessageTo;
|
||||||
|
import com.usatiuk.tjv.y.server.dto.converters.MessageMapper;
|
||||||
|
import com.usatiuk.tjv.y.server.entity.Message;
|
||||||
|
import com.usatiuk.tjv.y.server.entity.Person;
|
||||||
|
import com.usatiuk.tjv.y.server.service.ChatService;
|
||||||
|
import com.usatiuk.tjv.y.server.service.MessageService;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(value = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
|
@RequestMapping(value = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public class MessageController {
|
public class MessageController {
|
||||||
|
private final ChatService chatService;
|
||||||
|
private final EntityManager entityManager;
|
||||||
|
private final MessageMapper messageMapper;
|
||||||
|
private final MessageService messageService;
|
||||||
|
|
||||||
|
public MessageController(ChatService chatService, EntityManager entityManager, MessageMapper messageMapper, MessageService messageService) {
|
||||||
|
this.chatService = chatService;
|
||||||
|
this.entityManager = entityManager;
|
||||||
|
this.messageMapper = messageMapper;
|
||||||
|
this.messageService = messageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/by-chat/{chatTd}")
|
||||||
|
public Stream<MessageTo> get(Principal principal, @PathVariable Long chatTd) {
|
||||||
|
var chat = chatService.readById(chatTd).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
|
||||||
|
var userRef = entityManager.getReference(Person.class, principal.getName());
|
||||||
|
if (!chat.getMembers().contains(userRef))
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't member of the chat");
|
||||||
|
|
||||||
|
return chat.getMessages().stream().map(messageMapper::makeDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/by-chat/{chatId}")
|
||||||
|
public MessageTo post(Principal principal, @PathVariable Long chatId, @RequestBody MessageCreateTo messageCreateTo) {
|
||||||
|
var chat = chatService.readById(chatId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
|
||||||
|
var userRef = entityManager.getReference(Person.class, principal.getName());
|
||||||
|
if (!chat.getMembers().contains(userRef))
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't member of the chat");
|
||||||
|
|
||||||
|
Message message = new Message().setChat(chat).setAuthor(userRef).setContents(messageCreateTo.contents());
|
||||||
|
messageService.create(message);
|
||||||
|
return messageMapper.makeDto(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.usatiuk.tjv.y.server.dto;
|
package com.usatiuk.tjv.y.server.dto;
|
||||||
|
|
||||||
import java.util.Collection;
|
public record ChatTo(Long id, String name, String creatorUuid, Long memberCount) {
|
||||||
|
|
||||||
public record ChatTo(Long id, String name, String creatorUuid, Collection<PersonTo> members,
|
|
||||||
Collection<MessageTo> messages) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.usatiuk.tjv.y.server.dto;
|
||||||
|
|
||||||
|
public record MessageCreateTo(String contents) {
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
package com.usatiuk.tjv.y.server.dto;
|
package com.usatiuk.tjv.y.server.dto;
|
||||||
|
|
||||||
public record MessageTo(Long id, Long chatId, String authorUuid, String contents) {
|
public record MessageTo(Long id, Long chatId, String authorUuid, String authorUsername, String contents,
|
||||||
|
Long createdAt) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,7 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class ChatMapper {
|
public class ChatMapper {
|
||||||
|
|
||||||
private final PersonMapper personMapper;
|
|
||||||
private final MessageMapper messageMapper;
|
|
||||||
|
|
||||||
public ChatMapper(PersonMapper personMapper, MessageMapper messageMapper) {
|
|
||||||
this.personMapper = personMapper;
|
|
||||||
this.messageMapper = messageMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatTo makeDto(Chat chat) {
|
public ChatTo makeDto(Chat chat) {
|
||||||
return new ChatTo(chat.getId(), chat.getName(), chat.getCreator().getUuid(),
|
return new ChatTo(chat.getId(), chat.getName(), chat.getCreator().getUuid(), (long) chat.getMembers().size());
|
||||||
chat.getMembers().stream().map(personMapper::makeDto).toList(),
|
|
||||||
chat.getMessages().stream().map(messageMapper::makeDto).toList());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ import org.springframework.stereotype.Component;
|
|||||||
public class MessageMapper {
|
public class MessageMapper {
|
||||||
public MessageTo makeDto(Message message) {
|
public MessageTo makeDto(Message message) {
|
||||||
return new MessageTo(message.getId(), message.getChat().getId(),
|
return new MessageTo(message.getId(), message.getChat().getId(),
|
||||||
message.getAuthor().getUuid(), message.getContents());
|
message.getAuthor().getUuid(), message.getAuthor().getUsername(), message.getContents(), message.getCreatedAt().getEpochSecond());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
@@ -25,6 +28,9 @@ public class Message implements EntityWithId<Long> {
|
|||||||
@ManyToOne
|
@ManyToOne
|
||||||
private Person author;
|
private Person author;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
@Lob
|
@Lob
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String contents;
|
private String contents;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
jwt.secret=JKLASJKLASJKLJHKLDFAHJKFDSHJKFJHKDSHJKFHJKSDFJHKSDJHKFJHKS98346783467899782345jkhgsdoigh938g
|
jwt.secret=JKLASJKLASJKLJHKLDFAHJKFDSHJKFJHKDSHJKFHJKSDFJHKSDJHKFJHKS98346783467899782345jkhgsdoigh938g
|
||||||
logging.level.root=DEBUG
|
logging.level.root=DEBUG
|
||||||
logging.level.org.springframework.security=DEBUG
|
logging.level.org.springframework.security=DEBUG
|
||||||
|
spring.datasource.url=jdbc:h2:file:~/tjvserver.h2
|
||||||
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
Reference in New Issue
Block a user