1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-04 00:20:07 +00:00

[user notifications] rewrote it with react (#4486)

* [user notifications] rewrote it with react

* rewrote 'user notifications' page with react
* cleaned up the related files & code
* fixed 'popup notices'

* [seafile-js] updated the version
This commit is contained in:
llj
2020-03-24 15:24:47 +08:00
committed by GitHub
parent ac9e9b9ea4
commit 24b3b516bd
14 changed files with 287 additions and 253 deletions

View File

@@ -59,6 +59,11 @@ module.exports = {
require.resolve('react-dev-utils/webpackHotDevClient'), require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/tc-accept.js", paths.appSrc + "/tc-accept.js",
], ],
userNotifications: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/user-notifications.js",
],
wiki: [ wiki: [
require.resolve('./polyfills'), require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'), require.resolve('react-dev-utils/webpackHotDevClient'),

View File

@@ -60,6 +60,7 @@ module.exports = {
entry: { entry: {
markdownEditor: [require.resolve('./polyfills'), paths.appIndexJs], markdownEditor: [require.resolve('./polyfills'), paths.appIndexJs],
TCAccept: [require.resolve('./polyfills'), paths.appSrc + "/tc-accept.js"], TCAccept: [require.resolve('./polyfills'), paths.appSrc + "/tc-accept.js"],
userNotifications: [require.resolve('./polyfills'), paths.appSrc + "/user-notifications.js"],
wiki: [require.resolve('./polyfills'), paths.appSrc + "/wiki.js"], wiki: [require.resolve('./polyfills'), paths.appSrc + "/wiki.js"],
fileHistory: [require.resolve('./polyfills'), paths.appSrc + "/file-history.js"], fileHistory: [require.resolve('./polyfills'), paths.appSrc + "/file-history.js"],
fileHistoryOld: [require.resolve('./polyfills'), paths.appSrc + "/file-history-old.js"], fileHistoryOld: [require.resolve('./polyfills'), paths.appSrc + "/file-history-old.js"],

View File

@@ -44,7 +44,7 @@
"react-responsive": "^6.1.2", "react-responsive": "^6.1.2",
"react-select": "^2.4.1", "react-select": "^2.4.1",
"reactstrap": "^6.4.0", "reactstrap": "^6.4.0",
"seafile-js": "^0.2.144", "seafile-js": "^0.2.145",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"sw-precache-webpack-plugin": "0.11.4", "sw-precache-webpack-plugin": "0.11.4",
"unified": "^7.0.0", "unified": "^7.0.0",

View File

@@ -7,7 +7,7 @@ import { Utils } from '../../utils/utils';
const propTypes = { const propTypes = {
noticeItem: PropTypes.object.isRequired, noticeItem: PropTypes.object.isRequired,
onNoticeItemClick: PropTypes.func.isRequired, onNoticeItemClick: PropTypes.func
}; };
const MSG_TYPE_ADD_USER_TO_GROUP = 'add_user_to_group'; const MSG_TYPE_ADD_USER_TO_GROUP = 'add_user_to_group';
@@ -206,7 +206,19 @@ class NoticeItem extends React.Component {
return ''; return '';
} }
return ( return this.props.tr ? (
<tr className={noticeItem.seen ? 'read' : 'unread font-weight-bold'}>
<td className="text-center">
<img src={avatar_url} width="32" height="32" className="avatar" alt="" />
</td>
<td className="pr-8">
<p className="m-0" dangerouslySetInnerHTML={{__html: notice}}></p>
</td>
<td>
{moment(noticeItem.time).fromNow()}
</td>
</tr>
) : (
<li onClick={this.onNoticeItemClick} className={noticeItem.seen ? 'read' : 'unread'}> <li onClick={this.onNoticeItemClick} className={noticeItem.seen ? 'read' : 'unread'}>
<div className="notice-item"> <div className="notice-item">
<div className="main-info"> <div className="main-info">

View File

@@ -19,7 +19,8 @@ class Notification extends React.Component {
}); });
} }
onClick = () => { onClick = (e) => {
e.preventDefault();
if (this.state.showNotice) { if (this.state.showNotice) {
seafileAPI.updateNotifications(); seafileAPI.updateNotifications();
this.setState({ this.setState({
@@ -35,7 +36,7 @@ class Notification extends React.Component {
loadNotices = () => { loadNotices = () => {
let page = 1; let page = 1;
let perPage = 5; let perPage = 5;
seafileAPI.listPopupNotices(page, perPage).then(res => { seafileAPI.listNotifications(page, perPage).then(res => {
let noticeList = res.data.notification_list; let noticeList = res.data.notification_list;
this.setState({noticeList: noticeList}); this.setState({noticeList: noticeList});
}); });
@@ -61,7 +62,7 @@ class Notification extends React.Component {
return ( return (
<div id="notifications"> <div id="notifications">
<a href="#" onClick={this.onClick} className="no-deco" id="notice-icon" title="Notifications" aria-label={gettext('Notifications')}> <a href="#" onClick={this.onClick} className="no-deco" id="notice-icon" title={gettext('Notifications')} aria-label={gettext('Notifications')}>
<span className="sf2-icon-bell"></span> <span className="sf2-icon-bell"></span>
<span className={`num ${this.state.unseenCount ? '' : 'hide'}`}>{this.state.unseenCount}</span> <span className={`num ${this.state.unseenCount ? '' : 'hide'}`}>{this.state.unseenCount}</span>
</a> </a>

View File

@@ -0,0 +1,17 @@
body {
overflow: hidden;
}
#wrapper {
height: 100%;
}
.top-header {
background: #f4f4f7;
border-bottom: 1px solid #e8e8e8;
padding: .5rem 1rem;
flex-shrink: 0;
}
.op-bar {
padding: 9px 10px;
background: #f2f2f2;
border-radius: 2px;
}

View File

@@ -0,0 +1,215 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { navigate } from '@reach/router';
import { Utils } from './utils/utils';
import { gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants';
import { seafileAPI } from './utils/seafile-api';
import Loading from './components/loading';
import Paginator from './components/paginator';
import CommonToolbar from './components/toolbar/common-toolbar';
import NoticeItem from './components/common/notice-item';
import './css/toolbar.css';
import './css/search.css';
import './css/user-notifications.css';
class UserNotifications extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
errorMsg: '',
currentPage: 1,
perPage: 25,
hasNextPage: false,
items: []
};
}
componentDidMount() {
let urlParams = (new URL(window.location)).searchParams;
const {
currentPage, perPage
} = this.state;
this.setState({
perPage: parseInt(urlParams.get('per_page') || perPage),
currentPage: parseInt(urlParams.get('page') || currentPage)
}, () => {
this.getItems(this.state.currentPage);
});
}
getItems = (page) => {
const { perPage } = this.state;
seafileAPI.listNotifications(page, perPage).then((res) => {
this.setState({
isLoading: false,
items: res.data.notification_list,
currentPage: page,
hasNextPage: Utils.hasNextPage(page, perPage, res.data.count)
});
}).catch((error) => {
this.setState({
isLoading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
resetPerPage = (perPage) => {
this.setState({
perPage: perPage
}, () => {
this.getItems(1);
});
}
onSearchedClick = (selectedItem) => {
if (selectedItem.is_dir === true) {
let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path;
navigate(url, {repalce: true});
} else {
let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
let newWindow = window.open('about:blank');
newWindow.location.href = url;
}
}
markAllRead = () => {
seafileAPI.updateNotifications().then((res) => {
this.setState({
items: this.state.items.map(item => {
item.seen = true;
return item;
})
});
}).catch((error) => {
this.setState({
isLoading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
clearAll = () => {
seafileAPI.deleteNotifications().then((res) => {
this.setState({
items: []
});
}).catch((error) => {
this.setState({
isLoading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
render() {
return (
<React.Fragment>
<div className="h-100 d-flex flex-column">
<div className="top-header d-flex justify-content-between">
<a href={siteRoot}>
<img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
</a>
<CommonToolbar onSearchedClick={this.onSearchedClick} />
</div>
<div className="flex-auto container-fluid pt-4 pb-6 o-auto">
<div className="row">
<div className="col-md-10 offset-md-1">
<div className="d-flex justify-content-between align-items-center op-bar">
<h2 className="h4 m-0">{gettext('Notifications')}</h2>
<div>
<button className="btn btn-secondary op-bar-btn" onClick={this.markAllRead}>{gettext('Mark all read')}</button>
<button className="btn btn-secondary op-bar-btn ml-2" onClick={this.clearAll}>{gettext('Clear')}</button>
</div>
</div>
<Content
isLoading={this.state.isLoading}
errorMsg={this.state.errorMsg}
items={this.state.items}
currentPage={this.state.currentPage}
hasNextPage={this.state.hasNextPage}
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
getListByPage={this.getItems}
/>
</div>
</div>
</div>
</div>
</React.Fragment>
);
}
}
class Content extends React.Component {
constructor(props) {
super(props);
this.theadData = [
{width: '7%', text: ''},
{width: '73%', text: gettext('Message')},
{width: '20%', text: gettext('Time')}
];
}
getPreviousPage = () => {
this.props.getListByPage(this.props.currentPage - 1);
}
getNextPage = () => {
this.props.getListByPage(this.props.currentPage + 1);
}
render() {
const {
isLoading, errorMsg, items,
curPerPage, currentPage, hasNextPage
} = this.props;
if (isLoading) {
return <Loading />;
}
if (errorMsg) {
return <p className="error mt-6 text-center">{errorMsg}</p>;
}
return (
<React.Fragment>
<table className="table-hover">
<thead>
<tr>
{this.theadData.map((item, index) => {
return <th key={index} width={item.width}>{item.text}</th>;
})}
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<NoticeItem key={index} noticeItem={item} tr={true} />);
})}
</tbody>
</table>
{items.length > 0 &&
<Paginator
gotoPreviousPage={this.getPreviousPage}
gotoNextPage={this.getNextPage}
currentPage={currentPage}
hasNextPage={hasNextPage}
curPerPage={curPerPage}
resetPerPage={this.props.resetPerPage}
/>
}
</React.Fragment>
);
}
}
ReactDOM.render(
<UserNotifications />,
document.getElementById('wrapper')
);

View File

@@ -2006,37 +2006,6 @@ a.sf-popover-item {
width:180px; width:180px;
} }
/* notice page */
#notices-table .unread {
font-weight:bold;
}
#notices-table .avatar-cell {
vertical-align:top;
text-align:center;
padding-top:8px;
}
#notices-table .avatar {
border-radius:1000px;
}
#notices-table .brief,
#notices-table .detail {
margin:0.2em 100px .2em 0;
}
#notices-table .detail {
color:#9d9b9c;
cursor:pointer;
font-weight:normal;
}
#notice-list .topic,
#notices-table .topic {
padding:4px 13px;
border-left:3px solid #e0e0e0;
margin-left:7px;
}
#notices-table a {
font-weight:normal;
}
/* pwd strength */ /* pwd strength */
#pwd_strength { #pwd_strength {
margin:-15px 0 20px; margin:-15px 0 20px;

View File

@@ -218,6 +218,16 @@ a:hover { color:#eb8205; }
cursor: pointer; cursor: pointer;
} }
.op-bar-btn {
border-color: #ccc;
border-radius: 2px;
height: 30px;
line-height: 28px;
font-weight: normal;
padding: 0 0.5rem;
min-width: 55px;
}
/* UI Widget */ /* UI Widget */
/**** caret ****/ /**** caret ****/

View File

@@ -1,98 +0,0 @@
{% extends "base_wide_page.html" %}
{% load avatar_tags i18n seahub_tags %}
{% block sub_title %}{% trans "Notices" %} - {% endblock %}
{% block wide_page_content %}
<div class="tabnav">
<ul class="tabnav-tabs">
<li class="tabnav-tab tabnav-tab-cur"><a href="{% url 'user_notification_list' %}">{% trans "Notices" %}</a></li>
</ul>
<div class="fright">
<button id="mark-all-read">{% trans "Mark all read" %}</button>
<button id="clear">{% trans "Clear" %}</button>
</div>
</div>
{% if notices %}
<table id="notices-table">
<thead>
<tr>
<th width="7%"></th>
<th width="73%">{% trans "Message"%}</th>
<th width="20%">{% trans "Time"%}</th>
</tr>
</thead>
<tbody>
{% include "notifications/user_notification_tr.html" %}
</tbody>
</table>
{% if notices_more %}
<div id="notices-more">
<div id="notices-loading" class="hide"><span class="loading-icon"></span></div>
<button id="notices-more-btn" class="full-width-btn">{% trans 'More' %}</button>
<p id="notices-error" class="error hide"></p>
</div>
{% endif %}
{% endif %}
{% endblock %}
{% block extra_script %}
<script type="text/javascript">
$('#clear').on('click', function() {
location.href = "{% url 'user_notification_remove' %}";
});
$('#mark-all-read').on('click', function() {
var unread_items = $('#notices-table .unread');
if (unread_items.length > 0) {
$.ajax({
url: "{% url 'api-v2.1-notifications' %}",
type: 'PUT',
dataType: 'json',
beforeSend: prepareCSRFToken,
success: function() {
unread_items.removeClass('unread').addClass('read');
},
error: ajaxErrorHandler
});
}
});
$('#notices-table .detail').on('click', function() {
location.href = $('.brief a', $(this).parent()).attr('href');
});
var start = {{start}}, limit = {{limit}};
$('#notices-more-btn').on('click', function() {
$(this).addClass('hide');
$('#notices-loading').removeClass('hide');
$.ajax({
url:'{% url "user_notification_more" %}' + '?start=' + start + '&limit=' + limit,
dataType: 'json',
cache: false,
success: function(data) {
$('#notices-loading').addClass('hide');
$('#notices-table').append(data['html']);
$('#notices-table .detail').off().on('click', function() {
location.href = $('.brief a', $(this).parent()).attr('href');
});
start = data['new_start'];
if (data['notices_more']) {
$('#notices-more-btn').removeClass('hide');
}
},
error: function(jqXHR, textStatus, errorThrown) {
$('#notices-loading').addClass('hide');
if (!jqXHR.responseText && textStatus != 'abort') {
$('#notices-error').html("{% trans "Failed. Please check the network." %}").removeClass('hide');
}
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'base_for_react.html' %}
{% load seahub_tags i18n %}
{% load render_bundle from webpack_loader %}
{% block sub_title %}{% trans "Notifications" %} - {% endblock %}
{% block extra_style %}
{% render_bundle 'userNotifications' 'css' %}
{% endblock %}
{% block extra_script %}
<script type="text/javascript">
// overwrite the one in base_for_react.html
window.app.pageOptions = {
};
</script>
{% render_bundle 'userNotifications' 'js' %}
{% endblock %}

View File

@@ -1,48 +0,0 @@
{% load i18n seahub_tags avatar_tags %}
{% for notice in notices %}
{% if notice.seen %}
<tr class="read">
{% else %}
<tr class="unread">
{% endif %}
<td class="avatar-cell">
{% if notice.msg_from %}
<a href="{% url 'user_profile' notice.msg_from %}">{% avatar notice.msg_from 32 %}</a>
{% else %}
<img src={{notice.default_avatar_url}} width="32" height="32" class="avatar" alt="" />
{% endif %}
</td>
<td>
{% if notice.is_file_uploaded_msg %}
<p class="brief">{{ notice.format_file_uploaded_msg|safe }}</p>
{% elif notice.is_repo_share_msg %}
<p class="brief">{{ notice.format_repo_share_msg|safe }}</p>
{% elif notice.is_repo_share_to_group_msg %}
<p class="brief">{{ notice.format_repo_share_to_group_msg|safe }}</p>
{% elif notice.is_group_join_request %}
<p class="brief">{{ notice.format_group_join_request|safe }}</p>
{% elif notice.is_file_comment_msg %}
<p class="brief">{{ notice.format_file_comment_msg|safe }}</p>
{% elif notice.is_draft_comment_msg %}
<p class="brief">{{ notice.format_draft_comment_msg|safe }}</p>
{% elif notice.is_draft_reviewer_msg %}
<p class="brief">{{ notice.format_draft_reviewer_msg|safe }}</p>
{% elif notice.is_guest_invitation_accepted_msg %}
<p class="brief">{{ notice.format_guest_invitation_accepted_msg|safe }}</p>
{% elif notice.is_add_user_to_group %}
<p class="brief">{{ notice.format_add_user_to_group|safe }}</p>
{% endif %}
</td>
<td>{{ notice.timestamp|translate_seahub_time }}</td>
</tr>
{% endfor %}

View File

@@ -5,6 +5,4 @@ from .views import *
urlpatterns = [ urlpatterns = [
########## user notifications ########## user notifications
url(r'^list/$', user_notification_list, name='user_notification_list'), url(r'^list/$', user_notification_list, name='user_notification_list'),
url(r'^more/$', user_notification_more, name='user_notification_more'),
url(r'^remove/$', user_notification_remove, name='user_notification_remove'),
] ]

View File

@@ -22,74 +22,8 @@ logger = logging.getLogger(__name__)
########## user notifications ########## user notifications
@login_required @login_required
def user_notification_list(request): def user_notification_list(request):
""" return render(request, "notifications/user_notification_list_react.html", {
})
Arguments:
- `request`:
"""
username = request.user.username
count = 25 # initial notification count
limit = 25 # next a mount of notifications fetched by AJAX
notices = UserNotification.objects.get_user_notifications(username)[:count]
# Add 'msg_from' or 'default_avatar_url' to notice.
notices = add_notice_from_info(notices)
notices_more = True if len(notices) == count else False
return render(request, "notifications/user_notification_list.html", {
'notices': notices,
'start': count,
'limit': limit,
'notices_more': notices_more,
})
@login_required_ajax
def user_notification_more(request):
"""Fetch next ``limit`` notifications starts from ``start``.
Arguments:
- `request`:
- `start`:
- `limit`:
"""
username = request.user.username
start = int(request.GET.get('start', 0))
limit = int(request.GET.get('limit', 0))
notices = UserNotification.objects.get_user_notifications(username)[
start: start+limit]
# Add 'msg_from' or 'default_avatar_url' to notice.
notices = add_notice_from_info(notices)
notices_more = True if len(notices) == limit else False
new_start = start+limit
ctx = {'notices': notices}
html = render_to_string("notifications/user_notification_tr.html", ctx)
ct = 'application/json; charset=utf-8'
return HttpResponse(json.dumps({
'html':html,
'notices_more':notices_more,
'new_start': new_start}), content_type=ct)
@login_required
def user_notification_remove(request):
"""
Arguments:
- `request`:
"""
UserNotification.objects.remove_user_notifications(request.user.username)
messages.success(request, _("Successfully cleared all notices."))
next_page = request.META.get('HTTP_REFERER', None)
if not next_page:
next_page = settings.SITE_ROOT
return HttpResponseRedirect(next_page)
def add_notice_from_info(notices): def add_notice_from_info(notices):
'''Add 'msg_from' or 'default_avatar_url' to notice. '''Add 'msg_from' or 'default_avatar_url' to notice.