diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/admin_console/email_settings.jsx | 4 | ||||
-rw-r--r-- | web/react/components/channel_invite_modal.jsx | 31 | ||||
-rw-r--r-- | web/react/components/channel_members_modal.jsx | 49 | ||||
-rw-r--r-- | web/react/components/file_attachment.jsx | 2 | ||||
-rw-r--r-- | web/react/components/file_upload.jsx | 29 | ||||
-rw-r--r-- | web/react/components/invite_member_modal.jsx | 10 | ||||
-rw-r--r-- | web/react/components/post_info.jsx | 2 | ||||
-rw-r--r-- | web/react/components/posts_view.jsx | 7 | ||||
-rw-r--r-- | web/react/utils/async_client.jsx | 3 | ||||
-rw-r--r-- | web/react/utils/client.jsx | 11 | ||||
-rw-r--r-- | web/react/utils/utils.jsx | 15 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post.scss | 27 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post_right.scss | 1 | ||||
-rw-r--r-- | web/templates/head.html | 2 | ||||
-rw-r--r-- | web/web.go | 6 |
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 |