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/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..60a8cde 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' } @@ -1508,6 +1822,36 @@ 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 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']; + 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 +1883,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 +1906,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 +1936,56 @@ 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 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( + 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}) { @@ -1612,6 +2021,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 +2045,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 +2066,18 @@ 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); + }, + 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); + }, + 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); }, @@ -1653,12 +2090,18 @@ 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); } }; } -export type Sdk = ReturnType; +export type Sdk = ReturnType; \ No newline at end of file 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; 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/message.graphql b/database/graphql/message.graphql index 994647c..8dacea4 100644 --- a/database/graphql/message.graphql +++ b/database/graphql/message.graphql @@ -15,3 +15,29 @@ 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 + } +} + +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/database/graphql/reply.graphql b/database/graphql/reply.graphql new file mode 100644 index 0000000..56962bc --- /dev/null +++ b/database/graphql/reply.graphql @@ -0,0 +1,17 @@ +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!) { + reply(where: {msg_uuid: {_eq: $msg_uuid}}) { + uuid + user { + uuid + username + } + 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 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", diff --git a/frontend/.env b/frontend/.env index 6b25edb..ce277b9 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_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 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/public/about-me.html b/frontend/public/about-me.html new file mode 100644 index 0000000..80134f4 --- /dev/null +++ b/frontend/public/about-me.html @@ -0,0 +1,161 @@ + + + + + 关于我 + + + +
+

关于我

+

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

+
+
+

基本信息

+

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

+
+

参与项目

+

病情解卦系统demo

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

起卦样例:

+

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

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

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

子孙申金妻财亥水         

父母午火兄弟丑土         

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

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

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

个人诗歌选

+

应试有感二首

+

其一

+

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

+

2023.3

+

其二

+

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

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

+

2023.4

+

期末记

+

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

+

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

+

“共轭!”

+

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

+

“没米!”

+

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

+

“归一!”

+

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

+

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

+

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

+

“格!”

+

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

+

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

+

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

+

开摆。

+

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

+
+

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

+

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

+

你可以试着考个

+

及格的分数。

+

2025.6

+
+

Github仓库(最新)

+
    +
+
+
+ + + + + \ 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 @@

这是一个趣味会议软件

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

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

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

diff --git a/frontend/src/ChatBox.tsx b/frontend/src/ChatBox.tsx index b2619b7..075e282 100644 --- a/frontend/src/ChatBox.tsx +++ b/frontend/src/ChatBox.tsx @@ -3,14 +3,18 @@ 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, useContextMenu } from 'react-contexify'; +import 'react-contexify/ReactContexify.css'; +const MENU_ID = 'reply-menu'; 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); @@ -82,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" }); @@ -129,7 +134,7 @@ const MessageFeed: React.FC = ({ user, messages }) => { ref={index === messages.length - 1 ? bottomRef : null} key={index} > - + {handleReply(message.uuid)}}/>
)) ) : ( @@ -144,14 +149,51 @@ 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( dateUTC.getTime() - dateUTC.getTimezoneOffset() * 60000 ); + + const { show } = useContextMenu({ + 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, + props: { + key: 'value' + }, + position: { + x: 0, + y: 0 + }, + }) + } + // @ts-ignore + const handleItemClick = (args: ItemParams) => { + const { id } = args; + switch (id) { + case "reply": + handleReply(); + break; + default: + break; + //etc... + } + } + + return (
= ({ user, message }) => { flexWrap: "nowrap", alignItems: isSelf ? "flex-end" : "flex-start", }} + onContextMenu={handleContextMenu} >
{message.user.username} @@ -178,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 4a7f6f0..a0202a7 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,36 @@ 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 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']; + 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 +1883,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 +1906,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 +1987,153 @@ 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 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( + 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 +2290,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 +2400,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 diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 3abb96e..7fbe65b 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 [replyMsg, setReplyMsg] = useState(undefined); const [fileShareList, setFileShareList] = useState([]); const [currentDrag, setCurrentDrag] = useState(""); @@ -77,6 +79,12 @@ const App = () => { setChatBoxList([...chatBoxList, idx]); } }; + const setReplyBox = (msg_uuid: any) => { + if (!msg_uuid) return; + if (!replyMsg || replyMsg !== msg_uuid) { + setReplyMsg(msg_uuid); + } + }; const addFileShare = (idx: number) => { if (!fileShareList.includes(idx)) { setFileShareList([...fileShareList, idx]); @@ -88,6 +96,20 @@ 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} = graphql.useGetMessageByUuidSubscription({ + skip: !uuid, + variables: { + uuid: uuid, + }, + }); + return data; + } const { data, error, refetch } = graphql.useGetJoinedRoomsQuery({ skip: !user, @@ -131,6 +153,7 @@ const App = () => { user={user} room={data?.user_room[idx].room} handleClose={() => removeChatBox(idx)} + handleReply={setReplyBox} /> @@ -150,6 +173,17 @@ const App = () => { ))} + + + removeReplyBox(replyMsg)} + /> + +
); }; 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==