├── .gitignore ├── src ├── components │ ├── routes.js │ └── channels │ │ ├── SendMessageSubscription.js │ │ ├── RedirectToDefaultChannel.js │ │ ├── routes.js │ │ ├── channel │ │ ├── LeaveChannelSubscription.js │ │ ├── SendMessageSubscription.js │ │ ├── JoinChannelSubscription.js │ │ ├── Member.js │ │ ├── MessageComposer.js │ │ ├── Message.js │ │ ├── ChannelHeader.js │ │ ├── routes.js │ │ ├── MessageList.js │ │ ├── MemberList.js │ │ └── ChannelScreen.js │ │ ├── ChannelList.js │ │ ├── ChannelsScreen.js │ │ └── Channel.js ├── events │ ├── graphql.js │ └── index.js ├── schema │ ├── node.js │ ├── datetime.js │ ├── index.js │ ├── user.js │ ├── message.js │ └── channel.js ├── app.js ├── mutations │ ├── LeaveChannelMutation.js │ ├── JoinChannelMutation.js │ └── SendMessageMutation.js ├── server │ ├── bots.js │ └── socket.js ├── network │ └── SocketIONetworkLayer.js └── database │ └── index.js ├── public ├── css │ └── base.css └── index.html ├── scripts └── updateSchema.js ├── package.json ├── server.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | schema.json 3 | schema.graphql 4 | -------------------------------------------------------------------------------- /src/components/routes.js: -------------------------------------------------------------------------------- 1 | import ChannelsRoutes from './channels/routes'; 2 | 3 | export default ChannelsRoutes; 4 | -------------------------------------------------------------------------------- /public/css/base.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0, 3 | padding: 0 4 | } 5 | 6 | body { 7 | font-family: sans-serif; 8 | font-size: 14px; 9 | overflow: hidden; 10 | } 11 | -------------------------------------------------------------------------------- /src/events/graphql.js: -------------------------------------------------------------------------------- 1 | import {graphql} from 'graphql'; 2 | import {schema} from '../schema'; 3 | 4 | export const channelForSubscription = async ({query, variables}) => { 5 | let rootValue = {}; 6 | 7 | const response = await graphql(schema, query, rootValue, variables); 8 | 9 | return rootValue.channel; 10 | } 11 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Relay • Chat 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/schema/node.js: -------------------------------------------------------------------------------- 1 | import { 2 | fromGlobalId, 3 | nodeDefinitions 4 | } from 'graphql-relay'; 5 | 6 | import * as db from '../database'; 7 | 8 | const {nodeInterface: NodeInterface, nodeField: NodeField} = nodeDefinitions( 9 | (globalId) => { 10 | var {type, id} = fromGlobalId(globalId); 11 | return db.load(type, id); 12 | } 13 | ); 14 | 15 | export { 16 | NodeInterface, 17 | NodeField 18 | } 19 | -------------------------------------------------------------------------------- /src/schema/datetime.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { 4 | GraphQLObjectType, 5 | GraphQLInputObjectType, 6 | GraphQLNonNull, 7 | GraphQLID, 8 | GraphQLString 9 | } from 'graphql'; 10 | 11 | export const DateTimeType = new GraphQLObjectType({ 12 | name: 'DateTime', 13 | fields: () => ({ 14 | format: { 15 | type: GraphQLString, 16 | args: { 17 | string: {type: new GraphQLNonNull(GraphQLString)} 18 | }, 19 | resolve: (dt, {string}) => moment(dt).format(string) 20 | } 21 | }) 22 | }); 23 | -------------------------------------------------------------------------------- /src/events/index.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | // wrap event emitter to make events emit asynchronously 4 | const events = new EventEmitter(); 5 | events.setMaxListeners(1000); 6 | 7 | export const publish = (channel, event) => { 8 | setImmediate(() => events.emit(channel, event)); 9 | } 10 | 11 | export const subscribe = (channel, listener) => { 12 | events.addListener(channel, listener); 13 | } 14 | 15 | export const unsubscribe = (channel, listener) => { 16 | events.removeListener(channel, listener); 17 | } 18 | 19 | export { channelForSubscription } from './graphql'; 20 | -------------------------------------------------------------------------------- /src/components/channels/SendMessageSubscription.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | export default class SendMessageSubscription extends Relay.Subscription { 4 | 5 | static fragments = { 6 | channel: () => Relay.QL`fragment on Channel { id }` 7 | }; 8 | 9 | getSubscription() { 10 | return Relay.QL` 11 | subscription { 12 | sendMessageSubscribe(input: $input) { 13 | channel { 14 | totalMessages 15 | } 16 | } 17 | } 18 | `; 19 | } 20 | 21 | getVariables() { 22 | return { 23 | channelId: this.props.channel.id 24 | }; 25 | } 26 | 27 | getConfigs() { 28 | return []; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Relay from 'react-relay'; 4 | 5 | import createHashHistory from 'history/lib/createHashHistory'; 6 | import { useRouterHistory, Route } from 'react-router'; 7 | import { RelayRouter } from 'react-router-relay'; 8 | 9 | import routes from './components/routes'; 10 | 11 | import SocketIONetworkLayer from './network/SocketIONetworkLayer'; 12 | import io from 'socket.io-client'; 13 | 14 | const socket = io(); 15 | 16 | Relay.injectNetworkLayer(new SocketIONetworkLayer(socket)); 17 | 18 | const history = useRouterHistory(createHashHistory)({ queryKey: false }); 19 | 20 | ReactDOM.render( 21 | , 22 | document.getElementById('root') 23 | ); 24 | -------------------------------------------------------------------------------- /src/components/channels/RedirectToDefaultChannel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | 4 | 5 | class RedirectToDefaultChannel extends React.Component { 6 | static contextTypes = { 7 | router: React.PropTypes.object.isRequired 8 | }; 9 | 10 | componentWillMount() { 11 | const {router} = this.context; 12 | const {viewer} = this.props; 13 | 14 | router.replace(`/channels/${viewer.defaultChannel.id}`); 15 | } 16 | 17 | render() { 18 | return
; 19 | } 20 | } 21 | 22 | export default Relay.createContainer(RedirectToDefaultChannel, { 23 | fragments: { 24 | viewer: () => Relay.QL` 25 | fragment on User { 26 | defaultChannel { 27 | id 28 | } 29 | } 30 | ` 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/channels/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | import { Route, IndexRoute } from 'react-router'; 4 | 5 | import RedirectToDefaultChannel from './RedirectToDefaultChannel'; 6 | import ChannelsScreen from './ChannelsScreen'; 7 | import ChannelRoutes from './channel/routes'; 8 | 9 | const ViewerQueries = { 10 | viewer: () => Relay.QL`query { viewer }` 11 | }; 12 | 13 | export default ([ 14 | , 19 | 24 | 28 | {ChannelRoutes} 29 | 30 | ]); 31 | -------------------------------------------------------------------------------- /scripts/updateSchema.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { schema } from '../src/schema'; 4 | import { graphql } from 'graphql'; 5 | import { introspectionQuery, printSchema } from 'graphql/utilities'; 6 | 7 | // Save JSON of full schema introspection for Babel Relay Plugin to use 8 | (async () => { 9 | var result = await (graphql(schema, introspectionQuery)); 10 | if (result.errors) { 11 | console.error( 12 | 'ERROR introspecting schema: ', 13 | JSON.stringify(result.errors, null, 2) 14 | ); 15 | } else { 16 | fs.writeFileSync( 17 | path.join(__dirname, '../data/schema.json'), 18 | JSON.stringify(result, null, 2) 19 | ); 20 | } 21 | })(); 22 | 23 | // Save user readable type system shorthand of schema 24 | fs.writeFileSync( 25 | path.join(__dirname, '../data/schema.graphql'), 26 | printSchema(schema) 27 | ); 28 | -------------------------------------------------------------------------------- /src/components/channels/channel/LeaveChannelSubscription.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | export default class LeaveChannelSubscription extends Relay.Subscription { 4 | 5 | static fragments = { 6 | channel: () => Relay.QL`fragment on Channel { id }` 7 | }; 8 | 9 | getSubscription() { 10 | return Relay.QL` 11 | subscription { 12 | leaveChannelSubscribe(input: $input) { 13 | memberId 14 | channel { 15 | memberCount 16 | } 17 | } 18 | } 19 | `; 20 | } 21 | 22 | getVariables() { 23 | return { 24 | channelId: this.props.channel.id 25 | }; 26 | } 27 | 28 | getConfigs() { 29 | return [{ 30 | type: 'RANGE_DELETE', 31 | parentName: 'channel', 32 | parentID: this.props.channel.id, 33 | connectionName: 'members', 34 | pathToConnection: ['channel', 'members'], 35 | deletedIDFieldName: 'memberId' 36 | }]; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/components/channels/channel/SendMessageSubscription.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | import Message from './Message'; 4 | 5 | export default class SendMessageSubscription extends Relay.Subscription { 6 | 7 | static fragments = { 8 | channel: () => Relay.QL`fragment on Channel { id }` 9 | }; 10 | 11 | getSubscription() { 12 | return Relay.QL` 13 | subscription { 14 | sendMessageSubscribe(input: $input) { 15 | edge { 16 | node { 17 | ${Message.getFragment('message')} 18 | } 19 | } 20 | } 21 | } 22 | `; 23 | } 24 | 25 | getVariables() { 26 | return { 27 | channelId: this.props.channel.id 28 | }; 29 | } 30 | 31 | getConfigs() { 32 | return [{ 33 | type: 'RANGE_ADD', 34 | parentName: 'channel', 35 | parentID: this.props.channel.id, 36 | connectionName: 'messages', 37 | edgeName: 'edge', 38 | rangeBehaviors: { 39 | '': 'append' 40 | } 41 | }]; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/mutations/LeaveChannelMutation.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | export default class LeaveChannelMutation extends Relay.Mutation { 4 | 5 | static fragments = { 6 | channel: () => Relay.QL`fragment on Channel { id }` 7 | }; 8 | 9 | getMutation() { 10 | return Relay.QL`mutation { leaveChannel }`; 11 | } 12 | 13 | getFatQuery() { 14 | return Relay.QL` 15 | fragment on LeaveChannelPayload { 16 | channel { 17 | joined 18 | } 19 | } 20 | `; 21 | } 22 | 23 | getConfigs() { 24 | return [{ 25 | type: 'RANGE_DELETE', 26 | parentName: 'channel', 27 | parentID: this.props.channel.id, 28 | connectionName: 'members', 29 | pathToConnection: ['channel', 'members'], 30 | deletedIDFieldName: 'memberId' 31 | }, { 32 | type: 'FIELDS_CHANGE', 33 | fieldIDs: { 34 | channel: this.props.channel.id 35 | } 36 | }]; 37 | } 38 | 39 | getVariables() { 40 | return { 41 | channelId: this.props.channel.id 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/mutations/JoinChannelMutation.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | export default class JoinChannelMutation extends Relay.Mutation { 4 | 5 | static fragments = { 6 | channel: () => Relay.QL`fragment on Channel { id }` 7 | }; 8 | 9 | getMutation() { 10 | return Relay.QL`mutation { joinChannel }`; 11 | } 12 | 13 | getFatQuery() { 14 | return Relay.QL` 15 | fragment on JoinChannelPayload { 16 | edge 17 | channel { 18 | joined 19 | } 20 | } 21 | `; 22 | } 23 | 24 | getConfigs() { 25 | return [{ 26 | type: 'RANGE_ADD', 27 | parentName: 'channel', 28 | parentID: this.props.channel.id, 29 | connectionName: 'members', 30 | edgeName: 'edge', 31 | rangeBehaviors: { 32 | '': 'append' 33 | } 34 | }, { 35 | type: 'FIELDS_CHANGE', 36 | fieldIDs: { 37 | channel: this.props.channel.id 38 | } 39 | }]; 40 | } 41 | 42 | getVariables() { 43 | return { 44 | channelId: this.props.channel.id 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/mutations/SendMessageMutation.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | export default class SendMessageMutation extends Relay.Mutation { 4 | 5 | static fragments = { 6 | viewer: () => Relay.QL`fragment on User { id }`, 7 | channel: () => Relay.QL`fragment on Channel { id }` 8 | }; 9 | 10 | getMutation() { 11 | return Relay.QL`mutation { sendMessage }`; 12 | } 13 | 14 | getFatQuery() { 15 | return Relay.QL` 16 | fragment on SendMessagePayload { 17 | edge 18 | channel { 19 | messages 20 | } 21 | } 22 | `; 23 | } 24 | 25 | getConfigs() { 26 | return [{ 27 | type: 'RANGE_ADD', 28 | parentName: 'channel', 29 | parentID: this.props.channel.id, 30 | connectionName: 'messages', 31 | edgeName: 'edge', 32 | rangeBehaviors: { 33 | '': 'append' 34 | } 35 | }]; 36 | } 37 | 38 | getVariables() { 39 | return { 40 | text: this.props.text, 41 | senderId: this.props.viewer.id, 42 | channelId: this.props.channel.id 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/components/channels/channel/JoinChannelSubscription.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | import Member from './Member'; 4 | 5 | export default class JoinChannelSubscription extends Relay.Subscription { 6 | 7 | static fragments = { 8 | channel: () => Relay.QL`fragment on Channel { id }` 9 | }; 10 | 11 | getSubscription() { 12 | return Relay.QL` 13 | subscription { 14 | joinChannelSubscribe(input: $input) { 15 | edge { 16 | node { 17 | ${Member.getFragment('member')} 18 | } 19 | } 20 | channel { 21 | memberCount 22 | } 23 | } 24 | } 25 | `; 26 | } 27 | 28 | getVariables() { 29 | return { 30 | channelId: this.props.channel.id 31 | }; 32 | } 33 | 34 | getConfigs() { 35 | return [{ 36 | type: 'RANGE_ADD', 37 | parentName: 'channel', 38 | parentID: this.props.channel.id, 39 | connectionName: 'members', 40 | edgeName: 'edge', 41 | rangeBehaviors: { 42 | '': 'append' 43 | } 44 | }]; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "babel-node server.js", 5 | "update-schema": "babel-node scripts/updateSchema.js" 6 | }, 7 | "dependencies": { 8 | "babel-cli": "6.4.0", 9 | "babel-core": "6.4.0", 10 | "babel-loader": "6.2.1", 11 | "babel-preset-es2015": "6.3.13", 12 | "babel-preset-react": "6.3.13", 13 | "babel-preset-stage-0": "6.3.13", 14 | "babel-relay-plugin": "0.6.3", 15 | "compression": "1.6.1", 16 | "express": "4.13.3", 17 | "express-graphql": "0.4.5", 18 | "graphql": "0.4.14", 19 | "graphql-relay": "0.3.6", 20 | "history": "2.0.0-rc2", 21 | "moment": "2.11.1", 22 | "ramda": "0.19.1", 23 | "react": "0.14.6", 24 | "react-dom": "0.14.6", 25 | "react-relay": "file:../../git/relay", 26 | "react-router": "2.0.0-rc5", 27 | "react-router-relay": "0.9.0", 28 | "socket.io": "1.4.4", 29 | "socket.io-client": "1.4.4", 30 | "starwars": "1.0.0", 31 | "webpack": "1.12.11", 32 | "webpack-dev-middleware": "1.4.0", 33 | "webpack-dev-server": "1.14.1" 34 | }, 35 | "babel": { 36 | "presets": [ 37 | "es2015", 38 | "react", 39 | "stage-0" 40 | ], 41 | "plugins": [ 42 | "./build/babelRelayPlugin" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/channels/channel/Member.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | 4 | class Member extends React.Component { 5 | 6 | activeDot() { 7 | const {viewer,member} = this.props; 8 | 9 | if (viewer.id === member.id) { 10 | return ; 11 | } 12 | } 13 | 14 | render() { 15 | const {viewer,member} = this.props; 16 | 17 | return ( 18 |
19 | {this.props.member.name} 20 | {this.activeDot()} 21 |
22 | ); 23 | } 24 | 25 | } 26 | 27 | export default Relay.createContainer(Member, { 28 | fragments: { 29 | member: () => Relay.QL` 30 | fragment on User { 31 | id 32 | name 33 | } 34 | `, 35 | viewer: () => Relay.QL` 36 | fragment on User { 37 | id 38 | } 39 | ` 40 | } 41 | }); 42 | 43 | const styles = { 44 | member: { 45 | fontSize: '13px', 46 | lineHeight: '36px', 47 | }, 48 | active: { 49 | display: 'inline-block', 50 | backgroundColor: 'rgb(0,187,125)', 51 | width: '10px', 52 | height: '10px', 53 | borderRadius: '5px', 54 | margin: '0 8px' 55 | }, 56 | name: { 57 | color: 'rgb(85,83,89)', 58 | marginLeft: '14px' 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import express from 'express'; 3 | import graphQLHTTP from 'express-graphql'; 4 | import path from 'path'; 5 | import webpack from 'webpack'; 6 | import compression from 'compression'; 7 | import webpackMiddleware from 'webpack-dev-middleware'; 8 | import IO from 'socket.io'; 9 | 10 | import {schema} from './src/schema'; 11 | import {connect} from './src/server/socket'; 12 | import * as bots from './src/server/bots'; 13 | 14 | const APP_PORT = 3000; 15 | 16 | const compiler = webpack({ 17 | entry: path.resolve(__dirname, 'src', 'app.js'), 18 | output: {filename: 'app.js', path: '/'}, 19 | module: { 20 | loaders: [{ 21 | exclude: /node_modules/, 22 | loader: 'babel' 23 | }] 24 | } 25 | }); 26 | 27 | const app = express(); 28 | const server = http.Server(app); 29 | const io = IO(server); 30 | 31 | io.on('connect', connect); 32 | 33 | bots.start(); 34 | 35 | app.use(compression()); 36 | app.use(express.static(path.resolve(__dirname, 'public'))); 37 | 38 | app.use('/graphql', graphQLHTTP({schema, rootValue: {}, pretty:true, graphiql:true})); 39 | app.use(webpackMiddleware(compiler, { 40 | contentBase: '/public/', 41 | publicPath: '/js/', 42 | stats: {colors: true, chunks: false} 43 | })); 44 | 45 | server.listen(APP_PORT, () => { 46 | console.log(`App is now running on http://localhost:${APP_PORT}`); 47 | }); 48 | -------------------------------------------------------------------------------- /src/schema/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | GraphQLObjectType 4 | } from 'graphql'; 5 | 6 | import {NodeField} from './node'; 7 | import {UserField,ViewerField} from './user'; 8 | import { 9 | ChannelField, 10 | JoinChannelMutation, 11 | JoinChannelSubscribe, 12 | LeaveChannelMutation, 13 | LeaveChannelSubscribe 14 | } from './channel'; 15 | import { 16 | MessageField, 17 | SendMessageMutation, 18 | SendMessageSubscribe 19 | } from './message'; 20 | 21 | const Query = new GraphQLObjectType({ 22 | name: 'Query', 23 | fields: () => ({ 24 | node: NodeField, 25 | viewer: ViewerField, 26 | 27 | // convenience fields 28 | user: UserField, 29 | channel: ChannelField, 30 | message: MessageField 31 | }) 32 | }); 33 | 34 | const Mutation = new GraphQLObjectType({ 35 | name: 'Mutation', 36 | fields: () => ({ 37 | sendMessage: SendMessageMutation, 38 | joinChannel: JoinChannelMutation, 39 | leaveChannel: LeaveChannelMutation 40 | }) 41 | }); 42 | 43 | const Subscription = new GraphQLObjectType({ 44 | name: 'Subscription', 45 | fields: () => ({ 46 | sendMessageSubscribe: SendMessageSubscribe, 47 | joinChannelSubscribe: JoinChannelSubscribe, 48 | leaveChannelSubscribe: LeaveChannelSubscribe 49 | }) 50 | }); 51 | 52 | export const schema = new GraphQLSchema({ 53 | query: Query, 54 | mutation: Mutation, 55 | subscription: Subscription 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/channels/ChannelList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | 4 | import Channel from './Channel'; 5 | 6 | class ChannelList extends React.Component { 7 | 8 | render() { 9 | const {viewer,activeChannelId} = this.props; 10 | const {channels} = viewer; 11 | 12 | return ( 13 |
14 |
Channels ({channels.edges.length})
15 | {channels.edges.map(edge => ( 16 | 21 | ))} 22 |
23 | ); 24 | } 25 | 26 | } 27 | 28 | export default Relay.createContainer(ChannelList, { 29 | fragments: { 30 | viewer: () => Relay.QL` 31 | fragment on User { 32 | channels(first: 50) { 33 | edges { 34 | node { 35 | id 36 | ${Channel.getFragment('channel')} 37 | } 38 | } 39 | } 40 | } 41 | ` 42 | } 43 | }); 44 | 45 | const styles = { 46 | list: { 47 | margin: 0, 48 | padding: 0 49 | }, 50 | title: { 51 | fontSize: '16px', 52 | lineHeight: '30px', 53 | marginLeft: '14px', 54 | color: 'rgb(175,152,169)', 55 | fontWeight: 'bold' 56 | }, 57 | number: { 58 | color: 'rgb(128,103,122)' 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/server/bots.js: -------------------------------------------------------------------------------- 1 | import starwars from 'starwars'; 2 | 3 | import * as db from '../database'; 4 | import * as events from '../events'; 5 | 6 | // const [min, max] = [10000, 45000]; 7 | const [min, max] = [5000, 15000]; 8 | 9 | 10 | const general = db.general; 11 | 12 | const bill = db.createUser({name: 'Bill'}); 13 | const brian = db.createUser({name: 'Brian'}); 14 | const dave = db.createUser({name: 'Dave'}); 15 | const jing = db.createUser({name: 'Jing'}); 16 | 17 | const greeter = db.createUser({name: 'Greeter'}); 18 | 19 | const sendRandomMessage = user => { 20 | db.sendMessage(starwars(), user.id, general.id); 21 | setTimeout(() => { 22 | sendRandomMessage(user); 23 | }, Math.random() * (max - min) + min); 24 | } 25 | 26 | const greetChannel = (greeter, channel) => { 27 | db.joinChannel(greeter.id, channel.id); 28 | 29 | events.subscribe(`/channel/${channel.id}/members/join`, ev => { 30 | const user = db.load('User', ev.userId); 31 | if (user.id !== greeter.id) { 32 | db.sendMessage(`Hello ${user.name}! Hope you are having a great day!`, greeter.id, channel.id); 33 | } 34 | }); 35 | } 36 | 37 | export const start = () => { 38 | db.loginUser(bill.id); 39 | db.loginUser(brian.id); 40 | db.loginUser(dave.id); 41 | db.loginUser(jing.id); 42 | 43 | sendRandomMessage(bill); 44 | sendRandomMessage(brian); 45 | sendRandomMessage(dave); 46 | sendRandomMessage(jing); 47 | 48 | const needHelp = db.loadAll('Channel').find(c => c.name === 'need help'); 49 | greetChannel(greeter, needHelp); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/channels/ChannelsScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | 4 | import ChannelList from './ChannelList'; 5 | 6 | class ChannelsScreen extends React.Component { 7 | 8 | render() { 9 | 10 | const {children,viewer,params} = this.props; 11 | 12 | return ( 13 |
14 |
15 |
{viewer.name}
16 | 20 |
21 |
22 | {children} 23 |
24 |
25 | ); 26 | } 27 | 28 | } 29 | 30 | export default Relay.createContainer(ChannelsScreen, { 31 | fragments: { 32 | viewer: () => Relay.QL` 33 | fragment on User { 34 | name 35 | ${ChannelList.getFragment('viewer')} 36 | } 37 | ` 38 | } 39 | }); 40 | 41 | const styles = { 42 | container: { 43 | position: 'absolute', 44 | top: 0, 45 | left: 0, 46 | right: 0, 47 | bottom: 0 48 | }, 49 | sidebar: { 50 | overflowY: 'auto', 51 | position: 'absolute', 52 | backgroundColor: '#51354B', 53 | top: 0, 54 | left: 0, 55 | bottom: 0, 56 | width: 200 57 | }, 58 | name: { 59 | color: '#fff', 60 | fontSize: '24px', 61 | lineHeight: '36px', 62 | marginLeft: '14px' 63 | }, 64 | body: { 65 | position: 'absolute', 66 | top: 0, 67 | bottom: 0, 68 | left: 200, 69 | right: 0, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/schema/user.js: -------------------------------------------------------------------------------- 1 | import R from 'ramda'; 2 | 3 | import { 4 | GraphQLObjectType, 5 | GraphQLNonNull, 6 | GraphQLID, 7 | GraphQLInt, 8 | GraphQLString 9 | } from 'graphql'; 10 | 11 | import { 12 | connectionArgs, 13 | connectionDefinitions, 14 | connectionFromArray, 15 | globalIdField 16 | } from 'graphql-relay'; 17 | 18 | import * as db from '../database'; 19 | 20 | import {NodeInterface} from './node'; 21 | import {ChannelType,ChannelConnection} from './channel'; 22 | 23 | export const UserType = new GraphQLObjectType({ 24 | name: 'User', 25 | fields: () => ({ 26 | id: globalIdField('User'), 27 | name: {type: GraphQLString}, 28 | defaultChannel: {type: ChannelType, resolve: db.resolve }, 29 | channels: { 30 | type: ChannelConnection, 31 | args: connectionArgs, 32 | resolve: (_, args) => connectionFromArray(R.sortBy(R.prop('name'), db.loadAll('Channel')), args) 33 | } 34 | }), 35 | isTypeOf: (obj) => obj instanceof db.User, 36 | interfaces: () => [NodeInterface] 37 | }); 38 | 39 | const { 40 | connectionType: UserConnection, 41 | edgeType: UserEdge 42 | } = connectionDefinitions({ 43 | name: 'User', 44 | nodeType: UserType 45 | }); 46 | 47 | export { 48 | UserConnection, 49 | UserEdge 50 | }; 51 | 52 | export const UserField = { 53 | type: UserType, 54 | args: { 55 | id: {type: new GraphQLNonNull(GraphQLID)} 56 | }, 57 | resolve: (_, {id}) => db.load('User', id) 58 | } 59 | 60 | // default for graphiql 61 | const viewerId = db.create('User', {name: 'Huey'}).id 62 | export const ViewerField = { 63 | type: UserType, 64 | resolve: ({userId}) => db.load('User', userId || viewerId) 65 | } 66 | -------------------------------------------------------------------------------- /src/components/channels/channel/MessageComposer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | 4 | import SendMessageMutation from '../../../mutations/SendMessageMutation'; 5 | 6 | const ENTER_KEY_CODE = 13; 7 | 8 | class MessageComposer extends React.Component { 9 | 10 | state = { 11 | text: '' 12 | }; 13 | 14 | componentDidMount() { 15 | this.refs.textarea.focus(); 16 | } 17 | 18 | handleChange(event, value) { 19 | this.setState({text: event.target.value}); 20 | } 21 | 22 | handleKeyDown(event) { 23 | if (event.keyCode === ENTER_KEY_CODE) { 24 | event.preventDefault(); 25 | var text = this.state.text.trim(); 26 | if (text) { 27 | Relay.Store.commitUpdate(new SendMessageMutation({ 28 | text, 29 | viewer: this.props.viewer, 30 | channel: this.props.channel 31 | })); 32 | } 33 | this.setState({text: ''}); 34 | } 35 | } 36 | 37 | render() { 38 | return ( 39 |