Merge pull request #1740 from hmhealey/plt730
PLT-730/PLT-731 Added remaining mobile UI V2 components
Этот коммит содержится в:
@@ -7,6 +7,7 @@ import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
|||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
import Post from './post.jsx';
|
import Post from './post.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
import DelayedAction from '../utils/delayed_action.jsx';
|
||||||
const Preferences = Constants.Preferences;
|
const Preferences = Constants.Preferences;
|
||||||
|
|
||||||
export default class PostsView extends React.Component {
|
export default class PostsView extends React.Component {
|
||||||
@@ -15,18 +16,26 @@ export default class PostsView extends React.Component {
|
|||||||
|
|
||||||
this.updateState = this.updateState.bind(this);
|
this.updateState = this.updateState.bind(this);
|
||||||
this.handleScroll = this.handleScroll.bind(this);
|
this.handleScroll = this.handleScroll.bind(this);
|
||||||
|
this.handleScrollStop = this.handleScrollStop.bind(this);
|
||||||
this.isAtBottom = this.isAtBottom.bind(this);
|
this.isAtBottom = this.isAtBottom.bind(this);
|
||||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
||||||
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
|
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
|
||||||
this.createPosts = this.createPosts.bind(this);
|
this.createPosts = this.createPosts.bind(this);
|
||||||
this.updateScrolling = this.updateScrolling.bind(this);
|
this.updateScrolling = this.updateScrolling.bind(this);
|
||||||
this.handleResize = this.handleResize.bind(this);
|
this.handleResize = this.handleResize.bind(this);
|
||||||
|
this.scrollToBottom = this.scrollToBottom.bind(this);
|
||||||
|
|
||||||
this.jumpToPostNode = null;
|
this.jumpToPostNode = null;
|
||||||
this.wasAtBottom = true;
|
this.wasAtBottom = true;
|
||||||
this.scrollHeight = 0;
|
this.scrollHeight = 0;
|
||||||
|
|
||||||
this.state = {displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false')};
|
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||||
|
isScrolling: false,
|
||||||
|
topPostId: null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
static get SCROLL_TYPE_FREE() {
|
static get SCROLL_TYPE_FREE() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -69,6 +78,55 @@ export default class PostsView extends React.Component {
|
|||||||
this.props.postViewScrolled(this.isAtBottom());
|
this.props.postViewScrolled(this.isAtBottom());
|
||||||
this.prevScrollHeight = this.refs.postlist.scrollHeight;
|
this.prevScrollHeight = this.refs.postlist.scrollHeight;
|
||||||
this.prevOffsetTop = this.jumpToPostNode.offsetTop;
|
this.prevOffsetTop = this.jumpToPostNode.offsetTop;
|
||||||
|
|
||||||
|
this.updateFloatingTimestamp();
|
||||||
|
|
||||||
|
if (!this.state.isScrolling) {
|
||||||
|
this.setState({
|
||||||
|
isScrolling: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollStopAction.fireAfter(1000);
|
||||||
|
}
|
||||||
|
handleScrollStop() {
|
||||||
|
this.setState({
|
||||||
|
isScrolling: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateFloatingTimestamp() {
|
||||||
|
// skip this in non-mobile view since that's when the timestamp is visible
|
||||||
|
if ($(window).width() > 768) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.postList) {
|
||||||
|
// iterate through posts starting at the bottom since users are more likely to be viewing newer posts
|
||||||
|
for (let i = 0; i < this.props.postList.order.length; i++) {
|
||||||
|
const id = this.props.postList.order[i];
|
||||||
|
const element = ReactDOM.findDOMNode(this.refs[id]);
|
||||||
|
|
||||||
|
if (!element || element.offsetTop + element.clientHeight <= this.refs.postlist.scrollTop) {
|
||||||
|
// this post is off the top of the screen so the last one is at the top of the screen
|
||||||
|
let topPostId;
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
topPostId = this.props.postList.order[i - 1];
|
||||||
|
} else {
|
||||||
|
// the first post we look at should always be on the screen, but handle that case anyway
|
||||||
|
topPostId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topPostId !== this.state.topPostId) {
|
||||||
|
this.setState({
|
||||||
|
topPostId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loadMorePostsTop() {
|
loadMorePostsTop() {
|
||||||
this.props.loadMorePostsTopClicked();
|
this.props.loadMorePostsTopClicked();
|
||||||
@@ -226,9 +284,7 @@ export default class PostsView extends React.Component {
|
|||||||
}
|
}
|
||||||
updateScrolling() {
|
updateScrolling() {
|
||||||
if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) {
|
if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) {
|
||||||
window.requestAnimationFrame(() => {
|
this.scrollToBottom();
|
||||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
|
||||||
});
|
|
||||||
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) {
|
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
// If separator exists scroll to it. Otherwise scroll to bottom.
|
// If separator exists scroll to it. Otherwise scroll to bottom.
|
||||||
@@ -278,6 +334,11 @@ export default class PostsView extends React.Component {
|
|||||||
handleResize() {
|
handleResize() {
|
||||||
this.updateScrolling();
|
this.updateScrolling();
|
||||||
}
|
}
|
||||||
|
scrollToBottom() {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.postList != null) {
|
if (this.props.postList != null) {
|
||||||
this.updateScrolling();
|
this.updateScrolling();
|
||||||
@@ -322,6 +383,12 @@ export default class PostsView extends React.Component {
|
|||||||
if (nextState.displayNameType !== this.state.displayNameType) {
|
if (nextState.displayNameType !== this.state.displayNameType) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (this.state.topPostId !== nextState.topPostId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.state.isScrolling !== nextState.isScrolling) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -377,20 +444,36 @@ export default class PostsView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let topPost = null;
|
||||||
|
if (this.state.topPostId) {
|
||||||
|
topPost = this.props.postList.posts[this.state.topPostId];
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={activeClass}>
|
||||||
ref='postlist'
|
<FloatingTimestamp
|
||||||
className={'post-list-holder-by-time ' + activeClass}
|
isScrolling={this.state.isScrolling}
|
||||||
onScroll={this.handleScroll}
|
post={topPost}
|
||||||
>
|
/>
|
||||||
<div className='post-list__table'>
|
<ScrollToBottomArrows
|
||||||
<div
|
isScrolling={this.state.isScrolling}
|
||||||
ref='postlistcontent'
|
atBottom={this.wasAtBottom}
|
||||||
className='post-list__content'
|
onClick={this.scrollToBottom}
|
||||||
>
|
/>
|
||||||
{moreMessagesTop}
|
<div
|
||||||
{postElements}
|
ref='postlist'
|
||||||
{moreMessagesBottom}
|
className='post-list-holder-by-time'
|
||||||
|
onScroll={this.handleScroll}
|
||||||
|
>
|
||||||
|
<div className='post-list__table'>
|
||||||
|
<div
|
||||||
|
ref='postlistcontent'
|
||||||
|
className='post-list__content'
|
||||||
|
>
|
||||||
|
{moreMessagesTop}
|
||||||
|
{postElements}
|
||||||
|
{moreMessagesBottom}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -414,3 +497,46 @@ PostsView.propTypes = {
|
|||||||
messageSeparatorTime: React.PropTypes.number,
|
messageSeparatorTime: React.PropTypes.number,
|
||||||
postsToHighlight: React.PropTypes.object
|
postsToHighlight: React.PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function FloatingTimestamp({isScrolling, post}) {
|
||||||
|
// only show on mobile
|
||||||
|
if ($(window).width() > 768) {
|
||||||
|
return <noscript />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return <noscript />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateString = Utils.getDateForUnixTicks(post.create_at).toDateString();
|
||||||
|
|
||||||
|
let className = 'post-list__timestamp';
|
||||||
|
if (isScrolling) {
|
||||||
|
className += ' scrolling';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<span>{dateString}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollToBottomArrows({isScrolling, atBottom, onClick}) {
|
||||||
|
// only show on mobile
|
||||||
|
if ($(window).width() > 768) {
|
||||||
|
return <noscript />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let className = 'post-list__arrows';
|
||||||
|
if (isScrolling && !atBottom) {
|
||||||
|
className += ' scrolling';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
27
web/react/utils/delayed_action.jsx
Обычный файл
27
web/react/utils/delayed_action.jsx
Обычный файл
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
export default class DelayedAction {
|
||||||
|
constructor(action) {
|
||||||
|
this.action = action;
|
||||||
|
|
||||||
|
this.timer = -1;
|
||||||
|
|
||||||
|
// bind fire since it doesn't get passed the correct this value with setTimeout
|
||||||
|
this.fire = this.fire.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
fire() {
|
||||||
|
this.action();
|
||||||
|
|
||||||
|
this.timer = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireAfter(timeout) {
|
||||||
|
if (this.timer >= 0) {
|
||||||
|
window.clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timer = window.setTimeout(this.fire, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -567,7 +567,7 @@ export function applyTheme(theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (theme.sidebarHeaderBg) {
|
if (theme.sidebarHeaderBg) {
|
||||||
changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1);
|
changeCss('.sidebar--left .team__header, .sidebar--menu .team__header, .post-list__timestamp', 'background:' + theme.sidebarHeaderBg, 1);
|
||||||
changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
|
changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
|
||||||
changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
|
changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
|
||||||
changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
|
changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
|
||||||
@@ -575,7 +575,7 @@ export function applyTheme(theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (theme.sidebarHeaderTextColor) {
|
if (theme.sidebarHeaderTextColor) {
|
||||||
changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info', 'color:' + theme.sidebarHeaderTextColor, 1);
|
changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info, .post-list__timestamp', 'color:' + theme.sidebarHeaderTextColor, 1);
|
||||||
changeCss('.sidebar--left .team__header .navbar-right .dropdown__icon, .sidebar--menu .team__header .navbar-right .dropdown__icon', 'fill:' + theme.sidebarHeaderTextColor, 1);
|
changeCss('.sidebar--left .team__header .navbar-right .dropdown__icon, .sidebar--menu .team__header .navbar-right .dropdown__icon', 'fill:' + theme.sidebarHeaderTextColor, 1);
|
||||||
changeCss('.sidebar--left .team__header .user__name, .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8), 1);
|
changeCss('.sidebar--left .team__header .user__name, .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8), 1);
|
||||||
changeCss('.sidebar--left .team__header:hover .user__name, .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor, 1);
|
changeCss('.sidebar--left .team__header:hover .user__name, .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor, 1);
|
||||||
|
|||||||
@@ -211,6 +211,10 @@ body.ios {
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
.inactive {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.post-list-holder-by-time {
|
.post-list-holder-by-time {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@@ -222,9 +226,6 @@ body.ios {
|
|||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0px !important;
|
width: 0px !important;
|
||||||
}
|
}
|
||||||
&.inactive {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.active {
|
&.active {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
@@ -247,6 +248,50 @@ body.ios {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-list__timestamp {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 50;
|
||||||
|
width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
background: $primary-color;
|
||||||
|
color: #fff;
|
||||||
|
@include border-radius(3px);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 25px;
|
||||||
|
margin-left: -60px;
|
||||||
|
-webkit-font-smoothing: initial;
|
||||||
|
@include single-transition(all, 0.3s, ease);
|
||||||
|
@include translateY(-45px);
|
||||||
|
@include opacity(0);
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.scrolling {
|
||||||
|
@include single-transition(all, 0.3s, ease);
|
||||||
|
@include translateY(0);
|
||||||
|
@include opacity(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-list__arrows {
|
||||||
|
background: url('../images/postArrows.png') center;
|
||||||
|
@include background-size(28px 28px);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 50px;
|
||||||
|
right: 5px;
|
||||||
|
z-index: 50;
|
||||||
|
@include opacity(0);
|
||||||
|
@include single-transition(all, 0.3s);
|
||||||
|
|
||||||
|
&.scrolling {
|
||||||
|
@include opacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-create__container {
|
.post-create__container {
|
||||||
form {
|
form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -242,6 +242,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.post-list__timestamp {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.signup-team__container {
|
.signup-team__container {
|
||||||
padding: 30px 0;
|
padding: 30px 0;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
@@ -800,4 +803,4 @@
|
|||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Двоичные данные
web/static/images/postArrows.png
Обычный файл
Двоичные данные
web/static/images/postArrows.png
Обычный файл
Двоичный файл не отображается.
|
После Ширина: | Высота: | Размер: 5.6 KiB |
Ссылка в новой задаче
Block a user