Skip to content
This repository has been archived by the owner on Aug 22, 2022. It is now read-only.

Channel UI code refactoring #36

Closed
wants to merge 12 commits into from
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"file-loader": "^0.8.5",
"halogen": "^0.2.0",
"highlight.js": "^9.4.0",
"ipfs": "^0.11.0",
"ipfs": "^0.13.0",
"lodash": "^4.12.0",
"logplease": "^1.2.7",
"normalize.css": "~4.1.1",
Expand Down
152 changes: 66 additions & 86 deletions client/src/components/Channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@

import _ from 'lodash';
import React from 'react';
import TransitionGroup from "react-addons-css-transition-group";
import Message from 'components/Message';
import SendMessage from 'components/SendMessage';
import ChannelControls from 'components/ChannelControls';
import NewMessageNotification from 'components/NewMessageNotification';
import Dropzone from 'react-dropzone';
import MessageStore from 'stores/MessageStore';
import ChannelStore from 'stores/ChannelStore';
import LoadingStateStore from 'stores/LoadingStateStore';
import UIActions from 'actions/UIActions';
import ChannelActions from 'actions/ChannelActions';
import Halogen from 'halogen';
import 'styles/Channel.scss';
import Logger from 'logplease';
const logger = Logger.create('Channel', { color: Logger.Colors.Cyan });
Expand All @@ -31,7 +30,6 @@ class Channel extends React.Component {
error: null,
dragEnter: false,
username: props.user ? props.user.username : '',
displayNewMessagesIcon: false,
unreadMessages: 0,
appSettings: props.appSettings,
theme: props.theme
Expand All @@ -48,7 +46,7 @@ class Channel extends React.Component {
if(nextProps.channel !== this.state.channelName) {
this.setState({
channelChanged: true,
displayNewMessagesIcon: false,
unreadMessages: 0,
loading: true,
reachedChannelStart: false,
messages: []
Expand Down Expand Up @@ -158,7 +156,6 @@ class Channel extends React.Component {
&& this.state.messages.length > 0 && _.last(messages).payload.meta.ts > _.last(this.state.messages).payload.meta.ts
&& this.node.scrollHeight > 0) {
this.setState({
displayNewMessagesIcon: true,
unreadMessages: this.state.unreadMessages + 1
});
}
Expand All @@ -171,9 +168,9 @@ class Channel extends React.Component {
ChannelActions.sendMessage(this.state.channelName, text);
}

sendFile(filePath: string, buffer) {
sendFile(filePath: string, buffer, mimeType = 'application/octet-binary') {
Copy link
Contributor Author

@claus claus Jun 26, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to have an optional mime type stored in file post metadata (ipfs-post would need to support it). Maybe even allow to pass in further metadata as a generic object?

Copy link
Member

@haadcode haadcode Jul 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add the mimeType to ipfs-post which is what defines the data structure for each message: https://github.com/haadcode/ipfs-post/blob/master/src/FilePost.js and https://github.com/haadcode/ipfs-post/blob/master/src/Post.js.

Copy link
Contributor Author

@claus claus Jul 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to add that or should i? I'm not sure if we should have an explicit mimeType property or just a more generic object that we can use for more meta data like encoding and whatever else might be useful in the future, i.e. { name, hash, size, meta = { mimeType, encoding } } vs. { name, hash, size, mimeType }

Copy link
Member

@haadcode haadcode Jul 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely prefer the proposed meta field. Added it to ipfs-post (orbitdb-archive/ipfs-post@1224898).

You can use it by adding meta field to you data object in sendFile()

const data = {
  ...
  meta: { encoding: ..., ... }
}

if(filePath !== '' || buffer !== null)
ChannelActions.addFile(this.state.channelName, filePath, buffer);
ChannelActions.addFile(this.state.channelName, filePath, buffer, mimeType);
}

loadOlderMessages() {
Expand Down Expand Up @@ -230,7 +227,7 @@ class Channel extends React.Component {
const reader = new FileReader();
reader.onload = (event) => {
console.log(file, event);
this.sendFile(file.name, event.target.result)
this.sendFile(file.name, event.target.result, file.type);
};
reader.readAsArrayBuffer(file);
// console.error("File upload not yet implemented in browser. Try the electron app.");
Expand Down Expand Up @@ -261,7 +258,9 @@ class Channel extends React.Component {
// If we scrolled to the bottom, hide the "new messages" label
this.node = this.refs.MessagesView;
if(this.node.scrollHeight - this.node.scrollTop - 10 <= this.node.clientHeight) {
this.setState({ displayNewMessagesIcon: false, unreadMessages: 0 });
this.setState({
unreadMessages: 0
});
}
}

Expand All @@ -270,89 +269,70 @@ class Channel extends React.Component {
this.node.scrollTop = this.node.scrollHeight + this.node.clientHeight;
}

render() {
const theme = this.state.theme;
const channelMode = (<div className={"statusMessage"} style={theme}>{this.state.channelMode}</div>);

const controlsBar = (
<TransitionGroup
component="div"
transitionName="controlsAnimation"
transitionAppear={true}
transitionAppearTimeout={1000}
transitionEnterTimeout={0}
transitionLeaveTimeout={0}
>
<div className="Controls" key="controls">
<SendMessage onSendMessage={this.sendMessage.bind(this)} key="SendMessage" theme={theme} useEmojis={this.state.appSettings.useEmojis}/>
<Dropzone className="dropzone2" onDrop={this.onDrop.bind(this)} key="dropzone2">
<div className="icon flaticon-tool490" style={theme} key="icon"/>
</Dropzone>
</div>
</TransitionGroup>
);

const messages = this.state.messages.map((e) => {
return <Message
message={e.payload}
key={e.hash}
onDragEnter={this.onDragEnter.bind(this)}
highlightWords={this.state.username}
colorifyUsername={this.state.appSettings.colorifyUsernames}
useEmojis={this.state.appSettings.useEmojis}
style={{
fontFamily: this.state.appSettings.useMonospaceFont ? this.state.appSettings.monospaceFont : this.state.appSettings.font,
fontSize: this.state.appSettings.useMonospaceFont ? '0.9em' : '1.0em',
fontWeight: this.state.appSettings.useMonospaceFont ? '100' : '300',
padding: this.state.appSettings.spacing,
}}
/>;
});

// let channelStateText = this.state.loading && this.state.loadingText ? this.state.loadingText : `Loading messages...`;
let channelStateText = this.state.loadingText ? this.state.loadingText : `???`;
if(this.state.reachedChannelStart && !this.state.loading)
channelStateText = `Beginning of #${this.state.channelName}`;

messages.unshift(<div className="firstMessage" key="firstMessage" onClick={this.loadOlderMessages.bind(this)}>{channelStateText}</div>);

const fileDrop = this.state.dragEnter ? (
<Dropzone
className="dropzone"
activeClassName="dropzoneActive"
disableClick={true}
onDrop={this.onDrop.bind(this)}
renderMessages() {
const { messages, username, channelName, loading, loadingText, reachedChannelStart, appSettings } = this.state;
const { colorifyUsernames, useEmojis, useMonospaceFont, font, monospaceFont, spacing } = appSettings;
const elements = messages.map(message => (
<Message
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, let's use parenthesis around function parameters to make it explicit that it's a function:

const elements = messages.map((message) => ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f94ccb4

message={message.payload}
key={message.hash}
onDragEnter={this.onDragEnter.bind(this)}
onDragLeave={this.onDragLeave.bind(this)}
style={theme}
key="dropzone"
>
<div ref="dropLabel" style={theme}>Add files to #{this.state.channelName}</div>
</Dropzone>
) : "";

const showNewMessageNotification = this.state.displayNewMessagesIcon ? (
<div className="newMessagesBar" onClick={this.onScrollToBottom.bind(this)}>
There are <span className="newMessagesNumber">{this.state.unreadMessages}</span> new messages
highlightWords={username}
colorifyUsername={colorifyUsernames}
useEmojis={useEmojis}
style={{
fontFamily: useMonospaceFont ? monospaceFont : font,
fontSize: useMonospaceFont ? '0.9em' : '1.0em',
fontWeight: useMonospaceFont ? '100' : '300',
padding: spacing,
}}
/>
));
elements.unshift(
<div className="firstMessage" key="firstMessage" onClick={this.loadOlderMessages.bind(this)}>
{reachedChannelStart && !loading ? `Beginning of #${channelName}` : loadingText || '???'}
</div>
) : (<span></span>);
);
return elements;
}

const loadingIcon = this.state.loading ? (
<div className="loadingBar">
<Halogen.MoonLoader className="loadingIcon" color="rgba(255, 255, 255, 0.7)" size="16px"/>
</div>
) : "";
renderFileDrop() {
const { theme, dragEnter, channelName } = this.state;
if (dragEnter) {
return (
<Dropzone
className="dropzone"
activeClassName="dropzoneActive"
disableClick={true}
onDrop={this.onDrop.bind(this)}
onDragEnter={this.onDragEnter.bind(this)}
onDragLeave={this.onDragLeave.bind(this)}
style={theme} >
<div ref="dropLabel" style={theme}>Add files to #{channelName}</div>
</Dropzone>
);
}
return null;
}

render() {
const { unreadMessages, loading, channelMode, appSettings, theme } = this.state;
return (
<div className="Channel flipped" onDragEnter={this.onDragEnter.bind(this)}>
<div className="Messages" ref="MessagesView" onScroll={this.onScroll.bind(this)} >
{messages}
<div className="Messages" ref="MessagesView" onScroll={this.onScroll.bind(this)}>
{this.renderMessages()}
</div>
{showNewMessageNotification}
{controlsBar}
{fileDrop}
{loadingIcon}
{channelMode}
<NewMessageNotification
onClick={this.onScrollToBottom.bind(this)}
unreadMessages={unreadMessages} />
<ChannelControls
onSendMessage={this.sendMessage.bind(this)}
onSendFiles={this.onDrop.bind(this)}
isLoading={loading}
channelMode={channelMode}
appSettings={appSettings}
theme={theme} />
{this.renderFileDrop()}
</div>
);
}
Expand Down
46 changes: 46 additions & 0 deletions client/src/components/ChannelControls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';

import React, { PropTypes } from 'react';
import TransitionGroup from "react-addons-css-transition-group";
import Dropzone from 'react-dropzone';
import Halogen from 'halogen';
import Spinner from 'components/Spinner';
import SendMessage from 'components/SendMessage';

class ChannelControls extends React.Component {

static propTypes = {
onSendMessage: PropTypes.func,
onSendFiles: PropTypes.func,
isLoading: PropTypes.bool,
channelMode: PropTypes.string,
appSettings: PropTypes.object,
theme: PropTypes.object,
};

render() {
const { onSendMessage, onSendFiles, isLoading, channelMode, appSettings, theme } = this.props;
return (
<TransitionGroup
component="div"
transitionName="controlsAnimation"
transitionAppear={true}
transitionAppearTimeout={1000}
transitionEnterTimeout={0}
transitionLeaveTimeout={0}
>
<div className="Controls" key="controls">
<Spinner isLoading={isLoading} color="rgba(255, 255, 255, 0.7)" size="16px" />
<SendMessage onSendMessage={onSendMessage} theme={theme} useEmojis={appSettings.useEmojis} />
<Dropzone className="dropzone2" onDrop={onSendFiles}>
<div className="icon flaticon-tool490" style={theme} />
</Dropzone>
<div className="statusMessage" style={theme}>{channelMode}</div>
</div>
</TransitionGroup>
);
}

}

export default ChannelControls;
Loading