├── .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 |