feature (notification): inform user of anything happenning

This commit is contained in:
Mickael KERJEAN
2018-04-09 15:43:22 +10:00
parent 24592be54b
commit c25eb03540
8 changed files with 222 additions and 94 deletions

View File

@ -39,7 +39,9 @@
color: var(--light); color: var(--light);
span.title{ span.title{
position: absolute; position: absolute;
background: var(--bg-color); background: var(--color);
color: white;
font-size: 0.8em;
opacity: 0; opacity: 0;
transform: translateY(5px); transform: translateY(5px);
border-radius: 2px; border-radius: 2px;

View File

@ -13,41 +13,26 @@ export class Notification extends React.Component {
message_text: null, message_text: null,
message_type: null message_type: null
}; };
function TaskManager(){
let jobs = [];
let is_running = false;
const ret = {
addJob: (job) => {
jobs.push(job);
if(is_running === false){
is_running = true;
ret._executor();
}
},
_executor: () => {
let job = jobs.shift();
if(!job){
is_running = false;
return Promise.resolve();
}
return job().then(ret._executor);
}
};
return ret;
}
this.runner = new TaskManager(); this.runner = new TaskManager();
this.notification_current = null;
this.notification_is_first = null;
this.notification_is_last = null;
} }
componentDidMount(){ componentDidMount(){
notify.subscribe((_message, type) => { this.runner.before_run((task, isFirst, isLast) => {
let job = playMessage.bind(this, { this.notification_current = task;
text: stringify(_message),
type: type
}); });
this.runner.addJob(job);
notify.subscribe((message, type) => {
this.runner.addTask(Task(
this.openNotification.bind(this, {text: stringify(message), type: type}),
this.closeNotification.bind(this),
8000,
500
));
}); });
function stringify(data){ function stringify(data){
if(typeof data === 'object' && data.message){ if(typeof data === 'object' && data.message){
return data.message; return data.message;
@ -56,52 +41,134 @@ export class Notification extends React.Component {
} }
return JSON.stringify(data); return JSON.stringify(data);
} }
function playMessage(message){ }
const displayMessage = (message) => {
closeNotification(){
return new Promise((done ,err) => {
this.setState({
appear: false
}, done);
});
}
openNotification(message){
return new Promise((done ,err) => {
this.setState({ this.setState({
appear: true, appear: true,
message_text: message.text, message_text: message.text,
message_type: message.type message_type: message.type
}, done);
}); });
return Promise.resolve(message);
};
const waitForABit = (timeout, message) => {
return new Promise((done, err) => {
window.setTimeout(() => {
done(message);
}, timeout);
});
};
const hideMessage = (message) => {
this.setState({
appear: false
});
return Promise.resolve(message);
};
return displayMessage(message)
.then(waitForABit.bind(this, 5000))
.then(hideMessage)
.then(waitForABit.bind(this, 1000));
}
} }
close(){ cancelAnimation(){
this.setState({ appear: false }); return this.notification_current.cancel();
} }
render(){ render(){
return ( return (
<NgIf cond={this.state.appear === true} className="component_notification no-select"> <ReactCSSTransitionGroup transitionName="notification" transitionLeave={true} transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={100} transitionAppear={false} className="component_notification">
<ReactCSSTransitionGroup transitionName="notification" transitionLeave={true} transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={500}> <NgIf key={this.state.message_text+this.state.message_type+this.state.appear} cond={this.state.appear === true} className="no-select">
<div className={"component_notification--container "+(this.state.message_type || 'info')}> <div className={"component_notification--container "+(this.state.message_type || 'info')}>
<div className="message"> <div className="message">
{ this.state.message_text } { this.state.message_text }
</div> </div>
<div className="close" onClick={this.close.bind(this)}>X</div> <div className="close" onClick={this.cancelAnimation.bind(this)}>X</div>
</div> </div>
</ReactCSSTransitionGroup>
</NgIf> </NgIf>
</ReactCSSTransitionGroup>
); );
} }
} }
function TaskManager(){
let tasks = [];
let is_running = false;
let subscriber = null;
let current_task = null;
let is_first = null;
let is_last = null;
const ret ={
addTask: function(task){
current_task && current_task.cancel();
tasks.push(task);
if(is_running === false){
is_running = true;
ret._run();
}
},
before_run: function(fn){
subscriber = fn;
},
_run: function(){
current_task = tasks.shift();
is_last = tasks.length === 0;
if(!current_task){
is_running = false;
return Promise.resolve();
}else{
const mode = tasks.length > 0 ? 'minimal' : 'normal';
subscriber(current_task, mode);
return current_task.run(mode).then(ret._run);
}
}
};
return ret;
}
function Task(_runCallback, _finishCallback, wait_time_before_finish, minimum_running_time){
let start_date = null;
let done = null;
let promise = new Promise((_done) => { done = _done; });
let timeout = null;
const ret = {
run: function(mode = 'normal'){
const wait = mode === 'minimal' ? minimum_running_time : wait_time_before_finish;
start_date = new Date();
new Promise((_done, err) => {
timeout = window.setTimeout(() => {
_done();
}, 200);
})
.then(_runCallback)
.then(() => new Promise((_done, err) => {
timeout = window.setTimeout(() => {
_done();
}, wait);
}))
.then(() => {
ret._complete();
});
return promise;
},
cancel: function(){
window.clearTimeout(timeout);
timeout = null;
let elapsed_time = new Date() - start_date;
if(elapsed_time < minimum_running_time){
window.setTimeout(() => {
ret._complete();
}, minimum_running_time - elapsed_time);
}else{
ret._complete();
}
return promise;
},
_complete: function(){
if(done){
_finishCallback();
done();
}
done = null;
return Promise.resolve();
}
};
return ret;
}

