diff options
author | Joram Wilander <jwawilander@gmail.com> | 2015-12-28 09:02:20 -0500 |
---|---|---|
committer | Joram Wilander <jwawilander@gmail.com> | 2015-12-28 09:02:20 -0500 |
commit | 2f04ea6dd292e2b11e1fc4149ae1ea759d31bea5 (patch) | |
tree | 37dbea76dbe274d417b14226e7527a1c610d20d6 | |
parent | 0b55c5f86186ee5cce9cf29f3f560b2dd5b15277 (diff) | |
parent | cf0052556500dcb76b32e570a7806634cfe955da (diff) | |
download | chat-2f04ea6dd292e2b11e1fc4149ae1ea759d31bea5.tar.gz chat-2f04ea6dd292e2b11e1fc4149ae1ea759d31bea5.tar.bz2 chat-2f04ea6dd292e2b11e1fc4149ae1ea759d31bea5.zip |
Merge pull request #1740 from hmhealey/plt730
PLT-730/PLT-731 Added remaining mobile UI V2 components
-rw-r--r-- | web/react/components/posts_view.jsx | 160 | ||||
-rw-r--r-- | web/react/utils/delayed_action.jsx | 27 | ||||
-rw-r--r-- | web/react/utils/utils.jsx | 4 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post.scss | 51 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_responsive.scss | 5 | ||||
-rw-r--r-- | web/static/images/postArrows.png | bin | 0 -> 5684 bytes |
6 files changed, 224 insertions, 23 deletions
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index e116fdeea..a28efbd04 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -7,6 +7,7 @@ import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import * as Utils from '../utils/utils.jsx'; import Post from './post.jsx'; import Constants from '../utils/constants.jsx'; +import DelayedAction from '../utils/delayed_action.jsx'; const Preferences = Constants.Preferences; 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.handleScroll = this.handleScroll.bind(this); + this.handleScrollStop = this.handleScrollStop.bind(this); this.isAtBottom = this.isAtBottom.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this); this.createPosts = this.createPosts.bind(this); this.updateScrolling = this.updateScrolling.bind(this); this.handleResize = this.handleResize.bind(this); + this.scrollToBottom = this.scrollToBottom.bind(this); this.jumpToPostNode = null; this.wasAtBottom = true; 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() { return 1; @@ -69,6 +78,55 @@ export default class PostsView extends React.Component { this.props.postViewScrolled(this.isAtBottom()); this.prevScrollHeight = this.refs.postlist.scrollHeight; 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() { this.props.loadMorePostsTopClicked(); @@ -226,9 +284,7 @@ export default class PostsView extends React.Component { } updateScrolling() { if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) { - window.requestAnimationFrame(() => { - this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; - }); + this.scrollToBottom(); } else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) { window.requestAnimationFrame(() => { // If separator exists scroll to it. Otherwise scroll to bottom. @@ -278,6 +334,11 @@ export default class PostsView extends React.Component { handleResize() { this.updateScrolling(); } + scrollToBottom() { + window.requestAnimationFrame(() => { + this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; + }); + } componentDidMount() { if (this.props.postList != null) { this.updateScrolling(); @@ -322,6 +383,12 @@ export default class PostsView extends React.Component { if (nextState.displayNameType !== this.state.displayNameType) { return true; } + if (this.state.topPostId !== nextState.topPostId) { + return true; + } + if (this.state.isScrolling !== nextState.isScrolling) { + return true; + } 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 ( - <div - ref='postlist' - className={'post-list-holder-by-time ' + activeClass} - onScroll={this.handleScroll} - > - <div className='post-list__table'> - <div - ref='postlistcontent' - className='post-list__content' - > - {moreMessagesTop} - {postElements} - {moreMessagesBottom} + <div className={activeClass}> + <FloatingTimestamp + isScrolling={this.state.isScrolling} + post={topPost} + /> + <ScrollToBottomArrows + isScrolling={this.state.isScrolling} + atBottom={this.wasAtBottom} + onClick={this.scrollToBottom} + /> + <div + ref='postlist' + 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> @@ -414,3 +497,46 @@ PostsView.propTypes = { messageSeparatorTime: React.PropTypes.number, 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} + /> + ); +} diff --git a/web/react/utils/delayed_action.jsx b/web/react/utils/delayed_action.jsx new file mode 100644 index 000000000..4f6239ad0 --- /dev/null +++ b/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); + } +} diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 52d88c5b9..24d27b10a 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -567,7 +567,7 @@ export function applyTheme(theme) { } 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('#navbar .navbar-default', '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) { - 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 .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); diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 67b28381f..c2df7b769 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -211,6 +211,10 @@ body.ios { overflow-y: hidden; height: 100%; + .inactive { + display: none; + } + .post-list-holder-by-time { background: #fff; overflow-y: scroll; @@ -222,9 +226,6 @@ body.ios { &::-webkit-scrollbar { width: 0px !important; } - &.inactive { - display: none; - } &.active { 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 { form { width: 100%; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index e1ceea3ad..635b46077 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -242,6 +242,9 @@ } } } + .post-list__timestamp { + display: block; + } .signup-team__container { padding: 30px 0; margin-bottom: 30px; @@ -800,4 +803,4 @@ font-size: 2em; } } -}
\ No newline at end of file +} diff --git a/web/static/images/postArrows.png b/web/static/images/postArrows.png Binary files differnew file mode 100644 index 000000000..7b5919fc3 --- /dev/null +++ b/web/static/images/postArrows.png |