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(
+
+ {reachedChannelStart && !loading ? `Beginning of #${channelName}` : loadingText || '???'}
);
+ );
+ return elements;
+ }
- const loadingIcon = this.state.loading ? (
-
- ) : "";
+ renderFileDrop() {
+ const { theme, dragEnter, channelName } = this.state;
+ if (dragEnter) {
+ return (
+
+ );
+ }
+ return null;
+ }
+ render() {
+ const { unreadMessages, loading, channelMode, appSettings, theme } = this.state;
return (
-
- {messages}
+
+ {this.renderMessages()}
- {showNewMessageNotification}
- {controlsBar}
- {fileDrop}
- {loadingIcon}
- {channelMode}
+
+
+ {this.renderFileDrop()}
);
}
diff --git a/client/src/components/ChannelControls.js b/client/src/components/ChannelControls.js
new file mode 100644
index 0000000..3c21b39
--- /dev/null
+++ b/client/src/components/ChannelControls.js
@@ -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 (
+
+
+
+
+
+
+
+
{channelMode}
+
+
+ );
+ }
+
+}
+
+export default ChannelControls;
diff --git a/client/src/components/ChannelsPanel.js b/client/src/components/ChannelsPanel.js
index 3b76572..3db6b2e 100644
--- a/client/src/components/ChannelsPanel.js
+++ b/client/src/components/ChannelsPanel.js
@@ -8,7 +8,7 @@ import ChannelStore from 'stores/ChannelStore';
import AppStateStore from 'stores/AppStateStore';
import NetworkActions from 'actions/NetworkActions';
import BackgroundAnimation from 'components/BackgroundAnimation'; //eslint-disable-line
-import Halogen from 'halogen'; //eslint-disable-line
+import Spinner from 'components/Spinner';
import 'styles/ChannelsPanel.scss';
import 'styles/RecentChannels.scss';
@@ -87,48 +87,34 @@ class ChannelsPanel extends React.Component {
}
render() {
- var headerStyle = this.state.currentChannel ? "header" : "header no-close";
- var color = 'rgba(140, 80, 220, 1)';
- var loadingIcon = this.state.loading ? (
-
-
-
- ) : "";
-
- var channelsHeaderStyle = this.state.openChannels.length > 0 ? "panelHeader" : "hidden";
- var openChannels = this.state.openChannels.length > 0 ? this.state.openChannels.map((f) => this._renderChannel(f)) : [];
- var channelJoinInputStyle = !this.state.loading ? "joinChannelInput" : "joinChannelInput invisible";
+ const headerClass = this.state.currentChannel ? "header" : "header no-close";
+ const channelsHeaderClass = this.state.openChannels.length > 0 ? "panelHeader" : "hidden";
+ const channelJoinInputClass = !this.state.loading ? "joinChannelInput" : "joinChannelInput invisible";
+
+ const openChannels = this.state.openChannels.map((channel) => this._renderChannel(channel));
+
+ const transitionProps = {
+ component: 'div',
+ transitionAppear: true,
+ transitionAppearTimeout: 5000,
+ transitionEnterTimeout: 5000,
+ transitionLeaveTimeout: 5000,
+ };
return (
);
}
diff --git a/client/src/components/File.js b/client/src/components/File.js
index ff8ad6f..1cd2809 100644
--- a/client/src/components/File.js
+++ b/client/src/components/File.js
@@ -2,169 +2,137 @@
import React from 'react';
import TransitionGroup from "react-addons-css-transition-group";
-import { getHumanReadableBytes } from '../utils/utils.js';
-import apiurl from 'utils/apiurl';
-import 'styles/File.scss';
-import Highlight from 'components/plugins/highlight';
+import Clipboard from 'clipboard';
import highlight from 'highlight.js'
+import Highlight from 'components/plugins/highlight';
+import { getHumanReadableBytes } from '../utils/utils.js';
import ChannelActions from 'actions/ChannelActions';
-import Clipboard from 'clipboard';
-
import MessageStore from 'stores/MessageStore';
-
+import 'styles/File.scss';
import Logger from 'logplease';
const logger = Logger.create('Clipboard', { color: Logger.Colors.Magenta });
-var getFileUrl = apiurl.getApiUrl() + "/api/cat/";
+function readUTF8String(bytes) {
+ let i = 0;
+ let string = '';
+
+ // Remove UTF-8 BOM header, if present
+ const UTF8_BOM_HEADER = '\xef\xbb\xbf';
+ if (bytes.slice(0, 3) == UTF8_BOM_HEADER) {
+ i = 3;
+ }
+
+ // Convert UTF-8 encoded codepoints to JS string
+ // See https://en.wikipedia.org/wiki/UTF-8#Description
+ for (; i < bytes.byteLength; i++) {
+ const byte1 = bytes[i];
+ if (byte1 < 0x80) {
+ // U+0000 .. U+007F
+ string += String.fromCharCode(byte1);
+ } else if (byte1 >= 0xc2 && byte1 < 0xe0) {
+ // U+0080 .. U+07FF
+ const byte2 = bytes[++i];
+ string += String.fromCharCode(((byte1 & 0x1f) << 6) + (byte2 & 0x3f));
+ } else if (byte1 >= 0xe0 && byte1 < 0xf0) {
+ // U+0800 .. U+FFFF
+ const byte2 = bytes[++i];
+ const byte3 = bytes[++i];
+ string += String.fromCharCode(((byte1 & 0xff) << 12) + ((byte2 & 0x3f) << 6) + (byte3 & 0x3f));
+ } else if (byte1 >= 0xf0 && byte1 < 0xf5) {
+ // U+10000 .. U+1FFFFF
+ const byte2 = bytes[++i];
+ const byte3 = bytes[++i];
+ const byte4 = bytes[++i];
+ const codepoint = (((byte1 & 0x07) << 18) + ((byte2 & 0x3f) << 12) + ((byte3 & 0x3f) << 6) + (byte4 & 0x3f)) - 0x10000;
+ string += String.fromCharCode((codepoint >> 10) + 0xd800, (codepoint & 0x3ff) + 0xdc00);
+ }
+ }
+
+ return string;
+}
class File extends React.Component {
+
constructor(props) {
super(props);
+ this.ext = /(?:\.([^.]+))?$/.exec(props.name)[1];
this.state = {
- name: props.name,
- file: props.hash,
- size: props.size,
showPreview: false,
previewContent: "Loading...",
};
- var self = this;
- this.clipboard = new Clipboard('.clipboard-' + this.state.file, {
+ this.clipboard = new Clipboard('.clipboard-' + props.hash, {
text: function(trigger) {
- logger.info(self.state.file + " copied to clipboard!");
- return self.state.file;
+ logger.info(props.hash + " copied to clipboard!");
+ return props.hash;
}
});
}
- handleClick(name) {
- const isCode = this._isTextFile(this.state.name);
-
- if(!this.state.showPreview && isCode) {
- ChannelActions.loadFile(this.state.file, (file) => {
- this.setState({ previewContent:
{file} });
- });
- } else {
- this.setState({ previewContent: "Loading..." });
- }
+ get isVideo() {
+ return this.ext === 'mp4' || this.ext === 'webm' || this.ext === 'ogv';
+ }
- function toArrayBuffer(buffer) {
- var ab = new ArrayBuffer(buffer.length);
- var view = new Uint8Array(ab);
- for (var i = 0; i < buffer.length; ++i) {
- view[i] = buffer[i];
- }
- return ab;
- }
+ get isAudio() {
+ return this.ext === 'mp3' || this.ext === 'ogg';
+ }
- if(!this.state.showPreview) {
- setTimeout(() => {
- var mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
- let source = new MediaSource();
- this.refs.video_tag.src = window.URL.createObjectURL(source);
-
- source.addEventListener('sourceopen', (e) => {
- let sourceBuffer = source.addSourceBuffer(mimeCodec);
- MessageStore.orbit.ipfs.files.cat(this.state.file, (err, res) => {
- let buf = [];
- sourceBuffer.addEventListener('updateend', () => {
- if(buf.length > 0)
- sourceBuffer.appendBuffer(buf.shift())
- });
- res
- .on('error', (err) => console.error(err))
- .on('data', (data) => {
- if(!sourceBuffer.updating)
- sourceBuffer.appendBuffer(toArrayBuffer(data));
- else
- buf.push(toArrayBuffer(data));
- })
- .on('end', () => {
- console.log("END!!!")
- setTimeout(() => {
- source.endOfStream()
- this.refs.video_tag.play();
- }, 100);
- })
- });
- }, false);
- }, 500);
- }
+ get isImage() {
+ return this.ext === 'png' || this.ext === 'jpg' || this.ext === 'gif';
+ }
- this.setState({ showPreview: !this.state.showPreview });
+ get isHighlightable() {
+ return highlight.getLanguage(this.ext);
}
- _isTextFile(name) {
- return highlight.getLanguage(this.state.name.split('.').pop());
+ handleClick(name) {
+ this.setState({
+ showPreview: !this.state.showPreview,
+ previewContent: 'Loading...',
+ }, () => {
+ if (this.state.showPreview) {
+ ChannelActions.loadFile(this.props.hash, (blob) => {
+ let previewContent = 'Unable to display file.';
+ if (blob) {
+ const url = window.URL.createObjectURL(blob);
+ if (this.isAudio) {
+ previewContent =
;
+ } else if (this.isImage) {
+ previewContent =
;
+ } else if (this.isVideo) {
+ previewContent =
;;
+ } else {
+ var fileReader = new FileReader();
+ fileReader.onload = (event) => {
+ const text = readUTF8String(new Uint8Array(event.target.result));
+ if (this.isHighlightable) {
+ previewContent =
{text};
+ } else {
+ previewContent = text;
+ }
+ this.setState({ previewContent });
+ };
+ fileReader.readAsArrayBuffer(blob);
+ return;
+ }
+ }
+ this.setState({ previewContent });
+ });
+ }
+ });
}
render() {
- var openLink = "http://localhost:8080/ipfs/" + this.state.file;
- var downloadLink = getFileUrl + this.state.file + "?name=" + this.state.name + "&action=download";
- var size = getHumanReadableBytes(this.state.size);
-
- var split = this.state.name.split('.');
- var isVideo = split[split.length - 1] === 'mp4' || split[split.length - 1] === 'webm';
- var isAudio = split[split.length - 1] === 'mp3';
- var isCode = this._isTextFile(this.state.name);
- var isPicture = split[split.length - 1] === 'png';
-
- var video;
- var audio;
- var code;
- var picture;
-
- if(isVideo)
- video = this.state.showPreview ?
:
;
- if(isAudio)
- audio = this.state.showPreview ?
:
;
- if(isCode)
- code = this.state.showPreview ?
{this.state.previewContent}
:
;
- if(isPicture)
- picture = this.state.showPreview ?
:
;
-
- const className = "clipboard-" + this.state.file + " download";
-
- var content =
- if(isVideo) {
- content =
- (
- {this.state.name}
- Open
- Download
- Hash
- {size}
- {video}
- )
- } else if(isAudio) {
- content =
- (
- {this.state.name}
- Open
- Download
- Hash
- {size}
- {audio}
- )
- } else if(isCode) {
- content =
- (
+ {this.state.previewContent}
+
+
- {this.state.name}
- Open
- Download
- Hash
- {size}
- {code}
- )
- } else if(isPicture) {
- content =
- (
- {this.state.name}
- Open
- Download
- Hash
- {size}
- {picture}
- )
- } else {
- content =
- (
- {this.state.name}
- Open
- Download
- Hash
- {size}
- )
- }
-
- return (
-
- {content}
+ className="content">
+
{this.props.name}
+ {/*
+
Open
+
Download
+ */}
+
Hash
+
{size}
+ {this.state.showPreview && preview}
+
);
}
diff --git a/client/src/components/Message.js b/client/src/components/Message.js
index c565375..6eeac93 100644
--- a/client/src/components/Message.js
+++ b/client/src/components/Message.js
@@ -1,7 +1,6 @@
'use strict';
import React from "react";
-import TransitionGroup from "react-addons-css-transition-group";
import MentionHighlighter from 'components/plugins/mention-highlighter';
import User from "components/User";
import File from "components/File";
@@ -9,67 +8,81 @@ import TextMessage from "components/TextMessage";
import Directory from "components/Directory";
import ChannelActions from 'actions/ChannelActions';
import NotificationActions from 'actions/NotificationActions';
+import { getFormattedTime } from '../utils/utils.js';
import "styles/Message.scss";
class Message extends React.Component {
+
constructor(props) {
super(props);
this.state = {
post: null,
hasHighlights: false,
isCommand: false,
+ formattedTime: getFormattedTime(props.message.meta.ts),
};
}
componentDidMount() {
ChannelActions.loadPost(this.props.message.value, (err, post) => {
- if(post && post.content) {
- if(post.content.startsWith('/me'))
- this.setState({ isCommand: true });
-
- post.content.split(" ").forEach((word) => {
+ const state = {
+ post: post
+ };
+ if (post && post.content) {
+ if (post.content.startsWith('/me')) {
+ state.isCommand = true;
+ }
+ post.content.split(' ').forEach((word) => {
const highlight = MentionHighlighter.highlight(word, this.props.highlightWords);
if(typeof highlight[0] !== 'string' && this.props.highlightWords !== post.meta.from) {
- this.setState({ hasHighlights: true });
- NotificationActions.mention(this.state.channelName, post.content);
+ state.hasHighlights = true;
+ NotificationActions.mention(this.state.channelName, post.content); // TODO: where does channelName come from?
}
});
}
- this.setState({ post: post });
+ this.setState(state);
});
}
- onDragEnter(event) {
- this.props.onDragEnter(event);
- }
-
- render() {
- const safeTime = (time) => ("0" + time).slice(-2);
- const date = new Date(this.props.message.meta.ts);
- const ts = safeTime(date.getHours()) + ":" + safeTime(date.getMinutes()) + ":" + safeTime(date.getSeconds());
-
- const className = this.state.hasHighlights ? "Message highlighted" : "Message";
- const contentClass = this.state.isCommand ? "Content command" : "Content";
-
- const post = this.state.post;
+ renderContent() {
+ const { highlightWords, useEmojis } = this.props;
+ const { isCommand, post } = this.state;
+ const contentClass = isCommand ? "Content command" : "Content";
let content = (
...
);
-
- if(post && post.meta.type === "text") {
- content =
;
- } else if(post && post.meta.type === "file") {
- content = ;
- } else if(post && post.meta.type === "directory") {
- content = ;
+ if (post) {
+ switch (post.meta.type) {
+ case 'text':
+ content = (
+
+ );
+ break;
+ case 'file':
+ content = ;
+ break;
+ case 'directory':
+ content = ;
+ break;
+ }
}
+ return {content}
;
+ }
+ render() {
+ const { message, colorifyUsername, style, onDragEnter } = this.props;
+ const { post, isCommand, hasHighlights, formattedTime } = this.state;
+ const className = hasHighlights ? "Message highlighted" : "Message";
return (
-
-
{ts}
-
- {content}
+
+ {formattedTime}
+
+ {this.renderContent()}
);
}
diff --git a/client/src/components/NewMessageNotification.js b/client/src/components/NewMessageNotification.js
new file mode 100644
index 0000000..f0034e1
--- /dev/null
+++ b/client/src/components/NewMessageNotification.js
@@ -0,0 +1,34 @@
+'use strict';
+
+import React, { PropTypes } from 'react';
+
+class NewMessageNotification extends React.Component {
+
+ static propTypes = {
+ onClick: PropTypes.func,
+ unreadMessages: PropTypes.number,
+ };
+
+ render() {
+ const { onClick, unreadMessages } = this.props;
+ if (unreadMessages > 0) {
+ if (unreadMessages === 1) {
+ return (
+
+ There is 1 new message
+
+ );
+ } else {
+ return (
+
+ There are {unreadMessages} new messages
+
+ );
+ }
+ }
+ return null;
+ }
+
+}
+
+export default NewMessageNotification;
diff --git a/client/src/components/Spinner.js b/client/src/components/Spinner.js
new file mode 100644
index 0000000..0353c15
--- /dev/null
+++ b/client/src/components/Spinner.js
@@ -0,0 +1,30 @@
+'use strict';
+
+import React, { PropTypes } from 'react';
+import Halogen from 'halogen';
+
+class Spinner extends React.Component {
+
+ static propTypes = {
+ className: PropTypes.string,
+ isLoading: PropTypes.bool,
+ color: PropTypes.string,
+ size: PropTypes.string,
+ };
+
+ static defaultProps = {
+ className: 'spinner',
+ };
+
+ render() {
+ const { className, isLoading, color, size } = this.props;
+ return (
+
+
+
+ );
+ }
+
+}
+
+export default Spinner;
diff --git a/client/src/stores/MessageStore.js b/client/src/stores/MessageStore.js
index 7ba2959..53f4b6e 100644
--- a/client/src/stores/MessageStore.js
+++ b/client/src/stores/MessageStore.js
@@ -300,11 +300,11 @@ const MessageStore = Reflux.createStore({
UIActions.stopLoading(channel, "send");
});
},
- onAddFile: function(channel: string, filePath: string, buffer) {
- logger.debug("--> add file: " + filePath + buffer !== null);
- this.orbit.addFile(channel, filePath, buffer);
-
- if(this.socket) {
+ onAddFile: function(channel: string, filePath: string, buffer, meta) {
+ logger.debug(`--> add file: ${channel} ${filePath} ${buffer !== null}`);
+ if(!this.socket) {
+ this.orbit.addFile(channel, filePath, buffer, meta);
+ } else {
UIActions.startLoading(channel, "file");
this.socket.emit('file.add', channel, filePath, (err) => {
if(err) {
@@ -340,8 +340,10 @@ const MessageStore = Reflux.createStore({
}
},
onLoadFile: function(hash, cb) {
- if(!this.socket)
+ if(!this.socket) {
+ this.orbit.getFile(hash).then(cb).catch((err) => cb(null));
return;
+ }
if(hash) {
this.socket.emit('file.get', hash, (result) => {
diff --git a/client/src/styles/Channel.scss b/client/src/styles/Channel.scss
index a72b138..e5dedce 100644
--- a/client/src/styles/Channel.scss
+++ b/client/src/styles/Channel.scss
@@ -18,7 +18,7 @@ $controls-height: 2.8em;
width: 100%;
}
- .loadingIcon {
+ .spinner {
position: absolute;
bottom: 0.75em;
left: 0.75em;
diff --git a/client/src/styles/ChannelsPanel.scss b/client/src/styles/ChannelsPanel.scss
index 919abbc..ea88d36 100644
--- a/client/src/styles/ChannelsPanel.scss
+++ b/client/src/styles/ChannelsPanel.scss
@@ -115,7 +115,7 @@ $channel-name-min-height: 1.5em;
margin-bottom: 1.0em;
}
- .loadingIcon {
+ .spinner {
position: absolute;
box-sizing: border-box;
display: flex;
@@ -283,4 +283,4 @@ $channel-name-min-height: 1.5em;
animation-fill-mode: both;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
-}
\ No newline at end of file
+}
diff --git a/client/src/utils/utils.js b/client/src/utils/utils.js
index 72ebf7c..f48283c 100644
--- a/client/src/utils/utils.js
+++ b/client/src/utils/utils.js
@@ -4,3 +4,9 @@ export function getHumanReadableBytes(size) {
var i = Math.floor( Math.log(size) / Math.log(1024) );
return ( size / Math.pow(1024, i) ).toFixed(i > 2 ? 2 : 0) * 1 + ' ' + ['Bytes', 'kB', 'MB', 'GB', 'TB'][i];
}
+
+export function getFormattedTime(timestamp) {
+ const safeTime = (time) => ("0" + time).slice(-2);
+ const date = new Date(timestamp);
+ return safeTime(date.getHours()) + ":" + safeTime(date.getMinutes()) + ":" + safeTime(date.getSeconds());
+}
diff --git a/package.json b/package.json
index 9b13c5b..991e844 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"cors": "^2.7.1",
"du": "^0.1.0",
"express": "^4.13.4",
- "ipfs-post": "https://github.com/haadcode/ipfs-post",
+ "ipfs-post": "^0.0.4",
"ipfsd-ctl": "^0.14.0",
"lodash": "^4.8.2",
"logplease": "^1.2.7",
@@ -26,7 +26,7 @@
"grunt-contrib-clean": "^1.0.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-electron": "^2.0.1",
- "ipfs": "^0.11.0",
+ "ipfs": "^0.13.0",
"load-grunt-tasks": "^3.3.0",
"mocha": "^2.4.5",
"orbit-db-eventstore": "0.0.11",
diff --git a/src/Orbit.js b/src/Orbit.js
index f59bd95..b753f9b 100644
--- a/src/Orbit.js
+++ b/src/Orbit.js
@@ -174,7 +174,7 @@ class Orbit {
});
}
- addFile(channel, filePath, buffer) {
+ addFile(channel, filePath, buffer, meta) {
console.log("!!!!!!!!!!!!!", typeof filePath === 'string')
console.log(channel, filePath);
@@ -218,7 +218,8 @@ class Orbit {
name: filePath.split("/").pop(),
hash: hash,
size: size,
- from: this.orbitdb.user.id
+ from: this.orbitdb.user.id,
+ meta: meta,
};
const type = isDirectory ? Post.Types.Directory : Post.Types.File;
@@ -239,12 +240,25 @@ class Orbit {
}
getFile(hash, callback) {
- // request('http://localhost:8080/ipfs/' + hash, function (error, response, body) {
- // if(!error && response.statusCode === 200) {
- if(callback) callback(null);
- // if(callback) callback(body);
- // }
- // })
+ if (callback) {
+ callback(null);
+ return;
+ }
+ return this.ipfs.files.cat(hash).then(res => {
+ return new Promise((resolve, reject) => {
+ const buffers = [];
+ res
+ .on('error', err => {
+ reject(err);
+ })
+ .on('data', data => {
+ buffers.push(data);
+ })
+ .on('end', () => {
+ resolve(new Blob(buffers));
+ });
+ });
+ });
}
_handleError(e) {