View File

@ -1,23 +1,23 @@
.component_notification{ .component_notification{
position: fixed; position: fixed;
bottom: 25px; bottom: 20px;
left: 25px; left: 20px;
right: 0; right: 0;
font-size: 0.95em; font-size: 0.95em;
z-index: 10;
.component_notification--container{ .component_notification--container{
width: 400px; width: 400px;
text-align: left; text-align: left;
display: inline-block; display: inline-block;
padding: 15px 25px 15px 15px; padding: 15px 20px 15px 15px;
border-radius: 2px; border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px; box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px;
display: flex; display: flex;
align-items: center; align-items: center;
&.info{ &.info{
background: rgba(0,0,0,0.6); background: var(--color);
color: white; color: white;
} }
&.error{ &.error{
@ -40,3 +40,36 @@
} }
} }
} }
@media (max-width: 450px){
.component_notification{
bottom: 0px;
left: 0px;
.component_notification--container{
width: 100%;
box-sizing: border-box;
}
}
}
.component_notification{
.notification-leave{
opacity: 1;
}
.notification-leave.notification-leave-active{
opacity: 0;
transition: opacity 0.2s ease-out;
}
.notification-enter{
transform: translateY(50px);
opacity: 0;
display: inline-block;
}
.notification-enter.notification-enter-active{
opacity: 1;
transform: translateY(0);
transition: all 0.1s ease-out;
}
}

View File

