summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/channel.go36
-rw-r--r--api/post.go3
-rw-r--r--doc/developer/tests/test-links.md2
-rw-r--r--doc/install/Command-Line-Tools.md81
-rw-r--r--mattermost.go27
-rw-r--r--model/post.go14
-rw-r--r--model/post_test.go15
-rw-r--r--store/sql_post_store.go3
-rw-r--r--web/react/components/access_history_modal.jsx2
-rw-r--r--web/react/components/activity_log_modal.jsx2
-rw-r--r--web/react/components/center_panel.jsx3
-rw-r--r--web/react/components/invite_member_modal.jsx2
-rw-r--r--web/react/components/more_direct_channels.jsx2
-rw-r--r--web/react/components/post.jsx11
-rw-r--r--web/react/components/post_attachment_oembed.jsx35
-rw-r--r--web/react/components/post_body.jsx21
-rw-r--r--web/react/components/post_body_additional_content.jsx6
-rw-r--r--web/react/components/post_header.jsx12
-rw-r--r--web/react/components/post_info.jsx81
-rw-r--r--web/react/components/posts_view.jsx8
-rw-r--r--web/react/components/providers.json158
-rw-r--r--web/react/components/rhs_root_post.jsx20
-rw-r--r--web/react/components/search_results_item.jsx2
-rw-r--r--web/react/components/suggestion/suggestion_box.jsx2
-rw-r--r--web/react/components/team_members_modal.jsx2
-rw-r--r--web/react/components/user_profile.jsx9
-rw-r--r--web/react/components/user_settings/user_settings_advanced.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx2
-rw-r--r--web/react/stores/socket_store.jsx2
-rw-r--r--web/react/utils/constants.jsx11
-rw-r--r--web/react/utils/utils.jsx30
-rw-r--r--web/sass-files/sass/partials/_base.scss2
-rw-r--r--web/sass-files/sass/partials/_content.scss23
-rw-r--r--web/sass-files/sass/partials/_post.scss15
-rw-r--r--web/sass-files/sass/partials/_responsive.scss3
-rw-r--r--web/sass-files/sass/partials/_search.scss8
-rw-r--r--web/sass-files/sass/partials/_sidebar--right.scss8
-rw-r--r--web/sass-files/sass/partials/_suggestion_list.scss14
-rw-r--r--web/static/images/logo_compact.pngbin0 -> 6258 bytes
-rw-r--r--web/templates/channel.html2
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
new file mode 100644
index 000000000..b861b7c6d
--- /dev/null
+++ b/web/static/images/logo_compact.png
Binary files differ
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>