Implementing Permalinks and jumping to post from search. Performance

improvements.
Этот коммит содержится в:
Christopher Speller
2015-11-18 17:29:06 -05:00
родитель 5ee226d7f9
Коммит 9e8cd93790
29 изменённых файлов: 1340 добавлений и 696 удалений

Просмотреть файл

@@ -37,6 +37,8 @@ type Page struct {
ClientCfg map[string]string ClientCfg map[string]string
User *model.User User *model.User
Team *model.Team Team *model.Team
Channel *model.Channel
PostID string
SessionTokenIndex int64 SessionTokenIndex int64
} }

Просмотреть файл

@@ -23,6 +23,7 @@ func InitPost(r *mux.Router) {
l4g.Debug("Initializing post api routes") l4g.Debug("Initializing post api routes")
r.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("GET") r.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("GET")
r.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET")
sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter() sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter()
sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST") sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST")
@@ -767,6 +768,41 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
} }
} }
func getPostById(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
postId := params["post_id"]
if len(postId) != 26 {
c.SetInvalidParam("getPostById", "postId")
return
}
if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
c.Err = result.Err
return
} else {
list := result.Data.(*model.PostList)
if len(list.Order) != 1 {
c.Err = model.NewAppError("getPostById", "Unable to get post", "")
return
}
post := list.Posts[list.Order[0]]
cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId)
if !c.HasPermissionsToChannel(cchan, "getPostById") {
return
}
if HandleEtag(list.Etag(), w, r) {
return
}
w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag())
w.Write([]byte(list.ToJson()))
}
}
func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r) params := mux.Vars(r)

Просмотреть файл

