Created a new message indicator component to indicate new messages (#4299)
* Created a new message indicator component to indicate new messages are present outside the user's view: * Created new component with styling * Theming and i18n support for new messages indicator * Count new unviewed messages and integrate with component * Coding style for new message indicator * Fixed bugs with new message indicator: * Fix display issues with new message indicator * Update text to deal with plurals in React * Fix coded style for ticket 'PLT-1917': add new message indicator. * Use only server generated timestamps for checking new messages at the bottom * Move transitionend to prop, fix style selectors
Этот коммит содержится в:
коммит произвёл
Harrison Healey
родитель
9b96fcc7c6
Коммит
7399d349de
65
webapp/components/post_view/components/new_message_indicator.jsx
Обычный файл
65
webapp/components/post_view/components/new_message_indicator.jsx
Обычный файл
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
export default class NewMessageIndicator extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
visible: false,
|
||||||
|
rendered: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.newMessages > 0) {
|
||||||
|
this.setState({rendered: true}, () => {
|
||||||
|
this.setState({visible: true});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({visible: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let className = 'nav-pills__unread-indicator-bottom';
|
||||||
|
if (this.state.visible > 0) {
|
||||||
|
className += ' visible';
|
||||||
|
}
|
||||||
|
if (!this.state.rendered) {
|
||||||
|
className += ' disabled';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
onTransitionEnd={this.setRendered.bind(this)}
|
||||||
|
ref='indicator'
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
className='fa fa-arrow-circle-o-down'
|
||||||
|
/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='posts_view_newMsgBelow'
|
||||||
|
defaultMessage='{count} new {count, plural, one {message} other {messages}} below'
|
||||||
|
values={{count: this.props.newMessages}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync 'rendered' state with visibility param, only after transitions
|
||||||
|
// have ended
|
||||||
|
setRendered() {
|
||||||
|
this.setState({rendered: this.state.visible});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NewMessageIndicator.defaultProps = {
|
||||||
|
newMessages: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
NewMessageIndicator.propTypes = {
|
||||||
|
onClick: React.PropTypes.func.isRequired,
|
||||||
|
newMessages: React.PropTypes.number
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import $ from 'jquery';
|
|||||||
import Post from './post.jsx';
|
import Post from './post.jsx';
|
||||||
import FloatingTimestamp from './floating_timestamp.jsx';
|
import FloatingTimestamp from './floating_timestamp.jsx';
|
||||||
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
|
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
|
||||||
|
import NewMessageIndicator from './new_message_indicator.jsx';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
|
|
||||||
@@ -56,7 +57,8 @@ export default class PostList extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
isScrolling: false,
|
isScrolling: false,
|
||||||
fullWidthIntro: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
|
fullWidthIntro: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
|
||||||
topPostId: null
|
topPostId: null,
|
||||||
|
unViewedCount: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.channel) {
|
if (props.channel) {
|
||||||
@@ -74,6 +76,23 @@ export default class PostList extends React.Component {
|
|||||||
this.introText = createChannelIntroMessage(this.props.channel, this.state.fullWidthIntro);
|
this.introText = createChannelIntroMessage(this.props.channel, this.state.fullWidthIntro);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const posts = nextProps.postList.posts;
|
||||||
|
const order = nextProps.postList.order;
|
||||||
|
let unViewedCount = 0;
|
||||||
|
|
||||||
|
// Only count if we're not at the bottom, not in highlight view,
|
||||||
|
// or anything else
|
||||||
|
if (nextProps.scrollType === Constants.ScrollTypes.FREE) {
|
||||||
|
for (let i = order.length - 1; i >= 0; i--) {
|
||||||
|
const post = posts[order[i]];
|
||||||
|
if (post.create_at > nextProps.lastViewedBottom &&
|
||||||
|
post.user_id !== nextProps.currentUser.id) {
|
||||||
|
unViewedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({unViewedCount});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(e) {
|
handleKeyDown(e) {
|
||||||
@@ -553,6 +572,10 @@ export default class PostList extends React.Component {
|
|||||||
atBottom={this.wasAtBottom}
|
atBottom={this.wasAtBottom}
|
||||||
onClick={this.scrollToBottomAnimated}
|
onClick={this.scrollToBottomAnimated}
|
||||||
/>
|
/>
|
||||||
|
<NewMessageIndicator
|
||||||
|
newMessages={this.state.unViewedCount}
|
||||||
|
onClick={this.scrollToBottomAnimated}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
ref='postlist'
|
ref='postlist'
|
||||||
className='post-list-holder-by-time'
|
className='post-list-holder-by-time'
|
||||||
@@ -576,6 +599,7 @@ export default class PostList extends React.Component {
|
|||||||
|
|
||||||
PostList.defaultProps = {
|
PostList.defaultProps = {
|
||||||
lastViewed: 0,
|
lastViewed: 0,
|
||||||
|
lastViewedBottom: Number.MAX_VALUE,
|
||||||
ownNewMessage: false
|
ownNewMessage: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -590,6 +614,7 @@ PostList.propTypes = {
|
|||||||
showMoreMessagesTop: React.PropTypes.bool,
|
showMoreMessagesTop: React.PropTypes.bool,
|
||||||
showMoreMessagesBottom: React.PropTypes.bool,
|
showMoreMessagesBottom: React.PropTypes.bool,
|
||||||
lastViewed: React.PropTypes.number,
|
lastViewed: React.PropTypes.number,
|
||||||
|
lastViewedBottom: React.PropTypes.number,
|
||||||
ownNewMessage: React.PropTypes.bool,
|
ownNewMessage: React.PropTypes.bool,
|
||||||
postsToHighlight: React.PropTypes.object,
|
postsToHighlight: React.PropTypes.object,
|
||||||
displayNameType: React.PropTypes.string,
|
displayNameType: React.PropTypes.string,
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ export default class PostViewController extends React.Component {
|
|||||||
const profiles = UserStore.getProfiles();
|
const profiles = UserStore.getProfiles();
|
||||||
|
|
||||||
let lastViewed = Number.MAX_VALUE;
|
let lastViewed = Number.MAX_VALUE;
|
||||||
|
let lastViewedBottom = Number.MAX_VALUE;
|
||||||
const member = ChannelStore.getMyMember(channel.id);
|
const member = ChannelStore.getMyMember(channel.id);
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
lastViewed = member.last_viewed_at;
|
lastViewed = member.last_viewed_at;
|
||||||
|
lastViewedBottom = member.last_viewed_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
|
||||||
@@ -58,6 +60,7 @@ export default class PostViewController extends React.Component {
|
|||||||
statuses,
|
statuses,
|
||||||
atTop: PostStore.getVisibilityAtTop(channel.id),
|
atTop: PostStore.getVisibilityAtTop(channel.id),
|
||||||
lastViewed,
|
lastViewed,
|
||||||
|
lastViewedBottom,
|
||||||
ownNewMessage: false,
|
ownNewMessage: false,
|
||||||
loading,
|
loading,
|
||||||
scrollType: ScrollTypes.NEW_MESSAGE,
|
scrollType: ScrollTypes.NEW_MESSAGE,
|
||||||
@@ -187,9 +190,11 @@ export default class PostViewController extends React.Component {
|
|||||||
|
|
||||||
onPostsViewJumpRequest(type, postId) {
|
onPostsViewJumpRequest(type, postId) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Constants.PostsViewJumpTypes.BOTTOM:
|
case Constants.PostsViewJumpTypes.BOTTOM: {
|
||||||
this.setState({scrollType: ScrollTypes.BOTTOM});
|
const lastPost = PostStore.getLatestPost(this.state.channel.id);
|
||||||
|
this.setState({scrollType: ScrollTypes.BOTTOM, lastViewedBottom: lastPost.create_at || new Date().getTime()});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case Constants.PostsViewJumpTypes.POST:
|
case Constants.PostsViewJumpTypes.POST:
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollType: ScrollTypes.POST,
|
scrollType: ScrollTypes.POST,
|
||||||
@@ -213,7 +218,8 @@ export default class PostViewController extends React.Component {
|
|||||||
|
|
||||||
onPostListScroll(atBottom) {
|
onPostListScroll(atBottom) {
|
||||||
if (atBottom) {
|
if (atBottom) {
|
||||||
this.setState({scrollType: ScrollTypes.BOTTOM});
|
const lastPost = PostStore.getLatestPost(this.state.channel.id);
|
||||||
|
this.setState({scrollType: ScrollTypes.BOTTOM, lastViewedBottom: lastPost.create_at || new Date().getTime()});
|
||||||
} else {
|
} else {
|
||||||
this.setState({scrollType: ScrollTypes.FREE});
|
this.setState({scrollType: ScrollTypes.FREE});
|
||||||
}
|
}
|
||||||
@@ -334,6 +340,7 @@ export default class PostViewController extends React.Component {
|
|||||||
useMilitaryTime={this.state.useMilitaryTime}
|
useMilitaryTime={this.state.useMilitaryTime}
|
||||||
flaggedPosts={this.state.flaggedPosts}
|
flaggedPosts={this.state.flaggedPosts}
|
||||||
lastViewed={this.state.lastViewed}
|
lastViewed={this.state.lastViewed}
|
||||||
|
lastViewedBottom={this.state.lastViewedBottom}
|
||||||
ownNewMessage={this.state.ownNewMessage}
|
ownNewMessage={this.state.ownNewMessage}
|
||||||
statuses={this.state.statuses}
|
statuses={this.state.statuses}
|
||||||
isBusy={this.state.isBusy}
|
isBusy={this.state.isBusy}
|
||||||
|
|||||||
@@ -1624,6 +1624,7 @@
|
|||||||
"post_info.reply": "Reply",
|
"post_info.reply": "Reply",
|
||||||
"posts_view.loadMore": "Load more messages",
|
"posts_view.loadMore": "Load more messages",
|
||||||
"posts_view.newMsg": "New Messages",
|
"posts_view.newMsg": "New Messages",
|
||||||
|
"posts_view.newMsgBelow": "{count} new {count, plural, one {message} other {messages}} below",
|
||||||
"reaction.clickToAdd": "(click to add)",
|
"reaction.clickToAdd": "(click to add)",
|
||||||
"reaction.clickToRemove": "(click to remove)",
|
"reaction.clickToRemove": "(click to remove)",
|
||||||
"reaction.multipleReacted": "<b>{users} and {lastUser}</b> reacted with <b>:{emojiName}:</b>",
|
"reaction.multipleReacted": "<b>{users} and {lastUser}</b> reacted with <b>:{emojiName}:</b>",
|
||||||
|
|||||||
@@ -258,6 +258,33 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.nav-pills__unread-indicator-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -30px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
@include border-radius(50px);
|
||||||
|
font-size: 13.5px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 3px 0 4px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 250px;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: bottom 0.2s ease-out;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-list__timestamp {
|
.post-list__timestamp {
|
||||||
|
|||||||
@@ -560,6 +560,7 @@ export function applyTheme(theme) {
|
|||||||
changeCss('body.app__body', 'scrollbar-face-color:' + theme.centerChannelBg);
|
changeCss('body.app__body', 'scrollbar-face-color:' + theme.centerChannelBg);
|
||||||
changeCss('body.app__body', 'scrollbar-track-color:' + theme.centerChannelBg);
|
changeCss('body.app__body', 'scrollbar-track-color:' + theme.centerChannelBg);
|
||||||
changeCss('.app__body .post-list__new-messages-below', 'color:' + theme.centerChannelBg);
|
changeCss('.app__body .post-list__new-messages-below', 'color:' + theme.centerChannelBg);
|
||||||
|
changeCss('.app__body .nav-pills__unread-indicator-bottom', 'color:' + theme.centerChannelBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme.centerChannelColor) {
|
if (theme.centerChannelColor) {
|
||||||
@@ -642,6 +643,8 @@ export function applyTheme(theme) {
|
|||||||
changeCss('.app__body a, .app__body a:focus, .app__body a:hover, .app__body .btn, .app__body .btn:focus, .app__body .btn:hover', 'color:' + theme.linkColor);
|
changeCss('.app__body a, .app__body a:focus, .app__body a:hover, .app__body .btn, .app__body .btn:focus, .app__body .btn:hover', 'color:' + theme.linkColor);
|
||||||
changeCss('.app__body .attachment .attachment__container', 'border-left-color:' + changeOpacity(theme.linkColor, 0.5));
|
changeCss('.app__body .attachment .attachment__container', 'border-left-color:' + changeOpacity(theme.linkColor, 0.5));
|
||||||
changeCss('.app__body .channel-header__links .icon:hover, .app__body .post .flag-icon__container.visible, .app__body .post .comment-icon__container, .app__body .post .post__reply', 'fill:' + theme.linkColor);
|
changeCss('.app__body .channel-header__links .icon:hover, .app__body .post .flag-icon__container.visible, .app__body .post .comment-icon__container, .app__body .post .post__reply', 'fill:' + theme.linkColor);
|
||||||
|
changeCss('.app__body .nav-pills__unread-indicator-bottom', 'background:' + theme.linkColor);
|
||||||
|
changeCss('.app__body .nav-pills__unread-indicator-bottom:hover', 'background:' + changeColor(theme.linkColor, 0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme.buttonBg) {
|
if (theme.buttonBg) {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user