diff options
40 files changed, 503 insertions, 178 deletions
diff --git a/api/channel.go b/api/channel.go index 99640e71a..659121bf0 100644 --- a/api/channel.go +++ b/api/channel.go @@ -268,19 +268,51 @@ func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) { if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelHeader") { return } - + oldChannelHeader := channel.Header channel.Header = channelHeader if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil { c.Err = ucresult.Err return } else { + PostUpdateChannelHeaderMessageAndForget(c, channel.Id, oldChannelHeader, channelHeader) c.LogAudit("name=" + channel.Name) w.Write([]byte(channel.ToJson())) } } } +func PostUpdateChannelHeaderMessageAndForget(c *Context, channelId string, oldChannelHeader, newChannelHeader string) { + go func() { + uc := Srv.Store.User().Get(c.Session.UserId) + + if uresult := <-uc; uresult.Err != nil { + l4g.Error("Failed to retrieve user while trying to save update channel header message %v", uresult.Err) + return + } else { + user := uresult.Data.(*model.User) + + var message string + if oldChannelHeader == "" { + message = fmt.Sprintf("%s updated the channel header to: %s", user.Username, newChannelHeader) + } else if newChannelHeader == "" { + message = fmt.Sprintf("%s removed the channel header (was: %s)", user.Username, oldChannelHeader) + } else { + message = fmt.Sprintf("%s updated the channel header from: %s to: %s", user.Username, oldChannelHeader, newChannelHeader) + } + + post := &model.Post{ + ChannelId: channelId, + Message: message, + Type: model.POST_HEADER_CHANGE, + } + if _, err := CreatePost(c, post, false); err != nil { + l4g.Error("Failed to post join/leave message %v", err) + } + } + }() +} + func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) channelId := props["channel_id"] @@ -421,7 +453,7 @@ func JoinChannel(c *Context, channelId string, role string) { c.Err = err return } - PostUserAddRemoveMessageAndForget(c, channel.Id, fmt.Sprintf(`User %v has joined this channel.`, user.Username)) + PostUserAddRemoveMessageAndForget(c, channel.Id, fmt.Sprintf(`%v has joined the channel.`, user.Username)) } else { c.Err = model.NewAppError("join", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden diff --git a/api/post.go b/api/post.go index 40c5efe8c..e1adc1d98 100644 --- a/api/post.go +++ b/api/post.go @@ -561,11 +561,12 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, msg := model.PushNotification{} msg.Platform = model.PUSH_NOTIFY_APPLE - msg.Message = subjectPage.Render() msg.Badge = 1 msg.DeviceId = strings.TrimPrefix(session.DeviceId, "apple:") msg.ServerId = utils.CfgDiagnosticId + msg.Message = profileMap[id].FirstName + " mentioned you in " + channel.DisplayName + httpClient := http.Client{} request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson())) diff --git a/doc/developer/tests/test-links.md b/doc/developer/tests/test-links.md index 011542c82..91e3e9403 100644 --- a/doc/developer/tests/test-links.md +++ b/doc/developer/tests/test-links.md @@ -39,6 +39,8 @@ http:// @example.com ./make-compiled-client.sh test.:test +https://<your-mattermost-url>/signup/gitlab +https://your-mattermost-url>/signup/gitlab #### Only the links within these sentences should auto-link: diff --git a/doc/install/Command-Line-Tools.md b/doc/install/Command-Line-Tools.md new file mode 100644 index 000000000..ff6f110fd --- /dev/null +++ b/doc/install/Command-Line-Tools.md @@ -0,0 +1,81 @@ +# Command Line Tools + +From the directory where the Mattermost platform is installed a `platform` command is available for configuring the system, including: + +- Creating teams +- Creating users +- Assigning roles to users +- Reseting user passwords +- Permanently deleting users (use cautiously - database backup recommended before use) +- Permanently deleting teams (use cautiously - database backup recommended before use) + +Typing `platform -help` brings up the below documentation on usage. + +``` +Mattermost commands to help configure the system + +NAME: + platform -- platform configuation tool + +USAGE: + platform [options] + +FLAGS: + -config="config.json" Path to the config file + + -email="user@example.com" Email address used in other commands + + -password="mypassword" Password used in other commands + + -team_name="name" The team name used in other commands + + -role="admin" The role used in other commands + valid values are + "" - The empty role is basic user + permissions + "admin" - Represents a team admin and + is used to help administer one team. + "system_admin" - Represents a system + admin who has access to all teams + and configuration settings. +COMMANDS: + -create_team Creates a team. It requires the -team_name + and -email flag to create a team. + Example: + platform -create_team -team_name="name" -email="user@example.com" + + -create_user Creates a user. It requires the -team_name, + -email and -password flag to create a user. + Example: + platform -create_user -team_name="name" -email="user@example.com" -password="mypassword" + + -assign_role Assigns role to a user. It requires the -role, + -email and -team_name flag. You may need to log out + of your current sessions for the new role to be + applied. + Example: + platform -assign_role -team_name="name" -email="user@example.com" -role="admin" + + -reset_password Resets the password for a user. It requires the + -team_name, -email and -password flag. + Example: + platform -reset_password -team_name="name" -email="user@example.com" -password="newpassword" + + -permanent_delete_user Permanently deletes a user and all related information + including posts from the database. It requires the + -team_name, and -email flag. You may need to restart the + server to invalidate the cache + Example: + platform -permanent_delete_user -team_name="name" -email="user@example.com" + + -permanent_delete_team Permanently deletes a team and all users along with + all related information including posts from the database. + It requires the -team_name flag. You may need to restart + the server to invalidate the cache. + Example: + platform -permanent_delete_team -team_name="name" + + -version Display the current of the Mattermost platform + + -help Displays this help page` +``` diff --git a/mattermost.go b/mattermost.go index eaab1de88..da50a26c3 100644 --- a/mattermost.go +++ b/mattermost.go @@ -535,12 +535,14 @@ func flushLogAndExit(code int) { } var usage = `Mattermost commands to help configure the system -Usage: +NAME: + platform -- platform configuation tool + +USAGE: platform [options] - - -version Display the current version - + +FLAGS: -config="config.json" Path to the config file -email="user@example.com" Email address used in other commands @@ -557,10 +559,8 @@ Usage: is used to help administer one team. "system_admin" - Represents a system admin who has access to all teams - and configuration settings. This - role can only be created on the - team named "admin" - + and configuration settings. +COMMANDS: -create_team Creates a team. It requires the -team_name and -email flag to create a team. Example: @@ -572,7 +572,7 @@ Usage: platform -create_user -team_name="name" -email="user@example.com" -password="mypassword" -assign_role Assigns role to a user. It requires the -role, - -email and -team_name flag. You may need to logout + -email and -team_name flag. You may need to log out of your current sessions for the new role to be applied. Example: @@ -584,18 +584,19 @@ Usage: platform -reset_password -team_name="name" -email="user@example.com" -password="newpassword" -permanent_delete_user Permanently deletes a user and all related information - include posts from the database. It requires the + including posts from the database. It requires the -team_name, and -email flag. You may need to restart the - server to invlidate the cache + server to invalidate the cache Example: platform -permanent_delete_user -team_name="name" -email="user@example.com" -permanent_delete_team Permanently deletes a team and all users along with - all related information including posts from the database. + all related information including posts from the database. It requires the -team_name flag. You may need to restart the server to invalidate the cache. Example: platform -permanent_delete_team -team_name="name" + -version Display the current of the Mattermost platform -` + -help Displays this help page` diff --git a/model/post.go b/model/post.go index e66009dd6..5c86ce70d 100644 --- a/model/post.go +++ b/model/post.go @@ -10,9 +10,11 @@ import ( ) const ( - POST_DEFAULT = "" - POST_SLACK_ATTACHMENT = "slack_attachment" - POST_JOIN_LEAVE = "join_leave" + POST_SYSTEM_MESSAGE_PREFIX = "system_" + POST_DEFAULT = "" + POST_SLACK_ATTACHMENT = "slack_attachment" + POST_JOIN_LEAVE = "system_join_leave" + POST_HEADER_CHANGE = "system_header_change" ) type Post struct { @@ -104,7 +106,7 @@ func (o *Post) IsValid() *AppError { } // should be removed once more message types are supported - if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_SLACK_ATTACHMENT) { + if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE) { return NewAppError("Post.IsValid", "Invalid type", "id="+o.Type) } @@ -159,3 +161,7 @@ func (o *Post) AddProp(key string, value interface{}) { func (o *Post) PreExport() { } + +func (o *Post) IsSystemMessage() bool { + return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX +} diff --git a/model/post_test.go b/model/post_test.go index f498c83e6..cbd323fab 100644 --- a/model/post_test.go +++ b/model/post_test.go @@ -98,3 +98,18 @@ func TestPostPreSave(t *testing.T) { o.Etag() } + +func TestPostIsSystemMessage(t *testing.T) { + post1 := Post{Message: "test_1"} + post1.PreSave() + + if post1.IsSystemMessage() { + t.Fatalf("TestPostIsSystemMessage failed, expected post1.IsSystemMessage() to be false") + } + + post2 := Post{Message: "test_2", Type: POST_JOIN_LEAVE} + post2.PreSave() + if !post2.IsSystemMessage() { + t.Fatalf("TestPostIsSystemMessage failed, expected post2.IsSystemMessage() to be true") + } +} diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 9db5806c5..035309e21 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -38,7 +38,8 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore { } func (s SqlPostStore) UpgradeSchemaIfNeeded() { - s.RemoveColumnIfExists("Posts", "ImgCount") // remove after 1.3 release + s.RemoveColumnIfExists("Posts", "ImgCount") // remove after 1.3 release + s.GetMaster().Exec(`UPDATE Preferences SET Type = :NewType WHERE Type = :CurrentType`, map[string]string{"NewType": model.POST_JOIN_LEAVE, "CurrentType": "join_leave"}) // remove after 1.3 release } func (s SqlPostStore) CreateIndexesIfNotExists() { diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 165d32339..deef92a54 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -32,7 +32,7 @@ export default class AccessHistoryModal extends React.Component { onShow() { AsyncClient.getAudits(); - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50); if ($(window).width() > 768) { $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); } diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 869d648d2..200f4d724 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -51,7 +51,7 @@ export default class ActivityLogModal extends React.Component { onShow() { AsyncClient.getSessions(); - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50); if ($(window).width() > 768) { $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); } diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx index 848e8952e..a1043431d 100644 --- a/web/react/components/center_panel.jsx +++ b/web/react/components/center_panel.jsx @@ -71,7 +71,8 @@ export default class CenterPanel extends React.Component { href='' onClick={handleClick} > - {'You are viewing the Archives. Click here to jump to recent messages.'} + {'You are viewing the Archives. Click here to jump to recent messages. '} + {<i className='fa fa-arrow-down'></i>} </a> </div> ); diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 76f52faa9..25a915e22 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -143,7 +143,7 @@ export default class InviteMemberModal extends React.Component { componentDidUpdate(prevProps, prevState) { if (!prevState.show && this.state.show) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50); if ($(window).width() > 768) { $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); } diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 9116dc8f1..cf40af6ae 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -166,7 +166,7 @@ export default class MoreDirectChannels extends React.Component { componentDidUpdate(prevProps) { if (!prevProps.show && this.props.show) { - $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300); + $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 50); if ($(window).width() > 768) { $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar(); } diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index b32656bfc..695d7daef 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -152,7 +152,7 @@ export default class Post extends React.Component { } let currentUserCss = ''; - if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) { + if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook && !utils.isSystemMessage(post)) { currentUserCss = 'current--user'; } @@ -173,6 +173,11 @@ export default class Post extends React.Component { shouldHighlightClass = 'post--highlight'; } + let systemMessageClass = ''; + if (utils.isSystemMessage(post)) { + systemMessageClass = 'post--system'; + } + let profilePic = null; if (!this.props.hideProfilePic) { let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); @@ -180,6 +185,8 @@ export default class Post extends React.Component { if (post.props.override_icon_url) { src = post.props.override_icon_url; } + } else if (utils.isSystemMessage(post)) { + src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE; } profilePic = ( @@ -195,7 +202,7 @@ export default class Post extends React.Component { <div> <div id={'post_' + post.id} - className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass} + className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass} > <div className='post__content'> <div className='post__img'>{profilePic}</div> diff --git a/web/react/components/post_attachment_oembed.jsx b/web/react/components/post_attachment_oembed.jsx index f544dbc88..13c32f744 100644 --- a/web/react/components/post_attachment_oembed.jsx +++ b/web/react/components/post_attachment_oembed.jsx @@ -20,8 +20,11 @@ export default class PostAttachmentOEmbed extends React.Component { fetchData(link) { if (!this.isLoading) { this.isLoading = true; + let url = 'https://noembed.com/embed?nowrap=on'; + url += '&url=' + encodeURIComponent(link); + url += '&maxheight=' + this.props.provider.height; return $.ajax({ - url: 'https://noembed.com/embed?nowrap=on&url=' + encodeURIComponent(link), + url, dataType: 'jsonp', success: (result) => { this.isLoading = false; @@ -39,8 +42,18 @@ export default class PostAttachmentOEmbed extends React.Component { } render() { + let data = {}; + let content; if ($.isEmptyObject(this.state.data)) { - return <div></div>; + content = <div style={{height: this.props.provider.height}}/>; + } else { + data = this.state.data; + content = ( + <div + style={{height: this.props.provider.height}} + dangerouslySetInnerHTML={{__html: data.html}} + /> + ); } return ( @@ -57,18 +70,17 @@ export default class PostAttachmentOEmbed extends React.Component { > <a className='attachment__title-link' - href={this.state.data.url} + href={data.url} target='_blank' > - {this.state.data.title} + {data.title} </a> </h1> - <div> - <div className={'attachment__body attachment__body--no_thumb'}> - <div - dangerouslySetInnerHTML={{__html: this.state.data.html}} - > - </div> + <div > + <div + className={'attachment__body attachment__body--no_thumb'} + > + {content} </div> </div> </div> @@ -79,5 +91,6 @@ export default class PostAttachmentOEmbed extends React.Component { } PostAttachmentOEmbed.propTypes = { - link: React.PropTypes.string.isRequired + link: React.PropTypes.string.isRequired, + provider: React.PropTypes.object.isRequired }; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 27f7ad2de..296b9e7d7 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -6,6 +6,7 @@ import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; import * as Emoji from '../utils/emoticons.jsx'; import Constants from '../utils/constants.jsx'; +const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES; import * as TextFormatting from '../utils/text_formatting.jsx'; import twemoji from 'twemoji'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; @@ -109,11 +110,14 @@ export default class PostBody extends React.Component { const trimmedLink = link.trim(); - if (this.checkForOembedContent(trimmedLink)) { - post.props.oEmbedLink = trimmedLink; - post.type = 'oEmbed'; - this.setState({post}); - return ''; + if (Utils.isFeatureEnabled(PreReleaseFeatures.EMBED_PREVIEW)) { + const provider = this.getOembedProvider(trimmedLink); + if (provider != null) { + post.props.oEmbedLink = trimmedLink; + post.type = 'oEmbed'; + this.setState({post, provider}); + return ''; + } } const embed = this.createYoutubeEmbed(link); @@ -133,15 +137,15 @@ export default class PostBody extends React.Component { return null; } - checkForOembedContent(link) { + getOembedProvider(link) { for (let i = 0; i < providers.length; i++) { for (let j = 0; j < providers[i].patterns.length; j++) { if (link.match(providers[i].patterns[j])) { - return true; + return providers[i]; } } } - return false; + return null; } loadImg(src) { @@ -399,6 +403,7 @@ export default class PostBody extends React.Component { </div> <PostBodyAdditionalContent post={this.state.post} + provider={this.state.provider} /> {fileAttachmentHolder} {this.embed} diff --git a/web/react/components/post_body_additional_content.jsx b/web/react/components/post_body_additional_content.jsx index e19bf51eb..7e6f3f037 100644 --- a/web/react/components/post_body_additional_content.jsx +++ b/web/react/components/post_body_additional_content.jsx @@ -32,6 +32,7 @@ export default class PostBodyAdditionalContent extends React.Component { return ( <PostAttachmentOEmbed key={'post_body_additional_content' + this.props.post.id} + provider={this.props.provider} link={link} /> ); @@ -68,5 +69,6 @@ export default class PostBodyAdditionalContent extends React.Component { } PostBodyAdditionalContent.propTypes = { - post: React.PropTypes.object.isRequired -};
\ No newline at end of file + post: React.PropTypes.object.isRequired, + provider: React.PropTypes.object +}; diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index ffc32f82c..76b3a64be 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -3,6 +3,9 @@ import UserProfile from './user_profile.jsx'; import PostInfo from './post_info.jsx'; +import * as Utils from '../utils/utils.jsx'; + +import Constants from '../utils/constants.jsx'; export default class PostHeader extends React.Component { constructor(props) { @@ -27,6 +30,15 @@ export default class PostHeader extends React.Component { } botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>; + } else if (Utils.isSystemMessage(post)) { + userProfile = ( + <UserProfile + userId={''} + overwriteName={''} + overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE} + disablePopover={true} + /> + ); } return ( diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index cedb2b59b..0b8b07d8c 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -9,14 +9,15 @@ import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import Constants from '../utils/constants.jsx'; -const OverlayTrigger = ReactBootstrap.OverlayTrigger; +const Overlay = ReactBootstrap.Overlay; const Popover = ReactBootstrap.Popover; export default class PostInfo extends React.Component { constructor(props) { super(props); this.state = { - copiedLink: false + copiedLink: false, + show: false }; this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this); @@ -41,30 +42,37 @@ export default class PostInfo extends React.Component { dataComments = this.props.commentCount; } - if (isOwner) { + if (this.props.allowReply === 'true') { dropdownContents.push( <li - key='editPost' + key='replyLink' role='presentation' > <a + className='link__reply theme' href='#' - role='menuitem' - data-toggle='modal' - data-target='#edit_post' - data-refocusid='#post_textbox' - data-title={type} - data-message={post.message} - data-postid={post.id} - data-channelid={post.channel_id} - data-comments={dataComments} + onClick={this.props.handleCommentClick} > - {'Edit'} + {'Reply'} </a> </li> ); } + dropdownContents.push( + <li + key='copyLink' + role='presentation' + > + <a + href='#' + onClick={(e) => this.setState({target: e.target, show: !this.state.show})} + > + {'Permalink'} + </a> + </li> + ); + if (isOwner || isAdmin) { dropdownContents.push( <li @@ -82,18 +90,25 @@ export default class PostInfo extends React.Component { ); } - if (this.props.allowReply === 'true') { + if (isOwner) { dropdownContents.push( <li - key='replyLink' + key='editPost' role='presentation' > <a - className='link__reply theme' href='#' - onClick={this.props.handleCommentClick} + role='menuitem' + data-toggle='modal' + data-target='#edit_post' + data-refocusid='#post_textbox' + data-title={type} + data-message={post.message} + data-postid={post.id} + data-channelid={post.channel_id} + data-comments={dataComments} > - {'Reply'} + {'Edit'} </a> </li> ); @@ -121,6 +136,7 @@ export default class PostInfo extends React.Component { </div> ); } + handlePermalinkCopy() { const textBox = $(ReactDOM.findDOMNode(this.refs.permalinkbox)); textBox.select(); @@ -128,7 +144,7 @@ export default class PostInfo extends React.Component { try { const successful = document.execCommand('copy'); if (successful) { - this.setState({copiedLink: true}); + this.setState({copiedLink: true, show: false}); } else { this.setState({copiedLink: false}); } @@ -197,6 +213,8 @@ export default class PostInfo extends React.Component { </Popover> ); + const containerPadding = 20; + return ( <ul className='post__header post__header--info'> <li className='col'> @@ -206,18 +224,23 @@ export default class PostInfo extends React.Component { </li> <li className='col col__reply'> {comments} - <OverlayTrigger - trigger='click' - placement='left' - rootClose={true} - overlay={permalinkOverlay} + <div + className='dropdown' + ref='dotMenu' > - <i className={'permalink-icon fa fa-link ' + showCommentClass}/> - </OverlayTrigger> - - <div className='dropdown'> {dropdown} </div> + <Overlay + show={this.state.show} + target={() => ReactDOM.findDOMNode(this.refs.dotMenu)} + onHide={() => this.setState({show: false})} + placement='left' + container={this} + containerPadding={containerPadding} + rootClose={true} + > + {permalinkOverlay} + </Overlay> </li> </ul> ); diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index d0eee5a23..740ce04f6 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -87,6 +87,7 @@ export default class PostsView extends React.Component { const post = posts[order[i]]; const parentPost = posts[post.parent_id]; const prevPost = posts[order[i + 1]]; + const postUserId = Utils.isSystemMessage(post) ? '' : post.user_id; // If the post is a comment whose parent has been deleted, don't add it to the list. if (parentPost && parentPost.state === Constants.POST_DELETED) { @@ -102,6 +103,7 @@ export default class PostsView extends React.Component { const prevPostIsComment = Utils.isComment(prevPost); const postFromWebhook = Boolean(post.props && post.props.from_webhook); const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook); + const prevPostUserId = Utils.isSystemMessage(prevPost) ? '' : prevPostUserId; let prevWebhookName = ''; if (prevPost.props && prevPost.props.override_username) { prevWebhookName = prevPost.props.override_username; @@ -116,7 +118,7 @@ export default class PostsView extends React.Component { // the previous post was made within 5 minutes of the current post, // the previous post and current post are both from webhooks or both not, // the previous post and current post have the same webhook usernames - if (prevPost.user_id === post.user_id && + if (prevPostUserId === postUserId && post.create_at - prevPost.create_at <= 1000 * 60 * 5 && postFromWebhook === prevPostFromWebhook && prevWebhookName === curWebhookName) { @@ -144,7 +146,7 @@ export default class PostsView extends React.Component { // the current post is not a comment, // the previous post and current post are both from webhooks or both not, // the previous post and current post have the same webhook usernames - if (prevPost.user_id === post.user_id && + if (prevPostUserId === postUserId && !prevPostIsComment && !postIsComment && postFromWebhook === prevPostFromWebhook && @@ -191,7 +193,7 @@ export default class PostsView extends React.Component { ); } - if (post.user_id !== userId && + if (postUserId !== userId && this.props.messageSeparatorTime !== 0 && post.create_at > this.props.messageSeparatorTime && !renderedLastViewed) { diff --git a/web/react/components/providers.json b/web/react/components/providers.json index 5e4cbd656..33e377a39 100644 --- a/web/react/components/providers.json +++ b/web/react/components/providers.json @@ -3,265 +3,308 @@ "patterns": [ "http://(?:www\\.)?xkcd\\.com/\\d+/?" ], - "name": "XKCD" + "name": "XKCD", + "height": 110 }, { "patterns": [ "https?://soundcloud.com/.*/.*" ], - "name": "SoundCloud" + "name": "SoundCloud", + "height": 110 }, { "patterns": [ "https?://(?:www\\.)?flickr\\.com/.*", "https?://flic\\.kr/p/[a-zA-Z0-9]+" ], - "name": "Flickr" + "name": "Flickr", + "height": 110 }, { "patterns": [ "http://www\\.ted\\.com/talks/.+\\.html" ], - "name": "TED" + "name": "TED", + "height": 110 }, { "patterns": [ "http://(?:www\\.)?theverge\\.com/\\d{4}/\\d{1,2}/\\d{1,2}/\\d+/[^/]+/?$" ], - "name": "The Verge" + "name": "The Verge", + "height": 110 }, { "patterns": [ "http://.*\\.viddler\\.com/.*" ], - "name": "Viddler" + "name": "Viddler", + "height": 110 }, { "patterns": [ "https?://(?:www\\.)?avclub\\.com/article/[^/]+/?$" ], - "name": "The AV Club" + "name": "The AV Club", + "height": 110 }, { "patterns": [ "https?://(?:www\\.)?wired\\.com/([^/]+/)?\\d+/\\d+/[^/]+/?$" ], - "name": "Wired" + "name": "Wired", + "height": 110 }, { "patterns": [ "http://www\\.theonion\\.com/articles/[^/]+/?" ], - "name": "The Onion" + "name": "The Onion", + "height": 110 }, { "patterns": [ "http://yfrog\\.com/[0-9a-zA-Z]+/?$" ], - "name": "YFrog" + "name": "YFrog", + "height": 110 }, { "patterns": [ "http://www\\.duffelblog\\.com/\\d{4}/\\d{1,2}/[^/]+/?$" ], - "name": "The Duffel Blog" + "name": "The Duffel Blog", + "height": 110 }, { "patterns": [ "http://www\\.clickhole\\.com/article/[^/]+/?" ], - "name": "Clickhole" + "name": "Clickhole", + "height": 110 }, { "patterns": [ "https?://(?:www.)?skitch.com/([^/]+)/[^/]+/.+", "http://skit.ch/[^/]+" ], - "name": "Skitch" + "name": "Skitch", + "height": 110 }, { "patterns": [ "https?://(alpha|posts|photos)\\.app\\.net/.*" ], - "name": "ADN" + "name": "ADN", + "height": 110 }, { "patterns": [ "https?://gist\\.github\\.com/(?:[-0-9a-zA-Z]+/)?([0-9a-fA-f]+)" ], - "name": "Gist" + "name": "Gist", + "height": 110 }, { "patterns": [ "https?://www\\.(dropbox\\.com/s/.+\\.(?:jpg|png|gif))", "https?://db\\.tt/[a-zA-Z0-9]+" ], - "name": "Dropbox" + "name": "Dropbox", + "height": 110 }, { "patterns": [ "https?://[^\\.]+\\.wikipedia\\.org/wiki/(?!Talk:)[^#]+(?:#(.+))?" ], - "name": "Wikipedia" + "name": "Wikipedia", + "height": 110 }, { "patterns": [ "http://www.traileraddict.com/trailer/[^/]+/trailer" ], - "name": "TrailerAddict" + "name": "TrailerAddict", + "height": 110 }, { "patterns": [ "http://lockerz\\.com/[sd]/\\d+" ], - "name": "Lockerz" + "name": "Lockerz", + "height": 110 }, { "patterns": [ "http://gifuk\\.com/s/[0-9a-f]{16}" ], - "name": "GIFUK" + "name": "GIFUK", + "height": 110 }, { "patterns": [ "http://trailers\\.apple\\.com/trailers/[^/]+/[^/]+" ], - "name": "iTunes Movie Trailers" + "name": "iTunes Movie Trailers", + "height": 110 }, { "patterns": [ "http://gfycat\\.com/([a-zA-Z]+)" ], - "name": "Gfycat" + "name": "Gfycat", + "height": 110 }, { "patterns": [ "http://bash\\.org/\\?(\\d+)" ], - "name": "Bash.org" + "name": "Bash.org", + "height": 110 }, { "patterns": [ "http://arstechnica\\.com/[^/]+/\\d+/\\d+/[^/]+/?$" ], - "name": "Ars Technica" + "name": "Ars Technica", + "height": 110 }, { "patterns": [ "http://imgur\\.com/gallery/[0-9a-zA-Z]+" ], - "name": "Imgur" + "name": "Imgur", + "height": 110 }, { "patterns": [ "http://www\\.asciiartfarts\\.com/[0-9]+\\.html" ], - "name": "ASCII Art Farts" + "name": "ASCII Art Farts", + "height": 110 }, { "patterns": [ "http://www\\.monoprice\\.com/products/product\\.asp\\?.*p_id=\\d+" ], - "name": "Monoprice" + "name": "Monoprice", + "height": 110 }, { "patterns": [ "http://boingboing\\.net/\\d{4}/\\d{2}/\\d{2}/[^/]+\\.html" ], - "name": "Boing Boing" + "name": "Boing Boing", + "height": 110 }, { "patterns": [ "https?://github\\.com/([^/]+)/([^/]+)/commit/(.+)", "http://git\\.io/[_0-9a-zA-Z]+" ], - "name": "Github Commit" + "name": "Github Commit", + "height": 110 }, { "patterns": [ "https?://open\\.spotify\\.com/(track|album)/([0-9a-zA-Z]{22})" ], - "name": "Spotify" + "name": "Spotify", + "height": 110 }, { "patterns": [ "https?://path\\.com/p/([0-9a-zA-Z]+)$" ], - "name": "Path" + "name": "Path", + "height": 110 }, { "patterns": [ "http://www.funnyordie.com/videos/[^/]+/.+" ], - "name": "Funny or Die" + "name": "Funny or Die", + "height": 110 }, { "patterns": [ "http://(?:www\\.)?twitpic\\.com/([^/]+)" ], - "name": "Twitpic" + "name": "Twitpic", + "height": 110 }, { "patterns": [ "https?://www\\.giantbomb\\.com/videos/[^/]+/\\d+-\\d+/?" ], - "name": "GiantBomb" + "name": "GiantBomb", + "height": 110 }, { "patterns": [ "http://(?:www\\.)?beeradvocate\\.com/beer/profile/\\d+/\\d+" ], - "name": "Beer Advocate" + "name": "Beer Advocate", + "height": 110 }, { "patterns": [ "http://(?:www\\.)?imdb.com/title/(tt\\d+)" ], - "name": "IMDB" + "name": "IMDB", + "height": 110 }, { "patterns": [ "http://cl\\.ly/(?:image/)?[0-9a-zA-Z]+/?$" ], - "name": "CloudApp" + "name": "CloudApp", + "height": 110 }, { "patterns": [ "http://clyp\\.it/.*" ], - "name": "Clyp" + "name": "Clyp", + "height": 110 }, { "patterns": [ "http://www\\.hulu\\.com/watch/.*" ], - "name": "Hulu" + "name": "Hulu", + "height": 110 }, { "patterns": [ "https?://(?:www|mobile\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/?$", "https?://t\\.co/[a-zA-Z0-9]+" ], - "name": "Twitter" + "name": "Twitter", + "height": 110 }, { "patterns": [ "https?://(?:www\\.)?vimeo\\.com/.+" ], - "name": "Vimeo" + "name": "Vimeo", + "height": 110 }, { "patterns": [ "http://www\\.amazon\\.com/(?:.+/)?[gd]p/(?:product/)?(?:tags-on-product/)?([a-zA-Z0-9]+)", "http://amzn\\.com/([^/]+)" ], - "name": "Amazon" + "name": "Amazon", + "height": 110 }, { "patterns": [ "http://qik\\.com/video/.*" ], - "name": "Qik" + "name": "Qik", + "height": 110 }, { "patterns": [ @@ -269,56 +312,65 @@ "http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/track/[^/]+/?", "http://www\\.rdio\\.com/people/[^/]+/playlists/\\d+/[^/]+" ], - "name": "Rdio" + "name": "Rdio", + "height": 110 }, { "patterns": [ "http://www\\.slideshare\\.net/.*/.*" ], - "name": "SlideShare" + "name": "SlideShare", + "height": 110 }, { "patterns": [ "http://imgur\\.com/([0-9a-zA-Z]+)$" ], - "name": "Imgur" + "name": "Imgur", + "height": 110 }, { "patterns": [ "https?://instagr(?:\\.am|am\\.com)/p/.+" ], - "name": "Instagram" + "name": "Instagram", + "height": 110 }, { "patterns": [ "http://www\\.twitlonger\\.com/show/[a-zA-Z0-9]+", "http://tl\\.gd/[^/]+" ], - "name": "Twitlonger" + "name": "Twitlonger", + "height": 110 }, { "patterns": [ "https?://vine.co/v/[a-zA-Z0-9]+" ], - "name": "Vine" + "name": "Vine", + "height": 110 }, { "patterns": [ "http://www\\.urbandictionary\\.com/define\\.php\\?term=.+" ], - "name": "Urban Dictionary" + "name": "Urban Dictionary", + "height": 110 }, { "patterns": [ "http://picplz\\.com/user/[^/]+/pic/[^/]+" ], - "name": "Picplz" + "name": "Picplz", + "height": 110 }, { "patterns": [ "https?://(?:www\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/photo/\\d+(?:/large|/)?$", "https?://pic\\.twitter\\.com/.+" ], - "name": "Twitter" + "name": "Twitter", + "height": 110 } -]
\ No newline at end of file +] diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 0dd969ad0..0a37a6803 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -12,6 +12,8 @@ import twemoji from 'twemoji'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import Constants from '../utils/constants.jsx'; + export default class RhsRootPost extends React.Component { constructor(props) { super(props); @@ -58,6 +60,11 @@ export default class RhsRootPost extends React.Component { currentUserCss = 'current--user'; } + var systemMessageClass = ''; + if (utils.isSystemMessage(post)) { + systemMessageClass = 'post--system'; + } + var channelName; if (channel) { if (channel.type === 'D') { @@ -156,6 +163,15 @@ export default class RhsRootPost extends React.Component { } botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>; + } else if (utils.isSystemMessage(post)) { + userProfile = ( + <UserProfile + userId={''} + overwriteName={''} + overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE} + disablePopover={true} + /> + ); } let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); @@ -163,6 +179,8 @@ export default class RhsRootPost extends React.Component { if (post.props.override_icon_url) { src = post.props.override_icon_url; } + } else if (utils.isSystemMessage(post)) { + src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE; } const profilePic = ( @@ -175,7 +193,7 @@ export default class RhsRootPost extends React.Component { ); return ( - <div className={'post post--root ' + currentUserCss}> + <div className={'post post--root ' + currentUserCss + ' ' + systemMessageClass}> <div className='post-right-channel__name'>{channelName}</div> <div className='post__content'> <div className='post__img'> diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 6e17cfe32..1d4983026 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -65,7 +65,7 @@ export default class SearchResultsItem extends React.Component { className='search-item__jump' onClick={this.handleClick} > - {'[Jump]'} + {<i className='fa fa-mail-reply'></i>} </a> </li> </ul> diff --git a/web/react/components/suggestion/suggestion_box.jsx b/web/react/components/suggestion/suggestion_box.jsx index 4cfb38f8e..ad8ad1e9e 100644 --- a/web/react/components/suggestion/suggestion_box.jsx +++ b/web/react/components/suggestion/suggestion_box.jsx @@ -94,7 +94,7 @@ export default class SuggestionBox extends React.Component { } else if (e.which === KeyCodes.DOWN) { EventHelpers.emitSelectNextSuggestion(this.suggestionId); e.preventDefault(); - } else if (e.which === KeyCodes.ENTER || (e.which === KeyCodes.SPACE && SuggestionStore.shouldCompleteOnSpace(this.suggestionId))) { + } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB || (e.which === KeyCodes.SPACE && SuggestionStore.shouldCompleteOnSpace(this.suggestionId))) { EventHelpers.emitCompleteWordSuggestion(this.suggestionId); e.preventDefault(); } else if (this.props.onKeyDown) { diff --git a/web/react/components/team_members_modal.jsx b/web/react/components/team_members_modal.jsx index 0a30a2202..eed4a1f19 100644 --- a/web/react/components/team_members_modal.jsx +++ b/web/react/components/team_members_modal.jsx @@ -26,7 +26,7 @@ export default class TeamMembersModal extends React.Component { } onShow() { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50); if ($(window).width() > 768) { $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); } diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index ea104fedb..385cd0f52 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -65,11 +65,16 @@ export default class UserProfile extends React.Component { return <div>{name}</div>; } + var profileImg = '/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex(); + if (this.props.overwriteImage) { + profileImg = this.props.overwriteImage; + } + var dataContent = []; dataContent.push( <img className='user-popover__image' - src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex()} + src={profileImg} height='128' width='128' key='user-popover-image' @@ -130,10 +135,12 @@ export default class UserProfile extends React.Component { UserProfile.defaultProps = { userId: '', overwriteName: '', + overwriteImage: '', disablePopover: false }; UserProfile.propTypes = { userId: React.PropTypes.string, overwriteName: React.PropTypes.string, + overwriteImage: React.PropTypes.string, disablePopover: React.PropTypes.bool }; diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx index b4d34c658..c15936ccd 100644 --- a/web/react/components/user_settings/user_settings_advanced.jsx +++ b/web/react/components/user_settings/user_settings_advanced.jsx @@ -195,7 +195,7 @@ export default class AdvancedSettingsDisplay extends React.Component { inputs.push( <div key='advancedPreviewFeatures_helptext'> <br/> - {'Check any pre-released features you\'d like to preview.'} + {'Check any pre-released features you\'d like to preview. You may also need to refresh the page before the setting will take effect.'} </div> ); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index f9d03f56d..97c601b5e 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -47,7 +47,7 @@ export default class UserSettingsModal extends React.Component { } handleShow() { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50); if ($(window).width() > 768) { $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); } diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 2e0769cc4..29aa32a08 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -163,7 +163,7 @@ function handleNewPostEvent(msg) { } // Send desktop notification - if (UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') { + if ((UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) { const msgProps = msg.props; let mentions = []; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index b641e966b..170a16049 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -119,7 +119,9 @@ export default { POST_LOADING: 'loading', POST_FAILED: 'failed', POST_DELETED: 'deleted', - POST_TYPE_JOIN_LEAVE: 'join_leave', + POST_TYPE_JOIN_LEAVE: 'system_join_leave', + SYSTEM_MESSAGE_PREFIX: 'system_', + SYSTEM_MESSAGE_PROFILE_IMAGE: '/static/images/logo_compact.png', RESERVED_TEAM_NAMES: [ 'www', 'web', @@ -389,7 +391,8 @@ export default { BACKSPACE: 8, ENTER: 13, ESCAPE: 27, - SPACE: 32 + SPACE: 32, + TAB: 9 }, HighlightedLanguages: { diff: 'Diff', @@ -429,6 +432,10 @@ export default { MARKDOWN_PREVIEW: { label: 'markdown_preview', // github issue: https://github.com/mattermost/platform/pull/1389 description: 'Show markdown preview option in message input box' + }, + EMBED_PREVIEW: { + label: 'embed_preview', + description: 'Show preview snippet of links below message' } } }; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 0a52f5b37..f80da8415 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -252,13 +252,6 @@ export function getTimestamp() { // extracts links not styled by Markdown export function extractLinks(text) { - const urlMatcher = new Autolinker.matchParser.MatchParser({ - urls: true, - emails: false, - twitter: false, - phone: false, - hashtag: false - }); const links = []; let replaceText = text; @@ -271,7 +264,7 @@ export function extractLinks(text) { } } - function replaceFn(match) { + function replaceFn(autolinker, match) { let link = ''; const matchText = match.getMatchedText(); const tempText = replaceText; @@ -304,7 +297,16 @@ export function extractLinks(text) { links.push(link); } - urlMatcher.replace(text, replaceFn, this); + + Autolinker.link(text, { + replaceFn, + urls: true, + emails: false, + twitter: false, + phone: false, + hashtag: false + }); + return {links, text}; } @@ -681,7 +683,11 @@ export function applyTheme(theme) { } if (theme.mentionHighlightBg) { - changeCss('.mention-highlight, .search-highlight', 'background:' + theme.mentionHighlightBg, 1); + changeCss('.mention-highlight, .search-highlight, #archive-link-home', 'background:' + theme.mentionHighlightBg, 1); + } + + if (theme.mentionHighlightBg) { + changeCss('.post.post--highlight, #archive-link-home', 'background:' + changeOpacity(theme.mentionHighlightBg, 0.5), 1); } if (theme.mentionHighlightLink) { @@ -1243,3 +1249,7 @@ export function getPostTerm(post) { export function isFeatureEnabled(feature) { return PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label, {value: 'false'}).value === 'true'; } + +export function isSystemMessage(post) { + return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0); +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 7efe70cb4..0f8cd56f7 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -123,7 +123,7 @@ a:focus, a:hover { } select { - -webkit-appearance: none; + -moz-appearance:none; } .form-control { diff --git a/web/sass-files/sass/partials/_content.scss b/web/sass-files/sass/partials/_content.scss index 817817854..471ba63af 100644 --- a/web/sass-files/sass/partials/_content.scss +++ b/web/sass-files/sass/partials/_content.scss @@ -18,30 +18,29 @@ margin-left: 220px; position: relative; background: #fff; - @include display-flex; - @include flex-direction(column); + @include display-flex; + @include flex-direction(column); .channel__wrap & { padding-top: 0; } } #post-create { - @include flex(0 0 auto); + @include flex(0 0 auto); background: #fff; width: 100%; z-index: 3; } #archive-link-home { - @include flex(0 0 auto); - background: #fff; - width: 100%; - min-height: 50px; - z-index: 3; - background-color: beige; - text-align: center; - vertical-align: middle; - padding-top: 10px; + @include flex(0 0 auto); cursor: pointer; + padding: 10px; + font-size: 13px; + + a { + color: inherit; + } + } .post-list { diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index ed1632681..af603f692 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -371,6 +371,10 @@ body.ios { background-color: beige; } + &.post--system .post__header .col__name { + display: none; + } + ul { margin: 0; padding: 0; @@ -564,7 +568,6 @@ body.ios { display: inline-block; visibility: hidden; top: -1px; - float: right; .dropdown-menu { right: 0; @@ -599,6 +602,10 @@ body.ios { @include legacy-pie-clearfix; width: calc(100% - 70px); + img { + max-height: 400px; + } + ul { padding: 5px 0 0 20px; } @@ -625,6 +632,9 @@ body.ios { .post__time { font-size: 13px; + } + + .post__time, &.post--system .post__body { @include opacity(0.6); } @@ -740,6 +750,7 @@ body.ios { width: 80%; padding-right: 5px; overflow-x: auto; + overflow-y: hidden; &.attachment__body--no_thumb { width: 100%; } @@ -754,6 +765,7 @@ body.ios { margin: 5px 0; padding: 0; line-height: 16px; + height: 22px; font-size: 16px; a { font-size: 16px; @@ -794,4 +806,5 @@ body.ios { .permalink-popover { min-width: 320px; + margin-left: 50px !important; } diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 9b316d48e..2011a25f2 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -296,6 +296,9 @@ } } } + .section-min:hover { + background: none; + } .no-padding--left { padding-left: 15px; } diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index 0debb7e54..3af0f3f2c 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -1,9 +1,9 @@ #channel-header .search-bar__container { - padding: 0 8px 0 3px; + padding: 0 8px 0 3px; } .search-bar__container { padding: 12px 8px 0 0; - @include flex(0 0 56px); + @include flex(0 0 56px); } .search__clear { display: none; @@ -107,7 +107,9 @@ } .search-item__jump { - margin-left: 10px; + position: absolute; + right: 0; + top: 0; } .search-item-time { diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss index 735b2a99e..ada43fb99 100644 --- a/web/sass-files/sass/partials/_sidebar--right.scss +++ b/web/sass-files/sass/partials/_sidebar--right.scss @@ -12,6 +12,14 @@ right: 0; } + .post-body { + + img { + max-height: 200px; + } + + } + .sidebar--right__content { height: 100%; @include display-flex; diff --git a/web/sass-files/sass/partials/_suggestion_list.scss b/web/sass-files/sass/partials/_suggestion_list.scss index 0cf3fff5f..5e91a126d 100644 --- a/web/sass-files/sass/partials/_suggestion_list.scss +++ b/web/sass-files/sass/partials/_suggestion_list.scss @@ -47,17 +47,19 @@ } .emoticon-suggestion { + @include clearfix; width: 100%; - height: 36px; + height: 30px; cursor: pointer; font-size: 13px; - line-height: 36px; + line-height: 30px; } .emoticon-suggestion__image { - width: 32px; - height: 32px; - margin-right: 6px; - padding: 2px; + width: 20px; + height: 20px; + margin: 6px 6px 0 5px; + padding: 0; text-align: center; + vertical-align: top; } diff --git a/web/static/images/logo_compact.png b/web/static/images/logo_compact.png Binary files differnew file mode 100644 index 000000000..b861b7c6d --- /dev/null +++ b/web/static/images/logo_compact.png diff --git a/web/templates/channel.html b/web/templates/channel.html index 8abbe36df..a5c0354a3 100644 --- a/web/templates/channel.html +++ b/web/templates/channel.html @@ -30,7 +30,7 @@ window.setup_channel_page({{ .Props }}, {{ .Team }}, {{ .Channel }}, {{ .User }} if($(window).height() > 1200){ modals.css('max-height', 1000); } else { - modals.css('max-height', $(window).height() - 200); + modals.css('max-height', $(window).height() - 50); } modals.perfectScrollbar(); </script> |