@@ -4,11 +4,13 @@
import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx'; import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx';
import CreatePost from './create_post.jsx'; import CreatePost from './create_post.jsx';
import PostsViewContainer from './posts_view_container.jsx'; import PostsViewContainer from './posts_view_container.jsx';
import PostFocusView from './post_focus_view.jsx';
import ChannelHeader from './channel_header.jsx'; import ChannelHeader from './channel_header.jsx';
import Navbar from './navbar.jsx'; import Navbar from './navbar.jsx';
import FileUploadOverlay from './file_upload_overlay.jsx'; import FileUploadOverlay from './file_upload_overlay.jsx';
import PreferenceStore from '../stores/preference_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx'; import UserStore from '../stores/user_store.jsx';
import Constants from '../utils/constants.jsx'; import Constants from '../utils/constants.jsx';
@@ -20,26 +22,48 @@ export default class CenterPanel extends React.Component {
super(props); super(props);
this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.onChannelChange = this.onChannelChange.bind(this);
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS}; this.state = {
showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS,
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS
};
} }
componentDidMount() { componentDidMount() {
PreferenceStore.addChangeListener(this.onPreferenceChange); PreferenceStore.addChangeListener(this.onPreferenceChange);
ChannelStore.addChangeListener(this.onChannelChange);
} }
componentWillUnmount() { componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange); PreferenceStore.removeChangeListener(this.onPreferenceChange);
ChannelStore.removeChangeListener(this.onChannelChange);
} }
onPreferenceChange() { onPreferenceChange() {
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS}); this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
} }
onChannelChange() {
this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
}
render() { render() {
let postsContainer; let postsContainer;
let createPost;
if (this.state.showTutorialScreens) { if (this.state.showTutorialScreens) {
postsContainer = <TutorialIntroScreens />; postsContainer = <TutorialIntroScreens />;
createPost = null;
} else if (this.state.showPostFocus) {
postsContainer = <PostFocusView />;
createPost = null;
} else { } else {
postsContainer = <PostsViewContainer />; postsContainer = <PostsViewContainer />;
createPost = (
<div
className='post-create__container'
id='post-create'
>
<CreatePost />
</div>
);
} }
return ( return (
@@ -62,12 +86,7 @@ export default class CenterPanel extends React.Component {
<ChannelHeader /> <ChannelHeader />
</div> </div>
{postsContainer} {postsContainer}
<div {createPost}
className='post-create__container'
id='post-create'
>
<CreatePost />
</div>
</div> </div>
</div> </div>
</div> </div>

Просмотреть файл

@@ -22,6 +22,17 @@ export default class ChannelInviteModal extends React.Component {
this.state = this.getStateFromStores(); this.state = this.getStateFromStores();
} }
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(this.props, nextProps)) {
return true;
}
if (!Utils.areObjectsEqual(this.state, nextState)) {
return true;
}
return false;
}
getStateFromStores() { getStateFromStores() {
function getId(user) { function getId(user) {
return user.id; return user.id;
@@ -105,7 +116,7 @@ export default class ChannelInviteModal extends React.Component {
} }
this.setState({inviteError: null, memberIds, nonmembers}); this.setState({inviteError: null, memberIds, nonmembers});
AsyncClient.getChannelExtraInfo(true); AsyncClient.getChannelExtraInfo();
}, },
(err) => { (err) => {
this.setState({inviteError: err.message}); this.setState({inviteError: err.message});

Просмотреть файл

@@ -27,8 +27,8 @@ export default class ChannelLoader extends React.Component {
componentDidMount() { componentDidMount() {
/* Initial aysnc loads */ /* Initial aysnc loads */
AsyncClient.getPosts(ChannelStore.getCurrentId()); AsyncClient.getPosts(ChannelStore.getCurrentId());
AsyncClient.getChannels(true, true); AsyncClient.getChannels();
AsyncClient.getChannelExtraInfo(true); AsyncClient.getChannelExtraInfo();
AsyncClient.findTeams(); AsyncClient.findTeams();
AsyncClient.getMyTeam(); AsyncClient.getMyTeam();
setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit

Просмотреть файл

@@ -25,6 +25,17 @@ export default class ChannelMembersModal extends React.Component {
state.showInviteModal = false; state.showInviteModal = false;
this.state = state; this.state = state;
} }
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(this.props, nextProps)) {
return true;
}
if (!Utils.areObjectsEqual(this.state, nextState)) {
return true;
}
return false;
}
getStateFromStores() { getStateFromStores() {
const users = UserStore.getActiveOnlyProfiles(); const users = UserStore.getActiveOnlyProfiles();
const memberList = ChannelStore.getCurrentExtraInfo().members; const memberList = ChannelStore.getCurrentExtraInfo().members;
@@ -74,6 +85,7 @@ export default class ChannelMembersModal extends React.Component {
if ($(window).width() > 768) { if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
} }
this.onChange();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) { if (this.props.show && !prevProps.show) {
@@ -130,7 +142,7 @@ export default class ChannelMembersModal extends React.Component {
} }
this.setState({memberList, nonmemberList}); this.setState({memberList, nonmemberList});
AsyncClient.getChannelExtraInfo(true); AsyncClient.getChannelExtraInfo();
}, },
(err) => { (err) => {
this.setState({inviteError: err.message}); this.setState({inviteError: err.message});

Просмотреть файл

@@ -8,6 +8,7 @@ import FilePreview from './file_preview.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as Client from '../utils/client.jsx'; import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx'; import * as AsyncClient from '../utils/async_client.jsx';
import * as Utils from '../utils/utils.jsx'; import * as Utils from '../utils/utils.jsx';
@@ -19,6 +20,7 @@ import PreferenceStore from '../stores/preference_store.jsx';
import SocketStore from '../stores/socket_store.jsx'; import SocketStore from '../stores/socket_store.jsx';
import Constants from '../utils/constants.jsx'; import Constants from '../utils/constants.jsx';
const Preferences = Constants.Preferences; const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps; const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
@@ -176,9 +178,7 @@ export default class CreatePost extends React.Component {
const channel = ChannelStore.get(this.state.channelId); const channel = ChannelStore.get(this.state.channelId);
PostStore.storePendingPost(post); EventHelpers.emitUserPostedEvent(post);
PostStore.storeDraft(channel.id, null);
PostStore.jumpPostsViewToBottom();
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel, Client.createPost(post, channel,
@@ -190,10 +190,7 @@ export default class CreatePost extends React.Component {
member.last_viewed_at = Date.now(); member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member); ChannelStore.setChannelMember(member);
AppDispatcher.handleServerAction({ EventHelpers.emitPostRecievedEvent(data);
type: ActionTypes.RECIEVED_POST,
post: data
});
}, },
(err) => { (err) => {
const state = {}; const state = {};

Просмотреть файл

@@ -105,7 +105,7 @@ export default class Post extends React.Component {
} else { } else {
commentRootId = post.id; commentRootId = post.id;
} }
for (let postId in posts) { for (const postId in posts) {
if (posts[postId].root_id === commentRootId) { if (posts[postId].root_id === commentRootId) {
commentCount += 1; commentCount += 1;
} }
@@ -114,53 +114,58 @@ export default class Post extends React.Component {
return commentCount; return commentCount;
} }
render() { render() {
var post = this.props.post; const post = this.props.post;
var parentPost = this.props.parentPost; const parentPost = this.props.parentPost;
var posts = this.props.posts; const posts = this.props.posts;
if (!post.props) { if (!post.props) {
post.props = {}; post.props = {};
} }
var type = 'Post'; let type = 'Post';
if (post.root_id && post.root_id.length > 0) { if (post.root_id && post.root_id.length > 0) {
type = 'Comment'; type = 'Comment';
} }
const commentCount = this.getCommentCount(this.props); const commentCount = this.getCommentCount(this.props);
var rootUser; let rootUser;
if (this.props.sameRoot) { if (this.props.sameRoot) {
rootUser = 'same--root'; rootUser = 'same--root';
} else { } else {
rootUser = 'other--root'; rootUser = 'other--root';
} }
var postType = ''; let postType = '';
if (type !== 'Post') { if (type !== 'Post') {
postType = 'post--comment'; postType = 'post--comment';
} else if (commentCount > 0) { } else if (commentCount > 0) {
postType = 'post--root'; postType = 'post--root';
} }
var currentUserCss = ''; let currentUserCss = '';
if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) { if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) {
currentUserCss = 'current--user'; currentUserCss = 'current--user';
} }
var userProfile = UserStore.getProfile(post.user_id); const userProfile = UserStore.getProfile(post.user_id);
var timestamp = UserStore.getCurrentUser().update_at; let timestamp = UserStore.getCurrentUser().update_at;
if (userProfile) { if (userProfile) {
timestamp = userProfile.update_at; timestamp = userProfile.update_at;
} }
var sameUserClass = ''; let sameUserClass = '';
if (this.props.sameUser) { if (this.props.sameUser) {
sameUserClass = 'same--user'; sameUserClass = 'same--user';
} }
var profilePic = null; let shouldHighlightClass = '';
if (this.props.shouldHighlight) {
shouldHighlightClass = 'post--highlight';
}
let profilePic = null;
if (!this.props.hideProfilePic) { if (!this.props.hideProfilePic) {
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
@@ -182,7 +187,7 @@ export default class Post extends React.Component {
<div> <div>
<div <div
id={'post_' + post.id} id={'post_' + post.id}
className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss} className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass}
> >
<div className='post__content'> <div className='post__content'>
<div className='post__img'>{profilePic}</div> <div className='post__img'>{profilePic}</div>
@@ -218,5 +223,6 @@ Post.propTypes = {
sameUser: React.PropTypes.bool, sameUser: React.PropTypes.bool,
sameRoot: React.PropTypes.bool, sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool, hideProfilePic: React.PropTypes.bool,
isLastComment: React.PropTypes.bool isLastComment: React.PropTypes.bool,
shouldHighlight: React.PropTypes.bool
}; };

110
web/react/components/post_focus_view.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,110 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PostsView from './posts_view.jsx';
import PostStore from '../stores/post_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
export default class PostFocusView extends React.Component {
constructor(props) {
super(props);
this.onChannelChange = this.onChannelChange.bind(this);
this.onPostsChange = this.onPostsChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
const focusedPostId = PostStore.getFocusedPostId();
this.state = {
scrollType: PostsView.SCROLL_TYPE_POST,
scrollPostId: focusedPostId,
postList: PostStore.getVisiblePosts(focusedPostId),
atTop: PostStore.getVisibilityAtTop(focusedPostId),
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
};
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChannelChange);
PostStore.addChangeListener(this.onPostsChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChannelChange);
PostStore.removeChangeListener(this.onPostsChange);
}
onChannelChange() {
this.setState({
scrollType: PostsView.SCROLL_TYPE_POST
});
}
onPostsChange() {
const focusedPostId = PostStore.getFocusedPostId();
if (focusedPostId == null) {
return;
}
this.setState({
scrollPostId: focusedPostId,
postList: PostStore.getVisiblePosts(focusedPostId),
atTop: PostStore.getVisibilityAtTop(focusedPostId),
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
});
}
handlePostsViewScroll() {
this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
}
loadMorePostsTop() {
EventHelpers.emitLoadMorePostsFocusedTopEvent();
}
loadMorePostsBottom() {
EventHelpers.emitLoadMorePostsFocusedBottomEvent();
}
getIntroMessage() {
return (
<div className='channel-intro'>
<h4 className='channel-intro__title'>{'Beginning of Channel'}</h4>
</div>
);
}
render() {
const postsToHighlight = {};
postsToHighlight[this.state.scrollPostId] = true;
return (
<div id='post-list'>
<PostsView
key={'postfocusview'}
isActive={true}
postList={this.state.postList}
scrollType={this.state.scrollType}
scrollPostId={this.state.scrollPostId}
postViewScrolled={this.handlePostsViewScroll}
loadMorePostsTopClicked={this.loadMorePostsTop}
loadMorePostsBottomClicked={this.loadMorePostsBottom}
showMoreMessagesTop={!this.state.atTop}
showMoreMessagesBottom={!this.state.atBottom}
introText={this.getIntroMessage()}
messageSeparatorTime={0}
postsToHighlight={postsToHighlight}
/>
</div>
);
}
}
PostFocusView.defaultProps = {
};
PostFocusView.propTypes = {
};

Просмотреть файл

@@ -3,20 +3,28 @@
import DeletePostModal from './delete_post_modal.jsx'; import DeletePostModal from './delete_post_modal.jsx';
import UserStore from '../stores/user_store.jsx'; import UserStore from '../stores/user_store.jsx';
import * as utils from '../utils/utils.jsx'; import TeamStore from '../stores/team_store.jsx';
import * as Utils from '../utils/utils.jsx';
import TimeSince from './time_since.jsx'; import TimeSince from './time_since.jsx';
import Constants from '../utils/constants.jsx'; import Constants from '../utils/constants.jsx';
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
const Popover = ReactBootstrap.Popover;
export default class PostInfo extends React.Component { export default class PostInfo extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {}; this.state = {
copiedLink: false
};
this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this);
} }
createDropdown() { createDropdown() {
var post = this.props.post; var post = this.props.post;
var isOwner = UserStore.getCurrentId() === post.user_id; var isOwner = UserStore.getCurrentId() === post.user_id;
var isAdmin = utils.isAdmin(UserStore.getCurrentUser().roles); var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) { if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) {
return ''; return '';
@@ -113,6 +121,21 @@ export default class PostInfo extends React.Component {
</div> </div>
); );
} }
handlePermalinkCopy() {
const textBox = $(ReactDOM.findDOMNode(this.refs.permalinkbox));
textBox.select();
try {
const successful = document.execCommand('copy');
if (successful) {
this.setState({copiedLink: true});
} else {
this.setState({copiedLink: false});
}
} catch (err) {
this.setState({copiedLink: false});
}
}
render() { render() {
var post = this.props.post; var post = this.props.post;
var comments = ''; var comments = '';
@@ -143,6 +166,37 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown(); var dropdown = this.createDropdown();
const permalink = TeamStore.getCurrentTeamUrl() + '/pl/' + post.id;
const copyButtonText = this.state.copiedLink ? (<div>{'Copy '}<i className='fa fa-check'/></div>) : 'Copy';
const permalinkOverlay = (
<Popover
id='permalink-overlay'
className='permalink-popover'
placement='left'
title=''
>
<div className='form-inline'>
<input
type='text'
readOnly='true'
ref='permalinkbox'
className='permalink-text form-control no-resize min-height input-large'
rows='1'
value={permalink}
/>
<button
data-copy-btn='true'
type='button'
className='btn btn-primary'
onClick={this.handlePermalinkCopy}
data-clipboard-text={permalink}
>
{copyButtonText}
</button>
</div>
</Popover>
);
return ( return (
<ul className='post__header post__header--info'> <ul className='post__header post__header--info'>
<li className='col'> <li className='col'>
@@ -152,6 +206,15 @@ export default class PostInfo extends React.Component {
</li> </li>
<li className='col col__reply'> <li className='col col__reply'>
{comments} {comments}
<OverlayTrigger
trigger='click'
placement='left'
rootClose={true}
overlay={permalinkOverlay}
>
<i className={'permalink-icon fa fa-link ' + showCommentClass}/>
</OverlayTrigger>
<div className='dropdown'> <div className='dropdown'>
{dropdown} {dropdown}
</div> </div>

Просмотреть файл

@@ -2,6 +2,7 @@
// See License.txt for license information. // See License.txt for license information.
import UserStore from '../stores/user_store.jsx'; import UserStore from '../stores/user_store.jsx';
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';
@@ -13,6 +14,7 @@ export default class PostsView extends React.Component {
this.handleScroll = this.handleScroll.bind(this); this.handleScroll = this.handleScroll.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.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);
@@ -27,12 +29,15 @@ export default class PostsView extends React.Component {
static get SCROLL_TYPE_BOTTOM() { static get SCROLL_TYPE_BOTTOM() {
return 2; return 2;
} }
static get SIDEBAR_OPEN() { static get SCROLL_TYPE_SIDEBAR_OPEN() {
return 3; return 3;
} }
static get SCROLL_TYPE_NEW_MESSAGE() { static get SCROLL_TYPE_NEW_MESSAGE() {
return 4; return 4;
} }
static get SCROLL_TYPE_POST() {
return 5;
}
isAtBottom() { isAtBottom() {
return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight); return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
} }
@@ -47,15 +52,22 @@ export default class PostsView extends React.Component {
} }
} }
this.wasAtBottom = this.isAtBottom(); this.wasAtBottom = this.isAtBottom();
if (!this.jumpToPostNode && childNodes.length > 0) {
this.jumpToPostNode = childNodes[childNodes.length - 1];
}
// --- -------- // --- --------
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;
} }
loadMorePostsTop() { loadMorePostsTop() {
this.props.loadMorePostsTopClicked(); this.props.loadMorePostsTopClicked();
} }
loadMorePostsBottom() {
this.props.loadMorePostsBottomClicked();
}
createPosts(posts, order) { createPosts(posts, order) {
const postCtls = []; const postCtls = [];
let previousPostDay = new Date(0); let previousPostDay = new Date(0);
@@ -63,12 +75,7 @@ export default class PostsView extends React.Component {
let renderedLastViewed = false; let renderedLastViewed = false;
let numToDisplay = this.props.numPostsToDisplay; for (let i = order.length - 1; i >= 0; i--) {
if (order.length - 1 < numToDisplay) {
numToDisplay = order.length - 1;
}
for (let i = numToDisplay; i >= 0; i--) {
const post = posts[order[i]]; const post = posts[order[i]];
const parentPost = posts[post.parent_id]; const parentPost = posts[post.parent_id];
const prevPost = posts[order[i + 1]]; const prevPost = posts[order[i + 1]];
@@ -113,6 +120,8 @@ export default class PostsView extends React.Component {
const keyPrefix = post.id ? post.id : i; const keyPrefix = post.id ? post.id : i;
const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
const postCtl = ( const postCtl = (
<Post <Post
key={keyPrefix + 'postKey'} key={keyPrefix + 'postKey'}
@@ -124,6 +133,8 @@ export default class PostsView extends React.Component {
posts={posts} posts={posts}
hideProfilePic={hideProfilePic} hideProfilePic={hideProfilePic}
isLastComment={isLastComment} isLastComment={isLastComment}
shouldHighlight={shouldHighlight}
onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
/> />
); );
@@ -185,9 +196,12 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
} }
}); });
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPost) { } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPost]); const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
if (postNode == null) {
return;
}
postNode.scrollIntoView(); postNode.scrollIntoView();
if (this.refs.postlist.scrollTop === postNode.offsetTop) { if (this.refs.postlist.scrollTop === postNode.offsetTop) {
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3); this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
@@ -195,7 +209,7 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop); this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop);
} }
}); });
} else if (this.props.scrollType === PostsView.SIDEBAR_OPEN) { } else if (this.props.scrollType === PostsView.SCROLL_TYPE_SIDEBAR_OPEN) {
// If we are at the bottom then stay there // If we are at the bottom then stay there
if (this.wasAtBottom) { if (this.wasAtBottom) {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
@@ -211,7 +225,10 @@ export default class PostsView extends React.Component {
} }
} else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) { } else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight); // Only need to jump if we added posts to the top.
if (this.jumpToPostNode && (this.jumpToPostNode.offsetTop !== this.prevOffsetTop)) {
this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
}
}); });
} }
} }
@@ -219,14 +236,18 @@ export default class PostsView extends React.Component {
this.updateScrolling(); this.updateScrolling();
} }
componentDidMount() { componentDidMount() {
this.updateScrolling(); if (this.props.postList != null) {
this.updateScrolling();
}
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
} }
componentDidUpdate() { componentDidUpdate() {
this.updateScrolling(); if (this.props.postList != null) {
this.updateScrolling();
}
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
if (this.props.isActive !== nextProps.isActive) { if (this.props.isActive !== nextProps.isActive) {
@@ -235,15 +256,12 @@ export default class PostsView extends React.Component {
if (this.props.postList !== nextProps.postList) { if (this.props.postList !== nextProps.postList) {
return true; return true;
} }
if (this.props.scrollPost !== nextProps.scrollPost) { if (this.props.scrollPostId !== nextProps.scrollPostId) {
return true; return true;
} }
if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) { if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) {
return true; return true;
} }
if (this.props.numPostsToDisplay !== nextProps.numPostsToDisplay) {
return true;
}
if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) { if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
return true; return true;
} }
@@ -256,7 +274,8 @@ export default class PostsView extends React.Component {
render() { render() {
let posts = []; let posts = [];
let order = []; let order = [];
let moreMessages; let moreMessagesTop;
let moreMessagesBottom;
let postElements; let postElements;
let activeClass = 'inactive'; let activeClass = 'inactive';
if (this.props.postList != null) { if (this.props.postList != null) {
@@ -264,10 +283,10 @@ export default class PostsView extends React.Component {
order = this.props.postList.order; order = this.props.postList.order;
// Create intro message or top loadmore link // Create intro message or top loadmore link
if (order.length >= this.props.numPostsToDisplay) { if (this.props.showMoreMessagesTop) {
moreMessages = ( moreMessagesTop = (
<a <a
ref='loadmore' ref='loadmoretop'
className='more-messages-text theme' className='more-messages-text theme'
href='#' href='#'
onClick={this.loadMorePostsTop} onClick={this.loadMorePostsTop}
@@ -276,7 +295,23 @@ export default class PostsView extends React.Component {
</a> </a>
); );
} else { } else {
moreMessages = this.props.introText; moreMessagesTop = this.props.introText;
}
// Give option to load more posts at bottom if nessisary
if (this.props.showMoreMessagesBottom) {
moreMessagesBottom = (
<a
ref='loadmorebottom'
className='more-messages-text theme'
href='#'
onClick={this.loadMorePostsBottom}
>
{'Load more messages'}
</a>
);
} else {
moreMessagesBottom = null;
} }
// Create post elements // Create post elements
@@ -299,8 +334,9 @@ export default class PostsView extends React.Component {
ref='postlistcontent' ref='postlistcontent'
className='post-list__content' className='post-list__content'
> >
{moreMessages} {moreMessagesTop}
{postElements} {postElements}
{moreMessagesBottom}
</div> </div>
</div> </div>
</div> </div>
@@ -313,11 +349,14 @@ PostsView.defaultProps = {
PostsView.propTypes = { PostsView.propTypes = {
isActive: React.PropTypes.bool, isActive: React.PropTypes.bool,
postList: React.PropTypes.object, postList: React.PropTypes.object,
scrollPost: React.PropTypes.string, scrollPostId: React.PropTypes.string,
scrollType: React.PropTypes.number, scrollType: React.PropTypes.number,
postViewScrolled: React.PropTypes.func.isRequired, postViewScrolled: React.PropTypes.func.isRequired,
loadMorePostsTopClicked: React.PropTypes.func.isRequired, loadMorePostsTopClicked: React.PropTypes.func.isRequired,
numPostsToDisplay: React.PropTypes.number, loadMorePostsBottomClicked: React.PropTypes.func.isRequired,
showMoreMessagesTop: React.PropTypes.bool,
showMoreMessagesBottom: React.PropTypes.bool,
introText: React.PropTypes.element, introText: React.PropTypes.element,
messageSeparatorTime: React.PropTypes.number messageSeparatorTime: React.PropTypes.number,
postsToHighlight: React.PropTypes.object
}; };

Просмотреть файл

@@ -9,12 +9,9 @@ import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx'; import PostStore from '../stores/post_store.jsx';
import * as Utils from '../utils/utils.jsx'; import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx'; import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import Constants from '../utils/constants.jsx'; import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx'; import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
@@ -27,27 +24,26 @@ export default class PostsViewContainer extends React.Component {
this.onPostsChange = this.onPostsChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
this.postsLoaded = this.postsLoaded.bind(this);
this.postsLoadedFailure = this.postsLoadedFailure.bind(this);
this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
const currentChannelId = ChannelStore.getCurrentId(); const currentChannelId = ChannelStore.getCurrentId();
const state = { const state = {
scrollType: PostsView.SCROLL_TYPE_BOTTOM, scrollType: PostsView.SCROLL_TYPE_BOTTOM,
scrollPost: null, scrollPost: null
numPostsToDisplay: Constants.POST_CHUNK_SIZE
}; };
if (currentChannelId) { if (currentChannelId) {
Object.assign(state, { Object.assign(state, {
currentChannelIndex: 0, currentChannelIndex: 0,
channels: [currentChannelId], channels: [currentChannelId],
postLists: [this.getChannelPosts(currentChannelId)] postLists: [this.getChannelPosts(currentChannelId)],
atTop: [PostStore.getVisibilityAtTop(currentChannelId)]
}); });
} else { } else {
Object.assign(state, { Object.assign(state, {
currentChannelIndex: null, currentChannelIndex: null,
channels: [], channels: [],
postLists: [] postLists: [],
atTop: []
}); });
} }
@@ -78,24 +74,21 @@ export default class PostsViewContainer extends React.Component {
}); });
break; break;
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN: case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
this.setState({scrollType: PostsView.SIDEBAR_OPEN}); this.setState({scrollType: PostsView.SCROLL_TYPE_SIDEBAR_OPEN});
break; break;
} }
} }
onChannelChange() { onChannelChange() {
const postLists = this.state.postLists.slice(); const postLists = this.state.postLists.slice();
const atTop = this.state.atTop.slice();
const channels = this.state.channels.slice(); const channels = this.state.channels.slice();
const channelId = ChannelStore.getCurrentId(); const channelId = ChannelStore.getCurrentId();
// Has the channel really changed? // Has the channel really changed?
if (channelId === channels[this.state.currentChannelIndex]) { if (channelId === channels[this.state.currentChannelIndex]) {
// Dirty hack
this.forceUpdate();
return; return;
} }
PostStore.clearUnseenDeletedPosts(channelId);
let lastViewed = Number.MAX_VALUE; let lastViewed = Number.MAX_VALUE;
const member = ChannelStore.getMember(channelId); const member = ChannelStore.getMember(channelId);
if (member != null) { if (member != null) {
@@ -107,115 +100,45 @@ export default class PostsViewContainer extends React.Component {
newIndex = channels.length; newIndex = channels.length;
channels.push(channelId); channels.push(channelId);
postLists[newIndex] = this.getChannelPosts(channelId); postLists[newIndex] = this.getChannelPosts(channelId);
atTop[newIndex] = PostStore.getVisibilityAtTop(channelId);
} }
this.setState({ this.setState({
currentChannelIndex: newIndex, currentChannelIndex: newIndex,
currentLastViewed: lastViewed, currentLastViewed: lastViewed,
scrollType: PostsView.SCROLL_TYPE_NEW_MESSAGE, scrollType: PostsView.SCROLL_TYPE_NEW_MESSAGE,
channels, channels,
postLists}); postLists,
atTop});
} }
onChannelLeave(id) { onChannelLeave(id) {
const postLists = this.state.postLists.slice(); const postLists = this.state.postLists.slice();
const channels = this.state.channels.slice(); const channels = this.state.channels.slice();
const atTop = this.state.atTop.slice();
const index = channels.indexOf(id); const index = channels.indexOf(id);
if (index !== -1) { if (index !== -1) {
postLists.splice(index, 1); postLists.splice(index, 1);
channels.splice(index, 1); channels.splice(index, 1);
atTop.splice(index, 1);
} }
this.setState({channels, postLists}); this.setState({channels, postLists, atTop});
} }
onPostsChange() { onPostsChange() {
const channels = this.state.channels; const channels = this.state.channels;
const postLists = this.state.postLists.slice(); const postLists = this.state.postLists.slice();
const newPostsView = this.getChannelPosts(channels[this.state.currentChannelIndex]); const atTop = this.state.atTop.slice();
const currentChannelId = channels[this.state.currentChannelIndex];
const newPostsView = this.getChannelPosts(currentChannelId);
postLists[this.state.currentChannelIndex] = newPostsView; postLists[this.state.currentChannelIndex] = newPostsView;
this.setState({postLists}); atTop[this.state.currentChannelIndex] = PostStore.getVisibilityAtTop(currentChannelId);
this.setState({postLists, atTop});
} }
getChannelPosts(id) { getChannelPosts(id) {
const postList = PostStore.getPosts(id); return PostStore.getVisiblePosts(id);
if (postList != null) {
const deletedPosts = PostStore.getUnseenDeletedPosts(id);
if (deletedPosts && Object.keys(deletedPosts).length > 0) {
for (const pid in deletedPosts) {
if (deletedPosts.hasOwnProperty(pid)) {
postList.posts[pid] = deletedPosts[pid];
postList.order.unshift(pid);
}
}
postList.order.sort((a, b) => {
if (postList.posts[a].create_at > postList.posts[b].create_at) {
return -1;
}
if (postList.posts[a].create_at < postList.posts[b].create_at) {
return 1;
}
return 0;
});
}
const pendingPostList = PostStore.getPendingPosts(id);
if (pendingPostList) {
postList.order = pendingPostList.order.concat(postList.order);
for (const ppid in pendingPostList.posts) {
if (pendingPostList.posts.hasOwnProperty(ppid)) {
postList.posts[ppid] = pendingPostList.posts[ppid];
}
}
}
}
return postList;
} }
loadMorePostsTop() { loadMorePostsTop() {
const postLists = this.state.postLists; EventHelpers.emitLoadMorePostsEvent();
const channels = this.state.channels;
const currentChannelId = channels[this.state.currentChannelIndex];
const currentPostList = postLists[this.state.currentChannelIndex];
this.setState({numPostsToDisplay: this.state.numPostsToDisplay + Constants.POST_CHUNK_SIZE});
Client.getPostsPage(
currentChannelId,
currentPostList.order.length,
Constants.POST_CHUNK_SIZE,
this.postsLoaded,
this.postsLoadedFailure
);
}
postsLoaded(data) {
if (!data) {
return;
}
if (data.order.length === 0) {
return;
}
const postLists = this.state.postLists;
const currentPostList = postLists[this.state.currentChannelIndex];
const channels = this.state.channels;
const currentChannelId = channels[this.state.currentChannelIndex];
var newPostList = {};
newPostList.posts = Object.assign(currentPostList.posts, data.posts);
newPostList.order = currentPostList.order.concat(data.order);
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POSTS,
id: currentChannelId,
post_list: newPostList
});
Client.getProfiles();
}
postsLoadedFailure(err) {
AsyncClient.dispatchError(err, 'getPosts');
} }
handlePostsViewScroll(atBottom) { handlePostsViewScroll(atBottom) {
if (atBottom) { if (atBottom) {
@@ -246,15 +169,17 @@ export default class PostsViewContainer extends React.Component {
isActive={isActive} isActive={isActive}
postList={postLists[i]} postList={postLists[i]}
scrollType={this.state.scrollType} scrollType={this.state.scrollType}
scrollPost={this.state.scrollPost} scrollPostId={this.state.scrollPost}
postViewScrolled={this.handlePostsViewScroll} postViewScrolled={this.handlePostsViewScroll}
loadMorePostsTopClicked={this.loadMorePostsTop} loadMorePostsTopClicked={this.loadMorePostsTop}
numPostsToDisplay={this.state.numPostsToDisplay} loadMorePostsBottomClicked={() => {}}
showMoreMessagesTop={!this.state.atTop[this.state.currentChannelIndex]}
showMoreMessagesBottom={false}
introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null} introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null}
messageSeparatorTime={this.state.currentLastViewed} messageSeparatorTime={this.state.currentLastViewed}
/> />
); );
if ((!postLists[i] || !channel) && isActive) { if (!postLists[i] && isActive) {
postListCtls.push( postListCtls.push(
<LoadingScreen <LoadingScreen
position='absolute' position='absolute'

Просмотреть файл

@@ -94,7 +94,7 @@ export default class RhsThread extends React.Component {
return; return;
} }
var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id); var currentPosts = PostStore.getVisiblePosts(currentSelected.posts[currentSelected.order[0]].channel_id);
if (!currentPosts || currentPosts.order.length === 0) { if (!currentPosts || currentPosts.order.length === 0) {
return; return;

Просмотреть файл

@@ -1,17 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import SearchStore from '../stores/search_store.jsx';
import ChannelStore from '../stores/channel_store.jsx'; import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx'; import UserStore from '../stores/user_store.jsx';
import UserProfile from './user_profile.jsx'; import UserProfile from './user_profile.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as utils from '../utils/utils.jsx'; import * as utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from '../utils/constants.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx'; import * as TextFormatting from '../utils/text_formatting.jsx';
var ActionTypes = Constants.ActionTypes;
export default class SearchResultsItem extends React.Component { export default class SearchResultsItem extends React.Component {
constructor(props) { constructor(props) {
@@ -23,32 +18,7 @@ export default class SearchResultsItem extends React.Component {
handleClick(e) { handleClick(e) {
e.preventDefault(); e.preventDefault();
var self = this; EventHelpers.emitPostFocusEvent(this.props.post.id);
client.getPost(
this.props.post.channel_id,
this.props.post.id,
function success(data) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POST_SELECTED,
post_list: data,
from_search: SearchStore.getSearchTerm()
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_SEARCH,
results: null,
is_mention_search: self.props.isMentionSearch
});
},
function success(err) {
AsyncClient.dispatchError(err, 'getPost');
}
);
var postChannel = ChannelStore.get(this.props.post.channel_id);
utils.switchChannel(postChannel);
} }
render() { render() {

Просмотреть файл

@@ -201,10 +201,6 @@ export default class Sidebar extends React.Component {
}); });
} }
updateScrollbar() { updateScrollbar() {
if (this.state.windowWidth > 768) {
$('.nav-pills__container').perfectScrollbar();
$('.nav-pills__container').perfectScrollbar('update');
}
} }
onChange() { onChange() {
this.setState(this.getStateFromStores()); this.setState(this.getStateFromStores());

83
web/react/dispatcher/event_helpers.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,83 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
export function emitChannelClickEvent(channel) {
AsyncClient.getChannels();
AsyncClient.getChannelExtraInfo();
AsyncClient.updateLastViewedAt();
AsyncClient.getPosts(channel.id);
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: channel.name,
id: channel.id
});
}
export function emitPostFocusEvent(postId) {
Client.getPostById(
postId,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_FOCUSED_POST,
postId,
post_list: data
});
AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS);
AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS);
}
);
}
export function emitLoadMorePostsEvent() {
const id = ChannelStore.getCurrentId();
loadMorePostsTop(id);
}
export function emitLoadMorePostsFocusedTopEvent() {
const id = PostStore.getFocusedPostId();
loadMorePostsTop(id);
}
export function loadMorePostsTop(id) {
const earliestPostId = PostStore.getEarliestPost(id).id;
if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) {
AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE);
}
}
export function emitLoadMorePostsFocusedBottomEvent() {
const id = PostStore.getFocusedPostId();
const latestPostId = PostStore.getLatestPost(id).id;
AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE);
}
export function emitPostRecievedEvent(post) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POST,
post
});
}
export function emitUserPostedEvent(post) {
AppDispatcher.handleServerAction({
type: ActionTypes.CREATE_POST,
post
});
}
export function emitPostDeletedEvent(post) {
AppDispatcher.handleServerAction({
type: ActionTypes.POST_DELETED,
post
});
}

