diff --git a/client/src/App.tsx b/client/src/App.tsx
index 5885044..ca6dd51 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -12,6 +12,7 @@ import { Signup } from "./Signup";
import { Home } from "./Home";
import {
chatAction,
+ editChatAction,
homeAction,
loginAction,
newChatAction,
@@ -22,6 +23,7 @@ import {
import {
chatListLoader,
chatLoader,
+ editChatLoader,
feedLoader,
homeLoader,
newChatLoader,
@@ -34,6 +36,7 @@ import { UserList } from "./UserList";
import { Chats } from "./Chats";
import { ChatCreate } from "./ChatCreate";
import { Chat } from "./Chat";
+import { ChatEdit } from "./ChatEdit";
const router = createBrowserRouter([
{
@@ -71,6 +74,12 @@ const router = createBrowserRouter([
loader: chatLoader,
action: chatAction,
},
+ {
+ path: "messages/chat/:id/edit",
+ element: ,
+ loader: editChatLoader,
+ action: editChatAction,
+ },
{
path: "users",
element: ,
diff --git a/client/src/Chat.scss b/client/src/Chat.scss
index d6aaacc..6a5415f 100644
--- a/client/src/Chat.scss
+++ b/client/src/Chat.scss
@@ -27,6 +27,10 @@
margin-right: 1rem;
}
+ .chatHeaderActions {
+ flex-grow: 0;
+ }
+
margin-bottom: 1rem;
padding-bottom: 0.2rem;
}
diff --git a/client/src/Chat.tsx b/client/src/Chat.tsx
index c2d5d90..bb7d652 100644
--- a/client/src/Chat.tsx
+++ b/client/src/Chat.tsx
@@ -1,4 +1,9 @@
-import { useFetcher, useLoaderData, useRevalidator } from "react-router-dom";
+import {
+ Link,
+ useFetcher,
+ useLoaderData,
+ useRevalidator,
+} from "react-router-dom";
import { chatLoader, LoaderToType } from "./loaders";
import { isError } from "./api/dto";
@@ -37,7 +42,14 @@ export function Chat() {
return (
-
{chat.name}
+
+ {chat.name}
+ {chat.creatorUuid == getTokenUserUuid() && (
+
+ edit
+
+ )}
+
diff --git a/client/src/ChatEdit.tsx b/client/src/ChatEdit.tsx
new file mode 100644
index 0000000..14b335e
--- /dev/null
+++ b/client/src/ChatEdit.tsx
@@ -0,0 +1,86 @@
+import {
+ Form,
+ useActionData,
+ useLoaderData,
+ useNavigation,
+} from "react-router-dom";
+import { editChatLoader, LoaderToType } from "./loaders";
+import { isError } from "./api/dto";
+import { ActionToType, editChatAction } from "./actions";
+import { getTokenUserUuid } from "./api/utils";
+
+import "./ChatCreate.scss";
+
+export function ChatEdit() {
+ const loaderData = useLoaderData() as LoaderToType;
+
+ if (!loaderData) return error
;
+
+ const { people, chat, chatMembers } = loaderData;
+
+ if (
+ !people ||
+ isError(people) ||
+ !chat ||
+ isError(chat) ||
+ !chatMembers ||
+ isError(chatMembers)
+ ) {
+ return error
;
+ }
+ let errors: JSX.Element[] = [];
+
+ const actionData = useActionData() as ActionToType;
+
+ if (isError(actionData)) {
+ errors = actionData.errors.map((e) => {
+ return {e};
+ });
+ }
+
+ const navigation = useNavigation();
+ const busy = navigation.state === "submitting";
+
+ return (
+
+ );
+}
diff --git a/client/src/actions.ts b/client/src/actions.ts
index 5635352..c13a50d 100644
--- a/client/src/actions.ts
+++ b/client/src/actions.ts
@@ -10,7 +10,7 @@ import { login } from "./api/Token";
import { isError } from "./api/dto";
import { deleteToken, getTokenUserUuid, setToken } from "./api/utils";
import { createPost, deletePost, updatePost } from "./api/Post";
-import { createChat } from "./api/Chat";
+import { createChat, deleteChat, updateChat } from "./api/Chat";
import { addMessagesToChat, deleteMessage, editMessage } from "./api/Message";
export type ActionToType any> =
@@ -115,6 +115,33 @@ export async function newChatAction({ request }: ActionFunctionArgs) {
} else return ret;
}
+export async function editChatAction({ request }: ActionFunctionArgs) {
+ const formData = await request.formData();
+ const intent = formData.get("intent")!.toString();
+ if (intent == "update") {
+ const ret = await updateChat(
+ Number(formData.get("chatId")!.toString()),
+ formData.get("name")!.toString(),
+ [
+ ...formData.getAll("members")!.map((p) => p.toString()),
+ getTokenUserUuid()!,
+ ],
+ );
+
+ if (ret && !isError(ret)) {
+ return redirect("/home/messages/chat/" + ret.id);
+ } else return ret;
+ } else if (intent == "delete") {
+ const ret = await deleteChat(
+ Number(formData.get("chatId")!.toString()),
+ );
+
+ if (ret && !isError(ret)) {
+ return redirect("/home/messages/chats");
+ } else return ret;
+ }
+}
+
export async function chatAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent")!.toString();
diff --git a/client/src/api/Chat.ts b/client/src/api/Chat.ts
index a2028eb..b66713b 100644
--- a/client/src/api/Chat.ts
+++ b/client/src/api/Chat.ts
@@ -1,5 +1,14 @@
import { fetchJSONAuth } from "./utils";
-import { ChatsToResp, ChatToResp, TChatsToResp, TChatToResp } from "./dto";
+import {
+ ChatsToResp,
+ ChatToResp,
+ NoContentToResp,
+ PersonToArrResp,
+ TChatsToResp,
+ TChatToResp,
+ TNoContentToResp,
+ TPersonToArrResp,
+} from "./dto";
export async function getMyChats(): Promise {
return fetchJSONAuth("/chat/my", "GET", ChatsToResp);
@@ -9,9 +18,32 @@ export async function getChat(id: number): Promise {
return fetchJSONAuth("/chat/by-id/" + id, "GET", ChatToResp);
}
+export async function getMembers(id: number): Promise {
+ return fetchJSONAuth(
+ "/chat/by-id/" + id + "/members",
+ "GET",
+ PersonToArrResp,
+ );
+}
+
export async function createChat(
name: string,
memberUuids: string[],
): Promise {
return fetchJSONAuth("/chat", "POST", ChatToResp, { name, memberUuids });
}
+
+export async function updateChat(
+ id: number,
+ name: string,
+ memberUuids: string[],
+): Promise {
+ return fetchJSONAuth("/chat/by-id/" + id, "PATCH", ChatToResp, {
+ name,
+ memberUuids,
+ });
+}
+
+export async function deleteChat(id: number): Promise {
+ return fetchJSONAuth("/chat/by-id/" + id, "DELETE", NoContentToResp);
+}
diff --git a/client/src/loaders.ts b/client/src/loaders.ts
index 890ec5a..398890f 100644
--- a/client/src/loaders.ts
+++ b/client/src/loaders.ts
@@ -12,7 +12,7 @@ import {
getPostsByAuthorUuid,
getPostsByFollowees,
} from "./api/Post";
-import { getChat, getMyChats } from "./api/Chat";
+import { getChat, getMembers, getMyChats } from "./api/Chat";
import { getMessagesByChat } from "./api/Message";
export type LoaderToType any> =
@@ -86,6 +86,14 @@ export async function newChatLoader() {
return await getAllPerson();
}
+export async function editChatLoader({ params }: { params: { id?: number } }) {
+ return {
+ people: await getAllPerson(),
+ chat: await getChat(params.id!),
+ chatMembers: await getMembers(params.id!),
+ };
+}
+
export async function chatLoader({ params }: { params: { id?: number } }) {
return {
chat: await getChat(params.id!),
diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ChatController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ChatController.java
index cd83d07..15432f9 100644
--- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ChatController.java
+++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ChatController.java
@@ -2,7 +2,9 @@ package com.usatiuk.tjv.y.server.controller;
import com.usatiuk.tjv.y.server.dto.ChatCreateTo;
import com.usatiuk.tjv.y.server.dto.ChatTo;
+import com.usatiuk.tjv.y.server.dto.PersonTo;
import com.usatiuk.tjv.y.server.dto.converters.ChatMapper;
+import com.usatiuk.tjv.y.server.dto.converters.PersonMapper;
import com.usatiuk.tjv.y.server.entity.Chat;
import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.service.ChatService;
@@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.security.Principal;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;
@@ -23,11 +26,13 @@ public class ChatController {
private final EntityManager entityManager;
private final ChatService chatService;
private final ChatMapper chatMapper;
+ private final PersonMapper personMapper;
- public ChatController(EntityManager entityManager, ChatService chatService, ChatMapper chatMapper) {
+ public ChatController(EntityManager entityManager, ChatService chatService, ChatMapper chatMapper, PersonMapper personMapper) {
this.entityManager = entityManager;
this.chatService = chatService;
this.chatMapper = chatMapper;
+ this.personMapper = personMapper;
}
@PostMapping
@@ -68,11 +73,41 @@ public class ChatController {
chatService.deleteById(id);
}
+ @PatchMapping(path = "/by-id/{id}")
+ public ChatTo update(Principal principal, @PathVariable Long id, @RequestBody ChatCreateTo chatCreateTo) {
+ var chat = chatService.readById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
+ if (!Objects.equals(chat.getCreator().getUuid(), principal.getName()))
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User isn't creator of the chat");
+
+ 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");
+
+ if (chatCreateTo.memberUuids().length <= 1)
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Chat must have members other than its creator");
+
+ chat.setMembers(new ArrayList<>(Arrays.stream(chatCreateTo.memberUuids()).map(
+ p -> entityManager.getReference(Person.class, p)
+ ).toList()));
+ chat.setName(chatCreateTo.name());
+
+ chatService.update(chat);
+ return chatMapper.makeDto(chat);
+ }
+
@GetMapping(path = "/my")
public Stream getMy(Principal principal) {
return chatService.readByMember(principal.getName()).stream().map(chatMapper::makeDto);
}
+ @GetMapping(path = "/by-id/{id}/members")
+ public Stream getMembers(Principal principal, @PathVariable Long id) {
+ var chat = chatService.readById(id).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.getMembers().stream().map(personMapper::makeDto);
+ }
+
}