summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/.eslintrc4
-rw-r--r--web/react/components/center_panel.jsx33
-rw-r--r--web/react/components/channel_header.jsx27
-rw-r--r--web/react/components/channel_invite_modal.jsx13
-rw-r--r--web/react/components/channel_loader.jsx4
-rw-r--r--web/react/components/channel_members_modal.jsx14
-rw-r--r--web/react/components/create_post.jsx11
-rw-r--r--web/react/components/delete_post_modal.jsx9
-rw-r--r--web/react/components/edit_channel_header_modal.jsx126
-rw-r--r--web/react/components/edit_channel_modal.jsx150
-rw-r--r--web/react/components/edit_post_modal.jsx4
-rw-r--r--web/react/components/get_link_modal.jsx144
-rw-r--r--web/react/components/get_team_invite_link_modal.jsx45
-rw-r--r--web/react/components/invite_member_modal.jsx36
-rw-r--r--web/react/components/navbar.jsx56
-rw-r--r--web/react/components/navbar_dropdown.jsx9
-rw-r--r--web/react/components/post.jsx34
-rw-r--r--web/react/components/post_focus_view.jsx110
-rw-r--r--web/react/components/post_info.jsx75
-rw-r--r--web/react/components/posts_view.jsx91
-rw-r--r--web/react/components/posts_view_container.jsx129
-rw-r--r--web/react/components/rhs_comment.jsx4
-rw-r--r--web/react/components/rhs_root_post.jsx4
-rw-r--r--web/react/components/rhs_thread.jsx2
-rw-r--r--web/react/components/search_results_item.jsx34
-rw-r--r--web/react/components/sidebar.jsx4
-rw-r--r--web/react/components/sidebar_right_menu.jsx17
-rw-r--r--web/react/components/toggle_modal_button.jsx17
-rw-r--r--web/react/dispatcher/event_helpers.jsx106
-rw-r--r--web/react/package.json3
-rw-r--r--web/react/pages/channel.jsx28
-rw-r--r--web/react/pages/home.jsx3
-rw-r--r--web/react/stores/channel_store.jsx86
-rw-r--r--web/react/stores/modal_store.jsx1
-rw-r--r--web/react/stores/post_store.jsx509
-rw-r--r--web/react/stores/socket_store.jsx34
-rw-r--r--web/react/stores/team_store.jsx11
-rw-r--r--web/react/utils/async_client.jsx346
-rw-r--r--web/react/utils/channel_intro_mssages.jsx78
-rw-r--r--web/react/utils/client.jsx51
-rw-r--r--web/react/utils/constants.jsx9
-rw-r--r--web/react/utils/markdown.jsx47
-rw-r--r--web/react/utils/utils.jsx30
-rw-r--r--web/sass-files/sass/partials/_markdown.scss28
-rw-r--r--web/sass-files/sass/partials/_navbar.scss3
-rw-r--r--web/sass-files/sass/partials/_post.scss30
-rw-r--r--web/templates/channel.html13
-rw-r--r--web/templates/head.html1
-rw-r--r--web/web.go191
49 files changed, 1731 insertions, 1083 deletions
diff --git a/web/react/.eslintrc b/web/react/.eslintrc
index 29ca97faf..935bb638a 100644
--- a/web/react/.eslintrc
+++ b/web/react/.eslintrc
@@ -47,7 +47,7 @@
"no-irregular-whitespace": 2,
"no-unexpected-multiline": 2,
"no-unreachable": 2,
- "no-magic-numbers": [0, { "enforceConst": true, "detectObjects": true } ],
+ "no-magic-numbers": [1, { "enforceConst": true, "detectObjects": true } ],
"valid-typeof": 2,
"block-scoped-var": 2,
@@ -190,7 +190,7 @@
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-direct-mutation-state": 2,
- "react/no-multi-comp": 2,
+ "react/no-multi-comp": [2, { "ignoreStateless": true }],
"react/no-set-state": 0,
"react/no-unknown-property": 2,
"react/prefer-es6-class": 2,
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index c2ecf4fa2..3c6a36ad4 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -4,11 +4,13 @@
import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx';
import CreatePost from './create_post.jsx';
import PostsViewContainer from './posts_view_container.jsx';
+import PostFocusView from './post_focus_view.jsx';
import ChannelHeader from './channel_header.jsx';
import Navbar from './navbar.jsx';
import FileUploadOverlay from './file_upload_overlay.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
import Constants from '../utils/constants.jsx';
@@ -20,26 +22,48 @@ export default class CenterPanel extends React.Component {
super(props);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
+ this.onChannelChange = this.onChannelChange.bind(this);
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() {
PreferenceStore.addChangeListener(this.onPreferenceChange);
+ ChannelStore.addChangeListener(this.onChannelChange);
}
componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ ChannelStore.removeChangeListener(this.onChannelChange);
}
onPreferenceChange() {
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
}
+ onChannelChange() {
+ this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
+ }
render() {
let postsContainer;
+ let createPost;
if (this.state.showTutorialScreens) {
postsContainer = <TutorialIntroScreens />;
+ createPost = null;
+ } else if (this.state.showPostFocus) {
+ postsContainer = <PostFocusView />;
+ createPost = null;
} else {
postsContainer = <PostsViewContainer />;
+ createPost = (
+ <div
+ className='post-create__container'
+ id='post-create'
+ >
+ <CreatePost />
+ </div>
+ );
}
return (
@@ -62,12 +86,7 @@ export default class CenterPanel extends React.Component {
<ChannelHeader />
</div>
{postsContainer}
- <div
- className='post-create__container'
- id='post-create'
- >
- <CreatePost />
- </div>
+ {createPost}
</div>
</div>
</div>
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 8c721348f..6e12c7c14 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -4,6 +4,7 @@
import NavbarSearchBox from './search_bar.jsx';
import MessageWrapper from './message_wrapper.jsx';
import PopoverListMembers from './popover_list_members.jsx';
+import EditChannelHeaderModal from './edit_channel_header_modal.jsx';
import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx';
import ChannelInfoModal from './channel_info_modal.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
@@ -167,17 +168,13 @@ export default class ChannelHeader extends React.Component {
key='edit_header_direct'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
>
{'Set Channel Header...'}
- </a>
+ </ToggleModalButton>
</li>
);
} else {
@@ -235,17 +232,13 @@ export default class ChannelHeader extends React.Component {
key='set_channel_header'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
>
- {'Set '}{channelTerm}{' Header...'}
- </a>
+ {`Set ${channelTerm} Header...`}
+ </ToggleModalButton>
</li>
);
dropdownContents.push(
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 6d3203ae5..0518ccb86 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -22,6 +22,17 @@ export default class ChannelInviteModal extends React.Component {
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() {
function getId(user) {
return user.id;
@@ -105,7 +116,7 @@ export default class ChannelInviteModal extends React.Component {
}
this.setState({inviteError: null, memberIds, nonmembers});
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
},
(err) => {
this.setState({inviteError: err.message});
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index e29c659c7..c8f1196a8 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -27,8 +27,8 @@ export default class ChannelLoader extends React.Component {
componentDidMount() {
/* Initial aysnc loads */
AsyncClient.getPosts(ChannelStore.getCurrentId());
- AsyncClient.getChannels(true, true);
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannels();
+ AsyncClient.getChannelExtraInfo();
AsyncClient.findTeams();
AsyncClient.getMyTeam();
setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index 08ad95091..f07fc166a 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -25,6 +25,17 @@ export default class ChannelMembersModal extends React.Component {
state.showInviteModal = false;
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() {
const users = UserStore.getActiveOnlyProfiles();
const memberList = ChannelStore.getCurrentExtraInfo().members;
@@ -74,6 +85,7 @@ export default class ChannelMembersModal extends React.Component {
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
+ this.onChange();
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -130,7 +142,7 @@ export default class ChannelMembersModal extends React.Component {
}
this.setState({memberList, nonmemberList});
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
},
(err) => {
this.setState({inviteError: err.message});
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 0a2979e21..f7f63fb92 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -8,6 +8,7 @@ import FilePreview from './file_preview.jsx';
import TutorialTip from './tutorial/tutorial_tip.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 AsyncClient from '../utils/async_client.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 Constants from '../utils/constants.jsx';
+
const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
@@ -176,9 +178,7 @@ export default class CreatePost extends React.Component {
const channel = ChannelStore.get(this.state.channelId);
- PostStore.storePendingPost(post);
- PostStore.storeDraft(channel.id, null);
- PostStore.jumpPostsViewToBottom();
+ EventHelpers.emitUserPostedEvent(post);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
@@ -190,10 +190,7 @@ export default class CreatePost extends React.Component {
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST,
- post: data
- });
+ EventHelpers.emitPostRecievedEvent(data);
},
(err) => {
const state = {};
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index fab5b60ea..3c4b17905 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -159,13 +159,4 @@ export default class DeletePostModal extends React.Component {
</Modal>
);
}
-
- static show(post, commentCount) {
- AppDispatcher.handleViewAction({
- type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
- value: true,
- post,
- commentCount: commentCount || 0
- });
- }
}
diff --git a/web/react/components/edit_channel_header_modal.jsx b/web/react/components/edit_channel_header_modal.jsx
new file mode 100644
index 000000000..5529a419d
--- /dev/null
+++ b/web/react/components/edit_channel_header_modal.jsx
@@ -0,0 +1,126 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+const Modal = ReactBootstrap.Modal;
+
+export default class EditChannelHeaderModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleEdit = this.handleEdit.bind(this);
+
+ this.onShow = this.onShow.bind(this);
+ this.onHide = this.onHide.bind(this);
+
+ this.state = {
+ serverError: ''
+ };
+ }
+
+ componentDidMount() {
+ if (this.props.show) {
+ this.onShow();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.show && !prevProps.show) {
+ this.onShow();
+ }
+ }
+
+ handleEdit() {
+ var data = {};
+ data.channel_id = this.props.channel.id;
+
+ if (data.channel_id.length !== 26) {
+ return;
+ }
+
+ data.channel_header = ReactDOM.findDOMNode(this.refs.textarea).value;
+
+ Client.updateChannelHeader(data,
+ () => {
+ this.setState({serverError: ''});
+ AsyncClient.getChannel(this.props.channel.id);
+ this.onHide();
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_header parameter') {
+ this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
+ } else {
+ this.setState({serverError: err.message});
+ }
+ }
+ );
+ }
+
+ onShow() {
+ const textarea = ReactDOM.findDOMNode(this.refs.textarea);
+ Utils.placeCaretAtEnd(textarea);
+ }
+
+ onHide() {
+ this.setState({
+ serverError: ''
+ });
+
+ this.props.onHide();
+ }
+
+ render() {
+ var serverError = null;
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.onHide}
+ >
+ <Modal.Header closeButton={true}>
+ {'Edit Header for ' + this.props.channel.display_name}
+ </Modal.Header>
+ <Modal.Body>
+ <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
+ <textarea
+ ref='textarea'
+ className='form-control no-resize'
+ rows='6'
+ id='edit_header'
+ maxLength='1024'
+ defaultValue={this.props.channel.header}
+ />
+ {serverError}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onHide}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handleEdit}
+ >
+ {'Save'}
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+EditChannelHeaderModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
deleted file mode 100644
index 80dab4a57..000000000
--- a/web/react/components/edit_channel_modal.jsx
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import * as Client from '../utils/client.jsx';
-import * as AsyncClient from '../utils/async_client.jsx';
-
-export default class EditChannelModal extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleEdit = this.handleEdit.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.onShow = this.onShow.bind(this);
- this.handleShown = this.handleShown.bind(this);
-
- this.state = {
- header: '',
- title: '',
- channelId: '',
- serverError: ''
- };
- }
- handleEdit() {
- var data = {};
- data.channel_id = this.state.channelId;
-
- if (data.channel_id.length !== 26) {
- return;
- }
-
- data.channel_header = this.state.header.trim();
-
- Client.updateChannelHeader(data,
- () => {
- this.setState({serverError: ''});
- AsyncClient.getChannel(this.state.channelId);
- $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
- },
- (err) => {
- if (err.message === 'Invalid channel_header parameter') {
- this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
- } else {
- this.setState({serverError: err.message});
- }
- }
- );
- }
- handleUserInput(e) {
- this.setState({header: e.target.value});
- }
- handleClose() {
- this.setState({header: '', serverError: ''});
- }
- onShow(e) {
- const button = e.relatedTarget;
- this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
- }
- handleShown() {
- $('#edit_channel #edit_header').focus();
- }
- componentDidMount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.handleShown);
- }
- componentWillUnmount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
- }
- render() {
- var serverError = null;
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var editTitle = (
- <h4
- className='modal-title'
- ref='title'
- >
- {'Edit Header'}
- </h4>
- );
- if (this.state.title) {
- editTitle = (
- <h4
- className='modal-title'
- ref='title'
- >
- {'Edit Header for '}<span className='name'>{this.state.title}</span>
- </h4>
- );
- }
-
- return (
- <div
- className='modal fade'
- ref='modal'
- id='edit_channel'
- role='dialog'
- tabIndex='-1'
- aria-hidden='true'
- >
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>{'×'}</span>
- </button>
- {editTitle}
- </div>
- <div className='modal-body'>
- <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
- <textarea
- className='form-control no-resize'
- rows='6'
- id='edit_header'
- maxLength='1024'
- value={this.state.header}
- onChange={this.handleUserInput}
- />
- {serverError}
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- {'Cancel'}
- </button>
- <button
- type='button'
- className='btn btn-primary'
- onClick={this.handleEdit}
- >
- {'Save'}
- </button>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index ddbdee8a4..eb58fe721 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -3,7 +3,7 @@
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
-import DeletePostModal from './delete_post_modal.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Textbox from './textbox.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import PostStore from '../stores/post_store.jsx';
@@ -35,7 +35,7 @@ export default class EditPostModal extends React.Component {
delete tempState.editText;
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
- DeletePostModal.show(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
+ EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
return;
}
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 2bd2c42d6..df5d6b8e1 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -1,32 +1,28 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import UserStore from '../stores/user_store.jsx';
+const Modal = ReactBootstrap.Modal;
export default class GetLinkModal extends React.Component {
constructor(props) {
super(props);
- this.handleClick = this.handleClick.bind(this);
- this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
- this.state = {copiedLink: false};
- }
- onShow(e) {
- var button = e.relatedTarget;
- this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')});
+ this.copyLink = this.copyLink.bind(this);
+
+ this.state = {
+ copiedLink: false
+ };
}
+
onHide() {
this.setState({copiedLink: false});
+
+ this.props.onHide();
}
- componentDidMount() {
- if (this.refs.modal) {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', this.onHide);
- }
- }
- handleClick() {
+
+ copyLink() {
var copyTextarea = $(ReactDOM.findDOMNode(this.refs.textarea));
copyTextarea.select();
@@ -41,8 +37,18 @@ export default class GetLinkModal extends React.Component {
this.setState({copiedLink: false});
}
}
+
render() {
- var currentUser = UserStore.getCurrentUser();
+ let helpText = null;
+ if (this.props.helpText) {
+ helpText = (
+ <p>
+ {this.props.helpText}
+ <br />
+ <br />
+ </p>
+ );
+ }
let copyLink = null;
if (document.queryCommandSupported('copy')) {
@@ -51,75 +57,59 @@ export default class GetLinkModal extends React.Component {
data-copy-btn='true'
type='button'
className='btn btn-primary pull-left'
- onClick={this.handleClick}
- data-clipboard-text={this.state.value}
+ onClick={this.copyLink}
>
- Copy Link
+ {'Copy Link'}
</button>
);
}
var copyLinkConfirm = null;
if (this.state.copiedLink) {
- copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i> Link copied to clipboard.</p>;
+ copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i>{' Link copied to clipboard.'}</p>;
}
- if (currentUser != null) {
- return (
- <div
- className='modal fade'
- ref='modal'
- id='get_link'
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
- >
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='myModalLabel'
- >
- {this.state.title} Link
- </h4>
- </div>
- <div className='modal-body'>
- <p>
- Send teammates the link below for them to sign-up to this team site.
- <br /><br />
- </p>
- <textarea
- className='form-control no-resize min-height'
- readOnly='true'
- ref='textarea'
- value={this.state.value}
- />
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- Close
- </button>
- {copyLink}
- {copyLinkConfirm}
- </div>
- </div>
- </div>
- </div>
- );
- }
- return <div/>;
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.onHide}
+ >
+ <Modal.Header closeButton={true}>
+ {this.props.title}
+ </Modal.Header>
+ <Modal.Body>
+ {helpText}
+ <textarea
+ className='form-control no-resize min-height'
+ readOnly='true'
+ ref='textarea'
+ value={this.props.link}
+ />
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.onHide}
+ >
+ {'Close'}
+ </button>
+ {copyLink}
+ {copyLinkConfirm}
+ </Modal.Footer>
+ </Modal>
+ );
}
}
+
+GetLinkModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ title: React.PropTypes.string.isRequired,
+ helpText: React.PropTypes.string,
+ link: React.PropTypes.string.isRequired
+};
+
+GetLinkModal.defaultProps = {
+ helpText: null
+};
diff --git a/web/react/components/get_team_invite_link_modal.jsx b/web/react/components/get_team_invite_link_modal.jsx
new file mode 100644
index 000000000..a926c4451
--- /dev/null
+++ b/web/react/components/get_team_invite_link_modal.jsx
@@ -0,0 +1,45 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../utils/constants.jsx';
+import GetLinkModal from './get_link_modal.jsx';
+import ModalStore from '../stores/modal_store.jsx';
+import TeamStore from '../stores/team_store.jsx';
+
+export default class GetTeamInviteLinkModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleToggle = this.handleToggle.bind(this);
+
+ this.state = {
+ show: false
+ };
+ }
+
+ componentDidMount() {
+ ModalStore.addModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle);
+ }
+
+ componentWillUnmount() {
+ ModalStore.removeModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle);
+ }
+
+ handleToggle(value) {
+ this.setState({
+ show: value
+ });
+ }
+
+ render() {
+ return (
+ <GetLinkModal
+ show={this.state.show}
+ onHide={() => this.setState({show: false})}
+ title='Team Invite Link'
+ helpText='Send teammates the link below for them to sign-up to this team site.'
+ link={TeamStore.getCurrentInviteLink()}
+ />
+ );
+ }
+}
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 7df75252e..76f52faa9 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -4,8 +4,8 @@
import * as utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Client from '../utils/client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import ModalStore from '../stores/modal_store.jsx';
import UserStore from '../stores/user_store.jsx';
import TeamStore from '../stores/team_store.jsx';
@@ -23,6 +23,7 @@ export default class InviteMemberModal extends React.Component {
this.addInviteFields = this.addInviteFields.bind(this);
this.clearFields = this.clearFields.bind(this);
this.removeInviteFields = this.removeInviteFields.bind(this);
+ this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this);
this.state = {
show: false,
@@ -188,6 +189,12 @@ export default class InviteMemberModal extends React.Component {
this.setState({inviteIds: inviteIds, idCount: count});
}
+ showGetTeamInviteLinkModal() {
+ this.handleHide(false);
+
+ EventHelpers.showGetTeamInviteLinkModal();
+ }
+
render() {
var currentUser = UserStore.getCurrentUser();
@@ -333,22 +340,18 @@ export default class InviteMemberModal extends React.Component {
} else {
var teamInviteLink = null;
if (currentUser && TeamStore.getCurrent().type === 'O') {
- var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id;
- var link =
- (
- <a
- href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={linkUrl}
- onClick={() => this.handleHide(this, false)}
- >Team Invite Link</a>
+ var link = (
+ <a
+ href='#'
+ onClick={this.showGetTeamInviteLinkModal}
+ >
+ {'Team Invite Link'}
+ </a>
);
teamInviteLink = (
<p>
- You can also invite people using the {link}.
+ {'You can also invite people using the '}{link}{'.'}
</p>
);
}
@@ -405,13 +408,6 @@ export default class InviteMemberModal extends React.Component {
return null;
}
-
- static show() {
- AppDispatcher.handleViewAction({
- type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL,
- value: true
- });
- }
}
InviteMemberModal.propTypes = {
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 6848ee5da..03cc75a08 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import EditChannelHeaderModal from './edit_channel_header_modal.jsx';
import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx';
import MessageWrapper from './message_wrapper.jsx';
import NotifyCounts from './notify_counts.jsx';
@@ -33,11 +34,15 @@ export default class Navbar extends React.Component {
this.onChange = this.onChange.bind(this);
this.handleLeave = this.handleLeave.bind(this);
this.showSearch = this.showSearch.bind(this);
+
+ this.showEditChannelHeaderModal = this.showEditChannelHeaderModal.bind(this);
+
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
+ state.showEditChannelHeaderModal = false;
state.showMembersModal = false;
state.showInviteModal = false;
this.state = state;
@@ -110,6 +115,16 @@ export default class Navbar extends React.Component {
this.setState(this.getStateFromStores());
$('#navbar .navbar-brand .description').popover({placement: 'bottom', trigger: 'click', html: true});
}
+ showEditChannelHeaderModal() {
+ // this can't be done using a ToggleModalButton because we can't use one inside an OverlayTrigger
+ if (this.refs.headerOverlay) {
+ this.refs.headerOverlay.hide();
+ }
+
+ this.setState({
+ showEditChannelHeaderModal: true
+ });
+ }
createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
if (channel) {
var viewInfoOption = (
@@ -129,11 +144,7 @@ export default class Navbar extends React.Component {
<a
role='menuitem'
href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ onClick={this.showEditChannelHeaderModal}
>
{'Set Channel Header...'}
</a>
@@ -239,7 +250,7 @@ export default class Navbar extends React.Component {
dialogType={ChannelNotificationsModal}
dialogProps={{channel}}
>
- {'Notification Preferences'}
+ {'Notification Preferences'}
</ToggleModalButton>
</li>
);
@@ -249,6 +260,7 @@ export default class Navbar extends React.Component {
<div className='navbar-brand'>
<div className='dropdown'>
<OverlayTrigger
+ ref='headerOverlay'
trigger='click'
placement='bottom'
overlay={popoverContent}
@@ -358,6 +370,9 @@ export default class Navbar extends React.Component {
var isAdmin = false;
var isDirect = false;
+ var editChannelHeaderModal = null;
+ var editChannelPurposeModal = null;
+
if (channel) {
popoverContent = (
<Popover
@@ -400,11 +415,7 @@ export default class Navbar extends React.Component {
<br/>
<a
href='#'
- data-toggle='modal'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- data-target='#edit_channel'
+ onClick={this.showEditChannelHeaderModal}
>
{'Click here'}
</a>
@@ -413,6 +424,22 @@ export default class Navbar extends React.Component {
</Popover>
);
}
+
+ editChannelHeaderModal = (
+ <EditChannelHeaderModal
+ show={this.state.showEditChannelHeaderModal}
+ onHide={() => this.setState({showEditChannelHeaderModal: false})}
+ channel={channel}
+ />
+ );
+
+ editChannelPurposeModal = (
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ );
}
var collapseButtons = this.createCollapseButtons(currentId);
@@ -443,11 +470,8 @@ export default class Navbar extends React.Component {
</div>
</div>
</nav>
- <EditChannelPurposeModal
- show={this.state.showEditChannelPurposeModal}
- onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
- channel={channel}
- />
+ {editChannelHeaderModal}
+ {editChannelPurposeModal}
<ChannelMembersModal
show={this.state.showMembersModal}
onModalDismissed={() => this.setState({showMembersModal: false})}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index c0230fe5f..a14434bfc 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -5,9 +5,9 @@ import * as Utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import TeamStore from '../stores/team_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import AboutBuildModal from './about_build_modal.jsx';
-import InviteMemberModal from './invite_member_modal.jsx';
import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import Constants from '../utils/constants.jsx';
@@ -93,7 +93,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
- onClick={InviteMemberModal.show}
+ onClick={EventHelpers.showInviteMemberModal}
>
{'Invite New Member'}
</a>
@@ -105,10 +105,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
+ onClick={EventHelpers.showGetTeamInviteLinkModal}
>
{'Get Team Invite Link'}
</a>
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 5b61c711c..278261e22 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -105,7 +105,7 @@ export default class Post extends React.Component {
} else {
commentRootId = post.id;
}
- for (let postId in posts) {
+ for (const postId in posts) {
if (posts[postId].root_id === commentRootId) {
commentCount += 1;
}
@@ -114,53 +114,58 @@ export default class Post extends React.Component {
return commentCount;
}
render() {
- var post = this.props.post;
- var parentPost = this.props.parentPost;
- var posts = this.props.posts;
+ const post = this.props.post;
+ const parentPost = this.props.parentPost;
+ const posts = this.props.posts;
if (!post.props) {
post.props = {};
}
- var type = 'Post';
+ let type = 'Post';
if (post.root_id && post.root_id.length > 0) {
type = 'Comment';
}
const commentCount = this.getCommentCount(this.props);
- var rootUser;
+ let rootUser;
if (this.props.sameRoot) {
rootUser = 'same--root';
} else {
rootUser = 'other--root';
}
- var postType = '';
+ let postType = '';
if (type !== 'Post') {
postType = 'post--comment';
} else if (commentCount > 0) {
postType = 'post--root';
}
- var currentUserCss = '';
+ let currentUserCss = '';
if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) {
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) {
timestamp = userProfile.update_at;
}
- var sameUserClass = '';
+ let sameUserClass = '';
if (this.props.sameUser) {
sameUserClass = 'same--user';
}
- var profilePic = null;
+ let shouldHighlightClass = '';
+ if (this.props.shouldHighlight) {
+ shouldHighlightClass = 'post--highlight';
+ }
+
+ let profilePic = null;
if (!this.props.hideProfilePic) {
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') {
@@ -182,7 +187,7 @@ export default class Post extends React.Component {
<div>
<div
id={'post_' + post.id}
- className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}
+ className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass}
>
<div className='post__content'>
<div className='post__img'>{profilePic}</div>
@@ -218,5 +223,6 @@ Post.propTypes = {
sameUser: React.PropTypes.bool,
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
- isLastComment: React.PropTypes.bool
+ isLastComment: React.PropTypes.bool,
+ shouldHighlight: React.PropTypes.bool
};
diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx
new file mode 100644
index 000000000..5c6ad6c28
--- /dev/null
+++ b/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 = {
+};
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 5306edd3d..cedb2b59b 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -1,22 +1,30 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import DeletePostModal from './delete_post_modal.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 * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Constants from '../utils/constants.jsx';
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+const Popover = ReactBootstrap.Popover;
+
export default class PostInfo extends React.Component {
constructor(props) {
super(props);
- this.state = {};
+ this.state = {
+ copiedLink: false
+ };
+
+ this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this);
}
createDropdown() {
var post = this.props.post;
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) {
return '';
@@ -66,7 +74,7 @@ export default class PostInfo extends React.Component {
<a
href='#'
role='menuitem'
- onClick={() => DeletePostModal.show(post, dataComments)}
+ onClick={() => EventHelpers.showDeletePostModal(post, dataComments)}
>
{'Delete'}
</a>
@@ -81,7 +89,7 @@ export default class PostInfo extends React.Component {
role='presentation'
>
<a
- className='reply-link visible-xs theme'
+ className='link__reply theme'
href='#'
onClick={this.props.handleCommentClick}
>
@@ -113,6 +121,21 @@ export default class PostInfo extends React.Component {
</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() {
var post = this.props.post;
var comments = '';
@@ -143,6 +166,37 @@ export default class PostInfo extends React.Component {
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 (
<ul className='post__header post__header--info'>
<li className='col'>
@@ -152,6 +206,15 @@ export default class PostInfo extends React.Component {
</li>
<li className='col col__reply'>
{comments}
+ <OverlayTrigger
+ trigger='click'
+ placement='left'
+ rootClose={true}
+ overlay={permalinkOverlay}
+ >
+ <i className={'permalink-icon fa fa-link ' + showCommentClass}/>
+ </OverlayTrigger>
+
<div className='dropdown'>
{dropdown}
</div>
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 5b36ecbc5..5e374b877 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import UserStore from '../stores/user_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as Utils from '../utils/utils.jsx';
import Post from './post.jsx';
import Constants from '../utils/constants.jsx';
@@ -13,6 +14,7 @@ export default class PostsView extends React.Component {
this.handleScroll = this.handleScroll.bind(this);
this.isAtBottom = this.isAtBottom.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
this.createPosts = this.createPosts.bind(this);
this.updateScrolling = this.updateScrolling.bind(this);
this.handleResize = this.handleResize.bind(this);
@@ -27,12 +29,15 @@ export default class PostsView extends React.Component {
static get SCROLL_TYPE_BOTTOM() {
return 2;
}
- static get SIDEBAR_OPEN() {
+ static get SCROLL_TYPE_SIDEBAR_OPEN() {
return 3;
}
static get SCROLL_TYPE_NEW_MESSAGE() {
return 4;
}
+ static get SCROLL_TYPE_POST() {
+ return 5;
+ }
isAtBottom() {
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();
+ if (!this.jumpToPostNode && childNodes.length > 0) {
+ this.jumpToPostNode = childNodes[childNodes.length - 1];
+ }
// --- --------
this.props.postViewScrolled(this.isAtBottom());
this.prevScrollHeight = this.refs.postlist.scrollHeight;
+ this.prevOffsetTop = this.jumpToPostNode.offsetTop;
}
loadMorePostsTop() {
this.props.loadMorePostsTopClicked();
}
+ loadMorePostsBottom() {
+ this.props.loadMorePostsBottomClicked();
+ }
createPosts(posts, order) {
const postCtls = [];
let previousPostDay = new Date(0);
@@ -63,12 +75,7 @@ export default class PostsView extends React.Component {
let renderedLastViewed = false;
- let numToDisplay = this.props.numPostsToDisplay;
- if (order.length - 1 < numToDisplay) {
- numToDisplay = order.length - 1;
- }
-
- for (let i = numToDisplay; i >= 0; i--) {
+ for (let i = order.length - 1; i >= 0; i--) {
const post = posts[order[i]];
const parentPost = posts[post.parent_id];
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 shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
+
const postCtl = (
<Post
key={keyPrefix + 'postKey'}
@@ -124,6 +133,8 @@ export default class PostsView extends React.Component {
posts={posts}
hideProfilePic={hideProfilePic}
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;
}
});
- } 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(() => {
- const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPost]);
+ const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
+ if (postNode == null) {
+ return;
+ }
postNode.scrollIntoView();
if (this.refs.postlist.scrollTop === postNode.offsetTop) {
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);
}
});
- } 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 (this.wasAtBottom) {
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) {
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();
}
componentDidMount() {
- this.updateScrolling();
+ if (this.props.postList != null) {
+ this.updateScrolling();
+ }
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
componentDidUpdate() {
- this.updateScrolling();
+ if (this.props.postList != null) {
+ this.updateScrolling();
+ }
}
shouldComponentUpdate(nextProps) {
if (this.props.isActive !== nextProps.isActive) {
@@ -235,15 +256,12 @@ export default class PostsView extends React.Component {
if (this.props.postList !== nextProps.postList) {
return true;
}
- if (this.props.scrollPost !== nextProps.scrollPost) {
+ if (this.props.scrollPostId !== nextProps.scrollPostId) {
return true;
}
if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) {
return true;
}
- if (this.props.numPostsToDisplay !== nextProps.numPostsToDisplay) {
- return true;
- }
if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
return true;
}
@@ -256,7 +274,8 @@ export default class PostsView extends React.Component {
render() {
let posts = [];
let order = [];
- let moreMessages;
+ let moreMessagesTop;
+ let moreMessagesBottom;
let postElements;
let activeClass = 'inactive';
if (this.props.postList != null) {
@@ -264,10 +283,10 @@ export default class PostsView extends React.Component {
order = this.props.postList.order;
// Create intro message or top loadmore link
- if (order.length >= this.props.numPostsToDisplay) {
- moreMessages = (
+ if (this.props.showMoreMessagesTop) {
+ moreMessagesTop = (
<a
- ref='loadmore'
+ ref='loadmoretop'
className='more-messages-text theme'
href='#'
onClick={this.loadMorePostsTop}
@@ -276,7 +295,23 @@ export default class PostsView extends React.Component {
</a>
);
} 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
@@ -299,8 +334,9 @@ export default class PostsView extends React.Component {
ref='postlistcontent'
className='post-list__content'
>
- {moreMessages}
+ {moreMessagesTop}
{postElements}
+ {moreMessagesBottom}
</div>
</div>
</div>
@@ -313,11 +349,14 @@ PostsView.defaultProps = {
PostsView.propTypes = {
isActive: React.PropTypes.bool,
postList: React.PropTypes.object,
- scrollPost: React.PropTypes.string,
+ scrollPostId: React.PropTypes.string,
scrollType: React.PropTypes.number,
postViewScrolled: 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,
- messageSeparatorTime: React.PropTypes.number
+ messageSeparatorTime: React.PropTypes.number,
+ postsToHighlight: React.PropTypes.object
};
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index c71ef401e..367d3687e 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -9,12 +9,9 @@ import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
import * as Utils from '../utils/utils.jsx';
-import * as Client from '../utils/client.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import * as AsyncClient from '../utils/async_client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Constants from '../utils/constants.jsx';
-const ActionTypes = Constants.ActionTypes;
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.handlePostsViewScroll = this.handlePostsViewScroll.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);
const currentChannelId = ChannelStore.getCurrentId();
const state = {
scrollType: PostsView.SCROLL_TYPE_BOTTOM,
- scrollPost: null,
- numPostsToDisplay: Constants.POST_CHUNK_SIZE
+ scrollPost: null
};
if (currentChannelId) {
Object.assign(state, {
currentChannelIndex: 0,
channels: [currentChannelId],
- postLists: [this.getChannelPosts(currentChannelId)]
+ postLists: [this.getChannelPosts(currentChannelId)],
+ atTop: [PostStore.getVisibilityAtTop(currentChannelId)]
});
} else {
Object.assign(state, {
currentChannelIndex: null,
channels: [],
- postLists: []
+ postLists: [],
+ atTop: []
});
}
@@ -78,24 +74,21 @@ export default class PostsViewContainer extends React.Component {
});
break;
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
- this.setState({scrollType: PostsView.SIDEBAR_OPEN});
+ this.setState({scrollType: PostsView.SCROLL_TYPE_SIDEBAR_OPEN});
break;
}
}
onChannelChange() {
const postLists = this.state.postLists.slice();
+ const atTop = this.state.atTop.slice();
const channels = this.state.channels.slice();
const channelId = ChannelStore.getCurrentId();
// Has the channel really changed?
if (channelId === channels[this.state.currentChannelIndex]) {
- // Dirty hack
- this.forceUpdate();
return;
}
- PostStore.clearUnseenDeletedPosts(channelId);
-
let lastViewed = Number.MAX_VALUE;
const member = ChannelStore.getMember(channelId);
if (member != null) {
@@ -107,115 +100,45 @@ export default class PostsViewContainer extends React.Component {
newIndex = channels.length;
channels.push(channelId);
postLists[newIndex] = this.getChannelPosts(channelId);
+ atTop[newIndex] = PostStore.getVisibilityAtTop(channelId);
}
+
this.setState({
currentChannelIndex: newIndex,
currentLastViewed: lastViewed,
scrollType: PostsView.SCROLL_TYPE_NEW_MESSAGE,
channels,
- postLists});
+ postLists,
+ atTop});
}
onChannelLeave(id) {
const postLists = this.state.postLists.slice();
const channels = this.state.channels.slice();
+ const atTop = this.state.atTop.slice();
const index = channels.indexOf(id);
if (index !== -1) {
postLists.splice(index, 1);
channels.splice(index, 1);
+ atTop.splice(index, 1);
}
- this.setState({channels, postLists});
+ this.setState({channels, postLists, atTop});
}
onPostsChange() {
const channels = this.state.channels;
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;
- this.setState({postLists});
+ atTop[this.state.currentChannelIndex] = PostStore.getVisibilityAtTop(currentChannelId);
+ this.setState({postLists, atTop});
}
getChannelPosts(id) {
- const postList = PostStore.getPosts(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;
+ return PostStore.getVisiblePosts(id);
}
loadMorePostsTop() {
- const postLists = this.state.postLists;
- 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');
+ EventHelpers.emitLoadMorePostsEvent();
}
handlePostsViewScroll(atBottom) {
if (atBottom) {
@@ -246,15 +169,17 @@ export default class PostsViewContainer extends React.Component {
isActive={isActive}
postList={postLists[i]}
scrollType={this.state.scrollType}
- scrollPost={this.state.scrollPost}
+ scrollPostId={this.state.scrollPost}
postViewScrolled={this.handlePostsViewScroll}
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}
messageSeparatorTime={this.state.currentLastViewed}
/>
);
- if ((!postLists[i] || !channel) && isActive) {
+ if (!postLists[i] && isActive) {
postListCtls.push(
<LoadingScreen
position='absolute'
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 3e555c85a..7aae5177e 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -8,13 +8,13 @@ import UserStore from '../stores/user_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
-import DeletePostModal from './delete_post_modal.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
var ActionTypes = Constants.ActionTypes;
import * as TextFormatting from '../utils/text_formatting.jsx';
import twemoji from 'twemoji';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
export default class RhsComment extends React.Component {
constructor(props) {
@@ -115,7 +115,7 @@ export default class RhsComment extends React.Component {
<a
href='#'
role='menuitem'
- onClick={() => DeletePostModal.show(post, 0)}
+ onClick={() => EventHelpers.showDeletePostModal(post, 0)}
>
{'Delete'}
</a>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 96f43bdb5..8142888ba 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -6,11 +6,11 @@ import UserProfile from './user_profile.jsx';
import UserStore from '../stores/user_store.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
import * as utils from '../utils/utils.jsx';
-import DeletePostModal from './delete_post_modal.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import twemoji from 'twemoji';
import Constants from '../utils/constants.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
export default class RhsRootPost extends React.Component {
constructor(props) {
@@ -94,7 +94,7 @@ export default class RhsRootPost extends React.Component {
<a
href='#'
role='menuitem'
- onClick={() => DeletePostModal.show(post, this.props.commentCount)}
+ onClick={() => EventHelpers.showDeletePostModal(post, this.props.commentCount)}
>
{'Delete'}
</a>
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 4f453f76b..61f138539 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -94,7 +94,7 @@ export default class RhsThread extends React.Component {
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) {
return;
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 2202831a3..da422fe1b 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -1,17 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import SearchStore from '../stores/search_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
import UserProfile from './user_profile.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.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';
-var ActionTypes = Constants.ActionTypes;
export default class SearchResultsItem extends React.Component {
constructor(props) {
@@ -23,32 +18,7 @@ export default class SearchResultsItem extends React.Component {
handleClick(e) {
e.preventDefault();
- var self = this;
-
- 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);
+ EventHelpers.emitPostFocusEvent(this.props.post.id);
}
render() {
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 77d222436..30422ff7d 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -201,10 +201,6 @@ export default class Sidebar extends React.Component {
});
}
updateScrollbar() {
- if (this.state.windowWidth > 768) {
- $('.nav-pills__container').perfectScrollbar();
- $('.nav-pills__container').perfectScrollbar('update');
- }
}
onChange() {
this.setState(this.getStateFromStores());
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index f6c0c8adb..0525eca4b 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -1,11 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import InviteMemberModal from './invite_member_modal.jsx';
import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import UserStore from '../stores/user_store.jsx';
-import TeamStore from '../stores/team_store.jsx';
import * as client from '../utils/client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as utils from '../utils/utils.jsx';
export default class SidebarRightMenu extends React.Component {
@@ -46,7 +45,7 @@ export default class SidebarRightMenu extends React.Component {
<li>
<a
href='#'
- onClick={InviteMemberModal.show}
+ onClick={EventHelpers.showInviteMemberModal}
>
<i className='fa fa-user'></i>Invite New Member
</a>
@@ -56,12 +55,12 @@ export default class SidebarRightMenu extends React.Component {
if (this.props.teamType === 'O') {
teamLink = (
<li>
- <a href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
- ><i className='fa fa-link'></i>Get Team Invite Link</a>
+ <a
+ href='#'
+ onClick={EventHelpers.showGetTeamInviteLinkModal}
+ >
+ <i className='glyphicon glyphicon-link'></i>{'Get Team Invite Link'}
+ </a>
</li>
);
}
diff --git a/web/react/components/toggle_modal_button.jsx b/web/react/components/toggle_modal_button.jsx
index eae4a024d..ce8ff3f60 100644
--- a/web/react/components/toggle_modal_button.jsx
+++ b/web/react/components/toggle_modal_button.jsx
@@ -22,7 +22,17 @@ export default class ModalToggleButton extends React.Component {
}
render() {
- const {children, dialogType, dialogProps, ...props} = this.props; //eslint-disable-line no-redeclare
+ const {children, dialogType, dialogProps, onClick, ...props} = this.props; // eslint-disable-line no-redeclare
+
+ // allow callers to provide an onClick which will be called before the modal is shown
+ let clickHandler = this.show;
+ if (onClick) {
+ clickHandler = () => {
+ onClick();
+
+ this.show();
+ };
+ }
// this assumes that all modals will have a show property and an onHide event
const dialog = React.createElement(this.props.dialogType, Object.assign({}, dialogProps, {
@@ -42,7 +52,7 @@ export default class ModalToggleButton extends React.Component {
<a
{...props}
href='#'
- onClick={this.show}
+ onClick={clickHandler}
>
{children}
{dialog}
@@ -54,7 +64,8 @@ export default class ModalToggleButton extends React.Component {
ModalToggleButton.propTypes = {
children: React.PropTypes.node.isRequired,
dialogType: React.PropTypes.func.isRequired,
- dialogProps: React.PropTypes.object
+ dialogProps: React.PropTypes.object,
+ onClick: React.PropTypes.func
};
ModalToggleButton.defaultProps = {
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx
new file mode 100644
index 000000000..d7f255aaa
--- /dev/null
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -0,0 +1,106 @@
+// 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
+ });
+}
+
+export function showDeletePostModal(post, commentCount = 0) {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_DELETE_POST_MODAL,
+ value: true,
+ post,
+ commentCount
+ });
+}
+
+export function showGetTeamInviteLinkModal() {
+ AppDispatcher.handleViewAction({
+ type: Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL,
+ value: true
+ });
+}
+
+export function showInviteMemberModal() {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL,
+ value: true
+ });
+}
diff --git a/web/react/package.json b/web/react/package.json
index b63fe35fb..41b2468af 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -22,13 +22,14 @@
"watchify": "3.6.1",
"eslint": "1.9.0",
"eslint-plugin-react": "3.9.0",
+ "exorcist": "0.4.0",
"babel-eslint": "4.1.5"
},
"scripts": {
"check": "",
"build-libs": "browserify -r crypto -r autolinker -r flux -r keymirror -r marked -r object-assign -r twemoji | uglifyjs -c -m --screw-ie8 > ../static/js/libs.min.js",
"start": "watchify --fast -x crypto -x node -x autolinker -x flux -x keymirror -x marked -x object-assign -x twemoji -o ../static/js/bundle.js -v -d ./**/*.jsx",
- "build": "browserify -x crypto -x autolinker -x flux -x keymirror -x marked -x object-assign -x twemoji ./**/*.jsx | uglifyjs -c -m --screw-ie8 > ../static/js/bundle.min.js"
+ "build": "browserify -x crypto -x autolinker -x flux -x keymirror -x marked -x object-assign -x twemoji -d ./**/*.jsx | exorcist ../static/js/inter.js.map > ../static/js/tmp.js && uglifyjs ../static/js/tmp.js --in-source-map \"../static/js/inter.js.map\" --source-map \"../static/js/bundle.min.js.map\" --source-map-url \"/static/js/bundle.min.js.map\" -c -m --screw-ie8 > ../static/js/bundle.min.js && rm ../static/js/tmp.js && rm ../static/js/inter.js.map"
},
"browserify": {
"transform": [
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 126942e65..161e6ab22 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -1,15 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import ChannelView from '../components/channel_view.jsx';
import ChannelLoader from '../components/channel_loader.jsx';
import ErrorBar from '../components/error_bar.jsx';
import ErrorStore from '../stores/error_store.jsx';
import MentionList from '../components/mention_list.jsx';
-import GetLinkModal from '../components/get_link_modal.jsx';
-import EditChannelModal from '../components/edit_channel_modal.jsx';
+import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
import RenameChannelModal from '../components/rename_channel_modal.jsx';
import EditPostModal from '../components/edit_post_modal.jsx';
import DeletePostModal from '../components/delete_post_modal.jsx';
@@ -23,15 +21,14 @@ import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx
import InviteMemberModal from '../components/invite_member_modal.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
-import Constants from '../utils/constants.jsx';
-var ActionTypes = Constants.ActionTypes;
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-function setupChannelPage(props) {
- AppDispatcher.handleViewAction({
- type: ActionTypes.CLICK_CHANNEL,
- name: props.ChannelName,
- id: props.ChannelId
- });
+function setupChannelPage(props, team, channel) {
+ if (props.PostId === '') {
+ EventHelpers.emitChannelClickEvent(channel);
+ } else {
+ EventHelpers.emitPostFocusEvent(props.PostId);
+ }
AsyncClient.getAllPreferences();
@@ -70,8 +67,8 @@ function setupChannelPage(props) {
// Modals
//
ReactDOM.render(
- <GetLinkModal />,
- document.getElementById('get_link_modal')
+ <GetTeamInviteLinkModal />,
+ document.getElementById('get_team_invite_link_modal')
);
ReactDOM.render(
@@ -95,11 +92,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <EditChannelModal />,
- document.getElementById('edit_channel_modal')
- );
-
- ReactDOM.render(
<RenameChannelModal />,
document.getElementById('rename_channel_modal')
);
diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx
index 2c1edaa3a..ff81c4994 100644
--- a/web/react/pages/home.jsx
+++ b/web/react/pages/home.jsx
@@ -1,12 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ChannelStore from '../stores/channel_store.jsx';
import TeamStore from '../stores/team_store.jsx';
import Constants from '../utils/constants.jsx';
function setupHomePage() {
- var last = ChannelStore.getLastVisitedName();
+ var last = null;
if (last == null || last.length === 0) {
window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL;
} else {
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index 1d481ada4..dec4926f5 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -8,8 +8,6 @@ var Utils;
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import BrowserStore from '../stores/browser_store.jsx';
-
const CHANGE_EVENT = 'change';
const LEAVE_EVENT = 'leave';
const MORE_CHANGE_EVENT = 'change';
@@ -21,7 +19,38 @@ class ChannelStoreClass extends EventEmitter {
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.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() {
this.emit(CHANGE_EVENT);
@@ -90,16 +119,6 @@ class ChannelStoreClass extends EventEmitter {
setCurrentId(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) {
var cm = this.pGetChannelMembers();
for (var cmid in cm) {
@@ -192,10 +211,10 @@ class ChannelStoreClass extends EventEmitter {
this.pStoreChannels(channels);
}
pStoreChannels(channels) {
- BrowserStore.setItem('channels', channels);
+ this.channels = channels;
}
pGetChannels() {
- return BrowserStore.getItem('channels', []);
+ return this.channels;
}
pStoreChannelMember(channelMember) {
var members = this.pGetChannelMembers();
@@ -203,49 +222,58 @@ class ChannelStoreClass extends EventEmitter {
this.pStoreChannelMembers(members);
}
pStoreChannelMembers(channelMembers) {
- BrowserStore.setItem('channel_members', channelMembers);
+ this.channelMembers = channelMembers;
}
pGetChannelMembers() {
- return BrowserStore.getItem('channel_members', {});
+ return this.channelMembers;
}
pStoreMoreChannels(channels) {
- BrowserStore.setItem('more_channels', channels);
+ this.moreChannels = channels;
}
pGetMoreChannels() {
- var channels = BrowserStore.getItem('more_channels');
-
- if (channels == null) {
- channels = {};
- channels.loading = true;
- }
-
- return channels;
+ return this.moreChannels;
}
pStoreExtraInfos(extraInfos) {
- BrowserStore.setItem('extra_infos', extraInfos);
+ this.extraInfos = extraInfos;
}
pGetExtraInfos() {
- return BrowserStore.getItem('extra_infos', {});
+ return this.extraInfos;
}
isDefault(channel) {
return channel.name === Constants.DEFAULT_CHANNEL;
}
+
+ pSetPostMode(mode) {
+ this.postMode = mode;
+ }
+
+ getPostMode() {
+ return this.postMode;
+ }
}
var ChannelStore = new ChannelStoreClass();
-ChannelStore.dispatchToken = AppDispatcher.register(function handleAction(payload) {
+ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
var currentId;
switch (action.type) {
case ActionTypes.CLICK_CHANNEL:
ChannelStore.setCurrentId(action.id);
- ChannelStore.setLastVisitedName(action.name);
ChannelStore.resetCounts(action.id);
+ ChannelStore.pSetPostMode(ChannelStore.POST_MODE_CHANNEL);
ChannelStore.emitChange();
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:
ChannelStore.pStoreChannels(action.channels);
ChannelStore.pStoreChannelMembers(action.members);
diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx
index 69f43a5cf..a26a97f53 100644
--- a/web/react/stores/modal_store.jsx
+++ b/web/react/stores/modal_store.jsx
@@ -34,6 +34,7 @@ class ModalStoreClass extends EventEmitter {
case ActionTypes.TOGGLE_IMPORT_THEME_MODAL:
case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL:
case ActionTypes.TOGGLE_DELETE_POST_MODAL:
+ case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
this.emit(type, value, args);
break;
}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index ec01eef18..c76560c25 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -12,9 +12,10 @@ import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
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 POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
+const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
class PostStoreClass extends EventEmitter {
constructor() {
@@ -24,10 +25,6 @@ class PostStoreClass extends EventEmitter {
this.addChangeListener = this.addChangeListener.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.addEditPostListener = this.addEditPostListener.bind(this);
this.removeEditPostListener = this.removeEditPostListner.bind(this);
@@ -36,27 +33,49 @@ class PostStoreClass extends EventEmitter {
this.addPostsViewJumpListener = this.addPostsViewJumpListener.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.pStorePosts = this.pStorePosts.bind(this);
- this.getPosts = this.getPosts.bind(this);
- this.getPost = this.getPost.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.storePendingPost = this.storePendingPost.bind(this);
- this.pStorePendingPosts = this.pStorePendingPosts.bind(this);
+
this.getPendingPosts = this.getPendingPosts.bind(this);
- this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this);
- this.storeUnseenDeletedPosts = this.storeUnseenDeletedPosts.bind(this);
- this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this);
- this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this);
+ this.storePendingPost = this.storePendingPost.bind(this);
this.removePendingPost = this.removePendingPost.bind(this);
- this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
this.clearPendingPosts = this.clearPendingPosts.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.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.storeCurrentDraft = this.storeCurrentDraft.bind(this);
this.getCurrentDraft = this.getCurrentDraft.bind(this);
@@ -70,6 +89,9 @@ class PostStoreClass extends EventEmitter {
this.getLatestUpdate = this.getLatestUpdate.bind(this);
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
this.getCommentCount = this.getCommentCount.bind(this);
+
+ this.postsInfo = {};
+ this.currentFocusedPostId = null;
}
emitChange() {
this.emit(CHANGE_EVENT);
@@ -83,16 +105,16 @@ class PostStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT, callback);
}
- emitSelectedPostChange(fromSearch) {
- this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
+ emitPostFocused() {
+ this.emit(FOCUSED_POST_CHANGE);
}
- addSelectedPostChangeListener(callback) {
- this.on(SELECTED_POST_CHANGE_EVENT, callback);
+ addPostFocusedListener(callback) {
+ this.on(FOCUSED_POST_CHANGE, callback);
}
- removeSelectedPostChangeListener(callback) {
- this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
+ removePostFocusedListener(callback) {
+ this.removeListener(FOCUSED_POST_CHANGE, callback);
}
emitEditPost(post) {
@@ -131,104 +153,157 @@ class PostStoreClass extends EventEmitter {
this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null);
}
- getCurrentPosts() {
- var currentId = ChannelStore.getCurrentId();
+ // All this does is makes sure the postsInfo is not null for the specified channel
+ makePostsInfo(id) {
+ if (!this.postsInfo.hasOwnProperty(id)) {
+ this.postsInfo[id] = {};
+ }
+ }
- if (currentId != null) {
- return this.getPosts(currentId);
+ getAllPosts(id) {
+ if (this.postsInfo.hasOwnProperty(id)) {
+ return Object.assign({}, this.postsInfo[id].postList);
}
+
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;
}
- var postList = makePostListNonNull(this.getPosts(channelId));
+ const combinedPosts = makePostListNonNull(this.getAllPosts(id));
- for (const pid in newPostsView.posts) {
- if (newPostsView.posts.hasOwnProperty(pid)) {
- const np = newPostsView.posts[pid];
+ for (const pid in newPosts.posts) {
+ if (newPosts.posts.hasOwnProperty(pid)) {
+ const np = newPosts.posts[pid];
if (np.delete_at === 0) {
- postList.posts[pid] = np;
- if (postList.order.indexOf(pid) === -1) {
- postList.order.push(pid);
+ combinedPosts.posts[pid] = np;
+ if (combinedPosts.order.indexOf(pid) === -1) {
+ combinedPosts.order.push(pid);
}
} else {
- if (pid in postList.posts) {
- delete postList.posts[pid];
+ if (pid in combinedPosts.posts) {
+ Reflect.deleteProperty(combinedPosts.posts, pid);
}
- const index = postList.order.indexOf(pid);
+ const index = combinedPosts.order.indexOf(pid);
if (index !== -1) {
- postList.order.splice(index, 1);
+ combinedPosts.order.splice(index, 1);
}
}
}
}
- postList.order.sort((a, b) => {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
+ combinedPosts.order.sort((a, b) => {
+ if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) {
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 0;
});
- var latestUpdate = 0;
- for (var pid in postList.posts) {
- if (postList.posts[pid].update_at > latestUpdate) {
- latestUpdate = postList.posts[pid].update_at;
- }
- }
-
- this.storeLatestUpdate(channelId, latestUpdate);
- this.pStorePosts(channelId, postList);
- this.emitChange();
- }
- pStorePosts(channelId, posts) {
- BrowserStore.setItem('posts_' + channelId, posts);
- }
- getPosts(channelId) {
- return BrowserStore.getItem('posts_' + channelId);
+ this.makePostsInfo(id);
+ this.postsInfo[id].postList = combinedPosts;
}
- getPost(channelId, postId) {
- return this.getPosts(channelId).posts[postId];
- }
- getCurrentUsersLatestPost(channelId, rootId) {
- const userId = UserStore.getCurrentId();
- var postList = makePostListNonNull(this.getPosts(channelId));
- var i = 0;
- var len = postList.order.length;
- var lastPost = null;
- for (i; i < len; i++) {
- let post = postList.posts[postList.order[i]];
- if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) {
- if (rootId) {
- if (post.root_id === rootId || post.id === rootId) {
- lastPost = post;
- break;
- }
- } else {
- lastPost = post;
- break;
- }
- }
- }
-
- return lastPost;
- }
storePost(post) {
- this.pStorePost(post);
- this.emitChange();
- }
- pStorePost(post) {
- var postList = this.getPosts(post.channel_id);
- postList = makePostListNonNull(postList);
+ const postList = makePostListNonNull(this.getAllPosts(post.channel_id));
if (post.pending_post_id !== '') {
this.removePendingPost(post.channel_id, post.pending_post_id);
@@ -241,65 +316,117 @@ class PostStoreClass extends EventEmitter {
postList.order.unshift(post.id);
}
- this.pStorePosts(post.channel_id, postList);
+ 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;
+ }
+ }
}
- removePost(postId, channelId) {
- var postList = this.getPosts(channelId);
+
+ 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 (postId in postList.posts) {
- delete postList.posts[postId];
+ if (post.id in postList.posts) {
+ Reflect.deleteProperty(postList.posts, post.id);
}
- var index = postList.order.indexOf(postId);
+ const index = postList.order.indexOf(post.id);
if (index !== -1) {
postList.order.splice(index, 1);
}
- this.pStorePosts(channelId, postList);
+ 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;
- var postList = this.getPendingPosts(post.channel_id);
- postList = makePostListNonNull(postList);
+ const postList = makePostListNonNull(this.getPendingPosts(post.channel_id));
postList.posts[post.pending_post_id] = post;
postList.order.unshift(post.pending_post_id);
- this.pStorePendingPosts(post.channel_id, postList);
+
+ this.makePostsInfo(post.channel_id);
+ this.postsInfo[post.channel_id].pendingPosts = 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;
- }
+ removePendingPost(channelId, pendingPostId) {
+ const postList = makePostListNonNull(this.getPendingPosts(channelId));
- if (posts[a].create_at > posts[b].create_at) {
- return -1;
- }
- if (posts[a].create_at < posts[b].create_at) {
- return 1;
- }
+ Reflect.deleteProperty(postList.posts, pendingPostId);
+ const index = postList.order.indexOf(pendingPostId);
+ if (index !== -1) {
+ postList.order.splice(index, 1);
+ }
- return 0;
- });
+ this.postsInfo[channelId].pendingPosts = postList;
+ this.emitChange();
+ }
- BrowserStore.setGlobalItem('pending_posts_' + channelId, postList);
+ clearPendingPosts(channelId) {
+ if (this.postsInfo.hasOwnProperty(channelId)) {
+ Reflect.deleteProperty(this.postsInfo[channelId], 'pendingPosts');
+ }
}
- getPendingPosts(channelId) {
- return BrowserStore.getGlobalItem('pending_posts_' + channelId);
+
+ 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) {
- var posts = this.getUnseenDeletedPosts(post.channel_id);
+ let posts = this.getUnseenDeletedPosts(post.channel_id);
if (!posts) {
posts = {};
@@ -310,58 +437,68 @@ class PostStoreClass extends EventEmitter {
post.filenames = [];
posts[post.id] = post;
- this.storeUnseenDeletedPosts(post.channel_id, posts);
- }
- storeUnseenDeletedPosts(channelId, posts) {
- BrowserStore.setItem('deleted_posts_' + channelId, posts);
+ this.postsInfo[post.channel_id].deletedPosts = posts;
}
+
getUnseenDeletedPosts(channelId) {
- return BrowserStore.getItem('deleted_posts_' + channelId);
+ if (this.postsInfo.hasOwnProperty(channelId)) {
+ return this.postsInfo[channelId].deletedPosts;
+ }
+
+ return null;
}
+
clearUnseenDeletedPosts(channelId) {
- BrowserStore.setItem('deleted_posts_' + channelId, {});
+ if (this.postsInfo.hasOwnProperty(channelId)) {
+ Reflect.deleteProperty(this.postsInfo[channelId], 'deletedPosts');
+ }
}
- removePendingPost(channelId, pendingPostId) {
- this.pRemovePendingPost(channelId, pendingPostId);
- this.emitChange();
+
+ storeSelectedPost(postList) {
+ this.selectedPost = postList;
}
- 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);
- }
+ getSelectedPost() {
+ return this.selectedPost;
+ }
- this.pStorePendingPosts(channelId, postList);
+ emitSelectedPostChange(fromSearch) {
+ this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
}
- clearPendingPosts() {
- BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => {
- BrowserStore.removeItem(key);
- });
+
+ addSelectedPostChangeListener(callback) {
+ this.on(SELECTED_POST_CHANGE_EVENT, callback);
}
- updatePendingPost(post) {
- var postList = this.getPendingPosts(post.channel_id);
- postList = makePostListNonNull(postList);
- if (postList.order.indexOf(post.pending_post_id) === -1) {
- return;
+ removeSelectedPostChangeListener(callback) {
+ this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
+ }
+
+ getCurrentUsersLatestPost(channelId, rootId) {
+ const userId = UserStore.getCurrentId();
+ var postList = makePostListNonNull(this.getAllPosts(channelId));
+ var i = 0;
+ var len = postList.order.length;
+ var lastPost = null;
+
+ for (i; i < len; i++) {
+ const post = postList.posts[postList.order[i]];
+ if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) {
+ if (rootId) {
+ if (post.root_id === rootId || post.id === rootId) {
+ lastPost = post;
+ break;
+ }
+ } else {
+ lastPost = post;
+ break;
+ }
+ }
}
- 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');
+ return lastPost;
}
+
getEmptyDraft() {
return {message: '', uploadsInProgress: [], previews: []};
}
@@ -402,16 +539,23 @@ class PostStoreClass extends EventEmitter {
});
}
storeLatestUpdate(channelId, time) {
- BrowserStore.setItem('latest_post_' + channelId, time);
+ if (!this.postsInfo.hasOwnProperty(channelId)) {
+ this.postsInfo[channelId] = {};
+ }
+ this.postsInfo[channelId].latestPost = time;
}
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) {
const posts = this.getPosts(post.channel_id).posts;
let commentCount = 0;
- for (let id in posts) {
+ for (const id in posts) {
if (posts.hasOwnProperty(id)) {
if (posts[id].root_id === post.id) {
commentCount += 1;
@@ -429,20 +573,45 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
- case ActionTypes.RECIEVED_POSTS:
- PostStore.storePosts(action.id, makePostListNonNull(action.post_list));
+ case ActionTypes.RECIEVED_POSTS: {
+ 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;
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();
break;
case ActionTypes.RECIEVED_POST_SELECTED:
PostStore.storeSelectedPost(action.post_list);
PostStore.emitSelectedPostChange(action.from_search);
break;
- case ActionTypes.RECIEVED_EDIT_POST:
- PostStore.emitEditPost(action);
- break;
default:
}
});
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index f2936c50a..2e0769cc4 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import UserStore from './user_store.jsx';
import PostStore from './post_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 AsyncClient from '../utils/async_client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Constants from '../utils/constants.jsx';
-const ActionTypes = Constants.ActionTypes;
const SocketEvents = Constants.SocketEvents;
const CHANGE_EVENT = 'change';
@@ -91,10 +90,9 @@ class SocketStoreClass extends EventEmitter {
};
conn.onmessage = (evt) => {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_MSG,
- msg: JSON.parse(evt.data)
- });
+ const msg = JSON.parse(evt.data);
+ this.handleMessage(msg);
+ this.emitChange(msg);
};
}
}
@@ -153,12 +151,12 @@ class SocketStoreClass extends EventEmitter {
function handleNewPostEvent(msg) {
// Store post
const post = JSON.parse(msg.props.post);
- PostStore.storePost(post);
+ EventHelpers.emitPostRecievedEvent(post);
// Update channel state
if (ChannelStore.getCurrentId() === msg.channel_id) {
if (window.isActive) {
- AsyncClient.updateLastViewedAt(true);
+ AsyncClient.updateLastViewedAt();
}
} else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
AsyncClient.getChannel(msg.channel_id);
@@ -237,20 +235,17 @@ function handlePostEditEvent(msg) {
function handlePostDeleteEvent(msg) {
const post = JSON.parse(msg.props.post);
-
- PostStore.storeUnseenDeletedPost(post);
- PostStore.removePost(post, true);
- PostStore.emitChange();
+ EventHelpers.emitPostDeletedEvent(post);
}
function handleNewUserEvent() {
AsyncClient.getProfiles();
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
}
function handleUserAddedEvent(msg) {
if (ChannelStore.getCurrentId() === msg.channel_id) {
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
}
if (UserStore.getCurrentId() === msg.user_id) {
@@ -273,7 +268,7 @@ function handleUserRemovedEvent(msg) {
$('#removed_from_channel').modal('show');
}
} else if (ChannelStore.getCurrentId() === msg.channel_id) {
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
}
}
@@ -286,17 +281,12 @@ function handleChannelViewedEvent(msg) {
var SocketStore = new SocketStoreClass();
-SocketStore.dispatchToken = AppDispatcher.register((payload) => {
+/*SocketStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
- case ActionTypes.RECIEVED_MSG:
- SocketStore.handleMessage(action.msg);
- SocketStore.emitChange(action.msg);
- break;
-
default:
}
-});
+ });*/
export default SocketStore;
diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx
index 26c83cc8c..2d518d9e7 100644
--- a/web/react/stores/team_store.jsx
+++ b/web/react/stores/team_store.jsx
@@ -31,6 +31,7 @@ class TeamStoreClass extends EventEmitter {
this.getCurrentId = this.getCurrentId.bind(this);
this.getCurrent = this.getCurrent.bind(this);
this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this);
+ this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this);
this.saveTeam = this.saveTeam.bind(this);
}
@@ -92,6 +93,16 @@ class TeamStoreClass extends EventEmitter {
return null;
}
+ getCurrentInviteLink() {
+ const current = this.getCurrent();
+
+ if (current) {
+ return getWindowLocationOrigin() + '/signup_user_complete/?id=' + current.invite_id;
+ }
+
+ return '';
+ }
+
saveTeam(team) {
var teams = this.getAll();
teams[team.id] = team;
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index fac4cd009..8cf111d55 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -40,88 +40,42 @@ function isCallInProgress(callName) {
return true;
}
-export function getChannels(force, updateLastViewed, checkVersion) {
- var channels = ChannelStore.getAll();
-
- if (channels.length === 0 || force) {
- if (isCallInProgress('getChannels')) {
- return;
- }
-
- callTracker.getChannels = utils.getTimestamp();
+export function getChannels(checkVersion) {
+ if (isCallInProgress('getChannels')) {
+ return;
+ }
- client.getChannels(
- (data, textStatus, xhr) => {
- callTracker.getChannels = 0;
+ callTracker.getChannels = utils.getTimestamp();
- if (checkVersion) {
- var serverVersion = xhr.getResponseHeader('X-Version-ID');
+ client.getChannels(
+ (data, textStatus, xhr) => {
+ callTracker.getChannels = 0;
- if (!BrowserStore.getLastServerVersion()) {
- BrowserStore.setLastServerVersion(serverVersion);
- }
+ 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 (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 (xhr.status === 304 || !data) {
+ return;
}
- );
- }
- if (updateLastViewed && ChannelStore.getCurrentId() != null) {
- updateLastViewedAt();
- }
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_CHANNELS,
+ channels: data.channels,
+ members: data.members
+ });
+ },
+ (err) => {
+ callTracker.getChannels = 0;
+ dispatchError(err, 'getChannels');
+ }
+ );
}
export function getChannel(id) {
@@ -152,14 +106,14 @@ export function getChannel(id) {
);
}
-export function updateLastViewedAt(force) {
+export function updateLastViewedAt() {
const channelId = ChannelStore.getCurrentId();
if (channelId === null) {
return;
}
- if (isCallInProgress(`updateLastViewed${channelId}`) && !force) {
+ if (isCallInProgress(`updateLastViewed${channelId}`)) {
return;
}
@@ -205,40 +159,35 @@ export function getMoreChannels(force) {
}
}
-export function getChannelExtraInfo(force) {
- var channelId = ChannelStore.getCurrentId();
+export function getChannelExtraInfo() {
+ const channelId = ChannelStore.getCurrentId();
if (channelId != null) {
if (isCallInProgress('getChannelExtraInfo_' + channelId)) {
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();
- client.getChannelExtraInfo(
- channelId,
- function getChannelExtraInfoSuccess(data, textStatus, xhr) {
- callTracker['getChannelExtraInfo_' + channelId] = 0;
-
- if (xhr.status === 304 || !data) {
- return;
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO,
- extra_info: data
- });
- },
- function getChannelExtraInfoFailure(err) {
- callTracker['getChannelExtraInfo_' + channelId] = 0;
- dispatchError(err, 'getChannelExtraInfo');
+ callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp();
+
+ client.getChannelExtraInfo(
+ channelId,
+ (data, textStatus, xhr) => {
+ callTracker['getChannelExtraInfo_' + channelId] = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
}
- );
- }
+
+ 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) {
- if (PostStore.getCurrentPosts() == null || force) {
- var channelId = id;
+export function getPostsPage(id, maxPosts) {
+ let channelId = id;
+ if (channelId == null) {
+ channelId = ChannelStore.getCurrentId();
if (channelId == null) {
- channelId = ChannelStore.getCurrentId();
- }
-
- if (isCallInProgress('getPostsPage_' + channelId)) {
return;
}
+ }
- var postList = PostStore.getCurrentPosts();
+ if (isCallInProgress('getPostsPage_' + channelId)) {
+ return;
+ }
- var max = maxPosts;
- if (max == null) {
- max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS;
- }
+ var postList = PostStore.getAllPosts(id);
- // if we already have more than POST_CHUNK_SIZE posts,
- // 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));
- }
+ var max = maxPosts;
+ if (max == null) {
+ max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS;
+ }
+
+ // if we already have more than POST_CHUNK_SIZE posts,
+ // 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));
+ }
+
+ if (channelId != null) {
+ callTracker['getPostsPage_' + channelId] = utils.getTimestamp();
- if (channelId != null) {
- callTracker['getPostsPage_' + channelId] = utils.getTimestamp();
-
- client.getPostsPage(
- channelId,
- 0,
- numPosts,
- function getPostsPageSuccess(data, textStatus, xhr) {
- if (xhr.status === 304 || !data) {
- return;
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POSTS,
- id: channelId,
- post_list: data
- });
-
- getProfiles();
- },
- function getPostsPageFailure(err) {
- dispatchError(err, 'getPostsPage');
- },
- function getPostsPageComplete() {
- callTracker['getPostsPage_' + channelId] = 0;
+ client.getPostsPage(
+ channelId,
+ 0,
+ numPosts,
+ (data, textStatus, xhr) => {
+ if (xhr.status === 304 || !data) {
+ return;
}
- );
- }
+
+ 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) {
- var channelId = id;
+ let channelId = id;
if (channelId == null) {
- if (ChannelStore.getCurrentId() == null) {
+ channelId = ChannelStore.getCurrentId();
+ if (channelId == null) {
return;
}
- channelId = ChannelStore.getCurrentId();
}
if (isCallInProgress('getPosts_' + channelId)) {
return;
}
- if (PostStore.getCurrentPosts() == null) {
- getPostsPage(true, id, Constants.POST_CHUNK_SIZE);
+ if (PostStore.getAllPosts(channelId) == null) {
+ getPostsPage(channelId, Constants.POST_CHUNK_SIZE);
return;
}
- var latestUpdate = PostStore.getLatestUpdate(channelId);
+ const latestUpdate = PostStore.getLatestUpdate(channelId);
callTracker['getPosts_' + channelId] = utils.getTimestamp();
client.getPosts(
channelId,
latestUpdate,
- function success(data, textStatus, xhr) {
+ (data, textStatus, xhr) => {
if (xhr.status === 304 || !data) {
return;
}
@@ -547,20 +499,100 @@ export function getPosts(id) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POSTS,
id: channelId,
+ before: true,
+ numRequested: Constants.POST_CHUNK_SIZE,
post_list: data
});
getProfiles();
},
- function fail(err) {
+ (err) => {
dispatchError(err, 'getPosts');
},
- function complete() {
+ () => {
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() {
if (isCallInProgress('getMe')) {
return;
diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx
index aef1593dc..6f83778c9 100644
--- a/web/react/utils/channel_intro_mssages.jsx
+++ b/web/react/utils/channel_intro_mssages.jsx
@@ -1,13 +1,14 @@
-
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from './utils.jsx';
-import InviteMemberModal from '../components/invite_member_modal.jsx';
+import EditChannelHeaderModal from '../components/edit_channel_header_modal.jsx';
+import ToggleModalButton from '../components/toggle_modal_button.jsx';
import UserProfile from '../components/user_profile.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import Constants from '../utils/constants.jsx';
import TeamStore from '../stores/team_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
export function createChannelIntroMessage(channel, showInviteModal) {
if (channel.type === 'D') {
@@ -49,17 +50,7 @@ export function createDMIntroMessage(channel) {
{'This is the start of your direct message history with ' + teammateName + '.'}<br/>
{'Direct messages and files shared here are not shown to people outside this area.'}
</p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
+ {createSetHeaderButton(channel)}
</div>
);
}
@@ -71,7 +62,7 @@ export function createDMIntroMessage(channel) {
);
}
-export function createOffTopicIntroMessage(channel, showInviteModal) { //eslint-disable-line react/no-multi-comp
+export function createOffTopicIntroMessage(channel, showInviteModal) {
return (
<div className='channel-intro'>
<h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
@@ -79,17 +70,7 @@ export function createOffTopicIntroMessage(channel, showInviteModal) { //eslint-
{'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
<br/>
</p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
+ {createSetHeaderButton(channel)}
<a
href='#'
className='intro-links'
@@ -101,7 +82,7 @@ export function createOffTopicIntroMessage(channel, showInviteModal) { //eslint-
);
}
-export function createDefaultIntroMessage(channel) { //eslint-disable-line react/no-multi-comp
+export function createDefaultIntroMessage(channel) {
const team = TeamStore.getCurrent();
let inviteModalLink;
if (team.type === Constants.INVITE_TEAM) {
@@ -109,7 +90,7 @@ export function createDefaultIntroMessage(channel) { //eslint-disable-line react
<a
className='intro-links'
href='#'
- onClick={InviteMemberModal.show}
+ onClick={EventHelpers.showInviteMemberModal}
>
<i className='fa fa-user-plus'></i>{'Invite others to this team'}
</a>
@@ -119,10 +100,7 @@ export function createDefaultIntroMessage(channel) { //eslint-disable-line react
<a
className='intro-links'
href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
+ onClick={EventHelpers.showGetTeamInviteLinkModal}
>
<i className='fa fa-user-plus'></i>{'Invite others to this team'}
</a>
@@ -138,23 +116,13 @@ export function createDefaultIntroMessage(channel) { //eslint-disable-line react
{'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
</p>
{inviteModalLink}
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
+ {createSetHeaderButton(channel)}
<br/>
</div>
);
}
-export function createStandardIntroMessage(channel, showInviteModal) { //eslint-disable-line react/no-multi-comp
+export function createStandardIntroMessage(channel, showInviteModal) {
var uiName = channel.display_name;
var creatorName = '';
@@ -193,17 +161,7 @@ export function createStandardIntroMessage(channel, showInviteModal) { //eslint-
{memberMessage}
<br/>
</p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a header'}
- </a>
+ {createSetHeaderButton(channel)}
<a
className='intro-links'
href='#'
@@ -214,3 +172,15 @@ export function createStandardIntroMessage(channel, showInviteModal) { //eslint-
</div>
);
}
+
+function createSetHeaderButton(channel) {
+ return (
+ <ToggleModalButton
+ className='intro-links'
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </ToggleModalButton>
+ );
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index e6c24aa9c..09e962161 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -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({
cache: false,
url: '/api/v1/channels/' + channelId + '/post/' + postId,
@@ -831,7 +861,24 @@ export function getPost(channelId, postId, success, error) {
error: function onError(xhr, status, err) {
var e = handleError('getPost', xhr, status, err);
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
});
}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 958bfa8d2..6281813e9 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -10,12 +10,17 @@ export default {
CLICK_CHANNEL: null,
CREATE_CHANNEL: null,
LEAVE_CHANNEL: null,
+ CREATE_POST: null,
+ POST_DELETED: null,
+
RECIEVED_CHANNELS: null,
RECIEVED_CHANNEL: null,
RECIEVED_MORE_CHANNELS: null,
RECIEVED_CHANNEL_EXTRA_INFO: null,
+ FOCUS_POST: null,
RECIEVED_POSTS: null,
+ RECIEVED_FOCUSED_POST: null,
RECIEVED_POST: null,
RECIEVED_EDIT_POST: null,
RECIEVED_SEARCH: null,
@@ -43,7 +48,8 @@ export default {
TOGGLE_IMPORT_THEME_MODAL: null,
TOGGLE_INVITE_MEMBER_MODAL: null,
- TOGGLE_DELETE_POST_MODAL: null
+ TOGGLE_DELETE_POST_MODAL: null,
+ TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null
}),
PayloadSources: keyMirror({
@@ -99,6 +105,7 @@ export default {
EMAIL_SERVICE: 'email',
POST_CHUNK_SIZE: 60,
MAX_POST_CHUNKS: 3,
+ POST_FOCUS_CONTEXT_RADIUS: 10,
POST_LOADING: 'loading',
POST_FAILED: 'failed',
POST_DELETED: 'deleted',
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 7957ea31b..b0ec64bfd 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -110,32 +110,47 @@ class MattermostMarkdownRenderer extends marked.Renderer {
this.formattingOptions = formattingOptions;
}
- code(code, language) {
- let usedLanguage = language;
+ code(code, language, escaped) {
+ let usedLanguage = language || '';
+ usedLanguage = usedLanguage.toLowerCase();
- if (String(usedLanguage).toLocaleLowerCase() === 'html') {
+ // treat html as xml to prevent injection attacks
+ if (usedLanguage === 'html') {
usedLanguage = 'xml';
}
- if (usedLanguage && (usedLanguage === 'tex' || usedLanguage === 'latex')) {
+ if (HighlightedLanguages[usedLanguage]) {
+ const parsed = highlightJs.highlight(usedLanguage, code);
+
+ return (
+ '<div class="post-body--code">' +
+ '<span class="post-body--code__language">' +
+ HighlightedLanguages[usedLanguage] +
+ '</span>' +
+ '<pre>' +
+ '<code class="hljs">' +
+ parsed.value +
+ '</code>' +
+ '</pre>' +
+ '</div>'
+ );
+ } else if (usedLanguage === 'tex' || usedLanguage === 'latex') {
try {
- var html = katex.renderToString(TextFormatting.sanitizeHtml(code), {throwOnError: false, displayMode: true});
+ const html = katex.renderToString(TextFormatting.sanitizeHtml(code), {throwOnError: false, displayMode: true});
+
return '<div class="post-body--code tex">' + html + '</div>';
} catch (e) {
- return '<div class="post-body--code">' + TextFormatting.sanitizeHtml(code) + '</div>';
+ // fall through if latex parsing fails and handle below
}
}
- if (!usedLanguage || highlightJs.listLanguages().indexOf(usedLanguage) < 0) {
- let parsed = super.code(code, usedLanguage);
- return '<div class="post-body--code"><code class="hljs">' + TextFormatting.sanitizeHtml($(parsed).text()) + '</code></div>';
- }
-
- let parsed = highlightJs.highlight(usedLanguage, code);
- return '<div class="post-body--code">' +
- '<span class="post-body--code__language">' + HighlightedLanguages[usedLanguage] + '</span>' +
- '<code class="hljs">' + parsed.value + '</code>' +
- '</div>';
+ return (
+ '<pre>' +
+ '<code class="hljs">' +
+ (escaped ? code : TextFormatting.sanitizeHtml(code)) + '\n' +
+ '</code>' +
+ '</pre>'
+ );
}
br() {
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 668d8100f..9b2f7e057 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
@@ -748,19 +749,10 @@ export function updateCodeTheme(theme) {
export function placeCaretAtEnd(el) {
el.focus();
- if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') {
- var range = document.createRange();
- range.selectNodeContents(el);
- range.collapse(false);
- var sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- } else if (typeof document.body.createTextRange != 'undefined') {
- var textRange = document.body.createTextRange();
- textRange.moveToElementText(el);
- textRange.collapse(false);
- textRange.select();
- }
+ el.selectionStart = el.value.length;
+ el.selectionEnd = el.value.length;
+
+ return;
}
export function getCaretPosition(el) {
@@ -839,23 +831,15 @@ export function isValidUsername(name) {
}
export function updateAddressBar(channelName) {
- var teamURL = window.location.href.split('/channels')[0];
+ const teamURL = TeamStore.getCurrentTeamUrl();
history.replaceState('data', '', teamURL + '/channels/' + channelName);
}
export function switchChannel(channel) {
- AppDispatcher.handleViewAction({
- type: ActionTypes.CLICK_CHANNEL,
- name: channel.name,
- id: channel.id
- });
+ EventHelpers.emitChannelClickEvent(channel);
updateAddressBar(channel.name);
- AsyncClient.getChannels(true, true, true);
- AsyncClient.getChannelExtraInfo(true);
- AsyncClient.getPosts(channel.id);
-
$('.inner__wrap').removeClass('move--right');
$('.sidebar--left').removeClass('move--right');
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
index ea5ccd2d2..6d1ecbfa0 100644
--- a/web/sass-files/sass/partials/_markdown.scss
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -9,11 +9,26 @@
}
}
#post-list {
- .markdown-inline-img {
- -moz-force-broken-image-icon: 1;
- max-height: 500px;
- height: 500px;
- }
+ .markdown-inline-img {
+ -moz-force-broken-image-icon: 1;
+ max-height: 500px;
+ height: 500px;
+ }
+}
+
+.post-body--code {
+ position: relative;
+}
+
+.post-body--code__language {
+ position: absolute;
+ top: 0;
+ right: 0;
+ color: #fff;
+ background: #21586D;
+ padding: 4px 10px 5px 10px;
+ font-size: 13px;
+ opacity: 0.7;
}
.post__body {
@@ -25,6 +40,9 @@
border: 0 none;
@include opacity(0.2);
}
+ code {
+ white-space: pre;
+ }
}
.markdown__table {
background: #fff;
diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss
index c570c4663..aad46d3d9 100644
--- a/web/sass-files/sass/partials/_navbar.scss
+++ b/web/sass-files/sass/partials/_navbar.scss
@@ -33,6 +33,9 @@
background: #fff;
width: 21px;
}
+ .glyphicon-search {
+ top: -1px;
+ }
.icon--white {
color: #fff;
}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 99e985b0c..b7609bb7d 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -361,8 +361,15 @@ body.ios {
.dropdown, .comment-icon__container {
visibility: visible;
}
+ .permalink-icon {
+ visibility: visible;
+ }
}
+ &.post--highlight {
+ background-color: beige;
+ }
+
ul {
margin: 0;
padding: 0;
@@ -520,6 +527,8 @@ body.ios {
}
.post__header {
+ padding: 0;
+ list-style: none;
margin-bottom: 2px;
li {
@@ -588,6 +597,11 @@ body.ios {
padding: 0.2em 0.5em 0em;
@include legacy-pie-clearfix;
width: calc(100% - 70px);
+
+ ul {
+ padding: 5px 0 0 20px;
+ }
+
}
.post__link {
@@ -617,6 +631,12 @@ body.ios {
color: #999;
}
+ .permalink-icon {
+ display: inline-block;
+ color: $primary-color;
+ visibility: hidden;
+ }
+
.comment-icon__container {
fill: $primary-color;
display: inline-block;
@@ -755,4 +775,12 @@ body.ios {
margin: 0;
}
}
-} \ No newline at end of file
+}
+
+.permalink-text {
+ overflow: hidden;
+}
+
+.permalink-popover {
+ min-width: 320px;
+}
diff --git a/web/templates/channel.html b/web/templates/channel.html
index c15cea178..7b8f6a243 100644
--- a/web/templates/channel.html
+++ b/web/templates/channel.html
@@ -10,30 +10,21 @@
<div id="post_mention_tab"></div>
<div id="reply_mention_tab"></div>
<div id="edit_mention_tab"></div>
- <div id="get_link_modal"></div>
- <div id="user_settings_modal"></div>
+ <div id="get_team_invite_link_modal"></div>
<div id="import_theme_modal"></div>
<div id="team_settings_modal"></div>
<div id="invite_member_modal"></div>
- <div id="edit_channel_modal"></div>
- <div id="delete_channel_modal"></div>
<div id="rename_channel_modal"></div>
- <div id="rename_team_modal"></div>
<div id="edit_post_modal"></div>
<div id="delete_post_modal"></div>
<div id="more_channels_modal"></div>
- <div id="new_channel_modal"></div>
<div id="post_deleted_modal"></div>
<div id="channel_notifications_modal"></div>
<div id="team_members_modal"></div>
- <div id="direct_channel_modal"></div>
- <div id="channel_info_modal"></div>
- <div id="access_history_modal"></div>
- <div id="activity_log_modal"></div>
<div id="removed_from_channel_modal"></div>
<div id="register_app_modal"></div>
<script>
- window.setup_channel_page({{ .Props }});
+window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }}, {{ .User }});
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );
if($(window).height() > 1200){
$('.modal-body').css('max-height', 1000);
diff --git a/web/templates/head.html b/web/templates/head.html
index 30a83c4f2..ec06ba9bc 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -47,6 +47,7 @@
window.mm_config = {{ .ClientCfg }};
window.mm_team = {{ .Team }};
window.mm_user = {{ .User }};
+ window.mm_channel = {{ .Channel }};
if ({{.SessionTokenIndex}} >= 0) {
window.mm_session_token_index = {{.SessionTokenIndex}};
diff --git a/web/web.go b/web/web.go
index 477bd8b27..a72bff2bf 100644
--- a/web/web.go
+++ b/web/web.go
@@ -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-]+}/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}/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}/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.
@@ -342,15 +343,142 @@ func logout(c *api.Context, w http.ResponseWriter, r *http.Request) {
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) {
params := mux.Vars(r)
name := params["channelname"]
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
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
- return
+ return nil
} else {
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
- 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)
- }
+func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) {
+ userChan := api.Srv.Store.User().Get(c.Session.UserId)
var user *model.User
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)
}
- 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.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["TeamName"] = team.Name
- page.Props["TeamType"] = team.Type
- page.Props["TeamId"] = team.Id
- page.Props["ChannelName"] = name
- page.Props["ChannelId"] = channelId
- page.Props["UserId"] = c.Session.UserId
+ page.Props["ChannelName"] = channel.Name
+ page.Props["ChannelId"] = channel.Id
+ page.Props["PostId"] = postid
page.Team = team
page.User = user
+ page.Channel = channel
page.Render(c, w)
}