From 545b3094403ca925f9c22017adf5610bece1ce2f Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:28:40 +0800 Subject: [PATCH 01/16] feat: deleteUser, quitRoom & getMessageByUser --- database/graphql/message.graphql | 11 +++++++++++ database/graphql/room.graphql | 6 ++++++ database/graphql/user.graphql | 6 ++++++ 3 files changed, 23 insertions(+) diff --git a/database/graphql/message.graphql b/database/graphql/message.graphql index 994647c..40c0f2c 100644 --- a/database/graphql/message.graphql +++ b/database/graphql/message.graphql @@ -15,3 +15,14 @@ subscription getMessagesByRoom($room_uuid: uuid!) { created_at } } + +subscription getMessagesByUser($user_uuid: uuid!) { + message(where: {user_uuid: {_eq: $user_uuid}}) { + uuid + room { + uuid + } + content + created_at + } +} \ No newline at end of file diff --git a/database/graphql/room.graphql b/database/graphql/room.graphql index 4f37de0..3b61404 100644 --- a/database/graphql/room.graphql +++ b/database/graphql/room.graphql @@ -28,3 +28,9 @@ mutation joinRoom($user_uuid: uuid!, $room_uuid: uuid!) { room_uuid } } + +mutation quitRoom($user_uuid: uuid!, $room_uuid: uuid!) { + delete_user_room(where: {user_uuid: {_eq: $user_uuid}, room_uuid: {_eq: $room_uuid}}) { + affected_rows + } +} \ No newline at end of file diff --git a/database/graphql/user.graphql b/database/graphql/user.graphql index d7780cd..bc1a511 100644 --- a/database/graphql/user.graphql +++ b/database/graphql/user.graphql @@ -10,3 +10,9 @@ query getUsersByUsername($username: String!) { password } } + +mutation deleteUser($uuid: uuid!) { + delete_user_by_pk(uuid: $uuid) { + uuid + } +} \ No newline at end of file From fda7c0cf6b29f93df550b532e24496584dbbee35 Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:42:04 +0800 Subject: [PATCH 02/16] feat: delete file & delete user --- backend/src/file.ts | 19 ++++++++++++++ backend/src/graphql.ts | 59 ++++++++++++++++++++++++++++++++++++++++++ backend/src/user.ts | 17 ++++++++++++ 3 files changed, 95 insertions(+) diff --git a/backend/src/file.ts b/backend/src/file.ts index 7e77582..35bdda4 100644 --- a/backend/src/file.ts +++ b/backend/src/file.ts @@ -74,4 +74,23 @@ router.get("/download", authenticate, (req, res) => { } }); +router.post("/delete", authenticate, (req, res) => { + const { room, filename } = req.body; + if (!room || !filename) { + return res.status(422).send("422 Unprocessable Entity: Missing room or filename"); + } + const dir = path.resolve(baseDir, room, filename); + try { + if (fs.existsSync(dir)) { + fs.unlinkSync(dir); + return res.send("File deleted successfully"); + } else { + return res.status(404).send("404 Not Found: File does not exist"); + } + } catch (err) { + console.error(err); + return res.sendStatus(500); + } +}) + export default router; diff --git a/backend/src/graphql.ts b/backend/src/graphql.ts index 8e65cb6..03ca976 100644 --- a/backend/src/graphql.ts +++ b/backend/src/graphql.ts @@ -1508,6 +1508,13 @@ export type GetMessagesByRoomSubscriptionVariables = Exact<{ export type GetMessagesByRoomSubscription = { __typename?: 'subscription_root', message: Array<{ __typename?: 'message', uuid: any, content: string, created_at: any, user: { __typename?: 'user', uuid: any, username: string } }> }; +export type GetMessagesByUserSubscriptionVariables = Exact<{ + user_uuid: Scalars['uuid']['input']; +}>; + + +export type GetMessagesByUserSubscription = { __typename?: 'subscription_root', message: Array<{ __typename?: 'message', uuid: any, content: string, created_at: any, room: { __typename?: 'room', uuid: any } }> }; + export type AddRoomMutationVariables = Exact<{ name: Scalars['String']['input']; intro: Scalars['String']['input']; @@ -1539,6 +1546,14 @@ export type JoinRoomMutationVariables = Exact<{ export type JoinRoomMutation = { __typename?: 'mutation_root', insert_user_room_one?: { __typename?: 'user_room', user_uuid: any, room_uuid: any } | null }; +export type QuitRoomMutationVariables = Exact<{ + user_uuid: Scalars['uuid']['input']; + room_uuid: Scalars['uuid']['input']; +}>; + + +export type QuitRoomMutation = { __typename?: 'mutation_root', delete_user_room?: { __typename?: 'user_room_mutation_response', affected_rows: number } | null }; + export type AddUserMutationVariables = Exact<{ username: Scalars['String']['input']; password: Scalars['String']['input']; @@ -1554,6 +1569,13 @@ export type GetUsersByUsernameQueryVariables = Exact<{ export type GetUsersByUsernameQuery = { __typename?: 'query_root', user: Array<{ __typename?: 'user', uuid: any, password: string }> }; +export type DeleteUserMutationVariables = Exact<{ + uuid: Scalars['uuid']['input']; +}>; + + +export type DeleteUserMutation = { __typename?: 'mutation_root', delete_user_by_pk?: { __typename?: 'user', uuid: any } | null }; + export const AddMessageDocument = gql` mutation addMessage($user_uuid: uuid!, $room_uuid: uuid!, $content: String!) { @@ -1577,6 +1599,18 @@ export const GetMessagesByRoomDocument = gql` } } `; +export const GetMessagesByUserDocument = gql` + subscription getMessagesByUser($user_uuid: uuid!) { + message(where: {user_uuid: {_eq: $user_uuid}}) { + uuid + room { + uuid + } + content + created_at + } +} + `; export const AddRoomDocument = gql` mutation addRoom($name: String!, $intro: String!, $invite_code: String!) { insert_room_one(object: {name: $name, intro: $intro, invite_code: $invite_code}) { @@ -1612,6 +1646,15 @@ export const JoinRoomDocument = gql` } } `; +export const QuitRoomDocument = gql` + mutation quitRoom($user_uuid: uuid!, $room_uuid: uuid!) { + delete_user_room( + where: {user_uuid: {_eq: $user_uuid}, room_uuid: {_eq: $room_uuid}} + ) { + affected_rows + } +} + `; export const AddUserDocument = gql` mutation addUser($username: String!, $password: String!) { insert_user_one(object: {username: $username, password: $password}) { @@ -1627,6 +1670,13 @@ export const GetUsersByUsernameDocument = gql` } } `; +export const DeleteUserDocument = gql` + mutation deleteUser($uuid: uuid!) { + delete_user_by_pk(uuid: $uuid) { + uuid + } +} + `; export type SdkFunctionWrapper = (action: (requestHeaders?:Record) => Promise, operationName: string, operationType?: string, variables?: any) => Promise; @@ -1641,6 +1691,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getMessagesByRoom(variables: GetMessagesByRoomSubscriptionVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetMessagesByRoomDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getMessagesByRoom', 'subscription', variables); }, + getMessagesByUser(variables: GetMessagesByUserSubscriptionVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetMessagesByUserDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getMessagesByUser', 'subscription', variables); + }, addRoom(variables: AddRoomMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(AddRoomDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'addRoom', 'mutation', variables); }, @@ -1653,11 +1706,17 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = joinRoom(variables: JoinRoomMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(JoinRoomDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'joinRoom', 'mutation', variables); }, + quitRoom(variables: QuitRoomMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(QuitRoomDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'quitRoom', 'mutation', variables); + }, addUser(variables: AddUserMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(AddUserDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'addUser', 'mutation', variables); }, getUsersByUsername(variables: GetUsersByUsernameQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetUsersByUsernameDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getUsersByUsername', 'query', variables); + }, + deleteUser(variables: DeleteUserMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(DeleteUserDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteUser', 'mutation', variables); } }; } diff --git a/backend/src/user.ts b/backend/src/user.ts index a93bd91..7547e9c 100644 --- a/backend/src/user.ts +++ b/backend/src/user.ts @@ -1,6 +1,7 @@ import express from "express"; import jwt from "jsonwebtoken"; import { sdk as graphql } from "./index"; +import authenticate from "./authenticate"; interface userJWTPayload { uuid: string; @@ -71,4 +72,20 @@ router.post("/register", async (req, res) => { } }); +router.get("/delete", authenticate, (req, res) => { + const authHeader = req.get("Authorization"); + const token = authHeader!.substring(7); + const decoded = jwt.decode(token) as userJWTPayload; + const uuid = decoded?.uuid; + if (!uuid) { + return res.status(422).send("422 Unprocessable Entity: Missing uuid"); + } + graphql.deleteUser({ uuid: uuid as string }).then(() => { + return res.sendStatus(200); + }).catch((err) => { + console.error(err); + return res.sendStatus(500); + }); +}) + export default router; From 1267771f3840a4e5c5699ccb3a1d3a333ebfd42d Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Wed, 27 Aug 2025 00:01:00 +0800 Subject: [PATCH 03/16] feat: reply --- database/.local.env.template | 2 +- database/graphql/reply.graphql | 5 +++ database/hasura/hasura_metadata.json | 63 +++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 database/graphql/reply.graphql diff --git a/database/.local.env.template b/database/.local.env.template index 89074f3..992be1d 100644 --- a/database/.local.env.template +++ b/database/.local.env.template @@ -1,2 +1,2 @@ HASURA_GRAPHQL_ENDPOINT=
:/v1/graphql -HASURA_GRAPHQL_ADMIN_SECRET= +tHASURA_GRAPHQL_ADMIN_SECRET= diff --git a/database/graphql/reply.graphql b/database/graphql/reply.graphql new file mode 100644 index 0000000..c35215c --- /dev/null +++ b/database/graphql/reply.graphql @@ -0,0 +1,5 @@ +mutation addReply($user_uuid: uuid!, $msg_uuid: uuid!, $content: String!) { + insert_reply_one(object: {user_uuid: $user_uuid, room_uuid: $room_uuid, content: $content}) { + uuid + } +} \ No newline at end of file diff --git a/database/hasura/hasura_metadata.json b/database/hasura/hasura_metadata.json index 8c4ac68..703633b 100644 --- a/database/hasura/hasura_metadata.json +++ b/database/hasura/hasura_metadata.json @@ -1,5 +1,5 @@ { - "resource_version": 14, + "resource_version": 17, "metadata": { "version": 3, "sources": [ @@ -85,6 +85,67 @@ } ] }, + { + "table": { + "name": "reply", + "schema": "public" + }, + "insert_permissions": [ + { + "role": "user", + "permission": { + "check": {}, + "columns": [ + "content", + "msg_uuid", + "user_uuid", + "uuid" + ] + }, + "comment": "" + } + ], + "select_permissions": [ + { + "role": "user", + "permission": { + "columns": [ + "content", + "msg_uuid", + "user_uuid", + "uuid" + ], + "filter": {} + }, + "comment": "" + } + ], + "update_permissions": [ + { + "role": "user", + "permission": { + "columns": [ + "content", + "msg_uuid", + "user_uuid", + "uuid" + ], + "filter": {}, + "check": null + }, + "comment": "" + } + ], + "delete_permissions": [ + { + "role": "user", + "permission": { + "filter": {} + }, + "comment": "" + } + ] + }, { "table": { "name": "room", From 1cee6192a62d903206abcee5fd02e922bfe5507e Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Mon, 1 Sep 2025 10:31:52 +0800 Subject: [PATCH 04/16] fix: wrong argument --- database/graphql/reply.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/graphql/reply.graphql b/database/graphql/reply.graphql index c35215c..4cf0ff0 100644 --- a/database/graphql/reply.graphql +++ b/database/graphql/reply.graphql @@ -1,5 +1,5 @@ mutation addReply($user_uuid: uuid!, $msg_uuid: uuid!, $content: String!) { - insert_reply_one(object: {user_uuid: $user_uuid, room_uuid: $room_uuid, content: $content}) { + insert_reply_one(object: {user_uuid: $user_uuid, msg_uuid: $msg_uuid, content: $content}) { uuid } } \ No newline at end of file From 238b25eb7b0a11213c4a41fd973f6e388906b088 Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:32:09 +0800 Subject: [PATCH 05/16] feat: add query for replies --- database/graphql/reply.graphql | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/database/graphql/reply.graphql b/database/graphql/reply.graphql index 4cf0ff0..17d1f44 100644 --- a/database/graphql/reply.graphql +++ b/database/graphql/reply.graphql @@ -2,4 +2,16 @@ mutation addReply($user_uuid: uuid!, $msg_uuid: uuid!, $content: String!) { insert_reply_one(object: {user_uuid: $user_uuid, msg_uuid: $msg_uuid, content: $content}) { uuid } +} + +subscription getReplyByMessage($msg_uuid: uuid!) { + message(where: {msg_uuid: {_eq: $msg_uuid}}) { + uuid + user { + uuid + username + } + content + created_at + } } \ No newline at end of file From 013eac1d378eb4327eb254e59409d7b72c50dafb Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:49:58 +0800 Subject: [PATCH 06/16] fix: fix wrong query --- database/graphql/reply.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/graphql/reply.graphql b/database/graphql/reply.graphql index 17d1f44..56962bc 100644 --- a/database/graphql/reply.graphql +++ b/database/graphql/reply.graphql @@ -5,7 +5,7 @@ mutation addReply($user_uuid: uuid!, $msg_uuid: uuid!, $content: String!) { } subscription getReplyByMessage($msg_uuid: uuid!) { - message(where: {msg_uuid: {_eq: $msg_uuid}}) { + reply(where: {msg_uuid: {_eq: $msg_uuid}}) { uuid user { uuid From c18c931b0da3cac16e850fa961e7920db85dd3f7 Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:23:08 +0800 Subject: [PATCH 07/16] feat: add reply menu --- backend/src/graphql.ts | 362 +++++++++++++++++++++++++- frontend/src/ChatBox.tsx | 39 +++ frontend/src/graphql.tsx | 533 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 930 insertions(+), 4 deletions(-) diff --git a/backend/src/graphql.ts b/backend/src/graphql.ts index 03ca976..91c086f 100644 --- a/backend/src/graphql.ts +++ b/backend/src/graphql.ts @@ -212,6 +212,13 @@ export type Message_Mutation_Response = { returning: Array; }; +/** input type for inserting object relation for remote table "message" */ +export type Message_Obj_Rel_Insert_Input = { + data: Message_Insert_Input; + /** upsert condition */ + on_conflict?: InputMaybe; +}; + /** on_conflict condition type for table "message" */ export type Message_On_Conflict = { constraint: Message_Constraint; @@ -303,6 +310,10 @@ export type Mutation_Root = { delete_message?: Maybe; /** delete single row from the table: "message" */ delete_message_by_pk?: Maybe; + /** delete data from the table: "reply" */ + delete_reply?: Maybe; + /** delete single row from the table: "reply" */ + delete_reply_by_pk?: Maybe; /** delete data from the table: "room" */ delete_room?: Maybe; /** delete single row from the table: "room" */ @@ -319,6 +330,10 @@ export type Mutation_Root = { insert_message?: Maybe; /** insert a single row into the table: "message" */ insert_message_one?: Maybe; + /** insert data into the table: "reply" */ + insert_reply?: Maybe; + /** insert a single row into the table: "reply" */ + insert_reply_one?: Maybe; /** insert data into the table: "room" */ insert_room?: Maybe; /** insert a single row into the table: "room" */ @@ -337,6 +352,12 @@ export type Mutation_Root = { update_message_by_pk?: Maybe; /** update multiples rows of table: "message" */ update_message_many?: Maybe>>; + /** update data of the table: "reply" */ + update_reply?: Maybe; + /** update single row of the table: "reply" */ + update_reply_by_pk?: Maybe; + /** update multiples rows of table: "reply" */ + update_reply_many?: Maybe>>; /** update data of the table: "room" */ update_room?: Maybe; /** update single row of the table: "room" */ @@ -370,6 +391,18 @@ export type Mutation_RootDelete_Message_By_PkArgs = { }; +/** mutation root */ +export type Mutation_RootDelete_ReplyArgs = { + where: Reply_Bool_Exp; +}; + + +/** mutation root */ +export type Mutation_RootDelete_Reply_By_PkArgs = { + uuid: Scalars['uuid']['input']; +}; + + /** mutation root */ export type Mutation_RootDelete_RoomArgs = { where: Room_Bool_Exp; @@ -421,6 +454,20 @@ export type Mutation_RootInsert_Message_OneArgs = { }; +/** mutation root */ +export type Mutation_RootInsert_ReplyArgs = { + objects: Array; + on_conflict?: InputMaybe; +}; + + +/** mutation root */ +export type Mutation_RootInsert_Reply_OneArgs = { + object: Reply_Insert_Input; + on_conflict?: InputMaybe; +}; + + /** mutation root */ export type Mutation_RootInsert_RoomArgs = { objects: Array; @@ -483,6 +530,26 @@ export type Mutation_RootUpdate_Message_ManyArgs = { }; +/** mutation root */ +export type Mutation_RootUpdate_ReplyArgs = { + _set?: InputMaybe; + where: Reply_Bool_Exp; +}; + + +/** mutation root */ +export type Mutation_RootUpdate_Reply_By_PkArgs = { + _set?: InputMaybe; + pk_columns: Reply_Pk_Columns_Input; +}; + + +/** mutation root */ +export type Mutation_RootUpdate_Reply_ManyArgs = { + updates: Array; +}; + + /** mutation root */ export type Mutation_RootUpdate_RoomArgs = { _set?: InputMaybe; @@ -566,6 +633,12 @@ export type Query_Root = { message_aggregate: Message_Aggregate; /** fetch data from the table: "message" using primary key columns */ message_by_pk?: Maybe; + /** fetch data from the table: "reply" */ + reply: Array; + /** fetch aggregated fields from the table: "reply" */ + reply_aggregate: Reply_Aggregate; + /** fetch data from the table: "reply" using primary key columns */ + reply_by_pk?: Maybe; /** fetch data from the table: "room" */ room: Array; /** fetch aggregated fields from the table: "room" */ @@ -610,6 +683,29 @@ export type Query_RootMessage_By_PkArgs = { }; +export type Query_RootReplyArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootReply_AggregateArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootReply_By_PkArgs = { + uuid: Scalars['uuid']['input']; +}; + + export type Query_RootRoomArgs = { distinct_on?: InputMaybe>; limit?: InputMaybe; @@ -679,6 +775,186 @@ export type Query_RootUser_Room_By_PkArgs = { user_uuid: Scalars['uuid']['input']; }; +/** columns and relationships of "reply" */ +export type Reply = { + __typename?: 'reply'; + content: Scalars['String']['output']; + created_at: Scalars['timestamp']['output']; + /** An object relationship */ + message?: Maybe; + msg_uuid?: Maybe; + /** An object relationship */ + user?: Maybe; + user_uuid?: Maybe; + uuid: Scalars['uuid']['output']; +}; + +/** aggregated selection of "reply" */ +export type Reply_Aggregate = { + __typename?: 'reply_aggregate'; + aggregate?: Maybe; + nodes: Array; +}; + +/** aggregate fields of "reply" */ +export type Reply_Aggregate_Fields = { + __typename?: 'reply_aggregate_fields'; + count: Scalars['Int']['output']; + max?: Maybe; + min?: Maybe; +}; + + +/** aggregate fields of "reply" */ +export type Reply_Aggregate_FieldsCountArgs = { + columns?: InputMaybe>; + distinct?: InputMaybe; +}; + +/** Boolean expression to filter rows from the table "reply". All fields are combined with a logical 'AND'. */ +export type Reply_Bool_Exp = { + _and?: InputMaybe>; + _not?: InputMaybe; + _or?: InputMaybe>; + content?: InputMaybe; + created_at?: InputMaybe; + message?: InputMaybe; + msg_uuid?: InputMaybe; + user?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** unique or primary key constraints on table "reply" */ +export enum Reply_Constraint { + /** unique or primary key constraint on columns "uuid" */ + ReplyPkey = 'reply_pkey' +} + +/** input type for inserting data into table "reply" */ +export type Reply_Insert_Input = { + content?: InputMaybe; + created_at?: InputMaybe; + message?: InputMaybe; + msg_uuid?: InputMaybe; + user?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** aggregate max on columns */ +export type Reply_Max_Fields = { + __typename?: 'reply_max_fields'; + content?: Maybe; + created_at?: Maybe; + msg_uuid?: Maybe; + user_uuid?: Maybe; + uuid?: Maybe; +}; + +/** aggregate min on columns */ +export type Reply_Min_Fields = { + __typename?: 'reply_min_fields'; + content?: Maybe; + created_at?: Maybe; + msg_uuid?: Maybe; + user_uuid?: Maybe; + uuid?: Maybe; +}; + +/** response of any mutation on the table "reply" */ +export type Reply_Mutation_Response = { + __typename?: 'reply_mutation_response'; + /** number of rows affected by the mutation */ + affected_rows: Scalars['Int']['output']; + /** data from the rows affected by the mutation */ + returning: Array; +}; + +/** on_conflict condition type for table "reply" */ +export type Reply_On_Conflict = { + constraint: Reply_Constraint; + update_columns?: Array; + where?: InputMaybe; +}; + +/** Ordering options when selecting data from "reply". */ +export type Reply_Order_By = { + content?: InputMaybe; + created_at?: InputMaybe; + message?: InputMaybe; + msg_uuid?: InputMaybe; + user?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** primary key columns input for table: reply */ +export type Reply_Pk_Columns_Input = { + uuid: Scalars['uuid']['input']; +}; + +/** select columns of table "reply" */ +export enum Reply_Select_Column { + /** column name */ + Content = 'content', + /** column name */ + CreatedAt = 'created_at', + /** column name */ + MsgUuid = 'msg_uuid', + /** column name */ + UserUuid = 'user_uuid', + /** column name */ + Uuid = 'uuid' +} + +/** input type for updating data in table "reply" */ +export type Reply_Set_Input = { + content?: InputMaybe; + created_at?: InputMaybe; + msg_uuid?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** Streaming cursor of the table "reply" */ +export type Reply_Stream_Cursor_Input = { + /** Stream column input with initial value */ + initial_value: Reply_Stream_Cursor_Value_Input; + /** cursor ordering */ + ordering?: InputMaybe; +}; + +/** Initial value of the column from where the streaming should start */ +export type Reply_Stream_Cursor_Value_Input = { + content?: InputMaybe; + created_at?: InputMaybe; + msg_uuid?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** update columns of table "reply" */ +export enum Reply_Update_Column { + /** column name */ + Content = 'content', + /** column name */ + CreatedAt = 'created_at', + /** column name */ + MsgUuid = 'msg_uuid', + /** column name */ + UserUuid = 'user_uuid', + /** column name */ + Uuid = 'uuid' +} + +export type Reply_Updates = { + /** sets the columns of the filtered rows to the given values */ + _set?: InputMaybe; + /** filter the rows which have to be updated */ + where: Reply_Bool_Exp; +}; + /** columns and relationships of "room" */ export type Room = { __typename?: 'room'; @@ -926,6 +1202,14 @@ export type Subscription_Root = { message_by_pk?: Maybe; /** fetch data from the table in a streaming manner: "message" */ message_stream: Array; + /** fetch data from the table: "reply" */ + reply: Array; + /** fetch aggregated fields from the table: "reply" */ + reply_aggregate: Reply_Aggregate; + /** fetch data from the table: "reply" using primary key columns */ + reply_by_pk?: Maybe; + /** fetch data from the table in a streaming manner: "reply" */ + reply_stream: Array; /** fetch data from the table: "room" */ room: Array; /** fetch aggregated fields from the table: "room" */ @@ -983,6 +1267,36 @@ export type Subscription_RootMessage_StreamArgs = { }; +export type Subscription_RootReplyArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootReply_AggregateArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootReply_By_PkArgs = { + uuid: Scalars['uuid']['input']; +}; + + +export type Subscription_RootReply_StreamArgs = { + batch_size: Scalars['Int']['input']; + cursor: Array>; + where?: InputMaybe; +}; + + export type Subscription_RootRoomArgs = { distinct_on?: InputMaybe>; limit?: InputMaybe; @@ -1319,7 +1633,7 @@ export type User_Room_Bool_Exp = { /** unique or primary key constraints on table "user_room" */ export enum User_Room_Constraint { - /** unique or primary key constraint on columns "user_uuid", "room_uuid" */ + /** unique or primary key constraint on columns "room_uuid", "user_uuid" */ UserRoomPkey = 'user_room_pkey' } @@ -1515,6 +1829,22 @@ export type GetMessagesByUserSubscriptionVariables = Exact<{ export type GetMessagesByUserSubscription = { __typename?: 'subscription_root', message: Array<{ __typename?: 'message', uuid: any, content: string, created_at: any, room: { __typename?: 'room', uuid: any } }> }; +export type AddReplyMutationVariables = Exact<{ + user_uuid: Scalars['uuid']['input']; + msg_uuid: Scalars['uuid']['input']; + content: Scalars['String']['input']; +}>; + + +export type AddReplyMutation = { __typename?: 'mutation_root', insert_reply_one?: { __typename?: 'reply', uuid: any } | null }; + +export type GetReplyByMessageSubscriptionVariables = Exact<{ + msg_uuid: Scalars['uuid']['input']; +}>; + + +export type GetReplyByMessageSubscription = { __typename?: 'subscription_root', reply: Array<{ __typename?: 'reply', uuid: any, content: string, created_at: any, user?: { __typename?: 'user', uuid: any, username: string } | null }> }; + export type AddRoomMutationVariables = Exact<{ name: Scalars['String']['input']; intro: Scalars['String']['input']; @@ -1611,6 +1941,28 @@ export const GetMessagesByUserDocument = gql` } } `; +export const AddReplyDocument = gql` + mutation addReply($user_uuid: uuid!, $msg_uuid: uuid!, $content: String!) { + insert_reply_one( + object: {user_uuid: $user_uuid, msg_uuid: $msg_uuid, content: $content} + ) { + uuid + } +} + `; +export const GetReplyByMessageDocument = gql` + subscription getReplyByMessage($msg_uuid: uuid!) { + reply(where: {msg_uuid: {_eq: $msg_uuid}}) { + uuid + user { + uuid + username + } + content + created_at + } +} + `; export const AddRoomDocument = gql` mutation addRoom($name: String!, $intro: String!, $invite_code: String!) { insert_room_one(object: {name: $name, intro: $intro, invite_code: $invite_code}) { @@ -1694,6 +2046,12 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getMessagesByUser(variables: GetMessagesByUserSubscriptionVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetMessagesByUserDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getMessagesByUser', 'subscription', variables); }, + addReply(variables: AddReplyMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(AddReplyDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'addReply', 'mutation', variables); + }, + getReplyByMessage(variables: GetReplyByMessageSubscriptionVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetReplyByMessageDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getReplyByMessage', 'subscription', variables); + }, addRoom(variables: AddRoomMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(AddRoomDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'addRoom', 'mutation', variables); }, @@ -1720,4 +2078,4 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = } }; } -export type Sdk = ReturnType; +export type Sdk = ReturnType; \ No newline at end of file diff --git a/frontend/src/ChatBox.tsx b/frontend/src/ChatBox.tsx index b2619b7..1bd5f40 100644 --- a/frontend/src/ChatBox.tsx +++ b/frontend/src/ChatBox.tsx @@ -3,6 +3,9 @@ import { Button, Input, message, Spin } from "antd"; import { user } from "./getUser"; import * as graphql from "./graphql"; import { Bubble, Card, Container, Scroll, Text } from "./Components"; +import { Menu, Item, Separator, Submenu, useContextMenu } from 'react-contexify'; +import 'react-contexify/ReactContexify.css'; +const MENU_ID = 'reply-menu'; interface ChatBoxProps { user: user | null; @@ -152,6 +155,38 @@ const MessageBubble: React.FC = ({ user, message }) => { const date = new Date( dateUTC.getTime() - dateUTC.getTimezoneOffset() * 60000 ); + + const { show } = useContextMenu({ + id: MENU_ID, + }); + + function handleContextMenu(event: any){ + event.preventDefault(); + show({ + event, + props: { + key: 'value' + }, + position: { + x: event.clientX - event.currentTarget.getBoundingClientRect().left, + y: event.clientY + }, + }) + } + // @ts-ignore + const handleItemClick = (args: ItemParams) => { + const { id, event, props } = args; + switch (id) { + case "reply": + alert("reply"); + break; + default: + break; + //etc... + } + } + + return (
= ({ user, message }) => { flexWrap: "nowrap", alignItems: isSelf ? "flex-end" : "flex-start", }} + onContextMenu={handleContextMenu} > + + Reply +
{message.user.username} diff --git a/frontend/src/graphql.tsx b/frontend/src/graphql.tsx index 4a7f6f0..a39083a 100644 --- a/frontend/src/graphql.tsx +++ b/frontend/src/graphql.tsx @@ -212,6 +212,13 @@ export type Message_Mutation_Response = { returning: Array; }; +/** input type for inserting object relation for remote table "message" */ +export type Message_Obj_Rel_Insert_Input = { + data: Message_Insert_Input; + /** upsert condition */ + on_conflict?: InputMaybe; +}; + /** on_conflict condition type for table "message" */ export type Message_On_Conflict = { constraint: Message_Constraint; @@ -303,6 +310,10 @@ export type Mutation_Root = { delete_message?: Maybe; /** delete single row from the table: "message" */ delete_message_by_pk?: Maybe; + /** delete data from the table: "reply" */ + delete_reply?: Maybe; + /** delete single row from the table: "reply" */ + delete_reply_by_pk?: Maybe; /** delete data from the table: "room" */ delete_room?: Maybe; /** delete single row from the table: "room" */ @@ -319,6 +330,10 @@ export type Mutation_Root = { insert_message?: Maybe; /** insert a single row into the table: "message" */ insert_message_one?: Maybe; + /** insert data into the table: "reply" */ + insert_reply?: Maybe; + /** insert a single row into the table: "reply" */ + insert_reply_one?: Maybe; /** insert data into the table: "room" */ insert_room?: Maybe; /** insert a single row into the table: "room" */ @@ -337,6 +352,12 @@ export type Mutation_Root = { update_message_by_pk?: Maybe; /** update multiples rows of table: "message" */ update_message_many?: Maybe>>; + /** update data of the table: "reply" */ + update_reply?: Maybe; + /** update single row of the table: "reply" */ + update_reply_by_pk?: Maybe; + /** update multiples rows of table: "reply" */ + update_reply_many?: Maybe>>; /** update data of the table: "room" */ update_room?: Maybe; /** update single row of the table: "room" */ @@ -370,6 +391,18 @@ export type Mutation_RootDelete_Message_By_PkArgs = { }; +/** mutation root */ +export type Mutation_RootDelete_ReplyArgs = { + where: Reply_Bool_Exp; +}; + + +/** mutation root */ +export type Mutation_RootDelete_Reply_By_PkArgs = { + uuid: Scalars['uuid']['input']; +}; + + /** mutation root */ export type Mutation_RootDelete_RoomArgs = { where: Room_Bool_Exp; @@ -421,6 +454,20 @@ export type Mutation_RootInsert_Message_OneArgs = { }; +/** mutation root */ +export type Mutation_RootInsert_ReplyArgs = { + objects: Array; + on_conflict?: InputMaybe; +}; + + +/** mutation root */ +export type Mutation_RootInsert_Reply_OneArgs = { + object: Reply_Insert_Input; + on_conflict?: InputMaybe; +}; + + /** mutation root */ export type Mutation_RootInsert_RoomArgs = { objects: Array; @@ -483,6 +530,26 @@ export type Mutation_RootUpdate_Message_ManyArgs = { }; +/** mutation root */ +export type Mutation_RootUpdate_ReplyArgs = { + _set?: InputMaybe; + where: Reply_Bool_Exp; +}; + + +/** mutation root */ +export type Mutation_RootUpdate_Reply_By_PkArgs = { + _set?: InputMaybe; + pk_columns: Reply_Pk_Columns_Input; +}; + + +/** mutation root */ +export type Mutation_RootUpdate_Reply_ManyArgs = { + updates: Array; +}; + + /** mutation root */ export type Mutation_RootUpdate_RoomArgs = { _set?: InputMaybe; @@ -566,6 +633,12 @@ export type Query_Root = { message_aggregate: Message_Aggregate; /** fetch data from the table: "message" using primary key columns */ message_by_pk?: Maybe; + /** fetch data from the table: "reply" */ + reply: Array; + /** fetch aggregated fields from the table: "reply" */ + reply_aggregate: Reply_Aggregate; + /** fetch data from the table: "reply" using primary key columns */ + reply_by_pk?: Maybe; /** fetch data from the table: "room" */ room: Array; /** fetch aggregated fields from the table: "room" */ @@ -610,6 +683,29 @@ export type Query_RootMessage_By_PkArgs = { }; +export type Query_RootReplyArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootReply_AggregateArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootReply_By_PkArgs = { + uuid: Scalars['uuid']['input']; +}; + + export type Query_RootRoomArgs = { distinct_on?: InputMaybe>; limit?: InputMaybe; @@ -679,6 +775,186 @@ export type Query_RootUser_Room_By_PkArgs = { user_uuid: Scalars['uuid']['input']; }; +/** columns and relationships of "reply" */ +export type Reply = { + __typename?: 'reply'; + content: Scalars['String']['output']; + created_at: Scalars['timestamp']['output']; + /** An object relationship */ + message?: Maybe; + msg_uuid?: Maybe; + /** An object relationship */ + user?: Maybe; + user_uuid?: Maybe; + uuid: Scalars['uuid']['output']; +}; + +/** aggregated selection of "reply" */ +export type Reply_Aggregate = { + __typename?: 'reply_aggregate'; + aggregate?: Maybe; + nodes: Array; +}; + +/** aggregate fields of "reply" */ +export type Reply_Aggregate_Fields = { + __typename?: 'reply_aggregate_fields'; + count: Scalars['Int']['output']; + max?: Maybe; + min?: Maybe; +}; + + +/** aggregate fields of "reply" */ +export type Reply_Aggregate_FieldsCountArgs = { + columns?: InputMaybe>; + distinct?: InputMaybe; +}; + +/** Boolean expression to filter rows from the table "reply". All fields are combined with a logical 'AND'. */ +export type Reply_Bool_Exp = { + _and?: InputMaybe>; + _not?: InputMaybe; + _or?: InputMaybe>; + content?: InputMaybe; + created_at?: InputMaybe; + message?: InputMaybe; + msg_uuid?: InputMaybe; + user?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** unique or primary key constraints on table "reply" */ +export enum Reply_Constraint { + /** unique or primary key constraint on columns "uuid" */ + ReplyPkey = 'reply_pkey' +} + +/** input type for inserting data into table "reply" */ +export type Reply_Insert_Input = { + content?: InputMaybe; + created_at?: InputMaybe; + message?: InputMaybe; + msg_uuid?: InputMaybe; + user?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** aggregate max on columns */ +export type Reply_Max_Fields = { + __typename?: 'reply_max_fields'; + content?: Maybe; + created_at?: Maybe; + msg_uuid?: Maybe; + user_uuid?: Maybe; + uuid?: Maybe; +}; + +/** aggregate min on columns */ +export type Reply_Min_Fields = { + __typename?: 'reply_min_fields'; + content?: Maybe; + created_at?: Maybe; + msg_uuid?: Maybe; + user_uuid?: Maybe; + uuid?: Maybe; +}; + +/** response of any mutation on the table "reply" */ +export type Reply_Mutation_Response = { + __typename?: 'reply_mutation_response'; + /** number of rows affected by the mutation */ + affected_rows: Scalars['Int']['output']; + /** data from the rows affected by the mutation */ + returning: Array; +}; + +/** on_conflict condition type for table "reply" */ +export type Reply_On_Conflict = { + constraint: Reply_Constraint; + update_columns?: Array; + where?: InputMaybe; +}; + +/** Ordering options when selecting data from "reply". */ +export type Reply_Order_By = { + content?: InputMaybe; + created_at?: InputMaybe; + message?: InputMaybe; + msg_uuid?: InputMaybe; + user?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** primary key columns input for table: reply */ +export type Reply_Pk_Columns_Input = { + uuid: Scalars['uuid']['input']; +}; + +/** select columns of table "reply" */ +export enum Reply_Select_Column { + /** column name */ + Content = 'content', + /** column name */ + CreatedAt = 'created_at', + /** column name */ + MsgUuid = 'msg_uuid', + /** column name */ + UserUuid = 'user_uuid', + /** column name */ + Uuid = 'uuid' +} + +/** input type for updating data in table "reply" */ +export type Reply_Set_Input = { + content?: InputMaybe; + created_at?: InputMaybe; + msg_uuid?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** Streaming cursor of the table "reply" */ +export type Reply_Stream_Cursor_Input = { + /** Stream column input with initial value */ + initial_value: Reply_Stream_Cursor_Value_Input; + /** cursor ordering */ + ordering?: InputMaybe; +}; + +/** Initial value of the column from where the streaming should start */ +export type Reply_Stream_Cursor_Value_Input = { + content?: InputMaybe; + created_at?: InputMaybe; + msg_uuid?: InputMaybe; + user_uuid?: InputMaybe; + uuid?: InputMaybe; +}; + +/** update columns of table "reply" */ +export enum Reply_Update_Column { + /** column name */ + Content = 'content', + /** column name */ + CreatedAt = 'created_at', + /** column name */ + MsgUuid = 'msg_uuid', + /** column name */ + UserUuid = 'user_uuid', + /** column name */ + Uuid = 'uuid' +} + +export type Reply_Updates = { + /** sets the columns of the filtered rows to the given values */ + _set?: InputMaybe; + /** filter the rows which have to be updated */ + where: Reply_Bool_Exp; +}; + /** columns and relationships of "room" */ export type Room = { __typename?: 'room'; @@ -926,6 +1202,14 @@ export type Subscription_Root = { message_by_pk?: Maybe; /** fetch data from the table in a streaming manner: "message" */ message_stream: Array; + /** fetch data from the table: "reply" */ + reply: Array; + /** fetch aggregated fields from the table: "reply" */ + reply_aggregate: Reply_Aggregate; + /** fetch data from the table: "reply" using primary key columns */ + reply_by_pk?: Maybe; + /** fetch data from the table in a streaming manner: "reply" */ + reply_stream: Array; /** fetch data from the table: "room" */ room: Array; /** fetch aggregated fields from the table: "room" */ @@ -983,6 +1267,36 @@ export type Subscription_RootMessage_StreamArgs = { }; +export type Subscription_RootReplyArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootReply_AggregateArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootReply_By_PkArgs = { + uuid: Scalars['uuid']['input']; +}; + + +export type Subscription_RootReply_StreamArgs = { + batch_size: Scalars['Int']['input']; + cursor: Array>; + where?: InputMaybe; +}; + + export type Subscription_RootRoomArgs = { distinct_on?: InputMaybe>; limit?: InputMaybe; @@ -1319,7 +1633,7 @@ export type User_Room_Bool_Exp = { /** unique or primary key constraints on table "user_room" */ export enum User_Room_Constraint { - /** unique or primary key constraint on columns "user_uuid", "room_uuid" */ + /** unique or primary key constraint on columns "room_uuid", "user_uuid" */ UserRoomPkey = 'user_room_pkey' } @@ -1508,6 +1822,29 @@ export type GetMessagesByRoomSubscriptionVariables = Exact<{ export type GetMessagesByRoomSubscription = { __typename?: 'subscription_root', message: Array<{ __typename?: 'message', uuid: any, content: string, created_at: any, user: { __typename?: 'user', uuid: any, username: string } }> }; +export type GetMessagesByUserSubscriptionVariables = Exact<{ + user_uuid: Scalars['uuid']['input']; +}>; + + +export type GetMessagesByUserSubscription = { __typename?: 'subscription_root', message: Array<{ __typename?: 'message', uuid: any, content: string, created_at: any, room: { __typename?: 'room', uuid: any } }> }; + +export type AddReplyMutationVariables = Exact<{ + user_uuid: Scalars['uuid']['input']; + msg_uuid: Scalars['uuid']['input']; + content: Scalars['String']['input']; +}>; + + +export type AddReplyMutation = { __typename?: 'mutation_root', insert_reply_one?: { __typename?: 'reply', uuid: any } | null }; + +export type GetReplyByMessageSubscriptionVariables = Exact<{ + msg_uuid: Scalars['uuid']['input']; +}>; + + +export type GetReplyByMessageSubscription = { __typename?: 'subscription_root', reply: Array<{ __typename?: 'reply', uuid: any, content: string, created_at: any, user?: { __typename?: 'user', uuid: any, username: string } | null }> }; + export type AddRoomMutationVariables = Exact<{ name: Scalars['String']['input']; intro: Scalars['String']['input']; @@ -1539,6 +1876,14 @@ export type JoinRoomMutationVariables = Exact<{ export type JoinRoomMutation = { __typename?: 'mutation_root', insert_user_room_one?: { __typename?: 'user_room', user_uuid: any, room_uuid: any } | null }; +export type QuitRoomMutationVariables = Exact<{ + user_uuid: Scalars['uuid']['input']; + room_uuid: Scalars['uuid']['input']; +}>; + + +export type QuitRoomMutation = { __typename?: 'mutation_root', delete_user_room?: { __typename?: 'user_room_mutation_response', affected_rows: number } | null }; + export type AddUserMutationVariables = Exact<{ username: Scalars['String']['input']; password: Scalars['String']['input']; @@ -1554,6 +1899,13 @@ export type GetUsersByUsernameQueryVariables = Exact<{ export type GetUsersByUsernameQuery = { __typename?: 'query_root', user: Array<{ __typename?: 'user', uuid: any, password: string }> }; +export type DeleteUserMutationVariables = Exact<{ + uuid: Scalars['uuid']['input']; +}>; + + +export type DeleteUserMutation = { __typename?: 'mutation_root', delete_user_by_pk?: { __typename?: 'user', uuid: any } | null }; + export const AddMessageDocument = gql` mutation addMessage($user_uuid: uuid!, $room_uuid: uuid!, $content: String!) { @@ -1628,6 +1980,114 @@ export function useGetMessagesByRoomSubscription(baseOptions: Apollo.Subscriptio } export type GetMessagesByRoomSubscriptionHookResult = ReturnType; export type GetMessagesByRoomSubscriptionResult = Apollo.SubscriptionResult; +export const GetMessagesByUserDocument = gql` + subscription getMessagesByUser($user_uuid: uuid!) { + message(where: {user_uuid: {_eq: $user_uuid}}) { + uuid + room { + uuid + } + content + created_at + } +} + `; + +/** + * __useGetMessagesByUserSubscription__ + * + * To run a query within a React component, call `useGetMessagesByUserSubscription` and pass it any options that fit your needs. + * When your component renders, `useGetMessagesByUserSubscription` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetMessagesByUserSubscription({ + * variables: { + * user_uuid: // value for 'user_uuid' + * }, + * }); + */ +export function useGetMessagesByUserSubscription(baseOptions: Apollo.SubscriptionHookOptions & ({ variables: GetMessagesByUserSubscriptionVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSubscription(GetMessagesByUserDocument, options); + } +export type GetMessagesByUserSubscriptionHookResult = ReturnType; +export type GetMessagesByUserSubscriptionResult = Apollo.SubscriptionResult; +export const AddReplyDocument = gql` + mutation addReply($user_uuid: uuid!, $msg_uuid: uuid!, $content: String!) { + insert_reply_one( + object: {user_uuid: $user_uuid, msg_uuid: $msg_uuid, content: $content} + ) { + uuid + } +} + `; +export type AddReplyMutationFn = Apollo.MutationFunction; + +/** + * __useAddReplyMutation__ + * + * To run a mutation, you first call `useAddReplyMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAddReplyMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [addReplyMutation, { data, loading, error }] = useAddReplyMutation({ + * variables: { + * user_uuid: // value for 'user_uuid' + * msg_uuid: // value for 'msg_uuid' + * content: // value for 'content' + * }, + * }); + */ +export function useAddReplyMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(AddReplyDocument, options); + } +export type AddReplyMutationHookResult = ReturnType; +export type AddReplyMutationResult = Apollo.MutationResult; +export type AddReplyMutationOptions = Apollo.BaseMutationOptions; +export const GetReplyByMessageDocument = gql` + subscription getReplyByMessage($msg_uuid: uuid!) { + reply(where: {msg_uuid: {_eq: $msg_uuid}}) { + uuid + user { + uuid + username + } + content + created_at + } +} + `; + +/** + * __useGetReplyByMessageSubscription__ + * + * To run a query within a React component, call `useGetReplyByMessageSubscription` and pass it any options that fit your needs. + * When your component renders, `useGetReplyByMessageSubscription` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetReplyByMessageSubscription({ + * variables: { + * msg_uuid: // value for 'msg_uuid' + * }, + * }); + */ +export function useGetReplyByMessageSubscription(baseOptions: Apollo.SubscriptionHookOptions & ({ variables: GetReplyByMessageSubscriptionVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSubscription(GetReplyByMessageDocument, options); + } +export type GetReplyByMessageSubscriptionHookResult = ReturnType; +export type GetReplyByMessageSubscriptionResult = Apollo.SubscriptionResult; export const AddRoomDocument = gql` mutation addRoom($name: String!, $intro: String!, $invite_code: String!) { insert_room_one(object: {name: $name, intro: $intro, invite_code: $invite_code}) { @@ -1784,6 +2244,42 @@ export function useJoinRoomMutation(baseOptions?: Apollo.MutationHookOptions; export type JoinRoomMutationResult = Apollo.MutationResult; export type JoinRoomMutationOptions = Apollo.BaseMutationOptions; +export const QuitRoomDocument = gql` + mutation quitRoom($user_uuid: uuid!, $room_uuid: uuid!) { + delete_user_room( + where: {user_uuid: {_eq: $user_uuid}, room_uuid: {_eq: $room_uuid}} + ) { + affected_rows + } +} + `; +export type QuitRoomMutationFn = Apollo.MutationFunction; + +/** + * __useQuitRoomMutation__ + * + * To run a mutation, you first call `useQuitRoomMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useQuitRoomMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [quitRoomMutation, { data, loading, error }] = useQuitRoomMutation({ + * variables: { + * user_uuid: // value for 'user_uuid' + * room_uuid: // value for 'room_uuid' + * }, + * }); + */ +export function useQuitRoomMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(QuitRoomDocument, options); + } +export type QuitRoomMutationHookResult = ReturnType; +export type QuitRoomMutationResult = Apollo.MutationResult; +export type QuitRoomMutationOptions = Apollo.BaseMutationOptions; export const AddUserDocument = gql` mutation addUser($username: String!, $password: String!) { insert_user_one(object: {username: $username, password: $password}) { @@ -1858,4 +2354,37 @@ export function useGetUsersByUsernameSuspenseQuery(baseOptions?: Apollo.Suspense export type GetUsersByUsernameQueryHookResult = ReturnType; export type GetUsersByUsernameLazyQueryHookResult = ReturnType; export type GetUsersByUsernameSuspenseQueryHookResult = ReturnType; -export type GetUsersByUsernameQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetUsersByUsernameQueryResult = Apollo.QueryResult; +export const DeleteUserDocument = gql` + mutation deleteUser($uuid: uuid!) { + delete_user_by_pk(uuid: $uuid) { + uuid + } +} + `; +export type DeleteUserMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteUserMutation__ + * + * To run a mutation, you first call `useDeleteUserMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteUserMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteUserMutation, { data, loading, error }] = useDeleteUserMutation({ + * variables: { + * uuid: // value for 'uuid' + * }, + * }); + */ +export function useDeleteUserMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteUserDocument, options); + } +export type DeleteUserMutationHookResult = ReturnType; +export type DeleteUserMutationResult = Apollo.MutationResult; +export type DeleteUserMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file From fb698bc1e45ffa0a5538495afabe13e84116e40d Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:36:20 +0800 Subject: [PATCH 08/16] feat: add reply box --- backend/src/graphql.ts | 26 +++++ database/graphql/message.graphql | 15 +++ frontend/src/ChatBox.tsx | 33 +++--- frontend/src/ReplyBox.tsx | 188 +++++++++++++++++++++++++++++++ frontend/src/graphql.tsx | 46 ++++++++ frontend/src/index.tsx | 35 ++++++ 6 files changed, 330 insertions(+), 13 deletions(-) create mode 100644 frontend/src/ReplyBox.tsx diff --git a/backend/src/graphql.ts b/backend/src/graphql.ts index 91c086f..60a8cde 100644 --- a/backend/src/graphql.ts +++ b/backend/src/graphql.ts @@ -1829,6 +1829,13 @@ export type GetMessagesByUserSubscriptionVariables = Exact<{ export type GetMessagesByUserSubscription = { __typename?: 'subscription_root', message: Array<{ __typename?: 'message', uuid: any, content: string, created_at: any, room: { __typename?: 'room', uuid: any } }> }; +export type GetMessageByUuidSubscriptionVariables = Exact<{ + uuid: Scalars['uuid']['input']; +}>; + + +export type GetMessageByUuidSubscription = { __typename?: 'subscription_root', message_by_pk?: { __typename?: 'message', uuid: any, content: string, created_at: any, user: { __typename?: 'user', uuid: any, username: string }, room: { __typename?: 'room', uuid: any } } | null }; + export type AddReplyMutationVariables = Exact<{ user_uuid: Scalars['uuid']['input']; msg_uuid: Scalars['uuid']['input']; @@ -1941,6 +1948,22 @@ export const GetMessagesByUserDocument = gql` } } `; +export const GetMessageByUuidDocument = gql` + subscription getMessageByUUID($uuid: uuid!) { + message_by_pk(uuid: $uuid) { + uuid + user { + uuid + username + } + room { + uuid + } + content + created_at + } +} + `; export const AddReplyDocument = gql` mutation addReply($user_uuid: uuid!, $msg_uuid: uuid!, $content: String!) { insert_reply_one( @@ -2046,6 +2069,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getMessagesByUser(variables: GetMessagesByUserSubscriptionVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetMessagesByUserDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getMessagesByUser', 'subscription', variables); }, + getMessageByUUID(variables: GetMessageByUuidSubscriptionVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetMessageByUuidDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getMessageByUUID', 'subscription', variables); + }, addReply(variables: AddReplyMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(AddReplyDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'addReply', 'mutation', variables); }, diff --git a/database/graphql/message.graphql b/database/graphql/message.graphql index 40c0f2c..8dacea4 100644 --- a/database/graphql/message.graphql +++ b/database/graphql/message.graphql @@ -25,4 +25,19 @@ subscription getMessagesByUser($user_uuid: uuid!) { content created_at } +} + +subscription getMessageByUUID($uuid: uuid!) { + message_by_pk(uuid: $uuid) { + uuid + user { + uuid + username + } + room { + uuid + } + content + created_at + } } \ No newline at end of file diff --git a/frontend/src/ChatBox.tsx b/frontend/src/ChatBox.tsx index 1bd5f40..a2f2d9e 100644 --- a/frontend/src/ChatBox.tsx +++ b/frontend/src/ChatBox.tsx @@ -3,7 +3,7 @@ import { Button, Input, message, Spin } from "antd"; import { user } from "./getUser"; import * as graphql from "./graphql"; import { Bubble, Card, Container, Scroll, Text } from "./Components"; -import { Menu, Item, Separator, Submenu, useContextMenu } from 'react-contexify'; +import { Menu, Item, useContextMenu } from 'react-contexify'; import 'react-contexify/ReactContexify.css'; const MENU_ID = 'reply-menu'; @@ -11,9 +11,10 @@ interface ChatBoxProps { user: user | null; room: graphql.GetJoinedRoomsQuery["user_room"][0]["room"] | undefined; handleClose: () => void; + handleReply: (msg_uuid: any) => void; } -const ChatBox: React.FC = ({ user, room, handleClose }) => { +const ChatBox: React.FC = ({ user, room, handleClose, handleReply }) => { const [text, setText] = useState(""); const [loading, setLoading] = useState(false); @@ -85,7 +86,7 @@ const ChatBox: React.FC = ({ user, room, handleClose }) => { {room.intro} - +
= ({ user, room, handleClose }) => { interface MessageFeedProps { user: user; messages: graphql.GetMessagesByRoomSubscription["message"] | undefined; + handleReply: (msg_uuid: any) => void; } -const MessageFeed: React.FC = ({ user, messages }) => { +const MessageFeed: React.FC = ({ user, messages, handleReply }) => { const bottomRef = useRef(null); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); @@ -132,7 +134,7 @@ const MessageFeed: React.FC = ({ user, messages }) => { ref={index === messages.length - 1 ? bottomRef : null} key={index} > - + {handleReply(message.uuid)}}/>
)) ) : ( @@ -147,9 +149,10 @@ const MessageFeed: React.FC = ({ user, messages }) => { interface MessageBubbleProps { user: user; message: graphql.GetMessagesByRoomSubscription["message"][0]; + handleReply: () => void; } -const MessageBubble: React.FC = ({ user, message }) => { +const MessageBubble: React.FC = ({ user, message, handleReply }) => { const isSelf = user.uuid === message.user.uuid; const dateUTC = new Date(message.created_at); const date = new Date( @@ -157,10 +160,14 @@ const MessageBubble: React.FC = ({ user, message }) => { ); const { show } = useContextMenu({ - id: MENU_ID, + id: `${MENU_ID}-${message.uuid}`, }); function handleContextMenu(event: any){ + const rect = event.currentTarget.getBoundingClientRect(); + if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) { + return; + } event.preventDefault(); show({ event, @@ -168,8 +175,8 @@ const MessageBubble: React.FC = ({ user, message }) => { key: 'value' }, position: { - x: event.clientX - event.currentTarget.getBoundingClientRect().left, - y: event.clientY + x: 0, + y: 0 }, }) } @@ -178,7 +185,7 @@ const MessageBubble: React.FC = ({ user, message }) => { const { id, event, props } = args; switch (id) { case "reply": - alert("reply"); + handleReply(); break; default: break; @@ -198,9 +205,6 @@ const MessageBubble: React.FC = ({ user, message }) => { }} onContextMenu={handleContextMenu} > - - Reply -
{message.user.username} @@ -217,6 +221,9 @@ const MessageBubble: React.FC = ({ user, message }) => { : "rgba(255, 255, 255, 0.25)", }} > + + Reply + {message.content}
diff --git a/frontend/src/ReplyBox.tsx b/frontend/src/ReplyBox.tsx new file mode 100644 index 0000000..f646d4d --- /dev/null +++ b/frontend/src/ReplyBox.tsx @@ -0,0 +1,188 @@ +import { useEffect, useRef, useState } from "react"; +import { Button, Input, message, Spin } from "antd"; +import { user } from "./getUser"; +import * as graphql from "./graphql"; +import { Bubble, Card, Container, Scroll, Text } from "./Components"; + +interface ReplyBoxProps { + user: user | null; + msg: graphql.GetMessageByUuidSubscription["message_by_pk"] | undefined; + handleClose: () => void; +} + +const ReplyBox: React.FC = ({ user, msg, handleClose }) => { + const [text, setText] = useState(""); + const [loading, setLoading] = useState(false); + + const { data, error } = graphql.useGetReplyByMessageSubscription({ + skip: !msg, + variables: { + msg_uuid: msg?.uuid, + }, + }); + useEffect(() => { + if (error) { + console.error(error); + message.error("获取消息失败!"); + } + }, [error]); + + const [addReplyMutation] = graphql.useAddReplyMutation(); + + const handleSend = async () => { + setLoading(true); + if (!text) { + message.error("回复不能为空!"); + return setLoading(false); + } + const result = await addReplyMutation({ + variables: { + user_uuid: user?.uuid, + msg_uuid: msg?.uuid, + content: text, + }, + }); + if (result.errors) { + console.error(result.errors); + message.error("回复发送失败!"); + } + setText(""); + setLoading(false); + }; + + const Close = () => ( + + ); + + if (!user || !msg) { + return null; + } + return ( + + + + + 回复 + + + {msg.content} + + + +
+ setText(e.target.value)} + style={{ fontSize: "18px", height: "40px" }} + /> + +
+
+ ); +}; + +interface ReplyFeedProps { + user: user; + replies: graphql.GetReplyByMessageSubscription["reply"] | undefined; +} + +const ReplyFeed: React.FC = ({ user, replies }) => { + const bottomRef = useRef(null); + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [replies]); + + return ( + + {replies ? ( + replies.map((reply, index) => ( +
+ +
+ )) + ) : ( + + + + )} +
+ ); +}; + +interface ReplyBubbleProps { + user: user; + reply: graphql.GetReplyByMessageSubscription["reply"][0]; +} + +const ReplyBubble: React.FC = ({ user, reply }) => { + const isSelf = user.uuid === reply.user?.uuid; + const dateUTC = new Date(reply.created_at); + const date = new Date( + dateUTC.getTime() - dateUTC.getTimezoneOffset() * 60000 + ); + + return ( +
+
+ {reply.user?.username} + + {date.toLocaleString("zh-CN")} + +
+ + {reply.content} + +
+ ); +}; + +export default ReplyBox; diff --git a/frontend/src/graphql.tsx b/frontend/src/graphql.tsx index a39083a..a0202a7 100644 --- a/frontend/src/graphql.tsx +++ b/frontend/src/graphql.tsx @@ -1829,6 +1829,13 @@ export type GetMessagesByUserSubscriptionVariables = Exact<{ export type GetMessagesByUserSubscription = { __typename?: 'subscription_root', message: Array<{ __typename?: 'message', uuid: any, content: string, created_at: any, room: { __typename?: 'room', uuid: any } }> }; +export type GetMessageByUuidSubscriptionVariables = Exact<{ + uuid: Scalars['uuid']['input']; +}>; + + +export type GetMessageByUuidSubscription = { __typename?: 'subscription_root', message_by_pk?: { __typename?: 'message', uuid: any, content: string, created_at: any, user: { __typename?: 'user', uuid: any, username: string }, room: { __typename?: 'room', uuid: any } } | null }; + export type AddReplyMutationVariables = Exact<{ user_uuid: Scalars['uuid']['input']; msg_uuid: Scalars['uuid']['input']; @@ -2015,6 +2022,45 @@ export function useGetMessagesByUserSubscription(baseOptions: Apollo.Subscriptio } export type GetMessagesByUserSubscriptionHookResult = ReturnType; export type GetMessagesByUserSubscriptionResult = Apollo.SubscriptionResult; +export const GetMessageByUuidDocument = gql` + subscription getMessageByUUID($uuid: uuid!) { + message_by_pk(uuid: $uuid) { + uuid + user { + uuid + username + } + room { + uuid + } + content + created_at + } +} + `; + +/** + * __useGetMessageByUuidSubscription__ + * + * To run a query within a React component, call `useGetMessageByUuidSubscription` and pass it any options that fit your needs. + * When your component renders, `useGetMessageByUuidSubscription` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetMessageByUuidSubscription({ + * variables: { + * uuid: // value for 'uuid' + * }, + * }); + */ +export function useGetMessageByUuidSubscription(baseOptions: Apollo.SubscriptionHookOptions & ({ variables: GetMessageByUuidSubscriptionVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSubscription(GetMessageByUuidDocument, options); + } +export type GetMessageByUuidSubscriptionHookResult = ReturnType; +export type GetMessageByUuidSubscriptionResult = Apollo.SubscriptionResult; export const AddReplyDocument = gql` mutation addReply($user_uuid: uuid!, $msg_uuid: uuid!, $content: String!) { insert_reply_one( diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 3abb96e..6cea024 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -15,6 +15,7 @@ import getUser from "./getUser"; const MainPanel = React.lazy(() => import("./MainPanel")); const LoginPage = React.lazy(() => import("./LoginPage")); const ChatBox = React.lazy(() => import("./ChatBox")); +const ReplyBox = React.lazy(() => import("./ReplyBox")); const FileShare = React.lazy(() => import("./FileShare")); axios.defaults.baseURL = process.env.REACT_APP_BACKEND_URL!; @@ -64,6 +65,7 @@ const MyDraggable: React.FC> = ({ const App = () => { const user = getUser(); const [chatBoxList, setChatBoxList] = useState([]); + const [replyBoxList, setReplyBoxList] = useState([]); const [fileShareList, setFileShareList] = useState([]); const [currentDrag, setCurrentDrag] = useState(""); @@ -77,6 +79,11 @@ const App = () => { setChatBoxList([...chatBoxList, idx]); } }; + const addReplyBox = (idx: any) => { + if (!replyBoxList.includes(idx)) { + setReplyBoxList([...replyBoxList, idx]); + } + }; const addFileShare = (idx: number) => { if (!fileShareList.includes(idx)) { setFileShareList([...fileShareList, idx]); @@ -88,6 +95,15 @@ const App = () => { const removeFileShare = (idx: number) => { setFileShareList(fileShareList.filter((id) => id !== idx)); }; + function getMessageByUUID(uuid: any) { + const {data, error} = graphql.useGetMessageByUuidSubscription({ + skip: !uuid, + variables: { + uuid: uuid, + }, + }); + return data; + } const { data, error, refetch } = graphql.useGetJoinedRoomsQuery({ skip: !user, @@ -131,6 +147,7 @@ const App = () => { user={user} room={data?.user_room[idx].room} handleClose={() => removeChatBox(idx)} + handleReply={addReplyBox} /> @@ -150,6 +167,24 @@ const App = () => { ))} + {replyBoxList.map((msgIdx) => ( + + + + setReplyBoxList(replyBoxList.filter((id) => id !== msgIdx)) + } + /> + + + ))}
); }; From 71bf64c80a0713724c2fb28ce32f2cf07516aa05 Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:21:52 +0800 Subject: [PATCH 09/16] fix: fix hook problem --- frontend/src/index.tsx | 43 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 6cea024..7aedfb7 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -65,7 +65,7 @@ const MyDraggable: React.FC> = ({ const App = () => { const user = getUser(); const [chatBoxList, setChatBoxList] = useState([]); - const [replyBoxList, setReplyBoxList] = useState([]); + const [replyMsg, setReplyMsg] = useState(undefined); const [fileShareList, setFileShareList] = useState([]); const [currentDrag, setCurrentDrag] = useState(""); @@ -79,9 +79,10 @@ const App = () => { setChatBoxList([...chatBoxList, idx]); } }; - const addReplyBox = (idx: any) => { - if (!replyBoxList.includes(idx)) { - setReplyBoxList([...replyBoxList, idx]); + const setReplyBox = (msg_uuid: any) => { + if (!msg_uuid) return; + if (!replyMsg || replyMsg !== msg_uuid) { + setReplyMsg(msg_uuid); } }; const addFileShare = (idx: number) => { @@ -95,6 +96,11 @@ const App = () => { const removeFileShare = (idx: number) => { setFileShareList(fileShareList.filter((id) => id !== idx)); }; + const removeReplyBox = (msg_uuid: any) => { + if (replyMsg === msg_uuid) { + setReplyMsg(undefined); + } + } function getMessageByUUID(uuid: any) { const {data, error} = graphql.useGetMessageByUuidSubscription({ skip: !uuid, @@ -147,7 +153,7 @@ const App = () => { user={user} room={data?.user_room[idx].room} handleClose={() => removeChatBox(idx)} - handleReply={addReplyBox} + handleReply={setReplyBox} /> @@ -167,24 +173,17 @@ const App = () => { ))} - {replyBoxList.map((msgIdx) => ( - - - + + - setReplyBoxList(replyBoxList.filter((id) => id !== msgIdx)) - } - /> - - - ))} + msg={getMessageByUUID(replyMsg)?.message_by_pk} + handleClose={() => removeReplyBox(replyMsg)} + /> + +
); }; From 2b583ea9f58fa47921e5fe387d394a5aee0399df Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:32:48 +0800 Subject: [PATCH 10/16] feat: about-me page --- frontend/public/about-me.html | 85 +++++++++++++++++++++++++++++++++++ frontend/public/index.html | 2 + 2 files changed, 87 insertions(+) create mode 100644 frontend/public/about-me.html diff --git a/frontend/public/about-me.html b/frontend/public/about-me.html new file mode 100644 index 0000000..c89ee0e --- /dev/null +++ b/frontend/public/about-me.html @@ -0,0 +1,85 @@ + + + + + 关于我 + + + +
+

关于我

+

这是按作业要求制作的自我介绍页面,在此处可以了解到关于我的部分信息。

+
+
+

基本信息

+

我是来自未央-工物31班的冯翌航。爱好较广泛,对未知领域多抱有兴趣。

+
+

参与项目

+

病情解卦系统demo

+
    +
  • 项目为纯前端网页,用javascript实现了部分古代典籍中的解卦逻辑。
  • +
  • 项目仅供娱乐参考或民俗研究之用,不对结果正确性作任何保证。
  • +
  • 可点击此处体验。
  • +
+

起卦样例:

+

兄弟未土官鬼寅木         O未土兄弟

子孙酉金妻财子水         X酉金子孙

妻财亥水应爻兄弟戌土         X亥水妻财

子孙申金妻财亥水         

父母午火兄弟丑土         

兄弟辰土世爻官鬼卯木         
+
+

“拓竹杯”参赛作品《㸚爻乂》

+
    +
  • 项目为基于pygame制作的冒险游戏,通过解卦推进剧情。
  • +
  • 项目实现了二维刚体的碰撞交互、六爻关系的数学表述、程序生成的动画效果等内容。
  • +
  • 项目参评拓竹杯•清华大学第八届软件设计大赛,获三等奖。
  • +
+
+

个人诗歌选

+

应试有感二首

+

其一

+

《诗》云:“我生之初,尚无造。” 吟啸,不试多艺非君子,诗礼发冢止增笑。出则必由户,何莫由斯道?子曰不知也,如不可求,从吾所好。

+

2023.3

+

其二

+

道之行废从命转。自运而观,天与人实剌异,知无奈何宜安。万事乃一赌,科考又何异焉?买大买小,买定离手,韶光易散。

一赌审视无阙漏,二赌不陷罝罟,毋失选填。三则胶目放矢,期有所中,去简而就繁。命也,倘中之一旦,仁不能守,岂可胜其寒?逢考必过,逢赌必输,理固宜然。

+

2023.4

+

期末记

+

——仿门兴《跺脚精》而作

+

铀系玩游戏,铍九却不喝啤酒,幺模数对妖魔说:

+

“共轭!”

+

深渊已经断层,Shepp-Logan头仍是幽灵,有限状态机煞有介事地削苹果皮说:

+

“没米!”

+

于是内转换电子抡起碗口粗的撬棍,你支起模方,上街反对官倒,你看到零漂但更爱微扰,你因此不自量力,多退少补,裹上算符,口述:

+

“归一!”

+

你读伽马能谱,画衰变纲图,反投影变换,跳化铜高炉,你登顶由牛顿代替你下山,并拟合,并假设,并检验:

+

“弱水,强酸。左旋右矢,半高全宽。”

+

你用第一范式算散射截面,对分布函数做表子查询,你杀兔子,取胃酸,运渣土,填区间,你养龙给车库的佛塔添砖,命它以四阶收敛至:

+

“格!”

+

但复兴之路已儿孙满堂,如此后竟没有牛马你便是唯一的猪羊,你鸣放,你求根,你在造瘘的五十颗瘤胃上镌刻批注:

+

“前述有误,后继无人。”

+

你不拘小节为民除害,你迭代时总觉得雅可比但俗不可耐。电流形状固定,所以你在等效的电路里

+

开摆。

+

你终于对角占优,线性无关;广义逆沿着测地线,永远追不上广义波前……

+
+

哦,拙劣仿制的分行断句,别在每周二的十五点四十准时哭丧上坟,

+

词是减价的麦田,行有余力就该学文,瞧!多巧妙,一场新的考试

+

你可以试着考个

+

及格的分数。

+

2025.6

+
+
+ + + \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html index 2f21068..ec8beaf 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -29,6 +29,8 @@

这是一个趣味会议软件

进入主页      关于这个工程 +      + 关于我

当前时间加载中... ...

每日一句加载中... ...

From ae26cc0d043d3d3cde2f86c5b519f00416e79952 Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:18:14 +0800 Subject: [PATCH 11/16] feat: dark mode, top button & latest repo list --- frontend/public/about-me.html | 82 +++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/frontend/public/about-me.html b/frontend/public/about-me.html index c89ee0e..80134f4 100644 --- a/frontend/public/about-me.html +++ b/frontend/public/about-me.html @@ -17,10 +17,27 @@ text-align: justify; text-indent: 2em; } + a { + color: inherit; + } + a:hover { + color: gold; + } + #top-btn { + display: none; + position: fixed; + bottom: 20px; + right: 30px; + z-index: 99; + font-size: 16px; + } + .yao-element { + background: black; + } - -
+ +

关于我

这是按作业要求制作的自我介绍页面,在此处可以了解到关于我的部分信息。


@@ -36,7 +53,7 @@

病情解卦系统demo

  • 可点击此处体验。
  • 起卦样例:

    -

    兄弟未土官鬼寅木         O未土兄弟

    子孙酉金妻财子水         X酉金子孙

    妻财亥水应爻兄弟戌土         X亥水妻财

    子孙申金妻财亥水         

    父母午火兄弟丑土         

    兄弟辰土世爻官鬼卯木         
    +

    兄弟未土官鬼寅木         O未土兄弟

    子孙酉金妻财子水         X酉金子孙

    妻财亥水应爻兄弟戌土         X亥水妻财

    子孙申金妻财亥水         

    父母午火兄弟丑土         

    兄弟辰土世爻官鬼卯木         

    “拓竹杯”参赛作品《㸚爻乂》


    + + \ No newline at end of file From 1540a91e17a7322cd70f11ffdb174be4ffdbd4c6 Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:21:31 +0800 Subject: [PATCH 12/16] chore: edit settings --- .github/workflows/backend.yml | 2 +- frontend/.env | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 80df7db..06cb690 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -63,5 +63,5 @@ jobs: context: ./backend push: true tags: | - ghcr.io/${{ vars.DOCKER_TAG }} + ghcr.io/${{ vars.DOCKER_TAG_ALT }} ${{ vars.DOCKER_TAG }} diff --git a/frontend/.env b/frontend/.env index 6b25edb..d98600a 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,3 +1,3 @@ REACT_APP_BACKEND_URL=https://workshop.eesast.com -REACT_APP_HASURA_HTTPLINK=https://workshop.eesast.com/v1/graphql -REACT_APP_HASURA_WSLINK=wss://workshop.eesast.com/v1/graphql +REACT_APP_HASURA_HTTPLINK=https://modern-bulldog-71.hasura.app/v1/graphql +REACT_APP_HASURA_WSLINK=wss://modern-bulldog-71.hasura.app/v1/graphql From 6d4dd6d8b331172f035e15db0b47e2aa7bc73d54 Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:55:17 +0800 Subject: [PATCH 13/16] fix: fix wrong package info --- backend/package.json | 3 ++- backend/yarn.lock | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 6b6f66f..11778ee 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,8 @@ "jsonwebtoken": "9.0.2", "morgan": "1.10.0", "multer": "1.4.5-lts.1", - "nodemailer": "6.9.14" + "nodemailer": "6.9.14", + "react-contexify": "^6.0.0" }, "devDependencies": { "@types/express": "4.17.21", diff --git a/backend/yarn.lock b/backend/yarn.lock index 04728d2..b9ca8aa 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -300,6 +300,11 @@ chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -927,6 +932,13 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +react-contexify@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-contexify/-/react-contexify-6.0.0.tgz#52959bb507d6a31224fe870ae147e211e359abe1" + integrity sha512-jMhz6yZI81Jv3UDj7TXqCkhdkCFEEmvwGCPXsQuA2ZUC8EbCuVQ6Cy8FzKMXa0y454XTDClBN2YFvvmoFlrFkg== + dependencies: + clsx "^1.2.1" + readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" From 1a71213c406ebd609330455ee0ea2725d3bacbdb Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:59:17 +0800 Subject: [PATCH 14/16] fix: fix wrong package info --- backend/package.json | 3 +-- backend/yarn.lock | 12 ------------ frontend/package.json | 1 + frontend/yarn.lock | 40 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/backend/package.json b/backend/package.json index 11778ee..6b6f66f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,8 +7,7 @@ "jsonwebtoken": "9.0.2", "morgan": "1.10.0", "multer": "1.4.5-lts.1", - "nodemailer": "6.9.14", - "react-contexify": "^6.0.0" + "nodemailer": "6.9.14" }, "devDependencies": { "@types/express": "4.17.21", diff --git a/backend/yarn.lock b/backend/yarn.lock index b9ca8aa..04728d2 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -300,11 +300,6 @@ chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" -clsx@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -932,13 +927,6 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" -react-contexify@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/react-contexify/-/react-contexify-6.0.0.tgz#52959bb507d6a31224fe870ae147e211e359abe1" - integrity sha512-jMhz6yZI81Jv3UDj7TXqCkhdkCFEEmvwGCPXsQuA2ZUC8EbCuVQ6Cy8FzKMXa0y454XTDClBN2YFvvmoFlrFkg== - dependencies: - clsx "^1.2.1" - readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" diff --git a/frontend/package.json b/frontend/package.json index 5dbb77b..cabd2ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "jwt-decode": "4.0.0", "md5": "2.3.0", "react": "18.3.1", + "react-contexify": "^6.0.0", "react-dom": "18.3.1", "react-draggable": "4.4.6", "react-router-dom": "6.26.1" diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 54e0ff8..dfba36d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3791,7 +3791,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clsx@^1.1.1: +clsx@^1.1.1, clsx@^1.2.1: version "1.2.1" resolved "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== @@ -8870,6 +8870,13 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +react-contexify@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-contexify/-/react-contexify-6.0.0.tgz#52959bb507d6a31224fe870ae147e211e359abe1" + integrity sha512-jMhz6yZI81Jv3UDj7TXqCkhdkCFEEmvwGCPXsQuA2ZUC8EbCuVQ6Cy8FzKMXa0y454XTDClBN2YFvvmoFlrFkg== + dependencies: + clsx "^1.2.1" + react-dev-utils@^12.0.1: version "12.0.1" resolved "https://registry.npmmirror.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" @@ -9749,7 +9756,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.npmmirror.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9852,7 +9868,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11007,7 +11030,16 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 0734e24c75a7907544bdf81bc98136c2b6d89f8a Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:11:11 +0800 Subject: [PATCH 15/16] fix: fix eslint warning --- frontend/src/ChatBox.tsx | 2 +- frontend/src/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ChatBox.tsx b/frontend/src/ChatBox.tsx index a2f2d9e..075e282 100644 --- a/frontend/src/ChatBox.tsx +++ b/frontend/src/ChatBox.tsx @@ -182,7 +182,7 @@ const MessageBubble: React.FC = ({ user, message, handleRepl } // @ts-ignore const handleItemClick = (args: ItemParams) => { - const { id, event, props } = args; + const { id } = args; switch (id) { case "reply": handleReply(); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 7aedfb7..7fbe65b 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -102,7 +102,7 @@ const App = () => { } } function getMessageByUUID(uuid: any) { - const {data, error} = graphql.useGetMessageByUuidSubscription({ + const {data} = graphql.useGetMessageByUuidSubscription({ skip: !uuid, variables: { uuid: uuid, From 0c6bd211526534a186b16c810560ca6c4873ef6b Mon Sep 17 00:00:00 2001 From: Amen-under-Wu <79252921+Amen-under-Wu@users.noreply.github.com> Date: Thu, 11 Sep 2025 00:55:30 +0800 Subject: [PATCH 16/16] fix: fix wrong env var --- frontend/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/.env b/frontend/.env index d98600a..ce277b9 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,3 +1,3 @@ -REACT_APP_BACKEND_URL=https://workshop.eesast.com +REACT_APP_BACKEND_URL=https://web-workshop-homework-by-fyh.site REACT_APP_HASURA_HTTPLINK=https://modern-bulldog-71.hasura.app/v1/graphql REACT_APP_HASURA_WSLINK=wss://modern-bulldog-71.hasura.app/v1/graphql