summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/admin_console/email_settings.jsx4
-rw-r--r--web/react/components/channel_invite_modal.jsx31
-rw-r--r--web/react/components/channel_members_modal.jsx49
-rw-r--r--web/react/components/file_attachment.jsx2
-rw-r--r--web/react/components/file_upload.jsx29
-rw-r--r--web/react/components/invite_member_modal.jsx10
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/posts_view.jsx7
-rw-r--r--web/react/utils/async_client.jsx3
-rw-r--r--web/react/utils/client.jsx11
-rw-r--r--web/react/utils/utils.jsx15
-rw-r--r--web/sass-files/sass/partials/_post.scss27
-rw-r--r--web/sass-files/sass/partials/_post_right.scss1
-rw-r--r--web/templates/head.html2
-rw-r--r--web/web.go6
15 files changed, 141 insertions, 58 deletions
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 6aec3c7b1..c568c5a77 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -581,12 +581,12 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='PushNotificationServer'
ref='PushNotificationServer'
- placeholder='E.g.: "https://push.mattermost.com"'
+ placeholder='E.g.: "https://push-test.mattermost.com"'
defaultValue={this.props.config.EmailSettings.PushNotificationServer}
onChange={this.handleChange}
disabled={!this.state.sendPushNotifications}
/>
- <p className='help-text'>{'Location of Mattermost push notification service you can set up behind your firewall using https://github.com/mattermost/push-proxy. For testing you can use https://push.mattermost.com, which connects to the sample Mattermost iOS app in the public Apple AppStore. Please do not use test service for production deployments.'}</p>
+ <p className='help-text'>{'Location of Mattermost push notification service you can set up behind your firewall using https://github.com/mattermost/push-proxy. For testing you can use https://push-test.mattermost.com, which connects to the sample Mattermost iOS app in the public Apple AppStore. Please do not use test service for production deployments.'}</p>
</div>
</div>
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 7dac39942..8b7485e5f 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -20,9 +20,14 @@ export default class ChannelInviteModal extends React.Component {
this.onListenerChange = this.onListenerChange.bind(this);
this.handleInvite = this.handleInvite.bind(this);
- this.state = this.getStateFromStores();
+ // the state gets populated when the modal is shown
+ this.state = {};
}
shouldComponentUpdate(nextProps, nextState) {
+ if (!this.props.show && !nextProps.show) {
+ return false;
+ }
+
if (!Utils.areObjectsEqual(this.props, nextProps)) {
return true;
}
@@ -34,13 +39,25 @@ export default class ChannelInviteModal extends React.Component {
return false;
}
getStateFromStores() {
- function getId(user) {
- return user.id;
+ const users = UserStore.getActiveOnlyProfiles();
+
+ if ($.isEmptyObject(users)) {
+ return {
+ loading: true
+ };
+ }
+
+ // make sure we have all members of this channel before rendering
+ const extraInfo = ChannelStore.getCurrentExtraInfo();
+ if (extraInfo.member_count !== extraInfo.members.length) {
+ AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
+
+ return {
+ loading: true
+ };
}
- var users = UserStore.getActiveOnlyProfiles();
- var memberIds = ChannelStore.getCurrentExtraInfo().members.map(getId);
- var loading = $.isEmptyObject(users);
+ const memberIds = extraInfo.members.map((user) => user.id);
var nonmembers = [];
for (var id in users) {
@@ -55,7 +72,7 @@ export default class ChannelInviteModal extends React.Component {
return {
nonmembers,
- loading
+ loading: false
};
}
onShow() {
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index d1b9df988..513a720e7 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import LoadingScreen from './loading_screen.jsx';
import MemberList from './member_list.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
@@ -21,9 +22,10 @@ export default class ChannelMembersModal extends React.Component {
this.onChange = this.onChange.bind(this);
this.handleRemove = this.handleRemove.bind(this);
- const state = this.getStateFromStores();
- state.showInviteModal = false;
- this.state = state;
+ // the rest of the state gets populated when the modal is shown
+ this.state = {
+ showInviteModal: false
+ };
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(this.props, nextProps)) {
@@ -37,8 +39,18 @@ export default class ChannelMembersModal extends React.Component {
return false;
}
getStateFromStores() {
+ const extraInfo = ChannelStore.getCurrentExtraInfo();
+
+ if (extraInfo.member_count !== extraInfo.members.length) {
+ AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
+
+ return {
+ loading: true
+ };
+ }
+
const users = UserStore.getActiveOnlyProfiles();
- const memberList = ChannelStore.getCurrentExtraInfo().members;
+ const memberList = extraInfo.members;
const nonmemberList = [];
for (const id in users) {
@@ -71,14 +83,14 @@ export default class ChannelMembersModal extends React.Component {
return {
nonmemberList,
- memberList
+ memberList,
+ loading: false
};
}
onShow() {
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
- this.onChange();
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -89,6 +101,8 @@ export default class ChannelMembersModal extends React.Component {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange);
+
+ this.onChange();
} else if (this.props.show && !nextProps.show) {
ChannelStore.removeExtraInfoChangeListener(this.onChange);
ChannelStore.removeChangeListener(this.onChange);
@@ -154,6 +168,21 @@ export default class ChannelMembersModal extends React.Component {
isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
}
+ let content;
+ if (this.state.loading) {
+ content = (<LoadingScreen />);
+ } else {
+ content = (
+ <div className='team-member-list'>
+ <MemberList
+ memberList={this.state.memberList}
+ isAdmin={isAdmin}
+ handleRemove={this.handleRemove}
+ />
+ </div>
+ );
+ }
+
return (
<div>
<Modal
@@ -178,13 +207,7 @@ export default class ChannelMembersModal extends React.Component {
ref='modalBody'
style={{maxHeight}}
>
- <div className='team-member-list'>
- <MemberList
- memberList={this.state.memberList}
- isAdmin={isAdmin}
- handleRemove={this.handleRemove}
- />
- </div>
+ {content}
</Modal.Body>
<Modal.Footer>
<button
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c10269680..eeb218bfe 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -266,7 +266,7 @@ export default class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
data-toggle='tooltip'
- title={'Download ' + filenameString}
+ title={'Download \"' + filenameString + '\"'}
className='post-image__name'
>
{trimmedFilename}
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index a0c930ffb..6337afabc 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -4,7 +4,7 @@
import * as client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
import ChannelStore from '../stores/channel_store.jsx';
-import * as utils from '../utils/utils.jsx';
+import * as Utils from '../utils/utils.jsx';
export default class FileUpload extends React.Component {
constructor(props) {
@@ -52,7 +52,7 @@ export default class FileUpload extends React.Component {
}
// generate a unique id that can be used by other components to refer back to this upload
- let clientId = utils.generateId();
+ let clientId = Utils.generateId();
// prepare data to be uploaded
var formData = new FormData();
@@ -121,14 +121,14 @@ export default class FileUpload extends React.Component {
enter(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
- if (utils.isFileTransfer(files)) {
+ if (Utils.isFileTransfer(files)) {
$('.center-file-overlay').removeClass('hidden');
}
},
leave(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
- if (utils.isFileTransfer(files)) {
+ if (Utils.isFileTransfer(files)) {
$('.center-file-overlay').addClass('hidden');
}
},
@@ -142,14 +142,14 @@ export default class FileUpload extends React.Component {
enter(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
- if (utils.isFileTransfer(files)) {
+ if (Utils.isFileTransfer(files)) {
$('.right-file-overlay').removeClass('hidden');
}
},
leave(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
- if (utils.isFileTransfer(files)) {
+ if (Utils.isFileTransfer(files)) {
$('.right-file-overlay').addClass('hidden');
}
},
@@ -205,7 +205,7 @@ export default class FileUpload extends React.Component {
var channelId = self.props.channelId || ChannelStore.getCurrentId();
// generate a unique id that can be used by other components to refer back to this file upload
- var clientId = utils.generateId();
+ var clientId = Utils.generateId();
var formData = new FormData();
formData.append('channel_id', channelId);
@@ -268,6 +268,18 @@ export default class FileUpload extends React.Component {
}
render() {
+ let multiple = true;
+ if (Utils.isMobileApp()) {
+ // iOS WebViews don't upload videos properly in multiple mode
+ multiple = false;
+ }
+
+ let accept = '';
+ if (Utils.isIosChrome()) {
+ // iOS Chrome can't upload videos at all
+ accept = 'image/*';
+ }
+
return (
<span
ref='input'
@@ -280,7 +292,8 @@ export default class FileUpload extends React.Component {
ref='fileInput'
type='file'
onChange={this.handleChange}
- multiple='true'
+ multiple={multiple}
+ accept={accept}
/>
</span>
);
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 56bc00a7e..7e1627555 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -8,6 +8,7 @@ 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 ChannelStore from '../stores/channel_store.jsx';
import TeamStore from '../stores/team_store.jsx';
import ConfirmModal from './confirm_modal.jsx';
@@ -304,6 +305,11 @@ export default class InviteMemberModal extends React.Component {
var content = null;
var sendButton = null;
+ var defaultChannelName = '';
+ if (ChannelStore.getByName(Constants.DEFAULT_CHANNEL)) {
+ defaultChannelName = ChannelStore.getByName(Constants.DEFAULT_CHANNEL).display_name;
+ }
+
if (this.state.emailEnabled && this.state.userCreationEnabled) {
content = (
<div>
@@ -312,10 +318,10 @@ export default class InviteMemberModal extends React.Component {
type='button'
className='btn btn-default'
onClick={this.addInviteFields}
- >Add another</button>
+ >{'Add another'}</button>
<br/>
<br/>
- <span>People invited automatically join Town Square channel.</span>
+ <span>{'People invited automatically join the '}<strong>{defaultChannelName}</strong>{' channel.'}</span>
</div>
);
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 21683bb01..26bd6adde 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -223,13 +223,13 @@ export default class PostInfo extends React.Component {
/>
</li>
<li className='col col__reply'>
- {comments}
<div
className='dropdown'
ref='dotMenu'
>
{dropdown}
</div>
+ {comments}
<Overlay
show={this.state.show}
target={() => ReactDOM.findDOMNode(this.refs.dotMenu)}
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index a28efbd04..7d8c7e265 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -24,6 +24,7 @@ export default class PostsView extends React.Component {
this.updateScrolling = this.updateScrolling.bind(this);
this.handleResize = this.handleResize.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
+ this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -339,6 +340,10 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
});
}
+ scrollToBottomAnimated() {
+ var postList = $(this.refs.postlist);
+ postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
+ }
componentDidMount() {
if (this.props.postList != null) {
this.updateScrolling();
@@ -458,7 +463,7 @@ export default class PostsView extends React.Component {
<ScrollToBottomArrows
isScrolling={this.state.isScrolling}
atBottom={this.wasAtBottom}
- onClick={this.scrollToBottom}
+ onClick={this.scrollToBottomAnimated}
/>
<div
ref='postlist'
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index f218270da..0ee89b9fa 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -168,7 +168,7 @@ export function getMoreChannels(force) {
}
}
-export function getChannelExtraInfo(id) {
+export function getChannelExtraInfo(id, memberLimit) {
let channelId;
if (id) {
channelId = id;
@@ -185,6 +185,7 @@ export function getChannelExtraInfo(id) {
client.getChannelExtraInfo(
channelId,
+ memberLimit,
(data, textStatus, xhr) => {
callTracker['getChannelExtraInfo_' + channelId] = 0;
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index e1c331aff..96d1ef720 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -824,10 +824,17 @@ export function getChannelCounts(success, error) {
});
}
-export function getChannelExtraInfo(id, success, error) {
+export function getChannelExtraInfo(id, memberLimit, success, error) {
+ let url = '/api/v1/channels/' + id + '/extra_info';
+
+ if (memberLimit) {
+ url += '/' + memberLimit;
+ }
+
$.ajax({
- url: '/api/v1/channels/' + id + '/extra_info',
+ url,
dataType: 'json',
+ contentType: 'application/json',
type: 'GET',
success,
error: function onError(xhr, status, err) {
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 95eca7c3a..2ddd0e5e3 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -74,6 +74,21 @@ export function isSafari() {
return false;
}
+export function isIosChrome() {
+ // https://developer.chrome.com/multidevice/user-agent
+ return navigator.userAgent.indexOf('CriOS') !== -1;
+}
+
+export function isMobileApp() {
+ const userAgent = navigator.userAgent;
+
+ // the mobile app has different user agents for the native api calls and the shim, so handle them both
+ const isApi = userAgent.indexOf('Mattermost') !== -1;
+ const isShim = userAgent.indexOf('iPhone') !== -1 && userAgent.indexOf('Safari') === -1 && userAgent.indexOf('Chrome') === -1;
+
+ return isApi || isShim;
+}
+
export function isInRole(roles, inRole) {
var parts = roles.split(' ');
for (var i = 0; i < parts.length; i++) {
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 937b08084..7b7c2d73a 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -286,8 +286,10 @@ body.ios {
z-index: 50;
@include opacity(0);
@include single-transition(all, 0.3s);
+ display: none;
&.scrolling {
+ display: block;
@include opacity(1);
}
}
@@ -417,12 +419,6 @@ body.ios {
background-color: beige;
}
- ul {
- margin: 0;
- padding: 0;
- }
-
-
p {
margin: 0;
line-height: 1.6em;
@@ -601,6 +597,7 @@ body.ios {
right: 0;
top: 30px;
width: 65px;
+ white-space: nowrap;
}
.permalink-popover {
@@ -634,8 +631,7 @@ body.ios {
.dropdown {
display: inline-block;
visibility: hidden;
- position: absolute;
- right: 0;
+ margin-right: 5px;
top: -1px;
.dropdown-menu {
@@ -671,20 +667,17 @@ body.ios {
@include legacy-pie-clearfix;
width: calc(100% - 75px);
- img {
- max-height: 400px;
+ p {
+ margin: 0 0 0.4em;
}
- ul {
- margin-bottom: 0.6em;
- padding: 5px 0 0 20px;
- }
-
- ul + p {
- margin-top: 1em;
+ img {
+ max-height: 400px;
}
ul, ol {
+ margin-bottom: 0.4em;
+
p {
margin-bottom: 0;
}
diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss
index d820447f5..bd3d60622 100644
--- a/web/sass-files/sass/partials/_post_right.scss
+++ b/web/sass-files/sass/partials/_post_right.scss
@@ -25,6 +25,7 @@
.col__reply {
top: 0;
+ text-align: right;
}
}
diff --git a/web/templates/head.html b/web/templates/head.html
index 70c94e8ff..08d8726ea 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -98,7 +98,7 @@
});
if (window.mm_config.EnableDeveloper === 'true') {
- window.ErrorStore.storeLastError('DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').');
+ window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'});
window.ErrorStore.emitChange();
}
}
diff --git a/web/web.go b/web/web.go
index bf1208adc..634a9d851 100644
--- a/web/web.go
+++ b/web/web.go
@@ -4,8 +4,8 @@
package web
import (
- l4g "code.google.com/p/log4go"
"fmt"
+ l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
@@ -70,6 +70,8 @@ func InitWeb() {
mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET")
mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET")
mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET")
+ mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8)
+ mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8)
mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET")
mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET")
@@ -711,7 +713,7 @@ func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
- uri := c.GetSiteURL() + "/" + service + "/complete"
+ uri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8)
if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
c.Err = err