@ -23,9 +23,9 @@ export function http_get(url, type = 'json'){
} }
}else{ }else{
if(navigator.onLine === false){ if(navigator.onLine === false){
err({status: xhr.status, code: "CONNECTION_LOST", message: 'Connection Lost'}); err({status: xhr.status, code: "CONNECTION_LOST", message: 'Ooups! Looks like your internet has gone away'});
}else{ }else{
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'}); err({status: xhr.status, message: xhr.responseText || 'Oups! Something went wrong'});
} }
} }
} }
@ -35,7 +35,6 @@ export function http_get(url, type = 'json'){
}); });
} }
export function http_post(url, data, type = 'json'){ export function http_post(url, data, type = 'json'){
return new Promise((done, err) => { return new Promise((done, err) => {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();

View File

@ -27,7 +27,8 @@ class FileSystem{
window.setTimeout(() => done(), 2000); window.setTimeout(() => done(), 2000);
})) }))
.then(() => { .then(() => {
return keep_pulling_from_http === true? fetch_from_http(_path) : Promise.resolve(); if(keep_pulling_from_http === false) return Promise.resolve();
return fetch_from_http(_path);
}); });
}; };
fetch_from_http(path); fetch_from_http(path);
@ -64,18 +65,18 @@ class FileSystem{
} }
// publish // publish
cache.put(cache.FILE_PATH, path, {results: response.results}); cache.put(cache.FILE_PATH, path, {results: response.results});
if(this.current_path === path) this.obs && this.obs.next(response.results); if(this.current_path === path) this.obs && this.obs.next({status: 'ok', results: response.results});
}); });
}).catch((_err) => { }).catch((_err) => {
// TODO: user is in offline mode, notify this.obs.next(_err);
console.log(_err); return Promise.reject();
}); });
} }
_ls_from_cache(path, _record_access = false){ _ls_from_cache(path, _record_access = false){
return cache.get(cache.FILE_PATH, path, _record_access).then((_files) => { return cache.get(cache.FILE_PATH, path, _record_access).then((_files) => {
if(_files && _files.results){ if(_files && _files.results){
if(this.current_path === path){ if(this.current_path === path){
this.obs && this.obs.next(_files.results); this.obs && this.obs.next({status: 'ok', results: _files.results});
} }
}; };
return Promise.resolve(); return Promise.resolve();
@ -353,4 +354,3 @@ class FileSystem{
export const Files = new FileSystem(); export const Files = new FileSystem();
window.Files = Files;

View File

@ -25,7 +25,7 @@ export class FilesPage extends React.Component {
this.resetHeight = debounce(this.resetHeight.bind(this), 100); this.resetHeight = debounce(this.resetHeight.bind(this), 100);
this.goToFiles = goToFiles.bind(null, this.props.history); this.goToFiles = goToFiles.bind(null, this.props.history);
this.goToViewer = goToViewer.bind(null, this.props.history); this.goToViewer = goToViewer.bind(null, this.props.history);
this.observers = {ls: null}; this.observers = [];
} }
componentDidMount(){ componentDidMount(){
@ -50,7 +50,7 @@ export class FilesPage extends React.Component {
this.props.unsubscribe('file.delete'); this.props.unsubscribe('file.delete');
this.props.unsubscribe('file.refresh'); this.props.unsubscribe('file.refresh');
window.removeEventListener("resize", this.resetHeight); window.removeEventListener("resize", this.resetHeight);
if(this.observers.ls) this.observers.ls.unsubscribe(); this._cleanupListeners();
} }
componentWillReceiveProps(nextProps){ componentWillReceiveProps(nextProps){
@ -72,21 +72,36 @@ export class FilesPage extends React.Component {
onRefresh(path = this.state.path){ onRefresh(path = this.state.path){
this.resetHeight(); this.resetHeight();
if(this.observers.ls) this.observers.ls.unsubscribe(); this._cleanupListeners();
this.observers.ls = Files.ls(path).subscribe((files) => { const observer = Files.ls(path).subscribe((res) => {
if(res.status === 'ok'){
let files = res.results;
files = files.map((file) => { files = files.map((file) => {
let path = this.state.path+file.name; let path = this.state.path+file.name;
file.link = file.type === "file" ? "/view"+path : "/files"+path+"/"; file.link = file.type === "file" ? "/view"+path : "/files"+path+"/";
return file; return file;
}); });
this.setState({files: files, loading: false}); this.setState({files: files, loading: false});
}else{
notify.send(res, 'error');
}
}, (error) => { }, (error) => {
notify.send(error, 'error'); notify.send(error, 'error');
this.setState({error: error}); this.setState({error: error});
}); });
this.observers.push(observer);
this.setState({error: false}); this.setState({error: false});
} }
_cleanupListeners(){
if(this.observers.length > 0) {
this.observers = this.observers.filter((observer) => {
observer.unsubscribe();
return false;
});
}
}
onCreate(path, type, file){ onCreate(path, type, file){
if(type === 'file'){ if(type === 'file'){
return Files.touch(path, file) return Files.touch(path, file)

View File

@ -40,9 +40,22 @@
padding: 0 5px; padding: 0 5px;
line-height: 22px; line-height: 22px;
white-space: nowrap; white-space: nowrap;
span{
display: inline-block;
width: calc(100% - 130px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
color: inherit;
}
} }
form{ form{
display: inline; display: inline-block;
input{
border-width: 0px;
padding: 0 2px 0 2px;
}
} }
.component_icon{ .component_icon{

View File

@ -85,10 +85,9 @@ export class ViewerPage extends React.Component {
this.setState({needSaving: false}); this.setState({needSaving: false});
}) })
.catch((err) => { .catch((err) => {
if(err && err.code === 'CANCELLED'){ if(err && err.code === 'CANCELLED'){ return; }
notify.send(err, 'error');
}
this.setState({isSaving: false}); this.setState({isSaving: false});
notify.send(err, 'error');
}); });
} }