├── README.md ├── backend ├── .babelrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── app.ts │ ├── auth.ts │ ├── graphql │ │ ├── NodeDefinitions.ts │ │ └── registeredTypes.ts │ ├── modules │ │ ├── comments │ │ │ ├── CommentLoader.ts │ │ │ ├── CommentModel.ts │ │ │ ├── CommentType.ts │ │ │ ├── mutations │ │ │ │ ├── CreateComment.ts │ │ │ │ ├── LikeComment.ts │ │ │ │ └── index.ts │ │ │ └── subscriptions │ │ │ │ ├── CommentLikeSubscription.ts │ │ │ │ ├── CreateCommentSubscription.ts │ │ │ │ └── index.ts │ │ ├── posts │ │ │ ├── PostLoader.ts │ │ │ ├── PostModel.ts │ │ │ ├── PostType.ts │ │ │ ├── mutations │ │ │ │ ├── LikePost.ts │ │ │ │ ├── PostCreation.ts │ │ │ │ └── index.ts │ │ │ └── subscriptions │ │ │ │ ├── PostCreation.ts │ │ │ │ ├── PostLike.ts │ │ │ │ └── index.ts │ │ ├── reply │ │ │ ├── ReplyLoader.ts │ │ │ ├── ReplyModel.ts │ │ │ ├── ReplyType.ts │ │ │ ├── mutations │ │ │ │ ├── CreateReply.ts │ │ │ │ ├── LikeReply.ts │ │ │ │ └── index.ts │ │ │ └── subscriptions │ │ │ │ ├── ReplyCreationSubscription.ts │ │ │ │ ├── ReplyLikeSubscription.ts │ │ │ │ └── index.ts │ │ └── users │ │ │ ├── UserLoader.ts │ │ │ ├── UserModel.ts │ │ │ ├── UserType.ts │ │ │ └── mutations │ │ │ ├── CreateUser.ts │ │ │ ├── Login.ts │ │ │ └── index.ts │ ├── schema │ │ ├── MutationType.ts │ │ ├── QueryType.ts │ │ ├── Schema.ts │ │ └── SubscriptionType.ts │ └── server.ts ├── tsconfig.json └── tslint.json └── frontend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt ├── socialnetwork-post_example.gif └── socialnetwork-register_example.gif ├── schema └── schema.graphql ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── Components │ ├── Layout │ │ ├── Footer │ │ │ ├── Footer.tsx │ │ │ └── index.tsx │ │ ├── Header │ │ │ ├── Navbar │ │ │ │ ├── Navbar.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Layout.tsx │ │ └── index.tsx │ └── index.tsx ├── Pages │ ├── FeedPage │ │ ├── Components │ │ │ ├── Comments │ │ │ │ ├── Comment.tsx │ │ │ │ ├── CommentCreation.tsx │ │ │ │ ├── Comments.tsx │ │ │ │ └── index.tsx │ │ │ ├── Posts │ │ │ │ ├── Post.tsx │ │ │ │ ├── PostCreation.tsx │ │ │ │ ├── Posts.tsx │ │ │ │ └── index.tsx │ │ │ ├── Replies │ │ │ │ ├── Replies.tsx │ │ │ │ ├── Reply.tsx │ │ │ │ ├── ReplyCreation.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── FeedPage.tsx │ │ └── index.tsx │ ├── LoginPage │ │ ├── Components │ │ │ ├── LoginForm.tsx │ │ │ └── index.tsx │ │ ├── LoginPage.tsx │ │ └── index.tsx │ ├── RegisterPage │ │ ├── Components │ │ │ ├── RegisterForm.tsx │ │ │ └── index.tsx │ │ ├── RegisterPage.tsx │ │ └── index.tsx │ └── index.tsx ├── Services │ └── Subscriptions │ │ ├── CommentLikeSubscription.tsx │ │ ├── NewCommentsSubscription.tsx │ │ ├── NewPostsSubscription.tsx │ │ ├── NewRepliesSubscription.tsx │ │ ├── PostLikeSubscription.tsx │ │ ├── ReplyLikeSubscription.tsx │ │ ├── Subscriptions.tsx │ │ └── index.tsx ├── config.tsx ├── index.tsx ├── react-app-env.d.ts ├── relay │ ├── environment.tsx │ └── fetchGraphQL.tsx ├── routes.tsx ├── serviceWorker.ts ├── setupTests.ts └── tailwind.css ├── tailwind.config.js ├── tsconfig.json └── types ├── babel-plugin-relay.d.ts └── react-router-dom.d.ts /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | This is a social network made using a Graphql backend, MongoDB database and a Relay Web Client. 4 | 5 | ## How to contribute 6 | 7 | To contribute just create any issue or PR if you like. I will read all. Any doubts contact me if you want. 8 | -------------------------------------------------------------------------------- /backend/.babelrc: -------------------------------------------------------------------------------- 1 | //.babelrc 2 | 3 | { 4 | "presets": [ 5 | "@babel/preset-env", 6 | "@babel/preset-typescript" 7 | ], 8 | "plugins": [ 9 | // https://github.com/parcel-bundler/parcel/issues/871#issuecomment-370135105 10 | // https://github.com/babel/babel-loader/issues/560#issuecomment-370180866 11 | "@babel/plugin-transform-runtime" 12 | ] 13 | } -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .env -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ## Social Network Backend 2 | 3 | This is a backend GraphQL Api to serve solutions for an social network project. 4 | 5 | ### To run 6 | 7 | Firstle install dependencies using npm or yarn package manager `npm install` or `yarn install`. 8 | 9 | You must install a MongoDB environment and execute `mongod` to run it. 10 | 11 | Run backend using `npm start` on `yarn start` depending on your local environment. 12 | 13 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socialnetworkbackend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/server", 6 | "scripts": { 7 | "prebuild": "tslint -c tslint.json -p tsconfig.json --fix", 8 | "babel": "babel-node --extensions \".es6,.js,.es,.jsx,.mjs,.ts,.tsx\"", 9 | "build": "npm run clear && babel src --extensions \".es6,.js,.es,.jsx,.mjs,.ts\" --ignore *.spec.js --out-dir dist --copy-files", 10 | "clear": "rimraf ./dist", 11 | "prestart": "npm run build", 12 | "start": "nodemon .", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@babel/cli": "^7.8.4", 19 | "@babel/core": "^7.9.0", 20 | "@babel/node": "^7.8.7", 21 | "@babel/plugin-transform-runtime": "^7.10.5", 22 | "@babel/preset-env": "^7.9.0", 23 | "@babel/preset-typescript": "^7.10.4", 24 | "@types/bcrypt": "^3.0.0", 25 | "@types/graphql-relay": "^0.4.11", 26 | "@types/ioredis": "^4.16.2", 27 | "@types/jsonwebtoken": "^8.3.8", 28 | "@types/kcors": "^2.2.3", 29 | "@types/koa": "^2.11.3", 30 | "@types/koa-bodyparser": "^4.3.0", 31 | "@types/koa-graphql": "^0.8.3", 32 | "@types/koa-logger": "^3.1.1", 33 | "@types/koa-router": "^7.4.1", 34 | "@types/mongoose": "^5.7.8", 35 | "typescript": "^3.8.3" 36 | }, 37 | "dependencies": { 38 | "bcrypt": "^5.0.0", 39 | "dataloader": "^2.0.0", 40 | "dotenv": "^8.2.0", 41 | "graphql": "^15.7.2", 42 | "graphql-playground-middleware-koa": "^1.6.13", 43 | "graphql-redis-subscriptions": "^2.6.1", 44 | "graphql-relay": "^0.9.0", 45 | "graphql-relay-subscription": "^0.2.1", 46 | "graphql-subscriptions": "^2.0.0", 47 | "ioredis": "^4.17.1", 48 | "jsonwebtoken": "^8.5.1", 49 | "kcors": "^2.2.2", 50 | "koa": "^2.12.0", 51 | "koa-graphql": "^0.12.0", 52 | "koa-logger": "^3.2.1", 53 | "koa-router": "^8.0.8", 54 | "mongoose": "^5.9.7", 55 | "nodemon": "^2.0.2", 56 | "redis": "^3.0.2", 57 | "rimraf": "^3.0.2", 58 | "subscriptions-transport-ws": "^0.11.0", 59 | "tslint": "^6.1.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/app.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import dotenv from 'dotenv'; 3 | import path from 'path'; 4 | import { GraphQLError } from 'graphql'; 5 | import { PubSub } from 'graphql-subscriptions'; 6 | import Koa from 'koa'; 7 | import Router from 'koa-router'; 8 | import logger from 'koa-logger'; 9 | import cors from 'kcors'; 10 | import {graphqlHTTP} from 'koa-graphql'; 11 | import koaPlayground from 'graphql-playground-middleware-koa'; 12 | 13 | import { Schema } from './schema/Schema'; 14 | import User, { IUser } from './modules/users/UserModel'; 15 | import getUser from './auth'; 16 | 17 | dotenv.config({path: path.join(__dirname, '/./../.env')}); 18 | 19 | mongoose.connect(process.env.MONGODB_URL, {useNewUrlParser: true, useUnifiedTopology: true}); 20 | 21 | const router = new Router(); 22 | const app = new Koa(); 23 | 24 | app.use(logger()); 25 | app.use(cors()); 26 | 27 | const graphQLHttpSettings = async (req: any) => { 28 | const user: IUser | {user: null} = await getUser(req.headers.authorization); 29 | return { 30 | graphql: true, 31 | schema: Schema, 32 | context: { 33 | user, 34 | req 35 | }, 36 | formatError: (error: GraphQLError) => { 37 | console.log(error.message); 38 | console.log(error.locations); 39 | console.log(error.stack); 40 | return { 41 | message: error.message, 42 | locations: error.locations, 43 | stack: error.stack 44 | } 45 | } 46 | } 47 | } 48 | 49 | const graphqlServerConfig = graphqlHTTP(graphQLHttpSettings); 50 | 51 | router.all('/graphql', graphqlServerConfig); 52 | router.all('/graphql', koaPlayground({ 53 | endpoint: 'graphql', 54 | subscriptionEndpoint: '/subscriptions' 55 | })); 56 | 57 | app.use(router.routes()).use(router.allowedMethods()); 58 | 59 | 60 | export default app; 61 | 62 | 63 | 64 | const pubsub = new PubSub(); 65 | export { pubsub }; -------------------------------------------------------------------------------- /backend/src/auth.ts: -------------------------------------------------------------------------------- 1 | import User, { IUser } from "./modules/users/UserModel"; 2 | 3 | 4 | const getUser = async (token: string): Promise => { 5 | try { 6 | const user = await User.findByToken(token); 7 | if (user) { 8 | user.verifyAuthToken(); 9 | return user; 10 | } 11 | } catch(err) { 12 | return { user: null }; 13 | } 14 | } 15 | 16 | export default getUser; -------------------------------------------------------------------------------- /backend/src/graphql/NodeDefinitions.ts: -------------------------------------------------------------------------------- 1 | import { nodeDefinitions, fromGlobalId } from "graphql-relay"; 2 | import registeredTypes from "./registeredTypes"; 3 | 4 | export const {nodeInterface, nodeField, nodesField} = nodeDefinitions( 5 | (globalId) => { 6 | const {type, id} = fromGlobalId(globalId); 7 | const registeredType = registeredTypes.find(x => { 8 | return type === x.name 9 | }); 10 | return registeredType.loader(id); 11 | }, 12 | (obj) => { 13 | const registeredType = registeredTypes.find(x => obj instanceof x.dbType); 14 | if (registeredType) return registeredType.qlType 15 | return null; 16 | } 17 | ); -------------------------------------------------------------------------------- /backend/src/graphql/registeredTypes.ts: -------------------------------------------------------------------------------- 1 | import { loadUser } from '../modules/users/UserLoader'; 2 | import { postLoader } from '../modules/posts/PostLoader'; 3 | import { commentLoader } from '../modules/comments/CommentLoader'; 4 | import { replyLoader } from '../modules/reply/ReplyLoader'; 5 | import User from '../modules/users/UserModel'; 6 | import Post from '../modules/posts/PostModel'; 7 | import Reply from '../modules/reply/ReplyModel'; 8 | import Comment from '../modules/comments/CommentModel'; 9 | 10 | const registeredTypes = [ 11 | { 12 | name: 'User', 13 | qlType: 'UserType', 14 | dbType: User, 15 | loader: loadUser 16 | }, 17 | { 18 | name: 'Post', 19 | qlType: 'PostType', 20 | dbType: Post, 21 | loader: postLoader 22 | }, 23 | { 24 | name: 'Comment', 25 | qlType: 'CommentType', 26 | dbType: Comment, 27 | loader: commentLoader 28 | }, 29 | { 30 | name: 'Reply', 31 | qlType: 'ReplyType', 32 | dbType: Reply, 33 | loader: replyLoader 34 | } 35 | ] 36 | 37 | export default registeredTypes; -------------------------------------------------------------------------------- /backend/src/modules/comments/CommentLoader.ts: -------------------------------------------------------------------------------- 1 | import Comment, {IComment} from './CommentModel'; 2 | import Dataloader from 'dataloader'; 3 | 4 | 5 | const commentDataLoader = new Dataloader((keys: string[]) => Comment.find({_id: {$in: keys}})); 6 | const commentByReplyDataLoader = new Dataloader((keys: string[]) => Comment.find({replies: {$in: keys}})); 7 | 8 | export const commentLoader = async (id: string) => { 9 | const commentFounded = await commentDataLoader.load(id); 10 | console.log('comment founded by dataloader: ', commentFounded); 11 | return commentFounded; 12 | } 13 | 14 | export const commentLoaderByReply = async (replyId: string) => { 15 | const commentFounded = await commentByReplyDataLoader.load(replyId); 16 | return commentFounded; 17 | } 18 | 19 | // TODO implements DataLoader for this 20 | export const commentsFromPostLoader = async (postId: string) => { 21 | const comments = Comment.findCommentsForPost(postId); 22 | return comments 23 | } -------------------------------------------------------------------------------- /backend/src/modules/comments/CommentModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { mongo } from 'mongoose'; 2 | 3 | export interface IComment extends mongoose.Document { 4 | author: string, 5 | content: string, 6 | likes: string[], 7 | createdAt: Date, 8 | updatedAt: Date, 9 | replies: string[] 10 | } 11 | 12 | export interface ICommentModel extends mongoose.Model { 13 | findCommentsForPost(postId: string): IComment[] 14 | } 15 | 16 | const commentSchema = new mongoose.Schema({ 17 | author: { 18 | type: mongoose.Schema.Types.ObjectId, 19 | ref: 'User', 20 | required: true 21 | }, 22 | content: { 23 | type: String, 24 | required: true 25 | }, 26 | likes: [{ 27 | type: mongoose.Schema.Types.ObjectId, 28 | ref: 'User' 29 | }], 30 | replies: [{ 31 | type: mongoose.Schema.Types.ObjectId, 32 | ref: 'Reply' 33 | }] 34 | }, { 35 | timestamps: true 36 | }); 37 | 38 | commentSchema.statics.findCommentsForPost = async (postId: string) => { 39 | const commentsOfPost = await Comment.find({post: postId}).sort({createdAt: 1}); 40 | return commentsOfPost; 41 | }; 42 | 43 | const Comment = mongoose.model('Comment_SocialNetwork', commentSchema); 44 | 45 | export default Comment; 46 | -------------------------------------------------------------------------------- /backend/src/modules/comments/CommentType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLBoolean } from 'graphql'; 2 | import { connectionDefinitions, connectionArgs, connectionFromArray, globalIdField } from 'graphql-relay'; 3 | 4 | import userType from '../users/UserType'; 5 | import { IComment } from './CommentModel'; 6 | import { loadUser } from '../users/UserLoader'; 7 | import { ReplyConnection } from '../reply/ReplyType'; 8 | import { replyLoader } from '../reply/ReplyLoader'; 9 | import { nodeInterface } from '../../graphql/NodeDefinitions'; 10 | 11 | const CommentType = new GraphQLObjectType({ 12 | name: 'CommentType', 13 | description: 'Comment type', 14 | fields: () => ({ 15 | id: globalIdField('Comment'), 16 | author: { 17 | type: userType, 18 | resolve: (comment) => loadUser(comment.author) 19 | }, 20 | content: { 21 | type: GraphQLString, 22 | resolve: (comment) => comment.content 23 | }, 24 | likes: { 25 | type: GraphQLInt, 26 | resolve: (comment) => comment.likes.length 27 | }, 28 | userHasLiked: { 29 | type: GraphQLBoolean, 30 | resolve: (comment, args, {user}) => comment.likes.includes(user.id) 31 | }, 32 | createdAt: { 33 | type: GraphQLString, 34 | resolve: (comment) => comment.createdAt 35 | }, 36 | updatedAt: { 37 | type: GraphQLString, 38 | resolve: (comment) => comment.updatedAt 39 | }, 40 | replies: { 41 | type: ReplyConnection, 42 | args: connectionArgs, 43 | resolve: (comment, args) => { 44 | return connectionFromArray( 45 | comment.replies.map(replyLoader), 46 | args 47 | ) 48 | } 49 | } 50 | }), 51 | interfaces: [nodeInterface] 52 | }); 53 | 54 | export const {connectionType: CommentConnection} = 55 | connectionDefinitions({nodeType: CommentType}); 56 | 57 | export default CommentType -------------------------------------------------------------------------------- /backend/src/modules/comments/mutations/CreateComment.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString } from 'graphql'; 2 | import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay'; 3 | 4 | import Comment from '../CommentModel'; 5 | import CommentType from '../CommentType'; 6 | import { postLoader } from '../../../modules/posts/PostLoader'; 7 | import { IUser } from '../../../modules/users/UserModel'; 8 | import { pubsub } from '../../../app'; 9 | import { commentLoader } from '../CommentLoader'; 10 | 11 | const CreateComment = mutationWithClientMutationId({ 12 | name: 'CreateComment', 13 | description: 'Create Comment Mutation', 14 | inputFields: { 15 | content: { 16 | type: GraphQLString 17 | }, 18 | post: { 19 | type: GraphQLString 20 | } 21 | }, 22 | outputFields: { 23 | comment: { 24 | type: CommentType, 25 | resolve: async (comment) => await commentLoader(comment) 26 | } 27 | }, 28 | mutateAndGetPayload: async ({content, post} : { 29 | content: string, 30 | post: string 31 | }, {user}: {user: IUser}) => { 32 | try { 33 | 34 | const {type, id} = fromGlobalId(post); 35 | 36 | const postId = id; 37 | 38 | const comment = new Comment({author: user.id, content}); 39 | await comment.save(); 40 | 41 | const postFinded = await postLoader(postId); 42 | postFinded.comments.push(comment.id); 43 | await postFinded.save(); 44 | 45 | pubsub.publish('newComment', comment); 46 | 47 | return comment; 48 | } catch (err) { 49 | console.log(err); 50 | } 51 | } 52 | }); 53 | 54 | export default CreateComment; -------------------------------------------------------------------------------- /backend/src/modules/comments/mutations/LikeComment.ts: -------------------------------------------------------------------------------- 1 | import { mutationWithClientMutationId, fromGlobalId } from "graphql-relay"; 2 | import { GraphQLString } from "graphql"; 3 | 4 | import CommentType from "../CommentType"; 5 | import Comment from '../CommentModel'; 6 | import { IUser } from "../../../modules/users/UserModel"; 7 | import { pubsub } from "../../../app"; 8 | import { commentLoader } from "../CommentLoader"; 9 | 10 | const LikeComment = mutationWithClientMutationId({ 11 | name: 'LikeComment', 12 | description: 'Update total likes for a comment type', 13 | inputFields: { 14 | comment: { 15 | type: GraphQLString 16 | } 17 | }, 18 | outputFields: { 19 | comment: { 20 | type: CommentType, 21 | resolve: async (comment) => await commentLoader(comment) 22 | } 23 | }, 24 | mutateAndGetPayload: async ({comment}, {user}: {user: IUser}) => { 25 | try { 26 | 27 | const {type, id} = fromGlobalId(comment); 28 | 29 | const commentId = id; 30 | const commentFound = await Comment.findOne({_id: commentId}); 31 | 32 | if (commentFound.likes.includes(user.id)) { 33 | 34 | const indexOf = commentFound.likes.indexOf(user.id); 35 | commentFound.likes.splice(indexOf, 1); 36 | await commentFound.save(); 37 | 38 | pubsub.publish('commentLike', commentFound); 39 | return commentFound 40 | }; 41 | 42 | commentFound.likes.push(user.id); 43 | await commentFound.save(); 44 | 45 | pubsub.publish('commentLike', commentFound); 46 | 47 | return commentFound; 48 | } catch (err) { 49 | console.log(err); 50 | } 51 | } 52 | }); 53 | 54 | export default LikeComment; -------------------------------------------------------------------------------- /backend/src/modules/comments/mutations/index.ts: -------------------------------------------------------------------------------- 1 | import CreateComment from './CreateComment'; 2 | import LikeComment from './LikeComment' 3 | 4 | export default { 5 | CreateComment, 6 | LikeComment 7 | }; -------------------------------------------------------------------------------- /backend/src/modules/comments/subscriptions/CommentLikeSubscription.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString } from "graphql"; 2 | import { withFilter } from "graphql-subscriptions"; 3 | import { subscriptionWithClientId } from "graphql-relay-subscription"; 4 | 5 | import { commentLoader } from "../CommentLoader"; 6 | import CommentType from "../CommentType"; 7 | import { pubsub } from "../../../app"; 8 | import { postLoaderByComment } from "../../posts/PostLoader"; 9 | import { loadUser } from "../../users/UserLoader"; 10 | 11 | const CommentLikeSubscription = subscriptionWithClientId({ 12 | name: 'CommentLikeSubscription', 13 | description: 'Comment Like subscription', 14 | inputFields: {}, 15 | outputFields: { 16 | comment: { 17 | type: CommentType, 18 | resolve: (commentObj: any) => commentLoader(commentObj.id) 19 | } 20 | }, 21 | subscribe: withFilter( 22 | (input: any, context: any) => { 23 | return pubsub.asyncIterator('commentLike'); 24 | }, 25 | async (commentPayload: any, variables: any) => { 26 | 27 | const postFounded = await postLoaderByComment(commentPayload._id); 28 | const postFoundedAuthor = await loadUser(postFounded.author); 29 | 30 | const loggedUser = variables.user; 31 | 32 | return `${loggedUser._id}` === `${postFoundedAuthor._id}` || postFoundedAuthor.friends.includes(loggedUser._id); 33 | } 34 | ), 35 | getPayload: (payloadCommentLike: any) => { 36 | 37 | return { 38 | id: payloadCommentLike.id, 39 | } 40 | } 41 | }); 42 | 43 | export default CommentLikeSubscription; -------------------------------------------------------------------------------- /backend/src/modules/comments/subscriptions/CreateCommentSubscription.ts: -------------------------------------------------------------------------------- 1 | import { subscriptionWithClientId } from "graphql-relay-subscription"; 2 | import { withFilter } from "graphql-subscriptions"; 3 | 4 | import CommentType from "../CommentType"; 5 | import { commentLoader } from "../CommentLoader"; 6 | import { pubsub } from "../../../app"; 7 | import { IComment } from "../CommentModel"; 8 | import { postLoaderByComment } from "../../posts/PostLoader"; 9 | import { loadUser } from "../../users/UserLoader"; 10 | import { GraphQLString } from "graphql"; 11 | import PostType from "../../posts/PostType"; 12 | 13 | const CreateCommentSubscription = subscriptionWithClientId({ 14 | name: "CreateCommentSubscription", 15 | description: "Create Comment Subscription", 16 | inputFields: {}, 17 | outputFields: { 18 | comment: { 19 | type: CommentType, 20 | resolve: async (comment: any) => await commentLoader(comment.id) 21 | }, 22 | post: { 23 | type: PostType, 24 | resolve: async (comment: any) =>{ 25 | const postFounded = await postLoaderByComment(comment.id); 26 | return postFounded 27 | } 28 | } 29 | }, 30 | subscribe: withFilter( 31 | (input: any, context: any) => { 32 | return pubsub.asyncIterator('newComment'); 33 | }, async (comment: IComment, variables: any) => { 34 | const postFounded = await postLoaderByComment(comment._id); 35 | const postFoundedAuthor = await loadUser(postFounded.author); 36 | 37 | const loggedUser = variables.user; 38 | 39 | return `${loggedUser._id}` === `${postFoundedAuthor._id}` || postFoundedAuthor.friends.includes(loggedUser._id); 40 | } 41 | ), 42 | getPayload: (obj: any) => { 43 | return { 44 | id: obj.id 45 | } 46 | } 47 | }); 48 | 49 | export default CreateCommentSubscription; -------------------------------------------------------------------------------- /backend/src/modules/comments/subscriptions/index.ts: -------------------------------------------------------------------------------- 1 | import {default as CreateCommentSubscription} from './CreateCommentSubscription'; 2 | import {default as CommentLikeSubscription} from './CommentLikeSubscription'; 3 | 4 | export default { 5 | CreateCommentSubscription, 6 | CommentLikeSubscription 7 | } -------------------------------------------------------------------------------- /backend/src/modules/posts/PostLoader.ts: -------------------------------------------------------------------------------- 1 | import Post, { IPost } from './PostModel'; 2 | import Dataloader from 'dataloader'; 3 | 4 | 5 | const postDataLoader = new Dataloader((keys: string[]) => Post.find({_id: {$in: keys}})); 6 | const postByCommentDataLoader = new Dataloader((keys: string[]) => Post.find({comments: {$in: keys}})); 7 | 8 | export const postLoader = async (id: string) => { 9 | 10 | console.log('postloader call'); 11 | 12 | const postFounded = await postDataLoader.load(id); 13 | console.log('post founded by dataloader: ', postFounded); 14 | return postFounded; 15 | }; 16 | 17 | export const postLoaderByComment = async (commentId: string) => { 18 | 19 | console.log('postloader by comments call'); 20 | 21 | const postFounded = await postByCommentDataLoader.load(commentId); 22 | return postFounded; 23 | } 24 | 25 | // TODO implement dataloader for this database call 26 | export const postsLoaderByAuthors = async (ids: string[]) => { 27 | 28 | const postList = await Post.findByAuthorIdList(ids); 29 | return postList; 30 | } 31 | 32 | // TODO implement dataloader for multiple post for one author if is logical 33 | export const authorPostsLoader = async (id: string) => { 34 | 35 | console.log('postloader by author call'); 36 | 37 | const authorPosts = await Post.findAuthorPosts(id); 38 | return authorPosts 39 | }; 40 | 41 | export const loggedUserPosts = async (token: string) => { 42 | 43 | console.log('postloader by loggeduser call'); 44 | 45 | const posts = await Post.findLoggedUserPosts(token); 46 | return posts; 47 | }; -------------------------------------------------------------------------------- /backend/src/modules/posts/PostModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import jsonwebtoken from 'jsonwebtoken'; 3 | 4 | 5 | export interface IPost extends mongoose.Document { 6 | author: string; 7 | content: string; 8 | createdAt: Date; 9 | updatedAt: Date; 10 | likes: string[]; 11 | comments: string[] 12 | } 13 | 14 | export interface IPostModel extends mongoose.Model { 15 | findAuthorPosts(id: string): IPost[]; 16 | findByAuthorIdList(ids: string[]): IPost[]; 17 | findLoggedUserPosts(token: string): IPost[]; 18 | } 19 | 20 | const postSchema = new mongoose.Schema({ 21 | author: { 22 | type: Schema.Types.ObjectId, 23 | ref: 'User', 24 | required: true 25 | }, 26 | content: { 27 | type: String, 28 | required: true 29 | }, 30 | likes: [{ 31 | type: Schema.Types.ObjectId, 32 | ref: 'User' 33 | }], 34 | comments: [{ 35 | type: Schema.Types.ObjectId, 36 | ref: 'Comment' 37 | }] 38 | }, { 39 | timestamps: true 40 | }); 41 | 42 | postSchema.statics.findAuthorPosts = async (id: string) => { 43 | const posts = await Post.find({author: id}).sort({createdAt: -1}); 44 | return posts; 45 | } 46 | 47 | postSchema.statics.findByAuthorIdList = async (ids: string[]) => { 48 | const posts = await Post.find({author: {$in: ids}}).sort({createdAt: -1}); 49 | return posts; 50 | } 51 | 52 | postSchema.statics.findLoggedUserPosts = async (token: string) => { 53 | const jsonPayload: any = jsonwebtoken.decode(token); 54 | return await Post.find({author: jsonPayload._id}).sort({createdAt: -1}); 55 | } 56 | 57 | 58 | const Post = mongoose.model('Post_SocialNetwork', postSchema); 59 | 60 | export default Post; -------------------------------------------------------------------------------- /backend/src/modules/posts/PostType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLBoolean } from 'graphql'; 2 | import { connectionDefinitions, connectionArgs, connectionFromArray, globalIdField } from 'graphql-relay'; 3 | 4 | import userType from '../users/UserType'; 5 | import { IPost } from './PostModel'; 6 | import { CommentConnection } from '../comments/CommentType'; 7 | import { commentLoader } from '../comments/CommentLoader'; 8 | import { loadUser } from '../users/UserLoader'; 9 | import { nodeInterface } from '../../graphql/NodeDefinitions'; 10 | 11 | const PostType = new GraphQLObjectType({ 12 | name: 'PostType', 13 | description: 'Post type', 14 | fields: () => ({ 15 | id: globalIdField('Post'), 16 | author: { 17 | type: userType, 18 | resolve: async (post) => await loadUser(post.author) 19 | }, 20 | content: { 21 | type: GraphQLString, 22 | resolve: (post) => post.content 23 | }, 24 | likes: { 25 | type: GraphQLInt, 26 | resolve: (post) => post.likes.length 27 | }, 28 | userHasLiked: { 29 | type: GraphQLBoolean, 30 | resolve: (post, args, {user}) => post.likes.includes(user.id) 31 | }, 32 | createdAt: { 33 | type: GraphQLString, 34 | resolve: (post) => post.createdAt 35 | }, 36 | updatedAt: { 37 | type: GraphQLString, 38 | resolve: (post) => post.updatedAt 39 | }, 40 | comments: { 41 | type: CommentConnection, 42 | args: connectionArgs, 43 | resolve: (post, args) => { 44 | return connectionFromArray( 45 | post.comments.map(commentLoader), 46 | args 47 | ) 48 | } 49 | } 50 | }), 51 | interfaces: [nodeInterface] 52 | }); 53 | 54 | export const {connectionType: PostConnection} = 55 | connectionDefinitions({nodeType: PostType}); 56 | 57 | export default PostType; -------------------------------------------------------------------------------- /backend/src/modules/posts/mutations/LikePost.ts: -------------------------------------------------------------------------------- 1 | import { mutationWithClientMutationId, fromGlobalId } from "graphql-relay"; 2 | import { GraphQLString } from "graphql"; 3 | 4 | import PostType from "../PostType"; 5 | import { IUser } from "../../../modules/users/UserModel"; 6 | import { postLoader } from "../PostLoader"; 7 | import { pubsub } from "../../../app"; 8 | 9 | const LikePost = mutationWithClientMutationId({ 10 | name: 'LikePost', 11 | description: 'Mutation for like handling for posts', 12 | inputFields: { 13 | post: { 14 | type: GraphQLString 15 | } 16 | }, 17 | outputFields: { 18 | post: { 19 | type: PostType, 20 | resolve: async (post) => await postLoader(post.id) 21 | } 22 | }, 23 | mutateAndGetPayload: async ({post}: {post: string}, {user}: {user: IUser}) => { 24 | try { 25 | 26 | const {type, id} = fromGlobalId(post); 27 | 28 | const postId = id; 29 | const postFounded = await postLoader(postId); 30 | 31 | if (postFounded.likes.includes(user.id)) { 32 | 33 | const indexOf = postFounded.likes.indexOf(user.id); 34 | postFounded.likes.splice(indexOf, 1); 35 | await postFounded.save(); 36 | 37 | pubsub.publish('postLike', postFounded); 38 | 39 | return postFounded; 40 | } 41 | 42 | postFounded.likes.push(user._id); 43 | await postFounded.save(); 44 | 45 | pubsub.publish('postLike', postFounded); 46 | 47 | return postFounded; 48 | } catch(err) { 49 | console.log(err); 50 | } 51 | } 52 | }); 53 | 54 | export default LikePost; -------------------------------------------------------------------------------- /backend/src/modules/posts/mutations/PostCreation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import { mutationWithClientMutationId } from 'graphql-relay'; 3 | 4 | import PostType from '../PostType'; 5 | import Post from '../PostModel'; 6 | import { IUser } from '../../../modules/users/UserModel'; 7 | import { pubsub } from '../../../app'; 8 | import { postLoader } from '../PostLoader'; 9 | 10 | const PostCreation = mutationWithClientMutationId({ 11 | name: 'PostCreation', 12 | description: 'Post Creation', 13 | inputFields: { 14 | content: { 15 | type: GraphQLString 16 | } 17 | }, 18 | outputFields: { 19 | post: { 20 | type: PostType, 21 | resolve: async (post) => await postLoader(post.id) 22 | } 23 | }, 24 | mutateAndGetPayload: async ({content}, {user}: {user: IUser}) => { 25 | try { 26 | 27 | const postCreated = new Post({content, author: `${user.id}`}); 28 | await postCreated.save(); 29 | 30 | user.posts.push(`${postCreated.id}`); 31 | await user.save(); 32 | 33 | pubsub.publish('newPost', postCreated); 34 | 35 | return postCreated; 36 | } catch (err) { 37 | console.log(err); 38 | } 39 | } 40 | }); 41 | 42 | export default PostCreation; -------------------------------------------------------------------------------- /backend/src/modules/posts/mutations/index.ts: -------------------------------------------------------------------------------- 1 | import PostCreation from './PostCreation'; 2 | import LikePost from './LikePost'; 3 | 4 | export default { 5 | PostCreation, 6 | LikePost 7 | }; -------------------------------------------------------------------------------- /backend/src/modules/posts/subscriptions/PostCreation.ts: -------------------------------------------------------------------------------- 1 | import { subscriptionWithClientId } from 'graphql-relay-subscription'; 2 | import { withFilter } from 'graphql-subscriptions'; 3 | 4 | import PostType from '../PostType'; 5 | import { postLoader } from '../PostLoader'; 6 | import { IPost } from '../PostModel'; 7 | import { pubsub } from '../../../app'; 8 | import { loadUser } from '../../users/UserLoader'; 9 | 10 | const PostCreationSubscription = subscriptionWithClientId({ 11 | name: 'PostCreationSubscription', 12 | inputFields: {}, 13 | outputFields: { 14 | post: { 15 | type: PostType, 16 | resolve: async (post: IPost, _: any, context: any) => await postLoader(post.id) 17 | } 18 | }, 19 | subscribe: withFilter( 20 | (input: any, context: any) => { 21 | return pubsub.asyncIterator('newPost'); 22 | }, 23 | async (postCreated: IPost, variables: any) => { 24 | const loggedUser = variables.user; 25 | const author = await loadUser(postCreated.author); 26 | 27 | return `${loggedUser._id}` === `${author._id}` || !!author.friends.includes(loggedUser._id); 28 | } 29 | ), 30 | getPayload: async (obj: any) => ({ 31 | id: obj.id 32 | }) 33 | }); 34 | 35 | export default PostCreationSubscription; -------------------------------------------------------------------------------- /backend/src/modules/posts/subscriptions/PostLike.ts: -------------------------------------------------------------------------------- 1 | import { subscriptionWithClientId } from "graphql-relay-subscription"; 2 | import { withFilter } from "graphql-subscriptions"; 3 | 4 | import PostType from "../PostType"; 5 | import { pubsub } from "../../../app"; 6 | import { IPost } from "../PostModel"; 7 | import { postLoader } from "../PostLoader"; 8 | import { loadUser } from "../../users/UserLoader"; 9 | 10 | const PostLikeSubscription = subscriptionWithClientId({ 11 | name: 'PostLikeSubscription', 12 | description: 'Post Like subscription', 13 | inputFields: {}, 14 | outputFields: { 15 | post: { 16 | type: PostType, 17 | resolve: (post: IPost) => postLoader(post.id) 18 | } 19 | }, 20 | subscribe: withFilter( 21 | (input: any, context: any) => { 22 | return pubsub.asyncIterator('postLike'); 23 | }, 24 | async (postLiked: IPost, variables: any) => { 25 | const loggedUser = variables.user; 26 | const author = await loadUser(postLiked.author); 27 | 28 | return `${loggedUser._id}` === `${author._id}` || !!author.friends.includes(loggedUser._id); 29 | } 30 | ), 31 | getPayload: (obj: any) => { 32 | return { 33 | id: obj.id 34 | } 35 | } 36 | }); 37 | 38 | export default PostLikeSubscription -------------------------------------------------------------------------------- /backend/src/modules/posts/subscriptions/index.ts: -------------------------------------------------------------------------------- 1 | import PostCreationSubscription from './PostCreation'; 2 | import PostLikeSubscription from './PostLike'; 3 | 4 | export default { 5 | PostCreationSubscription, 6 | PostLikeSubscription 7 | } -------------------------------------------------------------------------------- /backend/src/modules/reply/ReplyLoader.ts: -------------------------------------------------------------------------------- 1 | import Reply, {IReply} from './ReplyModel'; 2 | import Dataloader from 'dataloader'; 3 | 4 | const replyDataLoader = new Dataloader((keys: string[]) => Reply.find({_id: {$in: keys}})); 5 | 6 | export const replyLoader = async (id: string) => { 7 | return await replyDataLoader.load(id); 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/modules/reply/ReplyModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export interface IReply extends mongoose.Document { 4 | author: string; 5 | content: string; 6 | createdAt: Date; 7 | updatedAt: Date; 8 | likes: string[]; 9 | } 10 | 11 | const replySchema = new mongoose.Schema({ 12 | author: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'User', 15 | required: true 16 | }, 17 | content: { 18 | type: String, 19 | required: true 20 | }, 21 | likes: [{ 22 | type: mongoose.Schema.Types.ObjectId, 23 | ref: 'User' 24 | }] 25 | }, { 26 | timestamps: true 27 | }); 28 | 29 | const Reply = mongoose.model('Reply_SocialNetwork', replySchema); 30 | 31 | export default Reply -------------------------------------------------------------------------------- /backend/src/modules/reply/ReplyType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLBoolean } from 'graphql'; 2 | import { globalIdField, connectionDefinitions, connectionArgs, connectionFromArray } from 'graphql-relay'; 3 | 4 | import userType from '../users/UserType'; 5 | import { IReply } from './ReplyModel'; 6 | import { loadUser } from '../users/UserLoader'; 7 | import { nodeInterface } from '../../graphql/NodeDefinitions'; 8 | 9 | const ReplyType = new GraphQLObjectType({ 10 | name: 'ReplyType', 11 | description: 'Reply type', 12 | fields: () => ({ 13 | id: globalIdField('Reply'), 14 | author: { 15 | type: userType, 16 | resolve: (reply) => loadUser(reply.author) 17 | }, 18 | content: { 19 | type: GraphQLString, 20 | resolve: (reply) => reply.content 21 | }, 22 | createdAt: { 23 | type: GraphQLString, 24 | resolve: (reply) => reply.createdAt 25 | }, 26 | updatedAt: { 27 | type: GraphQLString, 28 | resolve: (reply) => reply.updatedAt 29 | }, 30 | likes: { 31 | type: GraphQLInt, 32 | resolve: (reply) => reply.likes.length 33 | }, 34 | userHasLiked: { 35 | type: GraphQLBoolean, 36 | resolve: (reply, args, {user}) => reply.likes.includes(user.id) 37 | } 38 | }), 39 | interfaces: [nodeInterface] 40 | }); 41 | 42 | export const {connectionType: ReplyConnection} = 43 | connectionDefinitions({nodeType: ReplyType}); 44 | 45 | export default ReplyType -------------------------------------------------------------------------------- /backend/src/modules/reply/mutations/CreateReply.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString, GraphQLInt } from 'graphql'; 2 | import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay'; 3 | 4 | import ReplyType from '../ReplyType'; 5 | import Reply from '../ReplyModel'; 6 | import { commentLoader } from '../../../modules/comments/CommentLoader'; 7 | import { pubsub } from '../../../app'; 8 | import { replyLoader } from '../ReplyLoader'; 9 | 10 | const CreateReply = mutationWithClientMutationId({ 11 | name: 'CreateReply', 12 | description: 'Create Reply Mutation', 13 | inputFields: { 14 | content: { 15 | type: GraphQLString 16 | }, 17 | comment: { 18 | type: GraphQLString 19 | } 20 | }, 21 | outputFields: { 22 | reply: { 23 | type: ReplyType, 24 | resolve: async (reply) => await replyLoader(reply.id) 25 | } 26 | }, 27 | mutateAndGetPayload: async ({content, comment}, {user}) => { 28 | try { 29 | 30 | const {type, id} = fromGlobalId(comment); 31 | 32 | const reply = new Reply({author: user.id, content}); 33 | await reply.save(); 34 | 35 | const commentReturned = await commentLoader(id); 36 | commentReturned.replies = [reply.id].concat(commentReturned.replies); 37 | await commentReturned.save(); 38 | 39 | pubsub.publish('newReply', reply); 40 | 41 | return reply; 42 | } catch (err) { 43 | console.log(err); 44 | } 45 | } 46 | }); 47 | 48 | export default CreateReply; -------------------------------------------------------------------------------- /backend/src/modules/reply/mutations/LikeReply.ts: -------------------------------------------------------------------------------- 1 | import { mutationWithClientMutationId, fromGlobalId } from "graphql-relay"; 2 | import { GraphQLString } from "graphql"; 3 | 4 | import ReplyType from "../ReplyType"; 5 | import { IUser } from "../../../modules/users/UserModel"; 6 | import Reply from "../ReplyModel"; 7 | import { pubsub } from "../../../app"; 8 | import { replyLoader } from "../ReplyLoader"; 9 | 10 | const LikeReply = mutationWithClientMutationId({ 11 | name: 'LikeReplay', 12 | description: 'Handle likes for replies', 13 | inputFields: { 14 | reply: { 15 | type: GraphQLString 16 | } 17 | }, 18 | outputFields: { 19 | reply: { 20 | type: ReplyType, 21 | resolve: async (reply) => await replyLoader(reply.id) 22 | } 23 | }, 24 | mutateAndGetPayload: async ({reply}: {reply: string}, {user}: {user: IUser}) => { 25 | try { 26 | 27 | const {type, id} = fromGlobalId(reply); 28 | 29 | const replyId = id; 30 | const replyFounded = await Reply.findById(replyId); 31 | 32 | if (replyFounded.likes.includes(user.id)) { 33 | 34 | const indexOf = replyFounded.likes.indexOf(user.id); 35 | replyFounded.likes.splice(indexOf, 1); 36 | await replyFounded.save(); 37 | 38 | pubsub.publish('replyLike', replyFounded); 39 | return replyFounded; 40 | } 41 | 42 | replyFounded.likes.push(user.id); 43 | await replyFounded.save(); 44 | 45 | pubsub.publish('replyLike', replyFounded); 46 | 47 | return replyFounded; 48 | } catch(error) { 49 | console.log(error); 50 | } 51 | } 52 | }); 53 | 54 | export default LikeReply -------------------------------------------------------------------------------- /backend/src/modules/reply/mutations/index.ts: -------------------------------------------------------------------------------- 1 | import CreateReply from './CreateReply'; 2 | import LikeReply from './LikeReply'; 3 | 4 | export default { 5 | CreateReply, 6 | LikeReply 7 | } -------------------------------------------------------------------------------- /backend/src/modules/reply/subscriptions/ReplyCreationSubscription.ts: -------------------------------------------------------------------------------- 1 | import { subscriptionWithClientId } from "graphql-relay-subscription"; 2 | import { withFilter } from "graphql-subscriptions"; 3 | 4 | import ReplyType from "../ReplyType"; 5 | import { replyLoader } from "../ReplyLoader"; 6 | import CommentType from "../../comments/CommentType"; 7 | import { commentLoaderByReply } from "../../comments/CommentLoader"; 8 | import { IReply } from "../ReplyModel"; 9 | import { pubsub } from "../../../app"; 10 | import { IComment } from "../../comments/CommentModel"; 11 | import { IPost } from "../../posts/PostModel"; 12 | import { postLoaderByComment } from "../../posts/PostLoader"; 13 | import { loadUser } from "../../users/UserLoader"; 14 | 15 | const ReplyCreationSubscription = subscriptionWithClientId({ 16 | name: 'ReplyCreationSubscription', 17 | description: 'Reply Creation Subscription', 18 | inputFields: {}, 19 | outputFields: { 20 | reply: { 21 | type: ReplyType, 22 | resolve: (replyObj: any) => replyLoader(replyObj.id) 23 | }, 24 | comment: { 25 | type: CommentType, 26 | resolve: (replyObj: any) => commentLoaderByReply(replyObj.id) 27 | } 28 | }, 29 | subscribe: withFilter( 30 | (input: any, context: any) => { 31 | return pubsub.asyncIterator('newReply') 32 | }, 33 | async (reply: IReply, variables: any) => { 34 | const commentFounded: IComment = await commentLoaderByReply(reply._id); 35 | const postFounded: IPost = await postLoaderByComment(commentFounded._id); 36 | const postAuthor = await loadUser(postFounded.author); 37 | 38 | return `${postAuthor._id}` === `${reply.author}` || postAuthor.friends.includes(reply.author); 39 | } 40 | ), 41 | getPayload: (replyObj: any) => ({ 42 | id: replyObj.id 43 | }) 44 | }); 45 | 46 | export default ReplyCreationSubscription; -------------------------------------------------------------------------------- /backend/src/modules/reply/subscriptions/ReplyLikeSubscription.ts: -------------------------------------------------------------------------------- 1 | import { subscriptionWithClientId } from "graphql-relay-subscription"; 2 | import { withFilter } from "graphql-subscriptions"; 3 | 4 | import { replyLoader } from "../ReplyLoader"; 5 | import ReplyType from "../ReplyType"; 6 | import { pubsub } from "../../../app"; 7 | import { IReply } from "../ReplyModel"; 8 | import { IComment } from "../../comments/CommentModel"; 9 | import { commentLoaderByReply } from "../../comments/CommentLoader"; 10 | import { IPost } from "../../posts/PostModel"; 11 | import { postLoaderByComment } from "../../posts/PostLoader"; 12 | import { loadUser } from "../../users/UserLoader"; 13 | 14 | const replyLikeSubscription = subscriptionWithClientId({ 15 | name: 'ReplyLikeSubscription', 16 | description: 'Subscription to fetch likes in replies', 17 | inputFields: {}, 18 | outputFields: { 19 | reply: { 20 | type: ReplyType, 21 | resolve: (replyObj: any) => replyLoader((replyObj.id)) 22 | } 23 | }, 24 | subscribe: withFilter( 25 | (input: any, context: any)=>{ 26 | return pubsub.asyncIterator('replyLike'); 27 | }, 28 | async (reply: IReply, variables: any)=>{ 29 | const commentFounded: IComment = await commentLoaderByReply(reply._id); 30 | const postFounded: IPost = await postLoaderByComment(commentFounded._id); 31 | const postAuthor = await loadUser(postFounded.author); 32 | 33 | return `${postAuthor._id}` === `${reply.author}` || postAuthor.friends.includes(reply.author); 34 | } 35 | ), 36 | getPayload: (replyObj: any) => ({ 37 | id: replyObj.id 38 | }) 39 | }); 40 | 41 | export default replyLikeSubscription; -------------------------------------------------------------------------------- /backend/src/modules/reply/subscriptions/index.ts: -------------------------------------------------------------------------------- 1 | import ReplyCreationSubscription from './ReplyCreationSubscription'; 2 | import ReplyLikeSubscription from './ReplyLikeSubscription'; 3 | 4 | export default { 5 | ReplyCreationSubscription, 6 | ReplyLikeSubscription 7 | } -------------------------------------------------------------------------------- /backend/src/modules/users/UserLoader.ts: -------------------------------------------------------------------------------- 1 | import userModel, { IUser } from './UserModel'; 2 | import Dataloader from 'dataloader'; 3 | 4 | console.log('load user module'); 5 | 6 | const userLoader = new Dataloader((keys: string[]) => userModel.find({_id: {$in: keys}})); 7 | 8 | const loadUser = async (id: string) => { 9 | 10 | console.log('loaduser id: ', id); 11 | const user = await userLoader.load(id); 12 | 13 | console.log('user by dataloader: ', user); 14 | return user; 15 | } 16 | 17 | const userIdLoader = (user: IUser, field: keyof IUser) => { 18 | return field === 'tokens' ? user.tokens[0].token : user[field]; 19 | } 20 | 21 | const loadLoggedUser = async (token: string) => { 22 | console.log('loggeduser token: ', token); 23 | 24 | const user = await userModel.find({tokens: [{token}]}); 25 | 26 | console.log('logged user by dataloader: ', user); 27 | return user; 28 | } 29 | 30 | export { loadUser, loadLoggedUser, userLoader, userIdLoader }; -------------------------------------------------------------------------------- /backend/src/modules/users/UserModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import bcrypt from 'bcrypt'; 3 | import jsonwebtoken from 'jsonwebtoken'; 4 | 5 | 6 | export interface IUser extends mongoose.Document { 7 | name: string; 8 | password: string; 9 | email: string; 10 | createdAt: Date; 11 | updatedAt: Date; 12 | tokens:[{token: string}]; 13 | friends: string[]; 14 | posts: string[]; 15 | generateAuthToken(): string; 16 | verifyAuthToken(): void; 17 | } 18 | 19 | export interface IUserModel extends mongoose.Model{ 20 | findByCredentials(email: string, password: string): IUser; 21 | findByToken(token: string): IUser 22 | } 23 | 24 | const userSchema = new mongoose.Schema({ 25 | name: { 26 | type: String, 27 | required: true, 28 | trim: true 29 | }, 30 | password: { 31 | type: String, 32 | required: true, 33 | minlength: 7 34 | }, 35 | email: { 36 | type: String, 37 | required: true, 38 | unique: true, 39 | lowercase: true 40 | }, 41 | tokens: [{ 42 | token: { 43 | type: String, 44 | required: false 45 | } 46 | }], 47 | friends: [{ 48 | type: Schema.Types.ObjectId, 49 | ref: 'User' 50 | }], 51 | posts: [{ 52 | type: Schema.Types.ObjectId, 53 | ref: 'Post' 54 | }] 55 | }, { 56 | timestamps: true 57 | }); 58 | 59 | userSchema.pre('save', async function (next) { 60 | if (this.isModified('password')) { 61 | this.password = await bcrypt.hash(this.password, 8); 62 | } 63 | next(); 64 | }); 65 | 66 | userSchema.methods.generateAuthToken = async function() { 67 | const token = jsonwebtoken.sign({_id: this._id}, process.env.JWT_KEY, {expiresIn: 60 * 30}); 68 | this.tokens = [{token}].concat(this.tokens); 69 | this.save(); 70 | return token; 71 | } 72 | 73 | userSchema.methods.verifyAuthToken = function(callbackSuccess?: () => {}, callbackError?: (error: any) => {}) { 74 | const actualToken = this.tokens[0].token; 75 | try { 76 | jsonwebtoken.verify(actualToken, process.env.JWT_KEY); 77 | if (callbackSuccess) { 78 | callbackSuccess(); 79 | } 80 | } catch (err) { 81 | if (callbackError) { 82 | callbackError(err); 83 | } 84 | } 85 | } 86 | 87 | userSchema.statics.findByCredentials = async (email: string, password: string) => { 88 | const user = await User.findOne({email}); 89 | if (!user) { 90 | throw new Error('Invalid login credentials'); 91 | } 92 | const isPasswordMatch = await bcrypt.compare(password, user.password); 93 | if (!isPasswordMatch) { 94 | throw new Error('Invalid password'); 95 | } 96 | 97 | return user; 98 | } 99 | 100 | userSchema.statics.findByToken = async (token: string) => { 101 | const jsonPayload: any = jsonwebtoken.decode(token); 102 | const user = await User.findOne({_id: jsonPayload._id}); 103 | 104 | return user; 105 | } 106 | 107 | const User = mongoose.model('User_SocialNetwork', userSchema); 108 | 109 | 110 | export default User; -------------------------------------------------------------------------------- /backend/src/modules/users/UserType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql'; 2 | 3 | import { IUser } from './UserModel'; 4 | import { loadUser, userIdLoader } from './UserLoader'; 5 | import { connectionDefinitions, connectionArgs, connectionFromArray } from 'graphql-relay'; 6 | import PostType, { PostConnection } from '../posts/PostType'; 7 | 8 | 9 | const userType = new GraphQLObjectType({ 10 | name: 'UserType', 11 | description: 'User type', 12 | fields: () => ( 13 | { 14 | name: { 15 | type: GraphQLString, 16 | resolve: (user, _) => { 17 | return user.name 18 | } 19 | }, 20 | password: { 21 | type: GraphQLString, 22 | resolve: (user, _) => user.password 23 | }, 24 | email: { 25 | type: GraphQLString, 26 | resolve: (user, _) => user.email 27 | }, 28 | createdAt: { 29 | type: GraphQLString, 30 | resolve: (user) => user.createdAt 31 | }, 32 | updatedAt: { 33 | type: GraphQLString, 34 | resolve: (user) => user.updatedAt 35 | }, 36 | token: { 37 | type: GraphQLString, 38 | resolve: (user, _) => user.tokens[0].token 39 | }, 40 | friends: { 41 | type: UserConnection, 42 | args: connectionArgs, 43 | resolve: (user, args) => { 44 | return connectionFromArray( 45 | user.friends.map(id => loadUser(id)), 46 | args 47 | ) 48 | } 49 | }, 50 | posts: { 51 | type: PostConnection, 52 | args: connectionArgs, 53 | resolve: (user, args) => { 54 | return connectionFromArray( 55 | user.posts.map(id => loadUser(id)), 56 | args 57 | ) 58 | } 59 | }, 60 | _id: { 61 | type: GraphQLString, 62 | resolve: (user, _) => userIdLoader(user, '_id') 63 | } 64 | } 65 | ) 66 | }); 67 | 68 | const {connectionType: UserConnection} = 69 | connectionDefinitions({nodeType: userType}); 70 | 71 | export default userType; -------------------------------------------------------------------------------- /backend/src/modules/users/mutations/CreateUser.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLString } from "graphql"; 2 | 3 | import userType from "../UserType"; 4 | import userModel from "../UserModel"; 5 | import { mutationWithClientMutationId } from "graphql-relay"; 6 | import { loadUser } from "../UserLoader"; 7 | 8 | export const mutation = mutationWithClientMutationId({ 9 | name: 'UserCreation', 10 | description: 'Create new user', 11 | inputFields: { 12 | name: { 13 | type: GraphQLString 14 | }, 15 | password: { 16 | type: GraphQLString 17 | }, 18 | email: { 19 | type: GraphQLString 20 | } 21 | }, 22 | outputFields: { 23 | user: { 24 | type: userType, 25 | resolve: async (user) => await loadUser(user.id) 26 | } 27 | }, 28 | mutateAndGetPayload: async ({name, password, email}) => { 29 | try { 30 | const newUser = new userModel({name, password, email}); 31 | const returnNewUser = await newUser.save(); 32 | return returnNewUser; 33 | } catch (err) { 34 | console.log(err) 35 | return err; 36 | } 37 | } 38 | }); 39 | 40 | export default mutation; -------------------------------------------------------------------------------- /backend/src/modules/users/mutations/Login.ts: -------------------------------------------------------------------------------- 1 | import { mutationWithClientMutationId } from "graphql-relay"; 2 | import { GraphQLString, graphql } from "graphql"; 3 | import userType from "../UserType"; 4 | import User from "../UserModel"; 5 | 6 | 7 | const mutation = mutationWithClientMutationId({ 8 | name: 'Login', 9 | description: 'Login a user, generates new token', 10 | inputFields: { 11 | email: { 12 | type: GraphQLString 13 | }, 14 | password: { 15 | type: GraphQLString 16 | } 17 | }, 18 | outputFields: { 19 | user: { 20 | type: userType, 21 | resolve: (user) => user 22 | } 23 | }, 24 | mutateAndGetPayload: async ({email, password}) => { 25 | try { 26 | const user = await User.findByCredentials(email, password); 27 | const token = await user.generateAuthToken(); 28 | return user; 29 | } catch (err) { 30 | console.log('entrou erro catch'); 31 | console.log(err); 32 | } 33 | } 34 | }); 35 | 36 | export default mutation; -------------------------------------------------------------------------------- /backend/src/modules/users/mutations/index.ts: -------------------------------------------------------------------------------- 1 | import CreateUser from './CreateUser'; 2 | import Login from './Login'; 3 | 4 | export default { 5 | CreateUser, 6 | Login 7 | } -------------------------------------------------------------------------------- /backend/src/schema/MutationType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from "graphql"; 2 | import UserMutations from "../modules/users/mutations"; 3 | import PostMutation from "../modules/posts/mutations"; 4 | import CommentMutations from '../modules/comments/mutations'; 5 | import ReplyMutations from '../modules/reply/mutations'; 6 | 7 | const MutationType = new GraphQLObjectType({ 8 | name: 'MutationType', 9 | description: 'Mutation Type', 10 | fields: () => ({ 11 | ...UserMutations, 12 | ...PostMutation, 13 | ...CommentMutations, 14 | ...ReplyMutations 15 | }) 16 | }); 17 | 18 | export default MutationType; -------------------------------------------------------------------------------- /backend/src/schema/QueryType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLList, GraphQLNonNull } from 'graphql'; 2 | 3 | import { postsLoaderByAuthors } from '../modules/posts/PostLoader'; 4 | import userType from '../modules/users/UserType'; 5 | import { nodeField } from '../graphql/NodeDefinitions'; 6 | import { nodesField } from '../graphql/NodeDefinitions'; 7 | import { PostConnection } from '../modules/posts/PostType'; 8 | import { connectionArgs, connectionFromArray } from 'graphql-relay'; 9 | 10 | 11 | const QueryType = new GraphQLObjectType({ 12 | name: 'Query', 13 | description: 'General QueryType', 14 | fields: () => ({ 15 | node: nodeField, 16 | nodes: nodesField, 17 | myself: { 18 | type: userType, 19 | resolve: (value, args, {user}) => { 20 | return user ? user : null; 21 | } 22 | }, 23 | myPosts: { 24 | type: PostConnection, 25 | args: connectionArgs, 26 | resolve: async (value, args, context) => { 27 | return connectionFromArray( 28 | await postsLoaderByAuthors([context.user.id, ...context.user.friends]), 29 | args 30 | ) 31 | } 32 | }}) 33 | }); 34 | 35 | export default QueryType; -------------------------------------------------------------------------------- /backend/src/schema/Schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from "graphql"; 2 | 3 | import QueryType from "./QueryType"; 4 | import MutationType from "./MutationType"; 5 | import SubscriptionType from "./SubscriptionType"; 6 | 7 | 8 | export const Schema = new GraphQLSchema({ 9 | query: QueryType, 10 | mutation: MutationType, 11 | subscription: SubscriptionType 12 | }); -------------------------------------------------------------------------------- /backend/src/schema/SubscriptionType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from "graphql"; 2 | 3 | import PostSubscriptions from "../modules/posts/subscriptions/"; 4 | import CommentSubscriptions from '../modules/comments/subscriptions' 5 | import ReplySubscriptions from "../modules/reply/subscriptions"; 6 | 7 | 8 | const SubscriptionType = new GraphQLObjectType({ 9 | name: 'SubscriptionType', 10 | fields: () => ({ 11 | ...PostSubscriptions, 12 | ...CommentSubscriptions, 13 | ...ReplySubscriptions 14 | }) 15 | }); 16 | 17 | export default SubscriptionType -------------------------------------------------------------------------------- /backend/src/server.ts: -------------------------------------------------------------------------------- 1 | import App from './app.js'; 2 | import getUser from './auth.js'; 3 | import { execute, subscribe } from 'graphql'; 4 | 5 | import { SubscriptionServer } from 'subscriptions-transport-ws'; 6 | import { createServer } from 'http'; 7 | import { Schema } from './schema/Schema'; 8 | 9 | type ConnectionParams = { 10 | authorization?: string; 11 | }; 12 | 13 | (async () => { 14 | const server = createServer(App.callback()); 15 | 16 | server.listen(process.env.PORT ?? '3333', () => { 17 | console.log('O servidor foi iniciado'); 18 | }); 19 | 20 | const subscriptionServer = SubscriptionServer.create( 21 | { 22 | onConnect: async (connectionParams: ConnectionParams) => { 23 | const user = await getUser(connectionParams.authorization); 24 | return { 25 | req: {}, 26 | user 27 | } 28 | }, 29 | // eslint-disable-next-line 30 | onDisconnect: () => console.log('Client subscription disconnected!'), 31 | execute, 32 | subscribe, 33 | schema: Schema, 34 | }, 35 | { 36 | server, 37 | path: '/subscriptions', 38 | }, 39 | ); 40 | })(); -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES6", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": [ 13 | "node_modules/*" 14 | ] 15 | } 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ] 20 | } -------------------------------------------------------------------------------- /backend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "trailing-comma": [ false ], 9 | "no-var-requires": false, 10 | "no-console": false 11 | }, 12 | "rulesDirectory": [] 13 | } -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /build 4 | dockerBuild.log -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # Graphql Generated 27 | __generated__ 28 | index.css 29 | 30 | # Docker Logs 31 | dockerBuild.log -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # socialnetwork:frontDev 2 | FROM node:16-alpine as development 3 | 4 | RUN --mount=type=bind,source=/package.json,target=/package.json \ 5 | --mount=type=bind,source=/package-lock.json,target=/package-lock.json \ 6 | npm ci 7 | 8 | CMD npm run start 9 | 10 | FROM node:16-alpine as setup 11 | 12 | WORKDIR /socialnetwork 13 | 14 | COPY . . 15 | 16 | ENV REACT_APP_GRAPHQL_URL=http://localhost:3332/graphql 17 | 18 | # update first from experimental versions to remove --force 19 | RUN npm ci --force 20 | RUN npm run build 21 | 22 | # socialnetwork:frontProd 23 | FROM node:16-alpine as production 24 | 25 | WORKDIR /socialnetwork 26 | 27 | RUN --mount=type=bind,from=setup \ 28 | npm version 29 | 30 | RUN --mount=type=bind,from=setup \ 31 | cd socialnetwork && ls 32 | 33 | RUN --mount=type=bind,from=setup \ 34 | ls -a 35 | 36 | COPY --from=setup ./socialnetwork/build ./ 37 | 38 | RUN npm install --global http-server 39 | 40 | EXPOSE 3100 41 | CMD http-server . -o -p 3100 -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Social Network Frontend 2 | 3 | This is a Relay client to consume the backend graphql api. 4 | 5 | ## To Run 6 | 7 | Execute `yarn install` or `npm install` depending on your local package manager. To install all dependencies 8 | 9 | Execute `yarn start` or `npm start` depending on your local package manager. To run the frontend web client. 10 | 11 | ## Examples 12 | 13 | ### User creation and login 14 | 15 | ![](./public/socialnetwork-register_example.gif) 16 | 17 | ### Post creation and reply 18 | 19 | ![](./public/socialnetwork-post_example.gif) 20 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "social-network", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "react-scripts test", 7 | "eject": "react-scripts eject", 8 | "tailwind:css": "tailwind build src/tailwind.css -c tailwind.config.js -o src/index.css", 9 | "start": "npm run relay && npm run tailwind:css && react-scripts start", 10 | "build": "npm run relay && react-scripts build", 11 | "relay": "npx relay-compiler --schema ./schema/schema.graphql --src ./src/ --watchman false $@ --language typescript" 12 | }, 13 | "eslintConfig": { 14 | "extends": "react-app" 15 | }, 16 | "browserslist": { 17 | "production": [ 18 | ">0.2%", 19 | "not dead", 20 | "not op_mini all" 21 | ], 22 | "development": [ 23 | "last 1 chrome version", 24 | "last 1 firefox version", 25 | "last 1 safari version" 26 | ] 27 | }, 28 | "engines": { 29 | "node": "=16" 30 | }, 31 | "dependencies": { 32 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 33 | "@fortawesome/free-regular-svg-icons": "^5.13.0", 34 | "@fortawesome/react-fontawesome": "^0.1.9", 35 | "@testing-library/jest-dom": "^4.2.4", 36 | "@testing-library/react": "^9.5.0", 37 | "@testing-library/user-event": "^7.2.1", 38 | "babel-plugin-relay": "^9.0.0", 39 | "react": "0.0.0-experimental-33c3af284", 40 | "react-dom": "0.0.0-experimental-33c3af284", 41 | "react-relay": "0.0.0-experimental-8cc94ddc", 42 | "react-router": "5.2.0", 43 | "react-router-dom": "^5.2.0", 44 | "react-scripts": "3.4.1", 45 | "relay-runtime": "^9.0.0", 46 | "subscriptions-transport-ws": "^0.9.16", 47 | "typescript": "^3.7.5" 48 | }, 49 | "devDependencies": { 50 | "@types/jest": "^24.9.1", 51 | "@types/node": "^12.12.36", 52 | "@types/react": "^16.9.35", 53 | "@types/react-dom": "^16.9.8", 54 | "@types/react-relay": "^7.0.7", 55 | "@types/react-router-dom": "^5.1.5", 56 | "@types/relay-runtime": "^8.0.8", 57 | "autoprefixer": "^9.7.6", 58 | "graphql": "^14", 59 | "postcss-cli": "^7.1.0", 60 | "relay-compiler": "^9.0.0", 61 | "relay-compiler-language-typescript": "^12.0.1", 62 | "tailwindcss": "^1.2.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streeterxs/socialnetwork/93604057e1793c98f68d6b50e186ee1789f34d75/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streeterxs/socialnetwork/93604057e1793c98f68d6b50e186ee1789f34d75/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streeterxs/socialnetwork/93604057e1793c98f68d6b50e186ee1789f34d75/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/socialnetwork-post_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streeterxs/socialnetwork/93604057e1793c98f68d6b50e186ee1789f34d75/frontend/public/socialnetwork-post_example.gif -------------------------------------------------------------------------------- /frontend/public/socialnetwork-register_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streeterxs/socialnetwork/93604057e1793c98f68d6b50e186ee1789f34d75/frontend/public/socialnetwork-register_example.gif -------------------------------------------------------------------------------- /frontend/schema/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: MutationType 4 | subscription: SubscriptionType 5 | } 6 | 7 | input CommentLikeSubscriptionInput { 8 | clientSubscriptionId: String 9 | } 10 | 11 | type CommentLikeSubscriptionPayload { 12 | comment: CommentType 13 | clientSubscriptionId: String 14 | } 15 | 16 | """Comment type""" 17 | type CommentType implements Node { 18 | """The ID of an object""" 19 | id: ID! 20 | author: UserType 21 | content: String 22 | likes: Int 23 | userHasLiked: Boolean 24 | createdAt: String 25 | updatedAt: String 26 | replies(after: String, first: Int, before: String, last: Int): ReplyTypeConnection 27 | } 28 | 29 | """A connection to a list of items.""" 30 | type CommentTypeConnection { 31 | """Information to aid in pagination.""" 32 | pageInfo: PageInfo! 33 | 34 | """A list of edges.""" 35 | edges: [CommentTypeEdge] 36 | } 37 | 38 | """An edge in a connection.""" 39 | type CommentTypeEdge { 40 | """The item at the end of the edge""" 41 | node: CommentType 42 | 43 | """A cursor for use in pagination""" 44 | cursor: String! 45 | } 46 | 47 | input CreateCommentInput { 48 | content: String 49 | post: String 50 | clientMutationId: String 51 | } 52 | 53 | type CreateCommentPayload { 54 | comment: CommentType 55 | clientMutationId: String 56 | } 57 | 58 | input CreateCommentSubscriptionInput { 59 | clientSubscriptionId: String 60 | } 61 | 62 | type CreateCommentSubscriptionPayload { 63 | comment: CommentType 64 | post: PostType 65 | clientSubscriptionId: String 66 | } 67 | 68 | input CreateReplyInput { 69 | content: String 70 | comment: String 71 | clientMutationId: String 72 | } 73 | 74 | type CreateReplyPayload { 75 | reply: ReplyType 76 | clientMutationId: String 77 | } 78 | 79 | input LikeCommentInput { 80 | comment: String 81 | clientMutationId: String 82 | } 83 | 84 | type LikeCommentPayload { 85 | comment: CommentType 86 | clientMutationId: String 87 | } 88 | 89 | input LikePostInput { 90 | post: String 91 | clientMutationId: String 92 | } 93 | 94 | type LikePostPayload { 95 | post: PostType 96 | clientMutationId: String 97 | } 98 | 99 | input LikeReplayInput { 100 | reply: String 101 | clientMutationId: String 102 | } 103 | 104 | type LikeReplayPayload { 105 | reply: ReplyType 106 | clientMutationId: String 107 | } 108 | 109 | input LoginInput { 110 | email: String 111 | password: String 112 | clientMutationId: String 113 | } 114 | 115 | type LoginPayload { 116 | user: UserType 117 | clientMutationId: String 118 | } 119 | 120 | """Mutation Type""" 121 | type MutationType { 122 | """Create new user""" 123 | CreateUser(input: UserCreationInput!): UserCreationPayload 124 | 125 | """Login a user, generates new token""" 126 | Login(input: LoginInput!): LoginPayload 127 | 128 | """Post Creation""" 129 | PostCreation(input: PostCreationInput!): PostCreationPayload 130 | 131 | """Mutation for like handling for posts""" 132 | LikePost(input: LikePostInput!): LikePostPayload 133 | 134 | """Create Comment Mutation""" 135 | CreateComment(input: CreateCommentInput!): CreateCommentPayload 136 | 137 | """Update total likes for a comment type""" 138 | LikeComment(input: LikeCommentInput!): LikeCommentPayload 139 | 140 | """Create Reply Mutation""" 141 | CreateReply(input: CreateReplyInput!): CreateReplyPayload 142 | 143 | """Handle likes for replies""" 144 | LikeReply(input: LikeReplayInput!): LikeReplayPayload 145 | } 146 | 147 | """An object with an ID""" 148 | interface Node { 149 | """The id of the object.""" 150 | id: ID! 151 | } 152 | 153 | """Information about pagination in a connection.""" 154 | type PageInfo { 155 | """When paginating forwards, are there more items?""" 156 | hasNextPage: Boolean! 157 | 158 | """When paginating backwards, are there more items?""" 159 | hasPreviousPage: Boolean! 160 | 161 | """When paginating backwards, the cursor to continue.""" 162 | startCursor: String 163 | 164 | """When paginating forwards, the cursor to continue.""" 165 | endCursor: String 166 | } 167 | 168 | input PostCreationInput { 169 | content: String 170 | clientMutationId: String 171 | } 172 | 173 | type PostCreationPayload { 174 | post: PostType 175 | clientMutationId: String 176 | } 177 | 178 | input PostCreationSubscriptionInput { 179 | clientSubscriptionId: String 180 | } 181 | 182 | type PostCreationSubscriptionPayload { 183 | post: PostType 184 | clientSubscriptionId: String 185 | } 186 | 187 | input PostLikeSubscriptionInput { 188 | clientSubscriptionId: String 189 | } 190 | 191 | type PostLikeSubscriptionPayload { 192 | post: PostType 193 | clientSubscriptionId: String 194 | } 195 | 196 | """Post type""" 197 | type PostType implements Node { 198 | """The ID of an object""" 199 | id: ID! 200 | author: UserType 201 | content: String 202 | likes: Int 203 | userHasLiked: Boolean 204 | createdAt: String 205 | updatedAt: String 206 | comments(after: String, first: Int, before: String, last: Int): CommentTypeConnection 207 | } 208 | 209 | """A connection to a list of items.""" 210 | type PostTypeConnection { 211 | """Information to aid in pagination.""" 212 | pageInfo: PageInfo! 213 | 214 | """A list of edges.""" 215 | edges: [PostTypeEdge] 216 | } 217 | 218 | """An edge in a connection.""" 219 | type PostTypeEdge { 220 | """The item at the end of the edge""" 221 | node: PostType 222 | 223 | """A cursor for use in pagination""" 224 | cursor: String! 225 | } 226 | 227 | """General QueryType""" 228 | type Query { 229 | """Fetches an object given its ID""" 230 | node( 231 | """The ID of an object""" 232 | id: ID! 233 | ): Node 234 | 235 | """Fetches objects given their IDs""" 236 | nodes( 237 | """The IDs of objects""" 238 | ids: [ID!]! 239 | ): [Node]! 240 | myself: UserType 241 | myPosts(after: String, first: Int, before: String, last: Int): PostTypeConnection 242 | } 243 | 244 | input ReplyCreationSubscriptionInput { 245 | clientSubscriptionId: String 246 | } 247 | 248 | type ReplyCreationSubscriptionPayload { 249 | reply: ReplyType 250 | comment: CommentType 251 | clientSubscriptionId: String 252 | } 253 | 254 | input ReplyLikeSubscriptionInput { 255 | clientSubscriptionId: String 256 | } 257 | 258 | type ReplyLikeSubscriptionPayload { 259 | reply: ReplyType 260 | clientSubscriptionId: String 261 | } 262 | 263 | """Reply type""" 264 | type ReplyType implements Node { 265 | """The ID of an object""" 266 | id: ID! 267 | author: UserType 268 | content: String 269 | createdAt: String 270 | updatedAt: String 271 | likes: Int 272 | userHasLiked: Boolean 273 | } 274 | 275 | """A connection to a list of items.""" 276 | type ReplyTypeConnection { 277 | """Information to aid in pagination.""" 278 | pageInfo: PageInfo! 279 | 280 | """A list of edges.""" 281 | edges: [ReplyTypeEdge] 282 | } 283 | 284 | """An edge in a connection.""" 285 | type ReplyTypeEdge { 286 | """The item at the end of the edge""" 287 | node: ReplyType 288 | 289 | """A cursor for use in pagination""" 290 | cursor: String! 291 | } 292 | 293 | type SubscriptionType { 294 | PostCreationSubscription(input: PostCreationSubscriptionInput!): PostCreationSubscriptionPayload 295 | 296 | """Post Like subscription""" 297 | PostLikeSubscription(input: PostLikeSubscriptionInput!): PostLikeSubscriptionPayload 298 | 299 | """Create Comment Subscription""" 300 | CreateCommentSubscription(input: CreateCommentSubscriptionInput!): CreateCommentSubscriptionPayload 301 | 302 | """Comment Like subscription""" 303 | CommentLikeSubscription(input: CommentLikeSubscriptionInput!): CommentLikeSubscriptionPayload 304 | 305 | """Reply Creation Subscription""" 306 | ReplyCreationSubscription(input: ReplyCreationSubscriptionInput!): ReplyCreationSubscriptionPayload 307 | 308 | """Subscription to fetch likes in replies""" 309 | ReplyLikeSubscription(input: ReplyLikeSubscriptionInput!): ReplyLikeSubscriptionPayload 310 | } 311 | 312 | input UserCreationInput { 313 | name: String 314 | password: String 315 | email: String 316 | clientMutationId: String 317 | } 318 | 319 | type UserCreationPayload { 320 | user: UserType 321 | clientMutationId: String 322 | } 323 | 324 | """User type""" 325 | type UserType { 326 | name: String 327 | password: String 328 | email: String 329 | createdAt: String 330 | updatedAt: String 331 | token: String 332 | friends(after: String, first: Int, before: String, last: Int): UserTypeConnection 333 | posts(after: String, first: Int, before: String, last: Int): PostTypeConnection 334 | _id: String 335 | } 336 | 337 | """A connection to a list of items.""" 338 | type UserTypeConnection { 339 | """Information to aid in pagination.""" 340 | pageInfo: PageInfo! 341 | 342 | """A list of edges.""" 343 | edges: [UserTypeEdge] 344 | } 345 | 346 | """An edge in a connection.""" 347 | type UserTypeEdge { 348 | """The item at the end of the edge""" 349 | node: UserType 350 | 351 | """A cursor for use in pagination""" 352 | cursor: String! 353 | } 354 | 355 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Streeterxs/socialnetwork/93604057e1793c98f68d6b50e186ee1789f34d75/frontend/src/App.css -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {Suspense, useState, useEffect} from 'react'; 2 | import { RelayEnvironmentProvider, useRelayEnvironment } from 'react-relay/hooks'; 3 | 4 | import environment from './relay/environment'; 5 | import Routes from './routes'; 6 | import { Layout } from './Components'; 7 | import './App.css'; 8 | import { BrowserRouter } from 'react-router-dom'; 9 | import SubscriptionModule from './Services/Subscriptions'; 10 | 11 | 12 | 13 | const App = () => { 14 | const [userIsLogged, setUserIsLogged] = useState(!!localStorage.getItem('authToken')); 15 | const [environment, setEnvironment] = useState(useRelayEnvironment()); 16 | 17 | 18 | const handleLogoutLogin = () => { 19 | if (userIsLogged) { 20 | localStorage.removeItem('authToken'); 21 | setUserIsLogged(false); 22 | } 23 | } 24 | return ( 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | ); 35 | } 36 | 37 | const AppRoot = () => ( 38 | 39 | 40 | 41 | ) 42 | 43 | export default AppRoot; 44 | -------------------------------------------------------------------------------- /frontend/src/Components/Layout/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => ( 4 |
5 | developed by Afonso Araújo Neto 6 |
7 | ); 8 | 9 | export default Footer; -------------------------------------------------------------------------------- /frontend/src/Components/Layout/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './Footer'; -------------------------------------------------------------------------------- /frontend/src/Components/Layout/Header/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const Navbar = ({userIsLogged, handleLogoutLogin}: { 5 | userIsLogged: boolean, 6 | handleLogoutLogin: () => void 7 | }) => { 8 | 9 | return ( 10 | 46 | ) 47 | }; 48 | 49 | export default Navbar; -------------------------------------------------------------------------------- /frontend/src/Components/Layout/Header/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './Navbar'; -------------------------------------------------------------------------------- /frontend/src/Components/Layout/Header/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './Navbar'; -------------------------------------------------------------------------------- /frontend/src/Components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import {Header, Footer} from './'; 4 | 5 | const Layout = ({children, userIsLogged, handleLogoutLogin}: any) => { 6 | 7 | return ( 8 |
9 |
10 |
11 | {children} 12 |
13 |
14 |
15 | ) 16 | } 17 | 18 | export default Layout -------------------------------------------------------------------------------- /frontend/src/Components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Footer } from './Footer'; 2 | export { default as Header} from './Header'; 3 | export { default as Layout } from './Layout'; -------------------------------------------------------------------------------- /frontend/src/Components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Layout' -------------------------------------------------------------------------------- /frontend/src/Pages/FeedPage/Components/Comments/Comment.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useState } from 'react'; 2 | import { useFragment } from 'react-relay/hooks'; 3 | import { useMutation } from 'react-relay/lib/relay-experimental'; 4 | import graphql from 'babel-plugin-relay/macro'; 5 | 6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 7 | import { faThumbsUp } from '@fortawesome/free-regular-svg-icons' 8 | 9 | import { Replies, ReplyCreation } from '../'; 10 | 11 | const commentEdgeFragment = graphql` 12 | fragment CommentTypeFragment on CommentTypeEdge { 13 | cursor 14 | node { 15 | id 16 | author { 17 | name 18 | } 19 | content 20 | likes 21 | userHasLiked 22 | createdAt 23 | updatedAt 24 | ...RepliesTypeFragment 25 | } 26 | 27 | }`; 28 | 29 | const commentReplyCreationMutation = graphql` 30 | mutation CommentReplyCreationMutation ($content: String!, $comment: String!) { 31 | CreateReply (input: {content: $content, comment: $comment, clientMutationId: "3"}) { 32 | reply { 33 | content, 34 | author { 35 | name 36 | } 37 | } 38 | } 39 | } 40 | `; 41 | 42 | const commentLikeMutation = graphql ` 43 | mutation CommentLikeMutation($commentId: String!) { 44 | LikeComment (input: {comment: $commentId, clientMutationId: "6"}) { 45 | comment { 46 | likes 47 | userHasLiked 48 | } 49 | } 50 | } 51 | `; 52 | 53 | const Comment = ({comment}: any) => { 54 | const commentEdge = useFragment(commentEdgeFragment, comment); 55 | const [likes, setLikes] = useState(commentEdge.node ? commentEdge.node.likes : 0); 56 | const [hasLiked, setHasLiked] = useState(commentEdge.node ? commentEdge.node.userHasLiked : false); 57 | 58 | const [showReplyCreation, setShowReplyCreation] = useState(false); 59 | 60 | const [commitReplyCre, replyCreIsInFlight] = useMutation(commentReplyCreationMutation); 61 | const [commitLikeMut, likeMutIsInFlight] = useMutation(commentLikeMutation); 62 | 63 | let replyContent = ''; 64 | 65 | const replyCreationFormSubmit = (event: React.FormEvent) => { 66 | event.preventDefault(); 67 | repliesHandler(); 68 | const variables = { 69 | content: replyContent, 70 | comment: commentEdge.node ? commentEdge.node.id : null 71 | } 72 | if (variables.comment && variables.content) { 73 | commitReplyCre({ 74 | variables, 75 | onCompleted: (data: any) => { 76 | } 77 | }) 78 | } 79 | }; 80 | 81 | const likeHandler = () => { 82 | setLikes(hasLiked ? likes - 1 : likes + 1); 83 | setHasLiked(hasLiked ? false : true); 84 | const variables = { 85 | commentId: commentEdge.node ? commentEdge.node.id : null 86 | } 87 | if (variables.commentId) { 88 | commitLikeMut({ 89 | variables, 90 | onCompleted: ({LikeComment}: any) => { 91 | setLikes(LikeComment.comment.likes); 92 | setHasLiked(LikeComment.comment.userHasLiked); 93 | } 94 | }) 95 | } 96 | } 97 | 98 | const repliesHandler = () => { 99 | setShowReplyCreation(showReplyCreation ? false : true); 100 | } 101 | 102 | return ( 103 |
104 |
105 | 106 | 107 | {commentEdge.node.author.name} 108 | 109 | 110 |
111 |