Просмотреть файл

@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import ChannelView from '../components/channel_view.jsx'; import ChannelView from '../components/channel_view.jsx';
import ChannelLoader from '../components/channel_loader.jsx'; import ChannelLoader from '../components/channel_loader.jsx';
import ErrorBar from '../components/error_bar.jsx'; import ErrorBar from '../components/error_bar.jsx';
@@ -23,15 +22,14 @@ import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx
import InviteMemberModal from '../components/invite_member_modal.jsx'; import InviteMemberModal from '../components/invite_member_modal.jsx';
import * as AsyncClient from '../utils/async_client.jsx'; import * as AsyncClient from '../utils/async_client.jsx';
import Constants from '../utils/constants.jsx'; import * as EventHelpers from '../dispatcher/event_helpers.jsx';
var ActionTypes = Constants.ActionTypes;
function setupChannelPage(props) { function setupChannelPage(props, team, channel) {
AppDispatcher.handleViewAction({ if (props.PostId === '') {
type: ActionTypes.CLICK_CHANNEL, EventHelpers.emitChannelClickEvent(channel);
name: props.ChannelName, } else {
id: props.ChannelId EventHelpers.emitPostFocusEvent(props.PostId);
}); }
AsyncClient.getAllPreferences(); AsyncClient.getAllPreferences();

Просмотреть файл

@@ -1,12 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import ChannelStore from '../stores/channel_store.jsx';
import TeamStore from '../stores/team_store.jsx'; import TeamStore from '../stores/team_store.jsx';
import Constants from '../utils/constants.jsx'; import Constants from '../utils/constants.jsx';
function setupHomePage() { function setupHomePage() {
var last = ChannelStore.getLastVisitedName(); var last = null;
if (last == null || last.length === 0) { if (last == null || last.length === 0) {
window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL; window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL;
} else { } else {

Просмотреть файл

@@ -8,8 +8,6 @@ var Utils;
import Constants from '../utils/constants.jsx'; import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
import BrowserStore from '../stores/browser_store.jsx';
const CHANGE_EVENT = 'change'; const CHANGE_EVENT = 'change';
const LEAVE_EVENT = 'leave'; const LEAVE_EVENT = 'leave';
const MORE_CHANGE_EVENT = 'change'; const MORE_CHANGE_EVENT = 'change';
@@ -21,7 +19,38 @@ class ChannelStoreClass extends EventEmitter {
this.setMaxListeners(11); this.setMaxListeners(11);
this.emitChange = this.emitChange.bind(this);
this.addChangeListener = this.addChangeListener.bind(this);
this.removeChangeListener = this.removeChangeListener.bind(this);
this.emitMoreChange = this.emitMoreChange.bind(this);
this.addMoreChangeListener = this.addMoreChangeListener.bind(this);
this.removeMoreChangeListener = this.removeMoreChangeListener.bind(this);
this.emitExtraInfoChange = this.emitExtraInfoChange.bind(this);
this.addExtraInfoChangeListener = this.addExtraInfoChangeListener.bind(this);
this.removeExtraInfoChangeListener = this.removeExtraInfoChangeListener.bind(this);
this.emitLeave = this.emitLeave.bind(this);
this.addLeaveListener = this.addLeaveListener.bind(this);
this.removeLeaveListener = this.removeLeaveListener.bind(this);
this.findFirstBy = this.findFirstBy.bind(this);
this.get = this.get.bind(this);
this.getMember = this.getMember.bind(this);
this.getByName = this.getByName.bind(this);
this.pSetPostMode = this.pSetPostMode.bind(this);
this.getPostMode = this.getPostMode.bind(this);
this.currentId = null; this.currentId = null;
this.postMode = this.POST_MODE_CHANNEL;
this.channels = [];
this.channelMembers = {};
this.moreChannels = {};
this.moreChannels.loading = true;
this.extraInfos = {};
}
get POST_MODE_CHANNEL() {
return 1;
}
get POST_MODE_FOCUS() {
return 2;
} }
emitChange() { emitChange() {
this.emit(CHANGE_EVENT); this.emit(CHANGE_EVENT);
@@ -90,16 +119,6 @@ class ChannelStoreClass extends EventEmitter {
setCurrentId(id) { setCurrentId(id) {
this.currentId = id; this.currentId = id;
} }
setLastVisitedName(name) {
if (name == null) {
BrowserStore.removeItem('last_visited_name');
} else {
BrowserStore.setItem('last_visited_name', name);
}
}
getLastVisitedName() {
return BrowserStore.getItem('last_visited_name');
}
resetCounts(id) { resetCounts(id) {
var cm = this.pGetChannelMembers(); var cm = this.pGetChannelMembers();
for (var cmid in cm) { for (var cmid in cm) {
@@ -192,10 +211,10 @@ class ChannelStoreClass extends EventEmitter {
this.pStoreChannels(channels); this.pStoreChannels(channels);
} }
pStoreChannels(channels) { pStoreChannels(channels) {
BrowserStore.setItem('channels', channels); this.channels = channels;
} }
pGetChannels() { pGetChannels() {
return BrowserStore.getItem('channels', []); return this.channels;
} }
pStoreChannelMember(channelMember) { pStoreChannelMember(channelMember) {
var members = this.pGetChannelMembers(); var members = this.pGetChannelMembers();
@@ -203,49 +222,58 @@ class ChannelStoreClass extends EventEmitter {
this.pStoreChannelMembers(members); this.pStoreChannelMembers(members);
} }
pStoreChannelMembers(channelMembers) { pStoreChannelMembers(channelMembers) {
BrowserStore.setItem('channel_members', channelMembers); this.channelMembers = channelMembers;
} }
pGetChannelMembers() { pGetChannelMembers() {
return BrowserStore.getItem('channel_members', {}); return this.channelMembers;
} }
pStoreMoreChannels(channels) { pStoreMoreChannels(channels) {
BrowserStore.setItem('more_channels', channels); this.moreChannels = channels;
} }
pGetMoreChannels() { pGetMoreChannels() {
var channels = BrowserStore.getItem('more_channels'); return this.moreChannels;
if (channels == null) {
channels = {};
channels.loading = true;
}
return channels;
} }
pStoreExtraInfos(extraInfos) { pStoreExtraInfos(extraInfos) {
BrowserStore.setItem('extra_infos', extraInfos); this.extraInfos = extraInfos;
} }
pGetExtraInfos() { pGetExtraInfos() {
return BrowserStore.getItem('extra_infos', {}); return this.extraInfos;
} }
isDefault(channel) { isDefault(channel) {
return channel.name === Constants.DEFAULT_CHANNEL; return channel.name === Constants.DEFAULT_CHANNEL;
} }
pSetPostMode(mode) {
this.postMode = mode;
}
getPostMode() {
return this.postMode;
}
} }
var ChannelStore = new ChannelStoreClass(); var ChannelStore = new ChannelStoreClass();
ChannelStore.dispatchToken = AppDispatcher.register(function handleAction(payload) { ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action; var action = payload.action;
var currentId; var currentId;
switch (action.type) { switch (action.type) {
case ActionTypes.CLICK_CHANNEL: case ActionTypes.CLICK_CHANNEL:
ChannelStore.setCurrentId(action.id); ChannelStore.setCurrentId(action.id);
ChannelStore.setLastVisitedName(action.name);
ChannelStore.resetCounts(action.id); ChannelStore.resetCounts(action.id);
ChannelStore.pSetPostMode(ChannelStore.POST_MODE_CHANNEL);
ChannelStore.emitChange(); ChannelStore.emitChange();
break; break;
case ActionTypes.RECIEVED_FOCUSED_POST: {
const post = action.post_list.posts[action.postId];
ChannelStore.setCurrentId(post.channel_id);
ChannelStore.pSetPostMode(ChannelStore.POST_MODE_FOCUS);
ChannelStore.emitChange();
break;
}
case ActionTypes.RECIEVED_CHANNELS: case ActionTypes.RECIEVED_CHANNELS:
ChannelStore.pStoreChannels(action.channels); ChannelStore.pStoreChannels(action.channels);
ChannelStore.pStoreChannelMembers(action.members); ChannelStore.pStoreChannelMembers(action.members);

Просмотреть файл

@@ -12,9 +12,10 @@ import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
const CHANGE_EVENT = 'change'; const CHANGE_EVENT = 'change';
const SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; const FOCUSED_POST_CHANGE = 'focused_post_change';
const EDIT_POST_EVENT = 'edit_post'; const EDIT_POST_EVENT = 'edit_post';
const POSTS_VIEW_JUMP_EVENT = 'post_list_jump'; const POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
class PostStoreClass extends EventEmitter { class PostStoreClass extends EventEmitter {
constructor() { constructor() {
@@ -24,10 +25,6 @@ class PostStoreClass extends EventEmitter {
this.addChangeListener = this.addChangeListener.bind(this); this.addChangeListener = this.addChangeListener.bind(this);
this.removeChangeListener = this.removeChangeListener.bind(this); this.removeChangeListener = this.removeChangeListener.bind(this);
this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
this.emitEditPost = this.emitEditPost.bind(this); this.emitEditPost = this.emitEditPost.bind(this);
this.addEditPostListener = this.addEditPostListener.bind(this); this.addEditPostListener = this.addEditPostListener.bind(this);
this.removeEditPostListener = this.removeEditPostListner.bind(this); this.removeEditPostListener = this.removeEditPostListner.bind(this);
@@ -36,27 +33,49 @@ class PostStoreClass extends EventEmitter {
this.addPostsViewJumpListener = this.addPostsViewJumpListener.bind(this); this.addPostsViewJumpListener = this.addPostsViewJumpListener.bind(this);
this.removePostsViewJumpListener = this.removePostsViewJumpListener.bind(this); this.removePostsViewJumpListener = this.removePostsViewJumpListener.bind(this);
this.getCurrentPosts = this.getCurrentPosts.bind(this); this.emitPostFocused = this.emitPostFocused.bind(this);
this.addPostFocusedListener = this.addPostFocusedListener.bind(this);
this.removePostFocusedListener = this.removePostFocusedListener.bind(this);
this.makePostsInfo = this.makePostsInfo.bind(this);
this.getAllPosts = this.getAllPosts.bind(this);
this.getEarliestPost = this.getEarliestPost.bind(this);
this.getLatestPost = this.getLatestPost.bind(this);
this.getVisiblePosts = this.getVisiblePosts.bind(this);
this.getVisibilityAtTop = this.getVisibilityAtTop.bind(this);
this.getVisibilityAtBottom = this.getVisibilityAtBottom.bind(this);
this.requestVisibilityIncrease = this.requestVisibilityIncrease.bind(this);
this.getFocusedPostId = this.getFocusedPostId.bind(this);
this.storePosts = this.storePosts.bind(this); this.storePosts = this.storePosts.bind(this);
this.pStorePosts = this.pStorePosts.bind(this);
this.getPosts = this.getPosts.bind(this);
this.getPost = this.getPost.bind(this);
this.storePost = this.storePost.bind(this); this.storePost = this.storePost.bind(this);
this.pStorePost = this.pStorePost.bind(this); this.storeFocusedPost = this.storeFocusedPost.bind(this);
this.checkBounds = this.checkBounds.bind(this);
this.clearFocusedPost = this.clearFocusedPost.bind(this);
this.clearChannelVisibility = this.clearChannelVisibility.bind(this);
this.removePost = this.removePost.bind(this); this.removePost = this.removePost.bind(this);
this.storePendingPost = this.storePendingPost.bind(this);
this.pStorePendingPosts = this.pStorePendingPosts.bind(this);
this.getPendingPosts = this.getPendingPosts.bind(this); this.getPendingPosts = this.getPendingPosts.bind(this);
this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this); this.storePendingPost = this.storePendingPost.bind(this);
this.storeUnseenDeletedPosts = this.storeUnseenDeletedPosts.bind(this);
this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this);
this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this);
this.removePendingPost = this.removePendingPost.bind(this); this.removePendingPost = this.removePendingPost.bind(this);
this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
this.clearPendingPosts = this.clearPendingPosts.bind(this); this.clearPendingPosts = this.clearPendingPosts.bind(this);
this.updatePendingPost = this.updatePendingPost.bind(this); this.updatePendingPost = this.updatePendingPost.bind(this);
this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this);
this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this);
this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this);
// These functions are bad and work should be done to remove this system when the RHS dies
this.storeSelectedPost = this.storeSelectedPost.bind(this); this.storeSelectedPost = this.storeSelectedPost.bind(this);
this.getSelectedPost = this.getSelectedPost.bind(this); this.getSelectedPost = this.getSelectedPost.bind(this);
this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
this.selectedPost = null;
this.getEmptyDraft = this.getEmptyDraft.bind(this); this.getEmptyDraft = this.getEmptyDraft.bind(this);
this.storeCurrentDraft = this.storeCurrentDraft.bind(this); this.storeCurrentDraft = this.storeCurrentDraft.bind(this);
this.getCurrentDraft = this.getCurrentDraft.bind(this); this.getCurrentDraft = this.getCurrentDraft.bind(this);
@@ -70,6 +89,9 @@ class PostStoreClass extends EventEmitter {
this.getLatestUpdate = this.getLatestUpdate.bind(this); this.getLatestUpdate = this.getLatestUpdate.bind(this);
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this); this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
this.getCommentCount = this.getCommentCount.bind(this); this.getCommentCount = this.getCommentCount.bind(this);
this.postsInfo = {};
this.currentFocusedPostId = null;
} }
emitChange() { emitChange() {
this.emit(CHANGE_EVENT); this.emit(CHANGE_EVENT);
@@ -83,16 +105,16 @@ class PostStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT, callback); this.removeListener(CHANGE_EVENT, callback);
} }
emitSelectedPostChange(fromSearch) { emitPostFocused() {
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); this.emit(FOCUSED_POST_CHANGE);
} }
addSelectedPostChangeListener(callback) { addPostFocusedListener(callback) {
this.on(SELECTED_POST_CHANGE_EVENT, callback); this.on(FOCUSED_POST_CHANGE, callback);
} }
removeSelectedPostChangeListener(callback) { removePostFocusedListener(callback) {
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback); this.removeListener(FOCUSED_POST_CHANGE, callback);
} }
emitEditPost(post) { emitEditPost(post) {
@@ -131,82 +153,336 @@ class PostStoreClass extends EventEmitter {
this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null); this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null);
} }
getCurrentPosts() { // All this does is makes sure the postsInfo is not null for the specified channel
var currentId = ChannelStore.getCurrentId(); makePostsInfo(id) {
if (!this.postsInfo.hasOwnProperty(id)) {
if (currentId != null) { this.postsInfo[id] = {};
return this.getPosts(currentId);
} }
}
getAllPosts(id) {
if (this.postsInfo.hasOwnProperty(id)) {
return Object.assign({}, this.postsInfo[id].postList);
}
return null; return null;
} }
storePosts(channelId, newPostsView) {
if (isPostListNull(newPostsView)) { getEarliestPost(id) {
if (this.postsInfo.hasOwnProperty(id)) {
return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[this.postsInfo[id].postList.order.length - 1]];
}
return null;
}
getLatestPost(id) {
if (this.postsInfo.hasOwnProperty(id)) {
return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[0]];
}
return null;
}
getVisiblePosts(id) {
if (this.postsInfo.hasOwnProperty(id) && this.postsInfo[id].hasOwnProperty('postList')) {
const postList = JSON.parse(JSON.stringify(this.postsInfo[id].postList));
// Only limit visibility if we are not focused on a post
if (this.currentFocusedPostId === null) {
postList.order = postList.order.slice(0, this.postsInfo[id].endVisible);
}
// Add pending posts
if (this.postsInfo[id].hasOwnProperty('pendingPosts')) {
Object.assign(postList.posts, this.postsInfo[id].pendingPosts.posts);
postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order);
}
// Add delteted posts
if (this.postsInfo[id].hasOwnProperty('deletedPosts')) {
Object.assign(postList.posts, this.postsInfo[id].deletedPosts);
for (const postID in this.postsInfo[id].deletedPosts) {
if (this.postsInfo[id].deletedPosts.hasOwnProperty(postID)) {
postList.order.push(postID);
}
}
// Merge would be faster
postList.order.sort((a, b) => {
if (postList.posts[a].create_at > postList.posts[b].create_at) {
return -1;
}
if (postList.posts[a].create_at < postList.posts[b].create_at) {
return 1;
}
return 0;
});
}
return postList;
}
return null;
}
getVisibilityAtTop(id) {
if (this.postsInfo.hasOwnProperty(id)) {
return this.postsInfo[id].atTop && this.postsInfo[id].endVisible >= this.postsInfo[id].postList.order.length;
}
return false;
}
getVisibilityAtBottom(id) {
if (this.postsInfo.hasOwnProperty(id)) {
return this.postsInfo[id].atBottom;
}
return false;
}
// Returns true if posts need to be fetched
requestVisibilityIncrease(id, ammount) {
const endVisible = this.postsInfo[id].endVisible;
const postList = this.postsInfo[id].postList;
if (this.getVisibilityAtTop(id)) {
return false;
}
this.postsInfo[id].endVisible += ammount;
this.emitChange();
return endVisible + ammount > postList.order.length;
}
getFocusedPostId() {
return this.currentFocusedPostId;
}
storePosts(id, newPosts) {
if (isPostListNull(newPosts)) {
return; return;
} }
var postList = makePostListNonNull(this.getPosts(channelId)); const combinedPosts = makePostListNonNull(this.getAllPosts(id));
for (const pid in newPostsView.posts) { for (const pid in newPosts.posts) {
if (newPostsView.posts.hasOwnProperty(pid)) { if (newPosts.posts.hasOwnProperty(pid)) {
const np = newPostsView.posts[pid]; const np = newPosts.posts[pid];
if (np.delete_at === 0) { if (np.delete_at === 0) {
postList.posts[pid] = np; combinedPosts.posts[pid] = np;
if (postList.order.indexOf(pid) === -1) { if (combinedPosts.order.indexOf(pid) === -1) {
postList.order.push(pid); combinedPosts.order.push(pid);
} }
} else { } else {
if (pid in postList.posts) { if (pid in combinedPosts.posts) {
delete postList.posts[pid]; Reflect.deleteProperty(combinedPosts.posts, pid);
} }
const index = postList.order.indexOf(pid); const index = combinedPosts.order.indexOf(pid);
if (index !== -1) { if (index !== -1) {
postList.order.splice(index, 1); combinedPosts.order.splice(index, 1);
} }
} }
} }
} }
postList.order.sort((a, b) => { combinedPosts.order.sort((a, b) => {
if (postList.posts[a].create_at > postList.posts[b].create_at) { if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) {
return -1; return -1;
} }
if (postList.posts[a].create_at < postList.posts[b].create_at) { if (combinedPosts.posts[a].create_at < combinedPosts.posts[b].create_at) {
return 1; return 1;
} }
return 0; return 0;
}); });
var latestUpdate = 0; this.makePostsInfo(id);
for (var pid in postList.posts) { this.postsInfo[id].postList = combinedPosts;
if (postList.posts[pid].update_at > latestUpdate) { }
latestUpdate = postList.posts[pid].update_at;
} storePost(post) {
const postList = makePostListNonNull(this.getAllPosts(post.channel_id));
if (post.pending_post_id !== '') {
this.removePendingPost(post.channel_id, post.pending_post_id);
} }
this.storeLatestUpdate(channelId, latestUpdate); post.pending_post_id = '';
this.pStorePosts(channelId, postList);
postList.posts[post.id] = post;
if (postList.order.indexOf(post.id) === -1) {
postList.order.unshift(post.id);
}
this.makePostsInfo(post.channel_id);
this.postsInfo[post.channel_id].postList = postList;
}
storeFocusedPost(postId, postList) {
const focusedPost = postList.posts[postId];
if (!focusedPost) {
return;
}
this.currentFocusedPostId = postId;
this.storePosts(postId, postList);
}
checkBounds(id, numRequested, postList, before) {
if (numRequested > postList.order.length) {
if (before) {
this.postsInfo[id].atTop = true;
} else {
this.postsInfo[id].atBottom = true;
}
}
}
clearFocusedPost() {
if (this.currentFocusedPostId != null) {
Reflect.deleteProperty(this.postsInfo, this.currentFocusedPostId);
this.currentFocusedPostId = null;
}
}
clearChannelVisibility(id, atBottom) {
this.makePostsInfo(id);
this.postsInfo[id].endVisible = Constants.POST_CHUNK_SIZE;
this.postsInfo[id].atTop = false;
this.postsInfo[id].atBottom = atBottom;
}
removePost(post) {
const channelId = post.channel_id;
this.makePostsInfo(channelId);
const postList = this.postsInfo[channelId].postList;
if (isPostListNull(postList)) {
return;
}
if (post.id in postList.posts) {
Reflect.deleteProperty(postList.posts, post.id);
}
const index = postList.order.indexOf(post.id);
if (index !== -1) {
postList.order.splice(index, 1);
}
this.postsInfo[channelId].postList = postList;
}
getPendingPosts(channelId) {
if (this.postsInfo.hasOwnProperty(channelId)) {
return this.postsInfo[channelId].pendingPosts;
}
return null;
}
storePendingPost(post) {
post.state = Constants.POST_LOADING;
const postList = makePostListNonNull(this.getPendingPosts(post.channel_id));
postList.posts[post.pending_post_id] = post;
postList.order.unshift(post.pending_post_id);
this.makePostsInfo(post.channel_id);
this.postsInfo[post.channel_id].pendingPosts = postList;
this.emitChange(); this.emitChange();
} }
pStorePosts(channelId, posts) {
BrowserStore.setItem('posts_' + channelId, posts); removePendingPost(channelId, pendingPostId) {
const postList = makePostListNonNull(this.getPendingPosts(channelId));
Reflect.deleteProperty(postList.posts, pendingPostId);
const index = postList.order.indexOf(pendingPostId);
if (index !== -1) {
postList.order.splice(index, 1);
}
this.postsInfo[channelId].pendingPosts = postList;
this.emitChange();
} }
getPosts(channelId) {
return BrowserStore.getItem('posts_' + channelId); clearPendingPosts(channelId) {
if (this.postsInfo.hasOwnProperty(channelId)) {
Reflect.deleteProperty(this.postsInfo[channelId], 'pendingPosts');
}
} }
getPost(channelId, postId) {
return this.getPosts(channelId).posts[postId]; updatePendingPost(post) {
const postList = makePostListNonNull(this.getPendingPosts(post.channel_id));
if (postList.order.indexOf(post.pending_post_id) === -1) {
return;
}
postList.posts[post.pending_post_id] = post;
this.postsInfo[post.channel_id].pendingPosts = postList;
this.emitChange();
} }
storeUnseenDeletedPost(post) {
let posts = this.getUnseenDeletedPosts(post.channel_id);
if (!posts) {
posts = {};
}
post.message = '(message deleted)';
post.state = Constants.POST_DELETED;
post.filenames = [];
posts[post.id] = post;
this.postsInfo[post.channel_id].deletedPosts = posts;
}
getUnseenDeletedPosts(channelId) {
if (this.postsInfo.hasOwnProperty(channelId)) {
return this.postsInfo[channelId].deletedPosts;
}
return null;
}
clearUnseenDeletedPosts(channelId) {
if (this.postsInfo.hasOwnProperty(channelId)) {
Reflect.deleteProperty(this.postsInfo[channelId], 'deletedPosts');
}
}
storeSelectedPost(postList) {
this.selectedPost = postList;
}
getSelectedPost() {
return this.selectedPost;
}
emitSelectedPostChange(fromSearch) {
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
}
addSelectedPostChangeListener(callback) {
this.on(SELECTED_POST_CHANGE_EVENT, callback);
}
removeSelectedPostChangeListener(callback) {
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
}
getCurrentUsersLatestPost(channelId, rootId) { getCurrentUsersLatestPost(channelId, rootId) {
const userId = UserStore.getCurrentId(); const userId = UserStore.getCurrentId();
var postList = makePostListNonNull(this.getPosts(channelId)); var postList = makePostListNonNull(this.getAllPosts(channelId));
var i = 0; var i = 0;
var len = postList.order.length; var len = postList.order.length;
var lastPost = null; var lastPost = null;
for (i; i < len; i++) { for (i; i < len; i++) {
let post = postList.posts[postList.order[i]]; const post = postList.posts[postList.order[i]];
if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) { if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) {
if (rootId) { if (rootId) {
if (post.root_id === rootId || post.id === rootId) { if (post.root_id === rootId || post.id === rootId) {
@@ -222,146 +498,7 @@ class PostStoreClass extends EventEmitter {
return lastPost; return lastPost;
} }
storePost(post) {
this.pStorePost(post);
this.emitChange();
}
pStorePost(post) {
var postList = this.getPosts(post.channel_id);
postList = makePostListNonNull(postList);
if (post.pending_post_id !== '') {
this.removePendingPost(post.channel_id, post.pending_post_id);
}
post.pending_post_id = '';
postList.posts[post.id] = post;
if (postList.order.indexOf(post.id) === -1) {
postList.order.unshift(post.id);
}
this.pStorePosts(post.channel_id, postList);
}
removePost(postId, channelId) {
var postList = this.getPosts(channelId);
if (isPostListNull(postList)) {
return;
}
if (postId in postList.posts) {
delete postList.posts[postId];
}
var index = postList.order.indexOf(postId);
if (index !== -1) {
postList.order.splice(index, 1);
}
this.pStorePosts(channelId, postList);
}
storePendingPost(post) {
post.state = Constants.POST_LOADING;
var postList = this.getPendingPosts(post.channel_id);
postList = makePostListNonNull(postList);
postList.posts[post.pending_post_id] = post;
postList.order.unshift(post.pending_post_id);
this.pStorePendingPosts(post.channel_id, postList);
this.emitChange();
}
pStorePendingPosts(channelId, postList) {
var posts = postList.posts;
// sort failed posts to the bottom
postList.order.sort((a, b) => {
if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) {
return 1;
}
if (posts[a].state === Constants.POST_FAILED && posts[b].state === Constants.POST_LOADING) {
return -1;
}
if (posts[a].create_at > posts[b].create_at) {
return -1;
}
if (posts[a].create_at < posts[b].create_at) {
return 1;
}
return 0;
});
BrowserStore.setGlobalItem('pending_posts_' + channelId, postList);
}
getPendingPosts(channelId) {
return BrowserStore.getGlobalItem('pending_posts_' + channelId);
}
storeUnseenDeletedPost(post) {
var posts = this.getUnseenDeletedPosts(post.channel_id);
if (!posts) {
posts = {};
}
post.message = '(message deleted)';
post.state = Constants.POST_DELETED;
post.filenames = [];
posts[post.id] = post;
this.storeUnseenDeletedPosts(post.channel_id, posts);
}
storeUnseenDeletedPosts(channelId, posts) {
BrowserStore.setItem('deleted_posts_' + channelId, posts);
}
getUnseenDeletedPosts(channelId) {
return BrowserStore.getItem('deleted_posts_' + channelId);
}
clearUnseenDeletedPosts(channelId) {
BrowserStore.setItem('deleted_posts_' + channelId, {});
}
removePendingPost(channelId, pendingPostId) {
this.pRemovePendingPost(channelId, pendingPostId);
this.emitChange();
}
pRemovePendingPost(channelId, pendingPostId) {
var postList = this.getPendingPosts(channelId);
postList = makePostListNonNull(postList);
if (pendingPostId in postList.posts) {
delete postList.posts[pendingPostId];
}
var index = postList.order.indexOf(pendingPostId);
if (index !== -1) {
postList.order.splice(index, 1);
}
this.pStorePendingPosts(channelId, postList);
}
clearPendingPosts() {
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => {
BrowserStore.removeItem(key);
});
}
updatePendingPost(post) {
var postList = this.getPendingPosts(post.channel_id);
postList = makePostListNonNull(postList);
if (postList.order.indexOf(post.pending_post_id) === -1) {
return;
}
postList.posts[post.pending_post_id] = post;
this.pStorePendingPosts(post.channel_id, postList);
this.emitChange();
}
storeSelectedPost(postList) {
BrowserStore.setItem('select_post', postList);
}
getSelectedPost() {
return BrowserStore.getItem('select_post');
}
getEmptyDraft() { getEmptyDraft() {
return {message: '', uploadsInProgress: [], previews: []}; return {message: '', uploadsInProgress: [], previews: []};
} }
@@ -402,16 +539,23 @@ class PostStoreClass extends EventEmitter {
}); });
} }
storeLatestUpdate(channelId, time) { storeLatestUpdate(channelId, time) {
BrowserStore.setItem('latest_post_' + channelId, time); if (!this.postsInfo.hasOwnProperty(channelId)) {
this.postsInfo[channelId] = {};
}
this.postsInfo[channelId].latestPost = time;
} }
getLatestUpdate(channelId) { getLatestUpdate(channelId) {
return BrowserStore.getItem('latest_post_' + channelId, 0); if (this.postsInfo.hasOwnProperty(channelId) && this.postsInfo[channelId].hasOwnProperty('latestPost')) {
return this.postsInfo[channelId].latestPost;
}
return 0;
} }
getCommentCount(post) { getCommentCount(post) {
const posts = this.getPosts(post.channel_id).posts; const posts = this.getPosts(post.channel_id).posts;
let commentCount = 0; let commentCount = 0;
for (let id in posts) { for (const id in posts) {
if (posts.hasOwnProperty(id)) { if (posts.hasOwnProperty(id)) {
if (posts[id].root_id === post.id) { if (posts[id].root_id === post.id) {
commentCount += 1; commentCount += 1;
@@ -429,20 +573,45 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action; var action = payload.action;
switch (action.type) { switch (action.type) {
case ActionTypes.RECIEVED_POSTS: case ActionTypes.RECIEVED_POSTS: {
PostStore.storePosts(action.id, makePostListNonNull(action.post_list)); const id = PostStore.currentFocusedPostId == null ? action.id : PostStore.currentFocusedPostId;
PostStore.checkBounds(id, action.numRequested, makePostListNonNull(action.post_list), action.before);
PostStore.storePosts(id, makePostListNonNull(action.post_list));
PostStore.emitChange();
break;
}
case ActionTypes.RECIEVED_FOCUSED_POST:
PostStore.clearChannelVisibility(action.postId, false);
PostStore.storeFocusedPost(action.postId, makePostListNonNull(action.post_list));
PostStore.emitChange();
break; break;
case ActionTypes.RECIEVED_POST: case ActionTypes.RECIEVED_POST:
PostStore.pStorePost(action.post); PostStore.storePost(action.post);
PostStore.emitChange();
break;
case ActionTypes.RECIEVED_EDIT_POST:
PostStore.emitEditPost(action);
PostStore.emitChange();
break;
case ActionTypes.CLICK_CHANNEL:
PostStore.clearFocusedPost();
PostStore.clearChannelVisibility(action.id, true);
PostStore.clearUnseenDeletedPosts(action.id);
break;
case ActionTypes.CREATE_POST:
PostStore.storePendingPost(action.post);
PostStore.storeDraft(action.post.channel_id, null);
PostStore.jumpPostsViewToBottom();
break;
case ActionTypes.POST_DELETED:
PostStore.storeUnseenDeletedPost(action.post);
PostStore.removePost(action.post);
PostStore.emitChange(); PostStore.emitChange();
break; break;
case ActionTypes.RECIEVED_POST_SELECTED: case ActionTypes.RECIEVED_POST_SELECTED:
PostStore.storeSelectedPost(action.post_list); PostStore.storeSelectedPost(action.post_list);
PostStore.emitSelectedPostChange(action.from_search); PostStore.emitSelectedPostChange(action.from_search);
break; break;
case ActionTypes.RECIEVED_EDIT_POST:
PostStore.emitEditPost(action);
break;
default: default:
} }
}); });

Просмотреть файл

@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import UserStore from './user_store.jsx'; import UserStore from './user_store.jsx';
import PostStore from './post_store.jsx'; import PostStore from './post_store.jsx';
import ChannelStore from './channel_store.jsx'; import ChannelStore from './channel_store.jsx';
@@ -11,9 +10,9 @@ import EventEmitter from 'events';
import * as Utils from '../utils/utils.jsx'; import * as Utils from '../utils/utils.jsx';
import * as AsyncClient from '../utils/async_client.jsx'; import * as AsyncClient from '../utils/async_client.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Constants from '../utils/constants.jsx'; import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const SocketEvents = Constants.SocketEvents; const SocketEvents = Constants.SocketEvents;
const CHANGE_EVENT = 'change'; const CHANGE_EVENT = 'change';
@@ -91,10 +90,9 @@ class SocketStoreClass extends EventEmitter {
}; };
conn.onmessage = (evt) => { conn.onmessage = (evt) => {
AppDispatcher.handleServerAction({ const msg = JSON.parse(evt.data);
type: ActionTypes.RECIEVED_MSG, this.handleMessage(msg);
msg: JSON.parse(evt.data) this.emitChange(msg);
});
}; };
} }
} }
@@ -153,12 +151,12 @@ class SocketStoreClass extends EventEmitter {
function handleNewPostEvent(msg) { function handleNewPostEvent(msg) {
// Store post // Store post
const post = JSON.parse(msg.props.post); const post = JSON.parse(msg.props.post);
PostStore.storePost(post); EventHelpers.emitPostRecievedEvent(post);
// Update channel state // Update channel state
if (ChannelStore.getCurrentId() === msg.channel_id) { if (ChannelStore.getCurrentId() === msg.channel_id) {
if (window.isActive) { if (window.isActive) {
AsyncClient.updateLastViewedAt(true); AsyncClient.updateLastViewedAt();
} }
} else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) { } else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
AsyncClient.getChannel(msg.channel_id); AsyncClient.getChannel(msg.channel_id);
@@ -237,20 +235,17 @@ function handlePostEditEvent(msg) {
function handlePostDeleteEvent(msg) { function handlePostDeleteEvent(msg) {
const post = JSON.parse(msg.props.post); const post = JSON.parse(msg.props.post);
EventHelpers.emitPostDeletedEvent(post);
PostStore.storeUnseenDeletedPost(post);
PostStore.removePost(post, true);
PostStore.emitChange();
} }
function handleNewUserEvent() { function handleNewUserEvent() {
AsyncClient.getProfiles(); AsyncClient.getProfiles();
AsyncClient.getChannelExtraInfo(true); AsyncClient.getChannelExtraInfo();
} }
function handleUserAddedEvent(msg) { function handleUserAddedEvent(msg) {
if (ChannelStore.getCurrentId() === msg.channel_id) { if (ChannelStore.getCurrentId() === msg.channel_id) {
AsyncClient.getChannelExtraInfo(true); AsyncClient.getChannelExtraInfo();
} }
if (UserStore.getCurrentId() === msg.user_id) { if (UserStore.getCurrentId() === msg.user_id) {
@@ -273,7 +268,7 @@ function handleUserRemovedEvent(msg) {
$('#removed_from_channel').modal('show'); $('#removed_from_channel').modal('show');
} }
} else if (ChannelStore.getCurrentId() === msg.channel_id) { } else if (ChannelStore.getCurrentId() === msg.channel_id) {
AsyncClient.getChannelExtraInfo(true); AsyncClient.getChannelExtraInfo();
} }
} }
@@ -286,17 +281,12 @@ function handleChannelViewedEvent(msg) {
var SocketStore = new SocketStoreClass(); var SocketStore = new SocketStoreClass();
SocketStore.dispatchToken = AppDispatcher.register((payload) => { /*SocketStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action; var action = payload.action;
switch (action.type) { switch (action.type) {
case ActionTypes.RECIEVED_MSG:
SocketStore.handleMessage(action.msg);
SocketStore.emitChange(action.msg);
break;
default: default:
} }
}); });*/
export default SocketStore; export default SocketStore;

Просмотреть файл

@@ -40,88 +40,42 @@ function isCallInProgress(callName) {
return true; return true;
} }
export function getChannels(force, updateLastViewed, checkVersion) { export function getChannels(checkVersion) {
var channels = ChannelStore.getAll(); if (isCallInProgress('getChannels')) {
return;
if (channels.length === 0 || force) {
if (isCallInProgress('getChannels')) {
return;
}
callTracker.getChannels = utils.getTimestamp();
client.getChannels(
(data, textStatus, xhr) => {
callTracker.getChannels = 0;
if (checkVersion) {
var serverVersion = xhr.getResponseHeader('X-Version-ID');
if (!BrowserStore.getLastServerVersion()) {
BrowserStore.setLastServerVersion(serverVersion);
}
if (serverVersion !== BrowserStore.getLastServerVersion()) {
BrowserStore.setLastServerVersion(serverVersion);
window.location.reload(true);
console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
}
}
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_CHANNELS,
channels: data.channels,
members: data.members
});
},
(err) => {
callTracker.getChannels = 0;
dispatchError(err, 'getChannels');
}
);
} else {
if (isCallInProgress('getChannelCounts')) {
return;
}
callTracker.getChannelCounts = utils.getTimestamp();
client.getChannelCounts(
function getChannelCountsSuccess(data, textStatus, xhr) {
callTracker.getChannelCounts = 0;
if (xhr.status === 304 || !data) {
return;
}
var countMap = data.counts;
var updateAtMap = data.update_times;
for (var id in countMap) {
if ({}.hasOwnProperty.call(countMap, id)) {
var c = ChannelStore.get(id);
var count = countMap[id];
var updateAt = updateAtMap[id];
if (!c || c.total_msg_count !== count || updateAt > c.update_at) {
getChannel(id);
}
}
}
},
function getChannelCountsFailure(err) {
callTracker.getChannelCounts = 0;
dispatchError(err, 'getChannelCounts');
}
);
} }
if (updateLastViewed && ChannelStore.getCurrentId() != null) { callTracker.getChannels = utils.getTimestamp();
updateLastViewedAt();
} client.getChannels(
(data, textStatus, xhr) => {
callTracker.getChannels = 0;
if (checkVersion) {
var serverVersion = xhr.getResponseHeader('X-Version-ID');
if (serverVersion !== BrowserStore.getLastServerVersion()) {
BrowserStore.setLastServerVersion(serverVersion);
window.location.reload(true);
console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
}
}
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_CHANNELS,
channels: data.channels,
members: data.members
});
},
(err) => {
callTracker.getChannels = 0;
dispatchError(err, 'getChannels');
}
);
} }
export function getChannel(id) { export function getChannel(id) {
@@ -152,14 +106,14 @@ export function getChannel(id) {
); );
} }
export function updateLastViewedAt(force) { export function updateLastViewedAt() {
const channelId = ChannelStore.getCurrentId(); const channelId = ChannelStore.getCurrentId();
if (channelId === null) { if (channelId === null) {
return; return;
} }
if (isCallInProgress(`updateLastViewed${channelId}`) && !force) { if (isCallInProgress(`updateLastViewed${channelId}`)) {
return; return;
} }
@@ -205,40 +159,35 @@ export function getMoreChannels(force) {
} }
} }
export function getChannelExtraInfo(force) { export function getChannelExtraInfo() {
var channelId = ChannelStore.getCurrentId(); const channelId = ChannelStore.getCurrentId();
if (channelId != null) { if (channelId != null) {
if (isCallInProgress('getChannelExtraInfo_' + channelId)) { if (isCallInProgress('getChannelExtraInfo_' + channelId)) {
return; return;
} }
var minMembers = 0;
if (ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'D') {
minMembers = 1;
}
if (ChannelStore.getCurrentExtraInfo().members.length <= minMembers || force) { callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp();
callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp();
client.getChannelExtraInfo(
channelId,
function getChannelExtraInfoSuccess(data, textStatus, xhr) {
callTracker['getChannelExtraInfo_' + channelId] = 0;
if (xhr.status === 304 || !data) { client.getChannelExtraInfo(
return; channelId,
} (data, textStatus, xhr) => {
callTracker['getChannelExtraInfo_' + channelId] = 0;
AppDispatcher.handleServerAction({ if (xhr.status === 304 || !data) {
type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO, return;
extra_info: data
});
},
function getChannelExtraInfoFailure(err) {
callTracker['getChannelExtraInfo_' + channelId] = 0;
dispatchError(err, 'getChannelExtraInfo');
} }
);
} AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO,
extra_info: data
});
},
(err) => {
callTracker['getChannelExtraInfo_' + channelId] = 0;
dispatchError(err, 'getChannelExtraInfo');
}
);
} }
} }
@@ -457,89 +406,92 @@ export function search(terms) {
); );
} }
export function getPostsPage(force, id, maxPosts) { export function getPostsPage(id, maxPosts) {
if (PostStore.getCurrentPosts() == null || force) { let channelId = id;
var channelId = id; if (channelId == null) {
channelId = ChannelStore.getCurrentId();
if (channelId == null) { if (channelId == null) {
channelId = ChannelStore.getCurrentId();
}
if (isCallInProgress('getPostsPage_' + channelId)) {
return; return;
} }
}
var postList = PostStore.getCurrentPosts(); if (isCallInProgress('getPostsPage_' + channelId)) {
return;
}
var max = maxPosts; var postList = PostStore.getAllPosts(id);
if (max == null) {
max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS;
}
// if we already have more than POST_CHUNK_SIZE posts, var max = maxPosts;
// let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE, if (max == null) {
// with a max at maxPosts max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS;
var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE); }
if (postList && postList.order.length > 0) {
numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE));
}
if (channelId != null) { // if we already have more than POST_CHUNK_SIZE posts,
callTracker['getPostsPage_' + channelId] = utils.getTimestamp(); // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
// with a max at maxPosts
var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE);
if (postList && postList.order.length > 0) {
numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE));
}
client.getPostsPage( if (channelId != null) {
channelId, callTracker['getPostsPage_' + channelId] = utils.getTimestamp();
0,
numPosts,
function getPostsPageSuccess(data, textStatus, xhr) {
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({ client.getPostsPage(
type: ActionTypes.RECIEVED_POSTS, channelId,
id: channelId, 0,
post_list: data numPosts,
}); (data, textStatus, xhr) => {
if (xhr.status === 304 || !data) {
getProfiles(); return;
},
function getPostsPageFailure(err) {
dispatchError(err, 'getPostsPage');
},
function getPostsPageComplete() {
callTracker['getPostsPage_' + channelId] = 0;
} }
);
} AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POSTS,
id: channelId,
before: true,
numRequested: numPosts,
post_list: data
});
getProfiles();
},
(err) => {
dispatchError(err, 'getPostsPage');
},
() => {
callTracker['getPostsPage_' + channelId] = 0;
}
);
} }
} }
export function getPosts(id) { export function getPosts(id) {
var channelId = id; let channelId = id;
if (channelId == null) { if (channelId == null) {
if (ChannelStore.getCurrentId() == null) { channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return; return;
} }
channelId = ChannelStore.getCurrentId();
} }
if (isCallInProgress('getPosts_' + channelId)) { if (isCallInProgress('getPosts_' + channelId)) {
return; return;
} }
if (PostStore.getCurrentPosts() == null) { if (PostStore.getAllPosts(channelId) == null) {
getPostsPage(true, id, Constants.POST_CHUNK_SIZE); getPostsPage(channelId, Constants.POST_CHUNK_SIZE);
return; return;
} }
var latestUpdate = PostStore.getLatestUpdate(channelId); const latestUpdate = PostStore.getLatestUpdate(channelId);
callTracker['getPosts_' + channelId] = utils.getTimestamp(); callTracker['getPosts_' + channelId] = utils.getTimestamp();
client.getPosts( client.getPosts(
channelId, channelId,
latestUpdate, latestUpdate,
function success(data, textStatus, xhr) { (data, textStatus, xhr) => {
if (xhr.status === 304 || !data) { if (xhr.status === 304 || !data) {
return; return;
} }
@@ -547,20 +499,100 @@ export function getPosts(id) {
AppDispatcher.handleServerAction({ AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POSTS, type: ActionTypes.RECIEVED_POSTS,
id: channelId, id: channelId,
before: true,
numRequested: Constants.POST_CHUNK_SIZE,
post_list: data post_list: data
}); });
getProfiles(); getProfiles();
}, },
function fail(err) { (err) => {
dispatchError(err, 'getPosts'); dispatchError(err, 'getPosts');
}, },
function complete() { () => {
callTracker['getPosts_' + channelId] = 0; callTracker['getPosts_' + channelId] = 0;
} }
); );
} }
export function getPostsBefore(postId, offset, numPost) {
const channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return;
}
if (isCallInProgress('getPostsBefore_' + channelId)) {
return;
}
client.getPostsBefore(
channelId,
postId,
offset,
numPost,
(data, textStatus, xhr) => {
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POSTS,
id: channelId,
before: true,
numRequested: numPost,
post_list: data
});
getProfiles();
},
(err) => {
dispatchError(err, 'getPostsBefore');
},
() => {
callTracker['getPostsBefore_' + channelId] = 0;
}
);
}
export function getPostsAfter(postId, offset, numPost) {
const channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return;
}
if (isCallInProgress('getPostsAfter_' + channelId)) {
return;
}
client.getPostsAfter(
channelId,
postId,
offset,
numPost,
(data, textStatus, xhr) => {
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POSTS,
id: channelId,
before: false,
numRequested: numPost,
post_list: data
});
getProfiles();
},
(err) => {
dispatchError(err, 'getPostsAfter');
},
() => {
callTracker['getPostsAfter_' + channelId] = 0;
}
);
}
export function getMe() { export function getMe() {
if (isCallInProgress('getMe')) { if (isCallInProgress('getMe')) {
return; return;

Просмотреть файл

@@ -820,7 +820,37 @@ export function getPosts(channelId, since, success, error, complete) {
}); });
} }
export function getPost(channelId, postId, success, error) { export function getPostsBefore(channelId, post, offset, numPost, success, error, complete) {
$.ajax({
url: '/api/v1/channels/' + channelId + '/post/' + post + '/before/' + offset + '/' + numPost,
dataType: 'json',
type: 'GET',
ifModified: false,
success,
error: function onError(xhr, status, err) {
var e = handleError('getPostsBefore', xhr, status, err);
error(e);
},
complete: complete
});
}
export function getPostsAfter(channelId, post, offset, numPost, success, error, complete) {
$.ajax({
url: '/api/v1/channels/' + channelId + '/post/' + post + '/after/' + offset + '/' + numPost,
dataType: 'json',
type: 'GET',
ifModified: false,
success,
error: function onError(xhr, status, err) {
var e = handleError('getPostsAfter', xhr, status, err);
error(e);
},
complete: complete
});
}
export function getPost(channelId, postId, success, error, complete) {
$.ajax({ $.ajax({
cache: false, cache: false,
url: '/api/v1/channels/' + channelId + '/post/' + postId, url: '/api/v1/channels/' + channelId + '/post/' + postId,
@@ -831,7 +861,24 @@ export function getPost(channelId, postId, success, error) {
error: function onError(xhr, status, err) { error: function onError(xhr, status, err) {
var e = handleError('getPost', xhr, status, err); var e = handleError('getPost', xhr, status, err);
error(e); error(e);
} },
complete
});
}
export function getPostById(postId, success, error, complete) {
$.ajax({
cache: false,
url: '/api/v1/posts/' + postId,
dataType: 'json',
type: 'GET',
ifModified: false,
success,
error: function onError(xhr, status, err) {
var e = handleError('getPostById', xhr, status, err);
error(e);
},
complete
}); });
} }

Просмотреть файл

@@ -10,12 +10,17 @@ export default {
CLICK_CHANNEL: null, CLICK_CHANNEL: null,
CREATE_CHANNEL: null, CREATE_CHANNEL: null,
LEAVE_CHANNEL: null, LEAVE_CHANNEL: null,
CREATE_POST: null,
POST_DELETED: null,
RECIEVED_CHANNELS: null, RECIEVED_CHANNELS: null,
RECIEVED_CHANNEL: null, RECIEVED_CHANNEL: null,
RECIEVED_MORE_CHANNELS: null, RECIEVED_MORE_CHANNELS: null,
RECIEVED_CHANNEL_EXTRA_INFO: null, RECIEVED_CHANNEL_EXTRA_INFO: null,
FOCUS_POST: null,
RECIEVED_POSTS: null, RECIEVED_POSTS: null,
RECIEVED_FOCUSED_POST: null,
RECIEVED_POST: null, RECIEVED_POST: null,
RECIEVED_EDIT_POST: null, RECIEVED_EDIT_POST: null,
RECIEVED_SEARCH: null, RECIEVED_SEARCH: null,
@@ -99,6 +104,7 @@ export default {
EMAIL_SERVICE: 'email', EMAIL_SERVICE: 'email',
POST_CHUNK_SIZE: 60, POST_CHUNK_SIZE: 60,
MAX_POST_CHUNKS: 3, MAX_POST_CHUNKS: 3,
POST_FOCUS_CONTEXT_RADIUS: 10,
POST_LOADING: 'loading', POST_LOADING: 'loading',
POST_FAILED: 'failed', POST_FAILED: 'failed',
POST_DELETED: 'deleted', POST_DELETED: 'deleted',

Просмотреть файл

@@ -2,6 +2,7 @@
// See License.txt for license information. // See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import ChannelStore from '../stores/channel_store.jsx'; import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx'; import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx';
@@ -839,23 +840,15 @@ export function isValidUsername(name) {
} }
export function updateAddressBar(channelName) { export function updateAddressBar(channelName) {
var teamURL = window.location.href.split('/channels')[0]; const teamURL = TeamStore.getCurrentTeamUrl();
history.replaceState('data', '', teamURL + '/channels/' + channelName); history.replaceState('data', '', teamURL + '/channels/' + channelName);
} }
export function switchChannel(channel) { export function switchChannel(channel) {
AppDispatcher.handleViewAction({ EventHelpers.emitChannelClickEvent(channel);
type: ActionTypes.CLICK_CHANNEL,
name: channel.name,
id: channel.id
});
updateAddressBar(channel.name); updateAddressBar(channel.name);
AsyncClient.getChannels(true, true, true);
AsyncClient.getChannelExtraInfo(true);
AsyncClient.getPosts(channel.id);
$('.inner__wrap').removeClass('move--right'); $('.inner__wrap').removeClass('move--right');
$('.sidebar--left').removeClass('move--right'); $('.sidebar--left').removeClass('move--right');

Просмотреть файл

@@ -361,6 +361,19 @@ body.ios {
.dropdown, .comment-icon__container { .dropdown, .comment-icon__container {
visibility: visible; visibility: visible;
} }
.permalink-icon {
visibility: visible;
}
}
&.post--highlight {
background-color: beige;
}
ul {
margin: 0;
padding: 0;
list-style: none;
} }
p { p {
@@ -618,6 +631,12 @@ body.ios {
color: #999; color: #999;
} }
.permalink-icon {
display: inline-block;
color: $primary-color;
visibility: hidden;
}
.comment-icon__container { .comment-icon__container {
fill: $primary-color; fill: $primary-color;
display: inline-block; display: inline-block;
@@ -757,3 +776,11 @@ body.ios {
} }
} }
} }
.permalink-text {
overflow: hidden;
}
.permalink-popover {
min-width: 320px;
}

Просмотреть файл

@@ -33,7 +33,7 @@
<div id="removed_from_channel_modal"></div> <div id="removed_from_channel_modal"></div>
<div id="register_app_modal"></div> <div id="register_app_modal"></div>
<script> <script>
window.setup_channel_page({{ .Props }}); window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }}, {{ .User }});
$('body').tooltip( {selector: '[data-toggle=tooltip]'} ); $('body').tooltip( {selector: '[data-toggle=tooltip]'} );
if($(window).height() > 1200){ if($(window).height() > 1200){
$('.modal-body').css('max-height', 1000); $('.modal-body').css('max-height', 1000);

Просмотреть файл

@@ -47,6 +47,7 @@
window.mm_config = {{ .ClientCfg }}; window.mm_config = {{ .ClientCfg }};
window.mm_team = {{ .Team }}; window.mm_team = {{ .Team }};
window.mm_user = {{ .User }}; window.mm_user = {{ .User }};
window.mm_channel = {{ .Channel }};
if ({{.SessionTokenIndex}} >= 0) { if ({{.SessionTokenIndex}} >= 0) {
window.mm_session_token_index = {{.SessionTokenIndex}}; window.mm_session_token_index = {{.SessionTokenIndex}};

Просмотреть файл

@@ -91,6 +91,7 @@ func InitWeb() {
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET")
mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
@@ -342,15 +343,142 @@ func logout(c *api.Context, w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect) http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect)
} }
func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
teamName := params["team"]
postId := params["postid"]
if len(postId) != 26 {
c.Err = model.NewAppError("postPermalink", "Invalid Post ID", "id="+postId)
return
}
team := checkSessionSwitch(c, w, r, teamName)
if team == nil {
// Error already set by getTeam
return
}
var post *model.Post
if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil {
c.Err = result.Err
return
} else {
postlist := result.Data.(*model.PostList)
post = postlist.Posts[postlist.Order[0]]
}
var channel *model.Channel
if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil {
c.Err = result.Err
return
} else {
if result.Data.(int64) == 0 {
if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil {
http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
return
}
} else {
if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil {
c.Err = result.Err
return
} else {
channel = result.Data.(*model.Channel)
}
}
}
doLoadChannel(c, w, r, team, channel, post.Id)
}
func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r) params := mux.Vars(r)
name := params["channelname"] name := params["channelname"]
teamName := params["team"] teamName := params["team"]
team := checkSessionSwitch(c, w, r, teamName)
if team == nil {
// Error already set by getTeam
return
}
var channel *model.Channel
if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil {
c.Err = result.Err
return
} else {
channelId := result.Data.(string)
if len(channelId) == 0 {
if channel = autoJoinChannelName(c, w, r, name); channel == nil {
http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
return
}
} else {
if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil {
c.Err = result.Err
return
} else {
channel = result.Data.(*model.Channel)
}
}
}
doLoadChannel(c, w, r, team, channel, "")
}
func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel {
if strings.Index(channelName, "__") > 0 {
// It's a direct message channel that doesn't exist yet so let's create it
ids := strings.Split(channelName, "__")
otherUserId := ""
if ids[0] == c.Session.UserId {
otherUserId = ids[1]
} else {
otherUserId = ids[0]
}
if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil {
api.Handle404(w, r)
return nil
} else {
return sc
}
} else {
// We will attempt to auto-join open channels
return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName))
}
return nil
}
func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel {
return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId))
}
func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel {
if cr := <-channel; cr.Err != nil {
http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
return nil
} else {
channel := cr.Data.(*model.Channel)
if channel.Type == model.CHANNEL_OPEN {
api.JoinChannel(c, channel.Id, "")
if c.Err != nil {
return nil
}
} else {
http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
return nil
}
return channel
}
}
func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team {
var team *model.Team var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err c.Err = result.Err
return return nil
} else { } else {
team = result.Data.(*model.Team) team = result.Data.(*model.Team)
} }
@@ -368,15 +496,11 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
} }
} }
userChan := api.Srv.Store.User().Get(c.Session.UserId) return team
}
var channelId string func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) {
if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { userChan := api.Srv.Store.User().Get(c.Session.UserId)
c.Err = result.Err
return
} else {
channelId = result.Data.(string)
}
var user *model.User var user *model.User
if ur := <-userChan; ur.Err != nil { if ur := <-userChan; ur.Err != nil {
@@ -388,54 +512,15 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
user = ur.Data.(*model.User) user = ur.Data.(*model.User)
} }
if len(channelId) == 0 {
if strings.Index(name, "__") > 0 {
// It's a direct message channel that doesn't exist yet so let's create it
ids := strings.Split(name, "__")
otherUserId := ""
if ids[0] == c.Session.UserId {
otherUserId = ids[1]
} else {
otherUserId = ids[0]
}
if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil {
api.Handle404(w, r)
return
} else {
channelId = sc.Id
}
} else {
// We will attempt to auto-join open channels
if cr := <-api.Srv.Store.Channel().GetByName(c.Session.TeamId, name); cr.Err != nil {
http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
} else {
channel := cr.Data.(*model.Channel)
if channel.Type == model.CHANNEL_OPEN {
api.JoinChannel(c, channel.Id, "")
if c.Err != nil {
return
}
channelId = channel.Id
} else {
http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound)
}
}
}
}
page := NewHtmlTemplatePage("channel", "") page := NewHtmlTemplatePage("channel", "")
page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"] page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"]
page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamDisplayName"] = team.DisplayName
page.Props["TeamName"] = team.Name page.Props["ChannelName"] = channel.Name
page.Props["TeamType"] = team.Type page.Props["ChannelId"] = channel.Id
page.Props["TeamId"] = team.Id page.Props["PostId"] = postid
page.Props["ChannelName"] = name
page.Props["ChannelId"] = channelId
page.Props["UserId"] = c.Session.UserId
page.Team = team page.Team = team
page.User = user page.User = user
page.Channel = channel
page.Render(c, w) page.Render(c, w)
} }