├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .nvmrc
├── .travis.yml
├── LICENSE
├── README.md
├── components
├── _base.scss
├── _mixins.scss
├── _shapes.scss
├── avatar
│ ├── avatar.jsx
│ ├── gravatar.js
│ ├── index.js
│ └── style.scss
├── channel
│ ├── addchannel.jsx
│ ├── channel.jsx
│ ├── index.js
│ ├── list.jsx
│ └── style.scss
├── common
│ ├── contextMenuStyle.scss
│ ├── contextmenu.jsx
│ ├── index.js
│ ├── input.jsx
│ ├── menuitem.jsx
│ ├── navSectionStyle.scss
│ ├── navsection.jsx
│ ├── spinner.jsx
│ └── style.scss
├── index.js
├── markdown
│ ├── help.jsx
│ ├── index.js
│ └── style.scss
├── message
│ ├── action.jsx
│ ├── age.jsx
│ ├── base.scss
│ ├── counter.jsx
│ ├── counters.scss
│ ├── github.scss
│ ├── index.js
│ ├── message.jsx
│ ├── messageinput.jsx
│ ├── replyform.scss
│ ├── style.scss
│ ├── uploadbutton.jsx
│ └── username.jsx
├── thread
│ ├── contributors.jsx
│ ├── day.jsx
│ ├── index.js
│ ├── list.jsx
│ ├── style.scss
│ ├── thread.jsx
│ ├── threadform.jsx
│ └── util.js
├── user
│ ├── index.js
│ ├── list.jsx
│ ├── menu.jsx
│ ├── menuStyle.scss
│ └── style.scss
└── util.js
├── demo
├── actions.js
├── api.js
├── app.jsx
├── devtools.jsx
├── index.jsx
├── state.js
├── store.js
└── style.scss
├── deploy.sh
├── devserver.js
├── fiber.iml
├── index.html
├── lib
├── _base.scss
├── _mixins.scss
├── _shapes.scss
├── avatar
│ ├── avatar.js
│ ├── gravatar.js
│ ├── index.js
│ └── style.scss
├── channel
│ ├── addchannel.js
│ ├── channel.js
│ ├── index.js
│ ├── list.js
│ └── style.scss
├── common
│ ├── contextMenuStyle.scss
│ ├── contextmenu.js
│ ├── index.js
│ ├── input.js
│ ├── menuitem.js
│ ├── navSectionStyle.scss
│ ├── navsection.js
│ ├── spinner.js
│ └── style.scss
├── index.js
├── markdown
│ ├── help.js
│ ├── index.js
│ └── style.scss
├── message
│ ├── action.js
│ ├── age.js
│ ├── base.scss
│ ├── counter.js
│ ├── counters.scss
│ ├── github.scss
│ ├── index.js
│ ├── message.js
│ ├── messageinput.js
│ ├── replyform.scss
│ ├── style.scss
│ ├── uploadbutton.js
│ └── username.js
├── thread
│ ├── contributors.js
│ ├── day.js
│ ├── index.js
│ ├── list.js
│ ├── style.scss
│ ├── thread.js
│ ├── threadform.js
│ └── util.js
├── user
│ ├── index.js
│ ├── list.js
│ ├── menu.js
│ ├── menuStyle.scss
│ └── style.scss
└── util.js
├── lint.cmd
├── make.cmd
├── makesite.sh
├── package.json
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = crlf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules/
2 | build
3 | lib
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 |
5 | "env": {
6 | "browser": true,
7 | "node": true,
8 | "mocha": true,
9 | "es6": true
10 | },
11 |
12 | "globals": {
13 | "$": true,
14 | "swal": true,
15 | },
16 |
17 | "rules": {
18 | "id-length": 0,
19 | "indent": ["error", 2, {"SwitchCase": 1}],
20 | "no-console": 0,
21 | "no-plusplus": 0,
22 | "func-names": 0,
23 | "no-use-before-define": 0,
24 | "linebreak-style": 0,
25 | "import/no-extraneous-dependencies": 0,
26 | "react/no-multi-comp": 0,
27 | "react/prop-types": 0,
28 | "react/jsx-indent": ["error", 2],
29 | "jsx-a11y/no-static-element-interactions": 0
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 6.1.0
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 6.1.0
4 | env:
5 | global:
6 | - GH_REF: github.com/reactbits/fiber.git
7 | - secure: FB4oP9AX77IBVhPRTwSqoDmUX6xE1uk8nACcgRvM4oZXVBEh+dV4a4wDltg1Zli+Unbkl4U9pSKwLjAyQkiHntutmUvOxk2F1ZUq/qL7M4JbBpA93Xzb7+ecaXviomkQ5E7kXqvmgoLMI9TtSnf6oOnJ7RCUfu04pxVO+gpvcvOAk8V/hRKyJvNSS6FBKfUHZobDj93pxF51Jnr5JOH5nrpP3nYz/iaSj5lgT7+yeESYwXCfzgF9VPEmnLZykq4kOAIhwpm3yvxLM3zpt51TUaDZKAoljjcJEoE73BLwkmTthffL4pbRQ0odLIIGai5cUnsB2mDHBnwMYpyvO2tSwZcJhAXKp4yLL4r9oz99Zx5mSS+7PJ/EZ+inJ29DpcO03pfGoctIcdBWrwGTAwiHUbKKZselBQ1Vbugnn93vViOl+eLfeb9ERI4U8nsDe7ZacuZ0vEfwjaQ5LI+KDcnmsYhs3KvQu5COK82juj3sZbNbhSQPGGioIWAQcxaM69onIUK+TO9yz3J21qeHN4GD7GE0rYhakVyh2X2hJPdQwwVcwuzk38raMXsLnLZ6/Wj/5v82GKWgjWyO/wI0bALV5Apjc6f6zEDP+K69Tel+uKsP3baaiIUSOatyBiJY2rTjYfATYPmsv0mTIqShsfAIY+nOhu0sMZ28H56s2uPWNew=
8 | script:
9 | - npm test
10 | - bash ./deploy.sh
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 reactbits
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/js/react-fiber)
2 | [](https://travis-ci.org/reactbits/fiber)
3 | [](https://www.npmjs.com/package/react-fiber)
4 |
5 | [](https://david-dm.org/reactbits/fiber)
6 | [](https://david-dm.org/reactbits/fiber#info=devDependencies)
7 |
8 | # fiber
9 |
10 | React components to use in messaging applications.
11 |
--------------------------------------------------------------------------------
/components/_base.scss:
--------------------------------------------------------------------------------
1 | @import "./_mixins";
2 |
3 | $lightgreen: #42aa42;
4 | $lightblue: #1fadc5;
5 |
6 | $input-focus-color: $lightblue;
7 |
8 | .input {
9 | border: 1px solid rgba(0,0,0,0.1);
10 | border-radius: .2em;
11 | outline: none;
12 | }
13 |
14 | .input:focus {
15 | box-shadow: 0 0 .2em $input-focus-color inset;
16 | border-color: $input-focus-color;
17 | }
18 |
--------------------------------------------------------------------------------
/components/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin border-radius ($radius: 4px) {
2 | -webkit-border-radius: $radius;
3 | -moz-border-radius: $radius;
4 | -ms-border-radius: $radius;
5 | -o-border-radius: $radius;
6 | border-radius: $radius;
7 | }
8 |
9 | @mixin transition ($args...) {
10 | -webkit-transition: $args;
11 | -moz-transition: $args;
12 | -o-transition: $args;
13 | -ms-transition: $args;
14 | transition: $args;
15 | }
16 |
17 | @mixin transform-transition ($args...) {
18 | -webkit-transition: -webkit-transform $args;
19 | -moz-transition: -moz-transform $args;
20 | -o-transition: -o-transform $args;
21 | -ms-transition: -ms-transform $args;
22 | transition: transform $args;
23 | }
24 |
25 | @mixin transform ($args...) {
26 | -webkit-transform: $args;
27 | -moz-transform: $args;
28 | -o-transform: $args;
29 | -ms-transform: $args;
30 | transform: $args;
31 | }
32 |
33 | @mixin arrow-left ($position: 50%, $width: 7px, $length: 10px, $background-color: #fcfcfc, $border-color: #ddd) {
34 | &:before {
35 | content: '';
36 | position: absolute;
37 | border-style: solid;
38 | border-width: $width $length $width 0;
39 | border-color: transparent $border-color;
40 | display: block;
41 | width: 0;
42 | z-index: 0;
43 | margin-top: -$width;
44 | left: (-$length - 1);
45 | top: $position;
46 | }
47 | &:after {
48 | content: '';
49 | position: absolute;
50 | border-style: solid;
51 | border-width: $width $length $width 0;
52 | border-color: transparent $background-color;
53 | display: block;
54 | width: 0;
55 | z-index: 1;
56 | margin-top: -$width;
57 | left: -$length;
58 | top: $position;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/components/_shapes.scss:
--------------------------------------------------------------------------------
1 | @import "./_mixins";
2 |
3 | @mixin heart($size:24px, $color:red) {
4 | position: relative;
5 | width: $size;
6 | height: $size*0.9;
7 | &:before,
8 | &:after {
9 | position: absolute;
10 | content: "";
11 | left: $size/2;
12 | top: 0;
13 | width: $size/2;
14 | height: $size*0.8;
15 | background: $color;
16 | border-radius: $size/2 $size/2 0 0;
17 | transform: rotate(-45deg);
18 | transform-origin: 0 100%;
19 | }
20 | &:after {
21 | left: 0;
22 | transform: rotate(45deg);
23 | transform-origin :100% 100%;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/components/avatar/avatar.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React, { Component, PropTypes } from 'react';
3 | import classNames from 'classnames';
4 | import ImageLoader from 'react-imageloader';
5 | import { hover, hint } from 'css-effects';
6 | import is from 'is_js';
7 | import { Spinner } from '../common';
8 | import style from './style.scss';
9 | import gravatarURL from './gravatar';
10 | import { toPromise } from '../util';
11 |
12 | const avatarSizes = {
13 | small: 24,
14 | sm: 24,
15 | s: 24,
16 | normal: 32,
17 | medium: 32,
18 | m: 32,
19 | large: 40,
20 | l: 40,
21 | xl: 64,
22 | };
23 |
24 | const defaultSize = 32;
25 |
26 | export function avatarSize(value) {
27 | return (_.isString(value) ? avatarSizes[value.toLowerCase()] : value) || defaultSize;
28 | }
29 |
30 | function avatarURL(url) {
31 | if (is.email(url)) {
32 | return gravatarURL(url);
33 | }
34 | return url;
35 | }
36 |
37 | function makePreloader(size) {
38 | return () => {
39 | const css = {
40 | width: size,
41 | height: size,
42 | };
43 | return (
44 |
45 |
46 |
47 | );
48 | };
49 | }
50 |
51 | function RandomAvatar(props) {
52 | // TODO render random avatar
53 | const size = props.size;
54 | return (
55 |
56 | );
57 | }
58 |
59 | function makeWrapper(props) {
60 | return (sourceProps, content) => {
61 | const attrs = { ...sourceProps, ...props };
62 | if (props.title) {
63 | delete attrs.title;
64 | attrs.className = classNames(attrs.className, hint());
65 | attrs['data-hint'] = props.title;
66 | }
67 | return (
68 | {content}
69 | );
70 | };
71 | }
72 |
73 | export default class Avatar extends Component {
74 | static propTypes = {
75 | className: PropTypes.string,
76 | size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
77 | };
78 |
79 | static defaultProps = {
80 | className: '',
81 | size: 'normal',
82 | style: {},
83 | };
84 |
85 | constructor(props) {
86 | super(props);
87 |
88 | let source = props.source;
89 | if (!source) {
90 | const user = props.user;
91 | if (user && _.isObject(user)) {
92 | source = user.avatar_url || user.avatar;
93 | }
94 | }
95 |
96 | this.state = { source, user: props.user };
97 | }
98 |
99 | online() {
100 | if (this.props.online) {
101 | return this.props.online;
102 | }
103 | const user = this.state.user;
104 | return user && _.isObject(user) && user.online;
105 | }
106 |
107 | title() {
108 | const value = this.props.title || this.props.name;
109 | if (value) return value;
110 | const user = this.state.user;
111 | if (user && _.isObject(user)) {
112 | return user.name || user.login;
113 | }
114 | return '';
115 | }
116 |
117 | render() {
118 | // TODO shadow
119 | const props = this.props;
120 | const shape = style[props.shape || 'circle'];
121 | const hoverEffect = hover(props.hover);
122 | const className = classNames(
123 | this.props.className,
124 | style.avatar, {
125 | [shape]: true,
126 | [style.online]: this.online(),
127 | [style.circled]: props.circled,
128 | [hoverEffect]: true,
129 | },
130 | );
131 |
132 | const src = avatarURL(this.state.source);
133 | const size = avatarSize(this.props.size);
134 | const avatarStyle = {
135 | width: size + 8,
136 | height: size + 8,
137 | marginLeft: -(size + 8),
138 | ...this.props.style,
139 | };
140 |
141 | const imgProps = {
142 | width: size,
143 | height: size,
144 | };
145 |
146 | const wrapper = makeWrapper({
147 | title: this.title(),
148 | });
149 |
150 | const preloader = makePreloader(size);
151 |
152 | function empty() {
153 | return (
154 |
155 | {preloader()}
156 |
157 | );
158 | }
159 |
160 | const sourcePromise = toPromise(src);
161 | if (sourcePromise) {
162 | sourcePromise.then((value) => {
163 | // TODO fix possible call of setState after unmount
164 | this.setState({ source: value });
165 | });
166 | return empty();
167 | }
168 |
169 | if (!src && this.props.user) {
170 | const userPromise = toPromise(this.props.user);
171 | if (userPromise) {
172 | userPromise.then((user) => {
173 | // TODO fix possible call of setState after unmount
174 | this.setState({
175 | user,
176 | source: user.avatar_url || user.avatar,
177 | });
178 | });
179 | return empty();
180 | }
181 | }
182 |
183 | if (!_.isString(src)) {
184 | // TODO render error
185 | return empty();
186 | }
187 |
188 | const loaderProps = {
189 | src,
190 | wrapper,
191 | preloader,
192 | imgProps,
193 | };
194 |
195 | return (
196 |
197 |
198 |
199 | );
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/components/avatar/gravatar.js:
--------------------------------------------------------------------------------
1 | import md5 from 'md5';
2 | import queryString from 'query-string';
3 |
4 | const defaultOptions = {
5 | d: 'retro',
6 | };
7 |
8 | function isHash(s) {
9 | return /^[a-f0-9]{32}$/i.test((s || '').trim().toLowerCase());
10 | }
11 |
12 | // TODO cash gravatar URLs
13 | export default function gravatarURL(email, size = 32, options = defaultOptions) {
14 | let url = 'https://secure.gravatar.com/avatar/';
15 |
16 | if (isHash(email)) {
17 | url += email;
18 | } else {
19 | url += md5(email.toLowerCase());
20 | }
21 |
22 | const qs = queryString.stringify({ s: size, ...options });
23 | if (qs) {
24 | url += `?${qs}`;
25 | }
26 |
27 | return url;
28 | }
29 |
--------------------------------------------------------------------------------
/components/avatar/index.js:
--------------------------------------------------------------------------------
1 | export { default, avatarSize } from './avatar';
2 |
--------------------------------------------------------------------------------
/components/avatar/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 | @import "../_mixins";
3 |
4 | .preloader {
5 | justify-content: center;
6 | }
7 |
8 | .avatar {
9 | cursor: pointer;
10 | position: relative;
11 | margin: 4px 4px 4px -45px;
12 | overflow: visible;
13 | }
14 |
15 | .avatar.circle img {
16 | border-radius: 50%;
17 | }
18 |
19 | .avatar.round_rect img {
20 | border: 1px solid transparent;
21 | border-radius: 4px;
22 | }
23 |
24 | .avatar.round_rect img:hover {
25 | border: 1px solid transparentize(#1fadc5, 0.5);
26 | }
27 |
28 | .avatar.circled {
29 | border: 2px solid lighten(orange, 25%);
30 | border-radius: 50%;
31 | padding: 2px;
32 | }
33 |
34 | .online:after {
35 | content: "\f400";
36 | color: #4cbe00;
37 | position: absolute;
38 | top: -.4em;
39 | right: -.4em;
40 | text-shadow: 0 0 .1em #fff;
41 | font-size: .8em;
42 | font-family: "Ionicons";
43 | z-index: 99;
44 | }
45 |
46 | @mixin shadow ($size: 2px) {
47 | border: $size solid #fff;
48 | box-shadow: 0 1px 1px rgba(0,0,0,0.3);
49 | }
50 |
51 | .shadow {
52 | @include shadow()
53 | }
54 | .shadow_1 {
55 | @include shadow(1px)
56 | }
57 | .shadow_2 {
58 | @include shadow(2px)
59 | }
60 | .shadow_3 {
61 | @include shadow(3px)
62 | }
63 |
--------------------------------------------------------------------------------
/components/channel/addchannel.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Flex, Box } from 'reflexbox';
4 | import { Modal, Button } from 'react-bootstrap';
5 | import { Form, Input } from 'reactbits-input';
6 | import style from './style.scss';
7 |
8 | class Dialog extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | show: true,
13 | };
14 | }
15 |
16 | render() {
17 | const close = () => this.setState({ show: false });
18 | const inputs = {
19 | name: {
20 | name: 'name',
21 | placeholder: 'Channel name',
22 | required: true,
23 | },
24 | description: {
25 | name: 'description',
26 | placeholder: 'Description',
27 | },
28 | };
29 | return (
30 |
31 |
48 |
49 | );
50 | }
51 | }
52 |
53 | export default function newChannelDialog(callback) {
54 | let wrapper = null;
55 | const submit = (data) => {
56 | setTimeout(() => {
57 | ReactDOM.unmountComponentAtNode(wrapper);
58 | wrapper.remove();
59 | callback(data);
60 | }, 100);
61 | };
62 | wrapper = document.body.appendChild(document.createElement('div'));
63 | ReactDOM.render(, wrapper);
64 | }
65 |
--------------------------------------------------------------------------------
/components/channel/channel.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import classNames from 'classnames';
4 | import style from './style.scss';
5 | import { NavItem } from '../common';
6 |
7 | function removeButton(onClick) {
8 | return ×;
9 | }
10 |
11 | // TODO render channel actions
12 | export default function Channel(props) {
13 | const canRemove = _.isFunction(props.remove);
14 | const className = classNames(style.channel, {
15 | [style.selected_channel]: props.selected,
16 | });
17 |
18 | const select = () => {
19 | if (_.isFunction(props.select)) {
20 | props.select(props.data);
21 | }
22 | };
23 |
24 | const itemProps = {
25 | className,
26 | onClick: select,
27 | selected: props.selected,
28 | to: props.to,
29 | };
30 |
31 | return (
32 |
33 | {props.data.name}
34 |
35 | {canRemove ? removeButton(props.remove) : null}
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/components/channel/index.js:
--------------------------------------------------------------------------------
1 | export { default as Channel } from './channel';
2 | export { default as ChannelList } from './list';
3 |
--------------------------------------------------------------------------------
/components/channel/list.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import classNames from 'classnames';
4 | import Channel from './channel';
5 | import newChannelDialog from './addchannel';
6 | import style from './style.scss';
7 | import { NavSection, NavBody, NavHeader, NavHeaderButtons, PlusButton } from '../common';
8 |
9 | export default function ChannelList(props) {
10 | const className = classNames(props.className, style.channel_list);
11 | const selectedId = (props.selectedChannel || {}).id;
12 | const channels = _.map(props.channels, (cn, i) => {
13 | const cnprops = {
14 | key: cn.id || i,
15 | data: cn,
16 | selected: cn.id === selectedId,
17 | select: props.selectChannel,
18 | remove: _.isFunction(props.removeChannel) ? props.removeChannel.bind(null, cn) : undefined,
19 | to: cn.id && props.basePath ? `${props.basePath}/${cn.id}` : undefined,
20 | };
21 | return ;
22 | });
23 | const onPlusClick = () => {
24 | newChannelDialog(props.createChannel);
25 | };
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {channels}
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/components/channel/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .btn_add_channel {
4 | font-size: 20px;
5 | }
6 |
7 | .btn_remove_channel {
8 | margin-left: 4px;
9 | font-size: 20px;
10 | font-weight: bold;
11 | cursor: pointer;
12 | }
13 |
--------------------------------------------------------------------------------
/components/common/contextMenuStyle.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .context_menu .Popover-body {
4 | display: inline-flex;
5 | flex-direction: column;
6 | background: white;
7 | border-radius: 0.3rem;
8 | box-shadow: 0 0 10px rgba(0,0,0,0.25);
9 | }
10 |
11 | .context_menu .Popover-tipShape {
12 | fill: white;
13 | }
14 |
15 | .context_menu ul {
16 | list-style-type: none;
17 | margin: .75rem 0;
18 | padding: 0;
19 | }
20 |
21 | .context_menu ul li {
22 | line-height: 1.5rem;
23 | margin: 0;
24 | }
25 |
26 | .context_menu ul li a {
27 | cursor: pointer;
28 | text-decoration: none;
29 | padding: 0 1rem 0 .75rem;
30 | margin: 0 .75rem;
31 | border-radius: .25rem;
32 | overflow: hidden;
33 | text-overflow: ellipsis;
34 | white-space: nowrap;
35 | outline: 0;
36 | font-size: 15px;
37 | line-height: 25px;
38 | background: 0 0;
39 | color: #2c2d30;
40 | }
41 |
42 | .context_menu ul li a:hover {
43 | background-color: rgb(0, 156, 255);
44 | color: #fff;
45 | }
46 |
47 | .context_menu .header_item {
48 | cursor: default;
49 | margin: .625rem .75rem;
50 | position: relative;
51 | }
52 |
53 | .context_menu .header_item hr {
54 | border: 0;
55 | border-top: 1px solid #ddd;
56 | border-bottom: 1px solid #fff;
57 | position: absolute;
58 | left: 1rem;
59 | right: 0;
60 | margin: 1.25rem 0;
61 | margin-top: .6rem;
62 | margin-bottom: 0;
63 | }
64 |
65 | .context_menu .header_item .header_label {
66 | position: relative;
67 | background-color: #fff;
68 | color: #9e9ea6;
69 | padding-right: .8rem;
70 | }
71 |
72 | .show_button {
73 | cursor: pointer;
74 | text-decoration: none;
75 | }
76 |
77 | .show_button:hover {
78 | text-decoration: none;
79 | }
80 |
--------------------------------------------------------------------------------
/components/common/contextmenu.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import classNames from 'classnames';
3 | import PopoverClass from 'react-popover';
4 | import styles from './contextMenuStyle.scss';
5 |
6 | const popover = React.createFactory(PopoverClass);
7 |
8 | export default class ContextMenu extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | dropdownVisible: false,
13 | };
14 | }
15 |
16 | render() {
17 | const { button } = this.props;
18 |
19 | const hide = () => this.setState({ dropdownVisible: false });
20 | const showDropdown = (e) => {
21 | e.preventDefault();
22 | e.stopPropagation();
23 | this.setState({ dropdownVisible: true });
24 | return false;
25 | };
26 |
27 | const buttonProps = {
28 | className: classNames(styles.show_button, button.className),
29 | onMouseDown: showDropdown,
30 | };
31 | const buttonElement = {button.content};
32 |
33 | const dropdownProps = {
34 | className: styles.context_menu,
35 | isOpen: this.state.dropdownVisible,
36 | preferPlace: 'below',
37 | place: 'below',
38 | onOuterAction: hide,
39 | body: ,
40 | refreshIntervalMs: false,
41 | };
42 |
43 | return (
44 |
45 | {popover(dropdownProps, buttonElement)}
46 |
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/components/common/index.js:
--------------------------------------------------------------------------------
1 | export { default as Input } from './input';
2 | export { default as Spinner } from './spinner';
3 | export { default as ContextMenu } from './contextmenu';
4 | export { default as ContextMenuItem } from './menuitem';
5 | export {
6 | NavSection,
7 | NavHeader,
8 | NavHeaderButtons,
9 | NavBody,
10 | NavItem,
11 | IconButton,
12 | PlusButton,
13 | } from './navsection';
14 |
--------------------------------------------------------------------------------
/components/common/input.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React, { Component } from 'react';
3 | import { findDOMNode } from 'react-dom';
4 | import style from './style.scss';
5 |
6 | export default class Input extends Component {
7 | componentDidMount() {
8 | if (this.props.focused) {
9 | this.focus();
10 | }
11 | }
12 |
13 | componentWillReceiveProps(nextProps) {
14 | if (nextProps.focused) {
15 | this.focus();
16 | }
17 | }
18 |
19 | input = null;
20 |
21 | focus() {
22 | if (this.input) {
23 | const node = findDOMNode(this.input);
24 | if (node) {
25 | node.focus();
26 | }
27 | }
28 | }
29 |
30 | render() {
31 | // eslint-disable-next-line no-unused-vars
32 | const { cancel, submit, focused, ...props } = this.props || {};
33 |
34 | const onKeyUp = (event) => {
35 | if (event.which === 27) {
36 | const input = $(event.target);
37 | input.blur();
38 | if (_.isFunction(props.cancel)) {
39 | cancel();
40 | return;
41 | }
42 | return;
43 | }
44 | if (event.ctrlKey && event.which === 13 && _.isFunction(submit)) {
45 | submit();
46 | }
47 | };
48 |
49 | const onMouseDown = () => {
50 | this.focus();
51 | };
52 |
53 | const attrs = {
54 | ref: (c) => { this.input = c; },
55 | className: props.className || style.input,
56 | type: 'text',
57 | onKeyUp,
58 | onMouseDown,
59 | ...props,
60 | };
61 |
62 | return ;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/components/common/menuitem.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 | import styles from './contextMenuStyle.scss';
4 |
5 | export default class MenuItem extends Component {
6 | renderContent() {
7 | const { href, children, link, onClick } = this.props;
8 | if (href) {
9 | return {children};
10 | }
11 | if (link) {
12 | return {children};
13 | }
14 | return {children};
15 | }
16 |
17 | render() {
18 | if (this.props.header) {
19 | return (
20 |
21 |
22 | {this.props.children}
23 |
24 | );
25 | }
26 | return (
27 |
28 | {this.renderContent()}
29 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/components/common/navSectionStyle.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .nav_section {
4 | margin: 4px 0;
5 | }
6 |
7 | .nav_item {
8 | margin: 4px 0;
9 |
10 | a {
11 | display: inline-flex;
12 | align-items: center;
13 | justify-content: center;
14 | padding: .25em 0.5em;
15 | border-radius: .5em;
16 | color: #333;
17 | cursor: pointer;
18 | text-decoration: none;
19 | line-height: 1;
20 |
21 | &:hover {
22 | background-color: #ccc;
23 | }
24 | }
25 | }
26 |
27 | .nav_item_selected a {
28 | color: white;
29 | background-color: $lightgreen;
30 |
31 | &:hover {
32 | background-color: $lightgreen;
33 | }
34 | }
35 |
36 | .nav_title {
37 | font-weight: bold;
38 | cursor: pointer;
39 | }
40 |
41 | .nav_buttons {
42 | display: inline-block;
43 | padding-left: 8px;
44 | }
45 |
46 | .icon_button {
47 | cursor: pointer;
48 | }
49 |
--------------------------------------------------------------------------------
/components/common/navsection.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import { Link } from 'react-router';
4 | import classNames from 'classnames';
5 | import { hint } from 'css-effects';
6 | import styles from './navSectionStyle.scss';
7 |
8 | export function NavItem(props) {
9 | const selected = props.selected || location.pathname === props.to;
10 | const className = classNames(props.className, styles.nav_item, {
11 | [styles.nav_item_selected]: selected,
12 | });
13 | const link = props.to
14 | ? {props.children}
15 | : {props.children};
16 | return (
17 | {link}
18 | );
19 | }
20 |
21 | export function IconButton(props) {
22 | const className = classNames(styles.icon_button, hint());
23 | return (
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export function PlusButton(props) {
31 | return ;
32 | }
33 |
34 | export function NavHeaderButtons(props) {
35 | return (
36 |
37 | {props.children}
38 |
39 | );
40 | }
41 |
42 | export function NavHeader(props) {
43 | const className = classNames(props.className, styles.nav_header);
44 | const title = props.title ? {props.title} : null;
45 | return (
46 |
47 | {title}
48 | {props.children}
49 |
50 | );
51 | }
52 |
53 | export function NavBody(props) {
54 | const className = classNames(props.className, styles.nav_body);
55 | return (
56 |
57 | {props.children}
58 |
59 | );
60 | }
61 |
62 | export function NavSection(props) {
63 | const className = classNames(props.className, styles.nav_section);
64 | return (
65 |
66 | {props.children}
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/components/common/spinner.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import classNames from 'classnames';
4 | import css from 'loaders.css/src/loaders.scss';
5 | import styles from './style.scss';
6 |
7 | // const defaultType = 'ball-beat';
8 | const defaultType = 'ball-scale-multiple';
9 |
10 | // TODO fix loader styles (also make it relative to image size)
11 | const loaders = {
12 | 'ball-pulse': {
13 | dots: 3,
14 | size: '6px',
15 | },
16 | 'ball-grid-pulse': {
17 | dots: 9,
18 | },
19 | 'ball-clip-rotate': {
20 | dots: 1,
21 | },
22 | 'ball-clip-rotate-pulse': {
23 | dots: 2,
24 | },
25 | 'square-spin': {
26 | dots: 1,
27 | },
28 | 'ball-clip-rotate-multiple': {
29 | dots: 2,
30 | },
31 | 'ball-pulse-rise': {
32 | dots: 5,
33 | },
34 | 'ball-rotate': {
35 | dots: 1,
36 | },
37 | 'cube-transition': {
38 | dots: 2,
39 | },
40 | 'ball-zig-zag': {
41 | dots: 2,
42 | },
43 | 'ball-zig-zag-deflect': {
44 | dots: 2,
45 | },
46 | 'ball-triangle-path': {
47 | dots: 3,
48 | },
49 | 'ball-scale': {
50 | dots: 1,
51 | },
52 | 'line-scale': {
53 | dots: 5,
54 | },
55 | 'line-scale-party': {
56 | dots: 4,
57 | },
58 | 'ball-scale-multiple': {
59 | dots: 3,
60 | size: '32px',
61 | left: '-16px',
62 | top: '16px',
63 | },
64 | 'ball-pulse-sync': {
65 | dots: 3,
66 | },
67 | 'ball-beat': {
68 | dots: 3,
69 | size: '6px',
70 | },
71 | 'line-scale-pulse-out': {
72 | dots: 5,
73 | },
74 | 'line-scale-pulse-out-rapid': {
75 | dots: 5,
76 | },
77 | 'ball-scale-ripple': {
78 | dots: 1,
79 | },
80 | 'ball-scale-ripple-multiple': {
81 | dots: 3,
82 | size: '32px',
83 | left: '-16px',
84 | top: '8px',
85 | },
86 | 'ball-spin-fade-loader': {
87 | dots: 8,
88 | },
89 | 'line-spin-fade-loader': {
90 | dots: 8,
91 | },
92 | 'triangle-skew-spin': {
93 | dots: 1,
94 | },
95 | pacman: {
96 | dots: 5,
97 | },
98 | 'ball-grid-beat': {
99 | dots: 9,
100 | },
101 | 'semi-circle-spin': {
102 | dots: 1,
103 | },
104 | };
105 |
106 | export const loaderTypes = Object.keys(loaders);
107 |
108 | function dots(props) {
109 | return _.range(props.dots).map((i) => {
110 | const style = {};
111 | const attrs = { style };
112 | if (props.size) {
113 | style.width = props.size;
114 | style.height = props.size;
115 | }
116 | if (props.left) {
117 | style.left = props.left;
118 | }
119 | if (props.top) {
120 | style.top = props.top;
121 | }
122 | return ;
123 | });
124 | }
125 |
126 | export default function Spinner(props) {
127 | const type = props.type || defaultType;
128 | const className = classNames(styles.loader_inner, css[type]);
129 | const args = [
130 | { className },
131 | dots(loaders[type]),
132 | ];
133 | const inner = React.DOM.div(...args);
134 | const loaderProps = {
135 | className: styles.loader,
136 | };
137 | if (props.size) {
138 | loaderProps.style = {
139 | width: props.size,
140 | height: props.size,
141 | display: 'table-cell',
142 | verticalAlign: 'middle',
143 | };
144 | }
145 | return (
146 | {inner}
147 | );
148 | }
149 |
--------------------------------------------------------------------------------
/components/common/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | $spinner-color: transparentize($input-focus-color, 0.4);
4 |
5 | .loader {
6 | text-align: center;
7 | }
8 |
9 | .loader_inner {
10 | display: inline-block;
11 | }
12 |
13 | .loader .loader_inner > div {
14 | background-color: $spinner-color;
15 | }
16 |
--------------------------------------------------------------------------------
/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Markdown } from './markdown';
2 | export { default as Avatar } from './avatar';
3 | export { default as Message } from './message';
4 | export { Thread, ThreadList, ThreadForm } from './thread';
5 | export { Channel, ChannelList } from './channel';
6 | export { UserList, UserMenu } from './user';
7 | export {
8 | Spinner,
9 | ContextMenu,
10 | ContextMenuItem,
11 | NavSection,
12 | NavHeader,
13 | NavHeaderButtons,
14 | NavItem,
15 | IconButton,
16 | PlusButton,
17 | } from './common';
18 |
--------------------------------------------------------------------------------
/components/markdown/help.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import style from './style.scss';
3 | import { ContextMenu } from '../common';
4 |
5 | function Content() {
6 | const quote = '> ';
7 | const monospaced = '`';
8 | return (
9 |
10 |
11 | *bold*
12 | _italic_
13 | {quote}{'quoted'}
14 | {monospaced}{'monospaced'}{monospaced}
15 | [title](link)
16 |
17 |
18 | {'```js'}
19 | javascript code
20 | {'```'}
21 |
22 |
23 | ctrl
24 | {' + '}
25 | enter
26 | post
27 |
28 |
29 | );
30 | }
31 |
32 | export default function Help() {
33 | const menuProps = {
34 | button: {
35 | className: style.show_help,
36 | content: '?',
37 | },
38 | };
39 | return (
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/components/markdown/index.js:
--------------------------------------------------------------------------------
1 | export { default as Help } from './help'; // eslint-disable-line
2 |
--------------------------------------------------------------------------------
/components/markdown/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .focused .show_help {
4 | display: inline-block;
5 | }
6 |
7 | .show_help {
8 | cursor: pointer;
9 | display: none;
10 | position: absolute;
11 | right: .2em;
12 | padding: .3em;
13 | transition: color .2s;
14 | font-size: 16px;
15 | font-weight: bold;
16 | text-decoration: none;
17 | color: $input-focus-color;
18 | }
19 |
20 | .show_help:hover {
21 | text-decoration: none;
22 | }
23 |
24 | .monospaced {
25 | font-family: 'Bitstream Vera Sans Mono',Menlo,Monaco,Courier,monospace;
26 | }
27 |
28 | .help_content {
29 | margin: 2rem 4rem;
30 | }
31 |
32 | .help_format {
33 | padding: .6em 0;
34 | }
35 |
36 | .help_code {
37 | border: 1px solid #ddd;
38 | border-width: 1px 0;
39 | padding: .6em 0;
40 | margin-bottom: 1.2em;
41 | }
42 |
43 | .help_post em {
44 | background: #eee;
45 | border-radius: .3em;
46 | padding: .1em .4em 0;
47 | color: #333;
48 | font-weight: bold;
49 | display: inline-block;
50 | }
51 |
--------------------------------------------------------------------------------
/components/message/action.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import classNames from 'classnames';
4 | import { hint } from 'css-effects';
5 | import style from './style.scss';
6 | import Counter from './counter';
7 |
8 | // TODO configurable icons
9 | export const ionIconSet = {
10 | like: 'ion-ios-heart',
11 | reply: 'ion-ios-chatbubble',
12 | star: 'ion-star',
13 | remove: 'ion-trash-a',
14 | edit: 'ion-edit',
15 | };
16 |
17 | export const faIconSet = {
18 | like: 'fa fa-heart',
19 | reply: 'fa fa-comment',
20 | star: 'fa fa-star',
21 | remove: 'fa fa-trash',
22 | edit: 'fa fa-pencil',
23 | };
24 |
25 | export const tips = {
26 | like: 'Like',
27 | reply: 'Reply',
28 | star: 'Star',
29 | remove: 'Delete',
30 | edit: 'Edit',
31 | };
32 |
33 | const actionClassNames = {
34 | like: style.like_count,
35 | reply: style.message_count,
36 | };
37 |
38 | function getIconSet(name) {
39 | switch (name) {
40 | case 'fa':
41 | case 'awesome':
42 | return faIconSet;
43 | case 'ion':
44 | case 'ionic':
45 | default:
46 | return ionIconSet;
47 | }
48 | }
49 |
50 | export function Action(props) {
51 | const { action } = props;
52 | const count = props.count || 0;
53 | const onClick = (e) => {
54 | e.preventDefault();
55 | if (_.isFunction(props.onAction)) {
56 | props.onAction(props.type, action, props.data);
57 | }
58 | };
59 |
60 | if (action === 'reply') {
61 | const attrs = {
62 | className: actionClassNames[action],
63 | count,
64 | onClick,
65 | title: tips[action],
66 | element: React.DOM.a,
67 | };
68 | return ;
69 | }
70 |
71 | const className = classNames({
72 | [hint()]: true,
73 | [action]: true,
74 | [style.action]: true,
75 | 'pull-right': props.right,
76 | });
77 | const iconSet = getIconSet(props.iconSet);
78 |
79 | return (
80 |
81 |
82 | {count > 0 ? {count} : null}
83 |
84 | );
85 | }
86 |
87 | export default Action;
88 |
89 | export function renderActions(actions, type, data, options) {
90 | return Object.keys(actions)
91 | .filter((key) => {
92 | if (!_.isFunction(options.canExecute)) return true;
93 | return options.canExecute(type, key, data);
94 | })
95 | .map((key) => {
96 | const props = {
97 | data,
98 | type,
99 | action: key,
100 | onAction: options.onAction,
101 | iconSet: options.iconSet,
102 | ...actions[key],
103 | };
104 | return ;
105 | });
106 | }
107 |
--------------------------------------------------------------------------------
/components/message/age.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import classNames from 'classnames';
4 | import moment from 'moment';
5 | import style from './style.scss';
6 |
7 | function isToday(value) {
8 | if (!moment.isDate(value)) return false;
9 | const now = moment();
10 | const m = moment(value);
11 | return m.year() === now.year() && m.dayOfYear() === now.dayOfYear();
12 | }
13 |
14 | const formatTime = (value) => {
15 | if (!value) {
16 | return '';
17 | }
18 | if (_.isString(value)) {
19 | return value;
20 | }
21 | if (isToday(value)) {
22 | return moment(value).fromNow();
23 | }
24 | return moment(value).format('HH:mm');
25 | };
26 |
27 | export default function Age({ time }) {
28 | const text = formatTime(time);
29 | const className = classNames(style.time, {
30 | [style.today]: isToday(time),
31 | });
32 | const attrs = {
33 | className,
34 | };
35 |
36 | if (moment.isDate(time)) {
37 | attrs['data-toggle'] = 'tooltip';
38 | attrs.title = moment(time).format('ddd MMM D YYYY HH:mm:ss');
39 | }
40 |
41 | return (
42 | {text}
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/components/message/base.scss:
--------------------------------------------------------------------------------
1 | .message {
2 | margin: 2px 0px;
3 | padding: 0px 20px 0px 80px;
4 | }
5 |
6 | .message_body {
7 | min-height: 44px;
8 | }
9 |
10 | .reply {
11 | padding: 0px 0px 0px 40px;
12 | }
13 |
14 | .name {
15 | color: #bbb;
16 | }
17 |
18 | .time {
19 | color: #bbb;
20 | font-weight: 400;
21 | margin: 0 0 0 6px;
22 | }
23 |
24 | .today {
25 | color: #36a5d9;
26 | }
27 |
28 | .meta span, .meta a, .meta i {
29 | margin-right: 0.3em;
30 | }
31 |
32 | .action, .action:hover {
33 | font-size: 14px;
34 | text-decoration: none;
35 | cursor: pointer;
36 | margin-right: 0.3em;
37 | }
38 |
--------------------------------------------------------------------------------
/components/message/counter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { hint } from 'css-effects';
3 | import style from './style.scss';
4 |
5 | export default function Counter(props) {
6 | const className = props.className || style.message_count;
7 | const attrs = { className, onClick: props.onClick };
8 | const counter = {props.count};
9 | if (props.title) {
10 | return {counter};
11 | }
12 | return counter;
13 | }
14 |
--------------------------------------------------------------------------------
/components/message/counters.scss:
--------------------------------------------------------------------------------
1 | $mc-border-color: #9197a3;
2 |
3 | .message_count {
4 | margin: 0 8px;
5 | display: inline-block;
6 | position: relative;
7 | padding: 0px 4px;
8 | background: white;
9 | color: #4e5665;
10 | border: $mc-border-color solid 1px;
11 | border-radius: 2px;
12 | font-size: 11px;
13 | font-weight: normal;
14 | text-align: center;
15 | white-space: nowrap;
16 | min-width: 15px;
17 | cursor: pointer;
18 | text-decoration: none;
19 | @include arrow-left($width: 4px, $length: 4px, $background-color: white, $border-color: $mc-border-color);
20 | }
21 |
22 | .message_count:hover {
23 | text-decoration: none;
24 | }
25 |
26 | .like_count {
27 | display: inline-block;
28 | cursor: pointer;
29 | white-space: nowrap;
30 | text-align: center;
31 | text-decoration: none;
32 | @include heart($size: 20px);
33 | }
34 |
35 | .like_count:hover {
36 | text-decoration: none;
37 | }
38 |
--------------------------------------------------------------------------------
/components/message/github.scss:
--------------------------------------------------------------------------------
1 | @import "../_mixins";
2 |
3 | .message_wrapper.github {
4 | padding-left: 64px;
5 | }
6 |
7 | .message_wrapper.github .avatar {
8 | margin-left: -64px;
9 | }
10 |
11 | .message.github {
12 | position: relative;
13 | padding: 0;
14 | border: 1px solid #ddd;
15 | border-radius: 3px;
16 | @include arrow-left($position: 11px, $width: 5px, $length: 5px, $background-color: #f7f7f7, $border-color: #ddd);
17 | }
18 |
19 | .message.github .meta {
20 | padding: 10px 15px;
21 | background-color: #f7f7f7;
22 | border-bottom: 1px solid #eee;
23 | border-top-left-radius: 3px;
24 | border-top-right-radius: 3px;
25 | }
26 |
27 | .message.github .meta .name {
28 | color: #767676;
29 | font-weight: bold;
30 | }
31 |
32 | .message.github .message_body {
33 | padding: 15px;
34 | }
35 |
--------------------------------------------------------------------------------
/components/message/index.js:
--------------------------------------------------------------------------------
1 | export { Message, getTime } from './message';
2 | export { default as MessageInput } from './messageinput';
3 | export { default as Counter } from './counter';
4 | export { default } from './message';
5 |
--------------------------------------------------------------------------------
/components/message/message.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React, { Component, PropTypes } from 'react';
3 | import classNames from 'classnames';
4 | import Markdown from 'react-markdown2';
5 | import Avatar, { avatarSize } from '../avatar';
6 | import UserName from './username';
7 | import Age from './age';
8 | import MessageInput from './messageinput';
9 | import { renderActions } from './action';
10 | import style from './style.scss';
11 | import { promiseOnce, getOrFetch } from '../util';
12 |
13 | // TODO unread style
14 | // TODO custom background
15 | // TODO button with menu (reply, delete, star, like, etc)
16 |
17 | export const getTime = (msg) => {
18 | const t = msg.updated_at || msg.created_at || msg.time;
19 | if (!t) return null;
20 | const d = new Date(t);
21 | return isNaN(d.getTime()) ? null : d;
22 | };
23 |
24 | export class Message extends Component {
25 | static propTypes = {
26 | className: PropTypes.string,
27 | avatarSize: Avatar.propTypes.size,
28 | isReply: PropTypes.bool,
29 | theme: PropTypes.string,
30 | };
31 |
32 | static defaultProps = {
33 | className: '',
34 | data: {},
35 | avatarSize: '',
36 | isReply: false,
37 | theme: 'plain',
38 | };
39 |
40 | constructor(props) {
41 | super(props);
42 | this.state = {
43 | showReplyInput: false,
44 | showEdit: false,
45 | collapsed: false,
46 | };
47 | }
48 |
49 | renderReplyInput() {
50 | if (!this.state.showReplyInput) return null;
51 | const props = this.props;
52 | const data = props.data || props;
53 | const hideReplyInput = () => {
54 | this.setState({ showReplyInput: false });
55 | };
56 | const sendReply = (text) => {
57 | hideReplyInput();
58 | if (_.isFunction(props.sendMessage)) {
59 | props.sendMessage({ thread_id: data.thread_id, in_reply_to: data.id, body: text });
60 | }
61 | };
62 | return ;
63 | }
64 |
65 | renderEditor() {
66 | if (!this.state.showEdit) return null;
67 | const props = this.props;
68 | const data = props.data || props;
69 | const hideEdit = () => {
70 | this.setState({ showEdit: false });
71 | };
72 | const updateMessage = (text) => {
73 | hideEdit();
74 | if (_.isFunction(props.updateMessage)) {
75 | props.updateMessage({ thread_id: data.thread_id, id: data.id, body: text });
76 | }
77 | };
78 | return ;
79 | }
80 |
81 | renderActions() {
82 | const props = this.props;
83 | const data = props.data || props;
84 | const replies = data.replies || [];
85 |
86 | const showReply = () => {
87 | this.setState({ showReplyInput: true, showEdit: false });
88 | };
89 |
90 | const showEdit = () => {
91 | this.setState({ showEdit: true, showReplyInput: false });
92 | };
93 |
94 | const actions = {
95 | reply: { count: replies.length, onAction: showReply },
96 | like: { count: data.likes || 0 },
97 | edit: { onAction: showEdit },
98 | remove: { },
99 | star: { },
100 | };
101 |
102 | const actionProps = {
103 | onAction: props.onAction,
104 | canExecute: props.canExecute,
105 | iconSet: props.iconSet,
106 | };
107 |
108 | return renderActions(actions, 'message', data, actionProps);
109 | }
110 |
111 | renderReplies(fetchUser) {
112 | const props = this.props;
113 | const data = props.data || props;
114 | // TODO support data.replies as promise
115 | const replies = data.replies || [];
116 |
117 | return (this.state.collapsed ? [] : replies).map((d) => {
118 | const replyProps = {
119 | data: d,
120 | isReply: true,
121 | avatarSize: props.avatarSize,
122 | fetchUser,
123 | onAction: props.onAction,
124 | canExecute: props.canExecute,
125 | sendMessage: props.sendMessage,
126 | updateMessage: props.updateMessage,
127 | theme: props.theme,
128 | };
129 | return ;
130 | });
131 | }
132 |
133 | render() {
134 | const className = classNames(style.message, this.props.className, style[this.props.theme], {
135 | [style.reply]: !!this.props.isReply,
136 | });
137 | const data = this.props.data || this.props;
138 | const user = data.user;
139 | const time = getTime(data);
140 | const fetchUser = promiseOnce(data.fetchUser || this.props.fetchUser, data);
141 | const userName = getOrFetch(fetchUser, user, 'name', 'login');
142 |
143 | const outerAvatar = this.props.theme === 'github';
144 | const avatarProps = {
145 | user: user || fetchUser,
146 | size: this.props.avatarSize,
147 | circled: this.props.theme === 'plain',
148 | style: {
149 | float: 'left',
150 | },
151 | };
152 |
153 | const bodyProps = {
154 | className: classNames(style.message_body),
155 | style: {
156 | minHeight: avatarSize(this.props.avatarSize) - 16,
157 | },
158 | };
159 |
160 | // TODO render badges
161 | // TODO spam icon
162 | const toggleIconProps = {
163 | className: this.state.collapsed ? 'fa fa-plus-square-o' : 'fa fa-minus-square-o',
164 | onClick: () => this.setState({ collapsed: !this.state.collapsed }),
165 | };
166 |
167 | return (
168 |
169 | {outerAvatar ?
: null}
170 |
171 | {outerAvatar ? null :
}
172 |
173 |
174 | {userName ?
: null}
175 | {time ?
: null}
176 |
177 | {this.renderActions()}
178 |
179 |
180 | {
181 | this.state.collapsed ? null :
182 |
183 |
184 |
185 | }
186 | {this.renderReplyInput()}
187 | {this.renderEditor()}
188 | {this.renderReplies(fetchUser)}
189 |
190 |
191 | );
192 | }
193 | }
194 |
195 | export default Message;
196 |
--------------------------------------------------------------------------------
/components/message/messageinput.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React, { Component, PropTypes } from 'react';
3 | import classNames from 'classnames';
4 | import { Button } from 'react-bootstrap';
5 | import { Input } from '../common';
6 | import Help from '../markdown/help';
7 | import UploadButton from './uploadbutton';
8 | import style from './style.scss';
9 |
10 | // TODO render user avatar
11 | // TODO configure submit shortcut, ctrl-enter is default
12 |
13 | export default class MessageInput extends Component {
14 | static propTypes = {
15 | submit: PropTypes.func,
16 | cancel: PropTypes.func,
17 | focused: PropTypes.bool,
18 | };
19 |
20 | static defaultProps = {
21 | focused: false,
22 | submit: _.noop,
23 | cancel: _.noop,
24 | };
25 |
26 | constructor(props) {
27 | super(props);
28 |
29 | this.state = {
30 | focused: this.props.focused,
31 | value: props.value || '',
32 | helpVisible: false,
33 | };
34 |
35 | this.onChange = this.onChange.bind(this);
36 |
37 | const self = this;
38 | function makeFocusTransition(focused) {
39 | return () => {
40 | if (focused && _.isFunction(props.onFocus)) {
41 | props.onFocus();
42 | }
43 | if (!focused && _.isFunction(props.onBlur)) {
44 | props.onBlur();
45 | }
46 | self.setState({ focused });
47 | };
48 | }
49 | this.onFocus = makeFocusTransition(true);
50 | this.onBlur = makeFocusTransition(false);
51 | }
52 |
53 | componentWillReceiveProps(nextProps) {
54 | if (this.state.focused !== nextProps.focused) {
55 | this.setState({ focused: nextProps.focused });
56 | }
57 | }
58 |
59 | onChange(event) {
60 | const value = event.target.value || '';
61 | if (_.isFunction(this.props.onChange)) {
62 | this.props.onChange(value);
63 | }
64 | this.setState({ value });
65 | }
66 |
67 | render() {
68 | const canSubmit = _.isFunction(this.props.canSubmit) ?
69 | this.props.canSubmit
70 | : () => this.state.value.length > 0;
71 | const submit = () => {
72 | const { value } = this.state;
73 | if (!value) return;
74 | this.setState({ value: '' });
75 | this.props.submit(value);
76 | };
77 | const inputProps = {
78 | className: classNames(style.input, style.message_input),
79 | placeholder: this.props.placeholder || 'Reply...',
80 | value: this.state.value,
81 | onChange: this.onChange,
82 | onFocus: this.onFocus,
83 | onBlur: this.onBlur,
84 | cancel: this.props.cancel,
85 | submit,
86 | focused: this.state.focused,
87 | };
88 | const submitProps = {
89 | className: 'pull-right',
90 | bsStyle: 'primary',
91 | bsSize: 'small',
92 | onMouseDown: submit,
93 | disabled: !canSubmit(),
94 | };
95 | const formProps = {
96 | className: classNames(style.reply_form, { [style.focused]: this.state.focused }),
97 | style: (this.props.formStyle || {}),
98 | };
99 | const onUpload = (data) => {
100 | let content = this.state.value || '';
101 | if (content) {
102 | content += '\r\n';
103 | }
104 | content += `[${data.name}](${data.url})`;
105 | this.setState({ value: content });
106 | };
107 | const uploadProps = {
108 | onSuccess: onUpload,
109 | };
110 | return (
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | );
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/components/message/replyform.scss:
--------------------------------------------------------------------------------
1 | .reply_form {
2 | position: relative;
3 | padding: .6em 0 1.2em 4em;
4 | width: 100%;
5 | }
6 |
7 | .message_input {
8 | width: 100%;
9 | height: 100%;
10 | min-height: 2em;
11 | padding: .6em;
12 | overflow: hidden;
13 | font-size: .9em;
14 | line-height: 1.5;
15 | resize: vertical;
16 | }
17 |
18 | .reply_form.focused .reply_controls {
19 | display: block;
20 | }
21 | .reply_controls {
22 | display: none;
23 | text-align: right;
24 | margin: .6em 0;
25 | position: relative;
26 | }
27 |
28 | .upload_button {
29 | cursor: pointer;
30 | position: absolute;
31 | font-size: 20px;
32 | right: .4em;
33 | top: -2em;
34 | text-decoration: none;
35 | color: rgba(0,0,0,0.4);
36 | }
37 | .upload_button:hover {
38 | text-decoration: none;
39 | color: $input-focus-color;
40 | }
41 |
--------------------------------------------------------------------------------
/components/message/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 | @import "../_mixins";
3 | @import "../_shapes";
4 | @import "base";
5 | @import "github";
6 | @import "replyform";
7 | @import "counters";
8 |
--------------------------------------------------------------------------------
/components/message/uploadbutton.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import tus from 'tus-js-client';
4 | // import Upload from 'component-upload';
5 | import style from './style.scss';
6 |
7 | // TODO configurable upload client: tus, component-upload, etc
8 |
9 | function uploadFile(file, callback = _.noop) {
10 | // const upload = new Upload(file);
11 | // upload.on('end', res => {
12 | // console.log(res);
13 | // });
14 | // const options = {
15 | // path: window.UPLOAD_PATH || '/api/uploads/',
16 | // };
17 | // upload.to(options);
18 |
19 | // Create a new tus upload
20 | const upload = new tus.Upload(file, {
21 | // TODO enable resumable uploads
22 | resume: false,
23 | endpoint: window.UPLOAD_PATH || '/api/uploads/',
24 | onError(err) {
25 | console.log('upload failed:', err);
26 | callback(null, err);
27 | },
28 | onProgress(bytesUploaded, bytesTotal) {
29 | const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
30 | console.log('progress %s/%s, %s', bytesUploaded, bytesTotal, `${percentage}%`);
31 | },
32 | onSuccess() {
33 | console.log('download %s from %s', upload.file.name, upload.url);
34 | callback({ name: upload.file.name, url: upload.url });
35 | },
36 | });
37 |
38 | // Start the upload
39 | upload.start();
40 | }
41 |
42 | export default function UploadButton(props) {
43 | const onClick = () => {
44 | const inputStyle = 'display:block;visibility:hidden;width:0;height:0';
45 | const input = $(``);
46 | input.appendTo($('body'));
47 | input.change(() => {
48 | console.log('uploading...');
49 | const files = input[0].files;
50 | console.log(files);
51 | const file = files[0];
52 | input.remove();
53 | uploadFile(file, (data, err) => {
54 | if (err) {
55 | if (_.isFunction(props.onError)) {
56 | props.onError(err);
57 | }
58 | return;
59 | }
60 | if (_.isFunction(props.onSuccess)) {
61 | props.onSuccess(data);
62 | }
63 | });
64 | });
65 | input.click();
66 | };
67 | const btnProps = {
68 | className: style.upload_button,
69 | title: 'Upload files',
70 | 'data-toggle': 'tooltip',
71 | onMouseDown: onClick,
72 | };
73 | return (
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/components/message/username.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React, { Component } from 'react';
3 | import classNames from 'classnames';
4 | import style from './style.scss';
5 | import { toPromise } from '../util';
6 |
7 | export default class UserName extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = { name: props.name };
11 | this.setName = this.setName.bind(this);
12 | }
13 |
14 | setName(value) {
15 | return this.setState({ name: value });
16 | }
17 |
18 | render() {
19 | let value = this.state.name;
20 | if (!value) {
21 | return ;
22 | }
23 | if (!_.isString(value)) {
24 | const promise = toPromise(value);
25 | if (promise) {
26 | promise.then(this.setName);
27 | }
28 | // TODO render small spinner
29 | value = '';
30 | }
31 | return {value};
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/components/thread/contributors.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React, { Component } from 'react';
3 | import Avatar from '../avatar';
4 | import style from './style.scss';
5 |
6 | export default class ContributorList extends Component {
7 | constructor(props) {
8 | super(props);
9 | const { users } = props;
10 | this.state = {
11 | users: _.isFunction(users) ? users() : users,
12 | };
13 | }
14 |
15 | componentDidMount() {
16 | this.mounted = true;
17 | this.update(this.props);
18 | }
19 |
20 | componentWillReceiveProps(nextProps) {
21 | this.update(nextProps);
22 | }
23 |
24 | componentWillUnmount() {
25 | this.mounted = false;
26 | if (_.isFunction(this.unsubscribe)) {
27 | this.unsubscribe();
28 | this.unsubscribe = null;
29 | }
30 | }
31 |
32 | mounted = false;
33 |
34 | update(props) {
35 | if (_.isFunction(this.unsubscribe)) {
36 | this.unsubscribe();
37 | this.unsubscribe = null;
38 | }
39 |
40 | const { users } = props;
41 |
42 | if (_.isFunction(users)) {
43 | const result = users((list) => {
44 | if (!this.mounted) return;
45 | this.setState({ users: list });
46 | });
47 | if (_.isFunction(result)) {
48 | this.unsubscribe = result;
49 | } else if (_.isArray(result)) {
50 | this.setState({ users: result });
51 | }
52 | } else {
53 | this.setState({ users });
54 | }
55 | }
56 |
57 | render() {
58 | const items = _.map(this.state.users, (user) => {
59 | const avatarProps = {
60 | hover: 'grow',
61 | user,
62 | shape: 'round_rect',
63 | online: false,
64 | size: 24,
65 | style: {
66 | display: 'inline-block',
67 | margin: '0px',
68 | },
69 | };
70 | return ;
71 | });
72 | return (
73 |
74 | {items}
75 |
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/components/thread/day.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import moment from 'moment';
4 | import style from './style.scss';
5 | import { Counter } from '../message';
6 |
7 | // TODO move to common components
8 | export function TextBlock(props) {
9 | return (
10 |
11 | {props.text || ''}
12 |
13 | );
14 | }
15 |
16 | const formatDay = (time) => {
17 | const now = moment();
18 | const day = now.dayOfYear();
19 | const m = moment(time);
20 | // this year
21 | if (m.year() === now.year()) {
22 | if (m.dayOfYear() === day) {
23 | // TODO localization
24 | return 'Today';
25 | }
26 | if (m.dayOfYear() === day - 1) {
27 | // TODO localization
28 | return 'Yesterday';
29 | }
30 | // this week
31 | if (m.week() === now.week()) {
32 | return m.format('dddd');
33 | }
34 | return m.format('MMMM D, dddd');
35 | }
36 | return m.format('MMMM D YYYY, dddd');
37 | };
38 |
39 | export default function Day(props) {
40 | const className = classNames(style.day);
41 | const text = formatDay(props.time);
42 | return (
43 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/thread/index.js:
--------------------------------------------------------------------------------
1 | export { default, default as Thread } from './thread';
2 | export { default as ThreadList } from './list';
3 | export { default as ThreadForm } from './threadform';
4 |
--------------------------------------------------------------------------------
/components/thread/list.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import classNames from 'classnames';
4 | import moment from 'moment';
5 | import Avatar from '../avatar';
6 | import Thread from './thread';
7 | import style from './style.scss';
8 |
9 | function formatTime(value) {
10 | return value ? moment(value).format('HH:mm') : '';
11 | }
12 |
13 | // TODO reuse rendering of user name from message component
14 |
15 | export const Topic = (props) => {
16 | const className = classNames(style.topic, {
17 | [style.topic_selected]: !!props.selected,
18 | });
19 | const msg = props.last_message || props.message || {};
20 | const user = msg.user;
21 | const unread = props.unread ? `${props.unread > 10 ? '10+' : props.unread} new` : '';
22 | const avatarURL = user ? user.avatar_url || user.avatar : null;
23 |
24 | const onClick = (e) => {
25 | e.preventDefault();
26 | if (_.isFunction(props.onSelect)) {
27 | props.onSelect(props.thread);
28 | }
29 | };
30 |
31 | return (
32 |
33 | {avatarURL ?
: null}
34 |
35 | {props.topic}
36 | {unread ? {unread} : null}
37 |
38 |
{msg.body}
39 |
40 | {user && user.name ? {user.name} : null}
41 | {` at ${formatTime(props.updated_at)}`}
42 |
43 |
44 | );
45 | };
46 |
47 | // TODO render only topic in collapsed mode
48 |
49 | const threadPropNames = [
50 | 'avatarSize',
51 | 'iconSet',
52 | 'fetchUser',
53 | 'sendMessage',
54 | 'updateMessage',
55 | 'onSelect',
56 | 'onAction',
57 | 'canExecute',
58 | 'theme',
59 | ];
60 |
61 | export default function ThreadList(props) {
62 | const className = classNames(style.thread_list);
63 | // TODO use propTypes of Thread component
64 | const options = _.pick(props, ...threadPropNames);
65 | const items = _.map(props.threads, t => );
66 | return (
67 |
68 | {items}
69 |
70 | );
71 | }
72 |
73 | ThreadList.defaultProps = {
74 | theme: 'plain',
75 | };
76 |
--------------------------------------------------------------------------------
/components/thread/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .thread {
4 | padding: 4px;
5 | }
6 |
7 | .day {
8 | padding: 0px 0px 10px 25px;
9 | color: #bbb;
10 | }
11 |
12 | .day a {
13 | cursor: pointer;
14 | color: #bbb;
15 | }
16 |
17 | .thread_header {
18 | cursor: pointer;
19 | display: block;
20 | font-weight: bold;
21 | padding: 4px 0px;
22 | }
23 |
24 | .thread_header a {
25 | cursor: pointer;
26 | outline: none;
27 | }
28 |
29 | .topic_selected {
30 | color: #db6b9a;
31 | }
32 |
33 | .header {
34 | font-weight: bold;
35 | }
36 |
37 | .unread {
38 | color: #36a5d9;
39 | margin-left: 6px;
40 | border: 1px solid #9acde4;
41 | border-radius: 4px;
42 | padding: 1px 4px 0;
43 | display: inline-block;
44 | line-height: 15px;
45 | font-weight: 400;
46 | font-size: 11px;
47 | }
48 |
49 | .user_name {
50 | color: #bbb;
51 | font-weight: 400;
52 | font-size: 11px;
53 | }
54 |
55 | .time {
56 | color: #bbb;
57 | font-weight: 400;
58 | font-size: 11px;
59 | }
60 |
61 | .body {
62 | }
63 |
64 | .subject_input {
65 | width: 100%;
66 | resize: none;
67 | }
68 |
--------------------------------------------------------------------------------
/components/thread/thread.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React, { Component, PropTypes } from 'react';
3 | import classNames from 'classnames';
4 | import moment from 'moment';
5 | import observable from 'observable';
6 | import { Message, MessageInput, getTime, Counter } from '../message';
7 | import { renderActions } from '../message/action';
8 | import ContributorList from './contributors';
9 | import Avatar from '../avatar';
10 | import Day from './day';
11 | import style from './style.scss';
12 | import {
13 | getDay,
14 | getMsgDay,
15 | getDayMessages,
16 | countMessages,
17 | collectContributors,
18 | } from './util';
19 |
20 | // TODO allow to use custom MessageInput component
21 | export default class Thread extends Component {
22 | static propTypes = {
23 | className: PropTypes.string,
24 | avatarSize: Avatar.propTypes.size,
25 | fetchUser: PropTypes.func,
26 | theme: PropTypes.string,
27 | };
28 |
29 | static defaultProps = {
30 | className: '',
31 | topic: '',
32 | messages: [],
33 | theme: 'plain',
34 | avatarSize: 'normal',
35 | fetchUser: null,
36 | };
37 |
38 | constructor(props) {
39 | super(props);
40 | this.state = {
41 | collapsed: true,
42 | };
43 | }
44 |
45 | renderActions() {
46 | const props = this.props;
47 |
48 | const actions = {
49 | like: { count: props.likes || 0 },
50 | remove: { },
51 | star: { },
52 | };
53 |
54 | const actionProps = {
55 | onAction: props.onAction,
56 | canExecute: props.canExecute,
57 | iconSet: props.iconSet,
58 | };
59 |
60 | return renderActions(actions, 'thread', props, actionProps);
61 | }
62 |
63 | renderHeader() {
64 | const props = this.props;
65 | const subject = props.subject || props.topic;
66 | const className = classNames(style.thread_header);
67 | const count = countMessages(props.messages || []) || 0;
68 | const collapse = () => {
69 | this.setState({ collapsed: !this.state.collapsed });
70 | };
71 | return (
72 |
73 |
{subject}
74 |
75 |
76 | {this.renderActions()}
77 |
78 |
79 | );
80 | }
81 |
82 | render() {
83 | const className = classNames(style.thread, this.props.className);
84 | const messages = this.props.messages || [];
85 | const items = [];
86 |
87 | if (this.state.collapsed) {
88 | const users = observable([]);
89 | collectContributors(users, messages, this.props.fetchUser);
90 | items.push();
91 | } else {
92 | const collapseDay = (time) => {
93 | const k = `collapsedDay${+time}`;
94 | this.setState({ [k]: !this.state[k] });
95 | };
96 |
97 | const isCollapsedDay = (time) => {
98 | const k = `collapsedDay${+time}`;
99 | return !!this.state[k];
100 | };
101 |
102 | const makeDay = (time, msgcount) => {
103 | const dayProps = {
104 | time,
105 | count: msgcount,
106 | onClick: () => collapseDay(time),
107 | };
108 | return ;
109 | };
110 |
111 | // TODO make renderMessage as method
112 | const renderMessage = (msg) => {
113 | const msgProps = {
114 | data: msg,
115 | avatarSize: this.props.avatarSize,
116 | iconSet: this.props.iconSet,
117 | fetchUser: this.props.fetchUser,
118 | onAction: this.props.onAction,
119 | canExecute: this.props.canExecute,
120 | sendMessage: this.props.sendMessage,
121 | updateMessage: this.props.updateMessage,
122 | theme: this.props.theme,
123 | };
124 | return ;
125 | };
126 |
127 | let collapseMessages = false;
128 | for (let i = 0; i < messages.length; i += 1) {
129 | const msg = messages[i];
130 | const time = getTime(msg);
131 | const day = getDay(time);
132 | if (moment.isDate(time) && (i === 0 || day !== getMsgDay(messages[i - 1]))) {
133 | collapseMessages = isCollapsedDay(time);
134 | const dayMessages = getDayMessages(messages, i);
135 | const msgcount = countMessages(dayMessages);
136 | items.push(makeDay(time, msgcount));
137 | }
138 | if (!collapseMessages) {
139 | items.push(renderMessage(msg));
140 | }
141 | }
142 |
143 | const sendMessage = (body) => {
144 | if (_.isFunction(this.props.sendMessage)) {
145 | this.props.sendMessage({ thread_id: this.props.id, body });
146 | }
147 | };
148 |
149 | items.push();
150 | }
151 |
152 | return (
153 |
154 | {this.renderHeader()}
155 | {items}
156 |
157 | );
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/components/thread/threadform.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import classNames from 'classnames';
3 | import { Input } from '../common';
4 | import MessageInput from '../message/messageinput';
5 | import style from './style.scss';
6 |
7 | export default class ThreadForm extends Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = {
12 | subject: '',
13 | body: '',
14 | subjectFocused: false,
15 | bodyFocused: false,
16 | };
17 | }
18 |
19 | onSubjectFocus = () => {
20 | this.setState({ subjectFocused: true });
21 | };
22 |
23 | onSubjectBlur = () => {
24 | this.setState({ subjectFocused: false });
25 | };
26 |
27 | onBodyFocus = () => {
28 | this.setState({ bodyFocused: true });
29 | };
30 |
31 | onBodyBlur = () => {
32 | this.setState({ bodyFocused: false });
33 | };
34 |
35 | onSubjectChange = (event) => {
36 | const value = event.target.value || '';
37 | this.setState({ subject: value });
38 | };
39 |
40 | onBodyChange = (body) => {
41 | this.setState({ body });
42 | };
43 |
44 | render() {
45 | const props = this.props;
46 | const canSubmit = () => {
47 | const { subject, body } = this.state;
48 | return subject.length > 0 && body.length > 0;
49 | };
50 | const submit = () => {
51 | const subject = this.state.subject;
52 | const body = this.state.body;
53 | if (!subject || !body) {
54 | // TODO show validation errors
55 | return;
56 | }
57 | this.setState({ subject: '', body: '' });
58 | props.submit({ subject, body });
59 | };
60 | const subjectProps = {
61 | className: classNames(style.input, style.subject_input),
62 | rows: 1,
63 | placeholder: 'Subject',
64 | value: this.state.subject,
65 | onChange: this.onSubjectChange,
66 | onFocus: this.onSubjectFocus,
67 | onBlur: this.onSubjectBlur,
68 | focused: this.state.subjectFocused,
69 | };
70 | const bodyProps = {
71 | placeholder: 'Write your message here...',
72 | canSubmit,
73 | submit,
74 | value: this.state.body,
75 | onChange: this.onBodyChange,
76 | onFocus: this.onBodyFocus,
77 | onBlur: this.onBodyBlur,
78 | formStyle: {
79 | margin: '0px',
80 | padding: '0px',
81 | },
82 | focused: this.state.bodyFocused,
83 | };
84 | const formProps = {
85 | className: style.thread_form,
86 | style: { marginBottom: '24px' },
87 | };
88 | if (this.state.subjectFocused || this.state.bodyFocused) {
89 | return (
90 |
91 |
92 |
93 |
94 | );
95 | }
96 | const placeholderProps = {
97 | className: classNames(style.input, style.subject_input),
98 | rows: 1,
99 | placeholder: 'Start a new topic...',
100 | onFocus: this.onSubjectFocus,
101 | };
102 | return (
103 |
104 |
105 |
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/components/thread/util.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import _ from 'lodash';
3 | import { getTime } from '../message';
4 | import { toPromise, promiseOnce } from '../util';
5 |
6 | export function getDay(time) {
7 | const m = moment(time);
8 | return m.isValid ? m.year() + m.dayOfYear() : -1;
9 | }
10 |
11 | export const getMsgDay = msg => getDay(getTime(msg));
12 |
13 | export function getDayMessages(messages, start) {
14 | const msg = messages[start];
15 | const result = [msg];
16 | const day = getMsgDay(msg);
17 | for (let i = start + 1; i < messages.length; i += 1) {
18 | if (day !== getMsgDay(messages[i])) {
19 | i -= 1;
20 | break;
21 | }
22 | result.push(messages[i]);
23 | }
24 | return result;
25 | }
26 |
27 | export function countMessages(messages) {
28 | const countIt = (m) => {
29 | let n = 1;
30 | if (Array.isArray(m.replies)) {
31 | n += m.replies.reduce((a, b) => a + countIt(b), 0);
32 | }
33 | return n;
34 | };
35 | return messages.reduce((a, m) => a + countIt(m), 0);
36 | }
37 |
38 | export function collectContributors(users, messages, fetchUser) {
39 | function push(user) {
40 | const arr = users();
41 | if (_.find(arr, u => u.id === user.id)) return;
42 | users([...arr, user]);
43 | }
44 | messages.forEach((m) => {
45 | if (_.isObject(m.user)) {
46 | push(m.user);
47 | } else if (m.fetchUser || fetchUser) {
48 | const promise = toPromise(promiseOnce(m.fetchUser || fetchUser, m));
49 | if (promise) {
50 | promise.then(push);
51 | }
52 | }
53 | if (_.isArray(m.replies)) {
54 | collectContributors(users, m.replies, fetchUser);
55 | }
56 | });
57 | }
58 |
--------------------------------------------------------------------------------
/components/user/index.js:
--------------------------------------------------------------------------------
1 | export { default, default as UserList } from './list';
2 | export { default as UserMenu } from './menu';
3 |
--------------------------------------------------------------------------------
/components/user/list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Avatar from '../avatar';
3 | import style from './style.scss';
4 |
5 | export function UserList(props) {
6 | const users = props.users || [];
7 | const online = users.filter(u => !!u.online).length;
8 | const items = users.map((user) => {
9 | const avatarProps = {
10 | user,
11 | className: style.user_item,
12 | hover: 'grow',
13 | shape: 'round_rect',
14 | size: 32,
15 | style: {
16 | margin: 0,
17 | },
18 | };
19 | return ;
20 | });
21 | return (
22 |
23 |
24 |
25 | Online
26 | {online}
27 |
28 |
29 | {items}
30 |
31 |
32 | );
33 | }
34 |
35 | export default UserList;
36 |
--------------------------------------------------------------------------------
/components/user/menu.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Avatar from '../avatar';
3 | import { ContextMenu } from '../common';
4 | import style from './menuStyle.scss';
5 |
6 | // TODO use UserName component to render user name
7 |
8 | export default class UserMenu extends Component {
9 | renderContent(user) {
10 | const avatarProps = {
11 | className: style.user_avatar,
12 | user,
13 | size: 32,
14 | circled: true,
15 | style: {
16 | margin: 0,
17 | },
18 | };
19 | const menuProps = {
20 | button: {
21 | content: (
22 |
23 |
24 |
25 |
26 | ),
27 | },
28 | };
29 | return (
30 |
31 |
32 |
33 | {user.name || user.login}
34 |
35 | {this.props.children}
36 |
37 |
38 | );
39 | }
40 |
41 | render() {
42 | const props = this.props;
43 | const user = props.user;
44 | return (
45 |
46 | {user ? this.renderContent(user) : null}
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/components/user/menuStyle.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .user_menu {
4 | display: inline-block;
5 | }
6 |
7 | .user_menu .user_avatar {
8 | float: none;
9 | margin: 0;
10 | }
11 |
--------------------------------------------------------------------------------
/components/user/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .user_item {
4 | display: inline-block;
5 | }
6 |
7 | .user_list_body {
8 | max-width: 5 * 40px;
9 | }
10 |
11 | .user_list_header i {
12 | font-size: 24px;
13 | margin: 4px;
14 | }
15 |
16 | .user_list_header span {
17 | font-size: 20px;
18 | }
19 |
20 | .online_count {
21 | display: inline-block;
22 | margin: -.3em 0 0 .3em;
23 | width: 1.6em;
24 | height: 1.6em;
25 | border-radius: 50%;
26 | background-color: #4cbe00;
27 | text-align: center;
28 | vertical-align: middle;
29 | color: white;
30 | line-height: 1.7;
31 | font-style: normal;
32 | font-weight: bold;
33 | }
34 |
--------------------------------------------------------------------------------
/components/util.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export function isPromise(value) {
4 | return value && _.isFunction(value.then);
5 | }
6 |
7 | export function promiseOnce(fn, data) {
8 | if (!fn) return fn;
9 | let resolved;
10 | return function () {
11 | if (resolved) return resolved;
12 | if (isPromise(fn)) {
13 | return (resolved = fn);
14 | }
15 | const t = fn(data);
16 | resolved = isPromise(t) ? t : Promise.resolve(t);
17 | return resolved;
18 | };
19 | }
20 |
21 | export function toPromise(value) {
22 | if (isPromise(value)) {
23 | return value;
24 | }
25 | if (!_.isFunction(value)) {
26 | return null;
27 | }
28 | const p = value();
29 | return isPromise(p) ? p : null;
30 | }
31 |
32 | export function firstOrDefault(obj, ...keys) {
33 | if (!obj) return null;
34 | for (let i = 0; i < keys.length; i++) {
35 | const val = obj[keys[i]];
36 | if (val) return val;
37 | }
38 | return null;
39 | }
40 |
41 | export function getOrFetch(fetch, obj, ...keys) {
42 | const val = firstOrDefault(obj, ...keys);
43 | if (val) return val;
44 | const promise = toPromise(fetch);
45 | if (promise) {
46 | return promise.then(t => firstOrDefault(t, ...keys));
47 | }
48 | return null;
49 | }
50 |
--------------------------------------------------------------------------------
/demo/actions.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import store from './store';
3 | import { actions, nextDate } from './state';
4 | import { randomUser, randomJoke } from './api';
5 |
6 | function randomIndex(arr) {
7 | return Math.floor(Math.random() * arr.length);
8 | }
9 |
10 | function rnd(min, max) {
11 | return Math.floor(Math.random() * ((max - min) + 1)) + min;
12 | }
13 |
14 | // TODO support multiple sources
15 | let nextId = 1;
16 |
17 | function makeMessage() {
18 | return randomJoke().then((response) => {
19 | const data = response.value;
20 | const { users } = store.getState();
21 | const i = randomIndex(users);
22 | const user = nextId === 1 ? users[0] : users[i];
23 | const msg = {
24 | id: nextId++,
25 | body: data.joke,
26 | user_id: user.id,
27 | time: nextDate(nextId - 1),
28 | likes: rnd(0, 10),
29 | };
30 | if ((nextId & 1) === 0) { // eslint-disable-line
31 | msg.user = user;
32 | }
33 | return msg;
34 | });
35 | }
36 |
37 | export function fetchMessageUser(msg) {
38 | const { users } = store.getState();
39 | const user = _.find(users, u => u.id === msg.user_id);
40 | return new Promise(resolve => setTimeout(() => resolve(user), 100));
41 | }
42 |
43 | const maxMessages = 10;
44 |
45 | function pushMessage(msg) {
46 | store.dispatch(actions.addMessage(msg));
47 | if (nextId < maxMessages) {
48 | setTimeout(fetchQuote, 1000);
49 | }
50 | }
51 |
52 | function fetchQuote() {
53 | makeMessage().then((msg) => {
54 | const n = rnd(0, 3);
55 | if (n > 0) {
56 | // TODO async loading of replies
57 | return Promise.all(_.range(n).map(makeMessage)).then((replies) => {
58 | pushMessage({ ...msg, replies });
59 | });
60 | }
61 | pushMessage(msg);
62 | return msg;
63 | });
64 | }
65 |
66 | // load random users from uifaces.com
67 | const maxUsers = 10;
68 |
69 | function fetchUser() {
70 | randomUser().then((response) => {
71 | const data = response.results[0];
72 | const name = `${data.name.first} ${data.name.last}`;
73 | const { users } = store.getState();
74 | const user = {
75 | id: users.length + 1,
76 | name,
77 | avatar_url: `https://robohash.org/${name}`,
78 | // avatar_url: user.picture.large,
79 | };
80 | store.dispatch(actions.addUser(user));
81 | if (users.length + 1 >= maxUsers) {
82 | fetchQuote();
83 | }
84 | });
85 | }
86 |
87 | for (let i = 0; i < maxUsers; i++) {
88 | fetchUser();
89 | }
90 |
91 | export function canExecute(type, action, msg) {
92 | const { currentUser } = store.getState();
93 | if (!currentUser) return false;
94 | switch (action) {
95 | case 'delete':
96 | case 'remove':
97 | case 'edit':
98 | return msg.user_id === currentUser.id;
99 | default:
100 | return true;
101 | }
102 | }
103 |
104 | export function onAction(type, action, data) {
105 | if (type === 'message') {
106 | switch (action) {
107 | case 'delete':
108 | case 'remove':
109 | store.dispatch(actions.removeMessage(data.id));
110 | return;
111 | default:
112 | break;
113 | }
114 | }
115 | swal(`action ${action} on ${type} ${data.id}`);
116 | }
117 |
118 | export function selectThread(thread) {
119 | swal(`selected ${thread.topic}`);
120 | }
121 |
122 | export function sendMessage(m) {
123 | const state = store.getState();
124 | const msg = {
125 | ...m,
126 | id: nextId++,
127 | user: state.currentUser,
128 | user_id: state.currentUser.id,
129 | };
130 | store.dispatch(actions.addMessage(msg));
131 | }
132 |
133 | export function updateMessage(msg) {
134 | store.dispatch(actions.updateMessage(msg));
135 | }
136 |
137 | export function createThread({ subject, body }) {
138 | const { threads, currentUser } = store.getState();
139 | store.dispatch(actions.addThread({
140 | id: threads.length + 1,
141 | subject,
142 | messages: [
143 | {
144 | id: 1,
145 | body,
146 | user_id: currentUser.id,
147 | user: currentUser,
148 | replies: [],
149 | },
150 | ],
151 | }));
152 | }
153 |
--------------------------------------------------------------------------------
/demo/api.js:
--------------------------------------------------------------------------------
1 | export function randomUser() {
2 | return fetch('https://randomuser.me/api/').then(t => t.json());
3 | }
4 |
5 | export function randomFace() {
6 | return fetch('http://uifaces.com/api/v1/random').then(t => t.json());
7 | }
8 |
9 | export function randomJoke() {
10 | return fetch('http://api.icndb.com/jokes/random').then(t => t.json());
11 | }
12 |
--------------------------------------------------------------------------------
/demo/app.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React, { Component } from 'react';
3 | import {
4 | Row,
5 | Col,
6 | Panel,
7 | ButtonToolbar,
8 | DropdownButton,
9 | MenuItem,
10 | } from 'react-bootstrap';
11 | import { connect, Provider } from 'react-redux';
12 | import {
13 | ThreadList,
14 | ThreadForm,
15 | ChannelList,
16 | UserList,
17 | UserMenu,
18 | Spinner,
19 | ContextMenuItem,
20 | } from '../components';
21 | import { loaderTypes } from '../components/common/spinner';
22 | import DevTools from './devtools';
23 | import store from './store';
24 | import { actions } from './state';
25 | import {
26 | selectThread,
27 | fetchMessageUser,
28 | onAction,
29 | canExecute,
30 | sendMessage,
31 | updateMessage,
32 | createThread,
33 | } from './actions';
34 | import style from './style.scss';
35 |
36 | class App extends Component {
37 | state = {
38 | theme: 'github',
39 | };
40 |
41 | render() {
42 | const props = this.props;
43 | const { dispatch, currentUser } = props;
44 | const channelListProps = {
45 | channels: props.channels,
46 | selectedChannel: props.selectedChannel,
47 | selectChannel: cn => dispatch(actions.selectChannel(cn)),
48 | createChannel: cn => dispatch(actions.addChannel(cn)),
49 | removeChannel: cn => dispatch(actions.removeChannel(cn.id)),
50 | };
51 | const threadListProps = {
52 | threads: props.threads,
53 | onSelect: selectThread,
54 | avatarSize: 40,
55 | fetchUser: fetchMessageUser,
56 | onAction,
57 | canExecute,
58 | sendMessage,
59 | updateMessage,
60 | theme: this.state.theme,
61 | };
62 | const spinners = loaderTypes.map(t => );
63 |
64 | const makeThemeItem = (theme, label) => {
65 | const itemProps = {
66 | active: this.state.theme === theme,
67 | onSelect: () => this.setState({ theme }),
68 | };
69 | return ;
70 | };
71 |
72 | const logout = () => {
73 | console.log('logout');
74 | };
75 |
76 | return (
77 |
78 |
79 |
80 |
81 |
82 | You
83 | Preferences
84 | Logout
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {makeThemeItem('plain', 'Plain')}
95 | {makeThemeItem('github', 'GitHub')}
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | {spinners}
106 |
107 |
108 |
109 | );
110 | }
111 | }
112 |
113 | const AppConnected = connect(_.identity)(App);
114 |
115 | export default function Root(props) {
116 | return (
117 |
118 |
122 |
123 | );
124 | }
125 |
--------------------------------------------------------------------------------
/demo/devtools.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { createDevTools } from 'redux-devtools';
4 |
5 | import LogMonitor from 'redux-devtools-log-monitor';
6 | import DockMonitor from 'redux-devtools-dock-monitor';
7 |
8 | const devToolsProps = {
9 | defaultPosition: 'left',
10 | defaultSize: 0.2,
11 | toggleVisibilityKey: 'ctrl-h',
12 | changePositionKey: 'ctrl-q',
13 | };
14 |
15 | const monitor = (
16 |
17 |
18 |
19 | );
20 |
21 | export default createDevTools(monitor);
22 |
--------------------------------------------------------------------------------
/demo/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './app';
4 |
5 | render(, document.getElementById('root'));
6 |
--------------------------------------------------------------------------------
/demo/state.js:
--------------------------------------------------------------------------------
1 | import makeReducer from 'make-reducer';
2 | import moment from 'moment';
3 | import _ from 'lodash';
4 | import { updateArray } from 'update-it';
5 |
6 | const timeline = [
7 | // previous year
8 | moment()
9 | .subtract(1, 'years')
10 | .subtract(45, 'minutes')
11 | .toDate(),
12 | moment()
13 | .subtract(1, 'years')
14 | .subtract(53, 'minutes')
15 | .toDate(),
16 | // previous week
17 | moment()
18 | .subtract(7, 'days')
19 | .subtract(45, 'minutes')
20 | .toDate(),
21 | moment()
22 | .subtract(7, 'days')
23 | .subtract(53, 'minutes')
24 | .toDate(),
25 | // yesterday
26 | moment()
27 | .subtract(2, 'days')
28 | .subtract(45, 'minutes')
29 | .toDate(),
30 | moment()
31 | .subtract(2, 'days')
32 | .subtract(53, 'minutes')
33 | .toDate(),
34 | // today
35 | moment()
36 | .subtract(1, 'days')
37 | .subtract(32, 'minutes')
38 | .toDate(),
39 | moment()
40 | .subtract(1, 'days')
41 | .subtract(42, 'minutes')
42 | .toDate(),
43 | ];
44 |
45 | export function nextDate(i) {
46 | if (i < timeline.length) {
47 | return timeline[i];
48 | }
49 | return new Date();
50 | }
51 |
52 | export const users = [
53 | {
54 | id: 1,
55 | name: 'sergeyt',
56 | avatar_url: 'stodyshev@gmail.com',
57 | online: true,
58 | },
59 | {
60 | id: 2,
61 | name: 'sergeyt',
62 | avatar_url: 'https://robohash.org/sergeyt',
63 | },
64 | {
65 | id: 3,
66 | name: 'noavatar',
67 | avatar_url: 'noavatar.png',
68 | },
69 | ];
70 |
71 | const channels = [
72 | {
73 | id: 1,
74 | name: 'Issues',
75 | },
76 | {
77 | id: 2,
78 | name: 'Testing',
79 | },
80 | {
81 | id: 3,
82 | name: 'Chats',
83 | },
84 | ];
85 |
86 | const initialState = {
87 | currentUser: users[0],
88 | users,
89 | channels,
90 | selectedChannel: channels[0],
91 | threads: [
92 | {
93 | id: 1,
94 | user: users[0],
95 | user_id: users[0].id,
96 | subject: 'Chuck Norris Database',
97 | last_message: {
98 | user: users[0],
99 | body: 'test message',
100 | updated_at: nextDate(0),
101 | },
102 | unread: 4,
103 | selected: true,
104 | messages: [],
105 | },
106 | {
107 | id: 2,
108 | user: users[1],
109 | user_id: users[1].id,
110 | subject: 'Offtopic',
111 | last_message: {
112 | user: users[0],
113 | body: 'test message',
114 | updated_at: nextDate(0),
115 | },
116 | unread: 11,
117 | messages: [],
118 | },
119 | ],
120 | };
121 |
122 | function rnd(min, max) {
123 | return Math.floor(Math.random() * ((max - min) + 1)) + min;
124 | }
125 |
126 | function removeById(list, id) {
127 | return list.filter(t => t.id !== id);
128 | }
129 |
130 | function replaceById(list, obj) {
131 | const i = _.findIndex(list, t => t.id === obj.id);
132 | if (i >= 0) {
133 | const result = [...list];
134 | result[i] = { ...result[i], ...obj };
135 | return result;
136 | }
137 | return list;
138 | }
139 |
140 | function setThread(state, msg) {
141 | if (msg.thread_id) return msg;
142 | const i = rnd(0, state.threads.length - 1);
143 | return { ...msg, thread_id: state.threads[i].id };
144 | }
145 |
146 | export const actions = {
147 | addUser(state, user) {
148 | const list = [...state.users, user];
149 | return { ...state, users: list };
150 | },
151 |
152 | selectChannel(state, selectedChannel) {
153 | return { ...state, selectedChannel };
154 | },
155 |
156 | addChannel(state, cn) {
157 | const t = { ...cn, id: state.channels.length + 1 };
158 | return { ...state, channels: [...state.channels, t] };
159 | },
160 |
161 | removeChannel(state, id) {
162 | return { ...state, channels: removeById(state.channels, id) };
163 | },
164 |
165 | addThread(state, thread) {
166 | const threads = [...state.threads, thread];
167 | return { ...state, threads };
168 | },
169 |
170 |
171 | addMessage(state, message) {
172 | const msg = setThread(state, message);
173 | const threads = state.threads.map((t) => {
174 | if (t.id !== msg.thread_id) return t;
175 | if (msg.in_reply_to) {
176 | const messages = updateArray(t.messages, (m) => {
177 | const replies = [...(m.replies || []), msg];
178 | return { ...m, replies };
179 | }, m => m.body && m.id === msg.in_reply_to);
180 | return { ...t, messages };
181 | }
182 | return { ...t, messages: [...t.messages, msg] };
183 | });
184 | return { ...state, threads };
185 | },
186 |
187 | removeMessage(state, id) {
188 | const threads = state.threads.map((t) => {
189 | const messages = removeById(t.messages, id);
190 | return { ...t, messages };
191 | });
192 | return { ...state, threads };
193 | },
194 |
195 | updateMessage(state, msg) {
196 | const threads = state.threads.map((t) => {
197 | if (t.id !== msg.thread_id) return t;
198 | return { ...t, messages: replaceById(t.messages, msg) };
199 | });
200 | return { ...state, threads };
201 | },
202 | };
203 |
204 | export const reducer = makeReducer(initialState, actions);
205 |
--------------------------------------------------------------------------------
/demo/store.js:
--------------------------------------------------------------------------------
1 | import { compose, createStore } from 'redux';
2 | import { persistState } from 'redux-devtools';
3 | import DevTools from './devtools';
4 | import { reducer } from './state';
5 |
6 | const makeStore = compose(
7 | DevTools.instrument(),
8 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)),
9 | )(createStore);
10 |
11 | const store = makeStore(reducer);
12 |
13 | export default store;
14 |
--------------------------------------------------------------------------------
/demo/style.scss:
--------------------------------------------------------------------------------
1 | .loaders {
2 | width: 100%;
3 | box-sizing: border-box;
4 | display: none;
5 | flex: 0 1 auto;
6 | flex-direction: row;
7 | flex-wrap: wrap;
8 | }
9 |
10 | .loaders > div {
11 | width: 80px;
12 | height: 80px;
13 | box-sizing: border-box;
14 | display: flex;
15 | flex: 0 1 auto;
16 | flex-direction: column;
17 | flex-grow: 1;
18 | flex-shrink: 0;
19 | flex-basis: 25%;
20 | align-items: center;
21 | justify-content: center;
22 | border: 1px solid gray;
23 | }
24 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e # exit with nonzero exit code if anything fails
3 |
4 | # clear and re-create the out directory
5 | rm -rf site || exit 0;
6 | mkdir site;
7 |
8 | # build site
9 | bash makesite.sh
10 |
11 | # go to the site directory and create a *new* git repo
12 | cd site
13 |
14 | # fix absolute links
15 | OLD='/static/'
16 | NEW='./static/'
17 | sed -i 's/$OLD/$NEW/g' index.html
18 |
19 | git init
20 |
21 | # inside this git repo we'll pretend to be a new user
22 | git config user.name "Travis CI"
23 | git config user.email "stodyshev@gmail.com"
24 |
25 | # The first and only commit to this new git repo contains all the
26 | # files present with the commit message "Deploy to GitHub Pages".
27 | git add .
28 | git commit -m "Deploy to GitHub Pages"
29 |
30 | # Force push from the current repo's master branch to the remote
31 | # repo's gh-pages branch. (All previous history on the gh-pages branch
32 | # will be lost, since we are overwriting it.) We redirect any output to
33 | # /dev/null to hide any sensitive credential data that might otherwise be exposed.
34 | git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages > /dev/null 2>&1
35 |
--------------------------------------------------------------------------------
/devserver.js:
--------------------------------------------------------------------------------
1 | const dev = require('react-devpack');
2 |
3 | dev.startServer({
4 | port: 9000,
5 | proxy: 'http://localhost:3000/api',
6 | });
7 |
--------------------------------------------------------------------------------
/fiber.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Demo page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib/_base.scss:
--------------------------------------------------------------------------------
1 | @import "./_mixins";
2 |
3 | $lightgreen: #42aa42;
4 | $lightblue: #1fadc5;
5 |
6 | $input-focus-color: $lightblue;
7 |
8 | .input {
9 | border: 1px solid rgba(0,0,0,0.1);
10 | border-radius: .2em;
11 | outline: none;
12 | }
13 |
14 | .input:focus {
15 | box-shadow: 0 0 .2em $input-focus-color inset;
16 | border-color: $input-focus-color;
17 | }
18 |
--------------------------------------------------------------------------------
/lib/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin border-radius ($radius: 4px) {
2 | -webkit-border-radius: $radius;
3 | -moz-border-radius: $radius;
4 | -ms-border-radius: $radius;
5 | -o-border-radius: $radius;
6 | border-radius: $radius;
7 | }
8 |
9 | @mixin transition ($args...) {
10 | -webkit-transition: $args;
11 | -moz-transition: $args;
12 | -o-transition: $args;
13 | -ms-transition: $args;
14 | transition: $args;
15 | }
16 |
17 | @mixin transform-transition ($args...) {
18 | -webkit-transition: -webkit-transform $args;
19 | -moz-transition: -moz-transform $args;
20 | -o-transition: -o-transform $args;
21 | -ms-transition: -ms-transform $args;
22 | transition: transform $args;
23 | }
24 |
25 | @mixin transform ($args...) {
26 | -webkit-transform: $args;
27 | -moz-transform: $args;
28 | -o-transform: $args;
29 | -ms-transform: $args;
30 | transform: $args;
31 | }
32 |
33 | @mixin arrow-left ($position: 50%, $width: 7px, $length: 10px, $background-color: #fcfcfc, $border-color: #ddd) {
34 | &:before {
35 | content: '';
36 | position: absolute;
37 | border-style: solid;
38 | border-width: $width $length $width 0;
39 | border-color: transparent $border-color;
40 | display: block;
41 | width: 0;
42 | z-index: 0;
43 | margin-top: -$width;
44 | left: (-$length - 1);
45 | top: $position;
46 | }
47 | &:after {
48 | content: '';
49 | position: absolute;
50 | border-style: solid;
51 | border-width: $width $length $width 0;
52 | border-color: transparent $background-color;
53 | display: block;
54 | width: 0;
55 | z-index: 1;
56 | margin-top: -$width;
57 | left: -$length;
58 | top: $position;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/_shapes.scss:
--------------------------------------------------------------------------------
1 | @import "./_mixins";
2 |
3 | @mixin heart($size:24px, $color:red) {
4 | position: relative;
5 | width: $size;
6 | height: $size*0.9;
7 | &:before,
8 | &:after {
9 | position: absolute;
10 | content: "";
11 | left: $size/2;
12 | top: 0;
13 | width: $size/2;
14 | height: $size*0.8;
15 | background: $color;
16 | border-radius: $size/2 $size/2 0 0;
17 | transform: rotate(-45deg);
18 | transform-origin: 0 100%;
19 | }
20 | &:after {
21 | left: 0;
22 | transform: rotate(45deg);
23 | transform-origin :100% 100%;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/avatar/gravatar.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8 |
9 | exports.default = gravatarURL;
10 |
11 | var _md = require('md5');
12 |
13 | var _md2 = _interopRequireDefault(_md);
14 |
15 | var _queryString = require('query-string');
16 |
17 | var _queryString2 = _interopRequireDefault(_queryString);
18 |
19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20 |
21 | var defaultOptions = {
22 | d: 'retro'
23 | };
24 |
25 | function isHash(s) {
26 | return (/^[a-f0-9]{32}$/i.test((s || '').trim().toLowerCase())
27 | );
28 | }
29 |
30 | // TODO cash gravatar URLs
31 | function gravatarURL(email) {
32 | var size = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 32;
33 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : defaultOptions;
34 |
35 | var url = 'https://secure.gravatar.com/avatar/';
36 |
37 | if (isHash(email)) {
38 | url += email;
39 | } else {
40 | url += (0, _md2.default)(email.toLowerCase());
41 | }
42 |
43 | var qs = _queryString2.default.stringify(_extends({ s: size }, options));
44 | if (qs) {
45 | url += '?' + qs;
46 | }
47 |
48 | return url;
49 | }
--------------------------------------------------------------------------------
/lib/avatar/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _avatar = require('./avatar');
8 |
9 | Object.defineProperty(exports, 'default', {
10 | enumerable: true,
11 | get: function get() {
12 | return _interopRequireDefault(_avatar).default;
13 | }
14 | });
15 | Object.defineProperty(exports, 'avatarSize', {
16 | enumerable: true,
17 | get: function get() {
18 | return _avatar.avatarSize;
19 | }
20 | });
21 |
22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/lib/avatar/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 | @import "../_mixins";
3 |
4 | .preloader {
5 | justify-content: center;
6 | }
7 |
8 | .avatar {
9 | cursor: pointer;
10 | position: relative;
11 | margin: 4px 4px 4px -45px;
12 | overflow: visible;
13 | }
14 |
15 | .avatar.circle img {
16 | border-radius: 50%;
17 | }
18 |
19 | .avatar.round_rect img {
20 | border: 1px solid transparent;
21 | border-radius: 4px;
22 | }
23 |
24 | .avatar.round_rect img:hover {
25 | border: 1px solid transparentize(#1fadc5, 0.5);
26 | }
27 |
28 | .avatar.circled {
29 | border: 2px solid lighten(orange, 25%);
30 | border-radius: 50%;
31 | padding: 2px;
32 | }
33 |
34 | .online:after {
35 | content: "\f400";
36 | color: #4cbe00;
37 | position: absolute;
38 | top: -.4em;
39 | right: -.4em;
40 | text-shadow: 0 0 .1em #fff;
41 | font-size: .8em;
42 | font-family: "Ionicons";
43 | z-index: 99;
44 | }
45 |
46 | @mixin shadow ($size: 2px) {
47 | border: $size solid #fff;
48 | box-shadow: 0 1px 1px rgba(0,0,0,0.3);
49 | }
50 |
51 | .shadow {
52 | @include shadow()
53 | }
54 | .shadow_1 {
55 | @include shadow(1px)
56 | }
57 | .shadow_2 {
58 | @include shadow(2px)
59 | }
60 | .shadow_3 {
61 | @include shadow(3px)
62 | }
63 |
--------------------------------------------------------------------------------
/lib/channel/addchannel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | exports.default = newChannelDialog;
10 |
11 | var _react = require('react');
12 |
13 | var _react2 = _interopRequireDefault(_react);
14 |
15 | var _reactDom = require('react-dom');
16 |
17 | var _reactDom2 = _interopRequireDefault(_reactDom);
18 |
19 | var _reflexbox = require('reflexbox');
20 |
21 | var _reactBootstrap = require('react-bootstrap');
22 |
23 | var _reactbitsInput = require('reactbits-input');
24 |
25 | var _style = require('./style.scss');
26 |
27 | var _style2 = _interopRequireDefault(_style);
28 |
29 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
30 |
31 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
32 |
33 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
34 |
35 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
36 |
37 | var Dialog = function (_Component) {
38 | _inherits(Dialog, _Component);
39 |
40 | function Dialog(props) {
41 | _classCallCheck(this, Dialog);
42 |
43 | var _this = _possibleConstructorReturn(this, (Dialog.__proto__ || Object.getPrototypeOf(Dialog)).call(this, props));
44 |
45 | _this.state = {
46 | show: true
47 | };
48 | return _this;
49 | }
50 |
51 | _createClass(Dialog, [{
52 | key: 'render',
53 | value: function render() {
54 | var _this2 = this;
55 |
56 | var close = function close() {
57 | return _this2.setState({ show: false });
58 | };
59 | var inputs = {
60 | name: {
61 | name: 'name',
62 | placeholder: 'Channel name',
63 | required: true
64 | },
65 | description: {
66 | name: 'description',
67 | placeholder: 'Description'
68 | }
69 | };
70 | return _react2.default.createElement(
71 | _reactBootstrap.Modal,
72 | { dialogClassName: _style2.default.new_channel_dialog, show: this.state.show, onHide: close },
73 | _react2.default.createElement(
74 | _reactbitsInput.Form,
75 | { onSubmit: this.props.submit },
76 | _react2.default.createElement(
77 | _reactBootstrap.Modal.Header,
78 | { closeButton: true },
79 | _react2.default.createElement(
80 | _reactBootstrap.Modal.Title,
81 | null,
82 | 'Create new channel'
83 | )
84 | ),
85 | _react2.default.createElement(
86 | _reactBootstrap.Modal.Body,
87 | null,
88 | _react2.default.createElement(
89 | _reflexbox.Flex,
90 | null,
91 | _react2.default.createElement(
92 | _reflexbox.Box,
93 | { col: 12 },
94 | _react2.default.createElement(_reactbitsInput.Input, inputs.name),
95 | _react2.default.createElement(_reactbitsInput.Input, inputs.description)
96 | )
97 | )
98 | ),
99 | _react2.default.createElement(
100 | _reactBootstrap.Modal.Footer,
101 | null,
102 | _react2.default.createElement(
103 | _reactBootstrap.Button,
104 | { type: 'submit', bsStyle: 'primary' },
105 | 'Create'
106 | ),
107 | _react2.default.createElement(
108 | _reactBootstrap.Button,
109 | { onClick: close },
110 | 'Cancel'
111 | )
112 | )
113 | )
114 | );
115 | }
116 | }]);
117 |
118 | return Dialog;
119 | }(_react.Component);
120 |
121 | function newChannelDialog(callback) {
122 | var wrapper = null;
123 | var submit = function submit(data) {
124 | setTimeout(function () {
125 | _reactDom2.default.unmountComponentAtNode(wrapper);
126 | wrapper.remove();
127 | callback(data);
128 | }, 100);
129 | };
130 | wrapper = document.body.appendChild(document.createElement('div'));
131 | _reactDom2.default.render(_react2.default.createElement(Dialog, { submit: submit }), wrapper);
132 | }
--------------------------------------------------------------------------------
/lib/channel/channel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = Channel;
7 |
8 | var _lodash = require('lodash');
9 |
10 | var _lodash2 = _interopRequireDefault(_lodash);
11 |
12 | var _react = require('react');
13 |
14 | var _react2 = _interopRequireDefault(_react);
15 |
16 | var _classnames = require('classnames');
17 |
18 | var _classnames2 = _interopRequireDefault(_classnames);
19 |
20 | var _style = require('./style.scss');
21 |
22 | var _style2 = _interopRequireDefault(_style);
23 |
24 | var _common = require('../common');
25 |
26 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
27 |
28 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
29 |
30 | function removeButton(onClick) {
31 | return _react2.default.createElement(
32 | 'span',
33 | { className: _style2.default.btn_remove_channel, onClick: onClick },
34 | '\xD7'
35 | );
36 | }
37 |
38 | // TODO render channel actions
39 | function Channel(props) {
40 | var canRemove = _lodash2.default.isFunction(props.remove);
41 | var className = (0, _classnames2.default)(_style2.default.channel, _defineProperty({}, _style2.default.selected_channel, props.selected));
42 |
43 | var select = function select() {
44 | if (_lodash2.default.isFunction(props.select)) {
45 | props.select(props.data);
46 | }
47 | };
48 |
49 | var itemProps = {
50 | className: className,
51 | onClick: select,
52 | selected: props.selected,
53 | to: props.to
54 | };
55 |
56 | return _react2.default.createElement(
57 | _common.NavItem,
58 | itemProps,
59 | _react2.default.createElement(
60 | 'span',
61 | null,
62 | props.data.name
63 | ),
64 | _react2.default.createElement(
65 | 'span',
66 | { className: 'actions' },
67 | canRemove ? removeButton(props.remove) : null
68 | )
69 | );
70 | }
--------------------------------------------------------------------------------
/lib/channel/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _channel = require('./channel');
8 |
9 | Object.defineProperty(exports, 'Channel', {
10 | enumerable: true,
11 | get: function get() {
12 | return _interopRequireDefault(_channel).default;
13 | }
14 | });
15 |
16 | var _list = require('./list');
17 |
18 | Object.defineProperty(exports, 'ChannelList', {
19 | enumerable: true,
20 | get: function get() {
21 | return _interopRequireDefault(_list).default;
22 | }
23 | });
24 |
25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/lib/channel/list.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = ChannelList;
7 |
8 | var _lodash = require('lodash');
9 |
10 | var _lodash2 = _interopRequireDefault(_lodash);
11 |
12 | var _react = require('react');
13 |
14 | var _react2 = _interopRequireDefault(_react);
15 |
16 | var _classnames = require('classnames');
17 |
18 | var _classnames2 = _interopRequireDefault(_classnames);
19 |
20 | var _channel = require('./channel');
21 |
22 | var _channel2 = _interopRequireDefault(_channel);
23 |
24 | var _addchannel = require('./addchannel');
25 |
26 | var _addchannel2 = _interopRequireDefault(_addchannel);
27 |
28 | var _style = require('./style.scss');
29 |
30 | var _style2 = _interopRequireDefault(_style);
31 |
32 | var _common = require('../common');
33 |
34 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
35 |
36 | function ChannelList(props) {
37 | var className = (0, _classnames2.default)(props.className, _style2.default.channel_list);
38 | var selectedId = (props.selectedChannel || {}).id;
39 | var channels = _lodash2.default.map(props.channels, function (cn, i) {
40 | var cnprops = {
41 | key: cn.id || i,
42 | data: cn,
43 | selected: cn.id === selectedId,
44 | select: props.selectChannel,
45 | remove: _lodash2.default.isFunction(props.removeChannel) ? props.removeChannel.bind(null, cn) : undefined,
46 | to: cn.id && props.basePath ? props.basePath + '/' + cn.id : undefined
47 | };
48 | return _react2.default.createElement(_channel2.default, cnprops);
49 | });
50 | var onPlusClick = function onPlusClick() {
51 | (0, _addchannel2.default)(props.createChannel);
52 | };
53 | return _react2.default.createElement(
54 | _common.NavSection,
55 | { className: className },
56 | _react2.default.createElement(
57 | _common.NavHeader,
58 | { title: 'CHANNELS' },
59 | _react2.default.createElement(
60 | _common.NavHeaderButtons,
61 | null,
62 | _react2.default.createElement(_common.PlusButton, { tip: 'Create new channel', onClick: onPlusClick })
63 | )
64 | ),
65 | _react2.default.createElement(
66 | _common.NavBody,
67 | null,
68 | channels
69 | )
70 | );
71 | }
--------------------------------------------------------------------------------
/lib/channel/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .btn_add_channel {
4 | font-size: 20px;
5 | }
6 |
7 | .btn_remove_channel {
8 | margin-left: 4px;
9 | font-size: 20px;
10 | font-weight: bold;
11 | cursor: pointer;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/common/contextMenuStyle.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .context_menu .Popover-body {
4 | display: inline-flex;
5 | flex-direction: column;
6 | background: white;
7 | border-radius: 0.3rem;
8 | box-shadow: 0 0 10px rgba(0,0,0,0.25);
9 | }
10 |
11 | .context_menu .Popover-tipShape {
12 | fill: white;
13 | }
14 |
15 | .context_menu ul {
16 | list-style-type: none;
17 | margin: .75rem 0;
18 | padding: 0;
19 | }
20 |
21 | .context_menu ul li {
22 | line-height: 1.5rem;
23 | margin: 0;
24 | }
25 |
26 | .context_menu ul li a {
27 | cursor: pointer;
28 | text-decoration: none;
29 | padding: 0 1rem 0 .75rem;
30 | margin: 0 .75rem;
31 | border-radius: .25rem;
32 | overflow: hidden;
33 | text-overflow: ellipsis;
34 | white-space: nowrap;
35 | outline: 0;
36 | font-size: 15px;
37 | line-height: 25px;
38 | background: 0 0;
39 | color: #2c2d30;
40 | }
41 |
42 | .context_menu ul li a:hover {
43 | background-color: rgb(0, 156, 255);
44 | color: #fff;
45 | }
46 |
47 | .context_menu .header_item {
48 | cursor: default;
49 | margin: .625rem .75rem;
50 | position: relative;
51 | }
52 |
53 | .context_menu .header_item hr {
54 | border: 0;
55 | border-top: 1px solid #ddd;
56 | border-bottom: 1px solid #fff;
57 | position: absolute;
58 | left: 1rem;
59 | right: 0;
60 | margin: 1.25rem 0;
61 | margin-top: .6rem;
62 | margin-bottom: 0;
63 | }
64 |
65 | .context_menu .header_item .header_label {
66 | position: relative;
67 | background-color: #fff;
68 | color: #9e9ea6;
69 | padding-right: .8rem;
70 | }
71 |
72 | .show_button {
73 | cursor: pointer;
74 | text-decoration: none;
75 | }
76 |
77 | .show_button:hover {
78 | text-decoration: none;
79 | }
80 |
--------------------------------------------------------------------------------
/lib/common/contextmenu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _classnames = require('classnames');
14 |
15 | var _classnames2 = _interopRequireDefault(_classnames);
16 |
17 | var _reactPopover = require('react-popover');
18 |
19 | var _reactPopover2 = _interopRequireDefault(_reactPopover);
20 |
21 | var _contextMenuStyle = require('./contextMenuStyle.scss');
22 |
23 | var _contextMenuStyle2 = _interopRequireDefault(_contextMenuStyle);
24 |
25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
26 |
27 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
28 |
29 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
30 |
31 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
32 |
33 | var popover = _react2.default.createFactory(_reactPopover2.default);
34 |
35 | var ContextMenu = function (_Component) {
36 | _inherits(ContextMenu, _Component);
37 |
38 | function ContextMenu(props) {
39 | _classCallCheck(this, ContextMenu);
40 |
41 | var _this = _possibleConstructorReturn(this, (ContextMenu.__proto__ || Object.getPrototypeOf(ContextMenu)).call(this, props));
42 |
43 | _this.state = {
44 | dropdownVisible: false
45 | };
46 | return _this;
47 | }
48 |
49 | _createClass(ContextMenu, [{
50 | key: 'render',
51 | value: function render() {
52 | var _this2 = this;
53 |
54 | var button = this.props.button;
55 |
56 |
57 | var hide = function hide() {
58 | return _this2.setState({ dropdownVisible: false });
59 | };
60 | var showDropdown = function showDropdown(e) {
61 | e.preventDefault();
62 | e.stopPropagation();
63 | _this2.setState({ dropdownVisible: true });
64 | return false;
65 | };
66 |
67 | var buttonProps = {
68 | className: (0, _classnames2.default)(_contextMenuStyle2.default.show_button, button.className),
69 | onMouseDown: showDropdown
70 | };
71 | var buttonElement = _react2.default.createElement(
72 | 'a',
73 | buttonProps,
74 | button.content
75 | );
76 |
77 | var dropdownProps = {
78 | className: _contextMenuStyle2.default.context_menu,
79 | isOpen: this.state.dropdownVisible,
80 | preferPlace: 'below',
81 | place: 'below',
82 | onOuterAction: hide,
83 | body: _react2.default.createElement(
84 | 'ul',
85 | { className: _contextMenuStyle2.default.menu_items, onClick: hide },
86 | this.props.children
87 | ),
88 | refreshIntervalMs: false
89 | };
90 |
91 | return _react2.default.createElement(
92 | 'span',
93 | null,
94 | popover(dropdownProps, buttonElement)
95 | );
96 | }
97 | }]);
98 |
99 | return ContextMenu;
100 | }(_react.Component);
101 |
102 | exports.default = ContextMenu;
--------------------------------------------------------------------------------
/lib/common/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _input = require('./input');
8 |
9 | Object.defineProperty(exports, 'Input', {
10 | enumerable: true,
11 | get: function get() {
12 | return _interopRequireDefault(_input).default;
13 | }
14 | });
15 |
16 | var _spinner = require('./spinner');
17 |
18 | Object.defineProperty(exports, 'Spinner', {
19 | enumerable: true,
20 | get: function get() {
21 | return _interopRequireDefault(_spinner).default;
22 | }
23 | });
24 |
25 | var _contextmenu = require('./contextmenu');
26 |
27 | Object.defineProperty(exports, 'ContextMenu', {
28 | enumerable: true,
29 | get: function get() {
30 | return _interopRequireDefault(_contextmenu).default;
31 | }
32 | });
33 |
34 | var _menuitem = require('./menuitem');
35 |
36 | Object.defineProperty(exports, 'ContextMenuItem', {
37 | enumerable: true,
38 | get: function get() {
39 | return _interopRequireDefault(_menuitem).default;
40 | }
41 | });
42 |
43 | var _navsection = require('./navsection');
44 |
45 | Object.defineProperty(exports, 'NavSection', {
46 | enumerable: true,
47 | get: function get() {
48 | return _navsection.NavSection;
49 | }
50 | });
51 | Object.defineProperty(exports, 'NavHeader', {
52 | enumerable: true,
53 | get: function get() {
54 | return _navsection.NavHeader;
55 | }
56 | });
57 | Object.defineProperty(exports, 'NavHeaderButtons', {
58 | enumerable: true,
59 | get: function get() {
60 | return _navsection.NavHeaderButtons;
61 | }
62 | });
63 | Object.defineProperty(exports, 'NavBody', {
64 | enumerable: true,
65 | get: function get() {
66 | return _navsection.NavBody;
67 | }
68 | });
69 | Object.defineProperty(exports, 'NavItem', {
70 | enumerable: true,
71 | get: function get() {
72 | return _navsection.NavItem;
73 | }
74 | });
75 | Object.defineProperty(exports, 'IconButton', {
76 | enumerable: true,
77 | get: function get() {
78 | return _navsection.IconButton;
79 | }
80 | });
81 | Object.defineProperty(exports, 'PlusButton', {
82 | enumerable: true,
83 | get: function get() {
84 | return _navsection.PlusButton;
85 | }
86 | });
87 |
88 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/lib/common/input.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8 |
9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
10 |
11 | var _lodash = require('lodash');
12 |
13 | var _lodash2 = _interopRequireDefault(_lodash);
14 |
15 | var _react = require('react');
16 |
17 | var _react2 = _interopRequireDefault(_react);
18 |
19 | var _reactDom = require('react-dom');
20 |
21 | var _style = require('./style.scss');
22 |
23 | var _style2 = _interopRequireDefault(_style);
24 |
25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
26 |
27 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
28 |
29 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
30 |
31 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
32 |
33 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
34 |
35 | var Input = function (_Component) {
36 | _inherits(Input, _Component);
37 |
38 | function Input() {
39 | var _ref;
40 |
41 | var _temp, _this, _ret;
42 |
43 | _classCallCheck(this, Input);
44 |
45 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
46 | args[_key] = arguments[_key];
47 | }
48 |
49 | return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Input.__proto__ || Object.getPrototypeOf(Input)).call.apply(_ref, [this].concat(args))), _this), _this.input = null, _temp), _possibleConstructorReturn(_this, _ret);
50 | }
51 |
52 | _createClass(Input, [{
53 | key: 'componentDidMount',
54 | value: function componentDidMount() {
55 | if (this.props.focused) {
56 | this.focus();
57 | }
58 | }
59 | }, {
60 | key: 'componentWillReceiveProps',
61 | value: function componentWillReceiveProps(nextProps) {
62 | if (nextProps.focused) {
63 | this.focus();
64 | }
65 | }
66 | }, {
67 | key: 'focus',
68 | value: function focus() {
69 | if (this.input) {
70 | var node = (0, _reactDom.findDOMNode)(this.input);
71 | if (node) {
72 | node.focus();
73 | }
74 | }
75 | }
76 | }, {
77 | key: 'render',
78 | value: function render() {
79 | var _this2 = this;
80 |
81 | // eslint-disable-next-line no-unused-vars
82 | var _ref2 = this.props || {},
83 | cancel = _ref2.cancel,
84 | submit = _ref2.submit,
85 | focused = _ref2.focused,
86 | props = _objectWithoutProperties(_ref2, ['cancel', 'submit', 'focused']);
87 |
88 | var onKeyUp = function onKeyUp(event) {
89 | if (event.which === 27) {
90 | var input = $(event.target);
91 | input.blur();
92 | if (_lodash2.default.isFunction(props.cancel)) {
93 | cancel();
94 | return;
95 | }
96 | return;
97 | }
98 | if (event.ctrlKey && event.which === 13 && _lodash2.default.isFunction(submit)) {
99 | submit();
100 | }
101 | };
102 |
103 | var onMouseDown = function onMouseDown() {
104 | _this2.focus();
105 | };
106 |
107 | var attrs = _extends({
108 | ref: function ref(c) {
109 | _this2.input = c;
110 | },
111 | className: props.className || _style2.default.input,
112 | type: 'text',
113 | onKeyUp: onKeyUp,
114 | onMouseDown: onMouseDown
115 | }, props);
116 |
117 | return _react2.default.createElement('textarea', attrs);
118 | }
119 | }]);
120 |
121 | return Input;
122 | }(_react.Component);
123 |
124 | exports.default = Input;
--------------------------------------------------------------------------------
/lib/common/menuitem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _reactRouter = require('react-router');
14 |
15 | var _contextMenuStyle = require('./contextMenuStyle.scss');
16 |
17 | var _contextMenuStyle2 = _interopRequireDefault(_contextMenuStyle);
18 |
19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20 |
21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
22 |
23 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
24 |
25 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
26 |
27 | var MenuItem = function (_Component) {
28 | _inherits(MenuItem, _Component);
29 |
30 | function MenuItem() {
31 | _classCallCheck(this, MenuItem);
32 |
33 | return _possibleConstructorReturn(this, (MenuItem.__proto__ || Object.getPrototypeOf(MenuItem)).apply(this, arguments));
34 | }
35 |
36 | _createClass(MenuItem, [{
37 | key: 'renderContent',
38 | value: function renderContent() {
39 | var _props = this.props,
40 | href = _props.href,
41 | children = _props.children,
42 | link = _props.link,
43 | onClick = _props.onClick;
44 |
45 | if (href) {
46 | return _react2.default.createElement(
47 | 'a',
48 | { href: href, onClick: onClick },
49 | children
50 | );
51 | }
52 | if (link) {
53 | return _react2.default.createElement(
54 | _reactRouter.Link,
55 | { to: link },
56 | children
57 | );
58 | }
59 | return _react2.default.createElement(
60 | 'a',
61 | { onClick: onClick },
62 | children
63 | );
64 | }
65 | }, {
66 | key: 'render',
67 | value: function render() {
68 | if (this.props.header) {
69 | return _react2.default.createElement(
70 | 'div',
71 | { className: _contextMenuStyle2.default.header_item },
72 | _react2.default.createElement('hr', null),
73 | _react2.default.createElement(
74 | 'span',
75 | { className: _contextMenuStyle2.default.header_label },
76 | this.props.children
77 | )
78 | );
79 | }
80 | return _react2.default.createElement(
81 | 'li',
82 | { className: _contextMenuStyle2.default.menu_item, role: 'menuitem' },
83 | this.renderContent()
84 | );
85 | }
86 | }]);
87 |
88 | return MenuItem;
89 | }(_react.Component);
90 |
91 | exports.default = MenuItem;
--------------------------------------------------------------------------------
/lib/common/navSectionStyle.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .nav_section {
4 | margin: 4px 0;
5 | }
6 |
7 | .nav_item {
8 | margin: 4px 0;
9 |
10 | a {
11 | display: inline-flex;
12 | align-items: center;
13 | justify-content: center;
14 | padding: .25em 0.5em;
15 | border-radius: .5em;
16 | color: #333;
17 | cursor: pointer;
18 | text-decoration: none;
19 | line-height: 1;
20 |
21 | &:hover {
22 | background-color: #ccc;
23 | }
24 | }
25 | }
26 |
27 | .nav_item_selected a {
28 | color: white;
29 | background-color: $lightgreen;
30 |
31 | &:hover {
32 | background-color: $lightgreen;
33 | }
34 | }
35 |
36 | .nav_title {
37 | font-weight: bold;
38 | cursor: pointer;
39 | }
40 |
41 | .nav_buttons {
42 | display: inline-block;
43 | padding-left: 8px;
44 | }
45 |
46 | .icon_button {
47 | cursor: pointer;
48 | }
49 |
--------------------------------------------------------------------------------
/lib/common/navsection.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8 |
9 | exports.NavItem = NavItem;
10 | exports.IconButton = IconButton;
11 | exports.PlusButton = PlusButton;
12 | exports.NavHeaderButtons = NavHeaderButtons;
13 | exports.NavHeader = NavHeader;
14 | exports.NavBody = NavBody;
15 | exports.NavSection = NavSection;
16 |
17 | var _lodash = require('lodash');
18 |
19 | var _lodash2 = _interopRequireDefault(_lodash);
20 |
21 | var _react = require('react');
22 |
23 | var _react2 = _interopRequireDefault(_react);
24 |
25 | var _reactRouter = require('react-router');
26 |
27 | var _classnames = require('classnames');
28 |
29 | var _classnames2 = _interopRequireDefault(_classnames);
30 |
31 | var _cssEffects = require('css-effects');
32 |
33 | var _navSectionStyle = require('./navSectionStyle.scss');
34 |
35 | var _navSectionStyle2 = _interopRequireDefault(_navSectionStyle);
36 |
37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
38 |
39 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
40 |
41 | function NavItem(props) {
42 | var selected = props.selected || location.pathname === props.to;
43 | var className = (0, _classnames2.default)(props.className, _navSectionStyle2.default.nav_item, _defineProperty({}, _navSectionStyle2.default.nav_item_selected, selected));
44 | var link = props.to ? _react2.default.createElement(
45 | _reactRouter.Link,
46 | { to: props.to },
47 | props.children
48 | ) : _react2.default.createElement(
49 | 'a',
50 | { onClick: props.onClick || _lodash2.default.noop },
51 | props.children
52 | );
53 | return _react2.default.createElement(
54 | 'div',
55 | { className: className },
56 | link
57 | );
58 | }
59 |
60 | function IconButton(props) {
61 | var className = (0, _classnames2.default)(_navSectionStyle2.default.icon_button, (0, _cssEffects.hint)());
62 | return _react2.default.createElement(
63 | 'span',
64 | { className: className, onClick: props.onClick, 'data-hint': props.tip },
65 | _react2.default.createElement('i', { className: props.iconClass })
66 | );
67 | }
68 |
69 | function PlusButton(props) {
70 | return _react2.default.createElement(IconButton, _extends({}, props, { iconClass: 'ion-ios-plus-outline' }));
71 | }
72 |
73 | function NavHeaderButtons(props) {
74 | return _react2.default.createElement(
75 | 'span',
76 | { className: _navSectionStyle2.default.nav_buttons },
77 | props.children
78 | );
79 | }
80 |
81 | function NavHeader(props) {
82 | var className = (0, _classnames2.default)(props.className, _navSectionStyle2.default.nav_header);
83 | var title = props.title ? _react2.default.createElement(
84 | 'span',
85 | { className: _navSectionStyle2.default.nav_title },
86 | props.title
87 | ) : null;
88 | return _react2.default.createElement(
89 | 'div',
90 | { className: className },
91 | title,
92 | props.children
93 | );
94 | }
95 |
96 | function NavBody(props) {
97 | var className = (0, _classnames2.default)(props.className, _navSectionStyle2.default.nav_body);
98 | return _react2.default.createElement(
99 | 'div',
100 | { className: className },
101 | props.children
102 | );
103 | }
104 |
105 | function NavSection(props) {
106 | var className = (0, _classnames2.default)(props.className, _navSectionStyle2.default.nav_section);
107 | return _react2.default.createElement(
108 | 'div',
109 | { className: className },
110 | props.children
111 | );
112 | }
--------------------------------------------------------------------------------
/lib/common/spinner.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.loaderTypes = undefined;
7 |
8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
9 |
10 | exports.default = Spinner;
11 |
12 | var _lodash = require('lodash');
13 |
14 | var _lodash2 = _interopRequireDefault(_lodash);
15 |
16 | var _react = require('react');
17 |
18 | var _react2 = _interopRequireDefault(_react);
19 |
20 | var _classnames = require('classnames');
21 |
22 | var _classnames2 = _interopRequireDefault(_classnames);
23 |
24 | var _loaders = require('loaders.css/src/loaders.scss');
25 |
26 | var _loaders2 = _interopRequireDefault(_loaders);
27 |
28 | var _style = require('./style.scss');
29 |
30 | var _style2 = _interopRequireDefault(_style);
31 |
32 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
33 |
34 | // const defaultType = 'ball-beat';
35 | var defaultType = 'ball-scale-multiple';
36 |
37 | // TODO fix loader styles (also make it relative to image size)
38 | var loaders = {
39 | 'ball-pulse': {
40 | dots: 3,
41 | size: '6px'
42 | },
43 | 'ball-grid-pulse': {
44 | dots: 9
45 | },
46 | 'ball-clip-rotate': {
47 | dots: 1
48 | },
49 | 'ball-clip-rotate-pulse': {
50 | dots: 2
51 | },
52 | 'square-spin': {
53 | dots: 1
54 | },
55 | 'ball-clip-rotate-multiple': {
56 | dots: 2
57 | },
58 | 'ball-pulse-rise': {
59 | dots: 5
60 | },
61 | 'ball-rotate': {
62 | dots: 1
63 | },
64 | 'cube-transition': {
65 | dots: 2
66 | },
67 | 'ball-zig-zag': {
68 | dots: 2
69 | },
70 | 'ball-zig-zag-deflect': {
71 | dots: 2
72 | },
73 | 'ball-triangle-path': {
74 | dots: 3
75 | },
76 | 'ball-scale': {
77 | dots: 1
78 | },
79 | 'line-scale': {
80 | dots: 5
81 | },
82 | 'line-scale-party': {
83 | dots: 4
84 | },
85 | 'ball-scale-multiple': {
86 | dots: 3,
87 | size: '32px',
88 | left: '-16px',
89 | top: '16px'
90 | },
91 | 'ball-pulse-sync': {
92 | dots: 3
93 | },
94 | 'ball-beat': {
95 | dots: 3,
96 | size: '6px'
97 | },
98 | 'line-scale-pulse-out': {
99 | dots: 5
100 | },
101 | 'line-scale-pulse-out-rapid': {
102 | dots: 5
103 | },
104 | 'ball-scale-ripple': {
105 | dots: 1
106 | },
107 | 'ball-scale-ripple-multiple': {
108 | dots: 3,
109 | size: '32px',
110 | left: '-16px',
111 | top: '8px'
112 | },
113 | 'ball-spin-fade-loader': {
114 | dots: 8
115 | },
116 | 'line-spin-fade-loader': {
117 | dots: 8
118 | },
119 | 'triangle-skew-spin': {
120 | dots: 1
121 | },
122 | pacman: {
123 | dots: 5
124 | },
125 | 'ball-grid-beat': {
126 | dots: 9
127 | },
128 | 'semi-circle-spin': {
129 | dots: 1
130 | }
131 | };
132 |
133 | var loaderTypes = exports.loaderTypes = Object.keys(loaders);
134 |
135 | function dots(props) {
136 | return _lodash2.default.range(props.dots).map(function (i) {
137 | var style = {};
138 | var attrs = { style: style };
139 | if (props.size) {
140 | style.width = props.size;
141 | style.height = props.size;
142 | }
143 | if (props.left) {
144 | style.left = props.left;
145 | }
146 | if (props.top) {
147 | style.top = props.top;
148 | }
149 | return _react2.default.createElement('div', _extends({ key: i }, attrs));
150 | });
151 | }
152 |
153 | function Spinner(props) {
154 | var _React$DOM;
155 |
156 | var type = props.type || defaultType;
157 | var className = (0, _classnames2.default)(_style2.default.loader_inner, _loaders2.default[type]);
158 | var args = [{ className: className }, dots(loaders[type])];
159 | var inner = (_React$DOM = _react2.default.DOM).div.apply(_React$DOM, args);
160 | var loaderProps = {
161 | className: _style2.default.loader
162 | };
163 | if (props.size) {
164 | loaderProps.style = {
165 | width: props.size,
166 | height: props.size,
167 | display: 'table-cell',
168 | verticalAlign: 'middle'
169 | };
170 | }
171 | return _react2.default.createElement(
172 | 'div',
173 | loaderProps,
174 | inner
175 | );
176 | }
--------------------------------------------------------------------------------
/lib/common/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | $spinner-color: transparentize($input-focus-color, 0.4);
4 |
5 | .loader {
6 | text-align: center;
7 | }
8 |
9 | .loader_inner {
10 | display: inline-block;
11 | }
12 |
13 | .loader .loader_inner > div {
14 | background-color: $spinner-color;
15 | }
16 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _markdown = require('./markdown');
8 |
9 | Object.defineProperty(exports, 'Markdown', {
10 | enumerable: true,
11 | get: function get() {
12 | return _interopRequireDefault(_markdown).default;
13 | }
14 | });
15 |
16 | var _avatar = require('./avatar');
17 |
18 | Object.defineProperty(exports, 'Avatar', {
19 | enumerable: true,
20 | get: function get() {
21 | return _interopRequireDefault(_avatar).default;
22 | }
23 | });
24 |
25 | var _message = require('./message');
26 |
27 | Object.defineProperty(exports, 'Message', {
28 | enumerable: true,
29 | get: function get() {
30 | return _interopRequireDefault(_message).default;
31 | }
32 | });
33 |
34 | var _thread = require('./thread');
35 |
36 | Object.defineProperty(exports, 'Thread', {
37 | enumerable: true,
38 | get: function get() {
39 | return _thread.Thread;
40 | }
41 | });
42 | Object.defineProperty(exports, 'ThreadList', {
43 | enumerable: true,
44 | get: function get() {
45 | return _thread.ThreadList;
46 | }
47 | });
48 | Object.defineProperty(exports, 'ThreadForm', {
49 | enumerable: true,
50 | get: function get() {
51 | return _thread.ThreadForm;
52 | }
53 | });
54 |
55 | var _channel = require('./channel');
56 |
57 | Object.defineProperty(exports, 'Channel', {
58 | enumerable: true,
59 | get: function get() {
60 | return _channel.Channel;
61 | }
62 | });
63 | Object.defineProperty(exports, 'ChannelList', {
64 | enumerable: true,
65 | get: function get() {
66 | return _channel.ChannelList;
67 | }
68 | });
69 |
70 | var _user = require('./user');
71 |
72 | Object.defineProperty(exports, 'UserList', {
73 | enumerable: true,
74 | get: function get() {
75 | return _user.UserList;
76 | }
77 | });
78 | Object.defineProperty(exports, 'UserMenu', {
79 | enumerable: true,
80 | get: function get() {
81 | return _user.UserMenu;
82 | }
83 | });
84 |
85 | var _common = require('./common');
86 |
87 | Object.defineProperty(exports, 'Spinner', {
88 | enumerable: true,
89 | get: function get() {
90 | return _common.Spinner;
91 | }
92 | });
93 | Object.defineProperty(exports, 'ContextMenu', {
94 | enumerable: true,
95 | get: function get() {
96 | return _common.ContextMenu;
97 | }
98 | });
99 | Object.defineProperty(exports, 'ContextMenuItem', {
100 | enumerable: true,
101 | get: function get() {
102 | return _common.ContextMenuItem;
103 | }
104 | });
105 | Object.defineProperty(exports, 'NavSection', {
106 | enumerable: true,
107 | get: function get() {
108 | return _common.NavSection;
109 | }
110 | });
111 | Object.defineProperty(exports, 'NavHeader', {
112 | enumerable: true,
113 | get: function get() {
114 | return _common.NavHeader;
115 | }
116 | });
117 | Object.defineProperty(exports, 'NavHeaderButtons', {
118 | enumerable: true,
119 | get: function get() {
120 | return _common.NavHeaderButtons;
121 | }
122 | });
123 | Object.defineProperty(exports, 'NavItem', {
124 | enumerable: true,
125 | get: function get() {
126 | return _common.NavItem;
127 | }
128 | });
129 | Object.defineProperty(exports, 'IconButton', {
130 | enumerable: true,
131 | get: function get() {
132 | return _common.IconButton;
133 | }
134 | });
135 | Object.defineProperty(exports, 'PlusButton', {
136 | enumerable: true,
137 | get: function get() {
138 | return _common.PlusButton;
139 | }
140 | });
141 |
142 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/lib/markdown/help.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = Help;
7 |
8 | var _react = require('react');
9 |
10 | var _react2 = _interopRequireDefault(_react);
11 |
12 | var _style = require('./style.scss');
13 |
14 | var _style2 = _interopRequireDefault(_style);
15 |
16 | var _common = require('../common');
17 |
18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
19 |
20 | function Content() {
21 | var quote = '> ';
22 | var monospaced = '`';
23 | return _react2.default.createElement(
24 | 'div',
25 | { className: _style2.default.help_content },
26 | _react2.default.createElement(
27 | 'div',
28 | { className: _style2.default.help_format },
29 | _react2.default.createElement(
30 | 'span',
31 | null,
32 | _react2.default.createElement(
33 | 'b',
34 | null,
35 | '*bold*'
36 | )
37 | ),
38 | _react2.default.createElement('br', null),
39 | _react2.default.createElement(
40 | 'span',
41 | null,
42 | _react2.default.createElement(
43 | 'i',
44 | null,
45 | '_italic_'
46 | )
47 | ),
48 | _react2.default.createElement('br', null),
49 | _react2.default.createElement(
50 | 'span',
51 | null,
52 | quote,
53 | 'quoted'
54 | ),
55 | _react2.default.createElement('br', null),
56 | _react2.default.createElement(
57 | 'span',
58 | { className: _style2.default.monospaced },
59 | monospaced,
60 | 'monospaced',
61 | monospaced
62 | ),
63 | _react2.default.createElement('br', null),
64 | _react2.default.createElement(
65 | 'span',
66 | null,
67 | '[title](link)'
68 | ),
69 | _react2.default.createElement('br', null)
70 | ),
71 | _react2.default.createElement(
72 | 'div',
73 | { className: _style2.default.help_code },
74 | _react2.default.createElement(
75 | 'span',
76 | null,
77 | '```js'
78 | ),
79 | _react2.default.createElement('br', null),
80 | _react2.default.createElement(
81 | 'span',
82 | null,
83 | 'javascript code'
84 | ),
85 | _react2.default.createElement('br', null),
86 | _react2.default.createElement(
87 | 'span',
88 | null,
89 | '```'
90 | ),
91 | _react2.default.createElement('br', null)
92 | ),
93 | _react2.default.createElement(
94 | 'div',
95 | { className: _style2.default.help_post },
96 | _react2.default.createElement(
97 | 'em',
98 | null,
99 | 'ctrl'
100 | ),
101 | ' + ',
102 | _react2.default.createElement(
103 | 'em',
104 | null,
105 | 'enter'
106 | ),
107 | _react2.default.createElement(
108 | 'span',
109 | null,
110 | '\xA0post'
111 | )
112 | )
113 | );
114 | }
115 |
116 | function Help() {
117 | var menuProps = {
118 | button: {
119 | className: _style2.default.show_help,
120 | content: '?'
121 | }
122 | };
123 | return _react2.default.createElement(
124 | _common.ContextMenu,
125 | menuProps,
126 | _react2.default.createElement(Content, null)
127 | );
128 | }
--------------------------------------------------------------------------------
/lib/markdown/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _help = require('./help');
8 |
9 | Object.defineProperty(exports, 'Help', {
10 | enumerable: true,
11 | get: function get() {
12 | return _interopRequireDefault(_help).default;
13 | }
14 | });
15 |
16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/lib/markdown/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .focused .show_help {
4 | display: inline-block;
5 | }
6 |
7 | .show_help {
8 | cursor: pointer;
9 | display: none;
10 | position: absolute;
11 | right: .2em;
12 | padding: .3em;
13 | transition: color .2s;
14 | font-size: 16px;
15 | font-weight: bold;
16 | text-decoration: none;
17 | color: $input-focus-color;
18 | }
19 |
20 | .show_help:hover {
21 | text-decoration: none;
22 | }
23 |
24 | .monospaced {
25 | font-family: 'Bitstream Vera Sans Mono',Menlo,Monaco,Courier,monospace;
26 | }
27 |
28 | .help_content {
29 | margin: 2rem 4rem;
30 | }
31 |
32 | .help_format {
33 | padding: .6em 0;
34 | }
35 |
36 | .help_code {
37 | border: 1px solid #ddd;
38 | border-width: 1px 0;
39 | padding: .6em 0;
40 | margin-bottom: 1.2em;
41 | }
42 |
43 | .help_post em {
44 | background: #eee;
45 | border-radius: .3em;
46 | padding: .1em .4em 0;
47 | color: #333;
48 | font-weight: bold;
49 | display: inline-block;
50 | }
51 |
--------------------------------------------------------------------------------
/lib/message/action.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.tips = exports.faIconSet = exports.ionIconSet = undefined;
7 |
8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
9 |
10 | exports.Action = Action;
11 | exports.renderActions = renderActions;
12 |
13 | var _lodash = require('lodash');
14 |
15 | var _lodash2 = _interopRequireDefault(_lodash);
16 |
17 | var _react = require('react');
18 |
19 | var _react2 = _interopRequireDefault(_react);
20 |
21 | var _classnames = require('classnames');
22 |
23 | var _classnames2 = _interopRequireDefault(_classnames);
24 |
25 | var _cssEffects = require('css-effects');
26 |
27 | var _style = require('./style.scss');
28 |
29 | var _style2 = _interopRequireDefault(_style);
30 |
31 | var _counter = require('./counter');
32 |
33 | var _counter2 = _interopRequireDefault(_counter);
34 |
35 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
36 |
37 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
38 |
39 | // TODO configurable icons
40 | var ionIconSet = exports.ionIconSet = {
41 | like: 'ion-ios-heart',
42 | reply: 'ion-ios-chatbubble',
43 | star: 'ion-star',
44 | remove: 'ion-trash-a',
45 | edit: 'ion-edit'
46 | };
47 |
48 | var faIconSet = exports.faIconSet = {
49 | like: 'fa fa-heart',
50 | reply: 'fa fa-comment',
51 | star: 'fa fa-star',
52 | remove: 'fa fa-trash',
53 | edit: 'fa fa-pencil'
54 | };
55 |
56 | var tips = exports.tips = {
57 | like: 'Like',
58 | reply: 'Reply',
59 | star: 'Star',
60 | remove: 'Delete',
61 | edit: 'Edit'
62 | };
63 |
64 | var actionClassNames = {
65 | like: _style2.default.like_count,
66 | reply: _style2.default.message_count
67 | };
68 |
69 | function getIconSet(name) {
70 | switch (name) {
71 | case 'fa':
72 | case 'awesome':
73 | return faIconSet;
74 | case 'ion':
75 | case 'ionic':
76 | default:
77 | return ionIconSet;
78 | }
79 | }
80 |
81 | function Action(props) {
82 | var _classNames;
83 |
84 | var action = props.action;
85 |
86 | var count = props.count || 0;
87 | var onClick = function onClick(e) {
88 | e.preventDefault();
89 | if (_lodash2.default.isFunction(props.onAction)) {
90 | props.onAction(props.type, action, props.data);
91 | }
92 | };
93 |
94 | if (action === 'reply') {
95 | var attrs = {
96 | className: actionClassNames[action],
97 | count: count,
98 | onClick: onClick,
99 | title: tips[action],
100 | element: _react2.default.DOM.a
101 | };
102 | return _react2.default.createElement(_counter2.default, attrs);
103 | }
104 |
105 | var className = (0, _classnames2.default)((_classNames = {}, _defineProperty(_classNames, (0, _cssEffects.hint)(), true), _defineProperty(_classNames, action, true), _defineProperty(_classNames, _style2.default.action, true), _defineProperty(_classNames, 'pull-right', props.right), _classNames));
106 | var iconSet = getIconSet(props.iconSet);
107 |
108 | return _react2.default.createElement(
109 | 'a',
110 | { className: className, onClick: onClick, 'data-hint': tips[action] },
111 | _react2.default.createElement('i', { className: iconSet[action] }),
112 | count > 0 ? _react2.default.createElement(
113 | 'span',
114 | { className: 'count' },
115 | count
116 | ) : null
117 | );
118 | }
119 |
120 | exports.default = Action;
121 | function renderActions(actions, type, data, options) {
122 | return Object.keys(actions).filter(function (key) {
123 | if (!_lodash2.default.isFunction(options.canExecute)) return true;
124 | return options.canExecute(type, key, data);
125 | }).map(function (key) {
126 | var props = _extends({
127 | data: data,
128 | type: type,
129 | action: key,
130 | onAction: options.onAction,
131 | iconSet: options.iconSet
132 | }, actions[key]);
133 | return _react2.default.createElement(Action, _extends({ key: data.id + '/' + key }, props));
134 | });
135 | }
--------------------------------------------------------------------------------
/lib/message/age.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = Age;
7 |
8 | var _lodash = require('lodash');
9 |
10 | var _lodash2 = _interopRequireDefault(_lodash);
11 |
12 | var _react = require('react');
13 |
14 | var _react2 = _interopRequireDefault(_react);
15 |
16 | var _classnames = require('classnames');
17 |
18 | var _classnames2 = _interopRequireDefault(_classnames);
19 |
20 | var _moment = require('moment');
21 |
22 | var _moment2 = _interopRequireDefault(_moment);
23 |
24 | var _style = require('./style.scss');
25 |
26 | var _style2 = _interopRequireDefault(_style);
27 |
28 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
29 |
30 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
31 |
32 | function isToday(value) {
33 | if (!_moment2.default.isDate(value)) return false;
34 | var now = (0, _moment2.default)();
35 | var m = (0, _moment2.default)(value);
36 | return m.year() === now.year() && m.dayOfYear() === now.dayOfYear();
37 | }
38 |
39 | var formatTime = function formatTime(value) {
40 | if (!value) {
41 | return '';
42 | }
43 | if (_lodash2.default.isString(value)) {
44 | return value;
45 | }
46 | if (isToday(value)) {
47 | return (0, _moment2.default)(value).fromNow();
48 | }
49 | return (0, _moment2.default)(value).format('HH:mm');
50 | };
51 |
52 | function Age(_ref) {
53 | var time = _ref.time;
54 |
55 | var text = formatTime(time);
56 | var className = (0, _classnames2.default)(_style2.default.time, _defineProperty({}, _style2.default.today, isToday(time)));
57 | var attrs = {
58 | className: className
59 | };
60 |
61 | if (_moment2.default.isDate(time)) {
62 | attrs['data-toggle'] = 'tooltip';
63 | attrs.title = (0, _moment2.default)(time).format('ddd MMM D YYYY HH:mm:ss');
64 | }
65 |
66 | return _react2.default.createElement(
67 | 'span',
68 | attrs,
69 | text
70 | );
71 | }
--------------------------------------------------------------------------------
/lib/message/base.scss:
--------------------------------------------------------------------------------
1 | .message {
2 | margin: 2px 0px;
3 | padding: 0px 20px 0px 80px;
4 | }
5 |
6 | .message_body {
7 | min-height: 44px;
8 | }
9 |
10 | .reply {
11 | padding: 0px 0px 0px 40px;
12 | }
13 |
14 | .name {
15 | color: #bbb;
16 | }
17 |
18 | .time {
19 | color: #bbb;
20 | font-weight: 400;
21 | margin: 0 0 0 6px;
22 | }
23 |
24 | .today {
25 | color: #36a5d9;
26 | }
27 |
28 | .meta span, .meta a, .meta i {
29 | margin-right: 0.3em;
30 | }
31 |
32 | .action, .action:hover {
33 | font-size: 14px;
34 | text-decoration: none;
35 | cursor: pointer;
36 | margin-right: 0.3em;
37 | }
38 |
--------------------------------------------------------------------------------
/lib/message/counter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = Counter;
7 |
8 | var _react = require('react');
9 |
10 | var _react2 = _interopRequireDefault(_react);
11 |
12 | var _cssEffects = require('css-effects');
13 |
14 | var _style = require('./style.scss');
15 |
16 | var _style2 = _interopRequireDefault(_style);
17 |
18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
19 |
20 | function Counter(props) {
21 | var className = props.className || _style2.default.message_count;
22 | var attrs = { className: className, onClick: props.onClick };
23 | var counter = _react2.default.createElement(
24 | 'span',
25 | attrs,
26 | props.count
27 | );
28 | if (props.title) {
29 | return _react2.default.createElement(
30 | 'span',
31 | { className: (0, _cssEffects.hint)(), 'data-hint': props.title },
32 | counter
33 | );
34 | }
35 | return counter;
36 | }
--------------------------------------------------------------------------------
/lib/message/counters.scss:
--------------------------------------------------------------------------------
1 | $mc-border-color: #9197a3;
2 |
3 | .message_count {
4 | margin: 0 8px;
5 | display: inline-block;
6 | position: relative;
7 | padding: 0px 4px;
8 | background: white;
9 | color: #4e5665;
10 | border: $mc-border-color solid 1px;
11 | border-radius: 2px;
12 | font-size: 11px;
13 | font-weight: normal;
14 | text-align: center;
15 | white-space: nowrap;
16 | min-width: 15px;
17 | cursor: pointer;
18 | text-decoration: none;
19 | @include arrow-left($width: 4px, $length: 4px, $background-color: white, $border-color: $mc-border-color);
20 | }
21 |
22 | .message_count:hover {
23 | text-decoration: none;
24 | }
25 |
26 | .like_count {
27 | display: inline-block;
28 | cursor: pointer;
29 | white-space: nowrap;
30 | text-align: center;
31 | text-decoration: none;
32 | @include heart($size: 20px);
33 | }
34 |
35 | .like_count:hover {
36 | text-decoration: none;
37 | }
38 |
--------------------------------------------------------------------------------
/lib/message/github.scss:
--------------------------------------------------------------------------------
1 | @import "../_mixins";
2 |
3 | .message_wrapper.github {
4 | padding-left: 64px;
5 | }
6 |
7 | .message_wrapper.github .avatar {
8 | margin-left: -64px;
9 | }
10 |
11 | .message.github {
12 | position: relative;
13 | padding: 0;
14 | border: 1px solid #ddd;
15 | border-radius: 3px;
16 | @include arrow-left($position: 11px, $width: 5px, $length: 5px, $background-color: #f7f7f7, $border-color: #ddd);
17 | }
18 |
19 | .message.github .meta {
20 | padding: 10px 15px;
21 | background-color: #f7f7f7;
22 | border-bottom: 1px solid #eee;
23 | border-top-left-radius: 3px;
24 | border-top-right-radius: 3px;
25 | }
26 |
27 | .message.github .meta .name {
28 | color: #767676;
29 | font-weight: bold;
30 | }
31 |
32 | .message.github .message_body {
33 | padding: 15px;
34 | }
35 |
--------------------------------------------------------------------------------
/lib/message/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _message = require('./message');
8 |
9 | Object.defineProperty(exports, 'Message', {
10 | enumerable: true,
11 | get: function get() {
12 | return _message.Message;
13 | }
14 | });
15 | Object.defineProperty(exports, 'getTime', {
16 | enumerable: true,
17 | get: function get() {
18 | return _message.getTime;
19 | }
20 | });
21 |
22 | var _messageinput = require('./messageinput');
23 |
24 | Object.defineProperty(exports, 'MessageInput', {
25 | enumerable: true,
26 | get: function get() {
27 | return _interopRequireDefault(_messageinput).default;
28 | }
29 | });
30 |
31 | var _counter = require('./counter');
32 |
33 | Object.defineProperty(exports, 'Counter', {
34 | enumerable: true,
35 | get: function get() {
36 | return _interopRequireDefault(_counter).default;
37 | }
38 | });
39 | Object.defineProperty(exports, 'default', {
40 | enumerable: true,
41 | get: function get() {
42 | return _interopRequireDefault(_message).default;
43 | }
44 | });
45 |
46 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/lib/message/messageinput.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _lodash = require('lodash');
10 |
11 | var _lodash2 = _interopRequireDefault(_lodash);
12 |
13 | var _react = require('react');
14 |
15 | var _react2 = _interopRequireDefault(_react);
16 |
17 | var _classnames = require('classnames');
18 |
19 | var _classnames2 = _interopRequireDefault(_classnames);
20 |
21 | var _reactBootstrap = require('react-bootstrap');
22 |
23 | var _common = require('../common');
24 |
25 | var _help = require('../markdown/help');
26 |
27 | var _help2 = _interopRequireDefault(_help);
28 |
29 | var _uploadbutton = require('./uploadbutton');
30 |
31 | var _uploadbutton2 = _interopRequireDefault(_uploadbutton);
32 |
33 | var _style = require('./style.scss');
34 |
35 | var _style2 = _interopRequireDefault(_style);
36 |
37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
38 |
39 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
40 |
41 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
42 |
43 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
44 |
45 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
46 |
47 | // TODO render user avatar
48 | // TODO configure submit shortcut, ctrl-enter is default
49 |
50 | var MessageInput = function (_Component) {
51 | _inherits(MessageInput, _Component);
52 |
53 | function MessageInput(props) {
54 | _classCallCheck(this, MessageInput);
55 |
56 | var _this = _possibleConstructorReturn(this, (MessageInput.__proto__ || Object.getPrototypeOf(MessageInput)).call(this, props));
57 |
58 | _this.state = {
59 | focused: _this.props.focused,
60 | value: props.value || '',
61 | helpVisible: false
62 | };
63 |
64 | _this.onChange = _this.onChange.bind(_this);
65 |
66 | var self = _this;
67 | function makeFocusTransition(focused) {
68 | return function () {
69 | if (focused && _lodash2.default.isFunction(props.onFocus)) {
70 | props.onFocus();
71 | }
72 | if (!focused && _lodash2.default.isFunction(props.onBlur)) {
73 | props.onBlur();
74 | }
75 | self.setState({ focused: focused });
76 | };
77 | }
78 | _this.onFocus = makeFocusTransition(true);
79 | _this.onBlur = makeFocusTransition(false);
80 | return _this;
81 | }
82 |
83 | _createClass(MessageInput, [{
84 | key: 'componentWillReceiveProps',
85 | value: function componentWillReceiveProps(nextProps) {
86 | if (this.state.focused !== nextProps.focused) {
87 | this.setState({ focused: nextProps.focused });
88 | }
89 | }
90 | }, {
91 | key: 'onChange',
92 | value: function onChange(event) {
93 | var value = event.target.value || '';
94 | if (_lodash2.default.isFunction(this.props.onChange)) {
95 | this.props.onChange(value);
96 | }
97 | this.setState({ value: value });
98 | }
99 | }, {
100 | key: 'render',
101 | value: function render() {
102 | var _this2 = this;
103 |
104 | var canSubmit = _lodash2.default.isFunction(this.props.canSubmit) ? this.props.canSubmit : function () {
105 | return _this2.state.value.length > 0;
106 | };
107 | var submit = function submit() {
108 | var value = _this2.state.value;
109 |
110 | if (!value) return;
111 | _this2.setState({ value: '' });
112 | _this2.props.submit(value);
113 | };
114 | var inputProps = {
115 | className: (0, _classnames2.default)(_style2.default.input, _style2.default.message_input),
116 | placeholder: this.props.placeholder || 'Reply...',
117 | value: this.state.value,
118 | onChange: this.onChange,
119 | onFocus: this.onFocus,
120 | onBlur: this.onBlur,
121 | cancel: this.props.cancel,
122 | submit: submit,
123 | focused: this.state.focused
124 | };
125 | var submitProps = {
126 | className: 'pull-right',
127 | bsStyle: 'primary',
128 | bsSize: 'small',
129 | onMouseDown: submit,
130 | disabled: !canSubmit()
131 | };
132 | var formProps = {
133 | className: (0, _classnames2.default)(_style2.default.reply_form, _defineProperty({}, _style2.default.focused, this.state.focused)),
134 | style: this.props.formStyle || {}
135 | };
136 | var onUpload = function onUpload(data) {
137 | var content = _this2.state.value || '';
138 | if (content) {
139 | content += '\r\n';
140 | }
141 | content += '[' + data.name + '](' + data.url + ')';
142 | _this2.setState({ value: content });
143 | };
144 | var uploadProps = {
145 | onSuccess: onUpload
146 | };
147 | return _react2.default.createElement(
148 | 'div',
149 | formProps,
150 | _react2.default.createElement(_help2.default, null),
151 | _react2.default.createElement(_common.Input, inputProps),
152 | _react2.default.createElement(
153 | 'div',
154 | { className: _style2.default.reply_controls },
155 | _react2.default.createElement(_uploadbutton2.default, uploadProps),
156 | _react2.default.createElement(
157 | _reactBootstrap.Button,
158 | submitProps,
159 | 'Post'
160 | )
161 | )
162 | );
163 | }
164 | }]);
165 |
166 | return MessageInput;
167 | }(_react.Component);
168 |
169 | MessageInput.propTypes = {
170 | submit: _react.PropTypes.func,
171 | cancel: _react.PropTypes.func,
172 | focused: _react.PropTypes.bool
173 | };
174 | MessageInput.defaultProps = {
175 | focused: false,
176 | submit: _lodash2.default.noop,
177 | cancel: _lodash2.default.noop
178 | };
179 | exports.default = MessageInput;
--------------------------------------------------------------------------------
/lib/message/replyform.scss:
--------------------------------------------------------------------------------
1 | .reply_form {
2 | position: relative;
3 | padding: .6em 0 1.2em 4em;
4 | width: 100%;
5 | }
6 |
7 | .message_input {
8 | width: 100%;
9 | height: 100%;
10 | min-height: 2em;
11 | padding: .6em;
12 | overflow: hidden;
13 | font-size: .9em;
14 | line-height: 1.5;
15 | resize: vertical;
16 | }
17 |
18 | .reply_form.focused .reply_controls {
19 | display: block;
20 | }
21 | .reply_controls {
22 | display: none;
23 | text-align: right;
24 | margin: .6em 0;
25 | position: relative;
26 | }
27 |
28 | .upload_button {
29 | cursor: pointer;
30 | position: absolute;
31 | font-size: 20px;
32 | right: .4em;
33 | top: -2em;
34 | text-decoration: none;
35 | color: rgba(0,0,0,0.4);
36 | }
37 | .upload_button:hover {
38 | text-decoration: none;
39 | color: $input-focus-color;
40 | }
41 |
--------------------------------------------------------------------------------
/lib/message/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 | @import "../_mixins";
3 | @import "../_shapes";
4 | @import "base";
5 | @import "github";
6 | @import "replyform";
7 | @import "counters";
8 |
--------------------------------------------------------------------------------
/lib/message/uploadbutton.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = UploadButton;
7 |
8 | var _lodash = require('lodash');
9 |
10 | var _lodash2 = _interopRequireDefault(_lodash);
11 |
12 | var _react = require('react');
13 |
14 | var _react2 = _interopRequireDefault(_react);
15 |
16 | var _tusJsClient = require('tus-js-client');
17 |
18 | var _tusJsClient2 = _interopRequireDefault(_tusJsClient);
19 |
20 | var _style = require('./style.scss');
21 |
22 | var _style2 = _interopRequireDefault(_style);
23 |
24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25 |
26 | // TODO configurable upload client: tus, component-upload, etc
27 |
28 | function uploadFile(file) {
29 | var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _lodash2.default.noop;
30 |
31 | // const upload = new Upload(file);
32 | // upload.on('end', res => {
33 | // console.log(res);
34 | // });
35 | // const options = {
36 | // path: window.UPLOAD_PATH || '/api/uploads/',
37 | // };
38 | // upload.to(options);
39 |
40 | // Create a new tus upload
41 | var upload = new _tusJsClient2.default.Upload(file, {
42 | // TODO enable resumable uploads
43 | resume: false,
44 | endpoint: window.UPLOAD_PATH || '/api/uploads/',
45 | onError: function onError(err) {
46 | console.log('upload failed:', err);
47 | callback(null, err);
48 | },
49 | onProgress: function onProgress(bytesUploaded, bytesTotal) {
50 | var percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);
51 | console.log('progress %s/%s, %s', bytesUploaded, bytesTotal, percentage + '%');
52 | },
53 | onSuccess: function onSuccess() {
54 | console.log('download %s from %s', upload.file.name, upload.url);
55 | callback({ name: upload.file.name, url: upload.url });
56 | }
57 | });
58 |
59 | // Start the upload
60 | upload.start();
61 | }
62 | // import Upload from 'component-upload';
63 | function UploadButton(props) {
64 | var onClick = function onClick() {
65 | var inputStyle = 'display:block;visibility:hidden;width:0;height:0';
66 | var input = $('');
67 | input.appendTo($('body'));
68 | input.change(function () {
69 | console.log('uploading...');
70 | var files = input[0].files;
71 | console.log(files);
72 | var file = files[0];
73 | input.remove();
74 | uploadFile(file, function (data, err) {
75 | if (err) {
76 | if (_lodash2.default.isFunction(props.onError)) {
77 | props.onError(err);
78 | }
79 | return;
80 | }
81 | if (_lodash2.default.isFunction(props.onSuccess)) {
82 | props.onSuccess(data);
83 | }
84 | });
85 | });
86 | input.click();
87 | };
88 | var btnProps = {
89 | className: _style2.default.upload_button,
90 | title: 'Upload files',
91 | 'data-toggle': 'tooltip',
92 | onMouseDown: onClick
93 | };
94 | return _react2.default.createElement(
95 | 'a',
96 | btnProps,
97 | _react2.default.createElement('i', { className: 'ion-camera' })
98 | );
99 | }
--------------------------------------------------------------------------------
/lib/message/username.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _lodash = require('lodash');
10 |
11 | var _lodash2 = _interopRequireDefault(_lodash);
12 |
13 | var _react = require('react');
14 |
15 | var _react2 = _interopRequireDefault(_react);
16 |
17 | var _classnames = require('classnames');
18 |
19 | var _classnames2 = _interopRequireDefault(_classnames);
20 |
21 | var _style = require('./style.scss');
22 |
23 | var _style2 = _interopRequireDefault(_style);
24 |
25 | var _util = require('../util');
26 |
27 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28 |
29 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
30 |
31 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
32 |
33 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
34 |
35 | var UserName = function (_Component) {
36 | _inherits(UserName, _Component);
37 |
38 | function UserName(props) {
39 | _classCallCheck(this, UserName);
40 |
41 | var _this = _possibleConstructorReturn(this, (UserName.__proto__ || Object.getPrototypeOf(UserName)).call(this, props));
42 |
43 | _this.state = { name: props.name };
44 | _this.setName = _this.setName.bind(_this);
45 | return _this;
46 | }
47 |
48 | _createClass(UserName, [{
49 | key: 'setName',
50 | value: function setName(value) {
51 | return this.setState({ name: value });
52 | }
53 | }, {
54 | key: 'render',
55 | value: function render() {
56 | var value = this.state.name;
57 | if (!value) {
58 | return _react2.default.createElement('span', null);
59 | }
60 | if (!_lodash2.default.isString(value)) {
61 | var promise = (0, _util.toPromise)(value);
62 | if (promise) {
63 | promise.then(this.setName);
64 | }
65 | // TODO render small spinner
66 | value = '';
67 | }
68 | return _react2.default.createElement(
69 | 'a',
70 | { className: (0, _classnames2.default)(_style2.default.name) },
71 | value
72 | );
73 | }
74 | }]);
75 |
76 | return UserName;
77 | }(_react.Component);
78 |
79 | exports.default = UserName;
--------------------------------------------------------------------------------
/lib/thread/contributors.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8 |
9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
10 |
11 | var _lodash = require('lodash');
12 |
13 | var _lodash2 = _interopRequireDefault(_lodash);
14 |
15 | var _react = require('react');
16 |
17 | var _react2 = _interopRequireDefault(_react);
18 |
19 | var _avatar = require('../avatar');
20 |
21 | var _avatar2 = _interopRequireDefault(_avatar);
22 |
23 | var _style = require('./style.scss');
24 |
25 | var _style2 = _interopRequireDefault(_style);
26 |
27 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28 |
29 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
30 |
31 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
32 |
33 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
34 |
35 | var ContributorList = function (_Component) {
36 | _inherits(ContributorList, _Component);
37 |
38 | function ContributorList(props) {
39 | _classCallCheck(this, ContributorList);
40 |
41 | var _this = _possibleConstructorReturn(this, (ContributorList.__proto__ || Object.getPrototypeOf(ContributorList)).call(this, props));
42 |
43 | _this.mounted = false;
44 | var users = props.users;
45 |
46 | _this.state = {
47 | users: _lodash2.default.isFunction(users) ? users() : users
48 | };
49 | return _this;
50 | }
51 |
52 | _createClass(ContributorList, [{
53 | key: 'componentDidMount',
54 | value: function componentDidMount() {
55 | this.mounted = true;
56 | this.update(this.props);
57 | }
58 | }, {
59 | key: 'componentWillReceiveProps',
60 | value: function componentWillReceiveProps(nextProps) {
61 | this.update(nextProps);
62 | }
63 | }, {
64 | key: 'componentWillUnmount',
65 | value: function componentWillUnmount() {
66 | this.mounted = false;
67 | if (_lodash2.default.isFunction(this.unsubscribe)) {
68 | this.unsubscribe();
69 | this.unsubscribe = null;
70 | }
71 | }
72 | }, {
73 | key: 'update',
74 | value: function update(props) {
75 | var _this2 = this;
76 |
77 | if (_lodash2.default.isFunction(this.unsubscribe)) {
78 | this.unsubscribe();
79 | this.unsubscribe = null;
80 | }
81 |
82 | var users = props.users;
83 |
84 |
85 | if (_lodash2.default.isFunction(users)) {
86 | var result = users(function (list) {
87 | if (!_this2.mounted) return;
88 | _this2.setState({ users: list });
89 | });
90 | if (_lodash2.default.isFunction(result)) {
91 | this.unsubscribe = result;
92 | } else if (_lodash2.default.isArray(result)) {
93 | this.setState({ users: result });
94 | }
95 | } else {
96 | this.setState({ users: users });
97 | }
98 | }
99 | }, {
100 | key: 'render',
101 | value: function render() {
102 | var items = _lodash2.default.map(this.state.users, function (user) {
103 | var avatarProps = {
104 | hover: 'grow',
105 | user: user,
106 | shape: 'round_rect',
107 | online: false,
108 | size: 24,
109 | style: {
110 | display: 'inline-block',
111 | margin: '0px'
112 | }
113 | };
114 | return _react2.default.createElement(_avatar2.default, _extends({ key: user.id }, avatarProps));
115 | });
116 | return _react2.default.createElement(
117 | 'div',
118 | { className: _style2.default.contributor_list },
119 | items
120 | );
121 | }
122 | }]);
123 |
124 | return ContributorList;
125 | }(_react.Component);
126 |
127 | exports.default = ContributorList;
--------------------------------------------------------------------------------
/lib/thread/day.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.TextBlock = TextBlock;
7 | exports.default = Day;
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _classnames = require('classnames');
14 |
15 | var _classnames2 = _interopRequireDefault(_classnames);
16 |
17 | var _moment = require('moment');
18 |
19 | var _moment2 = _interopRequireDefault(_moment);
20 |
21 | var _style = require('./style.scss');
22 |
23 | var _style2 = _interopRequireDefault(_style);
24 |
25 | var _message = require('../message');
26 |
27 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28 |
29 | // TODO move to common components
30 | function TextBlock(props) {
31 | return _react2.default.createElement(
32 | 'div',
33 | { className: props.className, onClick: props.onClick },
34 | props.text || ''
35 | );
36 | }
37 |
38 | var formatDay = function formatDay(time) {
39 | var now = (0, _moment2.default)();
40 | var day = now.dayOfYear();
41 | var m = (0, _moment2.default)(time);
42 | // this year
43 | if (m.year() === now.year()) {
44 | if (m.dayOfYear() === day) {
45 | // TODO localization
46 | return 'Today';
47 | }
48 | if (m.dayOfYear() === day - 1) {
49 | // TODO localization
50 | return 'Yesterday';
51 | }
52 | // this week
53 | if (m.week() === now.week()) {
54 | return m.format('dddd');
55 | }
56 | return m.format('MMMM D, dddd');
57 | }
58 | return m.format('MMMM D YYYY, dddd');
59 | };
60 |
61 | function Day(props) {
62 | var className = (0, _classnames2.default)(_style2.default.day);
63 | var text = formatDay(props.time);
64 | return _react2.default.createElement(
65 | 'div',
66 | { className: className },
67 | _react2.default.createElement(
68 | 'a',
69 | { onClick: props.onClick },
70 | _react2.default.createElement(
71 | 'span',
72 | null,
73 | text
74 | ),
75 | _react2.default.createElement(_message.Counter, { count: props.count })
76 | )
77 | );
78 | }
--------------------------------------------------------------------------------
/lib/thread/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _thread = require('./thread');
8 |
9 | Object.defineProperty(exports, 'default', {
10 | enumerable: true,
11 | get: function get() {
12 | return _interopRequireDefault(_thread).default;
13 | }
14 | });
15 | Object.defineProperty(exports, 'Thread', {
16 | enumerable: true,
17 | get: function get() {
18 | return _interopRequireDefault(_thread).default;
19 | }
20 | });
21 |
22 | var _list = require('./list');
23 |
24 | Object.defineProperty(exports, 'ThreadList', {
25 | enumerable: true,
26 | get: function get() {
27 | return _interopRequireDefault(_list).default;
28 | }
29 | });
30 |
31 | var _threadform = require('./threadform');
32 |
33 | Object.defineProperty(exports, 'ThreadForm', {
34 | enumerable: true,
35 | get: function get() {
36 | return _interopRequireDefault(_threadform).default;
37 | }
38 | });
39 |
40 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/lib/thread/list.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.Topic = undefined;
7 |
8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
9 |
10 | exports.default = ThreadList;
11 |
12 | var _lodash = require('lodash');
13 |
14 | var _lodash2 = _interopRequireDefault(_lodash);
15 |
16 | var _react = require('react');
17 |
18 | var _react2 = _interopRequireDefault(_react);
19 |
20 | var _classnames = require('classnames');
21 |
22 | var _classnames2 = _interopRequireDefault(_classnames);
23 |
24 | var _moment = require('moment');
25 |
26 | var _moment2 = _interopRequireDefault(_moment);
27 |
28 | var _avatar = require('../avatar');
29 |
30 | var _avatar2 = _interopRequireDefault(_avatar);
31 |
32 | var _thread = require('./thread');
33 |
34 | var _thread2 = _interopRequireDefault(_thread);
35 |
36 | var _style = require('./style.scss');
37 |
38 | var _style2 = _interopRequireDefault(_style);
39 |
40 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
41 |
42 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
43 |
44 | function formatTime(value) {
45 | return value ? (0, _moment2.default)(value).format('HH:mm') : '';
46 | }
47 |
48 | // TODO reuse rendering of user name from message component
49 |
50 | var Topic = exports.Topic = function Topic(props) {
51 | var className = (0, _classnames2.default)(_style2.default.topic, _defineProperty({}, _style2.default.topic_selected, !!props.selected));
52 | var msg = props.last_message || props.message || {};
53 | var user = msg.user;
54 | var unread = props.unread ? (props.unread > 10 ? '10+' : props.unread) + ' new' : '';
55 | var avatarURL = user ? user.avatar_url || user.avatar : null;
56 |
57 | var onClick = function onClick(e) {
58 | e.preventDefault();
59 | if (_lodash2.default.isFunction(props.onSelect)) {
60 | props.onSelect(props.thread);
61 | }
62 | };
63 |
64 | return _react2.default.createElement(
65 | 'div',
66 | { className: className, onClick: onClick },
67 | avatarURL ? _react2.default.createElement(_avatar2.default, { source: avatarURL, size: props.avatarSize, name: user.name }) : null,
68 | _react2.default.createElement(
69 | 'div',
70 | { className: 'header ' + _style2.default.header },
71 | _react2.default.createElement(
72 | 'span',
73 | null,
74 | props.topic
75 | ),
76 | unread ? _react2.default.createElement(
77 | 'span',
78 | { className: 'unread ' + _style2.default.unread },
79 | unread
80 | ) : null
81 | ),
82 | _react2.default.createElement(
83 | 'div',
84 | { className: 'body ' + _style2.default.body },
85 | msg.body
86 | ),
87 | _react2.default.createElement(
88 | 'div',
89 | null,
90 | user && user.name ? _react2.default.createElement(
91 | 'span',
92 | { className: _style2.default.user_name },
93 | user.name
94 | ) : null,
95 | _react2.default.createElement(
96 | 'span',
97 | { className: _style2.default.time },
98 | ' at ' + formatTime(props.updated_at)
99 | )
100 | )
101 | );
102 | };
103 |
104 | // TODO render only topic in collapsed mode
105 |
106 | var threadPropNames = ['avatarSize', 'iconSet', 'fetchUser', 'sendMessage', 'updateMessage', 'onSelect', 'onAction', 'canExecute', 'theme'];
107 |
108 | function ThreadList(props) {
109 | var className = (0, _classnames2.default)(_style2.default.thread_list);
110 | // TODO use propTypes of Thread component
111 | var options = _lodash2.default.pick.apply(_lodash2.default, [props].concat(threadPropNames));
112 | var items = _lodash2.default.map(props.threads, function (t) {
113 | return _react2.default.createElement(_thread2.default, _extends({ key: t.id }, t, options));
114 | });
115 | return _react2.default.createElement(
116 | 'div',
117 | { className: className },
118 | items
119 | );
120 | }
121 |
122 | ThreadList.defaultProps = {
123 | theme: 'plain'
124 | };
--------------------------------------------------------------------------------
/lib/thread/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .thread {
4 | padding: 4px;
5 | }
6 |
7 | .day {
8 | padding: 0px 0px 10px 25px;
9 | color: #bbb;
10 | }
11 |
12 | .day a {
13 | cursor: pointer;
14 | color: #bbb;
15 | }
16 |
17 | .thread_header {
18 | cursor: pointer;
19 | display: block;
20 | font-weight: bold;
21 | padding: 4px 0px;
22 | }
23 |
24 | .thread_header a {
25 | cursor: pointer;
26 | outline: none;
27 | }
28 |
29 | .topic_selected {
30 | color: #db6b9a;
31 | }
32 |
33 | .header {
34 | font-weight: bold;
35 | }
36 |
37 | .unread {
38 | color: #36a5d9;
39 | margin-left: 6px;
40 | border: 1px solid #9acde4;
41 | border-radius: 4px;
42 | padding: 1px 4px 0;
43 | display: inline-block;
44 | line-height: 15px;
45 | font-weight: 400;
46 | font-size: 11px;
47 | }
48 |
49 | .user_name {
50 | color: #bbb;
51 | font-weight: 400;
52 | font-size: 11px;
53 | }
54 |
55 | .time {
56 | color: #bbb;
57 | font-weight: 400;
58 | font-size: 11px;
59 | }
60 |
61 | .body {
62 | }
63 |
64 | .subject_input {
65 | width: 100%;
66 | resize: none;
67 | }
68 |
--------------------------------------------------------------------------------
/lib/thread/threadform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _classnames = require('classnames');
14 |
15 | var _classnames2 = _interopRequireDefault(_classnames);
16 |
17 | var _common = require('../common');
18 |
19 | var _messageinput = require('../message/messageinput');
20 |
21 | var _messageinput2 = _interopRequireDefault(_messageinput);
22 |
23 | var _style = require('./style.scss');
24 |
25 | var _style2 = _interopRequireDefault(_style);
26 |
27 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28 |
29 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
30 |
31 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
32 |
33 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
34 |
35 | var ThreadForm = function (_Component) {
36 | _inherits(ThreadForm, _Component);
37 |
38 | function ThreadForm(props) {
39 | _classCallCheck(this, ThreadForm);
40 |
41 | var _this = _possibleConstructorReturn(this, (ThreadForm.__proto__ || Object.getPrototypeOf(ThreadForm)).call(this, props));
42 |
43 | _this.onSubjectFocus = function () {
44 | _this.setState({ subjectFocused: true });
45 | };
46 |
47 | _this.onSubjectBlur = function () {
48 | _this.setState({ subjectFocused: false });
49 | };
50 |
51 | _this.onBodyFocus = function () {
52 | _this.setState({ bodyFocused: true });
53 | };
54 |
55 | _this.onBodyBlur = function () {
56 | _this.setState({ bodyFocused: false });
57 | };
58 |
59 | _this.onSubjectChange = function (event) {
60 | var value = event.target.value || '';
61 | _this.setState({ subject: value });
62 | };
63 |
64 | _this.onBodyChange = function (body) {
65 | _this.setState({ body: body });
66 | };
67 |
68 | _this.state = {
69 | subject: '',
70 | body: '',
71 | subjectFocused: false,
72 | bodyFocused: false
73 | };
74 | return _this;
75 | }
76 |
77 | _createClass(ThreadForm, [{
78 | key: 'render',
79 | value: function render() {
80 | var _this2 = this;
81 |
82 | var props = this.props;
83 | var canSubmit = function canSubmit() {
84 | var _state = _this2.state,
85 | subject = _state.subject,
86 | body = _state.body;
87 |
88 | return subject.length > 0 && body.length > 0;
89 | };
90 | var submit = function submit() {
91 | var subject = _this2.state.subject;
92 | var body = _this2.state.body;
93 | if (!subject || !body) {
94 | // TODO show validation errors
95 | return;
96 | }
97 | _this2.setState({ subject: '', body: '' });
98 | props.submit({ subject: subject, body: body });
99 | };
100 | var subjectProps = {
101 | className: (0, _classnames2.default)(_style2.default.input, _style2.default.subject_input),
102 | rows: 1,
103 | placeholder: 'Subject',
104 | value: this.state.subject,
105 | onChange: this.onSubjectChange,
106 | onFocus: this.onSubjectFocus,
107 | onBlur: this.onSubjectBlur,
108 | focused: this.state.subjectFocused
109 | };
110 | var bodyProps = {
111 | placeholder: 'Write your message here...',
112 | canSubmit: canSubmit,
113 | submit: submit,
114 | value: this.state.body,
115 | onChange: this.onBodyChange,
116 | onFocus: this.onBodyFocus,
117 | onBlur: this.onBodyBlur,
118 | formStyle: {
119 | margin: '0px',
120 | padding: '0px'
121 | },
122 | focused: this.state.bodyFocused
123 | };
124 | var formProps = {
125 | className: _style2.default.thread_form,
126 | style: { marginBottom: '24px' }
127 | };
128 | if (this.state.subjectFocused || this.state.bodyFocused) {
129 | return _react2.default.createElement(
130 | 'div',
131 | formProps,
132 | _react2.default.createElement(_common.Input, subjectProps),
133 | _react2.default.createElement(_messageinput2.default, bodyProps)
134 | );
135 | }
136 | var placeholderProps = {
137 | className: (0, _classnames2.default)(_style2.default.input, _style2.default.subject_input),
138 | rows: 1,
139 | placeholder: 'Start a new topic...',
140 | onFocus: this.onSubjectFocus
141 | };
142 | return _react2.default.createElement(
143 | 'div',
144 | formProps,
145 | _react2.default.createElement(_common.Input, placeholderProps)
146 | );
147 | }
148 | }]);
149 |
150 | return ThreadForm;
151 | }(_react.Component);
152 |
153 | exports.default = ThreadForm;
--------------------------------------------------------------------------------
/lib/thread/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.getMsgDay = undefined;
7 | exports.getDay = getDay;
8 | exports.getDayMessages = getDayMessages;
9 | exports.countMessages = countMessages;
10 | exports.collectContributors = collectContributors;
11 |
12 | var _moment = require('moment');
13 |
14 | var _moment2 = _interopRequireDefault(_moment);
15 |
16 | var _lodash = require('lodash');
17 |
18 | var _lodash2 = _interopRequireDefault(_lodash);
19 |
20 | var _message = require('../message');
21 |
22 | var _util = require('../util');
23 |
24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25 |
26 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
27 |
28 | function getDay(time) {
29 | var m = (0, _moment2.default)(time);
30 | return m.isValid ? m.year() + m.dayOfYear() : -1;
31 | }
32 |
33 | var getMsgDay = exports.getMsgDay = function getMsgDay(msg) {
34 | return getDay((0, _message.getTime)(msg));
35 | };
36 |
37 | function getDayMessages(messages, start) {
38 | var msg = messages[start];
39 | var result = [msg];
40 | var day = getMsgDay(msg);
41 | for (var i = start + 1; i < messages.length; i += 1) {
42 | if (day !== getMsgDay(messages[i])) {
43 | i -= 1;
44 | break;
45 | }
46 | result.push(messages[i]);
47 | }
48 | return result;
49 | }
50 |
51 | function countMessages(messages) {
52 | var countIt = function countIt(m) {
53 | var n = 1;
54 | if (Array.isArray(m.replies)) {
55 | n += m.replies.reduce(function (a, b) {
56 | return a + countIt(b);
57 | }, 0);
58 | }
59 | return n;
60 | };
61 | return messages.reduce(function (a, m) {
62 | return a + countIt(m);
63 | }, 0);
64 | }
65 |
66 | function collectContributors(users, messages, fetchUser) {
67 | function push(user) {
68 | var arr = users();
69 | if (_lodash2.default.find(arr, function (u) {
70 | return u.id === user.id;
71 | })) return;
72 | users([].concat(_toConsumableArray(arr), [user]));
73 | }
74 | messages.forEach(function (m) {
75 | if (_lodash2.default.isObject(m.user)) {
76 | push(m.user);
77 | } else if (m.fetchUser || fetchUser) {
78 | var promise = (0, _util.toPromise)((0, _util.promiseOnce)(m.fetchUser || fetchUser, m));
79 | if (promise) {
80 | promise.then(push);
81 | }
82 | }
83 | if (_lodash2.default.isArray(m.replies)) {
84 | collectContributors(users, m.replies, fetchUser);
85 | }
86 | });
87 | }
--------------------------------------------------------------------------------
/lib/user/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _list = require('./list');
8 |
9 | Object.defineProperty(exports, 'default', {
10 | enumerable: true,
11 | get: function get() {
12 | return _interopRequireDefault(_list).default;
13 | }
14 | });
15 | Object.defineProperty(exports, 'UserList', {
16 | enumerable: true,
17 | get: function get() {
18 | return _interopRequireDefault(_list).default;
19 | }
20 | });
21 |
22 | var _menu = require('./menu');
23 |
24 | Object.defineProperty(exports, 'UserMenu', {
25 | enumerable: true,
26 | get: function get() {
27 | return _interopRequireDefault(_menu).default;
28 | }
29 | });
30 |
31 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
--------------------------------------------------------------------------------
/lib/user/list.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8 |
9 | exports.UserList = UserList;
10 |
11 | var _react = require('react');
12 |
13 | var _react2 = _interopRequireDefault(_react);
14 |
15 | var _avatar = require('../avatar');
16 |
17 | var _avatar2 = _interopRequireDefault(_avatar);
18 |
19 | var _style = require('./style.scss');
20 |
21 | var _style2 = _interopRequireDefault(_style);
22 |
23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
24 |
25 | function UserList(props) {
26 | var users = props.users || [];
27 | var online = users.filter(function (u) {
28 | return !!u.online;
29 | }).length;
30 | var items = users.map(function (user) {
31 | var avatarProps = {
32 | user: user,
33 | className: _style2.default.user_item,
34 | hover: 'grow',
35 | shape: 'round_rect',
36 | size: 32,
37 | style: {
38 | margin: 0
39 | }
40 | };
41 | return _react2.default.createElement(_avatar2.default, _extends({ key: user.id }, avatarProps));
42 | });
43 | return _react2.default.createElement(
44 | 'div',
45 | { className: _style2.default.user_list },
46 | _react2.default.createElement(
47 | 'div',
48 | { className: _style2.default.user_list_header },
49 | _react2.default.createElement('i', { className: 'ion-ios-people' }),
50 | _react2.default.createElement(
51 | 'span',
52 | null,
53 | 'Online'
54 | ),
55 | _react2.default.createElement(
56 | 'em',
57 | { className: _style2.default.online_count },
58 | online
59 | )
60 | ),
61 | _react2.default.createElement(
62 | 'div',
63 | { className: _style2.default.user_list_body },
64 | items
65 | )
66 | );
67 | }
68 |
69 | exports.default = UserList;
--------------------------------------------------------------------------------
/lib/user/menu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _avatar = require('../avatar');
14 |
15 | var _avatar2 = _interopRequireDefault(_avatar);
16 |
17 | var _common = require('../common');
18 |
19 | var _menuStyle = require('./menuStyle.scss');
20 |
21 | var _menuStyle2 = _interopRequireDefault(_menuStyle);
22 |
23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
24 |
25 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
26 |
27 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
28 |
29 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
30 |
31 | // TODO use UserName component to render user name
32 |
33 | var UserMenu = function (_Component) {
34 | _inherits(UserMenu, _Component);
35 |
36 | function UserMenu() {
37 | _classCallCheck(this, UserMenu);
38 |
39 | return _possibleConstructorReturn(this, (UserMenu.__proto__ || Object.getPrototypeOf(UserMenu)).apply(this, arguments));
40 | }
41 |
42 | _createClass(UserMenu, [{
43 | key: 'renderContent',
44 | value: function renderContent(user) {
45 | var avatarProps = {
46 | className: _menuStyle2.default.user_avatar,
47 | user: user,
48 | size: 32,
49 | circled: true,
50 | style: {
51 | margin: 0
52 | }
53 | };
54 | var menuProps = {
55 | button: {
56 | content: _react2.default.createElement(
57 | 'span',
58 | null,
59 | '\xA0',
60 | _react2.default.createElement('i', { className: 'fa fa-caret-down' })
61 | )
62 | }
63 | };
64 | return _react2.default.createElement(
65 | 'span',
66 | null,
67 | _react2.default.createElement(_avatar2.default, avatarProps),
68 | '\xA0',
69 | user.name || user.login,
70 | _react2.default.createElement(
71 | _common.ContextMenu,
72 | menuProps,
73 | this.props.children
74 | )
75 | );
76 | }
77 | }, {
78 | key: 'render',
79 | value: function render() {
80 | var props = this.props;
81 | var user = props.user;
82 | return _react2.default.createElement(
83 | 'div',
84 | { className: _menuStyle2.default.user_menu },
85 | user ? this.renderContent(user) : null
86 | );
87 | }
88 | }]);
89 |
90 | return UserMenu;
91 | }(_react.Component);
92 |
93 | exports.default = UserMenu;
--------------------------------------------------------------------------------
/lib/user/menuStyle.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .user_menu {
4 | display: inline-block;
5 | }
6 |
7 | .user_menu .user_avatar {
8 | float: none;
9 | margin: 0;
10 | }
11 |
--------------------------------------------------------------------------------
/lib/user/style.scss:
--------------------------------------------------------------------------------
1 | @import "../_base";
2 |
3 | .user_item {
4 | display: inline-block;
5 | }
6 |
7 | .user_list_body {
8 | max-width: 5 * 40px;
9 | }
10 |
11 | .user_list_header i {
12 | font-size: 24px;
13 | margin: 4px;
14 | }
15 |
16 | .user_list_header span {
17 | font-size: 20px;
18 | }
19 |
20 | .online_count {
21 | display: inline-block;
22 | margin: -.3em 0 0 .3em;
23 | width: 1.6em;
24 | height: 1.6em;
25 | border-radius: 50%;
26 | background-color: #4cbe00;
27 | text-align: center;
28 | vertical-align: middle;
29 | color: white;
30 | line-height: 1.7;
31 | font-style: normal;
32 | font-weight: bold;
33 | }
34 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.isPromise = isPromise;
7 | exports.promiseOnce = promiseOnce;
8 | exports.toPromise = toPromise;
9 | exports.firstOrDefault = firstOrDefault;
10 | exports.getOrFetch = getOrFetch;
11 |
12 | var _lodash = require('lodash');
13 |
14 | var _lodash2 = _interopRequireDefault(_lodash);
15 |
16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17 |
18 | function isPromise(value) {
19 | return value && _lodash2.default.isFunction(value.then);
20 | }
21 |
22 | function promiseOnce(fn, data) {
23 | if (!fn) return fn;
24 | var resolved = void 0;
25 | return function () {
26 | if (resolved) return resolved;
27 | if (isPromise(fn)) {
28 | return resolved = fn;
29 | }
30 | var t = fn(data);
31 | resolved = isPromise(t) ? t : Promise.resolve(t);
32 | return resolved;
33 | };
34 | }
35 |
36 | function toPromise(value) {
37 | if (isPromise(value)) {
38 | return value;
39 | }
40 | if (!_lodash2.default.isFunction(value)) {
41 | return null;
42 | }
43 | var p = value();
44 | return isPromise(p) ? p : null;
45 | }
46 |
47 | function firstOrDefault(obj) {
48 | if (!obj) return null;
49 |
50 | for (var _len = arguments.length, keys = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
51 | keys[_key - 1] = arguments[_key];
52 | }
53 |
54 | for (var i = 0; i < keys.length; i++) {
55 | var val = obj[keys[i]];
56 | if (val) return val;
57 | }
58 | return null;
59 | }
60 |
61 | function getOrFetch(fetch, obj) {
62 | for (var _len2 = arguments.length, keys = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
63 | keys[_key2 - 2] = arguments[_key2];
64 | }
65 |
66 | var val = firstOrDefault.apply(undefined, [obj].concat(keys));
67 | if (val) return val;
68 | var promise = toPromise(fetch);
69 | if (promise) {
70 | return promise.then(function (t) {
71 | return firstOrDefault.apply(undefined, [t].concat(keys));
72 | });
73 | }
74 | return null;
75 | }
--------------------------------------------------------------------------------
/lint.cmd:
--------------------------------------------------------------------------------
1 | npm run lint
2 |
--------------------------------------------------------------------------------
/make.cmd:
--------------------------------------------------------------------------------
1 | set NODE_ENV=production
2 | mkdir /q lib
3 | cpx ".\components\**\*.scss" .\lib && babel components --out-dir lib
4 |
--------------------------------------------------------------------------------
/makesite.sh:
--------------------------------------------------------------------------------
1 | npm run build:demo
2 | mkdir -p site
3 | cp -a ./static/. ./site/static
4 | cp -a ./index.html ./site/
5 | # fix absolute links
6 | sed -i -- "s/\/static\//\.&/g" ./site/index.html
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fiber",
3 | "version": "0.2.101",
4 | "description": "React components to use in messaging applications.",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "start": "node devserver.js",
8 | "lint": "eslint --ext .js,.jsx .",
9 | "sass": "cpx './components/**/*.scss' ./lib",
10 | "babel": "babel components --out-dir lib",
11 | "build": "NODE_ENV=production npm run babel && npm run sass",
12 | "build:demo": "webpack --config webpack.config.js",
13 | "test": "npm run lint"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/reactbits/fiber.git"
18 | },
19 | "keywords": [
20 | "react",
21 | "chat",
22 | "email"
23 | ],
24 | "author": "Sergey Todyshev ",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/reactbits/fiber/issues"
28 | },
29 | "homepage": "https://github.com/reactbits/fiber#readme",
30 | "dependencies": {
31 | "classnames": "^2.2.5",
32 | "component-upload": "^0.3.0",
33 | "css-effects": "^0.1.4",
34 | "is_js": "^0.9.0",
35 | "jquery": "^3.1.1",
36 | "loaders.css": "^0.1.2",
37 | "lodash": "^4.17.4",
38 | "md5": "^2.2.1",
39 | "moment": "^2.17.1",
40 | "observable": "^2.1.4",
41 | "query-string": "^4.3.1",
42 | "react": "^15.4.2",
43 | "react-bootstrap": "^0.30.7",
44 | "react-dom": "^15.4.2",
45 | "react-imageloader": "^2.1.0",
46 | "react-markdown2": "^0.11.6",
47 | "react-popover": "^0.4.4",
48 | "react-router": "^3.0.2",
49 | "reactbits-input": "^0.0.24",
50 | "reflexbox": "^2.2.3",
51 | "tus-js-client": "^1.4.2"
52 | },
53 | "devDependencies": {
54 | "bootstrap": "^3.3.7",
55 | "make-reducer": "^0.5.1",
56 | "react-devpack": "^0.2.16",
57 | "react-redux": "^5.0.2",
58 | "redux": "^3.6.0",
59 | "redux-devtools": "^3.3.2",
60 | "redux-devtools-dock-monitor": "^1.1.1",
61 | "redux-devtools-log-monitor": "^1.2.0",
62 | "sweetalert": "^1.1.3",
63 | "tus-js-client": "^1.4.2",
64 | "update-it": "0.2.2"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const makeWebpackConfig = require('react-devpack').makeWebpackConfig;
2 |
3 | module.exports = makeWebpackConfig({
4 | entry: './demo/index',
5 | resolve: {
6 | alias: {
7 | 'dev/raphael.core.js': './dev/raphael.core.js',
8 | 'raphael.core': './raphael.core.js',
9 | 'raphael.svg': './dev/raphael.svg.js',
10 | 'raphael.vml': './dev/raphael.vml.js',
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------