112 | {commentEdge.node ? commentEdge.node.content : null} 113 |

114 |
115 |
116 | 117 | {likeMutIsInFlight ? likes : commentEdge.node.likes} 118 | 119 | 120 | { 121 | (likeMutIsInFlight ? hasLiked : commentEdge.node.userHasLiked) ? 122 | <>Liked : 123 | <>Like 124 | } 125 | 126 | 127 | { 128 | showReplyCreation ? 129 | <>Replying : 130 | <>Reply 131 | } 132 | 133 |
134 |
135 |
136 | { 137 | showReplyCreation && commentEdge && commentEdge.node ? 138 | replyContent = newContent}/> : 139 | null 140 | } 141 |
142 | 143 | { 144 | commentEdge && commentEdge.node ? 145 | : 146 | null 147 | } 148 | 149 |
150 |
151 |
152 | ); 153 | }; 154 | 155 | export default Comment -------------------------------------------------------------------------------- /frontend/src/Pages/FeedPage/Components/Comments/CommentCreation.tsx: -------------------------------------------------------------------------------- 1 | import React, { unstable_useTransition as useTransition} from 'react'; 2 | 3 | const CommentCreation = ({formSubmit, commentContentChange}: { 4 | formSubmit: (event: React.FormEvent) => void, 5 | commentContentChange: (event: string) => void 6 | }) => { 7 | const [startTransition, isPending] = useTransition({ 8 | timeoutMs: 10000 9 | }); 10 | const concurrentFormSubmit = (event: any) => { 11 | startTransition(() => { 12 | formSubmit(event); 13 | }) 14 | }; 15 | return( 16 |
) => { 17 | concurrentFormSubmit(event); 18 | }}> 19 | commentContentChange(event.target.value)}/> 25 | 26 | {isPending ? 'loading' : null} 27 |
28 | ); 29 | }; 30 | 31 | export default CommentCreation; -------------------------------------------------------------------------------- /frontend/src/Pages/FeedPage/Components/Comments/Comments.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { useFragment, usePaginationFragment } from 'react-relay/hooks'; 3 | import graphql from 'babel-plugin-relay/macro'; 4 | import { Comment } from './' 5 | 6 | const commentsTypeFragment = graphql` 7 | fragment CommentsTypeFragment on PostType @argumentDefinitions( 8 | first: {type: "Int", defaultValue: 4}, 9 | last: {type: "Int"}, 10 | before: {type: "String"}, 11 | after: {type: "String"} 12 | ) @refetchable(queryName: "CommentsListPagination") { 13 | comments ( 14 | first: $first 15 | last: $last 16 | before: $before 17 | after: $after 18 | ) @connection(key: "CommentsTypeFragment_comments") { 19 | edges { 20 | ...CommentTypeFragment 21 | } 22 | pageInfo { 23 | startCursor 24 | endCursor 25 | hasNextPage 26 | hasPreviousPage 27 | } 28 | } 29 | } 30 | `; 31 | 32 | const Comments = ({comments}: { 33 | comments: any 34 | }) => { 35 | const { 36 | data, 37 | loadNext, 38 | loadPrevious, 39 | hasNext, 40 | hasPrevious, 41 | isLoadingNext, 42 | isLoadingPrevious, 43 | refetch // For refetching connection 44 | } = usePaginationFragment(commentsTypeFragment, comments); 45 | 46 | return ( 47 |
48 | { 49 | data && data.comments && data.comments.edges.length > 0 ? 50 | data.comments.edges.map((edge: any, index: number) => { 51 | return ( 52 | 53 |
54 | 55 |
56 |
57 | ) 58 | }): 59 | null 60 | } 61 | { 62 | hasNext && data && data.comments && data.comments.edges.length > 0 ? 63 | : 66 | null 67 | } 68 |
69 | ); 70 | }; 71 | 72 | export default Comments; -------------------------------------------------------------------------------- /frontend/src/Pages/FeedPage/Components/Comments/index.tsx: -------------------------------------------------------------------------------- 1 | export {default as Comments} from './Comments'; 2 | export {default as Comment} from './Comment'; 3 | export {default as CommentCreation} from './CommentCreation'; -------------------------------------------------------------------------------- /frontend/src/Pages/FeedPage/Components/Posts/Post.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useState } from 'react'; 2 | import { Comments, CommentCreation } from '../'; 3 | 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 5 | import { faThumbsUp, faThumbsDown } from '@fortawesome/free-regular-svg-icons' 6 | 7 | import { useFragment } from 'react-relay/hooks'; 8 | import { useMutation } from 'react-relay/lib/relay-experimental'; 9 | import graphql from 'babel-plugin-relay/macro'; 10 | 11 | 12 | 13 | const commentCreationMutation = graphql` 14 | mutation PostCommentCreationMutation($content: String!, $post: String!) { 15 | CreateComment(input: {content: $content, post: $post, clientMutationId: "2"}) { 16 | comment { 17 | id 18 | content 19 | author { 20 | name 21 | } 22 | } 23 | } 24 | } 25 | `; 26 | 27 | const postTypeFragment = graphql` 28 | fragment PostTypeFragment on PostType @argumentDefinitions( 29 | first: {type: "Int"} 30 | last: {type: "Int"} 31 | before: {type: "String"} 32 | after: {type: "String"} 33 | ) { 34 | id 35 | author { 36 | name 37 | } 38 | content 39 | likes 40 | userHasLiked 41 | createdAt 42 | updatedAt 43 | ...CommentsTypeFragment @arguments( 44 | first: $first, 45 | last: $last, 46 | before: $before, 47 | after: $after 48 | ) 49 | } 50 | `; 51 | 52 | const postLikeMutation = graphql` 53 | mutation PostLikeMutation($postId: String!) { 54 | LikePost(input: {post: $postId, clientMutationId: "5"}) { 55 | post { 56 | likes 57 | userHasLiked 58 | } 59 | } 60 | } 61 | `; 62 | 63 | const Post = ({post}: any) => { 64 | let commentContent = ''; 65 | 66 | const postEdge = useFragment(postTypeFragment, post); 67 | 68 | const [likes, setLikes] = useState(postEdge.likes); 69 | const [hasLiked, setHasLiked] = useState(postEdge.userHasLiked); 70 | 71 | const [commentCreationCommit, cmtCrtIsInFlight] = useMutation(commentCreationMutation); 72 | const [likeCrtCommit, likeCrtIsInFlight] = useMutation(postLikeMutation) 73 | 74 | const commentCreation = (event: React.FormEvent) => { 75 | event.preventDefault(); 76 | 77 | const variables = { 78 | content: commentContent, 79 | post: postEdge.id 80 | } 81 | commentCreationCommit({ 82 | variables, 83 | onCompleted: (data: any) => { 84 | } 85 | }); 86 | } 87 | 88 | const likesHandler = () => { 89 | setLikes(hasLiked ? likes - 1 : likes + 1); 90 | setHasLiked(hasLiked ? false : true); 91 | 92 | const variables = { 93 | postId: postEdge.id 94 | } 95 | 96 | likeCrtCommit({ 97 | variables, 98 | onCompleted: ({LikePost}: any) => { 99 | setLikes(LikePost.post.likes); 100 | setHasLiked(LikePost.post.userHasLiked); 101 | } 102 | }); 103 | }; 104 | 105 | 106 | return ( 107 |
108 |
109 |
110 |
111 | 112 | 113 | {postEdge.author.name} 114 | 115 | 116 |

117 | {postEdge.content} 118 |

119 |
120 |
121 | 122 | {likeCrtIsInFlight ? likes : postEdge.likes} 123 | 124 |
125 |
126 | 127 | { 128 | (likeCrtIsInFlight ? hasLiked : postEdge.userHasLiked) ? 129 | <> Liked : 130 | <> Like 131 | } 132 | 133 |
134 |
135 | 136 | { 137 | postEdge && postEdge ? 138 | : 139 | null 140 | } 141 | 142 |
143 | 144 |
145 |
146 | { 147 | commentContent = newContent 148 | }}/> 149 |
150 |
151 |
152 | ); 153 | }; 154 | 155 | export default Post; -------------------------------------------------------------------------------- /frontend/src/Pages/FeedPage/Components/Posts/PostCreation.tsx: -------------------------------------------------------------------------------- 1 | import React, { unstable_useTransition as useTransition } from 'react'; 2 | 3 | const PostCreation = ({contentChange, formSubmit}: 4 | { 5 | contentChange: (content: string) => void, 6 | formSubmit: (event: React.FormEvent) => void 7 | } 8 | ) => { 9 | 10 | const [startTransition, isPending] = useTransition(); 11 | 12 | const postSubmitTransition = (event: React.FormEvent) => { 13 | startTransition(() => { 14 | formSubmit(event); 15 | }) 16 | } 17 | 18 | return ( 19 |
20 |